diff options
author | 7PH <benjamin.raymond@sonarsource.com> | 2023-05-17 11:30:23 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-05-24 20:03:14 +0000 |
commit | 31c52cad6e60801c48999cc204c71d123d22c064 (patch) | |
tree | acaba58837dfaca9751785cf77749aff866f79fa /server/sonar-web/src/main/js | |
parent | 56e625210bff28970ce12cf8d7807ebda6c147e1 (diff) | |
download | sonarqube-31c52cad6e60801c48999cc204c71d123d22c064.tar.gz sonarqube-31c52cad6e60801c48999cc204c71d123d22c064.zip |
SONAR-19236 Implement correct layout for hotspot page sidebar
Diffstat (limited to 'server/sonar-web/src/main/js')
6 files changed, 421 insertions, 311 deletions
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 <LargeCenteredLayout id={MetricKey.security_hotspots}> <PageContentFontWrapper> - <div className="sw-grid sw-grid-cols-12 sw-w-full sw-body-sm"> - <DeferredSpinner className="sw-mt-3" loading={loading} /> - - <StyledFilterbar className="sw-col-span-4 sw-rounded-t-1 sw-mt-0 sw-z-filterbar sw-p-4 it__hotspot-list"> - <FilterBar - component={component} - filters={filters} - hotspotsReviewedMeasure={hotspotsReviewedMeasure} - isStaticListOfHotspots={isStaticListOfHotspots} - loadingMeasure={loadingMeasure} - onBranch={isBranch(branchLike)} - onChangeFilters={onChangeFilters} - onShowAllHotspots={onShowAllHotspots} - /> - {hotspots.length > 0 && selectedHotspot && ( - <> - {filterByCategory || filterByCWE || filterByFile ? ( - <HotspotSimpleList - filterByCategory={filterByCategory} - filterByCWE={filterByCWE} - filterByFile={filterByFile} - hotspots={hotspots} - hotspotsTotal={hotspotsTotal} - loadingMore={loadingMore} - onHotspotClick={props.onHotspotClick} - onLoadMore={props.onLoadMore} - onLocationClick={props.onLocationClick} - selectedHotspotLocation={selectedHotspotLocation} - selectedHotspot={selectedHotspot} - standards={standards} - /> - ) : ( - <HotspotList - hotspots={hotspots} - hotspotsTotal={hotspotsTotal} - isStaticListOfHotspots={isStaticListOfHotspots} - loadingMore={loadingMore} - onHotspotClick={props.onHotspotClick} - onLoadMore={props.onLoadMore} - onLocationClick={props.onLocationClick} - securityCategories={securityCategories} - selectedHotspot={selectedHotspot} - selectedHotspotLocation={selectedHotspotLocation} - statusFilter={filters.status} - /> - )} - </> - )} - </StyledFilterbar> - - <main className="sw-col-span-8 sw-pl-12"> - <StyledContentWrapper theme={theme} className="sw-h-full"> - {hotspots.length === 0 || !selectedHotspot ? ( - <EmptyHotspotsPage - filtered={ - filters.assignedToMe || - (isBranch(branchLike) && filters.inNewCodePeriod) || - filters.status !== HotspotStatusFilter.TO_REVIEW - } - filterByFile={Boolean(filterByFile)} + <div className="sw-grid sw-grid-cols-12 sw-w-full sw-min-h-[100vh]"> + <StyledSidebar + aria-label={translate('hotspots.list')} + className="sw--mt-8 sw-z-filterbar sw-col-span-4" + style={{ + height: `calc(100vh - ${ + LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_PROJECT_NAV_HEIGHT + }px - ${footerVisibleHeight}px)`, + top: LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_PROJECT_NAV_HEIGHT, + }} + > + {isProject && ( + <StyledSidebarHeader className="sw-w-full sw-top-0 sw-px-4 sw-py-2"> + <HotspotSidebarHeader + branchLike={branchLike} + filters={filters} isStaticListOfHotspots={isStaticListOfHotspots} + hotspotsReviewedMeasure={hotspotsReviewedMeasure} + loadingMeasure={loadingMeasure} + onChangeFilters={onChangeFilters} /> - ) : ( - <HotspotViewer - component={component} - hotspotKey={selectedHotspot.key} - onSwitchStatusFilter={props.onSwitchStatusFilter} - onUpdateHotspot={props.onUpdateHotspot} - onLocationClick={props.onLocationClick} - selectedHotspotLocation={selectedHotspotLocation} - standards={standards} + </StyledSidebarHeader> + )} + <StyledSidebarContent className="sw-p-4 it__hotspot-list"> + <DeferredSpinner className="sw-mt-3" loading={loading}> + <HotspotFilterByStatus + filters={filters} + isStaticListOfHotspots={isStaticListOfHotspots} + onChangeFilters={onChangeFilters} + onShowAllHotspots={onShowAllHotspots} /> - )} - </StyledContentWrapper> - </main> + {hotspots.length > 0 && selectedHotspot && ( + <> + {filterByCategory || filterByCWE || filterByFile ? ( + <HotspotSimpleList + filterByCategory={filterByCategory} + filterByCWE={filterByCWE} + filterByFile={filterByFile} + hotspots={hotspots} + hotspotsTotal={hotspotsTotal} + loadingMore={loadingMore} + onHotspotClick={props.onHotspotClick} + onLoadMore={props.onLoadMore} + onLocationClick={props.onLocationClick} + selectedHotspotLocation={selectedHotspotLocation} + selectedHotspot={selectedHotspot} + standards={standards} + /> + ) : ( + <HotspotList + hotspots={hotspots} + hotspotsTotal={hotspotsTotal} + isStaticListOfHotspots={isStaticListOfHotspots} + loadingMore={loadingMore} + onHotspotClick={props.onHotspotClick} + onLoadMore={props.onLoadMore} + onLocationClick={props.onLocationClick} + securityCategories={securityCategories} + selectedHotspot={selectedHotspot} + selectedHotspotLocation={selectedHotspotLocation} + statusFilter={filters.status} + /> + )} + </> + )} + </DeferredSpinner> + </StyledSidebarContent> + </StyledSidebar> + <StyledMain className="sw-col-span-8 sw-relative sw-pl-12"> + {hotspots.length === 0 || !selectedHotspot ? ( + <EmptyHotspotsPage + filtered={ + filters.assignedToMe || + (isBranch(branchLike) && filters.inNewCodePeriod) || + filters.status !== HotspotStatusFilter.TO_REVIEW + } + filterByFile={Boolean(filterByFile)} + isStaticListOfHotspots={isStaticListOfHotspots} + /> + ) : ( + <HotspotViewer + component={component} + hotspotKey={selectedHotspot.key} + onSwitchStatusFilter={props.onSwitchStatusFilter} + onUpdateHotspot={props.onUpdateHotspot} + onLocationClick={props.onLocationClick} + selectedHotspotLocation={selectedHotspotLocation} + standards={standards} + /> + )} + </StyledMain> </div> </PageContentFontWrapper> </LargeCenteredLayout> @@ -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<HotspotFilters>) => 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 ( - <div className="sw-flex sw-flex-col sw-justify-between sw-pb-4 sw-mb-3"> - {isStaticListOfHotspots ? ( - <StyledFilterWrapper className="sw-flex sw-px-2 sw-py-4"> - <FormattedMessage - id="hotspot.filters.by_file_or_list_x" - values={{ - show_all_link: ( - <DiscreetLink - className="sw-ml-1" - onClick={props.onShowAllHotspots} - preventDefault={true} - to={{}} - > - {translate('hotspot.filters.show_all')} - </DiscreetLink> - ), - }} - defaultMessage={translate('hotspot.filters.by_file_or_list_x')} - /> - </StyledFilterWrapper> - ) : ( - <> - {isProject && ( - <StyledFilterWrapper className="sw-flex sw-px-2 sw-py-4 sw-items-center sw-h-6"> - <DeferredSpinner loading={loadingMeasure}> - {hotspotsReviewedMeasure !== undefined && ( - <CoverageIndicator value={hotspotsReviewedMeasure} /> - )} - <Measure - className="sw-ml-2 sw-body-sm-highlight" - metricKey={ - onBranch && !filters.inNewCodePeriod - ? 'security_hotspots_reviewed' - : 'new_security_hotspots_reviewed' - } - metricType={MetricType.Percent} - value={hotspotsReviewedMeasure} - /> - <span className="sw-ml-1 sw-body-sm"> - {translate('metric.security_hotspots_reviewed.name')} - </span> - <HelpTooltip className="sw-ml-1" overlay={translate('hotspots.reviewed.tooltip')}> - <HelperHintIcon aria-label="help-tooltip" /> - </HelpTooltip> - </DeferredSpinner> - </StyledFilterWrapper> - )} - - <StyledFilterWrapper className="sw-flex sw-px-2 sw-py-4 sw-gap-2 sw-justify-between"> - <ToggleButton - aria-label={translate('hotspot.filters.status')} - onChange={(status: HotspotStatusFilter) => props.onChangeFilters({ status })} - options={statusOptions} - value={statusOptions.find((status) => status.value === filters.status)?.value} - /> - {(onBranch || userLoggedIn || isFiltered) && ( - <Dropdown - allowResizing={true} - closeOnClick={false} - id="filter-hotspots-menu" - overlay={ - <> - <ItemHeader>{translate('hotspot.filters.title')}</ItemHeader> - - {onBranch && ( - <ItemCheckbox - checked={Boolean(filters.inNewCodePeriod)} - onCheck={(inNewCodePeriod) => props.onChangeFilters({ inNewCodePeriod })} - > - <span className="sw-mx-2"> - {translate('hotspot.filters.period.since_leak_period')} - </span> - </ItemCheckbox> - )} - - {userLoggedIn && ( - <ItemCheckbox - checked={Boolean(filters.assignedToMe)} - onCheck={(assignedToMe) => props.onChangeFilters({ assignedToMe })} - > - <span className="sw-mx-2"> - {translate('hotspot.filters.assignee.assigned_to_me')} - </span> - </ItemCheckbox> - )} - - {isFiltered && <ItemDivider />} - - {isFiltered && ( - <ItemDangerButton - onClick={() => - props.onChangeFilters({ - assignedToMe: false, - inNewCodePeriod: false, - }) - } - > - {translate('hotspot.filters.clear')} - </ItemDangerButton> - )} - </> - } - placement={PopupPlacement.BottomRight} - > - <DiscreetInteractiveIcon - Icon={FilterIcon} - aria-label={translate('hotspot.filters.title')} - > - {isFiltered ? filtersCount : null} - </DiscreetInteractiveIcon> - </Dropdown> - )} - </StyledFilterWrapper> - </> - )} - </div> - ); -} - -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<HotspotFilters>) => 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 ( + <div className="sw-flex sw-py-4 sw-items-center sw-h-6 sw-px-4"> + <DeferredSpinner loading={loadingMeasure}> + {hotspotsReviewedMeasure !== undefined && ( + <CoverageIndicator value={hotspotsReviewedMeasure} /> + )} + <Measure + className="sw-ml-2 sw-body-sm-highlight" + metricKey={ + isBranch(branchLike) && !filters.inNewCodePeriod + ? MetricKey.security_hotspots_reviewed + : MetricKey.new_security_hotspots_reviewed + } + metricType={MetricType.Percent} + value={hotspotsReviewedMeasure} + /> + <span className="sw-ml-1 sw-body-sm"> + {translate('metric.security_hotspots_reviewed.name')} + </span> + <HelpTooltip className="sw-ml-1" overlay={translate('hotspots.reviewed.tooltip')}> + <HelperHintIcon aria-label="help-tooltip" /> + </HelpTooltip> + + {!isStaticListOfHotspots && (isBranch(branchLike) || userLoggedIn || isFiltered) && ( + <div className="sw-flex-grow sw-flex sw-justify-end"> + <Dropdown + allowResizing={true} + closeOnClick={false} + id="filter-hotspots-menu" + overlay={ + <> + <ItemHeader>{translate('hotspot.filters.title')}</ItemHeader> + + {isBranch(branchLike) && ( + <ItemCheckbox + checked={Boolean(filters.inNewCodePeriod)} + onCheck={(inNewCodePeriod: boolean) => + props.onChangeFilters({ inNewCodePeriod }) + } + > + <span className="sw-mx-2"> + {translate('hotspot.filters.period.since_leak_period')} + </span> + </ItemCheckbox> + )} + + {userLoggedIn && ( + <ItemCheckbox + checked={Boolean(filters.assignedToMe)} + onCheck={(assignedToMe: boolean) => props.onChangeFilters({ assignedToMe })} + > + <span className="sw-mx-2"> + {translate('hotspot.filters.assignee.assigned_to_me')} + </span> + </ItemCheckbox> + )} + + {isFiltered && <ItemDivider />} + + {isFiltered && ( + <ItemDangerButton + onClick={() => + props.onChangeFilters({ + assignedToMe: false, + inNewCodePeriod: false, + }) + } + > + {translate('hotspot.filters.clear')} + </ItemDangerButton> + )} + </> + } + placement={PopupPlacement.BottomRight} + isPortal={true} + > + <DiscreetInteractiveIcon + Icon={FilterIcon} + aria-label={translate('hotspot.filters.title')} + > + {isFiltered ? filtersCount : null} + </DiscreetInteractiveIcon> + </Dropdown> + </div> + )} + </DeferredSpinner> + </div> + ); +} + +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<HotspotFilters>) => 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 ( + <div className="sw-flex sw-flex-col sw-justify-between sw-pb-4 sw-mb-3"> + {isStaticListOfHotspots ? ( + <StyledFilterWrapper className="sw-flex sw-px-2 sw-py-4"> + <FormattedMessage + id="hotspot.filters.by_file_or_list_x" + values={{ + show_all_link: ( + <DiscreetLink + className="sw-ml-1" + onClick={props.onShowAllHotspots} + preventDefault={true} + to={{}} + > + {translate('hotspot.filters.show_all')} + </DiscreetLink> + ), + }} + defaultMessage={translate('hotspot.filters.by_file_or_list_x')} + /> + </StyledFilterWrapper> + ) : ( + <StyledFilterWrapper className="sw-flex sw-px-2 sw-py-4 sw-gap-2 sw-justify-between"> + <ToggleButton + aria-label={translate('hotspot.filters.status')} + onChange={(status: HotspotStatusFilter) => props.onChangeFilters({ status })} + options={statusOptions} + value={statusOptions.find((status) => status.value === filters.status)?.value} + /> + </StyledFilterWrapper> + )} + </div> + ); +} + +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 }; +} |