--- /dev/null
+/*
+ * 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)',
+ });
+});
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'};
+`;
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';
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,
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,
* 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';
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
onLocationSelect={this.props.onLocationSelect}
renderDuplicationPopup={this.renderDuplicationPopup}
snippet={snippet}
+ className={classNames({ 'sw-mt-2': index !== 0 })}
/>
))}
</>
* 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';
import { SourceViewerFile } from '../../../types/types';
import './IssueSourceViewerHeader.css';
+export const INTERACTIVE_TOOLTIP_DELAY = 0.5;
+
export interface Props {
branchLike: BranchLike | undefined;
className?: string;
sourceViewerFile: SourceViewerFile;
}
-export default function IssueSourceViewerHeader(props: Props) {
+function IssueSourceViewerHeader(props: Props & ThemeProp) {
const {
branchLike,
className,
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
</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);
* 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';
optimizeHighlightedSymbols,
optimizeLocationMessage,
} from '../../../components/SourceViewer/helpers/lines';
-import ExpandSnippetIcon from '../../../components/icons/ExpandSnippetIcon';
import { translate } from '../../../helpers/l10n';
import {
Duplication,
import './SnippetViewer.css';
import { LINES_BELOW_ISSUE } from './utils';
-interface Props {
+export interface SnippetViewerProps {
component: SourceViewerFile;
displayLineNumberOptions?: boolean;
displaySCM?: boolean;
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);
}
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);
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({
</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);
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' }),
};
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();
expect(screen.getByTestId('additional-child')).toBeInTheDocument();
});
-function renderSnippetViewer(props: Partial<SnippetViewer['props']> = {}) {
+function renderSnippetViewer(props: Partial<SnippetViewerProps> = {}) {
return renderComponent(
<SnippetViewer
component={mockSourceViewerFile()}