From 31c52cad6e60801c48999cc204c71d123d22c064 Mon Sep 17 00:00:00 2001 From: 7PH Date: Wed, 17 May 2023 11:30:23 +0200 Subject: SONAR-19236 Implement correct layout for hotspot page sidebar --- .../SecurityHotspotsAppRenderer.tsx | 223 ++++++++++++--------- .../security-hotspots/components/FilterBar.tsx | 216 -------------------- .../components/HotspotSidebarHeader.tsx | 159 +++++++++++++++ .../components/HotspotStatusFilter.tsx | 90 +++++++++ .../components/StatusUpdateSuccessModal.tsx | 2 +- .../sonar-web/src/main/js/hooks/useFollowScroll.ts | 42 ++++ 6 files changed, 421 insertions(+), 311 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotStatusFilter.tsx create mode 100644 server/sonar-web/src/main/js/hooks/useFollowScroll.ts (limited to 'server/sonar-web/src/main/js') diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx index 7498eb4c696..8af9b7bccfd 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx @@ -17,9 +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 { useTheme, withTheme } from '@emotion/react'; +import { withTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { + DeferredSpinner, + LAYOUT_FOOTER_HEIGHT, + LAYOUT_GLOBAL_NAV_HEIGHT, + LAYOUT_PROJECT_NAV_HEIGHT, LargeCenteredLayout, PageContentFontWrapper, themeBorder, @@ -29,18 +33,20 @@ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import A11ySkipTarget from '../../components/a11y/A11ySkipTarget'; import Suggestions from '../../components/embed-docs-modal/Suggestions'; -import DeferredSpinner from '../../components/ui/DeferredSpinner'; import { isBranch } from '../../helpers/branch-like'; import { translate } from '../../helpers/l10n'; +import useFollowScroll from '../../hooks/useFollowScroll'; import { BranchLike } from '../../types/branch-like'; +import { ComponentQualifier } from '../../types/component'; import { MetricKey } from '../../types/metrics'; import { SecurityStandard, Standards } from '../../types/security'; import { HotspotFilters, HotspotStatusFilter, RawHotspot } from '../../types/security-hotspots'; import { Component, StandardSecurityCategories } from '../../types/types'; import EmptyHotspotsPage from './components/EmptyHotspotsPage'; -import FilterBar from './components/FilterBar'; import HotspotList from './components/HotspotList'; +import HotspotSidebarHeader from './components/HotspotSidebarHeader'; import HotspotSimpleList from './components/HotspotSimpleList'; +import HotspotFilterByStatus from './components/HotspotStatusFilter'; import HotspotViewer from './components/HotspotViewer'; import './styles.css'; @@ -97,7 +103,12 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe onShowAllHotspots, } = props; - const theme = useTheme(); + const isProject = component.qualifier === ComponentQualifier.Project; + + const { top: topScroll } = useFollowScroll(); + const distanceFromBottom = topScroll + window.innerHeight - document.body.clientHeight; + const footerVisibleHeight = + distanceFromBottom > -LAYOUT_FOOTER_HEIGHT ? LAYOUT_FOOTER_HEIGHT + distanceFromBottom : 0; return ( <> @@ -107,81 +118,97 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe -
- - - - - {hotspots.length > 0 && selectedHotspot && ( - <> - {filterByCategory || filterByCWE || filterByFile ? ( - - ) : ( - - )} - - )} - - -
- - {hotspots.length === 0 || !selectedHotspot ? ( - + + {isProject && ( + + - ) : ( - + )} + + + - )} - -
+ {hotspots.length > 0 && selectedHotspot && ( + <> + {filterByCategory || filterByCWE || filterByFile ? ( + + ) : ( + + )} + + )} +
+ + + + {hotspots.length === 0 || !selectedHotspot ? ( + + ) : ( + + )} +
@@ -189,22 +216,30 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe ); } -const StyledFilterbar = withTheme( - styled.div` - box-sizing: border-box; - overflow-x: hidden; - overflow-y: auto; - background-color: ${themeColor('filterbar')}; - border-right: ${themeBorder('default', 'filterbarBorder')}; - // ToDo set proper height - height: calc(100vh - ${'100px'}); - ` -); +const StyledSidebar = withTheme(styled.section` + position: sticky; + box-sizing: border-box; + overflow-x: hidden; + overflow-y: scroll; + background-color: ${themeColor('filterbar')}; + border-right: ${themeBorder('default', 'filterbarBorder')}; +`); + +const StyledSidebarContent = styled.div` + position: relative; + box-sizing: border-box; + width: 100%; +`; + +const StyledSidebarHeader = withTheme(styled.div` + position: sticky; + box-sizing: border-box; + background-color: inherit; + border-bottom: ${themeBorder('default')}; + z-index: 1; +`); -const StyledContentWrapper = withTheme( - styled.div` - background-color: ${themeColor('backgroundSecondary')}; - border-right: ${themeBorder('default', 'pageBlockBorder')}; - border-left: ${themeBorder('default', 'pageBlockBorder')}; - ` -); +const StyledMain = styled.main` + flex-grow: 1; + background-color: ${themeColor('backgroundSecondary')}; +`; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx deleted file mode 100644 index 869e87986c6..00000000000 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx +++ /dev/null @@ -1,216 +0,0 @@ -/* - * 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 { withTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { - CoverageIndicator, - DiscreetInteractiveIcon, - DiscreetLink, - Dropdown, - FilterIcon, - HelperHintIcon, - ItemCheckbox, - ItemDangerButton, - ItemDivider, - ItemHeader, - PopupPlacement, - ToggleButton, - themeBorder, -} from 'design-system'; -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; -import HelpTooltip from '../../../components/controls/HelpTooltip'; -import Measure from '../../../components/measure/Measure'; -import DeferredSpinner from '../../../components/ui/DeferredSpinner'; -import { translate } from '../../../helpers/l10n'; -import { ComponentQualifier } from '../../../types/component'; -import { MetricType } from '../../../types/metrics'; -import { HotspotFilters, HotspotStatusFilter } from '../../../types/security-hotspots'; -import { Component } from '../../../types/types'; -import { CurrentUser, isLoggedIn } from '../../../types/users'; - -export interface FilterBarProps { - currentUser: CurrentUser; - component: Component; - filters: HotspotFilters; - hotspotsReviewedMeasure?: string; - isStaticListOfHotspots: boolean; - loadingMeasure: boolean; - onBranch: boolean; - onChangeFilters: (filters: Partial) => void; - onShowAllHotspots: VoidFunction; -} - -const statusOptions: Array<{ label: string; value: HotspotStatusFilter }> = [ - { value: HotspotStatusFilter.TO_REVIEW, label: translate('hotspot.filters.status.to_review') }, - { - value: HotspotStatusFilter.ACKNOWLEDGED, - label: translate('hotspot.filters.status.acknowledged'), - }, - { value: HotspotStatusFilter.FIXED, label: translate('hotspot.filters.status.fixed') }, - { value: HotspotStatusFilter.SAFE, label: translate('hotspot.filters.status.safe') }, -]; - -export enum AssigneeFilterOption { - ALL = 'all', - ME = 'me', -} - -export function FilterBar(props: FilterBarProps) { - const { - currentUser, - component, - filters, - hotspotsReviewedMeasure, - loadingMeasure, - onBranch, - isStaticListOfHotspots, - } = props; - const isProject = component.qualifier === ComponentQualifier.Project; - const userLoggedIn = isLoggedIn(currentUser); - const filtersCount = Number(filters.assignedToMe) + Number(filters.inNewCodePeriod); - const isFiltered = Boolean(filtersCount); - - return ( -
- {isStaticListOfHotspots ? ( - - - {translate('hotspot.filters.show_all')} - - ), - }} - defaultMessage={translate('hotspot.filters.by_file_or_list_x')} - /> - - ) : ( - <> - {isProject && ( - - - {hotspotsReviewedMeasure !== undefined && ( - - )} - - - {translate('metric.security_hotspots_reviewed.name')} - - - - - - - )} - - - props.onChangeFilters({ status })} - options={statusOptions} - value={statusOptions.find((status) => status.value === filters.status)?.value} - /> - {(onBranch || userLoggedIn || isFiltered) && ( - - {translate('hotspot.filters.title')} - - {onBranch && ( - props.onChangeFilters({ inNewCodePeriod })} - > - - {translate('hotspot.filters.period.since_leak_period')} - - - )} - - {userLoggedIn && ( - props.onChangeFilters({ assignedToMe })} - > - - {translate('hotspot.filters.assignee.assigned_to_me')} - - - )} - - {isFiltered && } - - {isFiltered && ( - - props.onChangeFilters({ - assignedToMe: false, - inNewCodePeriod: false, - }) - } - > - {translate('hotspot.filters.clear')} - - )} - - } - placement={PopupPlacement.BottomRight} - > - - {isFiltered ? filtersCount : null} - - - )} - - - )} -
- ); -} - -const StyledFilterWrapper = withTheme(styled.div` - border-bottom: ${themeBorder('default')}; -`); - -export default withCurrentUserContext(FilterBar); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx new file mode 100644 index 00000000000..254cb0d1d07 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx @@ -0,0 +1,159 @@ +/* + * 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 { + CoverageIndicator, + DiscreetInteractiveIcon, + Dropdown, + FilterIcon, + HelperHintIcon, + ItemCheckbox, + ItemDangerButton, + ItemDivider, + ItemHeader, +} from 'design-system'; +import * as React from 'react'; +import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; +import HelpTooltip from '../../../components/controls/HelpTooltip'; +import Measure from '../../../components/measure/Measure'; +import DeferredSpinner from '../../../components/ui/DeferredSpinner'; +import { PopupPlacement } from '../../../components/ui/popups'; +import { isBranch } from '../../../helpers/branch-like'; +import { translate } from '../../../helpers/l10n'; +import { BranchLike } from '../../../types/branch-like'; +import { MetricKey, MetricType } from '../../../types/metrics'; +import { HotspotFilters } from '../../../types/security-hotspots'; +import { CurrentUser, isLoggedIn } from '../../../types/users'; + +export interface SecurityHotspotsAppRendererProps { + branchLike?: BranchLike; + filters: HotspotFilters; + hotspotsReviewedMeasure?: string; + loadingMeasure: boolean; + onChangeFilters: (filters: Partial) => void; + currentUser: CurrentUser; + isStaticListOfHotspots: boolean; +} + +function HotspotSidebarHeader(props: SecurityHotspotsAppRendererProps) { + const { + branchLike, + filters, + hotspotsReviewedMeasure, + loadingMeasure, + currentUser, + isStaticListOfHotspots, + } = props; + + const userLoggedIn = isLoggedIn(currentUser); + const filtersCount = + Number(filters.assignedToMe) + Number(isBranch(branchLike) && filters.inNewCodePeriod); + const isFiltered = Boolean(filtersCount); + + return ( +
+ + {hotspotsReviewedMeasure !== undefined && ( + + )} + + + {translate('metric.security_hotspots_reviewed.name')} + + + + + + {!isStaticListOfHotspots && (isBranch(branchLike) || userLoggedIn || isFiltered) && ( +
+ + {translate('hotspot.filters.title')} + + {isBranch(branchLike) && ( + + props.onChangeFilters({ inNewCodePeriod }) + } + > + + {translate('hotspot.filters.period.since_leak_period')} + + + )} + + {userLoggedIn && ( + props.onChangeFilters({ assignedToMe })} + > + + {translate('hotspot.filters.assignee.assigned_to_me')} + + + )} + + {isFiltered && } + + {isFiltered && ( + + props.onChangeFilters({ + assignedToMe: false, + inNewCodePeriod: false, + }) + } + > + {translate('hotspot.filters.clear')} + + )} + + } + placement={PopupPlacement.BottomRight} + isPortal={true} + > + + {isFiltered ? filtersCount : null} + + +
+ )} +
+
+ ); +} + +export default withCurrentUserContext(HotspotSidebarHeader); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotStatusFilter.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotStatusFilter.tsx new file mode 100644 index 00000000000..3f19a1dac5e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotStatusFilter.tsx @@ -0,0 +1,90 @@ +/* + * 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 { withTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { DiscreetLink, ToggleButton, themeBorder } from 'design-system'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { translate } from '../../../helpers/l10n'; +import { HotspotFilters, HotspotStatusFilter } from '../../../types/security-hotspots'; + +export interface FilterBarProps { + filters: HotspotFilters; + isStaticListOfHotspots: boolean; + onChangeFilters: (filters: Partial) => void; + onShowAllHotspots: VoidFunction; +} + +const statusOptions: Array<{ label: string; value: HotspotStatusFilter }> = [ + { value: HotspotStatusFilter.TO_REVIEW, label: translate('hotspot.filters.status.to_review') }, + { + value: HotspotStatusFilter.ACKNOWLEDGED, + label: translate('hotspot.filters.status.acknowledged'), + }, + { value: HotspotStatusFilter.FIXED, label: translate('hotspot.filters.status.fixed') }, + { value: HotspotStatusFilter.SAFE, label: translate('hotspot.filters.status.safe') }, +]; + +export enum AssigneeFilterOption { + ALL = 'all', + ME = 'me', +} + +export default function HotspotFilterByStatus(props: FilterBarProps) { + const { filters, isStaticListOfHotspots } = props; + + return ( +
+ {isStaticListOfHotspots ? ( + + + {translate('hotspot.filters.show_all')} + + ), + }} + defaultMessage={translate('hotspot.filters.by_file_or_list_x')} + /> + + ) : ( + + props.onChangeFilters({ status })} + options={statusOptions} + value={statusOptions.find((status) => status.value === filters.status)?.value} + /> + + )} +
+ ); +} + +const StyledFilterWrapper = withTheme(styled.div` + border-bottom: ${themeBorder('default')}; +`); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx index 2a2ec0e2dec..9259575884d 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx @@ -19,8 +19,8 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Button, ButtonLink } from '../../../components/controls/buttons'; import Modal from '../../../components/controls/Modal'; +import { Button, ButtonLink } from '../../../components/controls/buttons'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; import { HotspotStatusOption } from '../../../types/security-hotspots'; diff --git a/server/sonar-web/src/main/js/hooks/useFollowScroll.ts b/server/sonar-web/src/main/js/hooks/useFollowScroll.ts new file mode 100644 index 00000000000..0692d5e2ab7 --- /dev/null +++ b/server/sonar-web/src/main/js/hooks/useFollowScroll.ts @@ -0,0 +1,42 @@ +/* + * 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 { throttle } from 'lodash'; +import { useEffect, useState } from 'react'; + +const THROTTLE_DELAY = 10; + +export default function useFollowScroll() { + const [left, setLeft] = useState(0); + const [top, setTop] = useState(0); + + useEffect(() => { + const followScroll = throttle(() => { + if (document.documentElement) { + setLeft(document.documentElement.scrollLeft); + setTop(document.documentElement.scrollTop); + } + }, THROTTLE_DELAY); + + document.addEventListener('scroll', followScroll); + return () => document.removeEventListener('scroll', followScroll); + }, []); + + return { left, top }; +} -- cgit v1.2.3