Repository: elastic/kibana
Pull request #217851: Add Query Rules Detail Page with Draggable list
Author: login: efegurkan
Labels: name: release_note:skip
description: Skip the PR/issue when compiling release notes
name: Team:Search
name: backport:version
description: Backport to applied version labels
name: v9.1.0
name: v8.19.0
Created: 2025-04-10T14:02:48Z | Merged: 2025-04-25T15:33:43Z

## Summary

**What is implemented?**

Add Query Rules detail page.
Draggable list implementation.
Updated navigation link from rulesets overview page.
Added some test coverage.

**What is not implemented**

Add rule button, action icon for rule, Data and Save buttons does nothing as the flows are not implemented yet. They will come as separate PRs.



https://github.com/user-attachments/assets/094b705d-9c3a-4d31-a2f0-8256bac25e90

<img width="1408" alt="Screenshot 2025-04-14 at 11 25 34" src="https://github.com/user-attachments/assets/2bf42d4b-e41f-4336-adec-6c51f2248d57" />


### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
- [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed
- [x] The PR  description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)


<!--ONMERGE {"backportTargets":["8.19"]} ONMERGE-->


Diff:
diff --git a/x-pack/solutions/search/plugins/search_query_rules/common/__fixtures__/query_rules_ruleset.ts b/x-pack/solutions/search/plugins/search_query_rules/common/__fixtures__/query_rules_ruleset.ts
new file mode 100644
index 0000000000000..84968a0910786
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/common/__fixtures__/query_rules_ruleset.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 const MOCK_QUERY_RULESET_RESPONSE_FIXTURE = {
+  ruleset_id: 'my-ruleset',
+  rules: [
+    {
+      rule_id: 'rule1',
+      type: 'pinned',
+      criteria: [
+        {
+          type: 'fuzzy',
+          metadata: 'query_string',
+          values: ['puggles', 'pugs'],
+        },
+        {
+          type: 'exact',
+          metadata: 'user_country',
+          values: ['us'],
+        },
+      ],
+      actions: {
+        ids: ['id1', 'id2'],
+      },
+    },
+    {
+      rule_id: 'rule2',
+      type: 'exclude',
+      criteria: [
+        {
+          type: 'contains',
+          metadata: 'query_string',
+          values: ['beagles'],
+        },
+      ],
+      actions: {
+        docs: [
+          {
+            _index: 'my-index-000001',
+            _id: 'id3',
+          },
+          {
+            _index: 'my-index-000002',
+            _id: 'id4',
+          },
+        ],
+      },
+    },
+  ],
+};
diff --git a/x-pack/solutions/search/plugins/search_query_rules/common/api_routes.ts b/x-pack/solutions/search/plugins/search_query_rules/common/api_routes.ts
index 3422af42dbbca..a632cfa07a827 100644
--- a/x-pack/solutions/search/plugins/search_query_rules/common/api_routes.ts
+++ b/x-pack/solutions/search/plugins/search_query_rules/common/api_routes.ts
@@ -9,4 +9,5 @@ export const PLUGIN_ROUTE_ROOT = '/app/elasticsearch/query_rules';
 
 export enum APIRoutes {
   QUERY_RULES_SETS = '/internal/search_query_rules/query_rules_sets',
+  QUERY_RULES_RULESET_FETCH = '/internal/search_query_rules/ruleset/{ruleset_id}',
 }
diff --git a/x-pack/solutions/search/plugins/search_query_rules/common/constants.ts b/x-pack/solutions/search/plugins/search_query_rules/common/constants.ts
index 04f223517af2a..21f4e261a9611 100644
--- a/x-pack/solutions/search/plugins/search_query_rules/common/constants.ts
+++ b/x-pack/solutions/search/plugins/search_query_rules/common/constants.ts
@@ -8,6 +8,7 @@
 import dedent from 'dedent';
 
 export const QUERY_RULES_SETS_QUERY_KEY = 'query-rules-sets-fetch';
