From 2124f36a93c6a190a58048e4dbd06b7983bca594 Mon Sep 17 00:00:00 2001 From: Revanshu Paliwal Date: Mon, 30 Sep 2024 11:50:51 +0200 Subject: SGB-163 Moving IssueDetails out of IssuesApp --- .../js/apps/issues/components/IssueDetails.tsx | 259 +++++++++++++++ .../main/js/apps/issues/components/IssuesApp.tsx | 357 +++++++-------------- 2 files changed, 375 insertions(+), 241 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx 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 new file mode 100644 index 00000000000..2e03f52fa6c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx @@ -0,0 +1,259 @@ +/* + * 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 styled from '@emotion/styled'; +import { Spinner } from '@sonarsource/echoes-react'; +import { + FlagMessage, + LargeCenteredLayout, + LAYOUT_FOOTER_HEIGHT, + PageContentFontWrapper, + themeBorder, + themeColor, +} from 'design-system'; +import React, { useEffect } 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 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 SubnavigationIssuesList from '../issues-subnavigation/SubnavigationIssuesList'; +import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments'; +import IssuesSourceViewer from './IssuesSourceViewer'; + +interface IssueDetailsProps { + component?: Component; + fetchMoreIssues: () => void; + handleIssueChange: (issue: Issue) => void; + handleOpenIssue: (issueKey: string) => void; + issues: Issue[]; + loading: boolean; + loadingMore: boolean; + locationsNavigator: boolean; + openIssue: Issue; + paging?: Paging; + selectFlow: (flowIndex: number) => void; + selectLocation: (locationIndex: number) => void; + selected?: string; + selectedFlowIndex?: number; + selectedLocationIndex?: number; +} + +export default function IssueDetails({ + handleOpenIssue, + handleIssueChange, + openIssue, + component, + fetchMoreIssues, + selectFlow, + selectLocation, + issues, + loading, + loadingMore, + locationsNavigator, + paging, + selected, + 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 warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( + + {translate('issues.not_all_issue_show')} + + ); + + return ( + + + +
+ +

{translate('issues.page')}

+ + + + {({ top }) => ( + +
+ +
+ {warning &&
{warning}
} + + +
+
+
+ )} +
+
+ +
+ + {({ top }) => ( + + + + + {openRuleDetails && ( + + } + codeTabContent={ + + } + suggestionTabContent={ + + } + extendedDescription={openRuleDetails.htmlNote} + issue={openIssue} + onIssueChange={handleIssueChange} + ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey} + ruleDetails={openRuleDetails} + cve={cve} + selectedFlowIndex={selectedFlowIndex} + selectedLocationIndex={selectedLocationIndex} + /> + )} + + + )} + +
+
+
+
+
+ ); +} + +const PageWrapperStyle = styled.div` + background-color: ${themeColor('backgroundPrimary')}; +`; + +const SideBarStyle = styled.div` + border-left: ${themeBorder('default', 'filterbarBorder')}; + border-right: ${themeBorder('default', 'filterbarBorder')}; + background-color: ${themeColor('backgroundSecondary')}; +`; + +const StyledIssueWrapper = styled.div` + &.details-open { + box-sizing: border-box; + border-radius: 4px; + border: ${themeBorder('default', 'filterbarBorder')}; + background-color: ${themeColor('filterbar')}; + border-bottom: none; + border-top: none; + } +`; + +const StyledNav = styled.nav` + /* +* On Firefox on Windows, the scrollbar hides the sidebar's content. +* Using 'scrollbar-gutter:stable' is a workaround to ensure consistency with other browsers. +* @see https://bugzilla.mozilla.org/show_bug.cgi?id=764076 +* @see https://discuss.sonarsource.com/t/unnecessary-horizontal-scrollbar-on-issues-page/14889/4 +*/ + scrollbar-gutter: stable; +`; diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index c9995f74bc7..2cd490c3c0b 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -20,7 +20,6 @@ import styled from '@emotion/styled'; import { Checkbox, Spinner } from '@sonarsource/echoes-react'; -import classNames from 'classnames'; import { ButtonSecondary, FlagMessage, @@ -41,9 +40,7 @@ import { getBranchLikeQuery, isPullRequest } from '~sonar-aligned/helpers/branch import { isPortfolioLike } from '~sonar-aligned/helpers/component'; import { ComponentQualifier } from '~sonar-aligned/types/component'; import { Location, RawQuery, Router } from '~sonar-aligned/types/router'; -import { getCve } from '../../../api/cves'; import { listIssues, searchIssues } from '../../../api/issues'; -import { getRuleDetails } from '../../../api/rules'; import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import EmptySearch from '../../../components/common/EmptySearch'; @@ -53,11 +50,9 @@ import withIndexationContext, { WithIndexationContextProps, } from '../../../components/hoc/withIndexationContext'; import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; -import { IssueSuggestionCodeTab } from '../../../components/rules/IssueSuggestionCodeTab'; -import IssueTabViewer from '../../../components/rules/IssueTabViewer'; import '../../../components/search-navigator.css'; import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; -import { fillBranchLike, isSameBranchLike } from '../../../helpers/branch-like'; +import { isSameBranchLike } from '../../../helpers/branch-like'; import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; import { parseIssueFromResponse } from '../../../helpers/issues'; import { isDropdown, isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; @@ -67,7 +62,6 @@ import { serializeDate } from '../../../helpers/query'; import { withBranchLikes } from '../../../queries/branch'; import { BranchLike } from '../../../types/branch-like'; import { isProject } from '../../../types/component'; -import { Cve } from '../../../types/cves'; import { ASSIGNEE_ME, Facet, @@ -77,10 +71,9 @@ import { ReferencedRule, } from '../../../types/issues'; import { SecurityStandard } from '../../../types/security'; -import { Component, Dict, Issue, Paging, RuleDetails } from '../../../types/types'; +import { Component, Dict, Issue, Paging } from '../../../types/types'; import { CurrentUser, UserBase } from '../../../types/users'; import * as actions from '../actions'; -import SubnavigationIssuesList from '../issues-subnavigation/SubnavigationIssuesList'; import { FiltersHeader } from '../sidebar/FiltersHeader'; import { Sidebar } from '../sidebar/Sidebar'; import '../styles.css'; @@ -100,12 +93,11 @@ import { shouldOpenStandardsFacet, } from '../utils'; import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal'; +import IssueDetails from './IssueDetails'; import IssueGuide from './IssueGuide'; import IssueNewStatusAndTransitionGuide from './IssueNewStatusAndTransitionGuide'; -import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments'; import IssuesList from './IssuesList'; import IssuesListTitle from './IssuesListTitle'; -import IssuesSourceViewer from './IssuesSourceViewer'; import NoIssues from './NoIssues'; import NoMyIssues from './NoMyIssues'; import PageActions from './PageActions'; @@ -124,20 +116,17 @@ export interface State { bulkChangeModal: boolean; checkAll?: boolean; checked: string[]; - cve?: Cve; effortTotal?: number; facets: Dict; issues: Issue[]; loading: boolean; loadingFacets: Dict; loadingMore: boolean; - loadingRule: boolean; locationsNavigator: boolean; myIssues: boolean; openFacets: Dict; openIssue?: Issue; openPopup?: { issue: string; name: string }; - openRuleDetails?: RuleDetails; paging?: Paging; query: Query; referencedComponentsById: Dict; @@ -174,7 +163,6 @@ export class App extends React.PureComponent { loading: true, loadingFacets: {}, loadingMore: false, - loadingRule: false, locationsNavigator: false, myIssues: areMyIssuesSelected(props.location.query), openFacets: { @@ -230,7 +218,7 @@ export class App extends React.PureComponent { } } - componentDidUpdate(prevProps: Props, prevState: State) { + componentDidUpdate(prevProps: Props) { const { query } = this.props.location; const { query: prevQuery } = prevProps.location; const { openIssue } = this.state; @@ -257,10 +245,6 @@ export class App extends React.PureComponent { selectedLocationIndex: undefined, }); } - - if (this.state.openIssue && this.state.openIssue.key !== prevState.openIssue?.key) { - this.loadRule().catch(() => undefined); - } } componentWillUnmount() { @@ -373,29 +357,6 @@ export class App extends React.PureComponent { } }; - async loadRule() { - const { openIssue } = this.state; - - if (openIssue === undefined) { - return; - } - - this.setState({ loadingRule: 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); - } - - if (this.mounted) { - this.setState({ loadingRule: false, openRuleDetails, cve }); - } - } - selectPreviousIssue = () => { const { issues } = this.state; const selectedIndex = this.getSelectedIndex(); @@ -1057,87 +1018,13 @@ export class App extends React.PureComponent { ); } - renderSide(openIssue: Issue | undefined) { - const { canBrowseAllChildProjects, qualifier = ComponentQualifier.Project } = - this.props.component ?? {}; - - const { - issues, - loading, - loadingMore, - paging, - selected, - selectedFlowIndex, - selectedLocationIndex, - } = this.state; - - const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( - - {translate('issues.not_all_issue_show')} - - ); - - return ( - - - {({ top }) => ( - -
- - - {openIssue ? ( -
- {warning &&
{warning}
} - - -
- ) : ( - this.renderFacets(warning) - )} -
-
- )} -
-
- ); - } - renderList() { const { branchLike, component, currentUser, branchLikes } = this.props; - const { issues, loading, loadingMore, openIssue, paging, query } = this.state; + const { issues, loading, loadingMore, paging, query } = this.state; const selectedIndex = this.getSelectedIndex(); const selectedIssue = selectedIndex !== undefined ? issues[selectedIndex] : undefined; - if (!paging || openIssue) { + if (!paging) { return null; } @@ -1193,113 +1080,50 @@ export class App extends React.PureComponent { ); } - renderHeader({ - openIssue, - paging, - }: { - openIssue: Issue | undefined; - paging: Paging | undefined; - }) { - return openIssue ? ( - - ) : ( - <> - -
- {this.renderBulkChange()} - - -
- - ); - } - - renderPage() { - const { openRuleDetails, cve, checkAll, issues, loading, openIssue, paging, loadingRule } = - this.state; + renderIssueList() { + const { checkAll, loading, paging } = this.state; return ( {({ top }) => ( - {this.renderHeader({ openIssue, paging })} - - - {openIssue && openRuleDetails ? ( - - } - codeTabContent={ - - } - suggestionTabContent={ - - } - extendedDescription={openRuleDetails.htmlNote} - issue={openIssue} - onIssueChange={this.handleIssueChange} - ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey} - ruleDetails={openRuleDetails} - cve={cve} - selectedFlowIndex={this.state.selectedFlowIndex} - selectedLocationIndex={this.state.selectedLocationIndex} - /> - ) : ( -
- - {checkAll && paging && paging.total > MAX_PAGE_SIZE && ( -
- - - {MAX_PAGE_SIZE} }} - /> - - -
- )} + +
+ {this.renderBulkChange()} + + +
- {this.renderList()} -
-
- )} -
+
+ + {checkAll && paging && paging.total > MAX_PAGE_SIZE && ( +
+ + + {MAX_PAGE_SIZE} }} + /> + + +
+ )} + + {this.renderList()} +
+
)}
@@ -1307,41 +1131,92 @@ export class App extends React.PureComponent { } render() { - const { openIssue, issues } = this.state; + const { + openIssue, + issues, + selectedFlowIndex, + selectedLocationIndex, + loading, + loadingMore, + paging, + selected, + locationsNavigator, + } = this.state; const { component, location } = this.props; const open = getOpen(location.query); + const { canBrowseAllChildProjects, qualifier = ComponentQualifier.Project } = + this.props.component ?? {}; + const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( + + {translate('issues.not_all_issue_show')} + + ); + + if (openIssue) { + return ( + + ); + } return (
- {openIssue ? ( - - ) : ( - <> - - 0} /> - 0} - togglePopup={this.handlePopupToggle} - issues={issues} - /> - - )} + + 0} /> + 0} + togglePopup={this.handlePopupToggle} + issues={issues} + />

{translate('issues.page')}

- {this.renderSide(openIssue)} + + + {({ top }) => ( + +
+ + + {this.renderFacets(warning)} +
+
+ )} +
+
-
{this.renderPage()}
+
{this.renderIssueList()}
-- cgit v1.2.3