Skip to content

[Search][Query Rules UI] Test in console and delete ruleset from details page #221350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const [isLoading, setIsLoading] = useState(false);

const onSuccess = () => {
setIsLoading(false);
closeDeleteModal();
if (onSuccessProp) {
onSuccessProp();
}
};

const confirmCheckboxId = useGeneratedHtmlId({
prefix: 'confirmCheckboxId',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ describe('Query rule detail', () => {
const TEST_IDS = {
DetailPage: 'queryRulesetDetailPage',
DetailPageHeader: 'queryRulesetDetailHeader',
HeaderDataButton: 'queryRulesetDetailHeaderDataButton',
HeaderSaveButton: 'queryRulesetDetailHeaderSaveButton',
AddRuleButton: 'queryRulesetDetailAddRuleButton',
DraggableItem: 'searchQueryRulesDraggableItem',
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@
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,
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';
Expand All @@ -20,6 +30,8 @@ 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';
import { DeleteRulesetModal } from '../query_rules_sets/delete_ruleset_modal';

export const QueryRulesetDetail: React.FC = () => {
const {
Expand All @@ -38,6 +50,31 @@ export const QueryRulesetDetail: React.FC = () => {

const [rules, setRules] = useState<QueryRulesQueryRule[]>(queryRulesetData?.rules ?? []);

const [isPopoverOpen, setPopover] = useState(false);
const splitButtonPopoverId = useGeneratedHtmlId({
prefix: 'splitButtonPopover',
});

const onButtonClick = () => {
setPopover(!isPopoverOpen);
};

const closePopover = () => {
setPopover(false);
};
const items = [
<EuiContextMenuItem
color="danger"
key="delete"
icon="trash"
onClick={() => setRulesetToDelete(rulesetId)}
>
Delete ruleset
</EuiContextMenuItem>,
];

const [rulesetToDelete, setRulesetToDelete] = useState<string | null>(null);

useEffect(() => {
if (queryRulesetData?.rules) {
setRules(queryRulesetData.rules);
Expand Down Expand Up @@ -72,40 +109,68 @@ export const QueryRulesetDetail: React.FC = () => {
rightSideItems={[
<EuiFlexGroup alignItems="center" key="queryRulesetDetailHeaderButtons">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="database"
color="primary"
data-test-subj="queryRulesetDetailHeaderDataButton"
onClick={() => {
// Logic to handle data button click
}}
>
<FormattedMessage
id="xpack.queryRules.queryRulesetDetail.dataButton"
defaultMessage="Data"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
iconType="save"
fill
color="primary"
data-test-subj="queryRulesetDetailHeaderSaveButton"
onClick={() => {
// Logic to save the query ruleset
}}
>
<FormattedMessage
id="xpack.queryRules.queryRulesetDetail.saveButton"
defaultMessage="Save"
/>
</EuiButton>
<UseRunQueryRuleset
rulesetId={rulesetId}
type="contextMenuItem"
content={i18n.translate('xpack.queryRules.queryRulesetDetail.testButton', {
defaultMessage: 'Test in Console',
})}
/>
</EuiFlexItem>
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
iconType="save"
fill
color="primary"
data-test-subj="queryRulesetDetailHeaderSaveButton"
onClick={() => {
// Logic to save the query ruleset
}}
>
<FormattedMessage
id="xpack.queryRules.queryRulesetDetail.saveButton"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
id={splitButtonPopoverId}
button={
<EuiButtonIcon
data-test-subj="searchQueryRulesQueryRulesetDetailButton"
display="fill"
size="m"
iconType="boxesVertical"
aria-label="More"
onClick={onButtonClick}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel size="s" items={items} />
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>,
]}
/>
)}
{rulesetToDelete && (
<DeleteRulesetModal
rulesetId={rulesetToDelete}
closeDeleteModal={() => {
setRulesetToDelete(null);
}}
onSuccess={() => {
application.navigateToUrl(http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}`));
}}
/>
)}
{!isError && <QueryRuleDetailPanel rules={rules} setRules={setRules} />}
{isError && (
<ErrorPrompt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ describe('UseRunQueryRuleset', () => {
console: mockConsole,
},
});

// Default mock for ruleset data
(useFetchQueryRuleset as jest.Mock).mockReturnValue({
data: {
rules: [
Expand All @@ -53,6 +55,13 @@ describe('UseRunQueryRuleset', () => {
},
],
},
criteria: [
{
metadata: 'user_query',
values: ['test-query'],
type: 'exact',
},
],
},
],
},
Expand All @@ -76,9 +85,15 @@ 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 the request contains the expected query
expect(buttonProps.request).toContain('"query": "pugs"');
// Verify ruleset_ids are included
expect(buttonProps.request).toContain('test-ruleset');
});

it('renders with custom type and content', () => {
Expand Down Expand Up @@ -107,11 +122,86 @@ describe('UseRunQueryRuleset', () => {
expect(buttonProps.request).toContain('my_index');
});

it('creates correct query with ruleset ID in the request', () => {
const rulesetId = 'special-test-ruleset';
render(<UseRunQueryRuleset rulesetId={rulesetId} />);
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(<UseRunQueryRuleset rulesetId="test-ruleset" />);

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(<UseRunQueryRuleset rulesetId="test-ruleset" />);

const buttonProps = (TryInConsoleButton as jest.Mock).mock.calls[0][0];
expect(buttonProps.request).toContain('"user_query": "search term"');
expect(buttonProps.request).toMatch(/"user_location":\s*\[\s*"US",\s*"UK"\s*\]/);
});

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(<UseRunQueryRuleset rulesetId="test-ruleset" />);

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).toMatch(/"another_field":\s*\[\s*"array",\s*"of",\s*"values"\s*\]/);
});
});
Loading