From 2472cdb438ee0c735f006a0074424bf950aec4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Gonz=C3=A1lez?= Date: Fri, 23 May 2025 09:48:16 +0200 Subject: [PATCH 1/9] Adding hook button --- .../query_ruleset_detail.tsx | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx index 02a203f99cde7..50045c36f45cc 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; -import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useParams } from 'react-router-dom'; import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types'; @@ -20,6 +20,7 @@ import { ErrorPrompt } from '../error_prompt/error_prompt'; import { isNotFoundError, isPermissionError } from '../../utils/query_rules_utils'; import { QueryRulesPageTemplate } from '../../layout/query_rules_page_template'; import { QueryRuleDetailPanel } from './query_rule_detail_panel'; +import { UseRunQueryRuleset } from '../../hooks/use_run_query_ruleset'; export const QueryRulesetDetail: React.FC = () => { const { @@ -72,19 +73,13 @@ export const QueryRulesetDetail: React.FC = () => { rightSideItems={[ - { - // Logic to handle data button click - }} - > - - + Date: Fri, 23 May 2025 10:01:02 +0200 Subject: [PATCH 2/9] Simplified query example --- .../public/hooks/use_run_query_ruleset.tsx | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx index 30a9c7336e5b8..66f2e2d35b3d2 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx @@ -24,27 +24,25 @@ export const UseRunQueryRuleset = ({ const { application, share, console: consolePlugin } = useKibana().services; const { data: queryRulesetData } = useFetchQueryRuleset(rulesetId); const indecesRuleset = queryRulesetData?.rules?.[0]?.actions?.docs?.[0]?._index || 'my_index'; - + // Example based on https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-rule-query#_example_request_2 const TEST_QUERY_RULESET_API_SNIPPET = dedent` # Test your query ruleset GET ${indecesRuleset}/_search { - "retriever": { + "query": { "rule": { - "retriever": { - "standard": { - "query": { - "query_string": { - "query": "puggles" - } - } - } + // Defines the match criteria to apply to rules in the given query ruleset + "match_criteria": { + "user_query": "pugs" }, - "match_criteria": { - "query_string": "puggles", - "user_country": "us" - }, - "ruleset_ids": [ "${rulesetId}" ] + // An array of one or more unique query ruleset ID with query-based rules to match and apply as applicable + "ruleset_ids": ["${rulesetId}"], + // Any choice of query used to return results, that may be modified by matching query rules + "organic": { + "match": { + "description": "puggles" + } + } } } } From c8e44bdfa2f385671bf0e3dc87998486b85fe493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Gonz=C3=A1lez?= Date: Fri, 23 May 2025 10:55:03 +0200 Subject: [PATCH 3/9] Delete and redirect --- .../query_rules_sets/delete_ruleset_modal.tsx | 12 +- .../query_ruleset_detail.tsx | 104 +++++++++++++++--- 2 files changed, 98 insertions(+), 18 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/delete_ruleset_modal.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/delete_ruleset_modal.tsx index 955a050f26abb..2dd07db3cbabf 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/delete_ruleset_modal.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/delete_ruleset_modal.tsx @@ -20,15 +20,25 @@ import { useDeleteRuleset } from '../../hooks/use_delete_query_rules_ruleset'; export interface DeleteRulesetModalProps { rulesetId: string; closeDeleteModal: () => void; + onSuccess?: () => void; } -export const DeleteRulesetModal = ({ closeDeleteModal, rulesetId }: DeleteRulesetModalProps) => { +export const DeleteRulesetModal = ({ + closeDeleteModal, + rulesetId, + onSuccess: onSuccessProp, +}: DeleteRulesetModalProps) => { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const onSuccess = () => { setIsLoading(false); closeDeleteModal(); + if (onSuccessProp) { + onSuccessProp(); + } }; + const confirmCheckboxId = useGeneratedHtmlId({ prefix: 'confirmCheckboxId', }); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx index 50045c36f45cc..748fff057d7d0 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx @@ -8,7 +8,17 @@ import React, { useEffect, useState } from 'react'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { + EuiButton, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + useGeneratedHtmlId, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useParams } from 'react-router-dom'; import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types'; @@ -21,6 +31,7 @@ import { isNotFoundError, isPermissionError } from '../../utils/query_rules_util import { QueryRulesPageTemplate } from '../../layout/query_rules_page_template'; import { QueryRuleDetailPanel } from './query_rule_detail_panel'; import { UseRunQueryRuleset } from '../../hooks/use_run_query_ruleset'; +import { DeleteRulesetModal } from '../query_rules_sets/delete_ruleset_modal'; export const QueryRulesetDetail: React.FC = () => { const { @@ -39,6 +50,31 @@ export const QueryRulesetDetail: React.FC = () => { const [rules, setRules] = useState(queryRulesetData?.rules ?? []); + const [isPopoverOpen, setPopover] = useState(false); + const splitButtonPopoverId = useGeneratedHtmlId({ + prefix: 'splitButtonPopover', + }); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + const items = [ + setRulesetToDelete(rulesetId)} + > + Delete ruleset + , + ]; + + const [rulesetToDelete, setRulesetToDelete] = useState(null); + useEffect(() => { if (queryRulesetData?.rules) { setRules(queryRulesetData.rules); @@ -81,26 +117,60 @@ export const QueryRulesetDetail: React.FC = () => { })} /> - - { - // Logic to save the query ruleset - }} - > - - - + + + { + // Logic to save the query ruleset + }} + > + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + , ]} /> )} + {rulesetToDelete && ( + { + setRulesetToDelete(null); + }} + onSuccess={() => { + application.navigateToUrl(http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}`)); + }} + /> + )} {!isError && } {isError && ( Date: Fri, 23 May 2025 12:06:15 +0200 Subject: [PATCH 4/9] More dynamic search example --- .../public/hooks/use_run_query_ruleset.tsx | 93 +++++++++++++++---- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx index 66f2e2d35b3d2..861a542ae3291 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx @@ -23,30 +23,87 @@ export const UseRunQueryRuleset = ({ }: UseRunQueryRulesetProps) => { const { application, share, console: consolePlugin } = useKibana().services; const { data: queryRulesetData } = useFetchQueryRuleset(rulesetId); - const indecesRuleset = queryRulesetData?.rules?.[0]?.actions?.docs?.[0]?._index || 'my_index'; + + // Loop through all actions children to gather unique _index values + const indices: Set = new Set(); + if (queryRulesetData?.rules) { + for (const rule of queryRulesetData.rules) { + if (rule.actions?.docs) { + for (const doc of rule.actions.docs) { + if (doc._index) { + indices.add(doc._index); + } + } + } + } + } + + // Use the found indices or default to 'my_index' + const indecesRuleset = indices.size > 0 ? Array.from(indices).join(',') : 'my_index'; + + // Extract match criteria metadata and values from the ruleset + const criteriaData = []; + if (queryRulesetData?.rules) { + for (const rule of queryRulesetData.rules) { + if (rule.criteria) { + // Handle both single criterion and array of criteria + const criteriaArray = Array.isArray(rule.criteria) ? rule.criteria : [rule.criteria]; + for (const criterion of criteriaArray) { + if ( + criterion.values && + typeof criterion.values === 'object' && + !Array.isArray(criterion.values) + ) { + // Handle nested values inside criterion.values + Object.entries(criterion.values).forEach(([key, value]) => { + criteriaData.push({ + metadata: key, + values: value, + }); + }); + } else { + criteriaData.push({ + metadata: criterion.metadata || null, + values: criterion.values || null, + }); + } + } + } + } + } + // Example based on https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-rule-query#_example_request_2 const TEST_QUERY_RULESET_API_SNIPPET = dedent` -# Test your query ruleset -GET ${indecesRuleset}/_search -{ - "query": { - "rule": { - // Defines the match criteria to apply to rules in the given query ruleset - "match_criteria": { - "user_query": "pugs" - }, - // An array of one or more unique query ruleset ID with query-based rules to match and apply as applicable - "ruleset_ids": ["${rulesetId}"], - // Any choice of query used to return results, that may be modified by matching query rules - "organic": { - "match": { - "description": "puggles" + # Test your query ruleset + # ℹ️ https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-rule-query + GET ${indecesRuleset}/_search + { + "query": { + "rule": { + "match_criteria": // Defines the match criteria to apply to rules in the given query ruleset ${ + criteriaData.length > 0 + ? (() => { + const matchCriteria = criteriaData.reduce>((acc, criterion) => { + if (criterion.metadata && criterion.values) { + acc[criterion.metadata] = criterion.values; + } + return acc; + }, {}); + // Format with proper indentation (6 spaces to align with the property) + return JSON.stringify(matchCriteria, null, 2).split('\n').join('\n '); + })() + : '{\n "user_query": "pugs"\n }' + }, + "ruleset_ids": ["${rulesetId}"], // An array of one or more unique query ruleset IDs + "organic": { + "match": { // Any choice of query used to return results + "description": "puggles" + } } } } } -} -`; + `; return ( Date: Fri, 23 May 2025 12:16:52 +0200 Subject: [PATCH 5/9] Adding comments --- .../search_query_rules/public/hooks/use_run_query_ruleset.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx index 861a542ae3291..93d14b2cf17db 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx @@ -71,7 +71,6 @@ export const UseRunQueryRuleset = ({ } } } - // Example based on https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-rule-query#_example_request_2 const TEST_QUERY_RULESET_API_SNIPPET = dedent` # Test your query ruleset @@ -80,7 +79,8 @@ export const UseRunQueryRuleset = ({ { "query": { "rule": { - "match_criteria": // Defines the match criteria to apply to rules in the given query ruleset ${ + // Defines the match criteria to apply to rules in the given query ruleset + "match_criteria": ${ criteriaData.length > 0 ? (() => { const matchCriteria = criteriaData.reduce>((acc, criterion) => { From eb6b53b28898cc04627841aeab1569b7c2c0760b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Gonz=C3=A1lez?= Date: Mon, 26 May 2025 11:45:30 +0200 Subject: [PATCH 6/9] Using retriever example --- .../query_ruleset_detail.test.tsx | 2 - .../hooks/use_run_query_ruleset.test.tsx | 98 ++++++++++++++++++- .../public/hooks/use_run_query_ruleset.tsx | 56 ++++++----- 3 files changed, 124 insertions(+), 32 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.test.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.test.tsx index 3ac351b91f1b0..0e537500c9309 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.test.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.test.tsx @@ -30,7 +30,6 @@ describe('Query rule detail', () => { const TEST_IDS = { DetailPage: 'queryRulesetDetailPage', DetailPageHeader: 'queryRulesetDetailHeader', - HeaderDataButton: 'queryRulesetDetailHeaderDataButton', HeaderSaveButton: 'queryRulesetDetailHeaderSaveButton', AddRuleButton: 'queryRulesetDetailAddRuleButton', DraggableItem: 'searchQueryRulesDraggableItem', @@ -49,7 +48,6 @@ describe('Query rule detail', () => { const header = screen.getByTestId(TEST_IDS.DetailPageHeader); expect(within(header).getByText('my-ruleset')).toBeInTheDocument(); - expect(within(header).getByTestId(TEST_IDS.HeaderDataButton)).toBeInTheDocument(); expect(within(header).getByTestId(TEST_IDS.HeaderSaveButton)).toBeInTheDocument(); expect(screen.getByTestId(TEST_IDS.AddRuleButton)).toBeInTheDocument(); expect(screen.getAllByTestId(TEST_IDS.DraggableItem)).toHaveLength( diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx index a6de8ea0fabe7..6f3d8079440e6 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx @@ -41,6 +41,8 @@ describe('UseRunQueryRuleset', () => { console: mockConsole, }, }); + + // Default mock for ruleset data (useFetchQueryRuleset as jest.Mock).mockReturnValue({ data: { rules: [ @@ -53,6 +55,13 @@ describe('UseRunQueryRuleset', () => { }, ], }, + criteria: [ + { + metadata: 'user_query', + values: ['test-query'], + type: 'exact', + }, + ], }, ], }, @@ -76,9 +85,13 @@ describe('UseRunQueryRuleset', () => { expect.anything() ); - // Verify that the request contains the index from the fetched data const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; + // Verify that the request contains the index from the fetched data expect(buttonProps.request).toContain('test-index'); + // Verify the request contains retriever structure + expect(buttonProps.request).toContain('retriever'); + // Verify ruleset_ids are included + expect(buttonProps.request).toContain('"ruleset_ids": ['); }); it('renders with custom type and content', () => { @@ -106,12 +119,87 @@ describe('UseRunQueryRuleset', () => { const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; expect(buttonProps.request).toContain('my_index'); }); + + it('handles multiple indices from ruleset data', () => { + (useFetchQueryRuleset as jest.Mock).mockReturnValue({ + data: { + rules: [ + { + actions: { + docs: [ + { _index: 'index1', _id: 'id1' }, + { _index: 'index2', _id: 'id2' }, + ], + }, + }, + ], + }, + isInitialLoading: false, + isError: false, + }); + + render(); + + const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; + expect(buttonProps.request).toContain('index1,index2'); + }); + + it('creates match criteria from ruleset data', () => { + (useFetchQueryRuleset as jest.Mock).mockReturnValue({ + data: { + rules: [ + { + criteria: [ + { + metadata: 'user_query', + values: 'search term', + type: 'exact', + }, + { + metadata: 'user_location', + values: ['US', 'UK'], + type: 'exact', + }, + ], + }, + ], + }, + isInitialLoading: false, + isError: false, + }); + + render(); + + const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; + expect(buttonProps.request).toContain('"user_query": "search term"'); + expect(buttonProps.request).toContain('"user_location": ['); + }); - it('creates correct query with ruleset ID in the request', () => { - const rulesetId = 'special-test-ruleset'; - render(); + it('handles complex nested criteria values', () => { + (useFetchQueryRuleset as jest.Mock).mockReturnValue({ + data: { + rules: [ + { + criteria: [ + { + values: { + nested_field: 'nested value', + another_field: ['array', 'of', 'values'], + }, + type: 'exact', + }, + ], + }, + ], + }, + isInitialLoading: false, + isError: false, + }); + + render(); const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; - expect(buttonProps.request).toContain(`"ruleset_ids": [ "${rulesetId}" ]`); + expect(buttonProps.request).toContain('"nested_field": "nested value"'); + expect(buttonProps.request).toContain('"another_field": ['); }); }); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx index 93d14b2cf17db..2ccc92411fe40 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx @@ -73,36 +73,42 @@ export const UseRunQueryRuleset = ({ } // Example based on https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-rule-query#_example_request_2 const TEST_QUERY_RULESET_API_SNIPPET = dedent` - # Test your query ruleset - # ℹ️ https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-rule-query - GET ${indecesRuleset}/_search - { - "query": { - "rule": { - // Defines the match criteria to apply to rules in the given query ruleset - "match_criteria": ${ - criteriaData.length > 0 - ? (() => { - const matchCriteria = criteriaData.reduce>((acc, criterion) => { - if (criterion.metadata && criterion.values) { - acc[criterion.metadata] = criterion.values; - } - return acc; - }, {}); - // Format with proper indentation (6 spaces to align with the property) - return JSON.stringify(matchCriteria, null, 2).split('\n').join('\n '); - })() - : '{\n "user_query": "pugs"\n }' - }, - "ruleset_ids": ["${rulesetId}"], // An array of one or more unique query ruleset IDs - "organic": { - "match": { // Any choice of query used to return results - "description": "puggles" +# Query Rules Retriever Example +# https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers#rule-retriever +GET ${indecesRuleset}/_search +{ + "retriever": { + "rule": { + "match_criteria": ${ + criteriaData.length > 0 + ? (() => { + const matchCriteria = criteriaData.reduce>((acc, criterion) => { + if (criterion.metadata && criterion.values) { + acc[criterion.metadata] = criterion.values; + } + return acc; + }, {}); + // Format with proper indentation (6 spaces to align with the property) + return JSON.stringify(matchCriteria, null, 2).split('\n').join('\n '); + })() + : '{\n "user_query": "pugs"\n }' + }, + "ruleset_ids": [ + "${rulesetId}" // An array of one or more unique query ruleset IDs + ], + "retriever": { + "standard": { + "query": { + "query_string": { // Any choice of query used to return results + "query": "pugs" + } } } } } } +} + `; return ( From c147824a58f62980a985176af0578b99343151ca Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 26 May 2025 10:10:14 +0000 Subject: [PATCH 7/9] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../public/hooks/use_run_query_ruleset.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx index 6f3d8079440e6..d17b142117bf1 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx @@ -41,7 +41,7 @@ describe('UseRunQueryRuleset', () => { console: mockConsole, }, }); - + // Default mock for ruleset data (useFetchQueryRuleset as jest.Mock).mockReturnValue({ data: { @@ -119,7 +119,7 @@ describe('UseRunQueryRuleset', () => { const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; expect(buttonProps.request).toContain('my_index'); }); - + it('handles multiple indices from ruleset data', () => { (useFetchQueryRuleset as jest.Mock).mockReturnValue({ data: { From 9c04112b9b1af7c051dbee3c931e042f72c06d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Gonz=C3=A1lez?= Date: Tue, 27 May 2025 09:57:01 +0200 Subject: [PATCH 8/9] Fixing tests --- .../public/hooks/use_run_query_ruleset.test.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx index d17b142117bf1..8224801fd239a 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.test.tsx @@ -90,8 +90,10 @@ describe('UseRunQueryRuleset', () => { expect(buttonProps.request).toContain('test-index'); // Verify the request contains retriever structure expect(buttonProps.request).toContain('retriever'); + // Verify the request contains the expected query + expect(buttonProps.request).toContain('"query": "pugs"'); // Verify ruleset_ids are included - expect(buttonProps.request).toContain('"ruleset_ids": ['); + expect(buttonProps.request).toContain('test-ruleset'); }); it('renders with custom type and content', () => { @@ -172,7 +174,7 @@ describe('UseRunQueryRuleset', () => { const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; expect(buttonProps.request).toContain('"user_query": "search term"'); - expect(buttonProps.request).toContain('"user_location": ['); + expect(buttonProps.request).toMatch(/"user_location":\s*\[\s*"US",\s*"UK"\s*\]/); }); it('handles complex nested criteria values', () => { @@ -200,6 +202,6 @@ describe('UseRunQueryRuleset', () => { const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0]; expect(buttonProps.request).toContain('"nested_field": "nested value"'); - expect(buttonProps.request).toContain('"another_field": ['); + expect(buttonProps.request).toMatch(/"another_field":\s*\[\s*"array",\s*"of",\s*"values"\s*\]/); }); }); From a4f6e3bc4baef8da09ebb1a22a5aba12917c7bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Gonz=C3=A1lez?= Date: Tue, 27 May 2025 10:12:18 +0200 Subject: [PATCH 9/9] Memoizing populated request --- .../public/hooks/use_run_query_ruleset.tsx | 135 ++++++++---------- 1 file changed, 63 insertions(+), 72 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx index 2ccc92411fe40..3f2a36f7d8ac0 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_run_query_ruleset.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import dedent from 'dedent'; import { TryInConsoleButton } from '@kbn/try-in-console'; import { useFetchQueryRuleset } from './use_fetch_query_ruleset'; @@ -25,90 +25,81 @@ export const UseRunQueryRuleset = ({ const { data: queryRulesetData } = useFetchQueryRuleset(rulesetId); // Loop through all actions children to gather unique _index values - const indices: Set = new Set(); - if (queryRulesetData?.rules) { - for (const rule of queryRulesetData.rules) { - if (rule.actions?.docs) { - for (const doc of rule.actions.docs) { - if (doc._index) { - indices.add(doc._index); - } - } - } - } - } + const { indices, matchCriteria } = useMemo((): { indices: string; matchCriteria: string } => { + const indicesSet = new Set(); + const criteriaData = []; - // Use the found indices or default to 'my_index' - const indecesRuleset = indices.size > 0 ? Array.from(indices).join(',') : 'my_index'; + for (const rule of queryRulesetData?.rules ?? []) { + // Collect indices + rule.actions?.docs?.forEach((doc) => { + if (doc._index) indicesSet.add(doc._index); + }); - // Extract match criteria metadata and values from the ruleset - const criteriaData = []; - if (queryRulesetData?.rules) { - for (const rule of queryRulesetData.rules) { - if (rule.criteria) { - // Handle both single criterion and array of criteria - const criteriaArray = Array.isArray(rule.criteria) ? rule.criteria : [rule.criteria]; - for (const criterion of criteriaArray) { - if ( - criterion.values && - typeof criterion.values === 'object' && - !Array.isArray(criterion.values) - ) { - // Handle nested values inside criterion.values - Object.entries(criterion.values).forEach(([key, value]) => { - criteriaData.push({ - metadata: key, - values: value, - }); - }); - } else { - criteriaData.push({ - metadata: criterion.metadata || null, - values: criterion.values || null, - }); - } + // Collect criteria + const criteriaArray = Array.isArray(rule.criteria) + ? rule.criteria + : rule.criteria + ? [rule.criteria] + : []; + + for (const criterion of criteriaArray) { + if ( + criterion.values && + typeof criterion.values === 'object' && + !Array.isArray(criterion.values) + ) { + Object.entries(criterion.values).forEach(([key, value]) => { + criteriaData.push({ metadata: key, values: value }); + }); + } else { + criteriaData.push({ + metadata: criterion.metadata || null, + values: criterion.values || null, + }); } } } - } + + const reducedCriteria = criteriaData.reduce>( + (acc, { metadata, values }) => { + if (metadata && values !== undefined) acc[metadata] = values; + return acc; + }, + {} + ); + + return { + indices: indicesSet.size > 0 ? Array.from(indicesSet).join(',') : 'my_index', + matchCriteria: + Object.keys(reducedCriteria).length > 0 + ? JSON.stringify(reducedCriteria, null, 2).split('\n').join('\n ') + : `{\n "user_query": "pugs"\n }`, + }; + }, [queryRulesetData]); // Example based on https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-rule-query#_example_request_2 const TEST_QUERY_RULESET_API_SNIPPET = dedent` -# Query Rules Retriever Example -# https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers#rule-retriever -GET ${indecesRuleset}/_search -{ - "retriever": { - "rule": { - "match_criteria": ${ - criteriaData.length > 0 - ? (() => { - const matchCriteria = criteriaData.reduce>((acc, criterion) => { - if (criterion.metadata && criterion.values) { - acc[criterion.metadata] = criterion.values; - } - return acc; - }, {}); - // Format with proper indentation (6 spaces to align with the property) - return JSON.stringify(matchCriteria, null, 2).split('\n').join('\n '); - })() - : '{\n "user_query": "pugs"\n }' - }, - "ruleset_ids": [ - "${rulesetId}" // An array of one or more unique query ruleset IDs - ], + # Query Rules Retriever Example + # https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers#rule-retriever + GET ${indices}/_search + { "retriever": { - "standard": { - "query": { - "query_string": { // Any choice of query used to return results - "query": "pugs" + "rule": { + "match_criteria": ${matchCriteria}, + "ruleset_ids": [ + "${rulesetId}" // An array of one or more unique query ruleset IDs + ], + "retriever": { + "standard": { + "query": { + "query_string": { + "query": "pugs" + } + } } } } } } - } -} - `; return (