]> source.dussan.org Git - sonarqube.git/commitdiff
SGB-163 Fetching rules and cve inside issue details page using react query
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Mon, 7 Oct 2024 10:37:13 +0000 (12:37 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 15 Oct 2024 20:03:07 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx
server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
server/sonar-web/src/main/js/queries/cves.ts [new file with mode: 0644]
server/sonar-web/src/main/js/queries/rules.ts

index 178b2ef5a68e41f78d0e73520bd0fe724abbc4e7..2eee2ba88b5f47719722d61317ff0d70f400da24 100644 (file)
@@ -63,6 +63,10 @@ jest.mock('../../../components/common/ScreenPositionHelper', () => ({
   },
 }));
 
+jest.mock('../../../api/cves', () => ({
+  getCve: jest.fn(),
+}));
+
 beforeEach(() => {
   issuesHandler.reset();
   cveHandler.reset();
index 2e03f52fa6cc28f92661290365977ae853896bde..414426f0a509eca2ec1cb308eb2597017042f525 100644 (file)
@@ -28,20 +28,18 @@ import {
   themeBorder,
   themeColor,
 } from 'design-system';
-import React, { useEffect } from 'react';
+import React from 'react';
 import { Helmet } from 'react-helmet-async';
-import { getCve } from '../../../api/cves';
-import { getRuleDetails } from '../../../api/rules';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
 import { IssueSuggestionCodeTab } from '../../../components/rules/IssueSuggestionCodeTab';
 import IssueTabViewer from '../../../components/rules/IssueTabViewer';
 import { fillBranchLike } from '../../../helpers/branch-like';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { useRuleDetailsQuery } from '../../../queries/rules';
 import A11ySkipTarget from '../../../sonar-aligned/components/a11y/A11ySkipTarget';
 import { isPortfolioLike } from '../../../sonar-aligned/helpers/component';
 import { ComponentQualifier } from '../../../sonar-aligned/types/component';
-import { Cve } from '../../../types/cves';
-import { Component, Issue, Paging, RuleDetails } from '../../../types/types';
+import { Component, Issue, Paging } from '../../../types/types';
 import SubnavigationIssuesList from '../issues-subnavigation/SubnavigationIssuesList';
 import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments';
 import IssuesSourceViewer from './IssuesSourceViewer';
@@ -81,29 +79,9 @@ export default function IssueDetails({
   selectedFlowIndex,
   selectedLocationIndex,
 }: Readonly<IssueDetailsProps>) {
-  const [loadingRule, setLoadingRule] = React.useState(false);
-  const [openRuleDetails, setOpenRuleDetails] = React.useState<RuleDetails | undefined>(undefined);
-  const [cve, setCve] = React.useState<Cve | undefined>(undefined);
   const { canBrowseAllChildProjects, qualifier = ComponentQualifier.Project } = component ?? {};
-
-  useEffect(() => {
-    const loadRule = async () => {
-      setLoadingRule(true);
-
-      const openRuleDetails = await getRuleDetails({ key: openIssue.rule })
-        .then((response) => response.rule)
-        .catch(() => undefined);
-
-      let cve: Cve | undefined;
-      if (typeof openIssue.cveId === 'string') {
-        cve = await getCve(openIssue.cveId);
-      }
-      setLoadingRule(false);
-      setOpenRuleDetails(openRuleDetails);
-      setCve(cve);
-    };
-    loadRule().catch(() => undefined);
-  }, [openIssue.key, openIssue.rule, openIssue.cveId]);
+  const { data: ruleData, isLoading: isLoadingRule } = useRuleDetailsQuery({ key: openIssue.rule });
+  const openRuleDetails = ruleData?.rule;
 
   const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
     <FlagMessage
@@ -177,7 +155,7 @@ export default function IssueDetails({
                   >
                     <A11ySkipTarget anchor="issues_main" />
 
-                    <Spinner isLoading={loadingRule}>
+                    <Spinner isLoading={isLoadingRule}>
                       {openRuleDetails && (
                         <IssueTabViewer
                           activityTabContent={
@@ -210,7 +188,7 @@ export default function IssueDetails({
                           onIssueChange={handleIssueChange}
                           ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey}
                           ruleDetails={openRuleDetails}
-                          cve={cve}
+                          cveId={openIssue.cveId}
                           selectedFlowIndex={selectedFlowIndex}
                           selectedLocationIndex={selectedLocationIndex}
                         />
index 30efacc1fdd60a73652b23ecb06d1272543775de..e56229dc6480d5b8681a5e9bbfc379e7f797d050 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { getCve } from '../../../api/cves';
 import { getRuleDetails } from '../../../api/rules';
 import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
 import { get } from '../../../helpers/storage';
-import { Cve } from '../../../types/cves';
 import { Standards } from '../../../types/security';
 import {
   Hotspot,
@@ -48,7 +46,6 @@ interface Props {
 }
 
 interface State {
-  cve?: Cve;
   hotspot?: Hotspot;
   lastStatusChangedTo?: HotspotStatusOption;
   loading: boolean;
@@ -87,10 +84,6 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
     try {
       const hotspot = await getSecurityHotspotDetails(this.props.hotspotKey);
       const ruleDetails = await getRuleDetails({ key: hotspot.rule.key }).then((r) => r.rule);
-      let cve;
-      if (typeof this.props.cveId === 'string') {
-        cve = await getCve(this.props.cveId);
-      }
 
       if (this.mounted) {
         this.setState({
@@ -98,7 +91,6 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
           loading: false,
           ruleLanguage: ruleDetails.lang,
           ruleDescriptionSections: ruleDetails.descriptionSections,
-          cve,
         });
       }
     } catch (error) {
@@ -135,13 +127,13 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { component, hotspotsReviewedMeasure, selectedHotspotLocation, standards } = this.props;
+    const { component, cveId, hotspotsReviewedMeasure, selectedHotspotLocation, standards } =
+      this.props;
 
     const {
       hotspot,
       ruleDescriptionSections,
       ruleLanguage,
-      cve,
       loading,
       showStatusUpdateSuccessModal,
       lastStatusChangedTo,
@@ -160,7 +152,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
         onUpdateHotspot={this.handleHotspotUpdate}
         ruleDescriptionSections={ruleDescriptionSections}
         ruleLanguage={ruleLanguage}
-        cve={cve}
+        cveId={cveId}
         selectedHotspotLocation={selectedHotspotLocation}
         showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
         standards={standards}
index acc3a9e4fdd0b4265af4c5e989ae4d46f7680347..a3ee2d8feff6cc0daa08813de69293fd07321119 100644 (file)
@@ -26,7 +26,6 @@ import { Component } from '../../../types/types';
 import { HotspotHeader } from './HotspotHeader';
 
 import { Spinner } from 'design-system';
-import { Cve } from '../../../types/cves';
 import { CurrentUser } from '../../../types/users';
 import { RuleDescriptionSection } from '../../coding-rules/rule';
 import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments';
@@ -38,7 +37,7 @@ import StatusUpdateSuccessModal from './StatusUpdateSuccessModal';
 export interface HotspotViewerRendererProps {
   component: Component;
   currentUser: CurrentUser;
-  cve?: Cve;
+  cveId?: string;
   hotspot?: Hotspot;
   hotspotsReviewedMeasure?: string;
   lastStatusChangedTo?: HotspotStatusOption;
@@ -64,7 +63,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
     loading,
     ruleDescriptionSections,
     ruleLanguage,
-    cve,
+    cveId,
     selectedHotspotLocation,
     showStatusUpdateSuccessModal,
     standards,
@@ -115,7 +114,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
             onUpdateHotspot={props.onUpdateHotspot}
             ruleDescriptionSections={ruleDescriptionSections}
             ruleLanguage={ruleLanguage}
-            cve={cve}
+            cveId={cveId}
           />
         </div>
       )}
index 31e347c21bc67e8fd878c6f89316564e47e53d88..057f225f6e51a40474e7b39458ca656002da7d2a 100644 (file)
@@ -35,7 +35,6 @@ import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
 import { KeyboardKeys } from '../../../helpers/keycodes';
 import { translate } from '../../../helpers/l10n';
 import { useRefreshBranchStatus } from '../../../queries/branch';
-import { Cve } from '../../../types/cves';
 import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots';
 import { RuleDescriptionSection, RuleDescriptionSections } from '../../coding-rules/rule';
 import useStickyDetection from '../hooks/useStickyDetection';
@@ -44,7 +43,7 @@ import StatusReviewButton from './status/StatusReviewButton';
 interface Props {
   activityTabContent: React.ReactNode;
   codeTabContent: React.ReactNode;
-  cve: Cve | undefined;
+  cveId?: string;
   hotspot: Hotspot;
   onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
   ruleDescriptionSections?: RuleDescriptionSection[];
@@ -74,7 +73,7 @@ export default function HotspotViewerTabs(props: Props) {
     hotspot,
     ruleDescriptionSections,
     ruleLanguage,
-    cve,
+    cveId,
   } = props;
 
   const { component } = useComponent();
@@ -217,7 +216,7 @@ export default function HotspotViewerTabs(props: Props) {
           <RuleDescription
             language={ruleLanguage}
             sections={rootCauseDescriptionSections}
-            cve={cve}
+            cveId={cveId}
           />
         )}
 
index 48dd46cf9891e34c71e9d938436de6c508243509..f81b7d994f649aadc0645e2b9095ce31ee0a5178 100644 (file)
@@ -32,7 +32,6 @@ import StyledHeader from '../../apps/issues/components/StyledHeader';
 import { fillBranchLike } from '../../helpers/branch-like';
 import { translate } from '../../helpers/l10n';
 import { withUseGetFixSuggestionsIssues } from '../../queries/fix-suggestions';
-import { Cve } from '../../types/cves';
 import { Issue, RuleDetails } from '../../types/types';
 import { CurrentUser, NoticeType } from '../../types/users';
 import ScreenPositionHelper from '../common/ScreenPositionHelper';
@@ -46,7 +45,7 @@ interface IssueTabViewerProps extends CurrentUserContextInterface {
   aiSuggestionAvailable: boolean;
   codeTabContent?: React.ReactNode;
   currentUser: CurrentUser;
-  cve?: Cve;
+  cveId?: string;
   extendedDescription?: string;
   issue: Issue;
   location: Location;
@@ -199,7 +198,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
       ruleDescriptionContextKey,
       extendedDescription,
       activityTabContent,
-      cve,
+      cveId,
       issue,
       suggestionTabContent,
       aiSuggestionAvailable,
@@ -243,7 +242,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
               descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ??
               descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
             ).concat(descriptionSectionsByKey[RuleDescriptionSections.INTRODUCTION] ?? [])}
-            cve={cve}
+            cveId={cveId}
           />
         ),
       },
index a5cf16b7a96a36675d1292b2f74cc2beff67f8af..428f65a9ecdab9091c40a45a9cb86a7fcdd9a81f 100644 (file)
@@ -32,7 +32,7 @@ import { RuleDescriptionSection, RuleDescriptionSections } from '../../apps/codi
 import applyCodeDifferences from '../../helpers/code-difference';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { isDefined } from '../../helpers/types';
-import { Cve as CveDetailsType } from '../../types/cves';
+import { useCveQuery } from '../../queries/cves';
 import { CveDetails } from './CveDetails';
 import OtherContextOption from './OtherContextOption';
 
@@ -40,41 +40,36 @@ const OTHERS_KEY = 'others';
 
 interface Props {
   className?: string;
-  cve?: CveDetailsType;
+  cveId?: string;
   defaultContextKey?: string;
   language?: string;
   sections: RuleDescriptionSection[];
 }
 
-interface State {
-  contexts: RuleDescriptionContextDisplay[];
-  defaultContext?: RuleDescriptionContextDisplay;
-  selectedContext?: RuleDescriptionContextDisplay;
-}
-
 interface RuleDescriptionContextDisplay {
   content: string;
   displayName: string;
   key: string;
 }
 
-export default class RuleDescription extends React.PureComponent<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = this.computeState();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    const { sections, defaultContextKey } = this.props;
-
-    if (prevProps.sections !== sections || prevProps.defaultContextKey !== defaultContextKey) {
-      this.setState(this.computeState());
-    }
-  }
-
-  computeState = () => {
-    const { sections, defaultContextKey } = this.props;
-
+export default function RuleDescription({
+  className,
+  cveId,
+  defaultContextKey,
+  language,
+  sections,
+}: Readonly<Props>) {
+  const [contexts, setContexts] = React.useState<RuleDescriptionContextDisplay[]>([]);
+  const [defaultContext, setDefaultContext] = React.useState<
+    RuleDescriptionContextDisplay | undefined
+  >();
+  const [selectedContext, setSelectedContext] = React.useState<
+    RuleDescriptionContextDisplay | undefined
+  >();
+
+  const { data: cveData } = useCveQuery({ cveId });
+
+  React.useEffect(() => {
     const contexts = sections
       .filter(
         (
@@ -103,95 +98,28 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
       defaultContext = contexts.find((context) => context.key === defaultContextKey);
     }
 
-    return {
-      contexts,
-      defaultContext,
-      selectedContext: defaultContext ?? contexts[0],
-    };
-  };
-
-  handleToggleContext = (value: string) => {
-    const { contexts } = this.state;
+    setContexts(contexts);
+    setDefaultContext(defaultContext);
+    setSelectedContext(defaultContext ?? contexts[0]);
+  }, [sections, defaultContextKey]);
 
+  const handleToggleContext = (value: string) => {
     const selected = contexts.find((ctxt) => ctxt.displayName === value);
-
     if (selected) {
-      this.setState({ selectedContext: selected });
+      setSelectedContext(selected);
     }
   };
 
-  render() {
-    const { className, language, sections, cve } = this.props;
-    const { contexts, defaultContext, selectedContext } = this.state;
-
-    const introductionSection = sections?.find(
-      (section) => section.key === RuleDescriptionSections.INTRODUCTION,
-    )?.content;
+  const introductionSection = sections?.find(
+    (section) => section.key === RuleDescriptionSections.INTRODUCTION,
+  )?.content;
 
-    const options = contexts.map((ctxt) => ({
-      label: ctxt.displayName,
-      value: ctxt.displayName,
-    }));
-
-    if (contexts.length > 0 && selectedContext) {
-      return (
-        <StyledHtmlFormatter
-          className={className}
-          ref={(node: HTMLDivElement) => {
-            applyCodeDifferences(node);
-          }}
-        >
-          <h2 className="sw-typo-semibold sw-mb-4">
-            {translate('coding_rules.description_context.title')}
-          </h2>
-          {isDefined(introductionSection) && (
-            <CodeSyntaxHighlighter
-              className="rule-desc"
-              htmlAsString={introductionSection}
-              language={language}
-              sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
-            />
-          )}
-          {defaultContext && (
-            <FlagMessage variant="info" className="sw-mb-4">
-              {translateWithParameters(
-                'coding_rules.description_context.default_information',
-                defaultContext.displayName,
-              )}
-            </FlagMessage>
-          )}
-          <div className="sw-mb-4">
-            <ToggleButton
-              label={translate('coding_rules.description_context.title')}
-              onChange={this.handleToggleContext}
-              options={options}
-              value={selectedContext.displayName}
-            />
-
-            {selectedContext.key !== OTHERS_KEY && (
-              <h2>
-                {translateWithParameters(
-                  'coding_rules.description_context.subtitle',
-                  selectedContext.displayName,
-                )}
-              </h2>
-            )}
-          </div>
-          {selectedContext.key === OTHERS_KEY ? (
-            <OtherContextOption />
-          ) : (
-            <CodeSyntaxHighlighter
-              htmlAsString={selectedContext.content}
-              language={language}
-              sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
-            />
-          )}
-
-          {cve && <CveDetails cve={cve} />}
-        </StyledHtmlFormatter>
-      );
-    }
+  const options = contexts.map((ctxt) => ({
+    label: ctxt.displayName,
+    value: ctxt.displayName,
+  }));
 
+  if (contexts.length > 0 && selectedContext) {
     return (
       <StyledHtmlFormatter
         className={className}
@@ -199,6 +127,9 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
           applyCodeDifferences(node);
         }}
       >
+        <h2 className="sw-typo-semibold sw-mb-4">
+          {translate('coding_rules.description_context.title')}
+        </h2>
         {isDefined(introductionSection) && (
           <CodeSyntaxHighlighter
             className="rule-desc"
@@ -207,17 +138,71 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
             sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
           />
         )}
+        {defaultContext && (
+          <FlagMessage variant="info" className="sw-mb-4">
+            {translateWithParameters(
+              'coding_rules.description_context.default_information',
+              defaultContext.displayName,
+            )}
+          </FlagMessage>
+        )}
+        <div className="sw-mb-4">
+          <ToggleButton
+            label={translate('coding_rules.description_context.title')}
+            onChange={handleToggleContext}
+            options={options}
+            value={selectedContext.displayName}
+          />
 
+          {selectedContext.key !== OTHERS_KEY && (
+            <h2>
+              {translateWithParameters(
+                'coding_rules.description_context.subtitle',
+                selectedContext.displayName,
+              )}
+            </h2>
+          )}
+        </div>
+        {selectedContext.key === OTHERS_KEY ? (
+          <OtherContextOption />
+        ) : (
+          <CodeSyntaxHighlighter
+            htmlAsString={selectedContext.content}
+            language={language}
+            sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
+          />
+        )}
+
+        {cveData && <CveDetails cve={cveData} />}
+      </StyledHtmlFormatter>
+    );
+  }
+
+  return (
+    <StyledHtmlFormatter
+      className={className}
+      ref={(node: HTMLDivElement) => {
+        applyCodeDifferences(node);
+      }}
+    >
+      {isDefined(introductionSection) && (
         <CodeSyntaxHighlighter
-          htmlAsString={sections[0].content}
+          className="rule-desc"
+          htmlAsString={introductionSection}
           language={language}
           sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
         />
+      )}
 
-        {cve && <CveDetails cve={cve} />}
-      </StyledHtmlFormatter>
-    );
-  }
+      <CodeSyntaxHighlighter
+        htmlAsString={sections[0].content}
+        language={language}
+        sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
+      />
+
+      {cveData && <CveDetails cve={cveData} />}
+    </StyledHtmlFormatter>
+  );
 }
 
 const StyledHtmlFormatter = styled(HtmlFormatter)`
diff --git a/server/sonar-web/src/main/js/queries/cves.ts b/server/sonar-web/src/main/js/queries/cves.ts
new file mode 100644 (file)
index 0000000..1ad1b89
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { queryOptions } from '@tanstack/react-query';
+import { getCve } from '../api/cves';
+import { createQueryHook, StaleTime } from './common';
+
+const KEY_PREFIX = 'cve';
+
+export const useCveQuery = createQueryHook(({ cveId }: { cveId?: string }) => {
+  return queryOptions({
+    queryKey: [KEY_PREFIX, cveId],
+    queryFn: () => getCve(cveId!),
+    staleTime: StaleTime.NEVER,
+    enabled: !!cveId,
+  });
+});
index 47294f6124f29cd2efb243978f1ff492c76121cd..46baf6bf2174e66ff78ed8f972a041bfb92df903 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { UseQueryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { createRule, deleteRule, getRuleDetails, searchRules, updateRule } from '../api/rules';
 import { mapRestRuleToRule } from '../apps/coding-rules/utils';
 import { SearchRulesResponse } from '../types/coding-rules';
 import { SearchRulesQuery } from '../types/rules';
 import { RuleActivation, RuleDetails } from '../types/types';
+import { createQueryHook, StaleTime } from './common';
 
 function getRulesQueryKey(type: 'search' | 'details', data?: SearchRulesQuery | string) {
   const key = ['rules', type] as (string | SearchRulesQuery)[];
@@ -42,22 +43,17 @@ export function useSearchRulesQuery(data: SearchRulesQuery) {
 
       return searchRules(data);
     },
+    staleTime: StaleTime.NEVER,
   });
 }
 
-export function useRuleDetailsQuery<T = Awaited<ReturnType<typeof getRuleDetails>>>(
-  data: { actives?: boolean; key: string },
-  options?: Omit<
-    UseQueryOptions<Awaited<ReturnType<typeof getRuleDetails>>, Error, T>,
-    'queryKey' | 'queryFn'
-  >,
-) {
-  return useQuery({
+export const useRuleDetailsQuery = createQueryHook((data: { actives?: boolean; key: string }) => {
+  return queryOptions({
     queryKey: getRulesQueryKey('details', data.key),
     queryFn: () => getRuleDetails(data),
-    ...options,
+    staleTime: StaleTime.NEVER,
   });
-}
+});
 
 export function useCreateRuleMutation(
   searchQuery?: SearchRulesQuery,