From b838452eb06ec6318e23d34cc1ee23e84aa489cd Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:36:28 +0300 Subject: [PATCH] SONAR-23098 Introduce Project Dependencies tab --- .../design-system/src/components/Pill.tsx | 27 ++- .../design-system/src/theme/light.ts | 8 + .../sonar-web/src/main/js/api/dependencies.ts | 28 +++ .../js/api/mocks/DependenciesServiceMock.ts | 62 +++++ .../js/app/components/nav/component/Menu.tsx | 12 + .../src/main/js/app/utils/startReactApp.tsx | 2 + .../js/apps/dependencies/DependenciesApp.tsx | 128 ++++++++++ .../__tests__/DependenciesApp-it.tsx | 224 ++++++++++++++++++ .../components/DependencyListItem.tsx | 144 +++++++++++ .../src/main/js/apps/dependencies/routes.tsx | 27 +++ .../src/main/js/helpers/doc-links.ts | 1 + .../src/main/js/queries/dependencies.ts | 37 +++ .../js/sonar-aligned/types/branch-like.ts | 5 + .../src/main/js/types/dependencies.ts | 43 ++++ .../resources/org/sonar/l10n/core.properties | 22 ++ 15 files changed, 765 insertions(+), 5 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/dependencies.ts create mode 100644 server/sonar-web/src/main/js/api/mocks/DependenciesServiceMock.ts create mode 100644 server/sonar-web/src/main/js/apps/dependencies/DependenciesApp.tsx create mode 100644 server/sonar-web/src/main/js/apps/dependencies/__tests__/DependenciesApp-it.tsx create mode 100644 server/sonar-web/src/main/js/apps/dependencies/components/DependencyListItem.tsx create mode 100644 server/sonar-web/src/main/js/apps/dependencies/routes.tsx create mode 100644 server/sonar-web/src/main/js/queries/dependencies.ts create mode 100644 server/sonar-web/src/main/js/types/dependencies.ts diff --git a/server/sonar-web/design-system/src/components/Pill.tsx b/server/sonar-web/design-system/src/components/Pill.tsx index 3f53c3473c5..bcd2b2f246c 100644 --- a/server/sonar-web/design-system/src/components/Pill.tsx +++ b/server/sonar-web/design-system/src/components/Pill.tsx @@ -31,6 +31,13 @@ export enum PillVariant { Caution = 'caution', Info = 'info', Accent = 'accent', + Success = 'success', + Neutral = 'neutral', +} + +export enum PillHighlight { + Medium = 'medium', + Low = 'low', } const variantThemeColors: Record = { @@ -40,6 +47,8 @@ const variantThemeColors: Record = { [PillVariant.Caution]: 'pillCaution', [PillVariant.Info]: 'pillInfo', [PillVariant.Accent]: 'pillAccent', + [PillVariant.Success]: 'pillSuccess', + [PillVariant.Neutral]: 'pillNeutral', }; const variantThemeBorderColors: Record = { @@ -49,6 +58,8 @@ const variantThemeBorderColors: Record = { [PillVariant.Caution]: 'pillCautionBorder', [PillVariant.Info]: 'pillInfoBorder', [PillVariant.Accent]: 'pillAccentBorder', + [PillVariant.Success]: 'pillSuccessBorder', + [PillVariant.Neutral]: 'pillNeutralBorder', }; const variantThemeHoverColors: Record = { @@ -58,12 +69,15 @@ const variantThemeHoverColors: Record = { [PillVariant.Caution]: 'pillCautionHover', [PillVariant.Info]: 'pillInfoHover', [PillVariant.Accent]: 'pillAccentHover', + [PillVariant.Success]: 'pillSuccessHover', + [PillVariant.Neutral]: 'pillNeutralHover', }; interface PillProps { ['aria-label']?: string; children: ReactNode; className?: string; + highlight?: PillHighlight; // If pill is wrapped with Tooltip, it will have onClick prop overriden. // So to avoid hover effect, we add additional prop to disable hover effect even with onClick. notClickable?: boolean; @@ -73,13 +87,13 @@ interface PillProps { // eslint-disable-next-line react/display-name export const Pill = forwardRef>( - ({ children, variant, onClick, notClickable, ...rest }, ref) => { + ({ children, variant, highlight = PillHighlight.Low, onClick, notClickable, ...rest }, ref) => { return onClick && !notClickable ? ( {children} ) : ( - + {children} ); @@ -101,14 +115,17 @@ const reusedStyles = css` `; const StyledPill = styled.span<{ + highlight: PillHighlight; variant: PillVariant; }>` ${reusedStyles}; - background-color: ${({ variant }) => themeColor(variantThemeColors[variant])}; + background-color: ${({ variant, highlight }) => + highlight === PillHighlight.Medium && themeColor(variantThemeColors[variant])}; color: ${({ variant }) => themeContrast(variantThemeColors[variant])}; - border-style: ${({ variant }) => (variant === PillVariant.Accent ? 'hidden' : 'solid')}; - border-color: ${({ variant }) => themeColor(variantThemeBorderColors[variant])}; + border-style: ${({ highlight }) => (highlight === PillHighlight.Medium ? 'hidden' : 'solid')}; + border-color: ${({ variant, highlight }) => + highlight === PillHighlight.Low && themeColor(variantThemeBorderColors[variant])}; `; const StyledPillButton = styled.button<{ diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index f885de25948..f9c0aaa006f 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -345,6 +345,12 @@ export const lightTheme = { pillAccent: COLORS.indigo[50], pillAccentBorder: 'transparent', pillAccentHover: COLORS.indigo[100], + pillSuccess: COLORS.green[100], + pillSuccessBorder: COLORS.green[600], + pillSuccessHover: COLORS.green[200], + pillNeutral: COLORS.blueGrey[50], + pillNeutralBorder: COLORS.blueGrey[400], + pillNeutralHover: COLORS.blueGrey[100], // input select selectOptionSelected: secondary.light, @@ -781,6 +787,8 @@ export const lightTheme = { pillCaution: COLORS.yellow[800], pillInfo: COLORS.blue[800], pillAccent: COLORS.indigo[500], + pillSuccess: COLORS.green[800], + pillNeutral: COLORS.blueGrey[500], // project cards overviewCardDefaultIcon: COLORS.blueGrey[500], diff --git a/server/sonar-web/src/main/js/api/dependencies.ts b/server/sonar-web/src/main/js/api/dependencies.ts new file mode 100644 index 00000000000..94498055268 --- /dev/null +++ b/server/sonar-web/src/main/js/api/dependencies.ts @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 axios from 'axios'; +import { BranchLikeParameters } from '../sonar-aligned/types/branch-like'; +import { DependenciesResponse } from '../types/dependencies'; + +const DEPENDENCY_PATH = '/api/v2/analysis/dependencies'; + +export function getDependencies(params: { projectKey: string; q?: string } & BranchLikeParameters) { + return axios.get(DEPENDENCY_PATH, { params }); +} diff --git a/server/sonar-web/src/main/js/api/mocks/DependenciesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/DependenciesServiceMock.ts new file mode 100644 index 00000000000..15578b9609e --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/DependenciesServiceMock.ts @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { cloneDeep } from 'lodash'; +import { DependenciesResponse } from '../../types/dependencies'; +import { getDependencies } from '../dependencies'; + +jest.mock('../dependencies'); + +export const DEFAULT_DEPENDENCIES_MOCK: DependenciesResponse = { + page: { + pageIndex: 1, + pageSize: 100, + total: 0, + }, + dependencies: [], +}; + +export default class DependenciesServiceMock { + #defaultDependenciesData: DependenciesResponse = DEFAULT_DEPENDENCIES_MOCK; + + constructor() { + jest.mocked(getDependencies).mockImplementation(this.handleGetDependencies); + } + + reset = () => { + this.#defaultDependenciesData = cloneDeep(DEFAULT_DEPENDENCIES_MOCK); + return this; + }; + + setDefaultDependencies = (response: DependenciesResponse) => { + this.#defaultDependenciesData = response; + }; + + handleGetDependencies = (data: { q?: string }) => { + return Promise.resolve({ + ...this.#defaultDependenciesData, + dependencies: this.#defaultDependenciesData.dependencies.filter( + (dependency) => + typeof data.q !== 'string' || + dependency.name.toLowerCase().includes(data.q.toLowerCase()), + ), + }); + }; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx index 6b06dd7ea5b..092f2dd6f57 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx @@ -209,6 +209,17 @@ export function Menu(props: Readonly) { ); }; + const renderDependenciesLink = () => { + const isPortfolio = isPortfolioLike(qualifier); + return ( + !isPortfolio && + renderMenuLink({ + label: translate('layout.dependencies'), + pathname: '/dependencies', + }) + ); + }; + const renderSecurityReports = () => { if (isPullRequest(branchLike)) { return null; @@ -551,6 +562,7 @@ export function Menu(props: Readonly) { {renderBreakdownLink()} {renderIssuesLink()} {renderSecurityHotspotsLink()} + {renderDependenciesLink()} {renderSecurityReports()} {renderComponentMeasuresLink()} {renderCodeLink()} diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index df15beb3d04..605c7910ce1 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -40,6 +40,7 @@ import ChangeAdminPasswordApp from '../../apps/change-admin-password/ChangeAdmin import codeRoutes from '../../apps/code/routes'; import codingRulesRoutes from '../../apps/coding-rules/routes'; import componentMeasuresRoutes from '../../apps/component-measures/routes'; +import { dependenciesRoutes } from '../../apps/dependencies/routes'; import groupsRoutes from '../../apps/groups/routes'; import { globalIssuesRoutes, projectIssuesRoutes } from '../../apps/issues/routes'; import maintenanceRoutes from '../../apps/maintenance/routes'; @@ -122,6 +123,7 @@ function renderComponentRoutes() { element={} /> {projectIssuesRoutes()} + {dependenciesRoutes()} } /> {projectQualityGateRoutes()} {projectQualityProfilesRoutes()} diff --git a/server/sonar-web/src/main/js/apps/dependencies/DependenciesApp.tsx b/server/sonar-web/src/main/js/apps/dependencies/DependenciesApp.tsx new file mode 100644 index 00000000000..712383f65c2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/dependencies/DependenciesApp.tsx @@ -0,0 +1,128 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 styled from '@emotion/styled'; +import { Spinner, Text } from '@sonarsource/echoes-react'; +import { InputSearch, LargeCenteredLayout } from 'design-system'; +import React, { useState } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { FormattedMessage } from 'react-intl'; +import withComponentContext from '../../app/components/componentContext/withComponentContext'; +import DocumentationLink from '../../components/common/DocumentationLink'; +import { DocLink } from '../../helpers/doc-links'; +import { translate } from '../../helpers/l10n'; +import { useCurrentBranchQuery } from '../../queries/branch'; +import { useDependenciesQuery } from '../../queries/dependencies'; +import { withRouter } from '../../sonar-aligned/components/hoc/withRouter'; +import { getBranchLikeQuery } from '../../sonar-aligned/helpers/branch-like'; +import { BranchLikeParameters } from '../../sonar-aligned/types/branch-like'; +import { Component } from '../../types/types'; +import DependencyListItem from './components/DependencyListItem'; + +const SEARCH_MIN_LENGTH = 3; + +interface Props { + component: Component; +} + +function App(props: Readonly) { + const { component } = props; + const { data: branchLike } = useCurrentBranchQuery(component); + + const [search, setSearch] = useState(''); + + const { data: { dependencies = [] } = {}, isLoading } = useDependenciesQuery({ + projectKey: component.key, + q: search, + branchParameters: getBranchLikeQuery(branchLike) as BranchLikeParameters, + }); + + const listName = search ? 'dependencies.list.name_search.title' : 'dependencies.list.title'; + + const resultsExist = dependencies.length > 0 || search.length >= SEARCH_MIN_LENGTH; + + return ( + + +
+ {resultsExist && ( +
+ setSearch(value.toLowerCase())} + placeholder={translate('search.search_for_dependencies')} + size="large" + /> +
+ )} + + + {dependencies.length === 0 && } + {resultsExist && ( +
+ + + +
    + {dependencies.map((dependency) => ( +
  • + +
  • + ))} +
+
+ )} +
+
+
+ ); +} + +function EmptyState() { + return ( + + {translate('dependencies.empty_state.title')} + {translate('dependencies.empty_state.body')} + + + {translate('dependencies.empty_state.link_text')} + + + + ); +} + +const CenteredDiv = styled('div')` + height: 50vh; +`; + +export default withRouter(withComponentContext(App)); diff --git a/server/sonar-web/src/main/js/apps/dependencies/__tests__/DependenciesApp-it.tsx b/server/sonar-web/src/main/js/apps/dependencies/__tests__/DependenciesApp-it.tsx new file mode 100644 index 00000000000..1e3526328d1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/dependencies/__tests__/DependenciesApp-it.tsx @@ -0,0 +1,224 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { byRole, byText } from '../../../sonar-aligned/helpers/testSelector'; + +import userEvent from '@testing-library/user-event'; +import { DEBOUNCE_DELAY } from 'design-system'; +import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock'; +import DependenciesServiceMock from '../../../api/mocks/DependenciesServiceMock'; +import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock'; +import { mockComponent } from '../../../helpers/mocks/component'; +import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils'; +import { DependenciesResponse } from '../../../types/dependencies'; +import { Component } from '../../../types/types'; +import routes from '../routes'; + +const depsHandler = new DependenciesServiceMock(); +const branchesHandler = new BranchesServiceMock(); +const settingsHandler = new SettingsServiceMock(); +const MOCK_RESPONSE: DependenciesResponse = { + page: { + pageIndex: 1, + pageSize: 100, + total: 4, + }, + dependencies: [ + { + key: '1', + name: 'jackson-databind', + longName: 'com.fasterxml.jackson.core:jackson-databind', + version: '2.10.0', + fixVersion: '2.12.13', + transitive: false, + findingsCount: 16, + findingsSeverities: { BLOCKER: 1, HIGH: 2, MEDIUM: 2, LOW: 2, INFO: 9 }, + findingsExploitableCount: 1, + project: 'project1', + }, + { + key: '2', + name: 'snappy-java', + longName: 'org.xerial.snappy:snappy-java', + version: '3.52', + fixVersion: '4.6.1', + transitive: true, + findingsCount: 2, + findingsSeverities: { LOW: 2 }, + findingsExploitableCount: 0, + project: 'project1', + }, + { + key: '3', + name: 'SnakeYAML', + longName: 'org.yaml:SnakeYAML', + version: '2.10.0', + transitive: true, + findingsCount: 3, + findingsSeverities: { INFO: 3 }, + findingsExploitableCount: 0, + project: 'project1', + }, + { + key: '4', + name: 'random-lib', + longName: 'com.random:random-lib', + version: '2.10.0', + transitive: true, + findingsCount: 0, + findingsSeverities: {}, + findingsExploitableCount: 0, + project: 'project1', + }, + ], +}; + +const MOCK_RESPONSE_NO_FINDINGS: DependenciesResponse = { + page: { + pageIndex: 1, + pageSize: 100, + total: 4, + }, + dependencies: [ + { + key: '1', + name: 'jackson-databind', + longName: 'com.fasterxml.jackson.core:jackson-databind', + version: '2.10.0', + transitive: false, + project: 'project1', + }, + { + key: '2', + name: 'snappy-java', + longName: 'org.xerial.snappy:snappy-java', + version: '3.52', + transitive: true, + project: 'project1', + }, + { + key: '3', + name: 'SnakeYAML', + longName: 'org.yaml:SnakeYAML', + version: '2.10.0', + transitive: true, + project: 'project1', + }, + { + key: '4', + name: 'random-lib', + longName: 'com.random:random-lib', + version: '2.10.0', + transitive: true, + project: 'project1', + }, + ], +}; + +beforeEach(() => { + branchesHandler.reset(); + depsHandler.reset(); + settingsHandler.reset(); +}); + +it('should correctly show an empty state', async () => { + const { ui } = getPageObject(); + renderDependenciesApp(); + + expect(await ui.emptyStateTitle.find()).toBeInTheDocument(); + expect(await ui.emptyStateLink.find()).toBeInTheDocument(); +}); + +it('should correctly render dependencies with findings', async () => { + depsHandler.setDefaultDependencies(MOCK_RESPONSE); + const { ui } = getPageObject(); + + renderDependenciesApp(); + + expect(await ui.dependencies.findAll()).toHaveLength(4); +}); + +it('should correctly render dependencies when no finding information is available', async () => { + depsHandler.setDefaultDependencies(MOCK_RESPONSE_NO_FINDINGS); + const { ui } = getPageObject(); + + renderDependenciesApp(); + + expect(await ui.dependencies.findAll()).toHaveLength(4); + expect(byText('dependencies.dependency.no_findings.label').query()).not.toBeInTheDocument(); +}); + +it('should correctly search for dependencies', async () => { + depsHandler.setDefaultDependencies(MOCK_RESPONSE_NO_FINDINGS); + const { ui, user } = getPageObject(); + + renderDependenciesApp(); + + expect(await ui.dependencies.findAll()).toHaveLength(4); + + user.type(ui.searchInput.get(), 'jackson'); + + // Wait for input debounce + await new Promise((resolve) => { + setTimeout(resolve, DEBOUNCE_DELAY); + }); + + expect(await ui.dependencies.findAll()).toHaveLength(1); +}); + +it('should correctly show empty results state when no dependencies are found', async () => { + depsHandler.setDefaultDependencies(MOCK_RESPONSE_NO_FINDINGS); + const { ui, user } = getPageObject(); + + renderDependenciesApp(); + + expect(await ui.dependencies.findAll()).toHaveLength(4); + + user.type(ui.searchInput.get(), 'asd'); + + // Wait for input debounce + await new Promise((resolve) => { + setTimeout(resolve, DEBOUNCE_DELAY); + }); + + expect(await ui.searchTitle.get()).toBeInTheDocument(); +}); + +function getPageObject() { + const user = userEvent.setup(); + const ui = { + emptyStateTitle: byText('dependencies.empty_state.title'), + emptyStateLink: byRole('link', { + name: /dependencies.empty_state.link_text/, + }), + dependencies: byRole('listitem'), + searchInput: byRole('searchbox'), + searchTitle: byText('dependencies.list.name_search.title0'), + }; + return { ui, user }; +} + +function renderDependenciesApp( + { navigateTo, component }: { component: Component; navigateTo?: string } = { + component: mockComponent(), + }, +) { + return renderAppWithComponentContext('dependencies', routes, { navigateTo }, { component }); +} diff --git a/server/sonar-web/src/main/js/apps/dependencies/components/DependencyListItem.tsx b/server/sonar-web/src/main/js/apps/dependencies/components/DependencyListItem.tsx new file mode 100644 index 00000000000..83ae36c7d84 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/dependencies/components/DependencyListItem.tsx @@ -0,0 +1,144 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { IconArrowRight, LinkStandalone, Text } from '@sonarsource/echoes-react'; +import { Card, Pill, PillHighlight, PillVariant } from 'design-system'; +import React from 'react'; + +import { FormattedMessage } from 'react-intl'; +import SoftwareImpactSeverityIcon from '../../../components/icon-mappers/SoftwareImpactSeverityIcon'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { isDefined } from '../../../helpers/types'; +import { Dependency } from '../../../types/dependencies'; + +function DependencyListItem({ dependency }: Readonly<{ dependency: Dependency }>) { + //TODO: Clean up once the response returns these values always + const findingsExist = isDefined(dependency.findingsCount); + const findingsCount = isDefined(dependency.findingsCount) ? dependency.findingsCount : 0; + const findingsExploitableCount = dependency.findingsExploitableCount ?? 0; + + const hasFindings = findingsCount > 0; + + return ( + +
+
+ + + {hasFindings ? ( + + {dependency.name} + + ) : ( + + {dependency.name} + + )} + + + {dependency.transitive + ? translate('dependencies.direct.label') + : translate('dependencies.transitive.label')} + + + {findingsExist && ( + + {hasFindings ? ( + <> + + {translateWithParameters( + 'dependencies.dependency.findings.label', + findingsCount, + )} + + {Object.entries(dependency.findingsSeverities || {}).map(([severity, count]) => ( + + + {count} + + ))} + {findingsExploitableCount > 0 && ( + + + + )} + + ) : ( + {translate('dependencies.dependency.no_findings.label')} + )} + + )} +
+
+ {isDefined(dependency.fixVersion) ? ( + <> + {translate('dependencies.dependency.version.label')} + + {dependency.version} + + + ) : ( + + {dependency.version} + + )} + + {isDefined(dependency.fixVersion) && ( + <> + + + {translate('dependencies.dependency.fix_version.label')} + + + {dependency.fixVersion} + + + )} +
+
+
+ ); +} + +export default DependencyListItem; diff --git a/server/sonar-web/src/main/js/apps/dependencies/routes.tsx b/server/sonar-web/src/main/js/apps/dependencies/routes.tsx new file mode 100644 index 00000000000..27525c61107 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/dependencies/routes.tsx @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 React from 'react'; +import { Route } from 'react-router-dom'; +import DependenciesApp from './DependenciesApp'; + +export const dependenciesRoutes = () => } />; + +export default dependenciesRoutes; diff --git a/server/sonar-web/src/main/js/helpers/doc-links.ts b/server/sonar-web/src/main/js/helpers/doc-links.ts index e02e3a1fafe..c142f5c32c4 100644 --- a/server/sonar-web/src/main/js/helpers/doc-links.ts +++ b/server/sonar-web/src/main/js/helpers/doc-links.ts @@ -89,6 +89,7 @@ export enum DocLink { SonarScannerMaven = '/analyzing-source-code/scanners/sonarscanner-for-maven/', SonarWayQualityGate = '/user-guide/quality-gates/#using-sonar-way-the-recommended-quality-gate', // to be confirmed Webhooks = '/project-administration/webhooks/', + Dependencies = '/project-administration/managing-dependencies/', } export const DocTitle = { diff --git a/server/sonar-web/src/main/js/queries/dependencies.ts b/server/sonar-web/src/main/js/queries/dependencies.ts new file mode 100644 index 00000000000..d74c3b57d4c --- /dev/null +++ b/server/sonar-web/src/main/js/queries/dependencies.ts @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { queryOptions } from '@tanstack/react-query'; +import { getDependencies } from '../api/dependencies'; +import { BranchLikeParameters } from '../sonar-aligned/types/branch-like'; +import { createQueryHook } from './common'; + +export const useDependenciesQuery = createQueryHook( + (data: { branchParameters: BranchLikeParameters; projectKey: string; q?: string }) => { + return queryOptions({ + queryKey: ['dependencies', data.projectKey, data.branchParameters, data.q], + queryFn: () => + getDependencies({ + projectKey: data.projectKey, + q: data.q, + ...data.branchParameters, + }), + }); + }, +); diff --git a/server/sonar-web/src/main/js/sonar-aligned/types/branch-like.ts b/server/sonar-web/src/main/js/sonar-aligned/types/branch-like.ts index 3f075d402e7..f71497a9992 100644 --- a/server/sonar-web/src/main/js/sonar-aligned/types/branch-like.ts +++ b/server/sonar-web/src/main/js/sonar-aligned/types/branch-like.ts @@ -19,8 +19,13 @@ */ import { Status } from './common'; +/** + * For Web API V2, use BranchLikeParameters instead + */ export type BranchParameters = { branch?: string } | { pullRequest?: string }; +export type BranchLikeParameters = { branchKey?: string } | { pullRequestKey?: string }; + export type BranchLikeBase = BranchBase | PullRequestBase; export interface BranchBase { diff --git a/server/sonar-web/src/main/js/types/dependencies.ts b/server/sonar-web/src/main/js/types/dependencies.ts new file mode 100644 index 00000000000..0974ce79754 --- /dev/null +++ b/server/sonar-web/src/main/js/types/dependencies.ts @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { SoftwareImpactSeverity } from './clean-code-taxonomy'; +import { Paging } from './types'; + +export interface Dependency { + description?: string; + //TODO: Remove optional flag when findings are implemented + findingsCount?: number; + findingsExploitableCount?: number; + findingsSeverities?: FindingsSeverities; + fixVersion?: string; + key: string; + longName: string; + name: string; + project: string; + transitive: boolean; + version?: string; +} + +export interface DependenciesResponse { + dependencies: Dependency[]; + page: Paging; +} + +type FindingsSeverities = Partial>; 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 9c8e346cb22..b0e96baa0be 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -598,6 +598,7 @@ layout.logout=Log out layout.measures=Measures layout.settings=Administration layout.security_hotspots=Security Hotspots +layout.dependencies=Dependencies layout.settings.TRK=Project Settings layout.settings.APP=Application Settings layout.settings.VW=Portfolio Settings @@ -773,6 +774,7 @@ quality_gates.page=Quality Gates issues.page=Issues issues.skip_to_filters=Skip to issue filters issues.skip_to_list=Skip to issues list +dependencies.page=Dependencies view_projects.page=Projects portfolios.page=Portfolios portfolio_breakdown.page=Portfolio Breakdown @@ -1276,6 +1278,25 @@ issue_bulk_change.select_tags=Select tags issue_bulk_change.selected_tags=Selected tags issue_bulk_change.resolution_comment=Resolution comment +#------------------------------------------------------------------------------ +# +# DEPENDENCIES PAGE +# +#------------------------------------------------------------------------------ + +dependencies.list.title={count} {count, plural, one {dependency} other {dependencies}} +dependencies.list.name_search.title={count} matching {count, plural, one {dependency} other {dependencies}} +dependencies.empty_state.title=There are no dependencies on this project +dependencies.empty_state.body=When you analyze 3rd party code dependencies you will see them be displayed here along with any vulnerabilities they may raise +dependencies.empty_state.link_text=Learn more about dependency analysis +dependencies.transitive.label=Transitive +dependencies.direct.label=Direct +dependencies.dependency.findings.label={0} findings +dependencies.dependency.exploitable_findings.label={count} exploitable {count, plural, one {finding} other {findings}} +dependencies.dependency.no_findings.label=No findings +dependencies.dependency.version.label=version +dependencies.dependency.fix_version.label=fix with + #------------------------------------------------------------------------------ # # PROJECTS PAGE @@ -1909,6 +1930,7 @@ search.shortcut_hint=Hint: Press {shortcut} from anywhere to open this search ba search.show_more.hint=Press {key} to display search.placeholder=Search for projects... search.search_for_projects=Search for projects... +search.search_for_dependencies=Search for dependencies... search.search_for_members=Search for members... search.search_for_users=Search for users... search.search_for_users_or_groups=Search for users or groups... -- 2.39.5