+export const QUERY_RULES_QUERY_RULESET_FETCH_KEY = 'query-rules-ruleset-fetch';
 export const CREATE_QUERY_RULE_SET_API_SNIPPET = dedent`# Create or update a query ruleset
 PUT /_query_rules/my-ruleset
 {
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 73d1d0fe99ef6..8cba31940be83 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
@@ -11,6 +11,15 @@ import { EuiEmptyPrompt } from '@elastic/eui';
 import { FormattedMessage } from '@kbn/i18n-react';
 
 const ERROR_MESSAGES = {
+  notFound: {
+    title: <FormattedMessage id="xpack.queryRules.notFoundTitle" defaultMessage="Not found" />,
+    body: (
+      <FormattedMessage
+        id="xpack.queryRuleset.notFoundDescription"
+        defaultMessage="Requested resource was not found. Check if the URL is correct."
+      />
+    ),
+  },
   generic: {
     title: <FormattedMessage id="xpack.queryRules.errorTitle" defaultMessage="An error occurred" />,
     body: (
@@ -36,9 +45,9 @@ const ERROR_MESSAGES = {
   },
 };
 
-export const ErrorPrompt: React.FC<{ errorType: 'missingPermissions' | 'generic' }> = ({
-  errorType,
-}) => {
+export const ErrorPrompt: React.FC<{
+  errorType: 'missingPermissions' | 'generic' | 'notFound';
+}> = ({ errorType }) => {
   return (
     <EuiEmptyPrompt
       iconType="logoElasticsearch"
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/overview/overview.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/overview/overview.tsx
index 020b12c46e116..fbbc500cf0d1c 100644
--- a/x-pack/solutions/search/plugins/search_query_rules/public/components/overview/overview.tsx
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/overview/overview.tsx
@@ -5,7 +5,7 @@
  * 2.0.
  */
 
-import React, { useMemo } from 'react';
+import React from 'react';
 
 import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
 import {
@@ -19,24 +19,17 @@ import {
 import { FormattedMessage } from '@kbn/i18n-react';
 import { css } from '@emotion/react';
 import { docLinks } from '../../../common/doc_links';
-import { useKibana } from '../../hooks/use_kibana';
 import { useFetchQueryRulesSets } from '../../hooks/use_fetch_query_rules_sets';
 import { EmptyPrompt } from '../empty_prompt/empty_prompt';
 import { ErrorPrompt } from '../error_prompt/error_prompt';
 import { isPermissionError } from '../../utils/query_rules_utils';
 import queryRulesBackground from '../../assets/query-rule-background.svg';
 import { QueryRulesSets } from '../query_rules_sets/query_rules_sets';
+import { QueryRulesPageTemplate } from '../../layout/query_rules_page_template';
 
 export const QueryRulesOverview = () => {
-  const {
-    services: { console: consolePlugin, history, searchNavigation },
-  } = useKibana();
   const { data: queryRulesData, isInitialLoading, isError, error } = useFetchQueryRulesSets();
 
-  const embeddableConsole = useMemo(
-    () => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
-    [consolePlugin]
-  );
   const backgroundProps = css({
     backgroundImage: `url(${queryRulesBackground})`,
     backgroundSize: 'contain',
@@ -48,21 +41,14 @@ export const QueryRulesOverview = () => {
     backgroundPosition: 'center center',
   });
   return (
-    <KibanaPageTemplate
-      offset={0}
-      restrictWidth={false}
-      grow={false}
-      data-test-subj="queryRulesOverviewPage"
-      solutionNav={searchNavigation?.useClassicNavigation(history)}
-      color="primary"
-    >
+    <QueryRulesPageTemplate restrictWidth={false}>
       {!isInitialLoading && !isError && queryRulesData?._meta.totalItemCount !== 0 && (
         <KibanaPageTemplate.Header
           pageTitle="Query Rules"
           restrictWidth
           color="primary"
           rightSideItems={[
-            <EuiFlexGroup alignItems="center">
+            <EuiFlexGroup alignItems="center" key="queryRulesOverviewHeaderButtons">
               <EuiFlexItem grow={false}>
                 <EuiLink
                   data-test-subj="queryRulesOverviewApiDocumentationLink"
@@ -130,7 +116,6 @@ export const QueryRulesOverview = () => {
           </EuiFlexGroup>
         )}
       </KibanaPageTemplate.Section>
-      {embeddableConsole}
-    </KibanaPageTemplate>
+    </QueryRulesPageTemplate>
   );
 };
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 7d31c4b66804f..d7a855f30177e 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
@@ -16,12 +16,17 @@ import {
   EuiLink,
 } from '@elastic/eui';
 import { i18n } from '@kbn/i18n';
+import { PLUGIN_ROUTE_ROOT } from '../../../common/api_routes';
 import { DEFAULT_PAGE_VALUE, paginationToPage } from '../../../common/pagination';
 import { useFetchQueryRulesSets } from '../../hooks/use_fetch_query_rules_sets';
+import { useKibana } from '../../hooks/use_kibana';
 import { useQueryRulesSetsTableData } from '../../hooks/use_query_rules_sets_table_data';
 import { QueryRulesSetsSearch } from './query_rules_sets_search';
 
 export const QueryRulesSets = () => {
+  const {
+    services: { application, http },
+  } = useKibana();
   const [pageIndex, setPageIndex] = useState(0);
   const [pageSize, setPageSize] = useState(DEFAULT_PAGE_VALUE.size);
   const [searchKey, setSearchKey] = useState('');
@@ -49,7 +54,9 @@ export const QueryRulesSets = () => {
         <EuiLink
           data-test-subj="queryRuleSetName"
           onClick={() => {
-            // Navigate to the ruleset details page
+            application.navigateToUrl(
+              http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}/ruleset/${name}`)
+            );
           }}
         >
           {name}
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
new file mode 100644
index 0000000000000..10cda9c499c86
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem, EuiButton, EuiText } from '@elastic/eui';
+import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types';
+import { QueryRuleDraggableList } from './query_rule_draggable_list/query_rule_draggable_list';
+
+interface QueryRuleDetailPanelProps {
+  rules: QueryRulesQueryRule[];
+  setRules: (rules: QueryRulesQueryRule[]) => void;
+}
+export const QueryRuleDetailPanel: React.FC<QueryRuleDetailPanelProps> = ({ rules, setRules }) => {
+  return (
+    <KibanaPageTemplate.Section restrictWidth>
+      <EuiFlexGroup justifyContent="spaceBetween" direction="column">
+        <EuiFlexItem grow={false}>
+          <EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
+            <EuiFlexItem grow={false}>
+              <EuiFlexGroup alignItems="center">
+                <EuiFlexItem>
+                  <EuiButton
+                    iconType="plusInCircle"
+                    color="primary"
+                    data-test-subj="queryRulesetDetailAddRuleButton"
+                    onClick={() => {
+                      // Logic to add a new rule
+                    }}
+                  >
+                    <FormattedMessage
+                      id="xpack.queryRules.queryRulesetDetail.addRuleButton"
+                      defaultMessage="Add rule"
+                    />
+                  </EuiButton>
+                </EuiFlexItem>
+                <EuiFlexItem>
+                  <EuiText>
+                    <FormattedMessage
+                      id="xpack.queryRules.queryRulesetDetail.ruleCount"
+                      defaultMessage="{ruleCount, plural, one {# rule} other {# rules}}"
+                      values={{ ruleCount: rules.length }}
+                      data-test-subj="queryRulesetDetailRuleCount"
+                    />
+                  </EuiText>
+                </EuiFlexItem>
+              </EuiFlexGroup>
+            </EuiFlexItem>
+          </EuiFlexGroup>
+        </EuiFlexItem>
+        <EuiFlexItem>
+          <QueryRuleDraggableList rules={rules} onReorder={setRules} />
+        </EuiFlexItem>
+      </EuiFlexGroup>
+    </KibanaPageTemplate.Section>
+  );
+};
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_item_action_type_badge.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_item_action_type_badge.tsx
new file mode 100644
index 0000000000000..ef70efe8c4d4d
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_item_action_type_badge.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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 { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types';
+import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiIcon, useEuiTheme } from '@elastic/eui';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+  DocsColumnContainer,
+  ActionTypeIconBadgeContainer,
+  DocumentCountLabelContainer,
+  DocumentCountLabelStyle,
+} from '../styles';
+
+export const QueryRuleDraggableListItemActionTypeBadge: React.FC<{
+  queryRule: QueryRulesQueryRule;
+}> = ({ queryRule }) => {
+  const { euiTheme } = useEuiTheme();
+  return (
+    <EuiFlexGroup
+      responsive={false}
+      alignItems="center"
+      gutterSize="m"
+      justifyContent="center"
+      css={DocsColumnContainer(euiTheme)}
+    >
+      <EuiFlexItem grow={false}>
+        <div css={ActionTypeIconBadgeContainer(euiTheme)}>
+          <EuiBadge iconType={queryRule.type === 'exclude' ? 'eyeClosed' : 'pinFilled'}>
+            {queryRule.type === 'exclude' ? (
+              <FormattedMessage
+                id="xpack.search.queryRulesetDetail.draggableList.excludeLabel"
+                defaultMessage="Exclude"
+              />
+            ) : (
+              <FormattedMessage
+                id="xpack.search.queryRulesetDetail.draggableList.pinLabel"
+                defaultMessage="Pin"
+              />
+            )}
+          </EuiBadge>
+        </div>
+      </EuiFlexItem>
+      <EuiFlexItem grow={false} css={DocumentCountLabelContainer(euiTheme)}>
+        <EuiFlexGroup responsive={false} alignItems="center" gutterSize="s">
+          <EuiFlexItem grow={false}>
+            <EuiIcon type="documents" />
+          </EuiFlexItem>
+          <EuiFlexItem grow={false} css={DocumentCountLabelStyle(euiTheme)}>
+            {queryRule.actions.docs?.length ?? 0}
+          </EuiFlexItem>
+        </EuiFlexGroup>
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+};
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_item_criteria_display.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_item_criteria_display.tsx
new file mode 100644
index 0000000000000..8f4a0078e2c36
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_item_criteria_display.tsx
@@ -0,0 +1,23 @@
+/*
+ * 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 { QueryRulesQueryRuleCriteria } from '@elastic/elasticsearch/lib/api/types';
+import { EuiText, EuiBadge, EuiTextColor, useEuiTheme } from '@elastic/eui';
+import React from 'react';
+
+export const QueryRuleDraggableItemCriteriaDisplay: React.FC<{
+  criteria: QueryRulesQueryRuleCriteria;
+}> = ({ criteria }) => {
+  const { euiTheme } = useEuiTheme();
+  return (
+    <EuiText size="s">
+      <EuiBadge>{criteria.metadata}</EuiBadge>&nbsp;
+      <EuiTextColor color={euiTheme.colors.textPrimary}>{criteria.type}</EuiTextColor>&nbsp;
+      {criteria.values?.join(', ')}
+    </EuiText>
+  );
+};
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
new file mode 100644
index 0000000000000..476eb20d35a85
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list.tsx
@@ -0,0 +1,146 @@
+/*
+ * 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 React from 'react';
+
+import {
+  EuiDragDropContext,
+  EuiDroppable,
+  EuiDraggable,
+  EuiPanel,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiIcon,
+  EuiText,
+  EuiButtonIcon,
+  useEuiTheme,
+  euiDragDropReorder,
+} from '@elastic/eui';
+import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { i18n } from '@kbn/i18n';
+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';
+
+export interface QueryRuleDraggableListProps {
+  rules: QueryRulesQueryRule[];
+  onReorder: (queryRules: QueryRulesQueryRule[]) => void;
+}
+export const QueryRuleDraggableList: React.FC<QueryRuleDraggableListProps> = ({
+  rules,
+  onReorder,
+}) => {
+  const { euiTheme } = useEuiTheme();
+
+  return (
+    <EuiDragDropContext
+      onDragEnd={({ source, destination }) => {
+        if (source && destination) {
+          const items = euiDragDropReorder(rules, source.index, destination.index);
+          onReorder(items);
+        }
+      }}
+    >
+      <>
+        <QueryRuleDraggableListHeader />
+        <EuiDroppable
+          droppableId="queryRuleListDropabble"
+          spacing="m"
+          css={DroppableContainer(euiTheme)}
+          data-test-subj="searchQueryRulesDroppable"
+        >
+          {rules.map((queryRule, index) => (
+            <EuiDraggable
+              spacing="m"
+              key={queryRule.rule_id}
+              index={index}
+              draggableId={queryRule.rule_id}
+              customDragHandle={true}
+              hasInteractiveChildren={true}
+              data-test-subj="searchQueryRulesDraggableItem"
+            >
+              {(provided) => (
+                <EuiPanel paddingSize="s" hasShadow={false}>
+                  <EuiFlexGroup alignItems="center" gutterSize="s">
+                    <EuiFlexItem grow={false}>
+                      <EuiPanel
+                        color="transparent"
+                        paddingSize="s"
+                        {...provided.dragHandleProps}
+                        aria-label="Drag Handle"
+                      >
+                        <EuiIcon type="grab" />
+                      </EuiPanel>
+                    </EuiFlexItem>
+                    <EuiFlexItem grow={true}>
+                      <EuiFlexGroup responsive={false} alignItems="center">
+                        <EuiFlexItem grow={7}>
+                          <EuiFlexGroup direction="column" gutterSize="xs" responsive={false}>
+                            {Array.isArray(queryRule.criteria) ? (
+                              <>
+                                {queryRule.criteria.slice(0, 3).map((criteria, criteriaIndex) => (
+                                  <EuiFlexItem key={criteria.type + criteriaIndex} grow={false}>
+                                    <QueryRuleDraggableItemCriteriaDisplay criteria={criteria} />
+                                  </EuiFlexItem>
+                                ))}
+                                {queryRule.criteria.length > 3 && (
+                                  <EuiFlexItem grow={false}>
+                                    <EuiText size="s">
+                                      <FormattedMessage
+                                        id="xpack.search.queryRulesetDetail.draggableList.queryRulesCriteriaMoreLabel"
+                                        defaultMessage="+{count} more"
+                                        values={{ count: queryRule.criteria.length - 3 }}
+                                      />
+                                    </EuiText>
+                                  </EuiFlexItem>
+                                )}
+                              </>
+                            ) : (
+                              <EuiFlexItem grow={false}>
+                                <QueryRuleDraggableItemCriteriaDisplay
+                                  criteria={queryRule.criteria}
+                                />
+                              </EuiFlexItem>
+                            )}
+                          </EuiFlexGroup>
+                        </EuiFlexItem>
+                        <EuiFlexItem grow={1}>
+                          <QueryRuleDraggableListItemActionTypeBadge queryRule={queryRule} />
+                        </EuiFlexItem>
+                        <EuiFlexItem grow={false}>
+                          <EuiButtonIcon
+                            aria-label={i18n.translate(
+                              'xpack.search.queryRulesetDetail.draggableList.actionsButton.aria.label',
+                              {
+                                defaultMessage: 'Click to reach actions menu for rule {ruleId}',
+                                values: {
+                                  ruleId: queryRule.rule_id,
+                                },
+                              }
+                            )}
+                            data-test-subj="searchQueryRulesQueryRulesetDetailButton"
+                            iconType="boxesHorizontal"
+                            color="text"
+                            onClick={() => {
+                              // Logic to open actions menu
+                            }}
+                          />
+                        </EuiFlexItem>
+                      </EuiFlexGroup>
+                    </EuiFlexItem>
+                  </EuiFlexGroup>
+                </EuiPanel>
+              )}
+            </EuiDraggable>
+          ))}
+        </EuiDroppable>
+      </>
+    </EuiDragDropContext>
+  );
+};
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list_header.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list_header.tsx
new file mode 100644
index 0000000000000..0ff56a659059f
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list/query_rule_draggable_list_header.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+  DraggableListHeader,
+  DocsColumnContainer,
+  ActionHeaderContainer,
+  DocumentCountLabelContainer,
+} from '../styles';
+
+export const QueryRuleDraggableListHeader: React.FC = () => {
+  const { euiTheme } = useEuiTheme();
+  return (
+    <EuiFlexGroup css={DraggableListHeader(euiTheme)}>
+      <EuiFlexItem grow={7}>
+        <EuiText size="xs">
+          <b>
+            <FormattedMessage
+              id="xpack.search.queryRulesetDetail.draggableList.ruleConditionsLabel"
+              defaultMessage="Rule Conditions"
+            />
+          </b>
+        </EuiText>
+      </EuiFlexItem>
+
+      <EuiFlexItem grow={false}>
+        <EuiFlexGroup
+          alignItems="center"
+          justifyContent="center"
+          gutterSize="m"
+          responsive={false}
+          css={DocsColumnContainer(euiTheme)}
+        >
+          <EuiFlexItem grow={false} css={ActionHeaderContainer(euiTheme)}>
+            <EuiText size="xs">
+              <b>
+                <FormattedMessage
+                  id="xpack.search.queryRulesetDetail.draggableList.actionLabel"
+                  defaultMessage="Action"
+                />
+              </b>
+            </EuiText>
+          </EuiFlexItem>
+          <EuiFlexItem grow={false} css={DocumentCountLabelContainer(euiTheme)}>
+            <EuiText size="xs">
+              <b>
+                <FormattedMessage
+                  id="xpack.search.queryRulesetDetail.draggableList.documentCountLabel"
+                  defaultMessage="Documents"
+                />
+              </b>
+            </EuiText>
+          </EuiFlexItem>
+        </EuiFlexGroup>
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+};
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
new file mode 100644
index 0000000000000..3ac351b91f1b0
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.test.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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 { render, screen, within } from '@testing-library/react';
+import { I18nProvider } from '@kbn/i18n-react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import React from 'react';
+import { QueryRulesetDetail } from './query_ruleset_detail';
+import { MOCK_QUERY_RULESET_RESPONSE_FIXTURE } from '../../../common/__fixtures__/query_rules_ruleset';
+
+jest.mock('../../hooks/use_fetch_query_ruleset', () => ({
+  useFetchQueryRuleset: jest.fn(() => ({
+    data: {
+      ...MOCK_QUERY_RULESET_RESPONSE_FIXTURE,
+    },
+    isLoading: false,
+    error: null,
+  })),
+}));
+
+jest.mock('react-router-dom', () => ({
+  useParams: jest.fn(() => ({ rulesetId: MOCK_QUERY_RULESET_RESPONSE_FIXTURE.ruleset_id })),
+}));
+
+describe('Query rule detail', () => {
+  const TEST_IDS = {
+    DetailPage: 'queryRulesetDetailPage',
+    DetailPageHeader: 'queryRulesetDetailHeader',
+    HeaderDataButton: 'queryRulesetDetailHeaderDataButton',
+    HeaderSaveButton: 'queryRulesetDetailHeaderSaveButton',
+    AddRuleButton: 'queryRulesetDetailAddRuleButton',
+    DraggableItem: 'searchQueryRulesDraggableItem',
+  };
+  const queryClient = new QueryClient();
+  const Wrapper = ({ children }: { children?: React.ReactNode }) => (
+    <I18nProvider>
+      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
+    </I18nProvider>
+  );
+  describe('existing query ruleset', () => {
+    it('should render the query ruleset detail page', () => {
+      render(<QueryRulesetDetail />, {
+        wrapper: Wrapper,
+      });
+
+      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(
+        MOCK_QUERY_RULESET_RESPONSE_FIXTURE.rules.length
+      );
+    });
+  });
+});
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
new file mode 100644
index 0000000000000..b3b5a2d96fdb0
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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 React, { useEffect, useState } from 'react';
+
+import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
+import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { useParams } from 'react-router-dom';
+import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types';
+import { useFetchQueryRuleset } from '../../hooks/use_fetch_query_ruleset';
+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';
+
+export const QueryRulesetDetail: React.FC = () => {
+  const { rulesetId = '' } = useParams<{
+    rulesetId?: string;
+  }>();
+
+  const {
+    data: queryRulesetData,
+    isInitialLoading,
+    isError,
+    error,
+  } = useFetchQueryRuleset(rulesetId);
+
+  const [rules, setRules] = useState<QueryRulesQueryRule[]>(queryRulesetData?.rules ?? []);
+
+  useEffect(() => {
+    if (queryRulesetData?.rules) {
+      setRules(queryRulesetData.rules);
+    }
+  }, [queryRulesetData?.rules]);
+
+  return (
+    <QueryRulesPageTemplate>
+      {!isInitialLoading && !isError && !!queryRulesetData && (
+        <KibanaPageTemplate.Header
+          pageTitle={rulesetId}
+          restrictWidth
+          color="primary"
+          data-test-subj="queryRulesetDetailHeader"
+          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>
+              </EuiFlexItem>
+            </EuiFlexGroup>,
+          ]}
+        />
+      )}
+      {!isError && <QueryRuleDetailPanel rules={rules} setRules={setRules} />}
+      {isError && (
+        <ErrorPrompt
+          errorType={
+            isPermissionError(error)
+              ? 'missingPermissions'
+              : isNotFoundError(error)
+              ? 'notFound'
+              : 'generic'
+          }
+          data-test-subj="queryRulesetDetailErrorPrompt"
+        />
+      )}
+    </QueryRulesPageTemplate>
+  );
+};
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/styles.ts b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/styles.ts
new file mode 100644
index 0000000000000..d2147f9d6f872
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/styles.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { css } from '@emotion/react';
+import { type EuiThemeComputed } from '@elastic/eui';
+
+export const DocsColumnContainer = (theme: EuiThemeComputed<{}>) =>
+  css({
+    minWidth: theme.base * 10,
+  });
+
+export const ActionTypeIconBadgeContainer = (theme: EuiThemeComputed<{}>) =>
+  css({
+    minWidth: theme.base * 5,
+    textAlign: 'end',
+  });
+
+export const DocumentCountLabelContainer = (theme: EuiThemeComputed<{}>) =>
+  css({
+    minWidth: theme.base * 4,
+  });
+
+export const DocumentCountLabelStyle = (theme: EuiThemeComputed<{}>) =>
+  css({
+    minWidth: theme.size.l,
+  });
+
+export const DraggableListHeader = (theme: EuiThemeComputed<{}>) =>
+  css({
+    padding: `0 ${theme.base * 2.25}px 0 ${theme.base * 3.5}px`,
+  });
+
+export const ActionHeaderContainer = (theme: EuiThemeComputed<{}>) =>
+  css({
+    minWidth: theme.size.xl,
+    textAlign: 'end',
+  });
+
+export const DroppableContainer = (theme: EuiThemeComputed<{}>) =>
+  css({
+    backgroundColor: theme.colors.backgroundBaseSubdued,
+  });
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_ruleset.test.ts b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_ruleset.test.ts
new file mode 100644
index 0000000000000..38f38c941bea7
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_ruleset.test.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { renderHook, waitFor } from '@testing-library/react';
+import { MOCK_QUERY_RULESET_RESPONSE_FIXTURE } from '../../common/__fixtures__/query_rules_ruleset';
+
+const mockHttpGet = jest.fn();
+
+jest.mock('@tanstack/react-query', () => ({
+  useQuery: jest.fn().mockImplementation(async ({ queryKey, queryFn, opts }) => {
+    try {
+      const res = await queryFn();
+      return Promise.resolve(res);
+    } catch (_) {
+      // silent fail as we don't handle error yet
+    }
+  }),
+}));
+
+jest.mock('./use_kibana', () => ({
+  useKibana: jest.fn().mockReturnValue({
+    services: {
+      http: {
+        get: mockHttpGet,
+      },
+    },
+  }),
+}));
+
+describe('useFetchQueryRuleset hook', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should return query ruleset', async () => {
+    mockHttpGet.mockReturnValue(MOCK_QUERY_RULESET_RESPONSE_FIXTURE);
+
+    const { useFetchQueryRuleset } = jest.requireActual('./use_fetch_query_ruleset');
+
+    const { result } = renderHook(() => useFetchQueryRuleset('my-ruleset'));
+    await waitFor(() => {
+      expect(result.current).resolves.toStrictEqual(MOCK_QUERY_RULESET_RESPONSE_FIXTURE);
+      expect(mockHttpGet).toHaveBeenCalledWith('/internal/search_query_rules/ruleset/my-ruleset');
+    });
+  });
+});
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_ruleset.ts b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_ruleset.ts
new file mode 100644
index 0000000000000..bb111f7c501e0
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/hooks/use_fetch_query_ruleset.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { KibanaServerError } from '@kbn/kibana-utils-plugin/common';
+import { useQuery } from '@tanstack/react-query';
+import { QueryRulesQueryRuleset } from '@elastic/elasticsearch/lib/api/types';
+import { QUERY_RULES_QUERY_RULESET_FETCH_KEY } from '../../common/constants';
+import { useKibana } from './use_kibana';
+
+export const useFetchQueryRuleset = (rulesetId: string) => {
+  const {
+    services: { http },
+  } = useKibana();
+
+  return useQuery<QueryRulesQueryRuleset, { body: KibanaServerError }>({
+    queryKey: [QUERY_RULES_QUERY_RULESET_FETCH_KEY, rulesetId],
+    queryFn: async () => {
+      return await http.get<QueryRulesQueryRuleset>(
+        `/internal/search_query_rules/ruleset/${rulesetId}`
+      );
+    },
+    retry: false,
+  });
+};
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/layout/query_rules_page_template.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/layout/query_rules_page_template.tsx
new file mode 100644
index 0000000000000..b340c331f3f56
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/layout/query_rules_page_template.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
+import React from 'react';
+import { useKibana } from '../hooks/use_kibana';
+
+type QueryRulesPageTemplateProps = React.ComponentProps<typeof KibanaPageTemplate> & {
+  pageTemplateProps?: React.ComponentProps<typeof KibanaPageTemplate>;
+  children: React.ReactNode;
+};
+
+export const QueryRulesPageTemplate: React.FC<QueryRulesPageTemplateProps> = ({
+  children,
+  ...props
+}) => {
+  const {
+    services: { console: consolePlugin, history, searchNavigation },
+  } = useKibana();
+
+  const embeddedConsole = React.useMemo(
+    () => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
+    [consolePlugin]
+  );
+
+  return (
+    <KibanaPageTemplate
+      offset={0}
+      restrictWidth
+      grow={false}
+      data-test-subj="queryRulesBasePage"
+      solutionNav={searchNavigation?.useClassicNavigation(history)}
+      color="primary"
+      {...props}
+    >
+      {children}
+      {embeddedConsole}
+    </KibanaPageTemplate>
+  );
+};
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/routes.ts b/x-pack/solutions/search/plugins/search_query_rules/public/routes.ts
index d6b0b0a669281..0e0899b6ab448 100644
--- a/x-pack/solutions/search/plugins/search_query_rules/public/routes.ts
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/routes.ts
@@ -6,3 +6,4 @@
  */
 
 export const ROOT_PATH = '/';
+export const QUERY_RULESET_DETAIL_PATH = `${ROOT_PATH}ruleset/:rulesetId`;
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/search_query_router.tsx b/x-pack/solutions/search/plugins/search_query_rules/public/search_query_router.tsx
index a5415eaff9160..d0c4693829fd4 100644
--- a/x-pack/solutions/search/plugins/search_query_rules/public/search_query_router.tsx
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/search_query_router.tsx
@@ -7,12 +7,16 @@
 
 import { Route, Routes } from '@kbn/shared-ux-router';
 import React from 'react';
-import { ROOT_PATH } from './routes';
+import { QUERY_RULESET_DETAIL_PATH, ROOT_PATH } from './routes';
 import { QueryRulesOverview } from './components/overview/overview';
+import { QueryRulesetDetail } from './components/query_ruleset_detail/query_ruleset_detail';
 
 export const QueryRulesRouter = () => {
   return (
     <Routes>
+      <Route exact path={QUERY_RULESET_DETAIL_PATH}>
+        <QueryRulesetDetail />
+      </Route>
       <Route exact path={ROOT_PATH}>
         <QueryRulesOverview />
       </Route>
diff --git a/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_rules_utils.ts b/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_rules_utils.ts
index c4c7974a72abd..159de44f29683 100644
--- a/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_rules_utils.ts
+++ b/x-pack/solutions/search/plugins/search_query_rules/public/utils/query_rules_utils.ts
@@ -10,3 +10,7 @@ import { KibanaServerError } from '@kbn/kibana-utils-plugin/common';
 export const isPermissionError = (error: { body: KibanaServerError }) => {
   return error.body.statusCode === 403;
 };
+
+export const isNotFoundError = (error: { body: KibanaServerError }) => {
+  return error.body.statusCode === 404;
+};
diff --git a/x-pack/solutions/search/plugins/search_query_rules/server/lib/fetch_query_rules_ruleset.test.ts b/x-pack/solutions/search/plugins/search_query_rules/server/lib/fetch_query_rules_ruleset.test.ts
new file mode 100644
index 0000000000000..96712b19aae99
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/server/lib/fetch_query_rules_ruleset.test.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { MOCK_QUERY_RULESET_RESPONSE_FIXTURE } from '../../common/__fixtures__/query_rules_ruleset';
+import { fetchQueryRulesRuleset } from './fetch_query_rules_ruleset';
+import { ElasticsearchClient } from '@kbn/core/server';
+
+describe('fetch query rules rulesets lib function', () => {
+  const mockClient = {
+    queryRules: {
+      getRuleset: jest.fn(),
+    },
+  };
+
+  const client = () => mockClient as unknown as ElasticsearchClient;
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should return query rules ruleset', async () => {
+    mockClient.queryRules.getRuleset.mockResolvedValue(MOCK_QUERY_RULESET_RESPONSE_FIXTURE);
+
+    const result = await fetchQueryRulesRuleset(client(), 'my-ruleset');
+    expect(result).toEqual(MOCK_QUERY_RULESET_RESPONSE_FIXTURE);
+  });
+});
diff --git a/x-pack/solutions/search/plugins/search_query_rules/server/lib/fetch_query_rules_ruleset.ts b/x-pack/solutions/search/plugins/search_query_rules/server/lib/fetch_query_rules_ruleset.ts
new file mode 100644
index 0000000000000..2cb02cc91a88a
--- /dev/null
+++ b/x-pack/solutions/search/plugins/search_query_rules/server/lib/fetch_query_rules_ruleset.ts
@@ -0,0 +1,19 @@
+/*
+ * 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 { QueryRulesQueryRuleset } from '@elastic/elasticsearch/lib/api/types';
+import { ElasticsearchClient } from '@kbn/core/server';
+
+export const fetchQueryRulesRuleset = async (
+  client: ElasticsearchClient,
+  rulesetId: string
+): Promise<QueryRulesQueryRuleset> => {
+  const result = await client.queryRules.getRuleset({
+    ruleset_id: rulesetId,
+  });
+  return result;
+};
diff --git a/x-pack/solutions/search/plugins/search_query_rules/server/routes.ts b/x-pack/solutions/search/plugins/search_query_rules/server/routes.ts
index 66b5b50f359e3..dc55f50661d16 100644
--- a/x-pack/solutions/search/plugins/search_query_rules/server/routes.ts
+++ b/x-pack/solutions/search/plugins/search_query_rules/server/routes.ts
@@ -12,6 +12,7 @@ import { APIRoutes } from '../common/api_routes';
 import { errorHandler } from './utils/error_handler';
 import { fetchQueryRulesSets } from './lib/fetch_query_rules_sets';
 import { DEFAULT_PAGE_VALUE } from '../common/pagination';
+import { fetchQueryRulesRuleset } from './lib/fetch_query_rules_ruleset';
 
 export function defineRoutes({ logger, router }: { logger: Logger; router: IRouter }) {
   router.get(
@@ -64,4 +65,51 @@ export function defineRoutes({ logger, router }: { logger: Logger; router: IRout
       });
     })
   );
+  router.get(
+    {
+      path: APIRoutes.QUERY_RULES_RULESET_FETCH,
+      options: {
+        access: 'internal',
+      },
+      security: {
+        authz: {
+          requiredPrivileges: ['manage_search_query_rules'],
+        },
+      },
+      validate: {
+        params: schema.object({
+          ruleset_id: schema.string(),
+        }),
+      },
+    },
+    errorHandler(logger)(async (context, request, response) => {
+      const core = await context.core;
+      const {
+        client: { asCurrentUser },
+      } = core.elasticsearch;
+      const user = core.security.authc.getCurrentUser();
+      if (!user) {
+        return response.customError({
+          statusCode: 502,
+          body: 'Could not retrieve current user, security plugin is not ready',
+        });
+      }
+      const hasSearchQueryRulesPrivilege = await asCurrentUser.security.hasPrivileges({
+        cluster: ['manage_search_query_rules'],
+      });
+      if (!hasSearchQueryRulesPrivilege.has_all_requested) {
+        return response.forbidden({
+          body: "Your user doesn't have manage_search_query_rules privileges",
+        });
+      }
+      const rulesetData = await fetchQueryRulesRuleset(asCurrentUser, request.params.ruleset_id);
+
+      return response.ok({
+        headers: {
+          'content-type': 'application/json',
+        },
+        body: rulesetData,
+      });
+    })
+  );
 }


Reviews:
user: login: TattdCodeMonkey
body: Overall looks reasonable to me. I left some questions and non-blocking comments.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:19:15Z
user: login: efegurkan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T09:05:32Z
user: login: Samiul-TheSoccerFan
body: Code changes looks good. 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T18:27:47Z
user: login: mdefazio
body: Can we include the subdued background and remove the shadows on these panels? We will also need the row headings—this was a specific feedback request—and the background helps with the association here.

![CleanShot 2025-04-18 at 06 43 35@2x](https://github.com/user-attachments/assets/298471fc-c540-46c8-baea-cdb09b178592)

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T10:44:23Z
user: login: TattdCodeMonkey
body: left some comments, but none are blocking and with this feature flagged I'm fine if you merge as-is
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T12:58:34Z
user: login: Samiul-TheSoccerFan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:13:37Z
user: login: Samiul-TheSoccerFan
body: nice iteration 👏 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:14:00Z

Review comments:
diff_hunk: @@ -9,4 +9,5 @@ export const PLUGIN_ROUTE_ROOT = '/app/elasticsearch/query_rules';
 
 export enum APIRoutes {
   QUERY_RULES_SETS = '/internal/search_query_rules/query_rules_sets',
+  QUERY_RULES_RULESET_FETCH = '/internal/search_query_rules/ruleset/{ruleset_id}',
path: x-pack/solutions/search/plugins/search_query_rules/common/api_routes.ts
user: login: TattdCodeMonkey
body: why use a different path here? `ruleset/{ruleset_id}` vs `/internal/search_query_rules/query_rules_sets`

Also if we stick with `ruleset/` we might want to make it plural `rulesets/{ruleset_id}`
created_at: 2025-04-16T14:30:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+/*
+ * 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 React, { useEffect, useMemo, useState } from 'react';
+
+import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
+import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { useParams } from 'react-router-dom';
+import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types';
+import { useKibana } from '../../hooks/use_kibana';
+import { QueryRuleDraggableList } from './query_rule_draggable_list';
+import { useFetchQueryRuleset } from '../../hooks/use_fetch_query_ruleset';
+import { ErrorPrompt } from '../error_prompt/error_prompt';
+import { isNotFoundError, isPermissionError } from '../../utils/query_rules_utils';
+
+export const QueryRulesetDetail: React.FC = () => {
+  const { rulesetId = '' } = useParams<{
+    rulesetId?: string;
+  }>();
+
+  const {
+    data: queryRulesetData,
+    isInitialLoading,
+    isError,
+    error,
+  } = useFetchQueryRuleset(rulesetId);
+
+  const {
+    services: { console: consolePlugin, history, searchNavigation },
+  } = useKibana();
+
+  const embeddableConsole = useMemo(
+    () => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
+    [consolePlugin]
+  );
+  const [rules, setRules] = useState<QueryRulesQueryRule[]>(queryRulesetData?.rules ?? []);
+
+  useEffect(() => {
+    if (queryRulesetData?.rules) {
+      setRules(queryRulesetData.rules);
+    }
+  }, [queryRulesetData?.rules]);
+
+  return (
+    <KibanaPageTemplate
+      offset={0}
+      restrictWidth
+      grow={false}
+      data-test-subj="queryRulesetDetailPage"
+      solutionNav={searchNavigation?.useClassicNavigation(history)}
+      color="primary"
+    >
+      {!isInitialLoading && !isError && !!queryRulesetData && (
+        <KibanaPageTemplate.Header
+          pageTitle={rulesetId}
+          restrictWidth
+          color="primary"
+          data-test-subj="queryRulesetDetailHeader"
+          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>
+              </EuiFlexItem>
+            </EuiFlexGroup>,
+          ]}
+        />
+      )}
+      {!isError && (
+        <KibanaPageTemplate.Section restrictWidth>
path: x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx
user: login: TattdCodeMonkey
body: **nitpick**(non-blocking): we should move this to a component to simplify the rendering here IMO.
created_at: 2025-04-16T14:35:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+/*
+ * 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 React, { useEffect, useMemo, useState } from 'react';
+
+import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
+import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { useParams } from 'react-router-dom';
+import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types';
+import { useKibana } from '../../hooks/use_kibana';
+import { QueryRuleDraggableList } from './query_rule_draggable_list';
+import { useFetchQueryRuleset } from '../../hooks/use_fetch_query_ruleset';
+import { ErrorPrompt } from '../error_prompt/error_prompt';
+import { isNotFoundError, isPermissionError } from '../../utils/query_rules_utils';
+
+export const QueryRulesetDetail: React.FC = () => {
+  const { rulesetId = '' } = useParams<{
+    rulesetId?: string;
+  }>();
+
+  const {
+    data: queryRulesetData,
+    isInitialLoading,
+    isError,
+    error,
+  } = useFetchQueryRuleset(rulesetId);
+
+  const {
+    services: { console: consolePlugin, history, searchNavigation },
+  } = useKibana();
+
+  const embeddableConsole = useMemo(
+    () => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
+    [consolePlugin]
+  );
+  const [rules, setRules] = useState<QueryRulesQueryRule[]>(queryRulesetData?.rules ?? []);
+
+  useEffect(() => {
+    if (queryRulesetData?.rules) {
+      setRules(queryRulesetData.rules);
+    }
+  }, [queryRulesetData?.rules]);
+
+  return (
+    <KibanaPageTemplate
+      offset={0}
+      restrictWidth
+      grow={false}
+      data-test-subj="queryRulesetDetailPage"
+      solutionNav={searchNavigation?.useClassicNavigation(history)}
+      color="primary"
+    >
path: x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_ruleset_detail.tsx
user: login: TattdCodeMonkey
body: Should we consider moving this to a shared component to have uniform handling of `solutionNav` and `embeddableConsole` vs having to repeat it across the different pages?
created_at: 2025-04-16T14:36:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * 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 React from 'react';
+
+import {
+  EuiDragDropContext,
+  EuiDroppable,
+  EuiDraggable,
+  EuiPanel,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiIcon,
+  EuiText,
+  EuiBadge,
+  EuiTextColor,
+  EuiButtonIcon,
+  useEuiTheme,
+  euiDragDropReorder,
+} from '@elastic/eui';
+import {
+  QueryRulesQueryRule,
+  QueryRulesQueryRuleCriteria,
+} from '@elastic/elasticsearch/lib/api/types';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { i18n } from '@kbn/i18n';
+
+export interface QueryRuleDraggableListProps {
+  rules: QueryRulesQueryRule[];
+  onReorder: (queryRules: QueryRulesQueryRule[]) => void;
+}
+export const QueryRuleDraggableList: React.FC<QueryRuleDraggableListProps> = ({
+  rules,
+  onReorder,
+}) => {
+  const {
+    euiTheme: { colors },
+  } = useEuiTheme();
+  const criteriaDisplay = (criteria: QueryRulesQueryRuleCriteria) => (
+    <EuiText size="s">
+      <EuiBadge>{criteria.metadata}</EuiBadge>&nbsp;
+      <EuiTextColor color={colors.textPrimary}>{criteria.type}</EuiTextColor>&nbsp;
+      {criteria.values?.join(',')}
+    </EuiText>
+  );
+  const actionIcon = (queryRule: QueryRulesQueryRule) => (
+    <EuiFlexGroup responsive={false} alignItems="center" gutterSize="m">
+      <EuiFlexItem grow={false}>
+        <EuiBadge iconType={queryRule.type === 'exclude' ? 'eyeClosed' : 'pinFilled'}>
+          {queryRule.type === 'exclude' ? (
+            <FormattedMessage
+              id="xpack.search.queryRulesetDetail.draggableList.excludeLabel"
+              defaultMessage="Exclude"
+            />
+          ) : (
+            <FormattedMessage
+              id="xpack.search.queryRulesetDetail.draggableList.pinLabel"
+              defaultMessage="Pin"
+            />
+          )}
+        </EuiBadge>
+      </EuiFlexItem>
+      <EuiFlexItem grow>
+        <EuiFlexGroup responsive={false} alignItems="center" gutterSize="s">
+          <EuiFlexItem grow={false}>
+            <EuiIcon type="documents" />
+          </EuiFlexItem>
+          <EuiFlexItem grow={false}>{queryRule.actions.docs?.length ?? 0}</EuiFlexItem>
+        </EuiFlexGroup>
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+
+  return (
+    <EuiDragDropContext
+      onDragEnd={({ source, destination }) => {
+        if (source && destination) {
+          const items = euiDragDropReorder(rules, source.index, destination.index);
+          onReorder(items);
+        }
+      }}
+    >
+      <EuiDroppable droppableId="droppable1" spacing="m">
path: x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list.tsx
user: login: TattdCodeMonkey
body: Could we have a more descriptive name here? `droppableId="droppable1"`
created_at: 2025-04-16T15:15:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * 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 React from 'react';
+
+import {
+  EuiDragDropContext,
+  EuiDroppable,
+  EuiDraggable,
+  EuiPanel,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiIcon,
+  EuiText,
+  EuiBadge,
+  EuiTextColor,
+  EuiButtonIcon,
+  useEuiTheme,
+  euiDragDropReorder,
+} from '@elastic/eui';
+import {
+  QueryRulesQueryRule,
+  QueryRulesQueryRuleCriteria,
+} from '@elastic/elasticsearch/lib/api/types';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { i18n } from '@kbn/i18n';
+
+export interface QueryRuleDraggableListProps {
+  rules: QueryRulesQueryRule[];
+  onReorder: (queryRules: QueryRulesQueryRule[]) => void;
+}
+export const QueryRuleDraggableList: React.FC<QueryRuleDraggableListProps> = ({
+  rules,
+  onReorder,
+}) => {
+  const {
+    euiTheme: { colors },
+  } = useEuiTheme();
+  const criteriaDisplay = (criteria: QueryRulesQueryRuleCriteria) => (
+    <EuiText size="s">
+      <EuiBadge>{criteria.metadata}</EuiBadge>&nbsp;
+      <EuiTextColor color={colors.textPrimary}>{criteria.type}</EuiTextColor>&nbsp;
+      {criteria.values?.join(',')}
path: x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list.tsx
user: login: TattdCodeMonkey
body: should we add a space after the `,` to add a game between terms? just from a visual perspective?
```suggestion
      {criteria.values?.join(', ')}
```
created_at: 2025-04-16T15:18:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,4 +9,5 @@ export const PLUGIN_ROUTE_ROOT = '/app/elasticsearch/query_rules';
 
 export enum APIRoutes {
   QUERY_RULES_SETS = '/internal/search_query_rules/query_rules_sets',
+  QUERY_RULES_RULESET_FETCH = '/internal/search_query_rules/ruleset/{ruleset_id}',
path: x-pack/solutions/search/plugins/search_query_rules/common/api_routes.ts
user: login: efegurkan
body: First one is started a before we have decided a path structure. I will update them to same structure on different PRs.
created_at: 2025-04-17T09:05:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,71 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem, EuiButton, EuiText } from '@elastic/eui';
+import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types';
+import { QueryRuleDraggableList } from './query_rule_draggable_list';
+
+interface QueryRuleDetailPanelProps {
+  rules: QueryRulesQueryRule[];
+  setRules: (rules: QueryRulesQueryRule[]) => void;
+}
+export const QueryRuleDetailPanel: React.FC<QueryRuleDetailPanelProps> = ({ rules, setRules }) => {
+  return (
+    <KibanaPageTemplate.Section restrictWidth>
+      <EuiFlexGroup justifyContent="spaceBetween" direction="column">
+        <EuiFlexItem grow={false}>
+          <EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
+            <EuiFlexItem grow={false}>
+              <EuiFlexGroup alignItems="center">
+                <EuiFlexItem>
+                  <EuiButton
+                    iconType="plusInCircle"
+                    color="primary"
+                    data-test-subj="queryRulesetDetailAddRuleButton"
+                    onClick={() => {
+                      // Logic to add a new rule
+                    }}
+                  >
+                    <FormattedMessage
+                      id="xpack.queryRules.queryRulesetDetail.addRuleButton"
+                      defaultMessage="Add rule"
+                    />
+                  </EuiButton>
+                </EuiFlexItem>
+                <EuiFlexItem>
+                  <EuiText>
+                    {rules.length > 1 ? (
path: x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_detail_panel.tsx
user: login: TattdCodeMonkey
body: nitpick: you can do this inline with `FormattedMessage` [message syntax](https://formatjs.github.io/docs/core-concepts/icu-syntax#plural-format)

```
<FormattedMessage
  id="xpack.queryRules.queryRulesetDetail.ruleCount"
  defaultMessage="{ruleCount, plural, one {# rule} other {# rules}}"
  values={{ ruleCount: rules.length }}
  data-test-subj="queryRulesetDetailRuleCount"
/>
```

created_at: 2025-04-24T12:47:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+/*
+ * 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 React from 'react';
+
+import {
+  EuiDragDropContext,
+  EuiDroppable,
+  EuiDraggable,
+  EuiPanel,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiIcon,
+  EuiText,
+  EuiBadge,
+  EuiTextColor,
+  EuiButtonIcon,
+  useEuiTheme,
+  euiDragDropReorder,
+} from '@elastic/eui';
+import {
+  QueryRulesQueryRule,
+  QueryRulesQueryRuleCriteria,
+} from '@elastic/elasticsearch/lib/api/types';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { i18n } from '@kbn/i18n';
+
+export interface QueryRuleDraggableListProps {
+  rules: QueryRulesQueryRule[];
+  onReorder: (queryRules: QueryRulesQueryRule[]) => void;
+}
+export const QueryRuleDraggableList: React.FC<QueryRuleDraggableListProps> = ({
+  rules,
+  onReorder,
+}) => {
+  const {
+    euiTheme: { colors, size, base },
+  } = useEuiTheme();
+  const DOC_COUNT_COLUMN_SIZING = {
+    columnMinWidth: base * 10,
+    actionMinWidth: base * 5,
+    docCountMinWidth: base * 4,
+    docTextMinWidth: size.l,
+    headerpaddingLeft: base * 3.5,
+  };
+  const criteriaDisplay = (criteria: QueryRulesQueryRuleCriteria) => (
+    <EuiText size="s">
+      <EuiBadge>{criteria.metadata}</EuiBadge>&nbsp;
+      <EuiTextColor color={colors.textPrimary}>{criteria.type}</EuiTextColor>&nbsp;
+      {criteria.values?.join(', ')}
+    </EuiText>
+  );
+  const actionIcon = (queryRule: QueryRulesQueryRule) => (
path: x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list.tsx
user: login: TattdCodeMonkey
body: nitpick: I'd prefer this be a component instead of an inline function
created_at: 2025-04-24T12:53:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+/*
+ * 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 React from 'react';
+
+import {
+  EuiDragDropContext,
+  EuiDroppable,
+  EuiDraggable,
+  EuiPanel,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiIcon,
+  EuiText,
+  EuiBadge,
+  EuiTextColor,
+  EuiButtonIcon,
+  useEuiTheme,
+  euiDragDropReorder,
+} from '@elastic/eui';
+import {
+  QueryRulesQueryRule,
+  QueryRulesQueryRuleCriteria,
+} from '@elastic/elasticsearch/lib/api/types';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { i18n } from '@kbn/i18n';
+
+export interface QueryRuleDraggableListProps {
+  rules: QueryRulesQueryRule[];
+  onReorder: (queryRules: QueryRulesQueryRule[]) => void;
+}
+export const QueryRuleDraggableList: React.FC<QueryRuleDraggableListProps> = ({
+  rules,
+  onReorder,
+}) => {
+  const {
+    euiTheme: { colors, size, base },
+  } = useEuiTheme();
+  const DOC_COUNT_COLUMN_SIZING = {
+    columnMinWidth: base * 10,
+    actionMinWidth: base * 5,
+    docCountMinWidth: base * 4,
+    docTextMinWidth: size.l,
+    headerpaddingLeft: base * 3.5,
+  };
+  const criteriaDisplay = (criteria: QueryRulesQueryRuleCriteria) => (
path: x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list.tsx
user: login: TattdCodeMonkey
body: nitpick: I'd prefer this be a component instead of an inline function
created_at: 2025-04-24T12:53:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+/*
+ * 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 React from 'react';
+
+import {
+  EuiDragDropContext,
+  EuiDroppable,
+  EuiDraggable,
+  EuiPanel,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiIcon,
+  EuiText,
+  EuiBadge,
+  EuiTextColor,
+  EuiButtonIcon,
+  useEuiTheme,
+  euiDragDropReorder,
+} from '@elastic/eui';
+import {
+  QueryRulesQueryRule,
+  QueryRulesQueryRuleCriteria,
+} from '@elastic/elasticsearch/lib/api/types';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { i18n } from '@kbn/i18n';
+
+export interface QueryRuleDraggableListProps {
+  rules: QueryRulesQueryRule[];
+  onReorder: (queryRules: QueryRulesQueryRule[]) => void;
+}
+export const QueryRuleDraggableList: React.FC<QueryRuleDraggableListProps> = ({
+  rules,
+  onReorder,
+}) => {
+  const {
+    euiTheme: { colors, size, base },
+  } = useEuiTheme();
+  const DOC_COUNT_COLUMN_SIZING = {
+    columnMinWidth: base * 10,
+    actionMinWidth: base * 5,
+    docCountMinWidth: base * 4,
+    docTextMinWidth: size.l,
+    headerpaddingLeft: base * 3.5,
+  };
+  const criteriaDisplay = (criteria: QueryRulesQueryRuleCriteria) => (
+    <EuiText size="s">
+      <EuiBadge>{criteria.metadata}</EuiBadge>&nbsp;
+      <EuiTextColor color={colors.textPrimary}>{criteria.type}</EuiTextColor>&nbsp;
+      {criteria.values?.join(', ')}
+    </EuiText>
+  );
+  const actionIcon = (queryRule: QueryRulesQueryRule) => (
+    <EuiFlexGroup
+      responsive={false}
+      alignItems="center"
+      gutterSize="m"
+      justifyContent="center"
+      css={{ minWidth: DOC_COUNT_COLUMN_SIZING.columnMinWidth }}
path: x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list.tsx
user: login: TattdCodeMonkey
body: nitpick: should we put these in a `styles.ts` using emotion? [example I've been trying](https://github.com/elastic/kibana/blob/main/x-pack/solutions/search/plugins/search_playground/public/components/query_mode/styles.ts)

the individual css objects seems like a code smell to me, but we also don't have consistency on when to do this vs grouping them so idk 🤷 
created_at: 2025-04-24T12:55:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+/*
+ * 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 React from 'react';
+
+import {
+  EuiDragDropContext,
+  EuiDroppable,
+  EuiDraggable,
+  EuiPanel,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiIcon,
+  EuiText,
+  EuiBadge,
+  EuiTextColor,
+  EuiButtonIcon,
+  useEuiTheme,
+  euiDragDropReorder,
+} from '@elastic/eui';
+import {
+  QueryRulesQueryRule,
+  QueryRulesQueryRuleCriteria,
+} from '@elastic/elasticsearch/lib/api/types';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { i18n } from '@kbn/i18n';
+
+export interface QueryRuleDraggableListProps {
+  rules: QueryRulesQueryRule[];
+  onReorder: (queryRules: QueryRulesQueryRule[]) => void;
+}
+export const QueryRuleDraggableList: React.FC<QueryRuleDraggableListProps> = ({
+  rules,
+  onReorder,
+}) => {
+  const {
+    euiTheme: { colors, size, base },
+  } = useEuiTheme();
+  const DOC_COUNT_COLUMN_SIZING = {
+    columnMinWidth: base * 10,
+    actionMinWidth: base * 5,
+    docCountMinWidth: base * 4,
+    docTextMinWidth: size.l,
+    headerpaddingLeft: base * 3.5,
+  };
+  const criteriaDisplay = (criteria: QueryRulesQueryRuleCriteria) => (
+    <EuiText size="s">
+      <EuiBadge>{criteria.metadata}</EuiBadge>&nbsp;
+      <EuiTextColor color={colors.textPrimary}>{criteria.type}</EuiTextColor>&nbsp;
+      {criteria.values?.join(', ')}
+    </EuiText>
+  );
+  const actionIcon = (queryRule: QueryRulesQueryRule) => (
+    <EuiFlexGroup
+      responsive={false}
+      alignItems="center"
+      gutterSize="m"
+      justifyContent="center"
+      css={{ minWidth: DOC_COUNT_COLUMN_SIZING.columnMinWidth }}
path: x-pack/solutions/search/plugins/search_query_rules/public/components/query_ruleset_detail/query_rule_draggable_list.tsx
user: login: Samiul-TheSoccerFan
body: This definitely looks neat 😄 
created_at: 2025-04-24T13:13:37Z
author_association: CONTRIBUTOR

PR comments:
user: login: efegurkan
created_at: 2025-04-23T17:30:57Z
body: Hey @elastic/search-kibana @TattdCodeMonkey @Samiul-TheSoccerFan 

I have updated the PR with the addition of the custom header as @mdefazio suggested.

I am not really happy with the solution but I think this needs another set of reviews and you suggestions if possible.
author_association: MEMBER
user: login: efegurkan
created_at: 2025-04-25T13:48:26Z
body: @elasticmachine merge upstream
author_association: MEMBER
user: login: kibanamachine
created_at: 2025-04-25T15:33:58Z
body: Starting backport for target branches: 8.19

https://github.com/elastic/kibana/actions/runs/14668197116
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2025-04-25T15:34:05Z
body: ## :green_heart: Build Succeeded
* [Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/296226)
* Commit: 0e2a3f8acdde5ba3e7154eb973abe3cf90f07f58

### Metrics [[docs](https://www.elastic.co/guide/en/kibana/master/ci-metrics.html)]
#### Module Count
> Fewer modules leads to a faster build time


| id | [before](https://github.com/elastic/kibana/commit/fd00ec8f57c2657097a406cb6381c072d0114001) | [after](https://github.com/elastic/kibana/commit/0e2a3f8acdde5ba3e7154eb973abe3cf90f07f58) | diff |
| --- | --- | --- | --- |
| `searchQueryRules` | 70 | 79 | +9 |

#### Async chunks
> Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app


| id | [before](https://github.com/elastic/kibana/commit/fd00ec8f57c2657097a406cb6381c072d0114001) | [after](https://github.com/elastic/kibana/commit/0e2a3f8acdde5ba3e7154eb973abe3cf90f07f58) | diff |
| --- | --- | --- | --- |
| `searchQueryRules` | 30.7KB | 38.0KB | +7.3KB |

#### Page load bundle
> Size of the bundles that are downloaded on every page load. Target size is below 100kb


| id | [before](https://github.com/elastic/kibana/commit/fd00ec8f57c2657097a406cb6381c072d0114001) | [after](https://github.com/elastic/kibana/commit/0e2a3f8acdde5ba3e7154eb973abe3cf90f07f58) | diff |
| --- | --- | --- | --- |
| `searchQueryRules` | 5.0KB | 5.1KB | +145.0B |



### History
* :broken_heart: [Build #296192](https://buildkite.com/elastic/kibana-pull-request/builds/296192) failed 39dcc109ee6c66b3922d0a6d60f0e7ec5338cd3c
* :green_heart: [Build #292737](https://buildkite.com/elastic/kibana-pull-request/builds/292737) succeeded 4508d34d440f214b6c13128f4bf3268ac96105da

<!--buildkite-pr-comment-kibana-pull-request
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/296226","number":296226,"commit":"0e2a3f8acdde5ba3e7154eb973abe3cf90f07f58"},{"buildStatus":{"state":"failed","success":false,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/296192","number":296192,"commit":"39dcc109ee6c66b3922d0a6d60f0e7ec5338cd3c"},{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/292737","number":292737,"commit":"4508d34d440f214b6c13128f4bf3268ac96105da"}],"number":296226}
buildkite-pr-comment-->
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-04-25T15:40:06Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|8.19|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/219271">](https://github.com/elastic/kibana/pull/219271)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR