From b2dbdc66e722870fab880e297636180385b791a7 Mon Sep 17 00:00:00 2001 From: Dan Torrey Date: Thu, 2 Jul 2026 14:09:11 -0500 Subject: [PATCH 1/5] Show effective query on event definition condition screen Adds a read-only effective-query preview under the Search Query on the Filter & Aggregation event definition condition screen, with a persisted show/hide toggle and copy-to-clipboard. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../changelog/unreleased/issue-14552.toml | 4 + .../EffectiveQueryField.test.tsx | 66 +++++++++++++ .../EffectiveQueryField.tsx | 94 +++++++++++++++++++ .../event-definition-types/FilterForm.tsx | 50 +++++++++- 4 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 graylog2-web-interface/changelog/unreleased/issue-14552.toml create mode 100644 graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx create mode 100644 graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.tsx diff --git a/graylog2-web-interface/changelog/unreleased/issue-14552.toml b/graylog2-web-interface/changelog/unreleased/issue-14552.toml new file mode 100644 index 000000000000..c27fefcab2ee --- /dev/null +++ b/graylog2-web-interface/changelog/unreleased/issue-14552.toml @@ -0,0 +1,4 @@ +type = "added" +message = "Show the effective query (base query combined with applied search filters) on the event definition condition screen." + +issues = ["14552"] diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx new file mode 100644 index 000000000000..25bb2e9cde41 --- /dev/null +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { render, screen } from 'wrappedTestingLibrary'; + +import asMock from 'helpers/mocking/AsMock'; +import fetch from 'logic/rest/FetchProvider'; +import { qualifyUrl } from 'util/URLUtils'; +import type { SearchFilter } from 'components/event-definitions/event-definitions-types'; + +import EffectiveQueryField from './EffectiveQueryField'; + +jest.mock('logic/rest/FetchProvider', () => jest.fn(() => Promise.resolve())); + +const URL = '/plugins/org.graylog.plugins.searchfilters/search_filters/effective_query'; + +describe('EffectiveQueryField', () => { + beforeEach(() => { + asMock(fetch).mockResolvedValue({ effective_query: '(action:login) AND (source:firewall)' }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const filters: SearchFilter[] = [{ type: 'inlineQueryString', queryString: 'source:firewall' }]; + + it('renders the effective query returned by the backend', async () => { + render(); + + await screen.findByText('(action:login) AND (source:firewall)'); + }); + + it('posts the query and filters to the effective-query endpoint', async () => { + render(); + + await screen.findByText('(action:login) AND (source:firewall)'); + + expect(fetch).toHaveBeenCalledWith('POST', qualifyUrl(URL), { + query_string: 'action:login', + filters: [{ type: 'inlineQueryString', queryString: 'source:firewall' }], + }); + }); + + it('shows a fallback message when the request fails', async () => { + asMock(fetch).mockRejectedValue(new Error('boom')); + + render(); + + await screen.findByText('(failed to render effective query)'); + }); +}); diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.tsx new file mode 100644 index 000000000000..f68d9f3b057d --- /dev/null +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.tsx @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import styled from 'styled-components'; +import omit from 'lodash/omit'; + +import fetch from 'logic/rest/FetchProvider'; +import { qualifyUrl } from 'util/URLUtils'; +import { ClipboardButton, Icon, Spinner } from 'components/common'; +import type { SearchFilter } from 'components/event-definitions/event-definitions-types'; + +const EFFECTIVE_QUERY_URL = '/plugins/org.graylog.plugins.searchfilters/search_filters/effective_query'; +// Debounce so typing in the base query doesn't fire a request per keystroke. +const REQUEST_DEBOUNCE_MS = 250; + +// Mirror the Search Query InputRow: a field that flex-grows plus a small control to its right, +// so this field lines up with the (validation-icon-narrowed) query input above. +const Row = styled.div` + display: flex; + align-items: flex-start; + gap: 5px; +`; + +// Read-only field styled to mirror the Search Query input above it. +const Field = styled.code( + ({ theme }) => ` + flex: 1; + padding: 6px 8px; + background-color: ${theme.colors.global.contentBackground}; + border: 1px solid ${theme.colors.input.border}; + border-radius: 4px; + color: ${theme.colors.gray[30]}; + white-space: pre-wrap; + word-break: break-word; +`, +); + +type Props = { + queryString: string; + filters: SearchFilter[]; +}; + +const EffectiveQueryField = ({ queryString, filters }: Props) => { + const [effectiveQuery, setEffectiveQuery] = useState(''); + const [loading, setLoading] = useState(true); + + // Serialize the request into a stable primitive so the effect only re-runs when it actually changes. + // The body is parsed back inside the effect to avoid referencing per-render values (refs/objects) + // from the effect, which the React compiler lint disallows. + const payloadKey = JSON.stringify({ + query_string: queryString ?? '', + filters: (filters ?? []).map((filter) => omit(filter, 'frontendId')), + }); + + useEffect(() => { + const timer = setTimeout(() => { + fetch('POST', qualifyUrl(EFFECTIVE_QUERY_URL), JSON.parse(payloadKey)) + .then((response) => setEffectiveQuery(response.effective_query)) + .catch(() => setEffectiveQuery('(failed to render effective query)')) + .finally(() => setLoading(false)); + }, REQUEST_DEBOUNCE_MS); + + return () => clearTimeout(timer); + }, [payloadKey]); + + return ( + + {loading && !effectiveQuery ? : effectiveQuery} + } + bsSize="xsmall" + text={effectiveQuery} + buttonTitle="Copy effective query" + /> + + ); +}; + +export default EffectiveQueryField; diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx index 185fcc6e0599..30d0edcbb576 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx @@ -39,7 +39,8 @@ import Query from 'views/logic/queries/Query'; import type { RelativeTimeRangeWithEnd } from 'views/logic/queries/Query'; import Search from 'views/logic/search/Search'; import { extractDurationAndUnit } from 'components/common/TimeUnitInput'; -import { Alert, ButtonToolbar, ControlLabel, FormGroup, HelpBlock, Input } from 'components/bootstrap'; +import { Alert, Button, ButtonToolbar, ControlLabel, FormGroup, HelpBlock, Input } from 'components/bootstrap'; +import EffectiveQueryField from 'components/event-definitions/event-definition-types/EffectiveQueryField'; import RelativeTime from 'components/common/RelativeTime'; import type { LookupTableParameterJson } from 'views/logic/parameters/LookupTableParameter'; import LookupTableParameter from 'views/logic/parameters/LookupTableParameter'; @@ -80,6 +81,20 @@ const InputRow = styled.div` align-items: center; `; +const EFFECTIVE_QUERY_STORAGE_KEY = 'event-definition-show-effective-query'; + +const InlineToggle = styled(Button)` + && { + padding: 0; + border: 0; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit; + vertical-align: baseline; + } +`; + const buildNewParameter = (name: string): LookupTableParameterJsonEmbryonic => ({ name: name, embryonic: true, @@ -179,6 +194,9 @@ const QueryParameters = ({ eventDefinition, onChange, userCanViewLookupTables, v const FilterForm = ({ currentUser, eventDefinition, onChange, streams, validation }: Props) => { const { execute_every_ms: executeEveryMs, search_within_ms: searchWithinMs } = eventDefinition.config; const [currentConfig, setCurrentConfig] = useState(eventDefinition.config); + const [showEffectiveQuery, setShowEffectiveQuery] = useState( + () => Store.get(EFFECTIVE_QUERY_STORAGE_KEY) === true, + ); const searchWithin = extractDurationAndUnit(searchWithinMs, TIME_UNITS); const executeEvery = extractDurationAndUnit(executeEveryMs, TIME_UNITS); const { userTimezone } = useUserDateTime(); @@ -549,10 +567,40 @@ const FilterForm = ({ currentUser, eventDefinition, onChange, streams, validatio Search query that Messages should match. You can use the same syntax as in the Search page, including declaring Query Parameters from Lookup Tables by using the $newParameter$ syntax. + {(currentConfig.filters?.length ?? 0) > 0 && ( + <> + {' '} + { + const next = !showEffectiveQuery; + setShowEffectiveQuery(next); + Store.set(EFFECTIVE_QUERY_STORAGE_KEY, next); + }}> + {showEffectiveQuery ? 'Hide effective query' : 'Show effective query'} + {' '} + including the search filters below. + + )} )} + {/* Scoped (Illuminate) definitions hide the non-editable base query, so the effective-query + preview is intentionally not shown for them; revealing the scoped query is handled separately + by the search-filter guardrails work. */} + {onlyFilters || + ((currentConfig.filters?.length ?? 0) > 0 && showEffectiveQuery && ( + + Effective query + + + The Search Query with the applied search filters included. This is what messages are matched against. + + + ))} + {onlyFilters || ( Date: Thu, 2 Jul 2026 14:37:48 -0500 Subject: [PATCH 2/5] Complete SearchFilter fixture in effective-query field test The event-definitions SearchFilter type requires id, title, disabled and negation; the test fixture was missing them (caught by the tsgo compiler in CI). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../EffectiveQueryField.test.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx index 25bb2e9cde41..d4ce987372aa 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx @@ -37,7 +37,16 @@ describe('EffectiveQueryField', () => { jest.clearAllMocks(); }); - const filters: SearchFilter[] = [{ type: 'inlineQueryString', queryString: 'source:firewall' }]; + const filters: SearchFilter[] = [ + { + id: 'filter-1', + type: 'inlineQueryString', + title: 'Firewall', + queryString: 'source:firewall', + disabled: false, + negation: false, + }, + ]; it('renders the effective query returned by the backend', async () => { render(); @@ -52,7 +61,16 @@ describe('EffectiveQueryField', () => { expect(fetch).toHaveBeenCalledWith('POST', qualifyUrl(URL), { query_string: 'action:login', - filters: [{ type: 'inlineQueryString', queryString: 'source:firewall' }], + filters: [ + { + id: 'filter-1', + type: 'inlineQueryString', + title: 'Firewall', + queryString: 'source:firewall', + disabled: false, + negation: false, + }, + ], }); }); From 3049f15ecf57b75f149bfc7d90c24ebab050a0e9 Mon Sep 17 00:00:00 2001 From: Dan Torrey Date: Thu, 2 Jul 2026 16:28:55 -0500 Subject: [PATCH 3/5] Make effective-query preview an enterprise plugin slot Search filters are enterprise-only, so the effective-query preview is now provided by the enterprise plugin. OSS only exposes a generic eventDefinitions.components.searchQueryPreview extension point beneath the Search Query on the event definition condition screen; the read-only field and toggle move to the enterprise plugin. Co-Authored-By: Claude Opus 4.8 (1M context) --- changelog/unreleased/issue-14552.toml | 4 + .../changelog/unreleased/issue-14552.toml | 4 - .../EffectiveQueryField.test.tsx | 84 ----------------- .../EffectiveQueryField.tsx | 94 ------------------- .../event-definition-types/FilterForm.tsx | 54 ++--------- .../src/components/event-definitions/types.ts | 6 +- 6 files changed, 17 insertions(+), 229 deletions(-) create mode 100644 changelog/unreleased/issue-14552.toml delete mode 100644 graylog2-web-interface/changelog/unreleased/issue-14552.toml delete mode 100644 graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx delete mode 100644 graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.tsx diff --git a/changelog/unreleased/issue-14552.toml b/changelog/unreleased/issue-14552.toml new file mode 100644 index 000000000000..e9f907644918 --- /dev/null +++ b/changelog/unreleased/issue-14552.toml @@ -0,0 +1,4 @@ +type = "added" +message = "Add an extension point to display a read-only query preview beneath the Search Query on the event definition condition screen." + +issues = ["14552"] diff --git a/graylog2-web-interface/changelog/unreleased/issue-14552.toml b/graylog2-web-interface/changelog/unreleased/issue-14552.toml deleted file mode 100644 index c27fefcab2ee..000000000000 --- a/graylog2-web-interface/changelog/unreleased/issue-14552.toml +++ /dev/null @@ -1,4 +0,0 @@ -type = "added" -message = "Show the effective query (base query combined with applied search filters) on the event definition condition screen." - -issues = ["14552"] diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx deleted file mode 100644 index d4ce987372aa..000000000000 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -import * as React from 'react'; -import { render, screen } from 'wrappedTestingLibrary'; - -import asMock from 'helpers/mocking/AsMock'; -import fetch from 'logic/rest/FetchProvider'; -import { qualifyUrl } from 'util/URLUtils'; -import type { SearchFilter } from 'components/event-definitions/event-definitions-types'; - -import EffectiveQueryField from './EffectiveQueryField'; - -jest.mock('logic/rest/FetchProvider', () => jest.fn(() => Promise.resolve())); - -const URL = '/plugins/org.graylog.plugins.searchfilters/search_filters/effective_query'; - -describe('EffectiveQueryField', () => { - beforeEach(() => { - asMock(fetch).mockResolvedValue({ effective_query: '(action:login) AND (source:firewall)' }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - const filters: SearchFilter[] = [ - { - id: 'filter-1', - type: 'inlineQueryString', - title: 'Firewall', - queryString: 'source:firewall', - disabled: false, - negation: false, - }, - ]; - - it('renders the effective query returned by the backend', async () => { - render(); - - await screen.findByText('(action:login) AND (source:firewall)'); - }); - - it('posts the query and filters to the effective-query endpoint', async () => { - render(); - - await screen.findByText('(action:login) AND (source:firewall)'); - - expect(fetch).toHaveBeenCalledWith('POST', qualifyUrl(URL), { - query_string: 'action:login', - filters: [ - { - id: 'filter-1', - type: 'inlineQueryString', - title: 'Firewall', - queryString: 'source:firewall', - disabled: false, - negation: false, - }, - ], - }); - }); - - it('shows a fallback message when the request fails', async () => { - asMock(fetch).mockRejectedValue(new Error('boom')); - - render(); - - await screen.findByText('(failed to render effective query)'); - }); -}); diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.tsx deleted file mode 100644 index f68d9f3b057d..000000000000 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-types/EffectiveQueryField.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -import * as React from 'react'; -import { useState, useEffect } from 'react'; -import styled from 'styled-components'; -import omit from 'lodash/omit'; - -import fetch from 'logic/rest/FetchProvider'; -import { qualifyUrl } from 'util/URLUtils'; -import { ClipboardButton, Icon, Spinner } from 'components/common'; -import type { SearchFilter } from 'components/event-definitions/event-definitions-types'; - -const EFFECTIVE_QUERY_URL = '/plugins/org.graylog.plugins.searchfilters/search_filters/effective_query'; -// Debounce so typing in the base query doesn't fire a request per keystroke. -const REQUEST_DEBOUNCE_MS = 250; - -// Mirror the Search Query InputRow: a field that flex-grows plus a small control to its right, -// so this field lines up with the (validation-icon-narrowed) query input above. -const Row = styled.div` - display: flex; - align-items: flex-start; - gap: 5px; -`; - -// Read-only field styled to mirror the Search Query input above it. -const Field = styled.code( - ({ theme }) => ` - flex: 1; - padding: 6px 8px; - background-color: ${theme.colors.global.contentBackground}; - border: 1px solid ${theme.colors.input.border}; - border-radius: 4px; - color: ${theme.colors.gray[30]}; - white-space: pre-wrap; - word-break: break-word; -`, -); - -type Props = { - queryString: string; - filters: SearchFilter[]; -}; - -const EffectiveQueryField = ({ queryString, filters }: Props) => { - const [effectiveQuery, setEffectiveQuery] = useState(''); - const [loading, setLoading] = useState(true); - - // Serialize the request into a stable primitive so the effect only re-runs when it actually changes. - // The body is parsed back inside the effect to avoid referencing per-render values (refs/objects) - // from the effect, which the React compiler lint disallows. - const payloadKey = JSON.stringify({ - query_string: queryString ?? '', - filters: (filters ?? []).map((filter) => omit(filter, 'frontendId')), - }); - - useEffect(() => { - const timer = setTimeout(() => { - fetch('POST', qualifyUrl(EFFECTIVE_QUERY_URL), JSON.parse(payloadKey)) - .then((response) => setEffectiveQuery(response.effective_query)) - .catch(() => setEffectiveQuery('(failed to render effective query)')) - .finally(() => setLoading(false)); - }, REQUEST_DEBOUNCE_MS); - - return () => clearTimeout(timer); - }, [payloadKey]); - - return ( - - {loading && !effectiveQuery ? : effectiveQuery} - } - bsSize="xsmall" - text={effectiveQuery} - buttonTitle="Copy effective query" - /> - - ); -}; - -export default EffectiveQueryField; diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx index 30d0edcbb576..7ef430170d41 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx @@ -39,8 +39,8 @@ import Query from 'views/logic/queries/Query'; import type { RelativeTimeRangeWithEnd } from 'views/logic/queries/Query'; import Search from 'views/logic/search/Search'; import { extractDurationAndUnit } from 'components/common/TimeUnitInput'; -import { Alert, Button, ButtonToolbar, ControlLabel, FormGroup, HelpBlock, Input } from 'components/bootstrap'; -import EffectiveQueryField from 'components/event-definitions/event-definition-types/EffectiveQueryField'; +import { Alert, ButtonToolbar, ControlLabel, FormGroup, HelpBlock, Input } from 'components/bootstrap'; +import usePluginEntities from 'hooks/usePluginEntities'; import RelativeTime from 'components/common/RelativeTime'; import type { LookupTableParameterJson } from 'views/logic/parameters/LookupTableParameter'; import LookupTableParameter from 'views/logic/parameters/LookupTableParameter'; @@ -81,20 +81,6 @@ const InputRow = styled.div` align-items: center; `; -const EFFECTIVE_QUERY_STORAGE_KEY = 'event-definition-show-effective-query'; - -const InlineToggle = styled(Button)` - && { - padding: 0; - border: 0; - font-size: inherit; - font-family: inherit; - font-weight: inherit; - line-height: inherit; - vertical-align: baseline; - } -`; - const buildNewParameter = (name: string): LookupTableParameterJsonEmbryonic => ({ name: name, embryonic: true, @@ -194,9 +180,7 @@ const QueryParameters = ({ eventDefinition, onChange, userCanViewLookupTables, v const FilterForm = ({ currentUser, eventDefinition, onChange, streams, validation }: Props) => { const { execute_every_ms: executeEveryMs, search_within_ms: searchWithinMs } = eventDefinition.config; const [currentConfig, setCurrentConfig] = useState(eventDefinition.config); - const [showEffectiveQuery, setShowEffectiveQuery] = useState( - () => Store.get(EFFECTIVE_QUERY_STORAGE_KEY) === true, - ); + const searchQueryPreviews = usePluginEntities('eventDefinitions.components.searchQueryPreview') ?? []; const searchWithin = extractDurationAndUnit(searchWithinMs, TIME_UNITS); const executeEvery = extractDurationAndUnit(executeEveryMs, TIME_UNITS); const { userTimezone } = useUserDateTime(); @@ -567,38 +551,16 @@ const FilterForm = ({ currentUser, eventDefinition, onChange, streams, validatio Search query that Messages should match. You can use the same syntax as in the Search page, including declaring Query Parameters from Lookup Tables by using the $newParameter$ syntax. - {(currentConfig.filters?.length ?? 0) > 0 && ( - <> - {' '} - { - const next = !showEffectiveQuery; - setShowEffectiveQuery(next); - Store.set(EFFECTIVE_QUERY_STORAGE_KEY, next); - }}> - {showEffectiveQuery ? 'Hide effective query' : 'Show effective query'} - {' '} - including the search filters below. - - )} )} - {/* Scoped (Illuminate) definitions hide the non-editable base query, so the effective-query - preview is intentionally not shown for them; revealing the scoped query is handled separately - by the search-filter guardrails work. */} + {/* Extension point (filled by the enterprise search-filters plugin) for a read-only preview of the + effective query beneath the Search Query. Scoped (Illuminate) definitions hide the base query, so + the preview is not rendered for them. */} {onlyFilters || - ((currentConfig.filters?.length ?? 0) > 0 && showEffectiveQuery && ( - - Effective query - - - The Search Query with the applied search filters included. This is what messages are matched against. - - + searchQueryPreviews.map(({ component: SearchQueryPreview, key }) => ( + ))} {onlyFilters || ( diff --git a/graylog2-web-interface/src/components/event-definitions/types.ts b/graylog2-web-interface/src/components/event-definitions/types.ts index adcf4eea9f0d..ce121e133ca3 100644 --- a/graylog2-web-interface/src/components/event-definitions/types.ts +++ b/graylog2-web-interface/src/components/event-definitions/types.ts @@ -19,7 +19,7 @@ import type React from 'react'; import type { SearchBarControl } from 'views/types'; import type User from 'logic/users/User'; -import type { EventDefinition } from 'components/event-definitions/event-definitions-types'; +import type { EventDefinition, SearchFilter } from 'components/event-definitions/event-definitions-types'; import type { Attribute } from 'stores/PaginationTypes'; export type AlertType = 'alert' | 'event' | 'event_definition'; @@ -105,5 +105,9 @@ declare module 'graylog-web-plugin/plugin' { 'eventDefinitions.components.sigmaGitImport'?: Array<{ component: React.FC; key: string }>; 'eventDefinitions.components.sigmaFileUpload'?: Array<{ component: React.FC; key: string }>; 'eventDefinitions.components.sigmaOptions'?: Array<{ component: React.FC; key: string }>; + 'eventDefinitions.components.searchQueryPreview'?: Array<{ + component: React.ComponentType<{ queryString: string; filters: SearchFilter[] }>; + key: string; + }>; } } From 061da09e3ace7c10edd8c0c3b0774b97ec75ddd8 Mon Sep 17 00:00:00 2001 From: Ezequiel Lopez Date: Tue, 7 Jul 2026 14:52:32 -0400 Subject: [PATCH 4/5] feat(14552): Show effective query on search pages --- .../common/SearchFiltersFormControls.tsx | 26 +++++++++++++++---- .../event-definition-types/FilterForm.tsx | 1 + 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/graylog2-web-interface/src/components/common/SearchFiltersFormControls.tsx b/graylog2-web-interface/src/components/common/SearchFiltersFormControls.tsx index 8459013ae42e..b24a3659043a 100644 --- a/graylog2-web-interface/src/components/common/SearchFiltersFormControls.tsx +++ b/graylog2-web-interface/src/components/common/SearchFiltersFormControls.tsx @@ -15,7 +15,7 @@ * . */ import React, { useEffect, useMemo } from 'react'; -import { Formik } from 'formik'; +import { Formik, useFormikContext } from 'formik'; import { OrderedMap } from 'immutable'; import { v4 as uuidv4 } from 'uuid'; @@ -27,9 +27,22 @@ type Props = { filters: SearchFilter[]; onChange: (filters: OrderedMap) => void; hideFiltersPreview?: (val: boolean) => void; + queryString?: string; }; -function SearchFiltersFormControls({ filters, onChange, hideFiltersPreview = () => {} }: Props) { +// Keeps the isolated Formik's `queryString` field in sync with the manually-entered query +// living outside this form, without resetting `searchFilters` on every keystroke. +const SyncQueryString = ({ queryString }: { queryString: string }) => { + const { setFieldValue } = useFormikContext(); + + useEffect(() => { + setFieldValue('queryString', queryString); + }, [queryString, setFieldValue]); + + return null; +}; + +function SearchFiltersFormControls({ filters, onChange, hideFiltersPreview = () => {}, queryString = '' }: Props) { const searchFiltersPlugin = usePluginEntities('eventDefinitions.components.searchForm') ?? []; const pluggableControls = searchFiltersPlugin.map((controlFn) => controlFn()).filter((control) => !!control); @@ -41,8 +54,8 @@ function SearchFiltersFormControls({ filters, onChange, hideFiltersPreview = () filters.map((filter) => [filter.id || uuidv4(), { frontendId: filter.id || uuidv4(), ...filter }]), ); - return { searchFilters }; - }, [filters]); + return { searchFilters, queryString }; + }, [filters, queryString]); if (!pluggableControls.length) return hideFiltersPreview(true)} pluggableControls={pluggableControls} />; @@ -58,7 +71,10 @@ function SearchFiltersFormControls({ filters, onChange, hideFiltersPreview = () return ( - + <> + + + ); } diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx index 185fcc6e0599..483018c8e8ce 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterForm.tsx @@ -572,6 +572,7 @@ const FilterForm = ({ currentUser, eventDefinition, onChange, streams, validatio filters={eventDefinition.config.filters} onChange={handleSearchFiltersChange} hideFiltersPreview={hideFiltersPreview} + queryString={currentConfig.query} /> From ffa78b054e20a98fbec35f83b12bd9a46210c532 Mon Sep 17 00:00:00 2001 From: Dan Torrey Date: Tue, 7 Jul 2026 15:16:19 -0500 Subject: [PATCH 5/5] Add effective query to search explain results (#14552) Co-Authored-By: Claude Opus 4.8 (1M context) --- changelog/unreleased/issue-14552.toml | 4 --- .../plugins/views/search/ExplainResults.java | 11 ++++++- .../views/search/engine/QueryEngine.java | 30 +++++++++++++++-- .../search/rest/EffectiveQueryRequest.java | 32 ++++++++++++++++++ .../views/search/rest/SearchResource.java | 33 ++++++++++++++++++- .../DefaultEffectiveQueryComposer.java | 29 ++++++++++++++++ .../searchfilters/EffectiveQueryComposer.java | 26 +++++++++++++++ .../module/SearchFiltersModule.java | 4 +++ 8 files changed, 161 insertions(+), 8 deletions(-) delete mode 100644 changelog/unreleased/issue-14552.toml create mode 100644 graylog2-server/src/main/java/org/graylog/plugins/views/search/rest/EffectiveQueryRequest.java create mode 100644 graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/DefaultEffectiveQueryComposer.java create mode 100644 graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/EffectiveQueryComposer.java diff --git a/changelog/unreleased/issue-14552.toml b/changelog/unreleased/issue-14552.toml deleted file mode 100644 index e9f907644918..000000000000 --- a/changelog/unreleased/issue-14552.toml +++ /dev/null @@ -1,4 +0,0 @@ -type = "added" -message = "Add an extension point to display a read-only query preview beneath the Search Query on the event definition condition screen." - -issues = ["14552"] diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/ExplainResults.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/ExplainResults.java index 09ceb17c23b7..824e2e2226f3 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/ExplainResults.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/ExplainResults.java @@ -16,6 +16,7 @@ */ package org.graylog.plugins.views.search; +import com.fasterxml.jackson.annotation.JsonInclude; import org.graylog.plugins.views.search.errors.SearchError; import org.graylog2.indexer.indexset.MongoIndexSet; import org.graylog2.indexer.ranges.IndexRange; @@ -32,7 +33,15 @@ public record SearchResult(Map queries) { public record QueryExplainResult(Map searchTypes) { } - public record ExplainResult(String queryString, Set searchedIndexRanges) { + public record ExplainResult(String queryString, Set searchedIndexRanges, + @JsonInclude(JsonInclude.Include.NON_NULL) String effectiveQuery) { + public ExplainResult(String queryString, Set searchedIndexRanges) { + this(queryString, searchedIndexRanges, null); + } + + public ExplainResult withEffectiveQuery(final String effectiveQuery) { + return new ExplainResult(queryString, searchedIndexRanges, effectiveQuery); + } } public record IndexRangeResult(String indexName, long begin, long end, boolean isWarmTiered, diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/QueryEngine.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/QueryEngine.java index e31d8aa3c765..5c25b0395c43 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/QueryEngine.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/QueryEngine.java @@ -27,6 +27,9 @@ import org.graylog.plugins.views.search.QueryResult; import org.graylog.plugins.views.search.Search; import org.graylog.plugins.views.search.SearchJob; +import org.graylog.plugins.views.search.SearchType; +import org.graylog.plugins.views.search.searchfilters.EffectiveQueryComposer; +import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; import org.graylog.plugins.views.search.errors.QueryError; import org.graylog.plugins.views.search.errors.SearchError; @@ -38,6 +41,7 @@ import org.slf4j.LoggerFactory; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -62,17 +66,20 @@ public class QueryEngine { private final Executor dataLakeJobsQueryPool; private final ElasticsearchBackendProvider elasticsearchBackendProvider; private final Map> unversionedBackends; + private final EffectiveQueryComposer effectiveQueryComposer; @Inject public QueryEngine(Configuration configuration, ElasticsearchBackendProvider elasticsearchBackendProvider, Map> unversionedBackends, Set queryMetadataDecorators, - QueryParser queryParser) { + QueryParser queryParser, + EffectiveQueryComposer effectiveQueryComposer) { this.elasticsearchBackendProvider = elasticsearchBackendProvider; this.unversionedBackends = unversionedBackends; this.queryMetadataDecorators = queryMetadataDecorators; this.queryParser = queryParser; + this.effectiveQueryComposer = effectiveQueryComposer; this.indexerJobsQueryPool = createThreadPool( configuration.searchQueryEngineIndexerJobsPoolSize(), @@ -111,12 +118,31 @@ public ExplainResults explain(SearchJob searchJob, Set validationEr var backend = getBackendForQuery(q); final GeneratedQueryContext generatedQueryContext = backend.generate(q, Set.of(), timezone); - return backend.explain(searchJob, q, generatedQueryContext); + return withEffectiveQueries(q, backend.explain(searchJob, q, generatedQueryContext)); })); return new ExplainResults(searchJob.getSearchId(), new ExplainResults.SearchResult(queries), validationErrors); } + private ExplainResults.QueryExplainResult withEffectiveQueries(final Query query, final ExplainResults.QueryExplainResult queryExplainResult) { + final Map searchTypesById = query.searchTypes().stream() + .collect(Collectors.toMap(SearchType::id, s -> s, (a, b) -> a)); + final String baseQuery = query.query().queryString(); + + final Map enriched = queryExplainResult.searchTypes().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> { + final List filters = new java.util.ArrayList<>( + query.filters() != null ? query.filters() : List.of()); + final SearchType searchType = searchTypesById.get(entry.getKey()); + if (searchType != null && searchType.filters() != null) { + filters.addAll(searchType.filters()); + } + return entry.getValue().withEffectiveQuery(effectiveQueryComposer.compose(baseQuery, filters)); + })); + + return new ExplainResults.QueryExplainResult(enriched); + } + @WithSpan public SearchJob execute(SearchJob searchJob, Set validationErrors, DateTimeZone timezone) { final Set validQueries = searchJob.getSearch().queries() diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/rest/EffectiveQueryRequest.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/rest/EffectiveQueryRequest.java new file mode 100644 index 000000000000..b913963676f1 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/rest/EffectiveQueryRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.plugins.views.search.rest; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; + +import java.util.List; + +public record EffectiveQueryRequest(@JsonProperty("query_string") String queryString, + @JsonProperty("filters") List filters) { + @JsonCreator + public EffectiveQueryRequest { + queryString = queryString == null ? "" : queryString; + filters = filters == null ? List.of() : filters; + } +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/rest/SearchResource.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/rest/SearchResource.java index d0f39a586df2..f0b9738cac51 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/rest/SearchResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/rest/SearchResource.java @@ -16,6 +16,7 @@ */ package org.graylog.plugins.views.search.rest; +import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -41,20 +42,23 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.shiro.authz.annotation.RequiresAuthentication; -import org.graylog.events.processor.systemnotification.TemplateRenderRequest; import org.graylog.plugins.views.audit.ViewsAuditEventTypes; import org.graylog.plugins.views.search.ExplainResults; +import org.graylog.plugins.views.search.Query; import org.graylog.plugins.views.search.Search; import org.graylog.plugins.views.search.SearchDomain; import org.graylog.plugins.views.search.SearchJob; import org.graylog.plugins.views.search.db.SearchJobService; +import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; import org.graylog.plugins.views.search.engine.SearchExecutor; import org.graylog.plugins.views.search.events.SearchJobExecutionEvent; import org.graylog.plugins.views.search.permissions.SearchUser; +import org.graylog.plugins.views.search.searchtypes.MessageList; import org.graylog2.audit.jersey.AuditEvent; import org.graylog2.audit.jersey.NoAuditEvent; import org.graylog2.indexer.searches.SearchesClusterConfig; import org.graylog2.plugin.cluster.ClusterConfigService; +import org.graylog2.plugin.indexer.searches.timeranges.RelativeRange; import org.graylog2.plugin.rest.PluginRestResource; import org.graylog2.shared.rest.PublicCloudAPI; import org.graylog2.shared.rest.resources.RestResource; @@ -65,6 +69,7 @@ import java.net.URI; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @PublicCloudAPI @@ -203,6 +208,32 @@ public ExplainResults explainQuery(@Parameter(name = "id") @PathParam("id") Stri return searchExecutor.explain(id, searchUser, executionState); } + @POST + @Operation(summary = "Explain an ad-hoc query plus search filters", + description = "Synthesizes a search from the given query and search filters and explains it, so callers " + + "without a saved search (e.g. the event definition editor) can read the effective query from " + + "the returned explain results.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Explain results (read effective_query per search type)", + content = @Content(schema = @Schema(implementation = ExplainResults.class))) + }) + @Path("effective_query") + @NoAuditEvent("Does not return any actual data") + public ExplainResults explainEffectiveQuery(@RequestBody(content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = EffectiveQueryRequest.class))) + EffectiveQueryRequest request, + @Context SearchUser searchUser) { + final Query query = Query.builder() + .id("effective-query") + .searchTypes(Set.of(MessageList.builder().id("effective-query-message-list").build())) + .query(ElasticsearchQueryString.of(request.queryString())) + .filters(request.filters()) + .timerange(RelativeRange.allTime()) + .build(); + final Search search = Search.builder().queries(ImmutableSet.of(query)).build(); + + return searchExecutor.explain(search, searchUser, ExecutionState.empty()); + } + @POST @Operation(summary = "Execute a new synchronous search", description = "Executes a new search and waits for its result") @ApiResponses({ diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/DefaultEffectiveQueryComposer.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/DefaultEffectiveQueryComposer.java new file mode 100644 index 000000000000..d2c6a3e1230c --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/DefaultEffectiveQueryComposer.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.plugins.views.search.searchfilters; + +import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; + +import java.util.List; + +public class DefaultEffectiveQueryComposer implements EffectiveQueryComposer { + + @Override + public String compose(final String baseQuery, final List filters) { + return baseQuery == null ? "" : baseQuery; + } +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/EffectiveQueryComposer.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/EffectiveQueryComposer.java new file mode 100644 index 000000000000..658acc383f56 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/EffectiveQueryComposer.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.plugins.views.search.searchfilters; + +import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; + +import java.util.List; + +public interface EffectiveQueryComposer { + + String compose(String baseQuery, List filters); +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/module/SearchFiltersModule.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/module/SearchFiltersModule.java index b071c1c3efe6..98f128c14520 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/module/SearchFiltersModule.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/module/SearchFiltersModule.java @@ -17,6 +17,8 @@ package org.graylog.plugins.views.search.searchfilters.module; import com.google.inject.multibindings.OptionalBinder; +import org.graylog.plugins.views.search.searchfilters.DefaultEffectiveQueryComposer; +import org.graylog.plugins.views.search.searchfilters.EffectiveQueryComposer; import org.graylog.plugins.views.search.searchfilters.db.IgnoreSearchFilters; import org.graylog.plugins.views.search.searchfilters.db.SearchFiltersReFetcher; import org.graylog.plugins.views.search.searchfilters.db.UsedSearchFiltersToQueryStringsMapper; @@ -33,5 +35,7 @@ protected void configure() { OptionalBinder.newOptionalBinder(binder(), SearchFiltersReFetcher.class) .setDefault().to(IgnoreSearchFilters.class); + OptionalBinder.newOptionalBinder(binder(), EffectiveQueryComposer.class) + .setDefault().to(DefaultEffectiveQueryComposer.class); } }