diff options
Diffstat (limited to 'server/sonar-web/src')
5 files changed, 152 insertions, 98 deletions
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()} |