From fd934a1834d0fe67539fda2eff33b70c1c899fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Wed, 18 Jun 2025 16:51:14 +0200 Subject: [PATCH 1/7] WIP --- .../query_rule_flyout/query_rule_flyout.tsx | 47 ++++--- .../query_rule_metadata_editor.tsx | 14 +- .../use_query_rule_flyout_state.ts | 15 +- .../public/hooks/use_run_query_ruleset.tsx | 8 +- .../providers/query_ruleset_details_form.tsx | 4 + .../query_ruleset_detail_form_resolver.ts | 129 ++++++++++++++++++ 6 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 x-pack/solutions/search/plugins/search_query_rules/public/utils/query_ruleset_detail_form_resolver.ts diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx index c2f1ead1b6e83..eecc8b8f79ec9 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx @@ -62,21 +62,22 @@ export const QueryRuleFlyout: React.FC = ({ actionIdsFields, appendAction: appendNewAction, control, - documentCount, - dragEndHandle, criteria, criteriaCount, + documentCount, + dragEndHandle, + formState, handleAddCriteria, handleSave, indexNames, isAlways, isFlyoutDirty, isIdRule, + onDeleteDocument, + onIdSelectorChange, onIndexSelectorChange, pinType, remove, - onIdSelectorChange, - onDeleteDocument, setCriteriaCalloutActive, setIsAlways, setIsFlyoutDirty, @@ -281,23 +282,27 @@ export const QueryRuleFlyout: React.FC = ({ )} {shouldShowMetadataEditor && ( <> - {criteria.map((field, index) => ( - - { - setIsFlyoutDirty(true); - update(index, newCriteria); - }} - onRemove={() => { - setIsFlyoutDirty(true); - remove(index); - }} - /> - - - ))} + {criteria.map((field, index) => { + const error = formState.errors?.criteria?.[index]; + return ( + + { + setIsFlyoutDirty(true); + update(index, newCriteria); + }} + onRemove={() => { + setIsFlyoutDirty(true); + remove(index); + }} + error={error} + /> + + + ); + })} void; criteria: QueryRulesQueryRuleCriteria; onChange: (criteria: QueryRulesQueryRuleCriteria) => void; + error?: { values?: FieldError; metadata?: FieldError; type?: FieldError }; } export const QueryRuleMetadataEditor: React.FC = ({ onRemove, criteria, onChange, + error, }) => { - const [metadataField, setMetadataField] = React.useState(criteria.metadata || ''); + const [metadataField, setMetadataField] = useState(criteria.metadata || ''); + return ( @@ -44,6 +48,8 @@ export const QueryRuleMetadataEditor: React.FC = ( defaultMessage: 'Metadata field', } )} + isInvalid={!!error?.metadata} + error={error?.metadata ? error.metadata.message : undefined} > = ( defaultMessage: 'Values', } )} + isInvalid={!!error?.values} + error={error?.values ? error.values.message : undefined} > { const [isFlyoutDirty, setIsFlyoutDirty] = useState(false); - const { control, getValues, reset, setValue } = useFormContext(); + const { control, getValues, reset, setValue, formState, trigger } = + useFormContext(); + const { fields: criteria, remove, @@ -69,6 +71,16 @@ export const useQueryRuleFlyoutState = ({ name: 'actions.ids', }); + useEffect(() => { + trigger('actions.ids'); + }, [actionIdsFields, trigger]); + useEffect(() => { + trigger('actions.docs'); + }, [actionFields, trigger]); + useEffect(() => { + trigger('criteria'); + }, [criteria, trigger]); + const { data: indexNames } = useFetchIndexNames(''); const ruleFromRuleset = rules.find((rule) => rule.rule_id === ruleId); @@ -292,6 +304,7 @@ export const useQueryRuleFlyoutState = ({ criteriaCount, documentCount, dragEndHandle, + formState, getValues, handleAddCriteria, handleSave, 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 f8d9254402fa7..94d523ce51713 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 @@ -85,6 +85,10 @@ export const UseRunQueryRuleset = ({ }, [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` + # Get Query Ruleset + GET _query_rules/${rulesetId} + + # Query Rules Retriever Example # https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers#rule-retriever GET ${indices}/_search @@ -98,9 +102,7 @@ export const UseRunQueryRuleset = ({ "retriever": { "standard": { "query": { - "query_string": { - "query": "pugs" - } + "match_all": {} // replace with your query } } } diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/providers/query_ruleset_details_form.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/providers/query_ruleset_details_form.tsx index 114df30413707..5df74ca39ef32 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/providers/query_ruleset_details_form.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/providers/query_ruleset_details_form.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { QueryRuleEditorForm } from '../types'; +import { queryRulesetDetailFormResolver } from '../utils/query_ruleset_detail_form_resolver'; interface QueryRulesetDetailFormProvider { children: React.ReactNode; @@ -26,6 +27,9 @@ export const QueryRulesetDetailsForm: React.FC< type: 'pinned', actions: { docs: [], ids: [] }, }, + resolver: queryRulesetDetailFormResolver, + mode: 'onChange', + reValidateMode: 'onChange', }); return {children}; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_ruleset_detail_form_resolver.ts b/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_ruleset_detail_form_resolver.ts new file mode 100644 index 0000000000000..04d2ca22eb6ee --- /dev/null +++ b/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_ruleset_detail_form_resolver.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldErrors, Resolver } from 'react-hook-form'; +import { QueryRuleEditorForm } from '../types'; + +const hasErrors = (errors: FieldErrors): boolean => + Object.keys(errors).length > 0; + +export const queryRulesetDetailFormResolver: Resolver = async (values) => { + const errors: FieldErrors = {}; + + if (!values.criteria || values.criteria.length === 0) { + errors.criteria = { + type: 'required', + message: 'At least one criteria is required.', + }; + } else { + values.criteria.forEach((criteria, index) => { + if ( + criteria.type === 'gt' || + criteria.type === 'lt' || + criteria.type === 'gte' || + criteria.type === 'lte' + ) { + if (criteria.values) { + criteria.values.forEach((value, valueIndex) => { + if (!value || isNaN(Number(value))) { + // ! operator is used as we check and validate the initialization for all, TS cannot infer this + if (!errors.criteria) errors.criteria = {}; + if (!errors.criteria[index]) errors.criteria[index] = {}; + if (!errors.criteria[index]!.values) errors.criteria[index]!.values = {}; + errors.criteria[index]!.values = { + type: 'required', + message: `Values must be numeric for criteria type "${criteria.type}".`, + }; + } + }); + } + } + if (criteria.type !== 'always') { + if (!criteria.values || criteria.values.length === 0) { + // ! operator is used as we check and validate the initialization for all, TS cannot infer this + if (!errors.criteria) errors.criteria = {}; + if (!errors.criteria[index]) errors.criteria[index] = {}; + errors.criteria[index]!.values = { + type: 'required', + message: 'At least one value is required', + }; + } + + if (!criteria.metadata || criteria.metadata.trim() === '') { + // ! operator is used as we check and validate the initialization for all, TS cannot infer this + if (!errors.criteria) errors.criteria = {}; + if (!errors.criteria[index]) errors.criteria[index] = {}; + errors.criteria[index]!.metadata = { + type: 'required', + message: 'Metadata is required', + }; + } + } + }); + } + + if (!values.actions) { + errors.actions = { + type: 'required', + message: 'At least one action is required.', + }; + } + + if (values.actions.docs && !values.actions.ids && values.actions.docs.length === 0) { + errors.actions = { + type: 'required', + message: 'At least one document action is required.', + }; + } + + if (values.actions.ids && !values.actions.docs && values.actions.ids.length === 0) { + errors.actions = { + type: 'required', + message: 'At least one ID action is required.', + }; + } + + // check for docs case all actions are filled + if (values.actions.docs && values.actions.docs.length > 0) { + values.actions.docs.forEach((doc, index) => { + if (!doc._id || !doc._index) { + if (!errors.actions) errors.actions = {}; + if (!errors.actions.docs) errors.actions.docs = {}; + errors.actions.docs[index] = { + type: 'required', + message: 'Document ID and Index are required.', + }; + } + }); + } + + // check for ids case all actions are filled + if (values.actions.ids && values.actions.ids.length > 0) { + values.actions.ids.forEach((id, index) => { + if (!id) { + if (!errors.actions) errors.actions = {}; + if (!errors.actions.ids) errors.actions.ids = {}; + errors.actions.ids[index] = { + type: 'required', + message: 'ID is required.', + }; + } + }); + } + + if (hasErrors(errors)) { + return { + values: {}, + errors, + }; + } + + return { + values, + errors: {}, + }; +}; From 58c7be90cec385111e744952c3a1a43e760929c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 23 Jun 2025 22:06:16 +0200 Subject: [PATCH 2/7] Add validations and move the isDirty check to rhf --- .../search_query_rules/common/types.ts | 1 + .../document_selector/document_selector.tsx | 19 +- .../document_selector/draggable_list.tsx | 154 +++++++------ .../document_selector/rule_type_selector.tsx | 3 - .../metadata_type_selector.tsx | 9 +- .../query_rule_flyout/query_rule_flyout.tsx | 132 +++++++---- .../query_rule_metadata_editor.tsx | 6 +- .../use_query_rule_flyout_state.test.tsx | 2 +- .../use_query_rule_flyout_state.ts | 30 +-- .../hooks/use_run_query_ruleset.test.tsx | 2 +- .../providers/query_ruleset_details_form.tsx | 1 + .../query_ruleset_detail_form_resolver.ts | 206 ++++++++++++------ 12 files changed, 369 insertions(+), 196 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/common/types.ts b/x-pack/solutions/search/plugins/search_query_rules/common/types.ts index 464c90b6dcce5..0055393487c29 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/common/types.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/common/types.ts @@ -45,6 +45,7 @@ export type QueryRuleEditorForm = Pick< 'criteria' | 'type' | 'actions' > & { mode: 'create' | 'edit'; + isAlways: boolean; rulesetId: string; ruleId: string; }; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/document_selector.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/document_selector.tsx index 22a29ad02c97c..7b84d6516e2fb 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/document_selector.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/document_selector.tsx @@ -7,8 +7,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiNotificationBadge, EuiPanel } from '@elastic/eui'; import { EditableResult } from '@kbn/search-index-documents'; -import React from 'react'; import { resultToFieldFromMappingResponse } from '@kbn/search-index-documents/components/result/result_metadata'; +import React from 'react'; +import { FieldError } from 'react-hook-form'; import { useFetchDocument } from '../../../../hooks/use_fetch_document'; interface DocumentSelectorProps { @@ -21,6 +22,7 @@ interface DocumentSelectorProps { onIndexSelectorChange?: (index: string) => void; indices?: string[]; hasIndexSelector?: boolean; + error?: FieldError; } export const DocumentSelector: React.FC = ({ @@ -33,11 +35,22 @@ export const DocumentSelector: React.FC = ({ onIndexSelectorChange = () => {}, indices = [], hasIndexSelector = true, + error: formError, }) => { - const { data, error, isError, isLoading } = useFetchDocument(index, initialDocId); + const { + data, + error: fetchDocumentError, + isError, + isLoading, + } = useFetchDocument(index, initialDocId); const { document, mappings } = data || {}; // Otherwise it will show loading until first document is fetched const showLoading = Boolean(isLoading && index && initialDocId); + const error = isError + ? fetchDocumentError?.body?.message + : formError + ? formError?.message + : undefined; return ( = ({ onIndexSelectorChange={onIndexSelectorChange} onDeleteDocument={onDeleteDocument} isLoading={showLoading} - error={isError ? error?.body?.message : undefined} + error={error} /> ); }; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/draggable_list.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/draggable_list.tsx index e955496d00d3e..0d33ad3f235a2 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/draggable_list.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/draggable_list.tsx @@ -7,8 +7,8 @@ import React from 'react'; -import { EuiDragDropContext, EuiDroppable, EuiDraggable } from '@elastic/eui'; -import type { FieldArrayWithId } from 'react-hook-form'; +import { EuiDragDropContext, EuiDroppable, EuiDraggable, EuiCallOut } from '@elastic/eui'; +import type { FieldArrayWithId, FieldError, FieldErrors } from 'react-hook-form'; import type { QueryRulesQueryRuleType } from '@elastic/elasticsearch/lib/api/types'; import type { OnDragEndResponder } from '@hello-pangea/dnd'; import { QueryRuleEditorForm } from '../../../../../common/types'; @@ -24,6 +24,7 @@ interface DraggableListProps { onDeleteDocument: (index: number) => void; onIndexSelectorChange: (index: number, indexName: string) => void; onIdSelectorChange: (index: number, id: string) => void; + errors?: FieldErrors; } export const DraggableList: React.FC = ({ actionFields, @@ -35,70 +36,97 @@ export const DraggableList: React.FC = ({ onIndexSelectorChange, onIdSelectorChange, dragEndHandle, + errors, }) => { return ( - - - {isIdRule && actionIdsFields - ? actionIdsFields.map((doc, index) => ( - - {() => ( - { - onDeleteDocument(index); - }} - onIdSelectorChange={(id) => { - onIdSelectorChange(index, id); - }} - /> - )} - - )) - : actionFields.map((doc, index) => ( - - {() => ( - { - onDeleteDocument(index); - }} - onIndexSelectorChange={(indexName) => { - onIndexSelectorChange(index, indexName); + <> + + + <> + {actionFields.length === 0 && actionIdsFields?.length === 0 && ( + + )} + {isIdRule && actionIdsFields + ? actionIdsFields.map((doc, index) => ( + + {() => { + const docError = errors?.actions?.docs?.[index]; + // check if it can be cast into FieldError from React Hook Form and cast it otherwise undefined + const fieldError = + docError?.type && docError.message ? (docError as FieldError) : undefined; + + return ( + { + onDeleteDocument(index); + }} + onIdSelectorChange={(id) => { + onIdSelectorChange(index, id); + }} + error={fieldError} + /> + ); }} - onIdSelectorChange={(id) => { - onIdSelectorChange(index, id); + + )) + : actionFields.map((doc, index) => ( + + {() => { + const docError = errors?.actions?.docs?.[index]; + const fieldError = + docError?.type && docError.message ? (docError as FieldError) : undefined; + return ( + { + onDeleteDocument(index); + }} + onIndexSelectorChange={(indexName) => { + onIndexSelectorChange(index, indexName); + }} + onIdSelectorChange={(id) => { + onIdSelectorChange(index, id); + }} + indices={indexNames} + error={fieldError} + /> + ); }} - indices={indexNames} - /> - )} - - )) || <>} - - + + )) || <>} + + + + ); }; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/rule_type_selector.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/rule_type_selector.tsx index cb86bb8cd6e90..3922303a78bb6 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/rule_type_selector.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/rule_type_selector.tsx @@ -11,12 +11,10 @@ import { EuiButtonGroup, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; interface QueryRuleTypeSelectorProps { - setIsFlyoutDirty: (isDirty: boolean) => void; onChange: (value: string) => void; selectedId: string; } export const QueryRuleTypeSelector: React.FC = ({ - setIsFlyoutDirty, onChange, selectedId, }) => ( @@ -54,7 +52,6 @@ export const QueryRuleTypeSelector: React.FC = ({ }, ]} onChange={(id) => { - setIsFlyoutDirty(true); onChange(id); }} buttonSize="compressed" diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/metadata_type_selector.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/metadata_type_selector.tsx index 3e995cb1f3e2e..fd89dea18a482 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/metadata_type_selector.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/metadata_type_selector.tsx @@ -11,14 +11,12 @@ import { EuiButtonGroup } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; interface MetadataTypeSelectorProps { - setIsFlyoutDirty: (isDirty: boolean) => void; isAlways: boolean; - setIsAlways: (isAlways: boolean) => void; + onChange: (isAlways: boolean) => void; } export const MetadataTypeSelector: React.FC = ({ - setIsFlyoutDirty, isAlways, - setIsAlways, + onChange, }) => { return ( = ({ }, ]} onChange={(id) => { - setIsFlyoutDirty(true); - setIsAlways(id === 'always'); + onChange(id === 'always'); }} buttonSize="compressed" type="single" diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx index eecc8b8f79ec9..deca468698981 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx @@ -26,7 +26,7 @@ import { import { css } from '@emotion/react'; import { DISCOVER_APP_ID } from '@kbn/deeplinks-analytics'; import { FormattedMessage } from '@kbn/i18n-react'; -import { Controller } from 'react-hook-form'; +import { Controller, FieldError } from 'react-hook-form'; import { useKibana } from '../../../hooks/use_kibana'; import { SearchQueryRulesQueryRule } from '../../../types'; import { QueryRuleFlyoutBody, QueryRuleFlyoutPanel } from '../styles'; @@ -70,7 +70,6 @@ export const QueryRuleFlyout: React.FC = ({ handleAddCriteria, handleSave, indexNames, - isAlways, isFlyoutDirty, isIdRule, onDeleteDocument, @@ -79,8 +78,6 @@ export const QueryRuleFlyout: React.FC = ({ pinType, remove, setCriteriaCalloutActive, - setIsAlways, - setIsFlyoutDirty, shouldShowCriteriaCallout, shouldShowMetadataEditor, update, @@ -130,7 +127,13 @@ export const QueryRuleFlyout: React.FC = ({ - + = ({ control={control} name="type" render={({ field: { value, onChange } }) => ( - + )} /> @@ -198,6 +197,26 @@ export const QueryRuleFlyout: React.FC = ({ + + {isIdRule && ( + <> + + +

+ +

+
+
+ + + )} = ({ indexNames={indexNames} dragEndHandle={dragEndHandle} onDeleteDocument={onDeleteDocument} + errors={formState.errors} /> {pinType === 'pinned' && documentCount !== 0 && ( @@ -232,7 +252,13 @@ export const QueryRuleFlyout: React.FC = ({
- + = ({ - ( + + )} /> @@ -282,27 +310,50 @@ export const QueryRuleFlyout: React.FC = ({ )} {shouldShowMetadataEditor && ( <> - {criteria.map((field, index) => { - const error = formState.errors?.criteria?.[index]; - return ( - - { - setIsFlyoutDirty(true); - update(index, newCriteria); - }} - onRemove={() => { - setIsFlyoutDirty(true); - remove(index); - }} - error={error} - /> - - - ); - })} + {criteria.length ? ( + criteria.map((field, index) => { + const error = formState.errors?.criteria?.[index]; + return ( + + { + update(index, newCriteria); + }} + onRemove={() => { + remove(index); + }} + error={ + error + ? (error as { + values: FieldError | undefined; + metadata: FieldError | undefined; + type: FieldError | undefined; + }) + : undefined + } + /> + + + ); + }) + ) : ( + <> + + } + /> + + + )} = ({ data-test-subj="searchQueryRulesQueryRuleFlyoutUpdateButton" fill onClick={handleSave} - disabled={!isFlyoutDirty} + disabled={ + // Id rule is not supported in the UI. We still allow saving it. + // To make it properly, we need to reimplement the action logic in RHF + (!isIdRule && !isFlyoutDirty) || + !formState.isValid || + formState.isSubmitting || + formState.isValidating + } > {createMode ? ( void; criteria: QueryRulesQueryRuleCriteria; onChange: (criteria: QueryRulesQueryRuleCriteria) => void; - error?: { values?: FieldError; metadata?: FieldError; type?: FieldError }; + error?: { + values?: FieldError; + metadata?: FieldError; + type?: FieldError; + }; } export const QueryRuleMetadataEditor: React.FC = ({ diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.test.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.test.tsx index 3c6ff39df7bf1..13c63681163f4 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.test.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.test.tsx @@ -49,6 +49,7 @@ const MockFormProvider = ({ ruleId: '', criteria: [], type: 'pinned', + isAlways: false, actions: { docs: [], ids: [] }, }, }, @@ -502,7 +503,6 @@ describe('useQueryRuleFlyoutState hook', () => { ); }); - await waitFor(() => expect(mockFlyoutState.isFlyoutDirty).toBe(true)); await waitFor(() => { expect(mockFlyoutState.actionIdsFields).toEqual(['id-1', 'id-2']); }); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.ts b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.ts index 2b5b46626bb82..13ddffe994dfd 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.ts @@ -27,6 +27,7 @@ export interface UseQueryRuleFlyoutStateProps { ruleId: string; rules: SearchQueryRulesQueryRule[]; setIsFormDirty?: (isDirty: boolean) => void; + isFlyoutDirty?: boolean; onSave: (rule: SearchQueryRulesQueryRule) => void; } @@ -38,10 +39,8 @@ export const useQueryRuleFlyoutState = ({ setIsFormDirty, onSave, }: UseQueryRuleFlyoutStateProps) => { - const [isFlyoutDirty, setIsFlyoutDirty] = useState(false); const { control, getValues, reset, setValue, formState, trigger } = useFormContext(); - const { fields: criteria, remove, @@ -62,6 +61,11 @@ export const useQueryRuleFlyoutState = ({ name: 'actions.docs', }); + const isAlways = useWatch({ + control, + name: 'isAlways', + }); + const pinType = useWatch({ control, name: 'type', @@ -84,9 +88,6 @@ export const useQueryRuleFlyoutState = ({ const { data: indexNames } = useFetchIndexNames(''); const ruleFromRuleset = rules.find((rule) => rule.rule_id === ruleId); - const [isAlways, setIsAlways] = useState( - (ruleFromRuleset?.criteria && isCriteriaAlways(ruleFromRuleset?.criteria)) ?? false - ); const isDocRule = Boolean( !actionIdsFields || actionFields.length > 0 || !!(actionIdsFields?.length === 0) ); @@ -100,11 +101,10 @@ export const useQueryRuleFlyoutState = ({ type: ruleFromRuleset.type, actions: ruleFromRuleset.actions, mode: 'edit', + isAlways: + (ruleFromRuleset?.criteria && isCriteriaAlways(ruleFromRuleset?.criteria)) ?? false, ruleId, }); - setIsAlways( - (ruleFromRuleset?.criteria && isCriteriaAlways(ruleFromRuleset?.criteria)) ?? false - ); } }, [ruleFromRuleset, reset, getValues, rulesetId, ruleId]); @@ -129,14 +129,13 @@ export const useQueryRuleFlyoutState = ({ ids: [], }, mode: 'create', + isAlways: false, ruleId, }); - setIsAlways(false); } }, [createMode, reset, ruleId]); const handleAddCriteria = () => { - setIsFlyoutDirty(true); append({ type: 'exact', metadata: '', @@ -145,7 +144,6 @@ export const useQueryRuleFlyoutState = ({ }; const appendNewAction = () => { - setIsFlyoutDirty(true); if (isIdRule) { setValue('actions.ids', [...(getValues('actions.ids') || []), '']); } else { @@ -245,19 +243,17 @@ export const useQueryRuleFlyoutState = ({ const dragEndHandle: OnDragEndResponder = ({ source, destination }) => { if (source && destination && (ruleFromRuleset || createMode)) { - setIsFlyoutDirty(true); if (isDocRule) { const newActions = euiDragDropReorder(actionFields, source.index, destination.index); replaceAction(newActions); } else if (isIdRule && actionIdsFields) { const newActions = euiDragDropReorder(actionIdsFields, source.index, destination.index); - setValue('actions.ids', newActions); + setValue('actions.ids', [...newActions]); } } }; const onDeleteDocument = (index: number) => { - setIsFlyoutDirty(true); if (createMode || !isIdRule) { removeAction(index); } else { @@ -269,7 +265,6 @@ export const useQueryRuleFlyoutState = ({ }; const onIndexSelectorChange = (index: number, indexName: string) => { - setIsFlyoutDirty(true); const updatedActions = actionFields.map((action, i) => i === index ? { ...action, _index: indexName } : action ); @@ -277,7 +272,6 @@ export const useQueryRuleFlyoutState = ({ }; const onIdSelectorChange = (index: number, id: string) => { - setIsFlyoutDirty(true); if (isIdRule && actionIdsFields) { const updatedActions = actionIdsFields.map((value, i) => (i === index ? id : value)); setValue('actions.ids', updatedActions); @@ -311,16 +305,14 @@ export const useQueryRuleFlyoutState = ({ indexNames, isAlways, isDocRule, - isFlyoutDirty, isIdRule, + isFlyoutDirty: formState.isDirty, onDeleteDocument, onIdSelectorChange, onIndexSelectorChange, pinType, remove, setCriteriaCalloutActive, - setIsAlways, - setIsFlyoutDirty, shouldShowCriteriaCallout, shouldShowMetadataEditor, update, 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 8224801fd239a..fe8da5242f939 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 @@ -91,7 +91,7 @@ describe('UseRunQueryRuleset', () => { // Verify the request contains retriever structure expect(buttonProps.request).toContain('retriever'); // Verify the request contains the expected query - expect(buttonProps.request).toContain('"query": "pugs"'); + expect(buttonProps.request).toContain('"match_all": {}'); // Verify ruleset_ids are included expect(buttonProps.request).toContain('test-ruleset'); }); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/providers/query_ruleset_details_form.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/providers/query_ruleset_details_form.tsx index 5df74ca39ef32..c70a2a694919e 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/providers/query_ruleset_details_form.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/providers/query_ruleset_details_form.tsx @@ -20,6 +20,7 @@ export const QueryRulesetDetailsForm: React.FC< > = ({ children }) => { const form = useForm({ defaultValues: { + isAlways: false, mode: 'create', rulesetId: '', ruleId: '', diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_ruleset_detail_form_resolver.ts b/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_ruleset_detail_form_resolver.ts index 04d2ca22eb6ee..78b4001e345e4 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_ruleset_detail_form_resolver.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_ruleset_detail_form_resolver.ts @@ -6,6 +6,7 @@ */ import { FieldErrors, Resolver } from 'react-hook-form'; +import { i18n } from '@kbn/i18n'; import { QueryRuleEditorForm } from '../types'; const hasErrors = (errors: FieldErrors): boolean => @@ -14,79 +15,157 @@ const hasErrors = (errors: FieldErrors): boolean => export const queryRulesetDetailFormResolver: Resolver = async (values) => { const errors: FieldErrors = {}; - if (!values.criteria || values.criteria.length === 0) { - errors.criteria = { - type: 'required', - message: 'At least one criteria is required.', - }; - } else { - values.criteria.forEach((criteria, index) => { - if ( - criteria.type === 'gt' || - criteria.type === 'lt' || - criteria.type === 'gte' || - criteria.type === 'lte' - ) { - if (criteria.values) { - criteria.values.forEach((value, valueIndex) => { - if (!value || isNaN(Number(value))) { - // ! operator is used as we check and validate the initialization for all, TS cannot infer this - if (!errors.criteria) errors.criteria = {}; - if (!errors.criteria[index]) errors.criteria[index] = {}; - if (!errors.criteria[index]!.values) errors.criteria[index]!.values = {}; - errors.criteria[index]!.values = { - type: 'required', - message: `Values must be numeric for criteria type "${criteria.type}".`, - }; - } - }); + if (!values.isAlways) { + if (!values.criteria || values.criteria.length === 0) { + errors.criteria = { + type: 'required', + message: i18n.translate( + 'xpack.queryRules.queryRulesetDetailFormResolver.criteria.required', + { defaultMessage: 'At least one criteria is required.' } + ), + }; + } else { + values.criteria.forEach((criteria, index) => { + validateCriteriaNumericTypes(criteria, index, errors); + if (criteria.type !== 'always') { + validateCriteriaValues(criteria, index, errors); + validateCriteriaMetadataField(criteria, index, errors); } - } - if (criteria.type !== 'always') { - if (!criteria.values || criteria.values.length === 0) { + }); + } + } + + validateActionsExist(values, errors); + + validateDocActions(values, errors); + + validateIdActions(values, errors); + + if (hasErrors(errors)) { + return { + values: {}, + errors, + }; + } + + return { + values, + errors: {}, + }; +}; + +const validateCriteriaNumericTypes = ( + criteria: QueryRuleEditorForm['criteria'][number], + index: number, + errors: FieldErrors +) => { + if ( + criteria.type === 'gt' || + criteria.type === 'lt' || + criteria.type === 'gte' || + criteria.type === 'lte' + ) { + if (criteria.values) { + criteria.values.forEach((value, valueIndex) => { + if (!value || isNaN(Number(value))) { // ! operator is used as we check and validate the initialization for all, TS cannot infer this if (!errors.criteria) errors.criteria = {}; if (!errors.criteria[index]) errors.criteria[index] = {}; + if (!errors.criteria[index]!.values) errors.criteria[index]!.values = {}; errors.criteria[index]!.values = { type: 'required', - message: 'At least one value is required', + message: i18n.translate( + 'xpack.queryRules.queryRulesetDetailFormResolver.criteria.numeric', + { defaultMessage: 'Values must be numeric for this criteria type.' } + ), }; } + }); + } + } +}; - if (!criteria.metadata || criteria.metadata.trim() === '') { - // ! operator is used as we check and validate the initialization for all, TS cannot infer this - if (!errors.criteria) errors.criteria = {}; - if (!errors.criteria[index]) errors.criteria[index] = {}; - errors.criteria[index]!.metadata = { - type: 'required', - message: 'Metadata is required', - }; - } - } - }); +const validateCriteriaMetadataField = ( + criteria: QueryRuleEditorForm['criteria'][number], + index: number, + errors: FieldErrors +) => { + if (!criteria.metadata || criteria.metadata.trim() === '') { + // ! operator is used as we check and validate the initialization for all, TS cannot infer this + if (!errors.criteria) errors.criteria = {}; + if (!errors.criteria[index]) errors.criteria[index] = {}; + errors.criteria[index]!.metadata = { + type: 'required', + message: i18n.translate( + 'xpack.queryRules.queryRulesetDetailFormResolver.criteria.metadata.required', + { defaultMessage: 'Metadata is required' } + ), + }; } +}; - if (!values.actions) { - errors.actions = { +const validateCriteriaValues = ( + criteria: QueryRuleEditorForm['criteria'][number], + index: number, + errors: FieldErrors +) => { + if (!criteria.values || criteria.values.length === 0) { + // ! operator is used as we check and validate the initialization for all, TS cannot infer this + if (!errors.criteria) errors.criteria = {}; + if (!errors.criteria[index]) errors.criteria[index] = {}; + errors.criteria[index]!.values = { type: 'required', - message: 'At least one action is required.', + message: i18n.translate('xpack.queryRules.queryRulesetDetailFormResolver.value.required', { + defaultMessage: 'At least one value is required', + }), }; } +}; - if (values.actions.docs && !values.actions.ids && values.actions.docs.length === 0) { +const validateActionsExist = ( + values: QueryRuleEditorForm, + errors: FieldErrors +) => { + if (!values.actions) { errors.actions = { type: 'required', - message: 'At least one document action is required.', + message: i18n.translate('xpack.queryRules.queryRulesetDetailFormResolver.action.required', { + defaultMessage: 'At least one action is required.', + }), }; } - if (values.actions.ids && !values.actions.docs && values.actions.ids.length === 0) { + if ( + values.actions.docs && + (!values.actions.ids || !values.actions.ids.length) && + values.actions.docs.length === 0 + ) { errors.actions = { type: 'required', - message: 'At least one ID action is required.', + message: i18n.translate('xpack.queryRules.queryRulesetDetailFormResolver.document.required', { + defaultMessage: 'At least one document action is required.', + }), }; + } else { + if ( + values.actions.ids && + (!values.actions.docs || !values.actions.docs.length) && + values.actions.ids.length === 0 + ) { + errors.actions = { + type: 'required', + message: i18n.translate('xpack.queryRules.queryRulesetDetailFormResolver.id.required', { + defaultMessage: 'At least one ID action is required.', + }), + }; + } } +}; +const validateDocActions = ( + values: QueryRuleEditorForm, + errors: FieldErrors +) => { // check for docs case all actions are filled if (values.actions.docs && values.actions.docs.length > 0) { values.actions.docs.forEach((doc, index) => { @@ -95,12 +174,22 @@ export const queryRulesetDetailFormResolver: Resolver = asy if (!errors.actions.docs) errors.actions.docs = {}; errors.actions.docs[index] = { type: 'required', - message: 'Document ID and Index are required.', + message: i18n.translate( + 'xpack.queryRules.queryRulesetDetailFormResolver.idAndIndex.required', + { + defaultMessage: 'Document ID and Index are required.', + } + ), }; } }); } +}; +const validateIdActions = ( + values: QueryRuleEditorForm, + errors: FieldErrors +) => { // check for ids case all actions are filled if (values.actions.ids && values.actions.ids.length > 0) { values.actions.ids.forEach((id, index) => { @@ -109,21 +198,14 @@ export const queryRulesetDetailFormResolver: Resolver = asy if (!errors.actions.ids) errors.actions.ids = {}; errors.actions.ids[index] = { type: 'required', - message: 'ID is required.', + message: i18n.translate( + 'xpack.queryRules.queryRulesetDetailFormResolver.action.id.required', + { + defaultMessage: 'ID is required.', + } + ), }; } }); } - - if (hasErrors(errors)) { - return { - values: {}, - errors, - }; - } - - return { - values, - errors: {}, - }; }; From 859d5f088937909462894c326f0c37b310c7c242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 23 Jun 2025 23:54:00 +0200 Subject: [PATCH 3/7] Review changes --- .../document_selector/draggable_list.tsx | 173 +++++++++--------- .../query_rule_flyout/query_rule_flyout.tsx | 13 +- .../query_rule_metadata_editor.tsx | 1 - .../public/utils/field_error_utils.ts | 25 +++ 4 files changed, 113 insertions(+), 99 deletions(-) create mode 100644 x-pack/solutions/search/plugins/search_query_rules/public/utils/field_error_utils.ts diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/draggable_list.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/draggable_list.tsx index 0d33ad3f235a2..e9b30b8ee468f 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/draggable_list.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/document_selector/draggable_list.tsx @@ -11,6 +11,7 @@ import { EuiDragDropContext, EuiDroppable, EuiDraggable, EuiCallOut } from '@ela import type { FieldArrayWithId, FieldError, FieldErrors } from 'react-hook-form'; import type { QueryRulesQueryRuleType } from '@elastic/elasticsearch/lib/api/types'; import type { OnDragEndResponder } from '@hello-pangea/dnd'; +import { isFieldError } from '../../../../utils/field_error_utils'; import { QueryRuleEditorForm } from '../../../../../common/types'; import { DocumentSelector } from './document_selector'; @@ -39,94 +40,90 @@ export const DraggableList: React.FC = ({ errors, }) => { return ( - <> - - - <> - {actionFields.length === 0 && actionIdsFields?.length === 0 && ( - - )} - {isIdRule && actionIdsFields - ? actionIdsFields.map((doc, index) => ( - - {() => { - const docError = errors?.actions?.docs?.[index]; - // check if it can be cast into FieldError from React Hook Form and cast it otherwise undefined - const fieldError = - docError?.type && docError.message ? (docError as FieldError) : undefined; + + + <> + {actionFields.length === 0 && actionIdsFields?.length === 0 && ( + + )} + {isIdRule && actionIdsFields + ? actionIdsFields.map((doc, index) => ( + + {() => { + const docError = errors?.actions?.docs?.[index]; + const fieldError = isFieldError(docError) ? docError : undefined; - return ( - { - onDeleteDocument(index); - }} - onIdSelectorChange={(id) => { - onIdSelectorChange(index, id); - }} - error={fieldError} - /> - ); - }} - - )) - : actionFields.map((doc, index) => ( - - {() => { - const docError = errors?.actions?.docs?.[index]; - const fieldError = - docError?.type && docError.message ? (docError as FieldError) : undefined; - return ( - { - onDeleteDocument(index); - }} - onIndexSelectorChange={(indexName) => { - onIndexSelectorChange(index, indexName); - }} - onIdSelectorChange={(id) => { - onIdSelectorChange(index, id); - }} - indices={indexNames} - error={fieldError} - /> - ); - }} - - )) || <>} - - - - + return ( + { + onDeleteDocument(index); + }} + onIdSelectorChange={(id) => { + onIdSelectorChange(index, id); + }} + error={fieldError} + /> + ); + }} + + )) + : actionFields.map((doc, index) => ( + + {() => { + const docError = errors?.actions?.docs?.[index]; + const fieldError = + docError?.type && docError.message ? (docError as FieldError) : undefined; + return ( + { + onDeleteDocument(index); + }} + onIndexSelectorChange={(indexName) => { + onIndexSelectorChange(index, indexName); + }} + onIdSelectorChange={(id) => { + onIdSelectorChange(index, id); + }} + indices={indexNames} + error={fieldError} + /> + ); + }} + + )) || <>} + + + ); }; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx index deca468698981..fc4a7461559cb 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_flyout.tsx @@ -26,7 +26,8 @@ import { import { css } from '@emotion/react'; import { DISCOVER_APP_ID } from '@kbn/deeplinks-analytics'; import { FormattedMessage } from '@kbn/i18n-react'; -import { Controller, FieldError } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; +import { isQueryRuleFieldError } from '../../../utils/field_error_utils'; import { useKibana } from '../../../hooks/use_kibana'; import { SearchQueryRulesQueryRule } from '../../../types'; import { QueryRuleFlyoutBody, QueryRuleFlyoutPanel } from '../styles'; @@ -324,15 +325,7 @@ export const QueryRuleFlyout: React.FC = ({ onRemove={() => { remove(index); }} - error={ - error - ? (error as { - values: FieldError | undefined; - metadata: FieldError | undefined; - type: FieldError | undefined; - }) - : undefined - } + error={isQueryRuleFieldError(error) ? error : undefined} /> diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_metadata_editor.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_metadata_editor.tsx index 52784e3200e69..122dd95052488 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_metadata_editor.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/query_rule_metadata_editor.tsx @@ -28,7 +28,6 @@ interface QueryRuleMetadataEditorProps { error?: { values?: FieldError; metadata?: FieldError; - type?: FieldError; }; } diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/utils/field_error_utils.ts b/x-pack/solutions/search/plugins/search_query_rules/public/utils/field_error_utils.ts new file mode 100644 index 0000000000000..736dd84bfe01c --- /dev/null +++ b/x-pack/solutions/search/plugins/search_query_rules/public/utils/field_error_utils.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldError } from 'react-hook-form'; + +export const isFieldError = (error?: { type?: string; message?: string }): error is FieldError => { + return !!error && error.type !== undefined && error.message !== undefined; +}; + +export const isQueryRuleFieldError = (error?: { + values?: { type?: string; message?: string }; + metadata?: { type?: string; message?: string }; +}): error is { + values?: FieldError; + metadata?: FieldError; +} => { + return ( + (error?.values !== undefined && isFieldError(error.values)) || + (error?.metadata !== undefined && isFieldError(error.metadata)) + ); +}; From 57f3be248d68cd15457920982825641123c4354a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Tue, 24 Jun 2025 20:48:24 +0200 Subject: [PATCH 4/7] Add telemetry data --- .../plugins/search_query_rules/kibana.jsonc | 1 + .../public/analytics/constants.ts | 42 +++++++++++++++++++ .../components/empty_prompt/empty_prompt.tsx | 18 +++++++- .../components/error_prompt/error_prompt.tsx | 11 ++++- .../public/components/overview/overview.tsx | 23 ++++------ .../query_rules_sets/delete_ruleset_modal.tsx | 4 ++ .../query_rules_sets/query_rules_sets.tsx | 20 +++++++-- .../query_rules_sets_search.tsx | 7 +++- .../query_rule_detail_panel.tsx | 5 +++ .../query_rule_draggable_list.tsx | 7 ++++ .../query_ruleset_detail.tsx | 19 +++++++-- .../hooks/use_fetch_query_rules_sets.ts | 1 + .../public/hooks/use_usage_tracker.ts | 40 ++++++++++++++++++ .../search_query_rules/public/types.ts | 2 + 14 files changed, 176 insertions(+), 24 deletions(-) create mode 100644 x-pack/solutions/search/plugins/search_query_rules/public/analytics/constants.ts create mode 100644 x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_usage_tracker.ts diff --git a/x-pack/solutions/search/plugins/search_query_rules/kibana.jsonc b/x-pack/solutions/search/plugins/search_query_rules/kibana.jsonc index ff86761f1e9e7..3146b2c38688b 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/kibana.jsonc +++ b/x-pack/solutions/search/plugins/search_query_rules/kibana.jsonc @@ -19,6 +19,7 @@ "console", "searchNavigation", "share", + "usageCollection", ], "requiredBundles": [ "kibanaReact", diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/analytics/constants.ts b/x-pack/solutions/search/plugins/search_query_rules/public/analytics/constants.ts new file mode 100644 index 0000000000000..bea8899e6a582 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_query_rules/public/analytics/constants.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum AnalyticsEvents { + // Empty promp actions + gettingStartedButtonClicked = 'getting_started_button_clicked', + createInConsoleClicked = 'create_in_console_clicked', + emptyPromptLoaded = 'empty_prompt_loaded', + + // Error prompt loaded + genericErrorPromptLoaded = 'generic_error_prompt_loaded', + notFoundErrorPromptLoaded = 'not_found_error_prompt_loaded', + missingPermissionsErrorPromptLoaded = 'missing_permissions_error_prompt_loaded', + + rulesetDeleted = 'ruleset_deleted', + rulesetCreated = 'ruleset_created', + rulesetUpdated = 'ruleset_updated', + + // ruleset detail page actions + rulesetDetailPageLoaded = 'ruleset_details_page_loaded', + addRuleClicked = 'add_rule_clicked', + editRuleClicked = 'edit_rule_clicked', + deleteRuleClicked = 'delete_rule_clicked', + testInConsoleClicked = 'test_in_console_clicked', + deleteRulesetFromHeaderClicked = 'delete_ruleset_from_header_clicked', + backToRulesetListClicked = 'back_to_ruleset_list_clicked', + + rulesReordered = 'rules_reordered', + + // ruleset list page actions + editRulesetInlineNameClicked = 'edit_ruleset_inline_name_clicked', + editRulesetInlineDropdownClicked = 'edit_ruleset_inline_dropdown_clicked', + rulesetListPageLoaded = 'ruleset_list_page_loaded', + deleteRulesetInlineDropdownClicked = 'delete_ruleset_inline_dropdown_clicked', + testRulesetInlineDropdownClicked = 'test_ruleset_inline_dropdown_clicked', + rulesetSearched = 'ruleset_searched', + addRulesetClicked = 'add_ruleset_clicked', +} diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/empty_prompt/empty_prompt.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/empty_prompt/empty_prompt.tsx index d094d6d6db458..8fe795acbdc9a 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/empty_prompt/empty_prompt.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/empty_prompt/empty_prompt.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiButton, @@ -34,13 +34,21 @@ import { useKibana } from '../../hooks/use_kibana'; import queryRulesImg from '../../assets/query-rules-context-alt.svg'; import backgroundPanelImg from '../../assets/query-rule-panel-background.svg'; +import { AnalyticsEvents } from '../../analytics/constants'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; interface EmptyPromptProps { getStartedAction: () => void; } export const EmptyPrompt: React.FC = ({ getStartedAction }) => { + const usageTracker = useUsageTracker(); const { application, share, console: consolePlugin } = useKibana().services; const { euiTheme } = useEuiTheme(); + + useEffect(() => { + usageTracker?.load(AnalyticsEvents.emptyPromptLoaded); + }, [usageTracker]); + const gradientOverlay = css({ background: `linear-gradient(180deg, ${transparentize( euiTheme.colors.backgroundBasePlain, @@ -99,7 +107,10 @@ export const EmptyPrompt: React.FC = ({ getStartedAction }) => data-test-subj="searchQueryRulesEmptyPromptGetStartedButton" color="primary" fill - onClick={getStartedAction} + onClick={() => { + usageTracker?.click(AnalyticsEvents.gettingStartedButtonClicked); + getStartedAction(); + }} > = ({ getStartedAction }) => defaultMessage: 'Create in Console', })} showIcon + onClick={() => { + usageTracker?.click(AnalyticsEvents.createInConsoleClicked); + }} /> diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/error_prompt/error_prompt.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/error_prompt/error_prompt.tsx index 8cba31940be83..c0d677f2d9cf9 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/error_prompt/error_prompt.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/error_prompt/error_prompt.tsx @@ -5,10 +5,12 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { AnalyticsEvents } from '../../analytics/constants'; const ERROR_MESSAGES = { notFound: { @@ -19,6 +21,7 @@ const ERROR_MESSAGES = { defaultMessage="Requested resource was not found. Check if the URL is correct." /> ), + analyticsEvent: AnalyticsEvents.notFoundErrorPromptLoaded, }, generic: { title: , @@ -28,6 +31,7 @@ const ERROR_MESSAGES = { defaultMessage="An error occurred while fetching query rules. Check Kibana logs for more information." /> ), + analyticsEvent: AnalyticsEvents.genericErrorPromptLoaded, }, missingPermissions: { title: ( @@ -42,12 +46,17 @@ const ERROR_MESSAGES = { defaultMessage="You do not have the necessary permissions to manage query rules. Contact your system administrator." /> ), + analyticsEvent: AnalyticsEvents.missingPermissionsErrorPromptLoaded, }, }; export const ErrorPrompt: React.FC<{ errorType: 'missingPermissions' | 'generic' | 'notFound'; }> = ({ errorType }) => { + const useTracker = useUsageTracker(); + useEffect(() => { + useTracker?.load?.(ERROR_MESSAGES[errorType].analyticsEvent); + }, [errorType, useTracker]); return ( { - const { - data: queryRulesData, - isInitialLoading, - isError, - error, - refetch, - } = useFetchQueryRulesSets(); + const usageTracker = useUsageTracker(); - useEffect(() => { - const interval = setInterval(() => { - refetch(); - }, 1000); - return () => clearInterval(interval); - }, [refetch]); + const { data: queryRulesData, isInitialLoading, isError, error } = useFetchQueryRulesSets(); const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); + const backgroundProps = css({ backgroundImage: `url(${queryRulesBackground})`, backgroundSize: 'contain', @@ -56,6 +48,7 @@ export const QueryRulesOverview = () => { alignContent: 'center', backgroundPosition: 'center center', }); + return ( {!isInitialLoading && !isError && queryRulesData?._meta.totalItemCount !== 0 && ( @@ -90,6 +83,7 @@ export const QueryRulesOverview = () => { fill iconType="plusInCircle" onClick={() => { + usageTracker?.click(AnalyticsEvents.addRulesetClicked); setIsCreateModalVisible(true); }} > @@ -138,6 +132,7 @@ export const QueryRulesOverview = () => { { + usageTracker?.click(AnalyticsEvents.gettingStartedButtonClicked); setIsCreateModalVisible(true); }} /> 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 1b6aaa8457fd1..f0f8e083b039d 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 @@ -16,6 +16,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useDeleteRuleset } from '../../hooks/use_delete_query_rules_ruleset'; +import { AnalyticsEvents } from '../../analytics/constants'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; export interface DeleteRulesetModalProps { rulesetId: string; @@ -30,9 +32,11 @@ export const DeleteRulesetModal = ({ }: DeleteRulesetModalProps) => { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const useTracker = useUsageTracker(); const onSuccess = () => { setIsLoading(false); closeDeleteModal(); + useTracker?.load(AnalyticsEvents.rulesetDeleted); if (onSuccessAction) { onSuccessAction(); } diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets.tsx index b0b17de0322a0..729a650ceb647 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { QueryRulesListRulesetsQueryRulesetListItem } from '@elastic/elasticsearch/lib/api/types'; import { @@ -24,8 +24,11 @@ import { useQueryRulesSetsTableData } from '../../hooks/use_query_rules_sets_tab import { QueryRulesSetsSearch } from './query_rules_sets_search'; import { DeleteRulesetModal } from './delete_ruleset_modal'; import { UseRunQueryRuleset } from '../../hooks/use_run_query_ruleset'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { AnalyticsEvents } from '../../analytics/constants'; export const QueryRulesSets = () => { + const useTracker = useUsageTracker(); const { services: { application, http }, } = useKibana(); @@ -43,6 +46,10 @@ export const QueryRulesSets = () => { pageSize ); + useEffect(() => { + useTracker?.load?.(AnalyticsEvents.rulesetListPageLoaded); + }, [useTracker]); + if (!queryRulesData) { return null; } @@ -57,6 +64,7 @@ export const QueryRulesSets = () => { { + useTracker?.click?.(AnalyticsEvents.editRulesetInlineNameClicked); application.navigateToUrl( http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}/ruleset/${name}`) ); @@ -86,6 +94,9 @@ export const QueryRulesSets = () => { content={i18n.translate('xpack.queryRules.queryRulesSetTable.actions.run', { defaultMessage: 'Test in Console', })} + onClick={() => { + useTracker?.click?.(AnalyticsEvents.testRulesetInlineDropdownClicked); + }} /> ); }, @@ -102,10 +113,12 @@ export const QueryRulesSets = () => { icon: 'pencil', color: 'text', type: 'icon', - onClick: (ruleset: QueryRulesListRulesetsQueryRulesetListItem) => + onClick: (ruleset: QueryRulesListRulesetsQueryRulesetListItem) => { + useTracker?.click?.(AnalyticsEvents.editRulesetInlineDropdownClicked); application.navigateToUrl( http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}/ruleset/${ruleset.ruleset_id}`) - ), + ); + }, }, { name: i18n.translate('xpack.queryRules.queryRulesSetTable.actions.delete', { @@ -121,6 +134,7 @@ export const QueryRulesSets = () => { type: 'icon', isPrimary: true, onClick: (ruleset: QueryRulesListRulesetsQueryRulesetListItem) => { + useTracker?.click?.(AnalyticsEvents.deleteRulesetInlineDropdownClicked); setRulesetToDelete(ruleset.ruleset_id); }, }, diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets_search.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets_search.tsx index 502f2c4c0869d..db8b06732478d 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets_search.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_rules_sets/query_rules_sets_search.tsx @@ -7,6 +7,8 @@ import { EuiFieldSearch } from '@elastic/eui'; import React, { useCallback } from 'react'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { AnalyticsEvents } from '../../analytics/constants'; interface QueryRulesSetsSearchProps { searchKey: string; @@ -17,12 +19,15 @@ export const QueryRulesSetsSearch: React.FC = ({ searchKey, setSearchKey, }) => { + const useTracker = useUsageTracker(); const onSearch = useCallback( (newSearch: string) => { + useTracker?.load?.(AnalyticsEvents.rulesetSearched); + const trimSearch = newSearch.trim(); setSearchKey(trimSearch); }, - [setSearchKey] + [setSearchKey, useTracker] ); return ( diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx index 198b57efbddd6..c4b54a47b9eeb 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx @@ -16,6 +16,8 @@ import { QueryRuleFlyout } from './query_rule_flyout/query_rule_flyout'; import { useGenerateRuleId } from '../../hooks/use_generate_rule_id'; import { SearchQueryRulesQueryRule } from '../../types'; import { RulesetDetailEmptyPrompt } from '../empty_prompt/ruleset_detail_empty_prompt'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { AnalyticsEvents } from '../../analytics/constants'; interface QueryRuleDetailPanelProps { rules: SearchQueryRulesQueryRule[]; @@ -46,6 +48,8 @@ export const QueryRuleDetailPanel: React.FC = ({ const [ruleIdToEdit, setRuleIdToEdit] = React.useState(null); const [flyoutMode, setFlyoutMode] = React.useState<'create' | 'edit'>('edit'); + const useTracker = useUsageTracker(); + const { mutate: generateRuleId } = useGenerateRuleId(rulesetId); useEffect(() => { if (createMode && rules.length === 0) { @@ -94,6 +98,7 @@ export const QueryRuleDetailPanel: React.FC = ({ color="primary" data-test-subj="queryRulesetDetailAddRuleButton" onClick={() => { + useTracker?.click(AnalyticsEvents.addRuleClicked); generateRuleId(undefined, { onSuccess: (newRuleId) => { setFlyoutMode('create'); diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list.tsx index 512b115efd23b..14eca3bb83cb6 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list.tsx @@ -27,12 +27,14 @@ import { css } from '@emotion/react'; import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { useUsageTracker } from '../../../hooks/use_usage_tracker'; import { SearchQueryRulesQueryRule } from '../../../../common/types'; import { DroppableContainer } from '../styles'; import { QueryRuleDraggableListHeader } from './query_rule_draggable_list_header'; import { QueryRuleDraggableListItemActionTypeBadge } from './query_rule_draggable_item_action_type_badge'; import { QueryRuleDraggableItemCriteriaDisplay } from './query_rule_draggable_item_criteria_display'; import { DeleteRulesetRuleModal } from './delete_ruleset_rule_modal'; +import { AnalyticsEvents } from '../../../analytics/constants'; export interface QueryRuleDraggableListItemProps { rules: SearchQueryRulesQueryRule[]; @@ -60,6 +62,7 @@ export const QueryRuleDraggableListItem: React.FC { const { euiTheme } = useEuiTheme(); + const useTracker = useUsageTracker(); const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const localTourTargetRef = useRef(null); const effectiveRef = tourInfo?.tourTargetRef || localTourTargetRef; @@ -174,6 +177,7 @@ export const QueryRuleDraggableListItem: React.FC { + useTracker?.click(AnalyticsEvents.editRuleClicked); onEditRuleFlyoutOpen(queryRule.rule_id); closePopover(); }} @@ -204,6 +208,7 @@ export const QueryRuleDraggableListItem: React.FC { + useTracker?.click(AnalyticsEvents.deleteRuleClicked); setRuleToDelete(queryRule.rule_id); closePopover(); }} @@ -263,11 +268,13 @@ export const QueryRuleDraggableList: React.FC = ({ tourInfo, }) => { const { euiTheme } = useEuiTheme(); + const useTracker = useUsageTracker(); return ( { if (source && destination) { + useTracker?.load(AnalyticsEvents.rulesReordered); const items = euiDragDropReorder(rules, source.index, destination.index); onReorder(items); } 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 f4656cd7b6735..a81f1fd188f17 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 @@ -40,6 +40,8 @@ import { DeleteRulesetModal } from '../query_rules_sets/delete_ruleset_modal'; import { QueryRuleDetailPanel } from './query_rule_detail_panel'; import { useQueryRulesetDetailState } from './use_query_ruleset_detail_state'; import { useFetchQueryRulesetExist } from '../../hooks/use_fetch_ruleset_exists'; +import { AnalyticsEvents } from '../../analytics/constants'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; export interface QueryRulesetDetailProps { createMode?: boolean; @@ -55,6 +57,7 @@ export const QueryRulesetDetail: React.FC = ({ createMo }>(); const { data: rulesetExists, isLoading: isFailsafeLoading } = useFetchQueryRulesetExist(rulesetId); + const useTracker = useUsageTracker(); useEffect(() => { // This is a failsafe in case user navigates to an existing ruleset via URL directly @@ -63,6 +66,10 @@ export const QueryRulesetDetail: React.FC = ({ createMo } }, [createMode, rulesetExists, application, http.basePath, rulesetId]); + useEffect(() => { + useTracker?.load?.(AnalyticsEvents.rulesetDetailPageLoaded); + }, [useTracker]); + const blockRender = (createMode && rulesetExists) || isFailsafeLoading; const { mutate: createRuleset } = usePutRuleset(() => { @@ -186,6 +193,7 @@ export const QueryRulesetDetail: React.FC = ({ createMo const handleSave = () => { setIsFormDirty(false); + useTracker?.click(createMode ? AnalyticsEvents.rulesetCreated : AnalyticsEvents.rulesetUpdated); createRuleset({ rulesetId, forceWrite: true, @@ -232,8 +240,10 @@ export const QueryRulesetDetail: React.FC = ({ createMo ), color: 'primary', 'aria-current': false, - onClick: () => - application.navigateToUrl(http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}`)), + onClick: () => { + useTracker?.click(AnalyticsEvents.backToRulesetListClicked); + application.navigateToUrl(http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}`)); + }, }, ]} restrictWidth @@ -331,7 +341,10 @@ export const QueryRulesetDetail: React.FC = ({ createMo content={i18n.translate('xpack.queryRules.queryRulesetDetail.testButton', { defaultMessage: 'Test in Console', })} - onClick={finishTour} + onClick={() => { + useTracker?.click(AnalyticsEvents.testInConsoleClicked); + finishTour(); + }} /> diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_rules_sets.ts b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_rules_sets.ts index 03c24002362f3..25f974793bd20 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_rules_sets.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_rules_sets.ts @@ -31,5 +31,6 @@ export const useFetchQueryRulesSets = (page: Page = DEFAULT_PAGE_VALUE) => { ); }, retry: false, + refetchInterval: 2000, }); }; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_usage_tracker.ts b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_usage_tracker.ts new file mode 100644 index 0000000000000..e2154a5ba511c --- /dev/null +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_usage_tracker.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { useKibana } from './use_kibana'; + +const APP_TRACKER_NAME = 'search_query_rules'; + +export const useUsageTracker = () => { + const { + services: { usageCollection }, + } = useKibana(); + + return useMemo(() => { + if (usageCollection) { + return { + click: usageCollection.reportUiCounter.bind( + usageCollection, + APP_TRACKER_NAME, + METRIC_TYPE.CLICK + ), + count: usageCollection.reportUiCounter.bind( + usageCollection, + APP_TRACKER_NAME, + METRIC_TYPE.COUNT + ), + load: usageCollection.reportUiCounter.bind( + usageCollection, + APP_TRACKER_NAME, + METRIC_TYPE.LOADED + ), + }; + } + }, [usageCollection]); +}; diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/types.ts b/x-pack/solutions/search/plugins/search_query_rules/public/types.ts index 059bc65b164d2..5471481c15ae8 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/types.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/public/types.ts @@ -9,6 +9,7 @@ import { SearchNavigationPluginStart } from '@kbn/search-navigation/public'; import { AppMountParameters, CoreStart } from '@kbn/core/public'; import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; +import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; export * from '../common/types'; export interface AppPluginStartDependencies { @@ -16,6 +17,7 @@ export interface AppPluginStartDependencies { console?: ConsolePluginStart; share?: SharePluginStart; searchNavigation?: SearchNavigationPluginStart; + usageCollection?: UsageCollectionStart; } export type AppServicesContext = CoreStart & AppPluginStartDependencies; From 864cf5e3506d60058cfacae279bacb3b98a881f9 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:07:04 +0000 Subject: [PATCH 5/7] [CI] Auto-commit changed files from 'node scripts/styled_components_mapping' --- .../solutions/search/plugins/search_query_rules/tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/solutions/search/plugins/search_query_rules/tsconfig.json b/x-pack/solutions/search/plugins/search_query_rules/tsconfig.json index f88604a80ec6b..78bfded065b66 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/tsconfig.json +++ b/x-pack/solutions/search/plugins/search_query_rules/tsconfig.json @@ -30,6 +30,8 @@ "@kbn/search-index-documents", "@kbn/unsaved-changes-prompt", "@kbn/deeplinks-analytics", + "@kbn/analytics", + "@kbn/usage-collection-plugin", ], "exclude": [ "target/**/*", From 2105c19c27c9b2f4d2beecae64239e81c94b9233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Wed, 25 Jun 2025 14:29:54 +0200 Subject: [PATCH 6/7] Review changes --- .../search_query_rules/public/analytics/constants.ts | 6 +++--- .../public/components/empty_prompt/empty_prompt.tsx | 4 +--- .../components/query_rules_sets/delete_ruleset_modal.tsx | 4 ---- .../query_rule_draggable_list/query_rule_draggable_list.tsx | 2 +- .../query_rule_flyout/use_query_rule_flyout_state.ts | 4 ++++ .../query_ruleset_detail/query_ruleset_detail.tsx | 4 +++- .../public/hooks/use_fetch_query_rules_sets.ts | 1 - 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/analytics/constants.ts b/x-pack/solutions/search/plugins/search_query_rules/public/analytics/constants.ts index bea8899e6a582..0630cbeed7e1d 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/analytics/constants.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/public/analytics/constants.ts @@ -16,9 +16,8 @@ export enum AnalyticsEvents { notFoundErrorPromptLoaded = 'not_found_error_prompt_loaded', missingPermissionsErrorPromptLoaded = 'missing_permissions_error_prompt_loaded', - rulesetDeleted = 'ruleset_deleted', - rulesetCreated = 'ruleset_created', - rulesetUpdated = 'ruleset_updated', + rulesetCreateClicked = 'ruleset_create_clicked', + rulesetUpdateClicked = 'ruleset_update_clicked', // ruleset detail page actions rulesetDetailPageLoaded = 'ruleset_details_page_loaded', @@ -30,6 +29,7 @@ export enum AnalyticsEvents { backToRulesetListClicked = 'back_to_ruleset_list_clicked', rulesReordered = 'rules_reordered', + ruleFlyoutDocumentsReordered = 'rule_flyout_documents_reordered', // ruleset list page actions editRulesetInlineNameClicked = 'edit_ruleset_inline_name_clicked', diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/empty_prompt/empty_prompt.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/empty_prompt/empty_prompt.tsx index 4ac009a9e3b47..0fc16616fd131 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/empty_prompt/empty_prompt.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/empty_prompt/empty_prompt.tsx @@ -251,9 +251,7 @@ export const EmptyPrompt: React.FC = ({ getStartedAction }) => defaultMessage: 'Create in Console', })} showIcon - onClick={() => { - usageTracker?.click(AnalyticsEvents.createInConsoleClicked); - }} + data-test-subj={AnalyticsEvents.createInConsoleClicked} /> 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 f0f8e083b039d..1b6aaa8457fd1 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 @@ -16,8 +16,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useDeleteRuleset } from '../../hooks/use_delete_query_rules_ruleset'; -import { AnalyticsEvents } from '../../analytics/constants'; -import { useUsageTracker } from '../../hooks/use_usage_tracker'; export interface DeleteRulesetModalProps { rulesetId: string; @@ -32,11 +30,9 @@ export const DeleteRulesetModal = ({ }: DeleteRulesetModalProps) => { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); - const useTracker = useUsageTracker(); const onSuccess = () => { setIsLoading(false); closeDeleteModal(); - useTracker?.load(AnalyticsEvents.rulesetDeleted); if (onSuccessAction) { onSuccessAction(); } diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list.tsx index 14eca3bb83cb6..2a30df2881451 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list.tsx +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list.tsx @@ -274,7 +274,7 @@ export const QueryRuleDraggableList: React.FC = ({ { if (source && destination) { - useTracker?.load(AnalyticsEvents.rulesReordered); + useTracker?.click(AnalyticsEvents.rulesReordered); const items = euiDragDropReorder(rules, source.index, destination.index); onReorder(items); } diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.ts b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.ts index 13ddffe994dfd..cd231b04c8259 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_flyout/use_query_rule_flyout_state.ts @@ -10,9 +10,11 @@ import { useEffect, useState } from 'react'; import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; import { OnDragEndResponder } from '@hello-pangea/dnd'; import { euiDragDropReorder } from '@elastic/eui'; +import { AnalyticsEvents } from '../../../analytics/constants'; import { QueryRuleEditorForm, SearchQueryRulesQueryRule } from '../../../../common/types'; import { useFetchIndexNames } from '../../../hooks/use_fetch_index_names'; import { isCriteriaAlways } from '../../../utils/query_rules_utils'; +import { useUsageTracker } from '../../../hooks/use_usage_tracker'; export const createEmptyRuleset = ( rulesetId: QueryRulesQueryRuleset['ruleset_id'] @@ -39,6 +41,7 @@ export const useQueryRuleFlyoutState = ({ setIsFormDirty, onSave, }: UseQueryRuleFlyoutStateProps) => { + const usageTracker = useUsageTracker(); const { control, getValues, reset, setValue, formState, trigger } = useFormContext(); const { @@ -242,6 +245,7 @@ export const useQueryRuleFlyoutState = ({ const shouldShowCriteriaCallout = criteriaCalloutActive && !isAlways; const dragEndHandle: OnDragEndResponder = ({ source, destination }) => { + usageTracker?.click(AnalyticsEvents.ruleFlyoutDocumentsReordered); if (source && destination && (ruleFromRuleset || createMode)) { if (isDocRule) { const newActions = euiDragDropReorder(actionFields, source.index, destination.index); 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 a81f1fd188f17..cb5346b13007f 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 @@ -193,7 +193,9 @@ export const QueryRulesetDetail: React.FC = ({ createMo const handleSave = () => { setIsFormDirty(false); - useTracker?.click(createMode ? AnalyticsEvents.rulesetCreated : AnalyticsEvents.rulesetUpdated); + useTracker?.click( + createMode ? AnalyticsEvents.rulesetCreateClicked : AnalyticsEvents.rulesetUpdateClicked + ); createRuleset({ rulesetId, forceWrite: true, diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_rules_sets.ts b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_rules_sets.ts index 25f974793bd20..03c24002362f3 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_rules_sets.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_rules_sets.ts @@ -31,6 +31,5 @@ export const useFetchQueryRulesSets = (page: Page = DEFAULT_PAGE_VALUE) => { ); }, retry: false, - refetchInterval: 2000, }); }; From 6dffe632293b74b51fc64160d9e12323e13ca597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Wed, 25 Jun 2025 14:31:16 +0200 Subject: [PATCH 7/7] Review changes --- .../solutions/search/plugins/search_query_rules/public/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/types.ts b/x-pack/solutions/search/plugins/search_query_rules/public/types.ts index 5471481c15ae8..0376f756bc472 100644 --- a/x-pack/solutions/search/plugins/search_query_rules/public/types.ts +++ b/x-pack/solutions/search/plugins/search_query_rules/public/types.ts @@ -9,7 +9,7 @@ import { SearchNavigationPluginStart } from '@kbn/search-navigation/public'; import { AppMountParameters, CoreStart } from '@kbn/core/public'; import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; -import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; export * from '../common/types'; export interface AppPluginStartDependencies {