diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2023-05-03 15:13:21 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-06-01 20:02:58 +0000 |
commit | cf7651008dc6326cd43624d92c45ed9c9b559283 (patch) | |
tree | 9a2fe3bbd3f6e90d0ac830e590b217e351a269d8 /server | |
parent | 3bcee0f3a74e7a5b144ed438f50b4239ee7339f7 (diff) | |
download | sonarqube-cf7651008dc6326cd43624d92c45ed9c9b559283.tar.gz sonarqube-cf7651008dc6326cd43624d92c45ed9c9b559283.zip |
SONAR-19174 Migrating issue code viewer header and code expander section to MIUI
Diffstat (limited to 'server')
9 files changed, 245 insertions, 98 deletions
diff --git a/server/sonar-web/design-system/src/components/__tests__/buttons-test.tsx b/server/sonar-web/design-system/src/components/__tests__/buttons-test.tsx new file mode 100644 index 00000000000..f2a0886ea84 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/buttons-test.tsx @@ -0,0 +1,41 @@ +/* + * 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 { screen } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { CodeViewerExpander } from '../buttons'; + +it('renders CodeViewerExpander correctly when direction is UP', () => { + render(<CodeViewerExpander direction="UP">Hello</CodeViewerExpander>); + const content = screen.getByText('Hello'); + expect(content).toHaveStyle({ + 'border-top': 'none', + 'border-bottom': '1px solid rgb(221,221,221)', + }); +}); + +it('renders CodeViewerExpander correctly when direction is DOWN', () => { + render(<CodeViewerExpander direction="DOWN">Hello</CodeViewerExpander>); + const content = screen.getByText('Hello'); + expect(content).toHaveStyle({ + 'border-bottom': 'none', + 'border-top': '1px solid rgb(221,221,221)', + }); +}); diff --git a/server/sonar-web/design-system/src/components/buttons.tsx b/server/sonar-web/design-system/src/components/buttons.tsx index c6d2ccd3ac8..6e955a2bb04 100644 --- a/server/sonar-web/design-system/src/components/buttons.tsx +++ b/server/sonar-web/design-system/src/components/buttons.tsx @@ -229,3 +229,29 @@ export const BareButton = styled.button` background-color: ${themeColor('dropdownMenuHover')}; } `; + +interface CodeViewerExpanderProps { + direction: 'UP' | 'DOWN'; +} + +export const CodeViewerExpander = styled(BareButton)<CodeViewerExpanderProps>` + ${tw`sw-flex sw-items-center sw-gap-2`} + ${tw`sw-px-2 sw-py-1`} + ${tw`sw-code`} + ${tw`sw-w-full`} + ${tw`sw-box-border`} + + color: ${themeContrast('codeLineEllipsis')}; + background-color: ${themeColor('codeLineEllipsis')}; + + &:hover { + color: ${themeContrast('codeLineEllipsisHover')}; + background-color: ${themeColor('codeLineEllipsisHover')}; + } + + border-top: ${(props) => + props.direction === 'DOWN' ? themeBorder('default', 'codeLineBorder') : 'none'}; + + border-bottom: ${(props) => + props.direction === 'UP' ? themeBorder('default', 'codeLineBorder') : 'none'}; +`; diff --git a/server/sonar-web/design-system/src/components/icons/index.ts b/server/sonar-web/design-system/src/components/icons/index.ts index 27e7dc2d9f6..492863fb778 100644 --- a/server/sonar-web/design-system/src/components/icons/index.ts +++ b/server/sonar-web/design-system/src/components/icons/index.ts @@ -27,6 +27,7 @@ export { ChevronRightIcon } from './ChevronRightIcon'; export { ClockIcon } from './ClockIcon'; export { CodeSmellIcon } from './CodeSmellIcon'; export { CommentIcon } from './CommentIcon'; +export { CopyIcon } from './CopyIcon'; export { DirectoryIcon } from './DirectoryIcon'; export { ExecutionFlowIcon } from './ExecutionFlowIcon'; export { FileIcon } from './FileIcon'; diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index 084037eb643..fb4ddc7a1d0 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -201,6 +201,25 @@ export const lightTheme = { codeLineCoveredUnderline: [...COLORS.green[500], 0.15], codeLineUncoveredUnderline: [...COLORS.red[500], 0.15], + codeLineHover: secondary.light, + codeLineHighlighted: COLORS.blueGrey[100], + codeLineNewCodeUnderline: [...COLORS.indigo[300], 0.15], + codeLineMeta: COLORS.blueGrey[300], + codeLineMetaHover: secondary.dark, + codeLineDuplication: secondary.default, + codeLineCovered: COLORS.green[300], + codeLineUncovered: danger.default, + codeLinePartiallyCoveredA: danger.default, + codeLinePartiallyCoveredB: COLORS.white, + codeLineIssueSquiggle: danger.lighter, + codeLineIssuePointerBorder: COLORS.white, + codeLineLocationHighlighted: [...COLORS.blueGrey[200], 0.6], + codeLineEllipsis: COLORS.white, + codeLineEllipsisHover: secondary.light, + codeLineIssueLocation: [...danger.lighter, 0.15], + codeLineIssueLocationSelected: [...danger.lighter, 0.5], + codeLineIssueMessageTooltip: secondary.darker, + // checkbox checkboxHover: COLORS.indigo[50], checkboxCheckedHover: primary.light, @@ -562,8 +581,14 @@ export const lightTheme = { toggleHover: secondary.darker, // code viewer + codeLineNewCodeUnderline: COLORS.indigo[500], + codeLineCoveredUnderline: COLORS.green[700], + codeLineUncoveredUnderline: COLORS.red[700], + codeLineEllipsis: COLORS.blueGrey[300], + codeLineEllipsisHover: secondary.dark, codeLineLocationMarker: COLORS.red[900], codeLineLocationMarkerSelected: COLORS.red[900], + codeLineIssueMessageTooltip: COLORS.blueGrey[25], // code snippet codeSnippetHighlight: danger.default, diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx index 194cdc3f7d1..93e16726fbc 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx @@ -17,13 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import classNames from 'classnames'; +import { FlagMessage } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { getSources } from '../../../api/components'; import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus'; import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing'; import IssueMessageBox from '../../../components/issue/IssueMessageBox'; -import { Alert } from '../../../components/ui/Alert'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; @@ -273,20 +274,26 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone return ( <> {issueIsClosed && ( - <Alert variant="success"> - <FormattedMessage - id={closedIssueMessageKey} - defaultMessage={translate(closedIssueMessageKey)} - values={{ - status: ( - <strong> - {translate('issue.status', issue.status)} ( - {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'}) - </strong> - ), - }} - /> - </Alert> + <FlagMessage + className="sw-mb-2 sw-flex" + variant="success" + ariaLabel={translate(closedIssueMessageKey)} + > + <div className="sw-block"> + <FormattedMessage + id={closedIssueMessageKey} + defaultMessage={translate(closedIssueMessageKey)} + values={{ + status: ( + <strong> + {translate('issue.status', issue.status)} ( + {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'}) + </strong> + ), + }} + /> + </div> + </FlagMessage> )} <IssueSourceViewerHeader @@ -337,6 +344,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone onLocationSelect={this.props.onLocationSelect} renderDuplicationPopup={this.renderDuplicationPopup} snippet={snippet} + className={classNames({ 'sw-mt-2': index !== 0 })} /> ))} </> diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx index cf4dbc06364..e51e0e58fb2 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx @@ -17,14 +17,24 @@ * 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 { + ChevronRightIcon, + CopyIcon, + DeferredSpinner, + HoverLink, + InteractiveIcon, + LightLabel, + Link, + ThemeProp, + UnfoldIcon, + themeColor, + withTheme, +} from 'design-system'; import * as React from 'react'; -import Link from '../../../components/common/Link'; -import { ButtonIcon } from '../../../components/controls/buttons'; -import { ClipboardIconButton } from '../../../components/controls/clipboard'; -import ExpandSnippetIcon from '../../../components/icons/ExpandSnippetIcon'; -import QualifierIcon from '../../../components/icons/QualifierIcon'; -import DeferredSpinner from '../../../components/ui/DeferredSpinner'; +import Tooltip from '../../../components/controls/Tooltip'; +import { ClipboardBase } from '../../../components/controls/clipboard'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; @@ -34,6 +44,8 @@ import { ComponentQualifier } from '../../../types/component'; import { SourceViewerFile } from '../../../types/types'; import './IssueSourceViewerHeader.css'; +export const INTERACTIVE_TOOLTIP_DELAY = 0.5; + export interface Props { branchLike: BranchLike | undefined; className?: string; @@ -45,7 +57,7 @@ export interface Props { sourceViewerFile: SourceViewerFile; } -export default function IssueSourceViewerHeader(props: Props) { +function IssueSourceViewerHeader(props: Props & ThemeProp) { const { branchLike, className, @@ -55,64 +67,83 @@ export default function IssueSourceViewerHeader(props: Props) { loading, onExpand, sourceViewerFile, + theme, } = props; const { measures, path, project, projectName, q } = sourceViewerFile; - const projectNameLabel = ( - <> - <QualifierIcon qualifier={ComponentQualifier.Project} /> <span>{projectName}</span> - </> - ); - const isProjectRoot = q === ComponentQualifier.Project; + const borderColor = themeColor('codeLineBorder')({ theme }); + + const IssueSourceViewerStyle = styled.div` + border: 1px solid ${borderColor}; + border-bottom: none; + `; + return ( - <div + <IssueSourceViewerStyle className={classNames( - 'issue-source-viewer-header display-flex-row display-flex-space-between', + 'sw-flex sw-justify-space-between sw-items-center sw-px-4 sw-py-3 sw-text-sm', className )} role="separator" aria-label={sourceViewerFile.path} > - <div className="display-flex-center flex-1"> + <div className="sw-flex-1"> {displayProjectName && ( - <div className="spacer-right"> + <> {linkToProject ? ( - <a - className="link-no-underline" - href={getPathUrlAsString(getBranchLikeUrl(project, branchLike))} + <HoverLink + to={getPathUrlAsString(getBranchLikeUrl(project, branchLike))} + className="sw-mr-2" > - {projectNameLabel} - </a> + <LightLabel>{projectName}</LightLabel> + </HoverLink> ) : ( - projectNameLabel + <LightLabel className="sw-ml-1 sw-mr-2">{projectName}</LightLabel> )} - </div> + </> )} {!isProjectRoot && ( <> - <div className="spacer-right"> - <QualifierIcon qualifier={q} /> <span>{collapsedDirFromPath(path)}</span> - <span className="component-name-file">{fileFromPath(path)}</span> - </div> + {displayProjectName && <ChevronRightIcon className="sw-mr-2" />} + <LightLabel> + {collapsedDirFromPath(path)} + {fileFromPath(path)} + </LightLabel> - <div className="spacer-right"> - <ClipboardIconButton - className="button-link link-no-underline" - copyValue={path} - aria-label={translate('source_viewer.click_to_copy_filepath')} - /> - </div> + <ClipboardBase> + {({ setCopyButton, copySuccess }) => { + return ( + <Tooltip + mouseEnterDelay={INTERACTIVE_TOOLTIP_DELAY} + overlay={ + <div className="sw-w-abs-150 sw-text-center"> + {translate(copySuccess ? 'copied_action' : 'copy_to_clipboard')} + </div> + } + {...(copySuccess ? { visible: copySuccess } : undefined)} + > + <InteractiveIcon + Icon={CopyIcon} + aria-label={translate('source_viewer.click_to_copy_filepath')} + data-clipboard-text={path} + className="sw-h-6 sw-mr-2" + innerRef={setCopyButton} + /> + </Tooltip> + ); + }} + </ClipboardBase> </> )} </div> {!isProjectRoot && measures.issues !== undefined && ( <div - className={classNames('flex-0 big-spacer-left', { - 'little-spacer-right': !expandable || loading, + className={classNames('sw-ml-4', { + 'sw-mr-1': !expandable || loading, })} > <Link @@ -127,19 +158,20 @@ export default function IssueSourceViewerHeader(props: Props) { </div> )} - {expandable && ( - <DeferredSpinner className="little-spacer-right" loading={loading}> - <div className="flex-0 big-spacer-left"> - <ButtonIcon - aria-label={translate('source_viewer.expand_all_lines')} - className="js-actions" - onClick={onExpand} - > - <ExpandSnippetIcon /> - </ButtonIcon> - </div> - </DeferredSpinner> + <DeferredSpinner className="sw-mr-1" loading={loading} /> + + {expandable && !loading && ( + <div className="sw-ml-4"> + <InteractiveIcon + Icon={UnfoldIcon} + aria-label={translate('source_viewer.expand_all_lines')} + className="sw-h-6" + onClick={onExpand} + /> + </div> )} - </div> + </IssueSourceViewerStyle> ); } + +export default withTheme(IssueSourceViewerHeader); diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx index 2180aa4bf6a..7a0becf21bb 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx @@ -18,6 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { + CodeViewerExpander, + ThemeProp, + UnfoldDownIcon, + UnfoldUpIcon, + themeColor, + withTheme, +} from 'design-system'; import * as React from 'react'; import Line from '../../../components/SourceViewer/components/Line'; import { symbolsByLine } from '../../../components/SourceViewer/helpers/indexing'; @@ -26,7 +34,6 @@ import { optimizeHighlightedSymbols, optimizeLocationMessage, } from '../../../components/SourceViewer/helpers/lines'; -import ExpandSnippetIcon from '../../../components/icons/ExpandSnippetIcon'; import { translate } from '../../../helpers/l10n'; import { Duplication, @@ -40,7 +47,7 @@ import { import './SnippetViewer.css'; import { LINES_BELOW_ISSUE } from './utils'; -interface Props { +export interface SnippetViewerProps { component: SourceViewerFile; displayLineNumberOptions?: boolean; displaySCM?: boolean; @@ -60,9 +67,10 @@ interface Props { renderAdditionalChildInLine?: (line: SourceLine) => React.ReactNode | undefined; renderDuplicationPopup: (index: number, line: number) => React.ReactNode; snippet: SourceLine[]; + className?: string; } -export default class SnippetViewer extends React.PureComponent<Props> { +class SnippetViewer extends React.PureComponent<SnippetViewerProps & ThemeProp> { expandBlock = (direction: ExpandDirection) => () => this.props.expandBlock(this.props.index, direction); @@ -136,8 +144,16 @@ export default class SnippetViewer extends React.PureComponent<Props> { } render() { - const { component, displaySCM, issue, lastSnippetOfLastGroup, locationsByLine, snippet } = - this.props; + const { + component, + displaySCM, + issue, + lastSnippetOfLastGroup, + locationsByLine, + snippet, + theme, + className, + } = this.props; const lastLine = component.measures && component.measures.lines && parseInt(component.measures.lines, 10); @@ -154,26 +170,24 @@ export default class SnippetViewer extends React.PureComponent<Props> { const displayDuplications = Boolean(this.props.loadDuplications) && snippet.some((s) => !!s.duplicated); + const borderColor = themeColor('codeLineBorder')({ theme }); + return ( - <div className="source-viewer-code snippet"> + <div + className={classNames('source-viewer-code', className)} + style={{ border: `1px solid ${borderColor}` }} + > <div> {snippet[0].line > 1 && ( - <div className="expand-block expand-block-above"> - <button - aria-label={translate('source_viewer.expand_above')} - onClick={this.expandBlock('up')} - type="button" - > - <ExpandSnippetIcon /> - </button> - </div> + <CodeViewerExpander + direction="UP" + className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2" + onClick={this.expandBlock('up')} + > + <UnfoldUpIcon aria-label={translate('source_viewer.expand_above')} /> + </CodeViewerExpander> )} - <table - className={classNames('source-table', { - 'expand-up': snippet[0].line > 1, - 'expand-down': !lastLine || snippet[snippet.length - 1].line < lastLine, - })} - > + <table> <tbody> {snippet.map((line, index) => this.renderLine({ @@ -190,18 +204,18 @@ export default class SnippetViewer extends React.PureComponent<Props> { </tbody> </table> {(!lastLine || snippet[snippet.length - 1].line < lastLine) && ( - <div className="expand-block expand-block-below"> - <button - aria-label={translate('source_viewer.expand_below')} - onClick={this.expandBlock('down')} - type="button" - > - <ExpandSnippetIcon /> - </button> - </div> + <CodeViewerExpander + className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2" + onClick={this.expandBlock('down')} + direction="DOWN" + > + <UnfoldDownIcon aria-label={translate('source_viewer.expand_below')} /> + </CodeViewerExpander> )} </div> </div> ); } } + +export default withTheme(SnippetViewer); diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx index d95876d1cde..fa075dc4c4c 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx @@ -26,7 +26,7 @@ import IssueSourceViewerHeader, { Props } from '../IssueSourceViewerHeader'; const ui = { expandAllLines: byRole('button', { name: 'source_viewer.expand_all_lines' }), - projectLink: byRole('link', { name: 'qualifier.TRK MyProject' }), + projectLink: byRole('link', { name: 'MyProject' }), projectName: byText('MyProject'), viewAllIssues: byRole('link', { name: 'source_viewer.view_all_issues' }), }; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx index 71915f77d7a..cdbcdb84b88 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx @@ -24,7 +24,7 @@ import { byRole } from 'testing-library-selector'; import { mockSourceLine, mockSourceViewerFile } from '../../../../helpers/mocks/sources'; import { mockIssue } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; -import SnippetViewer from '../SnippetViewer'; +import SnippetViewer, { SnippetViewerProps } from '../SnippetViewer'; beforeEach(() => { jest.clearAllMocks(); @@ -91,7 +91,7 @@ it('should render additional child in line', () => { expect(screen.getByTestId('additional-child')).toBeInTheDocument(); }); -function renderSnippetViewer(props: Partial<SnippetViewer['props']> = {}) { +function renderSnippetViewer(props: Partial<SnippetViewerProps> = {}) { return renderComponent( <SnippetViewer component={mockSourceViewerFile()} |