From 33d6dff86f9fc395b82d06e1e031e1f5a889dc5f Mon Sep 17 00:00:00 2001 From: Revanshu Paliwal Date: Mon, 7 Oct 2024 12:37:13 +0200 Subject: [PATCH] SGB-163 Fetching rules and cve inside issue details page using react query --- .../js/apps/issues/__tests__/IssueApp-it.tsx | 4 + .../apps/issues/components/IssueDetails.tsx | 36 +-- .../components/HotspotViewer.tsx | 14 +- .../components/HotspotViewerRenderer.tsx | 7 +- .../components/HotspotViewerTabs.tsx | 7 +- .../js/components/rules/IssueTabViewer.tsx | 7 +- .../js/components/rules/RuleDescription.tsx | 207 ++++++++---------- server/sonar-web/src/main/js/queries/cves.ts | 33 +++ server/sonar-web/src/main/js/queries/rules.ts | 18 +- 9 files changed, 159 insertions(+), 174 deletions(-) create mode 100644 server/sonar-web/src/main/js/queries/cves.ts diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx index 178b2ef5a68..2eee2ba88b5 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx @@ -63,6 +63,10 @@ jest.mock('../../../components/common/ScreenPositionHelper', () => ({ }, })); +jest.mock('../../../api/cves', () => ({ + getCve: jest.fn(), +})); + beforeEach(() => { issuesHandler.reset(); cveHandler.reset(); diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx index 2e03f52fa6c..414426f0a50 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx @@ -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) { - const [loadingRule, setLoadingRule] = React.useState(false); - const [openRuleDetails, setOpenRuleDetails] = React.useState(undefined); - const [cve, setCve] = React.useState(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) && ( - + {openRuleDetails && ( diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx index 30efacc1fdd..e56229dc648 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx @@ -18,11 +18,9 @@ * 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 { 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 { loading: false, ruleLanguage: ruleDetails.lang, ruleDescriptionSections: ruleDetails.descriptionSections, - cve, }); } } catch (error) { @@ -135,13 +127,13 @@ export default class HotspotViewer extends React.PureComponent { }; 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 { onUpdateHotspot={this.handleHotspotUpdate} ruleDescriptionSections={ruleDescriptionSections} ruleLanguage={ruleLanguage} - cve={cve} + cveId={cveId} selectedHotspotLocation={selectedHotspotLocation} showStatusUpdateSuccessModal={showStatusUpdateSuccessModal} standards={standards} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx index acc3a9e4fdd..a3ee2d8feff 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx @@ -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} /> )} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx index 31e347c21bc..057f225f6e5 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx @@ -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; 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) { )} diff --git a/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx b/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx index 48dd46cf989..f81b7d994f6 100644 --- a/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx +++ b/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx @@ -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 ), }, diff --git a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx index a5cf16b7a96..428f65a9ecd 100644 --- a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx +++ b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx @@ -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 { - 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) { + const [contexts, setContexts] = React.useState([]); + 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 { 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 ( - { - applyCodeDifferences(node); - }} - > -

- {translate('coding_rules.description_context.title')} -

- {isDefined(introductionSection) && ( - - )} - {defaultContext && ( - - {translateWithParameters( - 'coding_rules.description_context.default_information', - defaultContext.displayName, - )} - - )} -
- - - {selectedContext.key !== OTHERS_KEY && ( -

- {translateWithParameters( - 'coding_rules.description_context.subtitle', - selectedContext.displayName, - )} -

- )} -
- {selectedContext.key === OTHERS_KEY ? ( - - ) : ( - - )} - - {cve && } -
- ); - } + const options = contexts.map((ctxt) => ({ + label: ctxt.displayName, + value: ctxt.displayName, + })); + if (contexts.length > 0 && selectedContext) { return ( { applyCodeDifferences(node); }} > +

+ {translate('coding_rules.description_context.title')} +

{isDefined(introductionSection) && ( { sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} /> )} + {defaultContext && ( + + {translateWithParameters( + 'coding_rules.description_context.default_information', + defaultContext.displayName, + )} + + )} +
+ + {selectedContext.key !== OTHERS_KEY && ( +

+ {translateWithParameters( + 'coding_rules.description_context.subtitle', + selectedContext.displayName, + )} +

+ )} +
+ {selectedContext.key === OTHERS_KEY ? ( + + ) : ( + + )} + + {cveData && } +
+ ); + } + + return ( + { + applyCodeDifferences(node); + }} + > + {isDefined(introductionSection) && ( + )} - {cve && } - - ); - } + + + {cveData && } + + ); } 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 index 00000000000..1ad1b89a95b --- /dev/null +++ b/server/sonar-web/src/main/js/queries/cves.ts @@ -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, + }); +}); diff --git a/server/sonar-web/src/main/js/queries/rules.ts b/server/sonar-web/src/main/js/queries/rules.ts index 47294f6124f..46baf6bf217 100644 --- a/server/sonar-web/src/main/js/queries/rules.ts +++ b/server/sonar-web/src/main/js/queries/rules.ts @@ -17,12 +17,13 @@ * 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>>( - data: { actives?: boolean; key: string }, - options?: Omit< - UseQueryOptions>, 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, -- 2.39.5