diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2022-09-13 17:10:26 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-09-16 20:03:14 +0000 |
commit | 227f5d3bfc797c8ecc7ebe7e283af3b68522c1ad (patch) | |
tree | 4034b980dd5be42cbd58ea8a3a7c508f0c60d953 | |
parent | 6f2881c71996715ba3a9a7b2451f008c2239d7eb (diff) | |
download | sonarqube-227f5d3bfc797c8ecc7ebe7e283af3b68522c1ad.tar.gz sonarqube-227f5d3bfc797c8ecc7ebe7e283af3b68522c1ad.zip |
SONAR-17285 Display new data and execution type flows for issues
93 files changed, 793 insertions, 708 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts index cca51bbd35f..243c522293a 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -44,6 +44,7 @@ import { import { Standards } from '../../types/security'; import { Dict, + FlowType, RuleActivation, RuleDetails, SnippetsByComponent, @@ -97,6 +98,98 @@ export default class IssuesServiceMock { this.list = [ { issue: mockRawIssue(false, { + key: 'issue11', + component: 'project:file.foo', + message: 'FlowIssue', + rule: 'simpleRuleId', + textRange: { + startLine: 10, + endLine: 10, + startOffset: 0, + endOffset: 2 + }, + flows: [ + { + type: FlowType.DATA, + description: 'Backtracking 1', + locations: [ + { + component: 'project:file.foo', + msg: 'Data location 1', + textRange: { + startLine: 20, + endLine: 20, + startOffset: 0, + endOffset: 1 + } + }, + { + component: 'project:file.foo', + msg: 'Data location 2', + textRange: { + startLine: 21, + endLine: 21, + startOffset: 0, + endOffset: 1 + } + } + ] + }, + { + type: FlowType.EXECUTION, + locations: [ + { + component: 'project:file.bar', + msg: 'Execution location 1', + textRange: { + startLine: 20, + endLine: 20, + startOffset: 0, + endOffset: 1 + } + }, + { + component: 'project:file.bar', + msg: 'Execution location 2', + textRange: { + startLine: 22, + endLine: 22, + startOffset: 0, + endOffset: 1 + } + }, + { + component: 'project:file.bar', + msg: 'Execution location 3', + textRange: { + startLine: 5, + endLine: 5, + startOffset: 0, + endOffset: 1 + } + } + ] + } + ] + }), + snippets: keyBy( + [ + mockSnippetsByComponent( + 'file.foo', + 'project', + times(40, i => i + 1) + ), + mockSnippetsByComponent( + 'file.bar', + 'project', + times(40, i => i + 1) + ) + ], + 'component.key' + ) + }, + { + issue: mockRawIssue(false, { key: 'issue0', component: 'project:file.foo', message: 'Issue on file', diff --git a/server/sonar-web/src/main/js/api/webhooks.ts b/server/sonar-web/src/main/js/api/webhooks.ts index 3c9fe1300ef..b61b549b526 100644 --- a/server/sonar-web/src/main/js/api/webhooks.ts +++ b/server/sonar-web/src/main/js/api/webhooks.ts @@ -19,7 +19,8 @@ */ import { throwGlobalError } from '../helpers/error'; import { getJSON, post, postJSON } from '../helpers/request'; -import { Paging, Webhook, WebhookDelivery } from '../types/types'; +import { Paging } from '../types/types'; +import { Webhook, WebhookDelivery } from '../types/webhook'; export function createWebhook(data: { name: string; diff --git a/server/sonar-web/src/main/js/app/styles/components/boxed-group.css b/server/sonar-web/src/main/js/app/styles/components/boxed-group.css index c132dc87917..ac963312e42 100644 --- a/server/sonar-web/src/main/js/app/styles/components/boxed-group.css +++ b/server/sonar-web/src/main/js/app/styles/components/boxed-group.css @@ -24,6 +24,10 @@ background-color: #fff; } +.boxed-group.no-border { + border-color: transparent; +} + .boxed-group-centered { margin-left: auto; margin-right: auto; @@ -87,16 +91,14 @@ } .boxed-group-accordion { + border-color: var(--neutral200); margin-bottom: var(--gridSize); transition: border-color 0.3s ease; } -.boxed-group-accordion:not(.no-hover):hover { - border-color: var(--blue); -} - -.boxed-group-accordion:not(.no-hover):hover .boxed-group-accordion-title { - color: var(--blue); +.boxed-group-accordion:hover, +.boxed-group-accordion.open { + border-color: var(--info400); } .boxed-group-accordion .boxed-group-header { diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index 7c626a19ffd..defcfbeae9c 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -638,6 +638,10 @@ th.huge-spacer-right { color: inherit; } +.muted { + color: var(--neutral600); +} + .leak-box { background-color: var(--leakPrimaryColor); border: 1px solid var(--leakSecondaryColor); diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js index 6bfe13d93b9..f51756fb1b3 100644 --- a/server/sonar-web/src/main/js/app/theme.js +++ b/server/sonar-web/src/main/js/app/theme.js @@ -148,7 +148,9 @@ module.exports = { primarya40: 'rgba(35, 107, 151, 0.40)', primary400: '#297BAE', + info50: '#ECF6FE', info500: '#0271B9', + info400: '#4B9FD5', success500: '#008A25', success500a20: 'rgba(0, 138, 37, 0.20)', @@ -166,6 +168,7 @@ module.exports = { error500: '#D02F3A', error500a20: 'rgba(208, 47, 58, 0.20)', + neutral200: '#CCCCCC', neutral600: '#666666', neutral800: '#333333', diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx index 65d2281e84c..990b61db226 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx @@ -89,6 +89,54 @@ it('should be able to bulk change', async () => { ).toBeInTheDocument(); }); +it('should interact with flows and locations', async () => { + const user = userEvent.setup(); + renderProjectIssuesApp('project/issues?issues=issue11&open=issue11&id=myproject'); + const dataFlowButton = await screen.findByRole('button', { name: 'Backtracking 1' }); + const exectionFlowButton = await screen.findByRole('button', { name: 'issue.execution_flow' }); + + let dataLocation1Button = screen.getByRole('button', { name: '1 Data location 1' }); + let dataLocation2Button = screen.getByRole('button', { name: '2 Data location 2' }); + + expect(dataFlowButton).toBeInTheDocument(); + expect(dataLocation1Button).toBeInTheDocument(); + expect(dataLocation2Button).toBeInTheDocument(); + + await user.click(dataFlowButton); + // Colapsing flow + expect(dataLocation1Button).not.toBeInTheDocument(); + expect(dataLocation2Button).not.toBeInTheDocument(); + + await user.click(exectionFlowButton); + expect(screen.getByRole('button', { name: '1 Execution location 1' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '2 Execution location 2' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '3 Execution location 3' })).toBeInTheDocument(); + + // Keyboard interaction + await user.click(dataFlowButton); + dataLocation1Button = screen.getByRole('button', { name: '1 Data location 1' }); + dataLocation2Button = screen.getByRole('button', { name: '2 Data location 2' }); + + //Location navigation + await user.keyboard('{Alt>}{ArrowDown}{/Alt}'); + expect(dataLocation1Button).toHaveClass('selected'); + await user.keyboard('{Alt>}{ArrowDown}{/Alt}'); + expect(dataLocation1Button).not.toHaveClass('selected'); + expect(dataLocation2Button).toHaveClass('selected'); + await user.keyboard('{Alt>}{ArrowDown}{/Alt}'); + expect(dataLocation1Button).not.toHaveClass('selected'); + expect(dataLocation2Button).not.toHaveClass('selected'); + await user.keyboard('{Alt>}{ArrowUp}{/Alt}'); + expect(dataLocation1Button).not.toHaveClass('selected'); + expect(dataLocation2Button).toHaveClass('selected'); + + //Flow navigation + await user.keyboard('{Alt>}{ArrowRight}{/Alt}'); + expect(screen.getByRole('button', { name: '1 Execution location 1' })).toHaveClass('selected'); + await user.keyboard('{Alt>}{ArrowLeft}{/Alt}'); + expect(screen.getByRole('button', { name: '1 Data location 1' })).toHaveClass('selected'); +}); + it('should show education principles', async () => { const user = userEvent.setup(); renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject'); @@ -468,7 +516,7 @@ it('should show code tabs when any secondary location is selected', async () => }) ).not.toBeInTheDocument(); - await user.click(screen.getByRole('link', { name: '1 location 1' })); + await user.click(screen.getByRole('button', { name: '1 location 1' })); expect( screen.getByRole('row', { name: '2 source_viewer.tooltip.covered import java.util. ArrayList ;' @@ -485,7 +533,7 @@ it('should show code tabs when any secondary location is selected', async () => }) ).not.toBeInTheDocument(); - await user.click(screen.getByRole('link', { name: '1 location 1' })); + await user.click(screen.getByRole('button', { name: '1 location 1' })); expect( screen.getByRole('row', { name: '2 source_viewer.tooltip.covered import java.util. ArrayList ;' diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/actions-test.ts b/server/sonar-web/src/main/js/apps/issues/__tests__/actions-test.ts index 9fd0d4f26fc..1a5b23ef3f2 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/actions-test.ts +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/actions-test.ts @@ -26,7 +26,7 @@ describe('selectFlow', () => { expect(selectFlow(5)()).toEqual({ locationsNavigator: true, selectedFlowIndex: 5, - selectedLocationIndex: 0 + selectedLocationIndex: undefined }); }); }); diff --git a/server/sonar-web/src/main/js/apps/issues/actions.ts b/server/sonar-web/src/main/js/apps/issues/actions.ts index 94a84475d07..86077870ebe 100644 --- a/server/sonar-web/src/main/js/apps/issues/actions.ts +++ b/server/sonar-web/src/main/js/apps/issues/actions.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { State } from './components/IssuesApp'; +import { getLocations } from './utils'; export function enableLocationsNavigator(state: State) { const { openIssue, selectedLocationIndex } = state; @@ -45,17 +46,16 @@ export function selectNextLocation( ) { const { selectedFlowIndex, selectedLocationIndex: index = -1, openIssue } = state; if (openIssue) { - const locations = - selectedFlowIndex !== undefined - ? openIssue.flows[selectedFlowIndex] - : openIssue.secondaryLocations; + const locations = getLocations(openIssue, selectedFlowIndex); + const lastLocationIdx = locations.length - 1; if (index === lastLocationIdx) { // -1 to jump back to the issue itself - return { selectedLocationIndex: -1 }; + return { selectedLocationIndex: -1, locationsNavigator: true }; } return { - selectedLocationIndex: index !== undefined && index < lastLocationIdx ? index + 1 : index + selectedLocationIndex: index !== undefined && index < lastLocationIdx ? index + 1 : index, + locationsNavigator: true }; } return null; @@ -65,32 +65,48 @@ export function selectPreviousLocation(state: State) { const { selectedFlowIndex, selectedLocationIndex: index, openIssue } = state; if (openIssue) { if (index === -1) { - const locations = - selectedFlowIndex !== undefined - ? openIssue.flows[selectedFlowIndex] - : openIssue.secondaryLocations; + const locations = getLocations(openIssue, selectedFlowIndex); const lastLocationIdx = locations.length - 1; - return { selectedLocationIndex: lastLocationIdx }; + return { selectedLocationIndex: lastLocationIdx, locationsNavigator: true }; } - return { selectedLocationIndex: index !== undefined && index > 0 ? index - 1 : index }; + return { + selectedLocationIndex: index !== undefined && index > 0 ? index - 1 : index, + locationsNavigator: true + }; } return null; } export function selectFlow(nextIndex?: number) { return () => { - return { locationsNavigator: true, selectedFlowIndex: nextIndex, selectedLocationIndex: 0 }; + return { + locationsNavigator: true, + selectedFlowIndex: nextIndex, + selectedLocationIndex: undefined + }; }; } export function selectNextFlow(state: State) { const { openIssue, selectedFlowIndex } = state; + if ( openIssue && selectedFlowIndex !== undefined && - openIssue.flows.length > selectedFlowIndex + 1 + (openIssue.flows.length > selectedFlowIndex + 1 || + openIssue.flowsWithType.length > selectedFlowIndex + 1) ) { - return { selectedFlowIndex: selectedFlowIndex + 1, selectedLocationIndex: 0 }; + return { + selectedFlowIndex: selectedFlowIndex + 1, + selectedLocationIndex: 0, + locationsNavigator: true + }; + } else if ( + openIssue && + selectedFlowIndex === undefined && + (openIssue.flows.length > 0 || openIssue.flowsWithType.length > 0) + ) { + return { selectedFlowIndex: 0, selectedLocationIndex: 0, locationsNavigator: true }; } return null; } @@ -98,7 +114,11 @@ export function selectNextFlow(state: State) { export function selectPreviousFlow(state: State) { const { openIssue, selectedFlowIndex } = state; if (openIssue && selectedFlowIndex !== undefined && selectedFlowIndex > 0) { - return { selectedFlowIndex: selectedFlowIndex - 1, selectedLocationIndex: 0 }; + return { + selectedFlowIndex: selectedFlowIndex - 1, + selectedLocationIndex: 0, + locationsNavigator: true + }; } return null; } diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index ec5dce35d59..4c9aee53cec 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -230,9 +230,9 @@ export class App extends React.PureComponent<Props, State> { this.setState({ checkAll: false }); } else if (openIssue && openIssue.key !== this.state.selected) { this.setState({ - locationsNavigator: false, + locationsNavigator: true, selected: openIssue.key, - selectedFlowIndex: undefined, + selectedFlowIndex: 0, selectedLocationIndex: undefined }); } @@ -504,6 +504,7 @@ export class App extends React.PureComponent<Props, State> { effortTotal, facets: { ...state.facets, ...parseFacets(facets) }, loading: false, + locationsNavigator: true, issues, openIssue, paging, @@ -513,7 +514,7 @@ export class App extends React.PureComponent<Props, State> { referencedRules: keyBy(other.rules, 'key'), referencedUsers: keyBy(other.users, 'login'), selected, - selectedFlowIndex: undefined, + selectedFlowIndex: 0, selectedLocationIndex: undefined })); } diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap index 7f9e4bae817..0d8e3039633 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap @@ -21,6 +21,7 @@ exports[`should render correctly 2`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -61,6 +62,7 @@ exports[`should render correctly 2`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN3", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -97,6 +99,7 @@ exports[`should render correctly 2`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap index f835fb8a7b3..7e70e983693 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap @@ -30,7 +30,6 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = ` Array [ Object { "component": "main.js", - "index": 0, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -40,7 +39,6 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = ` }, Object { "component": "main.js", - "index": 1, "textRange": Object { "endLine": 12, "endOffset": 2, @@ -50,6 +48,7 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = ` }, ], ], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -61,6 +60,7 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = ` "secondaryLocations": Array [ Object { "component": "main.js", + "index": 0, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -70,6 +70,7 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = ` }, Object { "component": "main.js", + "index": 1, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -150,6 +151,7 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = ` }, ], ], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -207,9 +209,9 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = ` "component": "main.js", "index": 1, "textRange": Object { - "endLine": 12, + "endLine": 2, "endOffset": 2, - "startLine": 10, + "startLine": 1, "startOffset": 1, }, }, @@ -251,7 +253,6 @@ exports[`should render SourceViewer correctly: all secondary locations on same l Array [ Object { "component": "main.js", - "index": 0, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -261,7 +262,6 @@ exports[`should render SourceViewer correctly: all secondary locations on same l }, Object { "component": "main.js", - "index": 1, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -271,7 +271,6 @@ exports[`should render SourceViewer correctly: all secondary locations on same l }, Object { "component": "main.js", - "index": 2, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -281,6 +280,7 @@ exports[`should render SourceViewer correctly: all secondary locations on same l }, ], ], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -292,6 +292,7 @@ exports[`should render SourceViewer correctly: all secondary locations on same l "secondaryLocations": Array [ Object { "component": "main.js", + "index": 0, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -301,6 +302,7 @@ exports[`should render SourceViewer correctly: all secondary locations on same l }, Object { "component": "main.js", + "index": 1, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -381,6 +383,7 @@ exports[`should render SourceViewer correctly: all secondary locations on same l }, ], ], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -444,16 +447,6 @@ exports[`should render SourceViewer correctly: all secondary locations on same l "startOffset": 1, }, }, - Object { - "component": "main.js", - "index": 2, - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, ] } onIssueSelect={[MockFunction]} @@ -489,6 +482,7 @@ exports[`should render SourceViewer correctly: default 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -520,6 +514,7 @@ exports[`should render SourceViewer correctly: default 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -579,7 +574,6 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = ` Array [ Object { "component": "main.js", - "index": 0, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -589,6 +583,7 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = ` }, ], ], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -600,6 +595,7 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = ` "secondaryLocations": Array [ Object { "component": "main.js", + "index": 0, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -609,6 +605,7 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = ` }, Object { "component": "main.js", + "index": 1, "textRange": Object { "endLine": 2, "endOffset": 2, @@ -689,6 +686,7 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = ` }, ], ], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -742,6 +740,16 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = ` "startOffset": 1, }, }, + Object { + "component": "main.js", + "index": 1, + "textRange": Object { + "endLine": 2, + "endOffset": 2, + "startLine": 1, + "startOffset": 1, + }, + }, ] } onIssueSelect={[MockFunction]} diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap index 08db4edb2b8..c56504a36a7 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap @@ -39,6 +39,7 @@ exports[`should render correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -81,6 +82,7 @@ exports[`should render correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.tsx index 7ef7e23b8d3..22c9233fa22 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.tsx +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.tsx @@ -24,7 +24,7 @@ import ConciseIssueComponent from './ConciseIssueComponent'; export interface ConciseIssueProps { issue: Issue; - onFlowSelect: (index: number) => void; + onFlowSelect: (index?: number) => void; onLocationSelect: (index: number) => void; onSelect: (issueKey: string) => void; previousIssue: Issue | undefined; diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx index 2480741ff10..936aa79e414 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx @@ -18,19 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; -import { uniq } from 'lodash'; import * as React from 'react'; import { ButtonPlain } from '../../../components/controls/buttons'; +import FlowsList from '../../../components/locations/FlowsList'; import LocationsList from '../../../components/locations/LocationsList'; import TypeHelper from '../../../components/shared/TypeHelper'; -import { Issue } from '../../../types/types'; +import { translateWithParameters } from '../../../helpers/l10n'; +import { FlowType, Issue } from '../../../types/types'; import { getLocations } from '../utils'; import ConciseIssueLocations from './ConciseIssueLocations'; interface Props { issue: Issue; onClick: (issueKey: string) => void; - onFlowSelect: (index: number) => void; + onFlowSelect: (index?: number) => void; onLocationSelect: (index: number) => void; scroll: (element: Element, bottomOffset?: number) => void; selected: boolean; @@ -38,12 +39,10 @@ interface Props { selectedLocationIndex: number | undefined; } -const MAX_LOCATIONS_SCROLL = 15; const SCROLL_TOP_OFFSET = 250; export default class ConciseIssueBox extends React.PureComponent<Props> { messageElement?: HTMLElement | null; - rootElement?: HTMLElement | null; componentDidMount() { if (this.props.selected) { @@ -62,19 +61,7 @@ export default class ConciseIssueBox extends React.PureComponent<Props> { }; handleScroll = () => { - const { selectedFlowIndex } = this.props; - const { flows, secondaryLocations } = this.props.issue; - - const locations = flows.length > 0 ? flows[selectedFlowIndex || 0] : secondaryLocations; - - if (!locations || locations.length < MAX_LOCATIONS_SCROLL) { - // if there are no locations, or there are just few - // then ensuse that the whole box is visible - if (this.rootElement) { - this.props.scroll(this.rootElement); - } - } else if (this.messageElement) { - // otherwise scroll until the the message element is located on top + if (this.messageElement) { this.props.scroll(this.messageElement, window.innerHeight - SCROLL_TOP_OFFSET); } }; @@ -84,13 +71,9 @@ export default class ConciseIssueBox extends React.PureComponent<Props> { const locations = getLocations(issue, selectedFlowIndex); - const locationComponents = [issue.component, ...locations.map(location => location.component)]; - const isCrossFile = uniq(locationComponents).length > 1; - return ( <div className={classNames('concise-issue-box', 'clearfix', { selected })} - ref={node => (this.rootElement = node)} onClick={selected ? undefined : this.handleClick}> <ButtonPlain className="concise-issue-box-message" @@ -101,21 +84,38 @@ export default class ConciseIssueBox extends React.PureComponent<Props> { </ButtonPlain> <div className="concise-issue-box-attributes"> <TypeHelper className="display-block little-spacer-right" type={issue.type} /> - <ConciseIssueLocations - issue={issue} - onFlowSelect={this.props.onFlowSelect} - selectedFlowIndex={selectedFlowIndex} - /> + {issue.flowsWithType.length > 0 ? ( + <span className="concise-issue-box-flow-indicator muted"> + {translateWithParameters( + 'issue.x_data_flows', + issue.flowsWithType.filter(f => f.type === FlowType.DATA).length + )} + </span> + ) : ( + <ConciseIssueLocations + issue={issue} + onFlowSelect={this.props.onFlowSelect} + selectedFlowIndex={selectedFlowIndex} + /> + )} </div> - {selected && ( - <LocationsList - locations={locations} - isCrossFile={isCrossFile} - onLocationSelect={this.props.onLocationSelect} - scroll={this.props.scroll} - selectedLocationIndex={selectedLocationIndex} - /> - )} + {selected && + (issue.flowsWithType.length > 0 ? ( + <FlowsList + flows={issue.flowsWithType} + onLocationSelect={this.props.onLocationSelect} + onFlowSelect={this.props.onFlowSelect} + selectedLocationIndex={selectedLocationIndex} + selectedFlowIndex={selectedFlowIndex} + /> + ) : ( + <LocationsList + locations={locations} + componentKey={issue.component} + onLocationSelect={this.props.onLocationSelect} + selectedLocationIndex={selectedLocationIndex} + /> + ))} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.tsx index 9869805e862..679f5bdc0ce 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.tsx +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.tsx @@ -25,21 +25,25 @@ import { formatMeasure } from '../../../helpers/measures'; interface Props { count: number; + flow?: boolean; onClick?: () => void; selected: boolean; } export default function ConciseIssueLocationBadge(props: Props) { + const { count, flow, selected } = props; return ( <Tooltip mouseEnterDelay={0.5} overlay={translateWithParameters( - 'issue.this_issue_involves_x_code_locations', - formatMeasure(props.count, 'INT') + flow + ? 'issue.this_flow_involves_x_code_locations' + : 'issue.this_issue_involves_x_code_locations', + formatMeasure(count, 'INT') )}> - <LocationIndex onClick={props.onClick} selected={props.selected}> + <LocationIndex onClick={props.onClick} selected={selected}> {'+'} - {props.count} + {count} </LocationIndex> </Tooltip> ); diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.tsx index a735dbe413a..9e372f2f59a 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.tsx +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.tsx @@ -24,7 +24,7 @@ import ConciseIssue from './ConciseIssue'; export interface ConciseIssuesListProps { issues: Issue[]; - onFlowSelect: (index: number) => void; + onFlowSelect: (index?: number) => void; onIssueSelect: (issueKey: string) => void; onLocationSelect: (index: number) => void; selected: string | undefined; diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.tsx index a46e6ecb53f..206217baf01 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.tsx @@ -30,6 +30,7 @@ const issue: Issue = { componentUuid: '', creationDate: '', flows: [], + flowsWithType: [], key: '', message: '', project: '', diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.tsx.snap index 6125edf1b87..9e47281b714 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.tsx.snap @@ -18,6 +18,7 @@ exports[`should render 1`] = ` "componentUuid": "", "creationDate": "", "flows": Array [], + "flowsWithType": Array [], "key": "", "message": "", "project": "", diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap index b7b98c491a5..bce2e4edbab 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap @@ -30,6 +30,7 @@ exports[`should render correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -137,6 +138,7 @@ exports[`should render correctly 2`] = ` }, ], ], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -182,7 +184,7 @@ exports[`should render correctly 2`] = ` /> </div> <LocationsList - isCrossFile={false} + componentKey="main.js" locations={ Array [ Object { @@ -203,19 +205,9 @@ exports[`should render correctly 2`] = ` "startOffset": 1, }, }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, ] } onLocationSelect={[MockFunction]} - scroll={[MockFunction]} selectedLocationIndex={0} /> </div> diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap index beeb3c28c2b..38a4c317e8d 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap @@ -96,6 +96,7 @@ exports[`should render correctly 2`] = ` }, ], ], + "flowsWithType": Array [], "key": "1", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -252,6 +253,7 @@ exports[`should render correctly: no component found 1`] = ` }, ], ], + "flowsWithType": Array [], "key": "unknown", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -379,6 +381,7 @@ exports[`should render correctly: no component found 1`] = ` }, ], ], + "flowsWithType": Array [], "key": "unknown", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css index 48e40f1d674..b544224e116 100644 --- a/server/sonar-web/src/main/js/apps/issues/styles.css +++ b/server/sonar-web/src/main/js/apps/issues/styles.css @@ -105,6 +105,10 @@ justify-content: flex-start; } +.concise-issue-box-attributes .concise-issue-box-flow-indicator { + margin-left: auto; +} + .concise-issue-box:not(.selected) .location-index { background-color: var(--secondFontColor); } diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts index d7266b3d8fe..c6b9719e9aa 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/utils.ts @@ -202,18 +202,23 @@ export const saveMyIssues = (myIssues: boolean) => save(ISSUES_DEFAULT, myIssues ? LOCALSTORAGE_MY : LOCALSTORAGE_ALL); export function getLocations( - { flows, secondaryLocations }: Pick<Issue, 'flows' | 'secondaryLocations'>, + { + flows, + secondaryLocations, + flowsWithType + }: Pick<Issue, 'flows' | 'secondaryLocations' | 'flowsWithType'>, selectedFlowIndex: number | undefined ) { - if (selectedFlowIndex !== undefined) { - return flows[selectedFlowIndex] || []; - } else { - return flows.length > 0 ? flows[0] : secondaryLocations; + if (secondaryLocations.length > 0) { + return secondaryLocations; + } else if (selectedFlowIndex !== undefined) { + return flows[selectedFlowIndex] || flowsWithType[selectedFlowIndex]?.locations || []; } + return []; } export function getSelectedLocation( - issue: Pick<Issue, 'flows' | 'secondaryLocations'>, + issue: Pick<Issue, 'flows' | 'secondaryLocations' | 'flowsWithType'>, selectedFlowIndex: number | undefined, selectedLocationIndex: number | undefined ) { @@ -230,7 +235,7 @@ export function getSelectedLocation( } export function allLocationsEmpty( - issue: Pick<Issue, 'flows' | 'secondaryLocations'>, + issue: Pick<Issue, 'flows' | 'secondaryLocations' | 'flowsWithType'>, selectedFlowIndex: number | undefined ) { return getLocations(issue, selectedFlowIndex).every(location => !location.msg); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx index bbae623ea3b..260afe05f41 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx @@ -28,7 +28,6 @@ import { Location, Router, withRouter } from '../../components/hoc/withRouter'; import { getLeakValue } from '../../components/measure/utils'; import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpers/branch-like'; import { KeyboardKeys } from '../../helpers/keycodes'; -import { scrollToElement } from '../../helpers/scrolling'; import { getStandards } from '../../helpers/security-standard'; import { BranchLike } from '../../types/branch-like'; import { SecurityStandard, Standards } from '../../types/security'; @@ -494,13 +493,6 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { } }; - handleScroll = (element: Element, bottomOffset = 100) => { - const scrollableElement = document.querySelector('.layout-page-side'); - if (element && scrollableElement) { - scrollToElement(element, { topOffset: 150, bottomOffset, parent: scrollableElement }); - } - }; - render() { const { branchLike, component } = this.props; const { @@ -544,7 +536,6 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { onSwitchStatusFilter={this.handleChangeStatusFilter} onUpdateHotspot={this.handleHotspotUpdate} onLocationClick={this.handleLocationClick} - onScroll={this.handleScroll} securityCategories={standards[SecurityStandard.SONARSOURCE]} selectedHotspot={selectedHotspot} selectedHotspotLocation={selectedHotspotLocationIndex} 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 0ebff755db0..5de54085163 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 @@ -61,7 +61,6 @@ export interface SecurityHotspotsAppRendererProps { onShowAllHotspots: () => void; onSwitchStatusFilter: (option: HotspotStatusFilter) => void; onUpdateHotspot: (hotspotKey: string) => Promise<void>; - onScroll: (element: Element) => void; selectedHotspot?: RawHotspot; selectedHotspotLocation?: number; securityCategories: StandardSecurityCategories; @@ -153,7 +152,6 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe onHotspotClick={props.onHotspotClick} onLoadMore={props.onLoadMore} onLocationClick={props.onLocationClick} - onScroll={props.onScroll} selectedHotspotLocation={selectedHotspotLocation} selectedHotspot={selectedHotspot} standards={standards} @@ -171,7 +169,6 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe selectedHotspot={selectedHotspot} selectedHotspotLocation={selectedHotspotLocation} statusFilter={filters.status} - onScroll={props.onScroll} /> )} </div> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx index 78f58e927e1..afa9a24a863 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx @@ -24,9 +24,7 @@ import { getSecurityHotspotList, getSecurityHotspots } from '../../../api/securi import { KeyboardKeys } from '../../../helpers/keycodes'; import { mockBranch, mockPullRequest } from '../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../helpers/mocks/component'; -import { mockHtmlElement } from '../../../helpers/mocks/dom'; import { mockRawHotspot, mockStandards } from '../../../helpers/mocks/security-hotspots'; -import { scrollToElement } from '../../../helpers/scrolling'; import { getStandards } from '../../../helpers/security-standard'; import { mockCurrentUser, @@ -414,20 +412,6 @@ it('should handle secondary location click', () => { expect(wrapper.instance().state.selectedHotspotLocationIndex).toBeUndefined(); }); -it('should handle scroll properly', async () => { - const fakeElement = document.createElement('div'); - jest.spyOn(document, 'querySelector').mockImplementationOnce(() => fakeElement); - const wrapper = shallowRender(); - const element = mockHtmlElement(); - wrapper.instance().handleScroll(element); - await waitAndUpdate(wrapper); - expect(scrollToElement).toBeCalledWith(element, { - bottomOffset: 100, - parent: fakeElement, - topOffset: 150 - }); -}); - describe('keyboard navigation', () => { const hotspots = [ mockRawHotspot({ key: 'k1' }), diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx index 224eabe8688..d0c82bf5a59 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx @@ -155,7 +155,6 @@ function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) { onSwitchStatusFilter={jest.fn()} onUpdateHotspot={jest.fn()} onLocationClick={jest.fn()} - onScroll={jest.fn()} securityCategories={{}} selectedHotspot={undefined} standards={mockStandards()} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap index 434ed274597..4b6a0206bfb 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap @@ -49,7 +49,6 @@ exports[`should render correctly 1`] = ` onHotspotClick={[Function]} onLoadMore={[Function]} onLocationClick={[Function]} - onScroll={[Function]} onShowAllHotspots={[Function]} onSwitchStatusFilter={[Function]} onUpdateHotspot={[Function]} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap index c9edd7eb43b..9a9fc621891 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap @@ -118,7 +118,6 @@ exports[`should render correctly when filtered by category or cwe: category 1`] onHotspotClick={[MockFunction]} onLoadMore={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selectedHotspot={ Object { "author": "Developer 1", @@ -260,7 +259,6 @@ exports[`should render correctly when filtered by category or cwe: cwe 1`] = ` onHotspotClick={[MockFunction]} onLoadMore={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selectedHotspot={ Object { "author": "Developer 1", @@ -462,7 +460,6 @@ exports[`should render correctly with hotspots 2`] = ` onHotspotClick={[MockFunction]} onLoadMore={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} securityCategories={Object {}} selectedHotspot={ Object { diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx index f6328c13298..8197e305607 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx @@ -32,7 +32,6 @@ export interface HotspotCategoryProps { onHotspotClick: (hotspot: RawHotspot) => void; onToggleExpand?: (categoryKey: string, value: boolean) => void; onLocationClick: (index: number) => void; - onScroll: (element: Element) => void; selectedHotspot: RawHotspot; selectedHotspotLocation?: number; title: string; @@ -92,7 +91,6 @@ export default function HotspotCategory(props: HotspotCategoryProps) { hotspot={h} onClick={props.onHotspotClick} onLocationClick={props.onLocationClick} - onScroll={props.onScroll} selectedHotspotLocation={selectedHotspotLocation} selected={h.key === selectedHotspot.key} /> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css index 1521875add6..f4a2082dfad 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css @@ -63,7 +63,7 @@ color: var(--baseFontColor); display: block; padding: var(--gridSize) calc(2 * var(--gridSize)); - border: 1px solid transparent; + border: 2px solid transparent; border-top-color: var(--barBorderColor); transition: padding 0s, border 0s; width: 100%; @@ -76,14 +76,17 @@ .hotspot-category .hotspot-item:hover { background-color: var(--veryLightBlue); - border: 1px dashed var(--blue); + border: 2px dashed var(--blue); color: var(--baseFontColor); } +.hotspot-category .hotspot-item.highlight:hover { + background-color: transparent; +} + .hotspot-category .hotspot-item.highlight { - background-color: var(--veryLightBlue); color: var(--baseFontColor); - border: 1px solid var(--blue); + border: 2px solid var(--blue); cursor: unset; } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx index 3520a85c673..a8d298b0d04 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx @@ -38,7 +38,6 @@ interface Props { onHotspotClick: (hotspot: RawHotspot) => void; onLoadMore: () => void; onLocationClick: (index?: number) => void; - onScroll: (element: Element) => void; securityCategories: StandardSecurityCategories; selectedHotspot: RawHotspot; selectedHotspotLocation?: number; @@ -162,7 +161,6 @@ export default class HotspotList extends React.Component<Props, State> { onHotspotClick={this.props.onHotspotClick} onToggleExpand={this.handleToggleCategory} onLocationClick={this.props.onLocationClick} - onScroll={this.props.onScroll} selectedHotspot={selectedHotspot} selectedHotspotLocation={selectedHotspotLocation} title={cat.title} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx index 23135c4f6af..81ed5460eda 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx @@ -30,7 +30,6 @@ export interface HotspotListItemProps { hotspot: RawHotspot; onClick: (hotspot: RawHotspot) => void; onLocationClick: (index?: number) => void; - onScroll: (element: Element) => void; selected: boolean; selectedHotspotLocation?: number; } @@ -63,10 +62,10 @@ export default function HotspotListItem(props: HotspotListItemProps) { {selected && ( <LocationsList locations={locations} - isCrossFile={false} // Currently we are not supporting cross file for security hotspot + showCrossFile={false} // To removed once we support multi file location + componentKey={hotspot.component} onLocationSelect={props.onLocationClick} selectedLocationIndex={selectedHotspotLocation} - scroll={props.onScroll} /> )} </ButtonPlain> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx index 8f012882eb5..98128701214 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx @@ -43,7 +43,6 @@ export interface HotspotSimpleListProps { loadingMore: boolean; onHotspotClick: (hotspot: RawHotspot) => void; onLocationClick: (index?: number) => void; - onScroll: (element: Element) => void; onLoadMore: () => void; selectedHotspot: RawHotspot; selectedHotspotLocation?: number; @@ -115,7 +114,6 @@ export default class HotspotSimpleList extends React.Component<HotspotSimpleList hotspot={h} onClick={this.props.onHotspotClick} onLocationClick={this.props.onLocationClick} - onScroll={this.props.onScroll} selected={h.key === selectedHotspot.key} selectedHotspotLocation={selectedHotspotLocation} /> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx index 55f7f5c6dc0..293b318af5b 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx @@ -76,7 +76,6 @@ function shallowRender(props: Partial<HotspotCategoryProps> = {}) { title="Class Injection" isLastAndIncomplete={false} onLocationClick={jest.fn()} - onScroll={jest.fn()} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx index 574d0d4df6c..2876fa8dfa9 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx @@ -118,7 +118,6 @@ function shallowRender(props: Partial<HotspotList['props']> = {}) { onHotspotClick={jest.fn()} onLoadMore={jest.fn()} onLocationClick={jest.fn()} - onScroll={jest.fn()} securityCategories={{}} selectedHotspot={mockRawHotspot({ key: 'h2' })} statusFilter={HotspotStatusFilter.TO_REVIEW} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx index 99cfe471fbd..96d2c23adf2 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx @@ -52,7 +52,6 @@ function shallowRender(props: Partial<HotspotListItemProps> = {}) { <HotspotListItem hotspot={mockRawHotspot()} onClick={jest.fn()} - onScroll={jest.fn()} onLocationClick={jest.fn} selected={false} {...props} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx index 290851dab54..9a660270786 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx @@ -65,7 +65,6 @@ function shallowRender(props: Partial<HotspotSimpleListProps> = {}) { onHotspotClick={jest.fn()} onLoadMore={jest.fn()} onLocationClick={jest.fn()} - onScroll={jest.fn()} selectedHotspot={hotspots[0]} standards={{ cwe: { 327: { title: 'Use of a Broken or Risky Cryptographic Algorithm' } }, diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap index f165b400e5c..1b3be4b5acf 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap @@ -50,7 +50,6 @@ exports[`should render correctly with hotspots 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={false} /> </li> @@ -78,7 +77,6 @@ exports[`should render correctly with hotspots 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={false} /> </li> @@ -164,7 +162,6 @@ exports[`should render correctly with hotspots: contains selected 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={true} /> </li> @@ -192,7 +189,6 @@ exports[`should render correctly with hotspots: contains selected 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={false} /> </li> @@ -251,7 +247,6 @@ exports[`should render correctly with hotspots: lastAndIncomplete 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={false} /> </li> @@ -279,7 +274,6 @@ exports[`should render correctly with hotspots: lastAndIncomplete 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={false} /> </li> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap index f526a63a918..99ff86d6edf 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap @@ -132,7 +132,6 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` isLastAndIncomplete={false} onHotspotClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} onToggleExpand={[Function]} selectedHotspot={ Object { @@ -182,7 +181,6 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` isLastAndIncomplete={false} onHotspotClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} onToggleExpand={[Function]} selectedHotspot={ Object { @@ -267,7 +265,6 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` isLastAndIncomplete={false} onHotspotClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} onToggleExpand={[Function]} selectedHotspot={ Object { @@ -317,7 +314,6 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` isLastAndIncomplete={false} onHotspotClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} onToggleExpand={[Function]} selectedHotspot={ Object { @@ -412,7 +408,6 @@ exports[`should render correctly with hotspots: pagination 1`] = ` isLastAndIncomplete={false} onHotspotClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} onToggleExpand={[Function]} selectedHotspot={ Object { @@ -462,7 +457,6 @@ exports[`should render correctly with hotspots: pagination 1`] = ` isLastAndIncomplete={false} onHotspotClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} onToggleExpand={[Function]} selectedHotspot={ Object { @@ -547,7 +541,6 @@ exports[`should render correctly with hotspots: pagination 1`] = ` isLastAndIncomplete={false} onHotspotClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} onToggleExpand={[Function]} selectedHotspot={ Object { @@ -597,7 +590,6 @@ exports[`should render correctly with hotspots: pagination 1`] = ` isLastAndIncomplete={true} onHotspotClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} onToggleExpand={[Function]} selectedHotspot={ Object { diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap index b9a3f92bbbf..f9e251cc5ee 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap @@ -57,10 +57,10 @@ exports[`should render correctly 2`] = ` </div> </div> <LocationsList - isCrossFile={false} + componentKey="com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest" locations={Array []} onLocationSelect={[Function]} - scroll={[MockFunction]} + showCrossFile={false} /> </ButtonPlain> `; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap index b7e685bd3f0..1c547037892 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap @@ -54,7 +54,6 @@ exports[`should render correctly: filter by both 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={true} /> </li> @@ -82,7 +81,6 @@ exports[`should render correctly: filter by both 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={false} /> </li> @@ -150,7 +148,6 @@ exports[`should render correctly: filter by category 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={true} /> </li> @@ -178,7 +175,6 @@ exports[`should render correctly: filter by category 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={false} /> </li> @@ -246,7 +242,6 @@ exports[`should render correctly: filter by cwe 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={true} /> </li> @@ -274,7 +269,6 @@ exports[`should render correctly: filter by cwe 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={false} /> </li> @@ -352,7 +346,6 @@ exports[`should render correctly: filter by file 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={true} /> </li> @@ -380,7 +373,6 @@ exports[`should render correctly: filter by file 1`] = ` } onClick={[MockFunction]} onLocationClick={[MockFunction]} - onScroll={[MockFunction]} selected={false} /> </li> diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx index fe5581aae58..db4ffc207b2 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx @@ -69,7 +69,6 @@ export default function HealthCard({ {health && ( <HealthItem biggerHealth={biggerHealth} - className="pull-right" health={health} healthCauses={healthCauses} name={name} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx index d495d911cd2..679e4315950 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx @@ -23,7 +23,8 @@ import { createWebhook, deleteWebhook, searchWebhooks, updateWebhook } from '../ import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { translate } from '../../../helpers/l10n'; -import { Component, Webhook } from '../../../types/types'; +import { Component } from '../../../types/types'; +import { Webhook } from '../../../types/webhook'; import PageActions from './PageActions'; import PageHeader from './PageHeader'; import WebhooksList from './WebhooksList'; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx index c82b916686a..52dc7938607 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx @@ -24,7 +24,7 @@ import ValidationModal from '../../../components/controls/ValidationModal'; import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/types'; +import { Webhook } from '../../../types/webhook'; interface Props { onClose: () => void; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx index 0d8b7f96a8c..155076facd1 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx @@ -22,7 +22,7 @@ import { ResetButtonLink, SubmitButton } from '../../../components/controls/butt import SimpleModal from '../../../components/controls/SimpleModal'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/types'; +import { Webhook } from '../../../types/webhook'; interface Props { onClose: () => void; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx index e1dc74b309d..3851b830630 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx @@ -24,7 +24,8 @@ import ListFooter from '../../../components/controls/ListFooter'; import Modal from '../../../components/controls/Modal'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { Paging, Webhook, WebhookDelivery } from '../../../types/types'; +import { Paging } from '../../../types/types'; +import { Webhook, WebhookDelivery } from '../../../types/webhook'; import DeliveryAccordion from './DeliveryAccordion'; interface Props { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx index 2efed7808d1..9fa865fe7da 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx @@ -23,7 +23,8 @@ import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordio import AlertErrorIcon from '../../../components/icons/AlertErrorIcon'; import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; -import { WebhookDelivery } from '../../../types/types'; +import { translate } from '../../../helpers/l10n'; +import { WebhookDelivery } from '../../../types/webhook'; import DeliveryItem from './DeliveryItem'; interface Props { @@ -89,9 +90,9 @@ export default class DeliveryAccordion extends React.PureComponent<Props, State> open={open} renderHeader={() => delivery.success ? ( - <AlertSuccessIcon className="pull-right js-success" /> + <AlertSuccessIcon aria-label={translate('success')} className="js-success" /> ) : ( - <AlertErrorIcon className="pull-right js-error" /> + <AlertErrorIcon aria-label={translate('error')} className="js-error" /> ) } title={<DateTimeFormatter date={delivery.at} />}> diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx index cc37ab37e0e..fa6405dc905 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx @@ -22,7 +22,7 @@ import CodeSnippet from '../../../components/common/CodeSnippet'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; -import { WebhookDelivery } from '../../../types/types'; +import { WebhookDelivery } from '../../../types/webhook'; interface Props { className?: string; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx index c2ff1d6ab17..e23479b2785 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx @@ -22,7 +22,7 @@ import { getDelivery } from '../../../api/webhooks'; import { ResetButtonLink } from '../../../components/controls/buttons'; import Modal from '../../../components/controls/Modal'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { Webhook, WebhookDelivery } from '../../../types/types'; +import { Webhook, WebhookDelivery } from '../../../types/webhook'; import DeliveryItem from './DeliveryItem'; interface Props { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx index 506961aaa2d..246b31fb0ff 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx @@ -23,7 +23,7 @@ import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/types'; +import { Webhook } from '../../../types/webhook'; import CreateWebhookForm from './CreateWebhookForm'; import DeleteWebhookForm from './DeleteWebhookForm'; import DeliveriesForm from './DeliveriesForm'; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx index 89923988540..53790fd4dff 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/types'; +import { Webhook } from '../../../types/webhook'; import WebhookActions from './WebhookActions'; import WebhookItemLatestDelivery from './WebhookItemLatestDelivery'; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx index 63c28bbc643..44a608c9fc3 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx @@ -24,7 +24,7 @@ import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import BulletListIcon from '../../../components/icons/BulletListIcon'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/types'; +import { Webhook } from '../../../types/webhook'; import LatestDeliveryForm from './LatestDeliveryForm'; interface Props { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx index f8605363746..4ef486ec09a 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx @@ -20,7 +20,7 @@ import { sortBy } from 'lodash'; import * as React from 'react'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/types'; +import { Webhook } from '../../../types/webhook'; import WebhookItem from './WebhookItem'; interface Props { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryAccordion-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryAccordion-test.tsx index 394794236dd..d9377f7974c 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryAccordion-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryAccordion-test.tsx @@ -17,42 +17,56 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { getDelivery } from '../../../../api/webhooks'; +import { mockWebhookDelivery } from '../../../../helpers/mocks/webhook'; +import { HttpStatus } from '../../../../helpers/request'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import DeliveryAccordion from '../DeliveryAccordion'; jest.mock('../../../../api/webhooks', () => ({ - getDelivery: jest.fn(() => - Promise.resolve({ - delivery: { payload: '{ "success": true }' } - }) - ) + getDelivery: jest.fn().mockResolvedValue({ + delivery: { payload: '{ "message": "This was successful" }' } + }) })); -const delivery = { - at: '12.02.2018', - durationMs: 20, - httpStatus: 200, - id: '2', - success: true -}; +beforeEach(jest.clearAllMocks); -beforeEach(() => { - (getDelivery as jest.Mock<any>).mockClear(); +it('should render correctly for successful payloads', async () => { + const user = userEvent.setup(); + renderDeliveryAccordion(); + expect(screen.getByLabelText('success')).toBeInTheDocument(); + + await user.click(screen.getByRole('button')); + expect(screen.getByText(`webhooks.delivery.response_x.${HttpStatus.Ok}`)).toBeInTheDocument(); + expect(screen.getByText('webhooks.delivery.duration_x.20ms')).toBeInTheDocument(); + expect(screen.getByText('webhooks.delivery.payload')).toBeInTheDocument(); + + const codeSnippet = await screen.findByText('{ "message": "This was successful" }'); + expect(codeSnippet).toBeInTheDocument(); }); -it('should render correctly', async () => { - const wrapper = getWrapper(); - expect(wrapper).toMatchSnapshot(); +it('should render correctly for errored payloads', async () => { + const user = userEvent.setup(); + (getDelivery as jest.Mock).mockResolvedValueOnce({ + delivery: { payload: '503 Service Unavailable' } + }); + renderDeliveryAccordion({ + delivery: mockWebhookDelivery({ httpStatus: undefined, success: false }) + }); + expect(screen.getByLabelText('error')).toBeInTheDocument(); + + await user.click(screen.getByRole('button')); + expect( + screen.getByText('webhooks.delivery.response_x.webhooks.delivery.server_unreachable') + ).toBeInTheDocument(); - wrapper.find('BoxedGroupAccordion').prop<Function>('onClick')(); - await new Promise(setImmediate); - expect(getDelivery).lastCalledWith({ deliveryId: delivery.id }); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + const codeSnippet = await screen.findByText('503 Service Unavailable'); + expect(codeSnippet).toBeInTheDocument(); }); -function getWrapper(props = {}) { - return shallow(<DeliveryAccordion delivery={delivery} {...props} />); +function renderDeliveryAccordion(props: Partial<DeliveryAccordion['props']> = {}) { + return renderComponent(<DeliveryAccordion delivery={mockWebhookDelivery()} {...props} />); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryAccordion-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryAccordion-test.tsx.snap deleted file mode 100644 index 75c25d02d2c..00000000000 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryAccordion-test.tsx.snap +++ /dev/null @@ -1,56 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<BoxedGroupAccordion - onClick={[Function]} - open={false} - renderHeader={[Function]} - title={ - <DateTimeFormatter - date="12.02.2018" - /> - } -> - <DeliveryItem - className="big-spacer-left" - delivery={ - Object { - "at": "12.02.2018", - "durationMs": 20, - "httpStatus": 200, - "id": "2", - "success": true, - } - } - loading={false} - /> -</BoxedGroupAccordion> -`; - -exports[`should render correctly 2`] = ` -<BoxedGroupAccordion - onClick={[Function]} - open={true} - renderHeader={[Function]} - title={ - <DateTimeFormatter - date="12.02.2018" - /> - } -> - <DeliveryItem - className="big-spacer-left" - delivery={ - Object { - "at": "12.02.2018", - "durationMs": 20, - "httpStatus": 200, - "id": "2", - "success": true, - } - } - loading={false} - payload="{ \\"success\\": true }" - /> -</BoxedGroupAccordion> -`; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap deleted file mode 100644 index d645fae5197..00000000000 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap +++ /dev/null @@ -1,83 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<div> - <p - className="spacer-bottom" - > - webhooks.delivery.response_x.200 - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.duration_x.20ms - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.payload - </p> - <DeferredSpinner - className="spacer-left spacer-top" - loading={false} - > - <CodeSnippet - noCopy={true} - snippet="{ status: \\"SUCCESS\\" }" - /> - </DeferredSpinner> -</div> -`; - -exports[`should render correctly when no http status 1`] = ` -<div> - <p - className="spacer-bottom" - > - webhooks.delivery.response_x.webhooks.delivery.server_unreachable - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.duration_x.20ms - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.payload - </p> - <DeferredSpinner - className="spacer-left spacer-top" - loading={false} - > - <CodeSnippet - noCopy={true} - snippet="{ status: \\"SUCCESS\\" }" - /> - </DeferredSpinner> -</div> -`; - -exports[`should render correctly when no payload 1`] = ` -<div> - <p - className="spacer-bottom" - > - webhooks.delivery.response_x.200 - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.duration_x.20ms - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.payload - </p> - <DeferredSpinner - className="spacer-left spacer-top" - loading={true} - /> -</div> -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap index 64cd5ee5bbe..a9b7f2b3143 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap @@ -83,6 +83,7 @@ exports[`should render correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -116,6 +117,7 @@ exports[`should render correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx index 713c0ea288b..3604fee6804 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx @@ -86,6 +86,7 @@ export default class LineCode extends React.PureComponent<React.PropsWithChildre leading={leading} onClick={onClick} selected={selected} + aria-current={selected ? 'location' : false} aria-label={message ? `${index + 1}-${message}` : index + 1}> <IssueSourceViewerScrollContext.Consumer> {ctx => ( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap index 2dcef7709c8..308cb3b368f 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap @@ -112,6 +112,7 @@ exports[`render code: with secondary location 1`] = ` placement="top" > <LocationIndex + aria-current={false} aria-label="2-secondary-location-msg" leading={false} onClick={[Function]} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssueList-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssueList-test.tsx.snap index 358eecbf76e..b86ed52063c 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssueList-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssueList-test.tsx.snap @@ -23,6 +23,7 @@ exports[`should render issues 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "issue", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap index 24e1b6a9eb1..08768bed950 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap @@ -24,6 +24,7 @@ Array [ "componentQualifier": "FIL", "creationDate": "2016-08-15T15:25:38+0200", "flows": Array [], + "flowsWithType": Array [], "hash": "78417dcee7ba927b7e7c9161e29e02b8", "key": "AWaqVGl3tut9VbnJvk6M", "line": 62, diff --git a/server/sonar-web/src/main/js/components/common/LocationIndex.css b/server/sonar-web/src/main/js/components/common/LocationIndex.css index 6fcb019567b..0b05a89d85e 100644 --- a/server/sonar-web/src/main/js/components/common/LocationIndex.css +++ b/server/sonar-web/src/main/js/components/common/LocationIndex.css @@ -33,6 +33,7 @@ user-select: none; } +.selected > .location-index, .location-index.selected { background-color: var(--conciseIssueRedSelected); } diff --git a/server/sonar-web/src/main/js/components/common/LocationMessage.css b/server/sonar-web/src/main/js/components/common/LocationMessage.css index 24359d9a257..a9269cd242a 100644 --- a/server/sonar-web/src/main/js/components/common/LocationMessage.css +++ b/server/sonar-web/src/main/js/components/common/LocationMessage.css @@ -22,47 +22,15 @@ vertical-align: top; line-height: 16px; padding: 0 6px; - border-radius: 2px; - background-color: #9e9e9e; - color: #fff; - font-family: var(--baseFontFamily); font-size: var(--smallFontSize); text-overflow: ellipsis; overflow: hidden; - transition: background-color 0.3s ease; -} - -.location-message.selected { - background-color: #475760; } .location-index + .location-message { margin-left: 4px; } -.location-index > .location-message { - display: none; - position: absolute; - bottom: calc(100% + 4px); - left: 0; -} - -.location-index:hover > .location-message { - display: block; -} - -.location-index > .location-message::after { - position: absolute; - bottom: -5px; - left: 4px; - width: 0; - height: 0; - border-top: 5px solid #475760; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - content: ''; -} - .source-line-code .location-message { padding-top: 2px; padding-bottom: 2px; diff --git a/server/sonar-web/src/main/js/components/common/LocationMessage.tsx b/server/sonar-web/src/main/js/components/common/LocationMessage.tsx index d38e7f34d81..8681baa93e5 100644 --- a/server/sonar-web/src/main/js/components/common/LocationMessage.tsx +++ b/server/sonar-web/src/main/js/components/common/LocationMessage.tsx @@ -17,19 +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 classNames from 'classnames'; import * as React from 'react'; import './LocationMessage.css'; interface Props { children?: React.ReactNode; - selected: boolean; } export default function LocationMessage(props: Props) { - return ( - <div className={classNames('location-message', { selected: props.selected })}> - {props.children} - </div> - ); + return <div className="location-message">{props.children}</div>; } diff --git a/server/sonar-web/src/main/js/components/controls/BoxedGroupAccordion.tsx b/server/sonar-web/src/main/js/components/controls/BoxedGroupAccordion.tsx index 6ce64cf530a..a7056898971 100644 --- a/server/sonar-web/src/main/js/components/controls/BoxedGroupAccordion.tsx +++ b/server/sonar-web/src/main/js/components/controls/BoxedGroupAccordion.tsx @@ -18,11 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { uniqueId } from 'lodash'; import * as React from 'react'; import OpenCloseIcon from '../icons/OpenCloseIcon'; +import { ButtonPlain } from './buttons'; -interface Props { +interface BoxedGroupAccordionProps { children: React.ReactNode; + noBorder?: boolean; className?: string; data?: string; onClick: (data?: string) => void; @@ -31,48 +34,38 @@ interface Props { title: React.ReactNode; } -interface State { - hoveringInner: boolean; -} - -export default class BoxedGroupAccordion extends React.PureComponent<Props, State> { - state: State = { hoveringInner: false }; - - handleClick = () => { - this.props.onClick(this.props.data); - }; +export default function BoxedGroupAccordion(props: BoxedGroupAccordionProps) { + const { className, noBorder, open, renderHeader, title, data, onClick } = props; - onDetailEnter = () => { - this.setState({ hoveringInner: true }); - }; + const id = React.useMemo(() => uniqueId('accordion-'), []); + const handleClick = React.useCallback(() => { + onClick(data); + }, [onClick, data]); - onDetailLeave = () => { - this.setState({ hoveringInner: false }); - }; - - render() { - const { className, open, renderHeader, title } = this.props; - return ( - <div - className={classNames('boxed-group boxed-group-accordion', className, { - 'no-hover': this.state.hoveringInner - })}> - <div className="boxed-group-header" onClick={this.handleClick} role="listitem"> - <span className="boxed-group-accordion-title"> - <OpenCloseIcon className="little-spacer-right" open={open} /> - {title} - </span> - {renderHeader && renderHeader()} - </div> - {open && ( - <div - className="boxed-group-inner" - onMouseEnter={this.onDetailEnter} - onMouseLeave={this.onDetailLeave}> - {this.props.children} - </div> - )} + return ( + <div + className={classNames('boxed-group boxed-group-accordion', className, { + 'no-border': noBorder, + open + })} + role="listitem"> + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} + <div onClick={handleClick} className="display-flex-center boxed-group-header"> + <ButtonPlain + stopPropagation={true} + className="boxed-group-accordion-title flex-grow" + onClick={handleClick} + id={`${id}-header`} + aria-controls={`${id}-panel`} + aria-expanded={open}> + {title} + </ButtonPlain> + {renderHeader && renderHeader()} + <OpenCloseIcon aria-hidden={true} className="spacer-left" open={open} /> + </div> + <div id={`${id}-panel`} aria-labelledby={`${id}-header`} role="region"> + {open && <div className="boxed-group-inner">{props.children}</div>} </div> - ); - } + </div> + ); } diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/BoxedGroupAccordion-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/BoxedGroupAccordion-test.tsx index 9e1a386e1b7..c1569132cbe 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/BoxedGroupAccordion-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/BoxedGroupAccordion-test.tsx @@ -17,36 +17,39 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { click } from '../../../helpers/testUtils'; +import { renderComponent } from '../../../helpers/testReactTestingUtils'; import BoxedGroupAccordion from '../BoxedGroupAccordion'; -it('should render correctly', () => { - expect(getWrapper()).toMatchSnapshot(); +it('should behave correctly', async () => { + const user = userEvent.setup(); + renderDeliveryAccordion(); + expect(screen.queryByText('children')).not.toBeInTheDocument(); + await user.click(screen.getByRole('button', { expanded: false })); + expect(screen.queryByText('children')).toBeInTheDocument(); }); -it('should show the inner content after a click', () => { - const onClick = jest.fn(); - const wrapper = getWrapper({ onClick }); - click(wrapper.find('.boxed-group-header')); +it('should render header correctly', () => { + renderDeliveryAccordion(() => <div>header</div>); + expect(screen.getByText('header')).toBeInTheDocument(); +}); - expect(onClick).lastCalledWith('foo'); - wrapper.setProps({ open: true }); +function renderDeliveryAccordion(renderHeader?: () => React.ReactNode) { + function AccordionTest() { + const [open, setOpen] = React.useState(false); - expect(wrapper.find('.boxed-group-inner').exists()).toBe(true); -}); + return ( + <BoxedGroupAccordion + onClick={() => setOpen(!open)} + open={open} + title="test" + renderHeader={renderHeader}> + <div>children</div> + </BoxedGroupAccordion> + ); + } -function getWrapper(props = {}) { - return shallow( - <BoxedGroupAccordion - data="foo" - onClick={() => {}} - open={false} - renderHeader={() => <div>header content</div>} - title="Foo" - {...props}> - <div>inner content</div> - </BoxedGroupAccordion> - ); + return renderComponent(<AccordionTest />); } diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap deleted file mode 100644 index 0c7b74b79d9..00000000000 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<div - className="boxed-group boxed-group-accordion" -> - <div - className="boxed-group-header" - onClick={[Function]} - role="listitem" - > - <span - className="boxed-group-accordion-title" - > - <OpenCloseIcon - className="little-spacer-right" - open={false} - /> - Foo - </span> - <div> - header content - </div> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/components/controls/buttons.css b/server/sonar-web/src/main/js/components/controls/buttons.css index e0b88676247..aa468fb4648 100644 --- a/server/sonar-web/src/main/js/components/controls/buttons.css +++ b/server/sonar-web/src/main/js/components/controls/buttons.css @@ -103,23 +103,12 @@ /* #region .button-plain */ .button-plain { - display: inline-flex; - height: auto; - line-height: inherit; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - background: transparent; + box-sizing: border-box; + background: inherit; color: inherit; - border-bottom: 0; - font-weight: inherit; - font-size: inherit; -} - -.button-plain:hover { - color: var(--blue); - background-color: transparent; + cursor: pointer; + outline: none; + border: 0; } /* #endregion */ diff --git a/server/sonar-web/src/main/js/components/controls/buttons.tsx b/server/sonar-web/src/main/js/components/controls/buttons.tsx index e4cae47e83a..ad0d068724e 100644 --- a/server/sonar-web/src/main/js/components/controls/buttons.tsx +++ b/server/sonar-web/src/main/js/components/controls/buttons.tsx @@ -48,7 +48,6 @@ export class Button extends React.PureComponent<ButtonProps> { handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const { disabled, onClick, preventDefault = true, stopPropagation = false } = this.props; - event.currentTarget.blur(); if (preventDefault || disabled) { event.preventDefault(); } @@ -72,12 +71,15 @@ export class Button extends React.PureComponent<ButtonProps> { type = 'button', ...props } = this.props; + + // Instead of undoing button style we simply not apply the class. + const isPlain = className && className.indexOf('button-plain') !== -1; return ( <button {...props} aria-disabled={disabled} disabled={disabled} - className={classNames('button', className, { disabled })} + className={classNames(isPlain ? '' : 'button', className, { disabled })} id={this.props.id} onClick={this.handleClick} ref={this.props.innerRef} diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap index c0843b958a6..35f36ffc66e 100644 --- a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap @@ -17,6 +17,7 @@ exports[`should render hotspots correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -51,6 +52,7 @@ exports[`should render hotspots correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -110,6 +112,7 @@ exports[`should render issues correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -158,6 +161,7 @@ exports[`should render issues correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/issue-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/issue-test.tsx.snap index 383f9ff13dc..3b2f7d3baba 100644 --- a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/issue-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/issue-test.tsx.snap @@ -27,6 +27,7 @@ exports[`should render issues correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx index 2ada6ced27e..75fc9e1b3bc 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx @@ -50,7 +50,8 @@ export default function IssueTitleBar(props: IssueTitleBarProps) { const locationsCount = issue.secondaryLocations.length + - issue.flows.reduce((sum, locations) => sum + locations.length, 0); + issue.flows.reduce((sum, locations) => sum + locations.length, 0) + + issue.flowsWithType.reduce((sum, { locations }) => sum + locations.length, 0); const locationsBadge = ( <Tooltip diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap index 958f8e72598..03560f28c24 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap @@ -24,6 +24,7 @@ exports[`should render commentable correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -66,6 +67,7 @@ exports[`should render commentable correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -108,6 +110,7 @@ exports[`should render commentable correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -150,6 +153,7 @@ exports[`should render commentable correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -204,6 +208,7 @@ exports[`should render commentable correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -256,6 +261,7 @@ exports[`should render effort correctly 1`] = ` "creationDate": "2017-03-01T09:36:01+0100", "effort": "great", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -297,6 +303,7 @@ exports[`should render effort correctly 1`] = ` "creationDate": "2017-03-01T09:36:01+0100", "effort": "great", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -338,6 +345,7 @@ exports[`should render effort correctly 1`] = ` "creationDate": "2017-03-01T09:36:01+0100", "effort": "great", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -379,6 +387,7 @@ exports[`should render effort correctly 1`] = ` "creationDate": "2017-03-01T09:36:01+0100", "effort": "great", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -433,6 +442,7 @@ exports[`should render effort correctly 1`] = ` "creationDate": "2017-03-01T09:36:01+0100", "effort": "great", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -484,6 +494,7 @@ exports[`should render issue correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -524,6 +535,7 @@ exports[`should render issue correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -564,6 +576,7 @@ exports[`should render issue correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -604,6 +617,7 @@ exports[`should render issue correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -648,6 +662,7 @@ exports[`should render issue correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -699,6 +714,7 @@ exports[`should render security hotspot correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -739,6 +755,7 @@ exports[`should render security hotspot correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -779,6 +796,7 @@ exports[`should render security hotspot correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -823,6 +841,7 @@ exports[`should render security hotspot correctly 1`] = ` "componentUuid": "foo1234", "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap index 8692df798b0..4cf04e124e1 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap @@ -31,6 +31,7 @@ exports[`should render correctly: default 1`] = ` "creationDate": "2017-03-01T09:36:01+0100", "externalRuleEngine": "foo", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -120,6 +121,7 @@ exports[`should render correctly: with filter 1`] = ` "creationDate": "2017-03-01T09:36:01+0100", "externalRuleEngine": "foo", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -188,6 +190,7 @@ exports[`should render correctly: with filter 1`] = ` "creationDate": "2017-03-01T09:36:01+0100", "externalRuleEngine": "foo", "flows": Array [], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -297,6 +300,7 @@ exports[`should render correctly: with multi locations 1`] = ` }, ], ], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", @@ -464,6 +468,7 @@ exports[`should render correctly: with multi locations and link 1`] = ` }, ], ], + "flowsWithType": Array [], "key": "AVsae-CQS-9G3txfbFN2", "line": 25, "message": "Reduce the number of conditional operators (4) used in the expression", diff --git a/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx b/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx index 8254d91ef50..a724789cd44 100644 --- a/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx +++ b/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx @@ -27,7 +27,6 @@ import SingleFileLocationNavigator from './SingleFileLocationNavigator'; interface Props { locations: FlowLocation[]; onLocationSelect: (index: number) => void; - scroll: (element: Element) => void; selectedLocationIndex: number | undefined; } @@ -113,7 +112,6 @@ export default class CrossFileLocationNavigator extends React.PureComponent<Prop key={index} message={message} onClick={this.props.onLocationSelect} - scroll={this.props.scroll} selected={index === this.props.selectedLocationIndex} /> ); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx b/server/sonar-web/src/main/js/components/locations/FlowsList.css index 72317c29b71..0f348d33513 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx +++ b/server/sonar-web/src/main/js/components/locations/FlowsList.css @@ -17,38 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import DeliveryItem from '../DeliveryItem'; - -const delivery = { - at: '12.02.2018', - durationMs: 20, - httpStatus: 200, - id: '2', - success: true -}; - -it('should render correctly', () => { - const wrapper = getWrapper(); - expect(wrapper).toMatchSnapshot(); -}); +.issue-flows .boxed-group-header { + padding: calc(1.5 * var(--gridSize)); +} -it('should render correctly when no payload', () => { - expect(getWrapper({ loading: true, payload: undefined })).toMatchSnapshot(); -}); +.issue-flows .boxed-group-inner { + padding: 0 var(--gridSize) var(--gridSize); +} -it('should render correctly when no http status', () => { - expect(getWrapper({ delivery: { ...delivery, httpStatus: undefined } })).toMatchSnapshot(); -}); +.issue-flows .boxed-group-header .location-index { + background-color: var(--neutral600); +} -function getWrapper(props = {}) { - return shallow( - <DeliveryItem - delivery={delivery} - loading={false} - payload={'{ status: "SUCCESS" }'} - {...props} - /> - ); +.issue-flows .boxed-group-header .location-index.selected { + background-color: var(--conciseIssueRedSelected); } diff --git a/server/sonar-web/src/main/js/components/locations/FlowsList.tsx b/server/sonar-web/src/main/js/components/locations/FlowsList.tsx new file mode 100644 index 00000000000..6e2600da40d --- /dev/null +++ b/server/sonar-web/src/main/js/components/locations/FlowsList.tsx @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 'FlowsList.css'; +import * as React from 'react'; +import ConciseIssueLocationBadge from '../../apps/issues/conciseIssuesList/ConciseIssueLocationBadge'; +import { translate } from '../../helpers/l10n'; +import { Flow, FlowType } from '../../types/types'; +import BoxedGroupAccordion from '../controls/BoxedGroupAccordion'; +import SingleFileLocationNavigator from './SingleFileLocationNavigator'; + +export interface Props { + flows: Flow[]; + selectedLocationIndex?: number; + selectedFlowIndex?: number; + onFlowSelect: (index?: number) => void; + onLocationSelect: (index: number) => void; +} + +export default function FlowsList(props: Props) { + const { flows, selectedLocationIndex, selectedFlowIndex } = props; + + return ( + <div className="issue-flows little-padded-top" role="list"> + {flows.map((flow, index) => { + const open = selectedFlowIndex === index; + return ( + <BoxedGroupAccordion + className="spacer-top" + // eslint-disable-next-line react/no-array-index-key + key={index} + onClick={() => props.onFlowSelect(open ? undefined : index)} + open={open} + noBorder={flow.type === FlowType.EXECUTION} + title={ + flow.type === FlowType.EXECUTION + ? translate('issue.execution_flow') + : flow.description + } + renderHeader={() => ( + <ConciseIssueLocationBadge + count={flow.locations.length} + flow={true} + selected={open} + /> + )}> + <ul> + {flow.locations.map((location, locIndex) => ( + // eslint-disable-next-line react/no-array-index-key + <li className="display-flex-column" key={locIndex}> + <SingleFileLocationNavigator + index={locIndex} + message={location.msg} + onClick={props.onLocationSelect} + selected={locIndex === selectedLocationIndex} + /> + </li> + ))} + </ul> + </BoxedGroupAccordion> + ); + })} + </div> + ); +} diff --git a/server/sonar-web/src/main/js/components/locations/LocationsList.tsx b/server/sonar-web/src/main/js/components/locations/LocationsList.tsx index 65e3c6ebdf2..043de9d030a 100644 --- a/server/sonar-web/src/main/js/components/locations/LocationsList.tsx +++ b/server/sonar-web/src/main/js/components/locations/LocationsList.tsx @@ -17,51 +17,54 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { uniq } from 'lodash'; import * as React from 'react'; import { FlowLocation } from '../../types/types'; import CrossFileLocationNavigator from './CrossFileLocationNavigator'; import SingleFileLocationNavigator from './SingleFileLocationNavigator'; interface Props { - isCrossFile: boolean; + componentKey: string; locations: FlowLocation[]; onLocationSelect: (index: number) => void; - scroll: (element: Element) => void; selectedLocationIndex?: number; + showCrossFile?: boolean; } export default class LocationsList extends React.PureComponent<Props> { render() { - const { isCrossFile, locations, selectedLocationIndex } = this.props; + const { locations, componentKey, selectedLocationIndex, showCrossFile = true } = this.props; + + const locationComponents = [componentKey, ...locations.map(location => location.component)]; + const isCrossFile = uniq(locationComponents).length > 1; if (!locations || locations.length === 0 || locations.every(location => !location.msg)) { return null; } - if (isCrossFile) { + if (isCrossFile && showCrossFile) { return ( <CrossFileLocationNavigator locations={locations} onLocationSelect={this.props.onLocationSelect} - scroll={this.props.scroll} selectedLocationIndex={selectedLocationIndex} /> ); } return ( - <div className="spacer-top"> + <ul className="spacer-top "> {locations.map((location, index) => ( - <SingleFileLocationNavigator - index={index} - // eslint-disable-next-line react/no-array-index-key - key={index} - message={location.msg} - onClick={this.props.onLocationSelect} - scroll={this.props.scroll} - selected={index === selectedLocationIndex} - /> + // eslint-disable-next-line react/no-array-index-key + <li className="display-flex-column" key={index}> + <SingleFileLocationNavigator + index={index} + message={location.msg} + onClick={this.props.onLocationSelect} + selected={index === selectedLocationIndex} + /> + </li> ))} - </div> + </ul> ); } } diff --git a/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.css b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.css index f3e60e4a969..d4bc578ecb6 100644 --- a/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.css +++ b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.css @@ -18,10 +18,26 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ .locations-navigator { - position: relative; - z-index: var(--aboveNormalZIndex); - display: inline-flex; + display: flex; align-items: flex-start; - max-width: 100%; - border: none; + border: 1px solid transparent; + border-radius: 4px; + padding: calc(1 / 2 * var(--gridSize)); + margin-bottom: calc(1 / 4 * var(--gridSize)); + color: inherit; + text-align: left; +} + +.locations-navigator:hover, +.locations-navigator:active { + border-color: var(--info400); +} + +.locations-navigator:focus { + border-color: transparent; +} + +button.locations-navigator.selected { + border-color: var(--info400); + background-color: var(--info50); } diff --git a/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx index 07d329ed224..54be15b43ff 100644 --- a/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx +++ b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx @@ -17,16 +17,17 @@ * 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 * as React from 'react'; import LocationIndex from '../common/LocationIndex'; import LocationMessage from '../common/LocationMessage'; +import { ButtonPlain } from '../controls/buttons'; import './SingleFileLocationNavigator.css'; interface Props { index: number; message: string | undefined; onClick: (index: number) => void; - scroll: (element: Element) => void; selected: boolean; } @@ -35,18 +36,25 @@ export default class SingleFileLocationNavigator extends React.PureComponent<Pro componentDidMount() { if (this.props.selected && this.node) { - this.props.scroll(this.node); + this.node.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); } } componentDidUpdate(prevProps: Props) { if (this.props.selected && prevProps.selected !== this.props.selected && this.node) { - this.props.scroll(this.node); + this.node.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); } } - handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { - event.preventDefault(); + handleClick = () => { this.props.onClick(this.props.index); }; @@ -54,12 +62,18 @@ export default class SingleFileLocationNavigator extends React.PureComponent<Pro const { index, message, selected } = this.props; return ( - <div className="little-spacer-top" ref={node => (this.node = node)}> - <a className="locations-navigator" href="#" onClick={this.handleClick}> - <LocationIndex selected={selected}>{index + 1}</LocationIndex> - <LocationMessage selected={selected}>{message}</LocationMessage> - </a> - </div> + <ButtonPlain + preventDefault={true} + stopPropagation={true} + aria-current={selected ? 'location' : false} + className={classNames('locations-navigator', { selected })} + innerRef={node => { + this.node = node; + }} + onClick={this.handleClick}> + <LocationIndex>{index + 1}</LocationIndex> + <LocationMessage>{message}</LocationMessage> + </ButtonPlain> ); } } diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/CrossFileLocationsNavigator-test.tsx b/server/sonar-web/src/main/js/components/locations/__tests__/CrossFileLocationsNavigator-test.tsx index 2147bba7ad4..26ce5f7e0d0 100644 --- a/server/sonar-web/src/main/js/components/locations/__tests__/CrossFileLocationsNavigator-test.tsx +++ b/server/sonar-web/src/main/js/components/locations/__tests__/CrossFileLocationsNavigator-test.tsx @@ -82,7 +82,6 @@ function shallowRender(props: Partial<CrossFileLocationsNavigator['props']> = {} <CrossFileLocationsNavigator locations={[location1, location2, location3]} onLocationSelect={jest.fn()} - scroll={jest.fn()} selectedLocationIndex={undefined} {...props} /> diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx b/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx index a5edeafdcf8..74ca4eaa5ee 100644 --- a/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx +++ b/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx @@ -37,21 +37,9 @@ const location2: FlowLocation = { textRange: { startLine: 8, endLine: 8, startOffset: 0, endOffset: 5 } }; -const location3: FlowLocation = { - component: 'bar', - componentName: 'src/bar.js', - msg: 'Do not use bar', - textRange: { startLine: 15, endLine: 16, startOffset: 4, endOffset: 6 } -}; - it('should render locations in the same file', () => { const locations = [location1, location2]; - expect(shallowRender({ locations, isCrossFile: false })).toMatchSnapshot(); -}); - -it('should render flow locations in different file', () => { - const locations = [location1, location3]; - expect(shallowRender({ locations, isCrossFile: true })).toMatchSnapshot(); + expect(shallowRender({ locations })).toMatchSnapshot(); }); it('should not render locations', () => { @@ -63,9 +51,8 @@ function shallowRender(overrides: Partial<LocationsList['props']> = {}) { return shallow<LocationsList>( <LocationsList locations={mockIssue().secondaryLocations} - isCrossFile={true} + componentKey="foo" onLocationSelect={jest.fn()} - scroll={jest.fn()} selectedLocationIndex={undefined} {...overrides} /> diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/SingleFileLocationsNavigator-test.tsx b/server/sonar-web/src/main/js/components/locations/__tests__/SingleFileLocationsNavigator-test.tsx index 9410e6ac13e..58c0ea6fb50 100644 --- a/server/sonar-web/src/main/js/components/locations/__tests__/SingleFileLocationsNavigator-test.tsx +++ b/server/sonar-web/src/main/js/components/locations/__tests__/SingleFileLocationsNavigator-test.tsx @@ -32,7 +32,6 @@ function shallowRender(props: Partial<SingleFileLocationNavigator['props']> = {} index={0} message="" onClick={jest.fn()} - scroll={jest.fn()} selected={true} {...props} /> diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap index 53ee06d1024..c8e4b4eef3e 100644 --- a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap @@ -24,7 +24,6 @@ exports[`should render 1`] = ` key="0" message="Do not use foo" onClick={[MockFunction]} - scroll={[MockFunction]} selected={false} /> </div> @@ -67,7 +66,6 @@ exports[`should render 1`] = ` key="2" message="Do not use bar" onClick={[MockFunction]} - scroll={[MockFunction]} selected={false} /> </div> @@ -97,7 +95,6 @@ exports[`should render locations with no component name 1`] = ` index={0} key="0" onClick={[MockFunction]} - scroll={[MockFunction]} selected={false} /> </div> diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap index eb41475e47b..7e992bb77ff 100644 --- a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap @@ -1,57 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render flow locations in different file 1`] = ` -<CrossFileLocationNavigator - locations={ - Array [ - Object { - "component": "foo", - "componentName": "src/foo.js", - "msg": "Do not use foo", - "textRange": Object { - "endLine": 7, - "endOffset": 8, - "startLine": 7, - "startOffset": 5, - }, - }, - Object { - "component": "bar", - "componentName": "src/bar.js", - "msg": "Do not use bar", - "textRange": Object { - "endLine": 16, - "endOffset": 6, - "startLine": 15, - "startOffset": 4, - }, - }, - ] - } - onLocationSelect={[MockFunction]} - scroll={[MockFunction]} -/> -`; - exports[`should render locations in the same file 1`] = ` -<div - className="spacer-top" +<ul + className="spacer-top " > - <SingleFileLocationNavigator - index={0} + <li + className="display-flex-column" key="0" - message="Do not use foo" - onClick={[MockFunction]} - scroll={[MockFunction]} - selected={false} - /> - <SingleFileLocationNavigator - index={1} + > + <SingleFileLocationNavigator + index={0} + message="Do not use foo" + onClick={[MockFunction]} + selected={false} + /> + </li> + <li + className="display-flex-column" key="1" - message="Do not use foo" - onClick={[MockFunction]} - scroll={[MockFunction]} - selected={false} - /> -</div> + > + <SingleFileLocationNavigator + index={1} + message="Do not use foo" + onClick={[MockFunction]} + selected={false} + /> + </li> +</ul> `; diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap index 46f03290c53..ec2618e04cc 100644 --- a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap @@ -1,43 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly: index 1 1`] = ` -<div - className="little-spacer-top" +<ButtonPlain + aria-current="location" + className="locations-navigator selected" + innerRef={[Function]} + onClick={[Function]} + preventDefault={true} + stopPropagation={true} > - <a - className="locations-navigator" - href="#" - onClick={[Function]} - > - <LocationIndex - selected={true} - > - 1 - </LocationIndex> - <LocationMessage - selected={true} - /> - </a> -</div> + <LocationIndex> + 1 + </LocationIndex> + <LocationMessage /> +</ButtonPlain> `; exports[`should render correctly: index 2 1`] = ` -<div - className="little-spacer-top" +<ButtonPlain + aria-current="location" + className="locations-navigator selected" + innerRef={[Function]} + onClick={[Function]} + preventDefault={true} + stopPropagation={true} > - <a - className="locations-navigator" - href="#" - onClick={[Function]} - > - <LocationIndex - selected={true} - > - 2 - </LocationIndex> - <LocationMessage - selected={true} - /> - </a> -</div> + <LocationIndex> + 2 + </LocationIndex> + <LocationMessage /> +</ButtonPlain> `; diff --git a/server/sonar-web/src/main/js/helpers/issues.ts b/server/sonar-web/src/main/js/helpers/issues.ts index 1e72f660c58..2b0588f46a9 100644 --- a/server/sonar-web/src/main/js/helpers/issues.ts +++ b/server/sonar-web/src/main/js/helpers/issues.ts @@ -24,7 +24,7 @@ import SecurityHotspotIcon from '../components/icons/SecurityHotspotIcon'; import VulnerabilityIcon from '../components/icons/VulnerabilityIcon'; import { IssueType, RawIssue } from '../types/issues'; import { MetricKey } from '../types/metrics'; -import { Dict, FlowLocation, Issue, TextRange } from '../types/types'; +import { Dict, Flow, FlowLocation, Issue, TextRange } from '../types/types'; import { UserBase } from '../types/users'; import { ISSUE_TYPES } from './constants'; @@ -73,14 +73,10 @@ function injectCommentsRelational(issue: RawIssue, users?: UserBase[]) { return { comments }; } -function prepareClosed( - issue: RawIssue, - secondaryLocations: FlowLocation[], - flows: FlowLocation[][] -) { +function prepareClosed(issue: RawIssue) { return issue.status === 'CLOSED' ? { flows: [], line: undefined, textRange: undefined, secondaryLocations: [] } - : { flows, secondaryLocations }; + : {}; } function ensureTextRange(issue: RawIssue): { textRange?: TextRange } { @@ -105,7 +101,15 @@ function reverseLocations(locations: FlowLocation[]): FlowLocation[] { function splitFlows( issue: RawIssue, components: Component[] = [] -): { secondaryLocations: FlowLocation[]; flows: FlowLocation[][] } { +): { secondaryLocations: FlowLocation[]; flows: FlowLocation[][]; flowsWithType: Flow[] } { + if (issue.flows?.some(flow => flow.type !== undefined)) { + return { + flows: [], + flowsWithType: issue.flows.filter(flow => flow.type !== undefined) as Flow[], + secondaryLocations: [] + }; + } + const parsedFlows: FlowLocation[][] = (issue.flows || []) .filter(flow => flow.locations !== undefined) .map(flow => flow.locations!.filter(location => location.textRange != null)) @@ -119,8 +123,12 @@ function splitFlows( const onlySecondaryLocations = parsedFlows.every(flow => flow.length === 1); return onlySecondaryLocations - ? { secondaryLocations: orderLocations(flatten(parsedFlows)), flows: [] } - : { secondaryLocations: [], flows: parsedFlows.map(reverseLocations) }; + ? { secondaryLocations: orderLocations(flatten(parsedFlows)), flowsWithType: [], flows: [] } + : { + secondaryLocations: [], + flowsWithType: [], + flows: parsedFlows.map(reverseLocations) + }; } function orderLocations(locations: FlowLocation[]) { @@ -137,7 +145,6 @@ export function parseIssueFromResponse( users?: UserBase[], rules?: Rule[] ): Issue { - const { secondaryLocations, flows } = splitFlows(issue, components); return { ...issue, ...injectRelational(issue, components, 'component', 'key'), @@ -145,7 +152,8 @@ export function parseIssueFromResponse( ...injectRelational(issue, rules, 'rule', 'key'), ...injectRelational(issue, users, 'assignee', 'login'), ...injectCommentsRelational(issue, users), - ...prepareClosed(issue, secondaryLocations, flows), + ...splitFlows(issue, components), + ...prepareClosed(issue), ...ensureTextRange(issue) } as Issue; } diff --git a/server/sonar-web/src/main/js/helpers/mocks/webhook.ts b/server/sonar-web/src/main/js/helpers/mocks/webhook.ts new file mode 100644 index 00000000000..3f4859f4708 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/mocks/webhook.ts @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 { WebhookDelivery } from '../../types/webhook'; +import { HttpStatus } from '../request'; + +export function mockWebhookDelivery(overrides: Partial<WebhookDelivery> = {}): WebhookDelivery { + return { + at: '2019-06-14T09:45:52+0200', + durationMs: 20, + httpStatus: HttpStatus.Ok, + id: '1', + success: true, + ...overrides + }; +} diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index e666c701020..4c46878abcb 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -373,6 +373,7 @@ export function mockIssue(withLocations = false, overrides: Partial<Issue> = {}) componentUuid: 'foo1234', creationDate: '2017-03-01T09:36:01+0100', flows: [], + flowsWithType: [], key: 'AVsae-CQS-9G3txfbFN2', line: 25, message: 'Reduce the number of conditional operators (4) used in the expression', diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts index 2c106f1ba60..3ca63bfd492 100644 --- a/server/sonar-web/src/main/js/types/issues.ts +++ b/server/sonar-web/src/main/js/types/issues.ts @@ -50,7 +50,8 @@ export interface RawIssue { comments?: Array<Comment>; component: string; flows?: Array<{ - // `componentName` is not available in RawIssue + type?: string; + description?: string; locations?: Array<Omit<FlowLocation, 'componentName'>>; }>; key: string; diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index 83e2f6dd6ac..aabe02cedc5 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -229,6 +229,17 @@ export interface FacetValue<T = string> { val: T; } +export enum FlowType { + DATA = 'DATA', + EXECUTION = 'EXECUTION' +} + +export interface Flow { + type: FlowType; + description?: string; + locations: FlowLocation[]; +} + export interface FlowLocation { component: string; componentName?: string; @@ -276,6 +287,7 @@ export interface Issue { quickFixAvailable?: boolean; key: string; flows: FlowLocation[][]; + flowsWithType: Flow[]; line?: number; message: string; project: string; @@ -784,22 +796,6 @@ export interface UserSelected extends UserActive { export type Visibility = 'public' | 'private'; -export interface Webhook { - key: string; - latestDelivery?: WebhookDelivery; - name: string; - secret?: string; - url: string; -} - -export interface WebhookDelivery { - at: string; - durationMs: number; - httpStatus?: number; - id: string; - success: boolean; -} - export namespace WebApi { export interface Action { key: string; diff --git a/server/sonar-web/src/main/js/types/webhook.ts b/server/sonar-web/src/main/js/types/webhook.ts new file mode 100644 index 00000000000..6517194927f --- /dev/null +++ b/server/sonar-web/src/main/js/types/webhook.ts @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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. + */ + +export interface Webhook { + key: string; + latestDelivery?: WebhookDelivery; + name: string; + secret?: string; + url: string; +} + +export interface WebhookDelivery { + at: string; + durationMs: number; + httpStatus?: number; + id: string; + success: boolean; +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 75584c852e3..95279591fd2 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -73,6 +73,7 @@ download_verb=Download duplications=Duplications end_date=End Date edit=Edit +error=Error events=Events example=Example expand_all=Expand all @@ -177,6 +178,7 @@ rules=Rules save=Save search_results=Search results search_verb=Search +secondary=Secondary see_all=See all see_x=See {0} select_verb=Select @@ -191,12 +193,12 @@ start_date=Start Date x_show={0} shown x_selected={0} selected x_of_y_shown={0} of {1} shown -secondary=Secondary size=Size skip=Skip skip_to_content=Skip to main content status=Status support=Support +success=Success table=Table tags=Tags tags_list_x=Tags list: {0} @@ -867,6 +869,8 @@ issue.transition.resolveasreviewed.description=There is no Vulnerability in the issue.transition.resetastoreview=Reset as To Review issue.transition.resetastoreview.description=The Security Hotspot should be analyzed again issue.tabs.code=Where is the issue? +issue.x_data_flows={0} data flow(s) +issue.execution_flow=Full execution flow issues.action_select=Select issue issues.action_select.label=Select issue {0} @@ -911,6 +915,7 @@ issue.effort=Effort: issue.x_effort={0} effort issue.filter_similar_issues=Filter Similar Issues issue.this_issue_involves_x_code_locations=This issue involves {0} code location(s) +issue.this_flow_involves_x_code_locations=This flow involves {0} code location(s) issue.from_external_rule_engine=Issue detected by an external rule engine: {0} issue.external_issue_description=This is external rule {0}. No details are available. issues.cannot_open_issue_max_initial_X_fetched=Cannot open selected issue, as it's not part of the initial {0} loaded issues. |