Caution = 'caution',
Info = 'info',
Accent = 'accent',
+ Success = 'success',
+ Neutral = 'neutral',
+}
+
+export enum PillHighlight {
+ Medium = 'medium',
+ Low = 'low',
}
const variantThemeColors: Record<PillVariant, ThemeColors> = {
[PillVariant.Caution]: 'pillCaution',
[PillVariant.Info]: 'pillInfo',
[PillVariant.Accent]: 'pillAccent',
+ [PillVariant.Success]: 'pillSuccess',
+ [PillVariant.Neutral]: 'pillNeutral',
};
const variantThemeBorderColors: Record<PillVariant, ThemeColors> = {
[PillVariant.Caution]: 'pillCautionBorder',
[PillVariant.Info]: 'pillInfoBorder',
[PillVariant.Accent]: 'pillAccentBorder',
+ [PillVariant.Success]: 'pillSuccessBorder',
+ [PillVariant.Neutral]: 'pillNeutralBorder',
};
const variantThemeHoverColors: Record<PillVariant, ThemeColors> = {
[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;
// eslint-disable-next-line react/display-name
export const Pill = forwardRef<HTMLButtonElement, Readonly<PillProps>>(
- ({ children, variant, onClick, notClickable, ...rest }, ref) => {
+ ({ children, variant, highlight = PillHighlight.Low, onClick, notClickable, ...rest }, ref) => {
return onClick && !notClickable ? (
<StyledPillButton onClick={onClick} ref={ref} variant={variant} {...rest}>
{children}
</StyledPillButton>
) : (
- <StyledPill ref={ref} variant={variant} {...rest}>
+ <StyledPill highlight={highlight} ref={ref} variant={variant} {...rest}>
{children}
</StyledPill>
);
`;
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<{
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,
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],
--- /dev/null
+/*
+ * 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<DependenciesResponse>(DEPENDENCY_PATH, { params });
+}
--- /dev/null
+/*
+ * 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()),
+ ),
+ });
+ };
+}
);
};
+ const renderDependenciesLink = () => {
+ const isPortfolio = isPortfolioLike(qualifier);
+ return (
+ !isPortfolio &&
+ renderMenuLink({
+ label: translate('layout.dependencies'),
+ pathname: '/dependencies',
+ })
+ );
+ };
+
const renderSecurityReports = () => {
if (isPullRequest(branchLike)) {
return null;
{renderBreakdownLink()}
{renderIssuesLink()}
{renderSecurityHotspotsLink()}
+ {renderDependenciesLink()}
{renderSecurityReports()}
{renderComponentMeasuresLink()}
{renderCodeLink()}
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';
element={<ProjectPageExtension />}
/>
{projectIssuesRoutes()}
+ {dependenciesRoutes()}
<Route path="security_hotspots" element={<SecurityHotspotsApp />} />
{projectQualityGateRoutes()}
{projectQualityProfilesRoutes()}
--- /dev/null
+/*
+ * 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<Props>) {
+ 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 (
+ <LargeCenteredLayout className="sw-py-8 sw-typo-lg sw-h-full" id="dependencies-page">
+ <Helmet defer={false} title={translate('dependencies.page')} />
+ <main className="sw-relative sw-flex-1 sw-min-w-0 sw-h-full">
+ {resultsExist && (
+ <div className="sw-flex sw-justify-between">
+ <InputSearch
+ className="sw-mb-4"
+ searchInputAriaLabel={translate('search.search_for_dependencies')}
+ minLength={SEARCH_MIN_LENGTH}
+ value={search}
+ onChange={(value) => setSearch(value.toLowerCase())}
+ placeholder={translate('search.search_for_dependencies')}
+ size="large"
+ />
+ </div>
+ )}
+
+ <Spinner isLoading={isLoading}>
+ {dependencies.length === 0 && <EmptyState />}
+ {resultsExist && (
+ <div className="sw-overflow-auto">
+ <Text>
+ <FormattedMessage
+ id={listName}
+ defaultMessage={translate(listName)}
+ values={{
+ count: dependencies.length,
+ }}
+ />
+ </Text>
+ <ul className="sw-py-4">
+ {dependencies.map((dependency) => (
+ <li key={dependency.key}>
+ <DependencyListItem dependency={dependency} />
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+ </Spinner>
+ </main>
+ </LargeCenteredLayout>
+ );
+}
+
+function EmptyState() {
+ return (
+ <CenteredDiv className="sw-w-[450px] sw-mt-[185px] sw-flex sw-flex-col sw-gap-4 sw-text-center sw-mx-auto">
+ <Text isHighlighted>{translate('dependencies.empty_state.title')}</Text>
+ <Text>{translate('dependencies.empty_state.body')}</Text>
+ <Text>
+ <DocumentationLink
+ to={DocLink.Dependencies}
+ shouldOpenInNewTab
+ className="sw-font-semibold"
+ >
+ {translate('dependencies.empty_state.link_text')}
+ </DocumentationLink>
+ </Text>
+ </CenteredDiv>
+ );
+}
+
+const CenteredDiv = styled('div')`
+ height: 50vh;
+`;
+
+export default withRouter(withComponentContext(App));
--- /dev/null
+/*
+ * 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 });
+}
--- /dev/null
+/*
+ * 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 (
+ <Card className="sw-p-3 sw-mb-4">
+ <div className="sw-flex sw-justify-between sw-items-center">
+ <div className="sw-flex sw-items-center">
+ <span className="sw-w-[305px] sw-inline-flex sw-overflow-hidden">
+ <span className="sw-flex-shrink sw-overflow-hidden sw-text-ellipsis sw-whitespace-nowrap">
+ {hasFindings ? (
+ <LinkStandalone
+ to={`/dependencies/${dependency.key}`}
+ className="sw-mr-2 sw-text-sm"
+ >
+ {dependency.name}
+ </LinkStandalone>
+ ) : (
+ <Text isHighlighted isSubdued className="sw-mr-2">
+ {dependency.name}
+ </Text>
+ )}
+ </span>
+ <Pill
+ variant={PillVariant.Accent}
+ highlight={PillHighlight.Medium}
+ className="sw-flex-shrink-0 sw-mr-2"
+ >
+ {dependency.transitive
+ ? translate('dependencies.direct.label')
+ : translate('dependencies.transitive.label')}
+ </Pill>
+ </span>
+ {findingsExist && (
+ <span className="sw-flex">
+ {hasFindings ? (
+ <>
+ <Text isHighlighted className="sw-mr-2">
+ {translateWithParameters(
+ 'dependencies.dependency.findings.label',
+ findingsCount,
+ )}
+ </Text>
+ {Object.entries(dependency.findingsSeverities || {}).map(([severity, count]) => (
+ <span key={severity} className="sw-flex sw-items-center sw-mr-1">
+ <SoftwareImpactSeverityIcon
+ severity={severity}
+ className="sw-mr-1"
+ width={16}
+ height={16}
+ />
+ <Text>{count}</Text>
+ </span>
+ ))}
+ {findingsExploitableCount > 0 && (
+ <Pill
+ variant={PillVariant.Danger}
+ highlight={PillHighlight.Medium}
+ className="sw-ml-2"
+ >
+ <FormattedMessage
+ id="dependencies.dependency.exploitable_findings.label"
+ defaultMessage={translate(
+ 'dependencies.dependency.exploitable_findings.label',
+ )}
+ values={{
+ count: findingsExploitableCount,
+ }}
+ />
+ </Pill>
+ )}
+ </>
+ ) : (
+ <Text isSubdued>{translate('dependencies.dependency.no_findings.label')}</Text>
+ )}
+ </span>
+ )}
+ </div>
+ <div className="sw-flex sw-items-center">
+ {isDefined(dependency.fixVersion) ? (
+ <>
+ <Text className="sw-mr-1">{translate('dependencies.dependency.version.label')}</Text>
+ <Pill variant={PillVariant.Caution} highlight={PillHighlight.Medium}>
+ {dependency.version}
+ </Pill>
+ </>
+ ) : (
+ <Pill variant={PillVariant.Neutral} highlight={PillHighlight.Medium}>
+ {dependency.version}
+ </Pill>
+ )}
+
+ {isDefined(dependency.fixVersion) && (
+ <>
+ <IconArrowRight />
+ <Text className="sw-mr-1">
+ {translate('dependencies.dependency.fix_version.label')}
+ </Text>
+ <Pill variant={PillVariant.Success} highlight={PillHighlight.Medium}>
+ {dependency.fixVersion}
+ </Pill>
+ </>
+ )}
+ </div>
+ </div>
+ </Card>
+ );
+}
+
+export default DependencyListItem;
--- /dev/null
+/*
+ * 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 = () => <Route path="dependencies" element={<DependenciesApp />} />;
+
+export default dependenciesRoutes;
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 = {
--- /dev/null
+/*
+ * 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,
+ }),
+ });
+ },
+);
*/
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 {
--- /dev/null
+/*
+ * 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<Record<SoftwareImpactSeverity, number>>;
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
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
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
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...