},
}));
+jest.mock('../../../api/cves', () => ({
+ getCve: jest.fn(),
+}));
+
beforeEach(() => {
issuesHandler.reset();
cveHandler.reset();
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';
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
>
<A11ySkipTarget anchor="issues_main" />
- <Spinner isLoading={loadingRule}>
+ <Spinner isLoading={isLoadingRule}>
{openRuleDetails && (
<IssueTabViewer
activityTabContent={
onIssueChange={handleIssueChange}
ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey}
ruleDetails={openRuleDetails}
- cve={cve}
+ cveId={openIssue.cveId}
selectedFlowIndex={selectedFlowIndex}
selectedLocationIndex={selectedLocationIndex}
/>
* 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,
}
interface State {
- cve?: Cve;
hotspot?: Hotspot;
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean;
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({
loading: false,
ruleLanguage: ruleDetails.lang,
ruleDescriptionSections: ruleDetails.descriptionSections,
- cve,
});
}
} catch (error) {
};
render() {
- const { component, hotspotsReviewedMeasure, selectedHotspotLocation, standards } = this.props;
+ const { component, cveId, hotspotsReviewedMeasure, selectedHotspotLocation, standards } =
+ this.props;
const {
hotspot,
ruleDescriptionSections,
ruleLanguage,
- cve,
loading,
showStatusUpdateSuccessModal,
lastStatusChangedTo,
onUpdateHotspot={this.handleHotspotUpdate}
ruleDescriptionSections={ruleDescriptionSections}
ruleLanguage={ruleLanguage}
- cve={cve}
+ cveId={cveId}
selectedHotspotLocation={selectedHotspotLocation}
showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
standards={standards}
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';
export interface HotspotViewerRendererProps {
component: Component;
currentUser: CurrentUser;
- cve?: Cve;
+ cveId?: string;
hotspot?: Hotspot;
hotspotsReviewedMeasure?: string;
lastStatusChangedTo?: HotspotStatusOption;
loading,
ruleDescriptionSections,
ruleLanguage,
- cve,
+ cveId,
selectedHotspotLocation,
showStatusUpdateSuccessModal,
standards,
onUpdateHotspot={props.onUpdateHotspot}
ruleDescriptionSections={ruleDescriptionSections}
ruleLanguage={ruleLanguage}
- cve={cve}
+ cveId={cveId}
/>
</div>
)}
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';
interface Props {
activityTabContent: React.ReactNode;
codeTabContent: React.ReactNode;
- cve: Cve | undefined;
+ cveId?: string;
hotspot: Hotspot;
onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
ruleDescriptionSections?: RuleDescriptionSection[];
hotspot,
ruleDescriptionSections,
ruleLanguage,
- cve,
+ cveId,
} = props;
const { component } = useComponent();
<RuleDescription
language={ruleLanguage}
sections={rootCauseDescriptionSections}
- cve={cve}
+ cveId={cveId}
/>
)}
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';
aiSuggestionAvailable: boolean;
codeTabContent?: React.ReactNode;
currentUser: CurrentUser;
- cve?: Cve;
+ cveId?: string;
extendedDescription?: string;
issue: Issue;
location: Location;
ruleDescriptionContextKey,
extendedDescription,
activityTabContent,
- cve,
+ cveId,
issue,
suggestionTabContent,
aiSuggestionAvailable,
descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ??
descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
).concat(descriptionSectionsByKey[RuleDescriptionSections.INTRODUCTION] ?? [])}
- cve={cve}
+ cveId={cveId}
/>
),
},
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';
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(
(
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}
applyCodeDifferences(node);
}}
>
+ <h2 className="sw-typo-semibold sw-mb-4">
+ {translate('coding_rules.description_context.title')}
+ </h2>
{isDefined(introductionSection) && (
<CodeSyntaxHighlighter
className="rule-desc"
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)`
--- /dev/null
+/*
+ * 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,
+ });
+});
* 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)[];
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,