/* * SonarQube * Copyright (C) 2009-2023 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 classNames from 'classnames'; import { ButtonSecondary, Checkbox, FlagMessage, LAYOUT_FOOTER_HEIGHT, LargeCenteredLayout, PageContentFontWrapper, ToggleButton, themeBorder, themeColor, } from 'design-system'; import { keyBy, omit, without } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; 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 A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import EmptySearch from '../../../components/common/EmptySearch'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import ListFooter from '../../../components/controls/ListFooter'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import withIndexationContext, { WithIndexationContextProps, } from '../../../components/hoc/withIndexationContext'; import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import IssueTabViewer from '../../../components/rules/IssueTabViewer'; import '../../../components/search-navigator.css'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { fillBranchLike, getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; import { parseIssueFromResponse } from '../../../helpers/issues'; import { isDatePicker, isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { addSideBarClass, addWhitePageClass, removeSideBarClass, removeWhitePageClass, } from '../../../helpers/pages'; import { serializeDate } from '../../../helpers/query'; import { withBranchLikes } from '../../../queries/branch'; import { BranchLike } from '../../../types/branch-like'; import { ComponentQualifier, isPortfolioLike, isProject } from '../../../types/component'; import { ASSIGNEE_ME, Facet, FetchIssuesPromise, ReferencedComponent, ReferencedLanguage, ReferencedRule, } from '../../../types/issues'; import { SecurityStandard } from '../../../types/security'; import { Component, Dict, Issue, Paging, RawQuery, RuleDetails } 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'; import { Query, STANDARDS, areMyIssuesSelected, areQueriesEqual, getOpen, getOpenIssue, parseFacets, parseQuery, saveMyIssues, serializeQuery, shouldOpenSonarSourceSecurityFacet, shouldOpenStandardsChildFacet, shouldOpenStandardsFacet, } from '../utils'; import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal'; import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments'; import IssuesList from './IssuesList'; import IssuesSourceViewer from './IssuesSourceViewer'; import NoIssues from './NoIssues'; import NoMyIssues from './NoMyIssues'; import PageActions from './PageActions'; import StyledHeader, { PSEUDO_SHADOW_HEIGHT } from './StyledHeader'; interface Props extends WithIndexationContextProps { branchLike?: BranchLike; component?: Component; currentUser: CurrentUser; isFetchingBranch?: boolean; location: Location; router: Router; } export interface State { bulkChangeModal: boolean; cannotShowOpenIssue?: boolean; checkAll?: boolean; checked: string[]; effortTotal?: number; facets: Dict; issues: Issue[]; loading: boolean; loadingRule: boolean; loadingFacets: Dict; loadingMore: boolean; locationsNavigator: boolean; myIssues: boolean; openFacets: Dict; showVariantsFilter: boolean; openIssue?: Issue; openPopup?: { issue: string; name: string }; openRuleDetails?: RuleDetails; paging?: Paging; query: Query; referencedComponentsById: Dict; referencedComponentsByKey: Dict; referencedLanguages: Dict; referencedRules: Dict; referencedUsers: Dict; selected?: string; selectedFlowIndex?: number; selectedLocationIndex?: number; } const DEFAULT_QUERY = { resolved: 'false' }; const MAX_INITAL_FETCH = 1000; const VARIANTS_FACET = 'codeVariants'; const ISSUES_PAGE_SIZE = 100; export class App extends React.PureComponent { mounted = false; bulkButtonRef: React.RefObject; constructor(props: Props) { super(props); const query = parseQuery(props.location.query); this.bulkButtonRef = React.createRef(); this.state = { bulkChangeModal: false, checked: [], facets: {}, issues: [], loading: true, loadingFacets: {}, loadingMore: false, loadingRule: false, locationsNavigator: false, myIssues: areMyIssuesSelected(props.location.query), openFacets: { owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10), 'owaspTop10-2021': shouldOpenStandardsChildFacet( {}, query, SecurityStandard.OWASP_TOP10_2021 ), severities: true, sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query), standards: shouldOpenStandardsFacet({}, query), types: true, }, query, referencedComponentsById: {}, referencedComponentsByKey: {}, referencedLanguages: {}, referencedRules: {}, referencedUsers: {}, selected: getOpen(props.location.query), showVariantsFilter: false, }; } static getDerivedStateFromProps(props: Props, state: State) { const { location: { query }, } = props; return { myIssues: areMyIssuesSelected(query), openIssue: getOpenIssue(props, state.issues), query: parseQuery(query), }; } componentDidMount() { this.mounted = true; if (this.state.myIssues && !this.props.currentUser.isLoggedIn) { handleRequiredAuthentication(); return; } addWhitePageClass(); addSideBarClass(); this.attachShortcuts(); if (!this.props.isFetchingBranch) { this.fetchFirstIssues(true).catch(() => undefined); } } componentDidUpdate(prevProps: Props, prevState: State) { const { query } = this.props.location; const { query: prevQuery } = prevProps.location; const { openIssue } = this.state; if ( prevProps.component !== this.props.component || !isSameBranchLike(prevProps.branchLike, this.props.branchLike) || !areQueriesEqual(prevQuery, query) || areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query) ) { this.fetchFirstIssues(false).catch(() => undefined); this.setState({ checkAll: false }); } else if (openIssue && openIssue.key !== this.state.selected) { this.setState({ locationsNavigator: true, selected: openIssue.key, selectedFlowIndex: 0, selectedLocationIndex: undefined, }); } if (this.state.openIssue && this.state.openIssue.key !== prevState.openIssue?.key) { this.loadRule().catch(() => undefined); } } componentWillUnmount() { this.detachShortcuts(); this.mounted = false; removeWhitePageClass(); removeSideBarClass(); } attachShortcuts() { document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keyup', this.handleKeyUp); } detachShortcuts() { document.removeEventListener('keydown', this.handleKeyDown); document.removeEventListener('keyup', this.handleKeyUp); } handleKeyDown = (event: KeyboardEvent) => { // Ignore if modal is open if (this.state.bulkChangeModal) { return; } if (isInput(event) || isShortcut(event)) { return; } // Ignore if date picker is open (to be removed when upgrading to React 17+) if (isDatePicker(event)) { return; } if (event.key === KeyboardKeys.Alt) { event.preventDefault(); this.setState(actions.enableLocationsNavigator); return; } switch (event.key) { case KeyboardKeys.DownArrow: { event.preventDefault(); if (event.altKey) { this.selectNextLocation(); } else { this.selectNextIssue(); } break; } case KeyboardKeys.UpArrow: { event.preventDefault(); if (event.altKey) { this.selectPreviousLocation(); } else { this.selectPreviousIssue(); } break; } case KeyboardKeys.LeftArrow: { event.preventDefault(); if (event.altKey) { this.selectPreviousFlow(); } else { this.closeIssue(); } break; } case KeyboardKeys.RightArrow: { event.preventDefault(); if (event.altKey) { this.selectNextFlow(); } else { this.openSelectedIssue(); } break; } } }; handleKeyUp = (event: KeyboardEvent) => { if (event.key === KeyboardKeys.Alt) { this.setState(actions.disableLocationsNavigator); } }; getSelectedIndex() { const { issues = [], selected } = this.state; const index = issues.findIndex((issue) => issue.key === selected); return index !== -1 ? index : undefined; } selectNextIssue = () => { const { issues } = this.state; const selectedIndex = this.getSelectedIndex(); if (selectedIndex !== undefined && selectedIndex < issues.length - 1) { if (this.state.openIssue) { this.openIssue(issues[selectedIndex + 1].key); } else { this.setState({ selected: issues[selectedIndex + 1].key, selectedFlowIndex: undefined, selectedLocationIndex: undefined, }); } } }; 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); if (this.mounted) { this.setState({ loadingRule: false, openRuleDetails }); } } selectPreviousIssue = () => { const { issues } = this.state; const selectedIndex = this.getSelectedIndex(); if (selectedIndex !== undefined && selectedIndex > 0) { if (this.state.openIssue) { this.openIssue(issues[selectedIndex - 1].key); } else { this.setState({ selected: issues[selectedIndex - 1].key, selectedFlowIndex: undefined, selectedLocationIndex: undefined, }); } } }; openIssue = (issueKey: string) => { const path = { pathname: this.props.location.pathname, query: { ...serializeQuery(this.state.query), ...getBranchLikeQuery(this.props.branchLike), id: this.props.component?.key, myIssues: this.state.myIssues ? 'true' : undefined, open: issueKey, }, }; if (this.state.openIssue) { if (path.query.open && path.query.open === this.state.openIssue.key) { this.setState({ locationsNavigator: false, selectedLocationIndex: -1, }); } else { this.props.router.replace(path); } } else { this.props.router.push(path); } }; selectIssue = (issueKey: string) => { this.setState({ selected: issueKey, selectedFlowIndex: undefined, selectedLocationIndex: undefined, }); }; closeIssue = () => { if (this.state.query) { this.props.router.push({ pathname: this.props.location.pathname, query: { ...serializeQuery(this.state.query), ...getBranchLikeQuery(this.props.branchLike), id: this.props.component?.key, myIssues: this.state.myIssues ? 'true' : undefined, open: undefined, }, }); } }; openSelectedIssue = () => { const { selected } = this.state; if (selected) { this.openIssue(selected); } }; createdAfterIncludesTime = () => Boolean(this.props.location.query.createdAfter?.includes('T')); fetchIssuesHelper = (query: RawQuery) => { if (this.props.component?.needIssueSync) { return listIssues({ ...query, }).then((response) => { const { components, issues, rules } = response; const parsedIssues = issues.map((issue) => parseIssueFromResponse(issue, components, undefined, rules) ); return { ...response, issues: parsedIssues } as FetchIssuesPromise; }); } return searchIssues({ ...query, additionalFields: '_all', timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, }).then((response) => { const parsedIssues = response.issues.map((issue) => parseIssueFromResponse(issue, response.components, response.users, response.rules) ); return { ...response, issues: parsedIssues } as FetchIssuesPromise; }); }; fetchIssues = ( additional: RawQuery, requestFacets = false, firstRequest = false ): Promise => { const { component } = this.props; const { myIssues, openFacets, query } = this.state; let facets = requestFacets ? Object.keys(openFacets) .filter((facet) => facet !== STANDARDS && openFacets[facet]) .join(',') : undefined; if (firstRequest && isProject(component?.qualifier)) { facets = facets ? `${facets},${VARIANTS_FACET}` : VARIANTS_FACET; } const parameters: Dict = component?.needIssueSync ? { ...getBranchLikeQuery(this.props.branchLike, true), project: component?.key, ...serializeQuery(query), ps: `${ISSUES_PAGE_SIZE}`, ...additional, } : { ...getBranchLikeQuery(this.props.branchLike), componentKeys: component?.key, s: 'FILE_LINE', ...serializeQuery(query), ps: `${ISSUES_PAGE_SIZE}`, facets, ...additional, }; if (query.createdAfter !== undefined && this.createdAfterIncludesTime()) { parameters.createdAfter = serializeDate(query.createdAfter); } // only sorting by CREATION_DATE is allowed, so let's sort DESC if (query.sort) { Object.assign(parameters, { asc: 'false' }); } if (myIssues) { Object.assign(parameters, { assignees: ASSIGNEE_ME }); } return this.fetchIssuesHelper(parameters); }; fetchFirstIssues(firstRequest: boolean) { const prevQuery = this.props.location.query; const openIssueKey = getOpen(this.props.location.query); let fetchPromise; this.setState({ checked: [], loading: true }); if (openIssueKey !== undefined) { fetchPromise = this.fetchIssuesUntil(1, (pageIssues: Issue[], paging: Paging) => { if ( paging.total <= paging.pageIndex * paging.pageSize || paging.pageIndex * paging.pageSize >= MAX_INITAL_FETCH ) { return true; } return pageIssues.some((issue) => issue.key === openIssueKey); }); } else { fetchPromise = this.fetchIssues({}, true, firstRequest); } return fetchPromise.then(this.parseFirstIssues(firstRequest, openIssueKey, prevQuery), () => { if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) { this.setState({ loading: false }); } return []; }); } parseFirstIssues = (firstRequest: boolean, openIssueKey: string | undefined, prevQuery: RawQuery) => ({ effortTotal, facets, issues, paging, ...other }: FetchIssuesPromise) => { if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) { const openIssue = getOpenIssue(this.props, issues); let selected: string | undefined = undefined; if (issues.length > 0) { selected = openIssue ? openIssue.key : issues[0].key; } this.setState(({ showVariantsFilter }) => ({ cannotShowOpenIssue: Boolean(openIssueKey) && !openIssue, effortTotal, facets: parseFacets(facets), issues, loading: false, locationsNavigator: true, openIssue, paging, referencedComponentsById: keyBy(other.components, 'uuid'), referencedComponentsByKey: keyBy(other.components, 'key'), referencedLanguages: keyBy(other.languages, 'key'), referencedRules: keyBy(other.rules, 'key'), referencedUsers: keyBy(other.users, 'login'), selected, selectedFlowIndex: 0, selectedLocationIndex: undefined, showVariantsFilter: firstRequest ? Boolean(facets?.find((f) => f.property === VARIANTS_FACET)?.values.length) : showVariantsFilter, })); } return issues; }; fetchIssuesPage = (p: number) => { return this.fetchIssues({ p }); }; fetchIssuesUntil = ( page: number, done: (pageIssues: Issue[], paging: Paging) => boolean ): Promise => { const recursiveFetch = (p: number, prevIssues: Issue[]): Promise => { return this.fetchIssuesPage(p).then(({ issues: pageIssues, paging, ...other }) => { const issues = [...prevIssues, ...pageIssues]; // eslint-disable-next-line promise/no-callback-in-promise return done(pageIssues, paging) ? { issues, paging, ...other } : recursiveFetch(p + 1, issues); }); }; return recursiveFetch(page, []); }; fetchMoreIssues = () => { const { paging } = this.state; if (!paging) { return Promise.reject(); } const p = paging.pageIndex + 1; this.setState({ checkAll: false, loadingMore: true }); return this.fetchIssuesPage(p).then( (response) => { if (this.mounted) { this.setState((state) => ({ issues: [...state.issues, ...response.issues], loadingMore: false, paging: response.paging, })); } }, () => { if (this.mounted) { this.setState({ loadingMore: false }); } } ); }; fetchFacet = (facet: string) => { return this.fetchIssues({ ps: 1, facets: facet }, false).then( ({ facets, ...other }) => { if (this.mounted) { this.setState((state) => ({ facets: { ...state.facets, ...parseFacets(facets) }, loadingFacets: omit(state.loadingFacets, facet), referencedComponentsById: { ...state.referencedComponentsById, ...keyBy(other.components, 'uuid'), }, referencedComponentsByKey: { ...state.referencedComponentsByKey, ...keyBy(other.components, 'key'), }, referencedLanguages: { ...state.referencedLanguages, ...keyBy(other.languages, 'key'), }, referencedRules: { ...state.referencedRules, ...keyBy(other.rules, 'key') }, referencedUsers: { ...state.referencedUsers, ...keyBy(other.users, 'login') }, })); } }, () => { /* Do nothing */ } ); }; isFiltered = () => { const serialized = serializeQuery(this.state.query); return !areQueriesEqual(serialized, DEFAULT_QUERY); }; getCheckedIssues = () => { const issues = this.state.checked .map((checked) => this.state.issues.find((issue) => issue.key === checked)) .filter((issue): issue is Issue => issue !== undefined); const paging = { pageIndex: 1, pageSize: issues.length, total: issues.length }; return Promise.resolve({ issues, paging }); }; getButtonLabel = (checked: string[], checkAll = false, paging?: Paging) => { if (checked.length === 0) { return translate('bulk_change'); } let count; if (checkAll && paging && !this.props.component?.needIssueSync) { count = paging.total > MAX_PAGE_SIZE ? MAX_PAGE_SIZE : paging.total; } else { count = Math.min(checked.length, MAX_PAGE_SIZE); } return translateWithParameters('issues.bulk_change_X_issues', count); }; handleFilterChange = (changes: Partial) => { this.props.router.push({ pathname: this.props.location.pathname, query: { ...serializeQuery({ ...this.state.query, ...changes }), ...getBranchLikeQuery(this.props.branchLike), id: this.props.component?.key, myIssues: this.state.myIssues ? 'true' : undefined, }, }); this.setState(({ openFacets }) => ({ openFacets: { ...openFacets, sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet(openFacets, changes), standards: shouldOpenStandardsFacet(openFacets, changes), }, })); }; handleMyIssuesChange = (myIssues: boolean) => { this.closeFacet('assignees'); if (!this.props.component) { saveMyIssues(myIssues); } this.props.router.push({ pathname: this.props.location.pathname, query: { ...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }), ...getBranchLikeQuery(this.props.branchLike), id: this.props.component?.key, myIssues: myIssues ? 'true' : undefined, }, }); }; loadSearchResultCount = (property: string, changes: Partial) => { const { component } = this.props; const { myIssues, query } = this.state; const parameters = { ...getBranchLikeQuery(this.props.branchLike), componentKeys: component?.key, facets: property, s: 'FILE_LINE', ...serializeQuery({ ...query, ...changes }), ps: 1, }; if (myIssues) { Object.assign(parameters, { assignees: ASSIGNEE_ME }); } return this.fetchIssuesHelper(parameters).then(({ facets }) => parseFacets(facets)[property]); }; closeFacet = (property: string) => { this.setState((state) => ({ openFacets: { ...state.openFacets, [property]: false }, })); }; handleFacetToggle = (property: string) => { this.setState((state) => { const willOpenProperty = !state.openFacets[property]; const newState = { loadingFacets: state.loadingFacets, openFacets: { ...state.openFacets, [property]: willOpenProperty }, }; // Try to open sonarsource security "subfacet" by default if the standard facet is open if (willOpenProperty && property === STANDARDS) { newState.openFacets.sonarsourceSecurity = shouldOpenSonarSourceSecurityFacet( newState.openFacets, state.query ); // Force loading of sonarsource security facet data property = newState.openFacets.sonarsourceSecurity ? 'sonarsourceSecurity' : property; } // No need to load facets data for standard facet if (property !== STANDARDS && !state.facets[property]) { newState.loadingFacets[property] = true; this.fetchFacet(property).catch(() => undefined); } return newState; }); }; handleReset = () => { this.props.router.push({ pathname: this.props.location.pathname, query: { ...DEFAULT_QUERY, ...getBranchLikeQuery(this.props.branchLike), id: this.props.component?.key, myIssues: this.state.myIssues ? 'true' : undefined, }, }); }; handlePopupToggle = (issue: string, popupName: string, open: boolean | undefined = undefined) => { this.setState((state: State) => { const { openPopup } = state; const samePopup = openPopup && openPopup.name === popupName && openPopup.issue === issue; if (open !== false && !samePopup) { return { ...state, openPopup: { issue, name: popupName } }; } else if (open !== true && samePopup) { return { ...state, openPopup: undefined }; } return state; }); }; handleIssueCheck = (issue: string) => { this.setState((state) => ({ checkAll: false, checked: state.checked.includes(issue) ? without(state.checked, issue) : [...state.checked, issue], })); }; handleIssueChange = (issue: Issue) => { this.setState((state) => ({ issues: state.issues.map((candidate) => (candidate.key === issue.key ? issue : candidate)), })); }; handleOpenBulkChange = () => { this.setState({ bulkChangeModal: true }); }; handleCloseBulkChange = () => { this.setState({ bulkChangeModal: false }, () => { if (this.bulkButtonRef.current) { this.bulkButtonRef.current.focus(); } }); }; handleBulkChangeDone = () => { this.setState({ checkAll: false }); this.fetchFirstIssues(false).catch(() => undefined); this.handleCloseBulkChange(); }; selectLocation = (index: number) => { const { selectedLocationIndex } = this.state; if (index === selectedLocationIndex) { this.setState({ selectedLocationIndex: undefined }, () => { this.setState({ selectedLocationIndex: index }); }); } else { this.setState(({ openIssue }) => { if (openIssue) { return { locationsNavigator: true, selectedLocationIndex: index }; } return null; }); } }; selectNextLocation = () => { this.setState(actions.selectNextLocation); }; selectPreviousLocation = () => { this.setState(actions.selectPreviousLocation); }; handleCheckAll = (checked: boolean) => { if (checked) { this.setState((state) => ({ checkAll: true, checked: state.issues.map((issue) => issue.key), })); } else { this.setState({ checkAll: false, checked: [] }); } }; selectFlow = (index?: number) => { this.setState(actions.selectFlow(index)); }; selectNextFlow = () => { this.setState(actions.selectNextFlow); }; selectPreviousFlow = () => { this.setState(actions.selectPreviousFlow); }; renderBulkChange() { const { component, currentUser } = this.props; const { checkAll, bulkChangeModal, checked, issues, paging } = this.state; const isAllChecked = checked.length > 0 && issues.length === checked.length; const thirdState = checked.length > 0 && !isAllChecked; const isChecked = isAllChecked || thirdState; if (!currentUser.isLoggedIn) { return null; } return (
{this.getButtonLabel(checked, checkAll, paging)} {bulkChangeModal && ( )}
); } renderFacets(warning?: React.ReactNode) { const { component, currentUser, branchLike } = this.props; const { facets, loadingFacets, myIssues, openFacets, query, referencedComponentsById, referencedComponentsByKey, referencedLanguages, referencedRules, referencedUsers, showVariantsFilter, } = this.state; return (
{warning &&
{warning}
} {currentUser.isLoggedIn && !component?.needIssueSync && (
)}
); } 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 }) => ( )} ); } renderList() { const { branchLike, component, currentUser } = this.props; const { issues, loading, loadingMore, openIssue, paging } = this.state; const selectedIndex = this.getSelectedIndex(); const selectedIssue = selectedIndex !== undefined ? issues[selectedIndex] : undefined; if (!paging || openIssue) { return null; } let noIssuesMessage = null; if (issues.length === 0 && !loading) { if (this.isFiltered()) { noIssuesMessage = ; } else if (this.state.myIssues) { noIssuesMessage = ; } else { noIssuesMessage = ; } } return (

{translate('list_of_issues')}

{issues.length > 0 && ( )} {issues.length > 0 && ( { this.fetchMoreIssues().catch(() => undefined); }} loading={loadingMore} pageSize={ISSUES_PAGE_SIZE} total={paging.total} useMIUIButtons /> )} {noIssuesMessage}
); } renderHeader({ openIssue, paging, selectedIndex, }: { openIssue: Issue | undefined; paging: Paging | undefined; selectedIndex: number | undefined; }) { return openIssue ? ( ) : ( <>
{this.renderBulkChange()}
); } renderPage() { const { cannotShowOpenIssue, openRuleDetails, checkAll, issues, loading, openIssue, paging, loadingRule, } = this.state; const selectedIndex = this.getSelectedIndex(); return ( {({ top }) => (
{this.renderHeader({ openIssue, paging, selectedIndex })} {/* eslint-disable-next-line local-rules/no-conditional-rendering-of-deferredspinner */} {openIssue && openRuleDetails ? ( } codeTabContent={ } extendedDescription={openRuleDetails.htmlNote} issue={openIssue} onIssueChange={this.handleIssueChange} ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey} ruleDetails={openRuleDetails} selectedFlowIndex={this.state.selectedFlowIndex} selectedLocationIndex={this.state.selectedLocationIndex} /> ) : (
{checkAll && paging && paging.total > MAX_PAGE_SIZE && (
{MAX_PAGE_SIZE} }} />
)} {cannotShowOpenIssue && (!paging || paging.total > 0) && ( {translateWithParameters( 'issues.cannot_open_issue_max_initial_X_fetched', MAX_INITAL_FETCH )} )} {this.renderList()}
)}
)}
); } render() { const { openIssue } = this.state; return (
{openIssue ? ( ) : ( )}

{translate('issues.page')}

{this.renderSide(openIssue)} {this.renderPage()}
); } } export default withRouter( withComponentContext( withCurrentUserContext( withBranchLikes( withIndexationContext( withIndexationGuard({ Component: App, showIndexationMessage: ({ component, indexationContext: { status: { completedCount, hasFailures, isCompleted, total }, }, }) => (!component && (isCompleted === false || hasFailures === true || completedCount !== total)) || (component?.qualifier !== ComponentQualifier.Project && component?.needIssueSync === true), }) ) ) ) ) ); const PageWrapperStyle = styled.div` background-color: ${themeColor('backgroundPrimary')}; `; const MainContentStyle = styled.main` background-color: ${themeColor('subnavigation')}; border-left: ${themeBorder('default', 'filterbarBorder')}; border-right: ${themeBorder('default', 'filterbarBorder')}; `; const SideBarStyle = styled.div` border-left: ${themeBorder('default', 'filterbarBorder')}; border-right: ${themeBorder('default', 'filterbarBorder')}; background-color: ${themeColor('backgroundSecondary')}; `;