diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer')
5 files changed, 162 insertions, 163 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 93e16726fbc..f6029dfa639 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 @@ -62,7 +62,6 @@ interface Props { isLastOccurenceOfPrimaryComponent: boolean; issue: TypeIssue; issuesByLine: IssuesByLine; - lastSnippetGroup: boolean; loadDuplications: (component: string, line: SourceLine) => void; locations: FlowLocation[]; onIssueSelect: (issueKey: string) => void; @@ -256,8 +255,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone }; render() { - const { branchLike, isLastOccurenceOfPrimaryComponent, issue, lastSnippetGroup, snippetGroup } = - this.props; + const { branchLike, isLastOccurenceOfPrimaryComponent, issue, snippetGroup } = this.props; const { additionalLines, loading, snippets } = this.state; const snippetLines = linesForSnippets(snippets, { @@ -320,7 +318,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone </IssueSourceViewerScrollContext.Consumer> )} - {snippetLines.map((snippet, index) => ( + {snippetLines.map(({ snippet, sourcesMap }, index) => ( <SnippetViewer key={snippets[index].index} renderAdditionalChildInLine={this.renderIssuesList} @@ -332,8 +330,6 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone highlightedLocationMessage={this.props.highlightedLocationMessage} highlightedSymbols={this.state.highlightedSymbols} index={snippets[index].index} - issue={this.props.issue} - lastSnippetOfLastGroup={lastSnippetGroup && index === snippets.length - 1} loadDuplications={this.loadDuplications} locations={this.props.locations} locationsByLine={getLocationsByLine( @@ -345,6 +341,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone renderDuplicationPopup={this.renderDuplicationPopup} snippet={snippet} className={classNames({ 'sw-mt-2': index !== 0 })} + snippetSourcesMap={sourcesMap} /> ))} </> diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx index d1ec05f8ba0..8e5e8551a02 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx @@ -21,6 +21,7 @@ import { findLastIndex, keyBy } from 'lodash'; import * as React from 'react'; import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components'; import { getIssueFlowSnippets } from '../../../api/issues'; +import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext'; import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup'; import { filterDuplicationBlocksByLine, @@ -31,7 +32,6 @@ import { duplicationsByLine as getDuplicationsByLine, issuesByComponentAndLine, } from '../../../components/SourceViewer/helpers/indexing'; -import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext'; import { Alert } from '../../../components/ui/Alert'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { WorkspaceContext } from '../../../components/workspace/context'; @@ -175,10 +175,11 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop <DuplicationPopup blocks={filterDuplicationBlocksByLine(blocks, line)} branchLike={this.props.branchLike} - duplicatedFiles={duplicatedFiles} inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)} + duplicatedFiles={duplicatedFiles} openComponent={openComponent} sourceViewerFile={component} + duplicationHeader={translate('component_viewer.transition.duplication')} /> )} </WorkspaceContext.Consumer> @@ -234,7 +235,6 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop issue={issue} issuesByLine={issuesByComponent[snippetGroup.component.key] || {}} isLastOccurenceOfPrimaryComponent={i === lastOccurenceOfPrimaryComponent} - lastSnippetGroup={i === locationsByComponent.length - 1} loadDuplications={this.fetchDuplications} locations={snippetGroup.locations || []} onIssueSelect={this.props.onIssueSelect} 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 7a0becf21bb..b428953ca11 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 @@ -20,13 +20,15 @@ import classNames from 'classnames'; import { CodeViewerExpander, + SonarCodeColorizer, ThemeProp, UnfoldDownIcon, UnfoldUpIcon, themeColor, withTheme, } from 'design-system'; -import * as React from 'react'; +import { debounce, throttle } from 'lodash'; +import React from 'react'; import Line from '../../../components/SourceViewer/components/Line'; import { symbolsByLine } from '../../../components/SourceViewer/helpers/indexing'; import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations'; @@ -39,13 +41,11 @@ import { Duplication, ExpandDirection, FlowLocation, - Issue, + LineMap, LinearIssueLocation, SourceLine, SourceViewerFile, } from '../../../types/types'; -import './SnippetViewer.css'; -import { LINES_BELOW_ISSUE } from './utils'; export interface SnippetViewerProps { component: SourceViewerFile; @@ -58,8 +58,6 @@ export interface SnippetViewerProps { highlightedLocationMessage: { index: number; text: string | undefined } | undefined; highlightedSymbols: string[]; index: number; - issue: Pick<Issue, 'key' | 'textRange' | 'line'>; - lastSnippetOfLastGroup: boolean; loadDuplications?: (line: SourceLine) => void; locations: FlowLocation[]; locationsByLine: { [line: number]: LinearIssueLocation[] }; @@ -68,154 +66,142 @@ export interface SnippetViewerProps { renderDuplicationPopup: (index: number, line: number) => React.ReactNode; snippet: SourceLine[]; className?: string; + snippetSourcesMap?: LineMap; } -class SnippetViewer extends React.PureComponent<SnippetViewerProps & ThemeProp> { - expandBlock = (direction: ExpandDirection) => () => - this.props.expandBlock(this.props.index, direction); +type Props = SnippetViewerProps & ThemeProp; - renderLine({ - displayDuplications, - displaySCM, - index, - issueLocations, - line, - snippet, - symbols, - verticalBuffer, - }: { - displayDuplications: boolean; - displaySCM?: boolean; - index: number; - issueLocations: LinearIssueLocation[]; - line: SourceLine; - snippet: SourceLine[]; - symbols: string[]; - verticalBuffer: number; - }) { - const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations); +function SnippetViewer(props: Props) { + const expandBlock = (direction: ExpandDirection) => () => { + props.expandBlock(props.index, direction); + }; - const { displayLineNumberOptions, duplications, duplicationsByLine } = this.props; - const duplicationsCount = duplications ? duplications.length : 0; - const lineDuplications = - (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || []; + const { component, displaySCM, locationsByLine, snippet, theme, className } = props; - const firstLineNumber = snippet && snippet.length ? snippet[0].line : 0; - const noop = () => {}; + const { displayLineNumberOptions, duplications, duplicationsByLine, snippetSourcesMap } = props; + const duplicationsCount = duplications ? duplications.length : 0; - return ( - <Line - displayCoverage={true} - displayDuplications={displayDuplications} - displayIssues={false} - displayLineNumberOptions={displayLineNumberOptions} - displayLocationMarkers={true} - displaySCM={displaySCM} - duplications={lineDuplications} - duplicationsCount={duplicationsCount} - firstLineNumber={firstLineNumber} - highlighted={false} - highlightedLocationMessage={optimizeLocationMessage( - this.props.highlightedLocationMessage, - secondaryIssueLocations - )} - highlightedSymbols={optimizeHighlightedSymbols(symbols, this.props.highlightedSymbols)} - issueLocations={issueLocations} - issues={[]} - key={line.line} - last={false} - line={line} - loadDuplications={this.props.loadDuplications || noop} - onIssueSelect={noop} - onIssueUnselect={noop} - onIssuesClose={noop} - onIssuesOpen={noop} - onLocationSelect={this.props.onLocationSelect} - onSymbolClick={this.props.handleSymbolClick} - openIssues={false} - previousLine={index > 0 ? snippet[index - 1] : undefined} - renderDuplicationPopup={this.props.renderDuplicationPopup} - secondaryIssueLocations={secondaryIssueLocations} - verticalBuffer={verticalBuffer} - > - {this.props.renderAdditionalChildInLine && this.props.renderAdditionalChildInLine(line)} - </Line> - ); - } + const firstLineNumber = snippet?.length ? snippet[0].line : 0; + const noop = () => { + /* noop */ + }; + const lastLine = component.measures?.lines && parseInt(component.measures.lines, 10); + + const symbols = symbolsByLine(snippet); - render() { - 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(props.loadDuplications) && snippet.some((s) => !!s.duplicated); - const symbols = symbolsByLine(snippet); + const borderColor = themeColor('codeLineBorder')({ theme }); - const bottomLine = snippet[snippet.length - 1].line; - const issueLine = issue.textRange ? issue.textRange.endLine : issue.line; + const THROTTLE_SHORT_DELAY = 10; + const [hoveredLine, setHoveredLine] = React.useState<SourceLine | undefined>(); - const verticalBuffer = - lastSnippetOfLastGroup && issueLine - ? Math.max(0, LINES_BELOW_ISSUE - (bottomLine - issueLine)) - : 0; + const onLineMouseEnter = React.useMemo( + () => + throttle( + (hoveredLine: number) => + snippetSourcesMap ? setHoveredLine(snippetSourcesMap[hoveredLine]) : undefined, + THROTTLE_SHORT_DELAY + ), + [snippetSourcesMap] + ); - const displayDuplications = - Boolean(this.props.loadDuplications) && snippet.some((s) => !!s.duplicated); + const onLineMouseLeave = React.useMemo( + () => + debounce( + (line: number) => + setHoveredLine((hoveredLine) => (hoveredLine?.line === line ? undefined : hoveredLine)), + THROTTLE_SHORT_DELAY + ), + [] + ); - const borderColor = themeColor('codeLineBorder')({ theme }); + return ( + <div + className={classNames('it__source-viewer-code', className)} + style={{ border: `1px solid ${borderColor}` }} + > + <SonarCodeColorizer> + {snippet[0].line > 1 && ( + <CodeViewerExpander + direction="UP" + className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2" + onClick={expandBlock('up')} + > + <UnfoldUpIcon aria-label={translate('source_viewer.expand_above')} /> + </CodeViewerExpander> + )} + <table className="sw-w-full"> + <tbody> + {snippet.map((line, index) => { + const secondaryIssueLocations = getSecondaryIssueLocationsForLine( + line, + props.locations + ); + const lineDuplications = + (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || []; - return ( - <div - className={classNames('source-viewer-code', className)} - style={{ border: `1px solid ${borderColor}` }} - > - <div> - {snippet[0].line > 1 && ( - <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> - <tbody> - {snippet.map((line, index) => - this.renderLine({ - displayDuplications, - displaySCM, - index, - issueLocations: locationsByLine[line.line] || [], - line, - snippet, - symbols: symbols[line.line], - verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0, - }) - )} - </tbody> - </table> - {(!lastLine || snippet[snippet.length - 1].line < lastLine) && ( - <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> - ); - } + const displayCoverageUnderline = hoveredLine?.coverageBlock === line.coverageBlock; + const displayNewCodeUnderline = hoveredLine?.newCodeBlock === line.line; + return ( + <Line + displayCoverage={true} + displayCoverageUnderline={displayCoverageUnderline} + displayNewCodeUnderline={displayNewCodeUnderline} + displayDuplications={displayDuplications} + displayIssues={false} + displayLineNumberOptions={displayLineNumberOptions} + displayLocationMarkers={true} + displaySCM={displaySCM} + duplications={lineDuplications} + duplicationsCount={duplicationsCount} + firstLineNumber={firstLineNumber} + highlighted={false} + highlightedLocationMessage={optimizeLocationMessage( + props.highlightedLocationMessage, + secondaryIssueLocations + )} + highlightedSymbols={optimizeHighlightedSymbols( + symbols[line.line], + props.highlightedSymbols + )} + issueLocations={locationsByLine[line.line] || []} + issues={[]} + key={line.line} + line={line} + loadDuplications={props.loadDuplications ?? noop} + onIssueSelect={noop} + onIssueUnselect={noop} + onIssuesClose={noop} + onIssuesOpen={noop} + onLocationSelect={props.onLocationSelect} + onSymbolClick={props.handleSymbolClick} + openIssues={false} + previousLine={index > 0 ? snippet[index - 1] : undefined} + renderDuplicationPopup={props.renderDuplicationPopup} + secondaryIssueLocations={secondaryIssueLocations} + onLineMouseEnter={onLineMouseEnter} + onLineMouseLeave={onLineMouseLeave} + > + {props.renderAdditionalChildInLine?.(line)} + </Line> + ); + })} + </tbody> + </table> + {(!lastLine || snippet[snippet.length - 1].line < lastLine) && ( + <CodeViewerExpander + className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2" + onClick={expandBlock('down')} + direction="DOWN" + > + <UnfoldDownIcon aria-label={translate('source_viewer.expand_below')} /> + </CodeViewerExpander> + )} + </SonarCodeColorizer> + </div> + ); } export default withTheme(SnippetViewer); 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 cdbcdb84b88..df138d37b67 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 @@ -22,7 +22,6 @@ import { range } from 'lodash'; import * as React from 'react'; 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, { SnippetViewerProps } from '../SnippetViewer'; @@ -102,8 +101,6 @@ function renderSnippetViewer(props: Partial<SnippetViewerProps> = {}) { highlightedLocationMessage={{ index: 0, text: '' }} highlightedSymbols={[]} index={0} - issue={mockIssue()} - lastSnippetOfLastGroup={false} loadDuplications={jest.fn()} locations={[]} locationsByLine={{}} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts index 10287fb3267..f84d5db25b0 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { isDefined } from '../../../helpers/types'; import { ComponentQualifier } from '../../../types/component'; import { ExpandDirection, @@ -134,18 +135,36 @@ export function createSnippets(params: { return hasSecondaryLocations ? ranges.sort((a, b) => a.start - b.start) : ranges; } +function decorateWithUnderlineFlags(line: SourceLine, sourcesMap: LineMap) { + const previousLine = sourcesMap[line.line - 1]; + const decoratedLine = { ...line }; + if (isDefined(line.coverageStatus)) { + decoratedLine.coverageBlock = + line.coverageStatus === previousLine?.coverageStatus ? previousLine.coverageBlock : line.line; + } + if (line.isNew) { + decoratedLine.newCodeBlock = previousLine?.isNew ? previousLine.newCodeBlock : line.line; + } + return decoratedLine; +} + export function linesForSnippets(snippets: Snippet[], componentLines: LineMap) { - return snippets - .map((snippet) => { - const lines = []; - for (let i = snippet.start; i <= snippet.end; i++) { - if (componentLines[i]) { - lines.push(componentLines[i]); - } + return snippets.reduce<Array<{ snippet: SourceLine[]; sourcesMap: LineMap }>>((acc, snippet) => { + const snippetSources = []; + const snippetSourcesMap: LineMap = {}; + for (let idx = snippet.start; idx <= snippet.end; idx++) { + if (isDefined(componentLines[idx])) { + const line = decorateWithUnderlineFlags(componentLines[idx], snippetSourcesMap); + snippetSourcesMap[line.line] = line; + snippetSources.push(line); } - return lines; - }) - .filter((snippet) => snippet.length > 0); + } + + if (snippetSources.length > 0) { + acc.push({ snippet: snippetSources, sourcesMap: snippetSourcesMap }); + } + return acc; + }, []); } export function groupLocationsByComponent( |