--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import styled from '@emotion/styled';
+import tw from 'twin.macro';
+import { themeColor } from '../helpers/theme';
+
+export const Title = styled.h1`
+ ${tw`sw-heading-lg`}
+ ${tw`sw-mb-4`}
+ color: ${themeColor('pageTitle')};
+`;
+
+export const SubTitle = styled.h2`
+ ${tw`sw-heading-md`}
+ ${tw`sw-mb-4`}
+ color: ${themeColor('pageTitle')};
+`;
+
+export const SubHeading = styled.h3`
+ ${tw`sw-body-md-highlight`}
+ ${tw`sw-mb-2`}
+ color: ${themeColor('pageContent')};
+`;
+
+export const SubHeadingHighlight = styled(SubHeading)`
+ color: ${themeColor('pageContentDark')};
+`;
export * from './Tags';
export * from './TagsSelector';
export * from './Text';
+export * from './Title';
export { ToggleButton } from './ToggleButton';
export { TopBar } from './TopBar';
export * from './TreeMap';
import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks';
import { Component, Status } from '../../types/types';
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
+import ComponentContainerNotFound from './ComponentContainerNotFound';
import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from './available-features/withAvailableFeatures';
import withBranchStatusActions from './branch-status/withBranchStatusActions';
-import ComponentContainerNotFound from './ComponentContainerNotFound';
import { ComponentContext } from './componentContext/ComponentContext';
import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation';
import ComponentNav from './nav/component/ComponentNav';
currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)}
isInProgress={isInProgress}
isPending={isPending}
- onComponentChange={this.handleComponentChange}
onWarningDismiss={this.handleWarningDismiss}
projectBinding={projectBinding}
projectBindingErrors={projectBindingErrors}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { LAYOUT_GLOBAL_NAV_HEIGHT, LAYOUT_PROJECT_NAV_HEIGHT, TopBar } from 'design-system';
+import { TopBar } from 'design-system';
import * as React from 'react';
import { translate } from '../../../../helpers/l10n';
import {
import Header from './Header';
import HeaderMeta from './HeaderMeta';
import Menu from './Menu';
-import InfoDrawer from './projectInformation/InfoDrawer';
-import ProjectInformation from './projectInformation/ProjectInformation';
export interface ComponentNavProps {
branchLikes: BranchLike[];
currentTaskOnSameBranch?: boolean;
isInProgress?: boolean;
isPending?: boolean;
- onComponentChange: (changes: Partial<Component>) => void;
onWarningDismiss: () => void;
projectBinding?: ProjectAlmBindingResponse;
projectBindingErrors?: ProjectAlmBindingConfigurationErrors;
}}
projectInfoDisplayed={displayProjectInfo}
/>
- <InfoDrawer
- displayed={displayProjectInfo}
- onClose={() => {
- setDisplayProjectInfo(false);
- }}
- top={LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_PROJECT_NAV_HEIGHT}
- >
- <ProjectInformation
- branchLike={currentBranchLike}
- component={component}
- onComponentChange={props.onComponentChange}
- />
- </InfoDrawer>
</TopBar>
{prDecoNotifComponent}
</>
const isApplication = this.isApplication();
const label = translate(isProject ? 'project' : 'application', 'info.title');
const isApplicationChildInaccessble = this.isApplicationChildInaccessble();
+ const query = this.getQuery();
if (isPullRequest(this.props.branchLike)) {
return null;
(isProject || isApplication) && (
<li className="sw-body-md sw-pb-4">
<Link
- onClick={this.props.onToggleProjectInfo}
- preventDefault
ref={(node: HTMLAnchorElement | null) => (this.projectInfoLink = node)}
- to={{}}
+ to={{ pathname: '/project/info', search: new URLSearchParams(query).toString() }}
>
{label}
</Link>
it('correctly returns focus to the Project Information link when the drawer is closed', () => {
renderComponentNav();
screen.getByRole('link', { name: 'project.info.title' }).click();
- expect(screen.getByRole('link', { name: 'project.info.title' })).not.toHaveFocus();
-
- screen.getByRole('button', { name: 'close' }).click();
- expect(screen.getByRole('link', { name: 'project.info.title' })).toHaveFocus();
+ expect(screen.getByText('/project/info?id=my-project')).toBeInTheDocument();
});
function renderComponentNav(props: Partial<ComponentNavProps> = {}) {
currentBranchLike={undefined}
isInProgress={false}
isPending={false}
- onComponentChange={jest.fn()}
onWarningDismiss={jest.fn()}
warnings={[]}
{...props}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import ChevronRightIcon from '../../../../../components/icons/ChevronRightIcon';
-
-export interface DrawerLinkProps<P> {
- label: string;
- onPageChange: (page: P) => void;
- to: P;
-}
-
-export function DrawerLink<P>(props: DrawerLinkProps<P>) {
- const { label, to } = props;
-
- return (
- <a
- className="display-flex-space-between bordered-bottom big-padded"
- onClick={() => props.onPageChange(to)}
- role="link"
- tabIndex={0}
- >
- {label}
- <ChevronRightIcon />
- </a>
- );
-}
-
-export default React.memo(DrawerLink);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-:root {
- --drawer-width: 380px;
-}
-
-.info-drawer-pane {
- background-color: white;
- right: calc(-1 * var(--drawer-width));
- width: var(--drawer-width);
- transition: right 0.3s ease-in-out;
- border-top: 1px solid var(--barBorderColor);
- border-left: 1px solid var(--barBorderColor);
- box-sizing: border-box;
-}
-
-.info-drawer-pane.open {
- right: 0;
-}
-
-.info-drawer {
- position: fixed;
- /* top is defined programmatically by ComponentNav */
- bottom: 0;
- z-index: var(--pageSideZIndex);
-}
-
-.info-drawer .close-button {
- position: absolute;
- top: 0;
- right: 0;
- background: white;
- padding: calc(2 * var(--gridSize));
- z-index: var(--normalZIndex);
-}
-
-.info-drawer .back-button {
- cursor: pointer;
-}
-
-.info-drawer .back-button:hover {
- color: var(--blue);
-}
-
-.info-drawer-page {
- position: absolute;
- top: 0;
- bottom: 0;
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import classNames from 'classnames';
-import * as React from 'react';
-import { ClearButton } from '../../../../../components/controls/buttons';
-import EscKeydownHandler from '../../../../../components/controls/EscKeydownHandler';
-import OutsideClickHandler from '../../../../../components/controls/OutsideClickHandler';
-import { translate } from '../../../../../helpers/l10n';
-import './InfoDrawer.css';
-
-export interface InfoDrawerProps {
- children: React.ReactNode;
- displayed: boolean;
- onClose: () => void;
- top: number;
-}
-
-export default function InfoDrawer(props: InfoDrawerProps) {
- const { children, displayed, onClose, top } = props;
-
- return (
- <div
- className={classNames('info-drawer info-drawer-pane', { open: displayed })}
- style={{ top }}
- >
- {displayed && (
- <>
- <div className="close-button">
- <ClearButton aria-label={translate('close')} onClick={onClose} />
- </div>
- <EscKeydownHandler onKeydown={onClose}>
- <OutsideClickHandler onClickOutside={onClose}>
- <div className="display-flex-column max-height-100">{children}</div>
- </OutsideClickHandler>
- </EscKeydownHandler>
- </>
- )}
- </div>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import classNames from 'classnames';
-import * as React from 'react';
-import BackIcon from '../../../../../components/icons/BackIcon';
-import { translate } from '../../../../../helpers/l10n';
-
-export interface InfoDrawerPageProps {
- children: React.ReactNode;
- displayed: boolean;
- onPageChange: () => void;
-}
-
-export default function InfoDrawerPage(props: InfoDrawerPageProps) {
- const { children, displayed, onPageChange } = props;
- return (
- <div
- className={classNames(
- 'info-drawer-page info-drawer-pane display-flex-column overflow-hidden',
- {
- open: displayed,
- }
- )}
- >
- {displayed && (
- <>
- <a
- className="h2 back-button big-padded bordered-bottom"
- onClick={() => onPageChange()}
- role="link"
- tabIndex={-1}
- >
- <BackIcon className="little-spacer-right" />
- {translate('back')}
- </a>
- <div className="overflow-y-auto big-padded">{children}</div>
- </>
- )}
- </div>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.project-info-list > li {
- /* 1px to not cut icons on the left */
- padding-left: 1px;
- padding-bottom: 4px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.project-info-tags {
- position: relative;
-}
-
-.project-info-deleted-profile,
-.project-info-deprecated-rules {
- margin: 4px -6px 4px;
- padding: 3px 6px !important;
- border: 1px solid var(--alertBorderError);
- border-radius: 3px;
- background-color: var(--alertBackgroundError);
-}
-
-.project-info-deleted-profile a,
-.project-info-deprecated-rules a {
- color: var(--veryDarkBlue);
- border-color: darken(var(--lightBlue));
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { getMeasures } from '../../../../../api/measures';
-import { BranchLike } from '../../../../../types/branch-like';
-import { ComponentQualifier } from '../../../../../types/component';
-import { MetricKey } from '../../../../../types/metrics';
-import { Component, Dict, Measure, Metric } from '../../../../../types/types';
-import { CurrentUser, isLoggedIn } from '../../../../../types/users';
-import withCurrentUserContext from '../../../current-user/withCurrentUserContext';
-import withMetricsContext from '../../../metrics/withMetricsContext';
-import ProjectBadges from './badges/ProjectBadges';
-import InfoDrawerPage from './InfoDrawerPage';
-import ProjectNotifications from './notifications/ProjectNotifications';
-import './ProjectInformation.css';
-import { ProjectInformationPages } from './ProjectInformationPages';
-import ProjectInformationRenderer from './ProjectInformationRenderer';
-
-interface Props {
- branchLike?: BranchLike;
- component: Component;
- currentUser: CurrentUser;
- onComponentChange: (changes: {}) => void;
- metrics: Dict<Metric>;
-}
-
-interface State {
- measures?: Measure[];
- page: ProjectInformationPages;
-}
-
-export class ProjectInformation extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = {
- page: ProjectInformationPages.main,
- };
-
- componentDidMount() {
- this.mounted = true;
- this.loadMeasures();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- setPage = (page: ProjectInformationPages = ProjectInformationPages.main) => {
- this.setState({ page });
- };
-
- loadMeasures = () => {
- const {
- component: { key },
- } = this.props;
-
- return getMeasures({
- component: key,
- metricKeys: [MetricKey.ncloc, MetricKey.projects].join(),
- }).then((measures) => {
- if (this.mounted) {
- this.setState({ measures });
- }
- });
- };
-
- render() {
- const { branchLike, component, currentUser, metrics } = this.props;
- const { measures, page } = this.state;
-
- const canConfigureNotifications =
- isLoggedIn(currentUser) && component.qualifier === ComponentQualifier.Project;
- const canUseBadges =
- metrics !== undefined &&
- (component.qualifier === ComponentQualifier.Application ||
- component.qualifier === ComponentQualifier.Project);
-
- return (
- <>
- <ProjectInformationRenderer
- canConfigureNotifications={canConfigureNotifications}
- canUseBadges={canUseBadges}
- component={component}
- branchLike={branchLike}
- measures={measures}
- onComponentChange={this.props.onComponentChange}
- onPageChange={this.setPage}
- />
- {canUseBadges && (
- <InfoDrawerPage
- displayed={page === ProjectInformationPages.badges}
- onPageChange={this.setPage}
- >
- <ProjectBadges branchLike={branchLike} component={component} />
- </InfoDrawerPage>
- )}
- {canConfigureNotifications && (
- <InfoDrawerPage
- displayed={page === ProjectInformationPages.notifications}
- onPageChange={this.setPage}
- >
- <ProjectNotifications component={component} />
- </InfoDrawerPage>
- )}
- </>
- );
- }
-}
-
-export default withCurrentUserContext(withMetricsContext(ProjectInformation));
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-export enum ProjectInformationPages {
- main,
- badges,
- notifications,
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import PrivacyBadgeContainer from '../../../../../components/common/PrivacyBadgeContainer';
-import { ButtonLink } from '../../../../../components/controls/buttons';
-import ModalButton from '../../../../../components/controls/ModalButton';
-import { translate } from '../../../../../helpers/l10n';
-import { BranchLike } from '../../../../../types/branch-like';
-import { ComponentQualifier } from '../../../../../types/component';
-import { Feature } from '../../../../../types/features';
-import { Component, Measure } from '../../../../../types/types';
-import withAvailableFeatures, {
- WithAvailableFeaturesProps,
-} from '../../../available-features/withAvailableFeatures';
-import DrawerLink from './DrawerLink';
-import MetaKey from './meta/MetaKey';
-import MetaLinks from './meta/MetaLinks';
-import MetaQualityGate from './meta/MetaQualityGate';
-import MetaQualityProfiles from './meta/MetaQualityProfiles';
-import MetaSize from './meta/MetaSize';
-import MetaTags from './meta/MetaTags';
-import { ProjectInformationPages } from './ProjectInformationPages';
-import RegulatoryReportModal from './projectRegulatoryReport/RegulatoryReportModal';
-
-export interface ProjectInformationRendererProps extends WithAvailableFeaturesProps {
- canConfigureNotifications: boolean;
- canUseBadges: boolean;
- component: Component;
- branchLike?: BranchLike;
- measures?: Measure[];
- onComponentChange: (changes: {}) => void;
- onPageChange: (page: ProjectInformationPages) => void;
-}
-
-export function ProjectInformationRenderer(props: ProjectInformationRendererProps) {
- const { canConfigureNotifications, canUseBadges, component, measures = [], branchLike } = props;
-
- const heading = React.useRef<HTMLHeadingElement>(null);
- const isApp = component.qualifier === ComponentQualifier.Application;
-
- React.useEffect(() => {
- if (heading.current) {
- // a11y: provide focus to the heading when the Project Information is opened.
- heading.current.focus();
- }
- }, [heading]);
-
- const regulatoryReportFeatureEnabled = props.hasFeature(Feature.RegulatoryReport);
-
- return (
- <>
- <div>
- <h2 className="big-padded bordered-bottom" tabIndex={-1} ref={heading}>
- {translate(isApp ? 'application' : 'project', 'info.title')}
- </h2>
- </div>
-
- <div className="overflow-y-auto">
- <div className="big-padded bordered-bottom">
- <div className="display-flex-center">
- <h3 className="spacer-right">{translate('project.info.description')}</h3>
- {component.visibility && (
- <PrivacyBadgeContainer
- qualifier={component.qualifier}
- visibility={component.visibility}
- />
- )}
- </div>
-
- {component.description && (
- <p className="it__project-description">{component.description}</p>
- )}
-
- <MetaTags component={component} onComponentChange={props.onComponentChange} />
- </div>
-
- <div className="big-padded bordered-bottom it__project-loc-value">
- <MetaSize component={component} measures={measures} />
- </div>
-
- {!isApp &&
- (component.qualityGate ||
- (component.qualityProfiles && component.qualityProfiles.length > 0)) && (
- <div className="big-padded bordered-bottom">
- {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
-
- {component.qualityProfiles && component.qualityProfiles.length > 0 && (
- <MetaQualityProfiles
- headerClassName={component.qualityGate ? 'big-spacer-top' : undefined}
- profiles={component.qualityProfiles}
- />
- )}
- </div>
- )}
-
- {!isApp && <MetaLinks component={component} />}
-
- <div className="big-padded bordered-bottom">
- <MetaKey componentKey={component.key} qualifier={component.qualifier} />
- </div>
-
- <ul>
- {canUseBadges && (
- <li>
- <DrawerLink
- label={translate('overview.badges.get_badge', component.qualifier)}
- onPageChange={props.onPageChange}
- to={ProjectInformationPages.badges}
- />
- </li>
- )}
- {canConfigureNotifications && (
- <li>
- <DrawerLink
- label={translate('project.info.to_notifications')}
- onPageChange={props.onPageChange}
- to={ProjectInformationPages.notifications}
- />
- </li>
- )}
- {component.qualifier === ComponentQualifier.Project && regulatoryReportFeatureEnabled && (
- <li className="big-padded bordered-bottom">
- <ModalButton
- modal={({ onClose }) => (
- <RegulatoryReportModal
- component={component}
- branchLike={branchLike}
- onClose={onClose}
- />
- )}
- >
- {({ onClick }) => (
- <ButtonLink onClick={onClick}>{translate('regulatory_report.page')}</ButtonLink>
- )}
- </ModalButton>
- </li>
- )}
- </ul>
- </div>
- </>
- );
-}
-
-export default withAvailableFeatures(React.memo(ProjectInformationRenderer));
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import classNames from 'classnames';
-import * as React from 'react';
-import { Button } from '../../../../../../components/controls/buttons';
-import { translate } from '../../../../../../helpers/l10n';
-import { BadgeType } from './utils';
-
-interface Props {
- onClick: (type: BadgeType) => void;
- selected: boolean;
- type: BadgeType;
- url: string;
-}
-
-export default class BadgeButton extends React.PureComponent<Props> {
- handleClick = () => {
- this.props.onClick(this.props.type);
- };
-
- render() {
- const { selected, type, url } = this.props;
- const width = type !== BadgeType.measure ? '128px' : undefined;
- return (
- <Button className={classNames('badge-button', { selected })} onClick={this.handleClick}>
- <img alt={translate('overview.badges', type, 'alt')} src={url} width={width} />
- </Button>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import classNames from 'classnames';
-import * as React from 'react';
-import { fetchWebApi } from '../../../../../../api/web-api';
-import Select from '../../../../../../components/controls/Select';
-import { getLocalizedMetricName, translate } from '../../../../../../helpers/l10n';
-import { Dict, Metric } from '../../../../../../types/types';
-import withMetricsContext from '../../../../metrics/withMetricsContext';
-import { BadgeFormats, BadgeOptions, BadgeType } from './utils';
-
-interface Props {
- className?: string;
- metrics: Dict<Metric>;
- options: BadgeOptions;
- type: BadgeType;
- updateOptions: (options: Partial<BadgeOptions>) => void;
-}
-
-interface State {
- badgeMetrics: string[];
-}
-
-export class BadgeParams extends React.PureComponent<Props> {
- mounted = false;
-
- state: State = { badgeMetrics: [] };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchBadgeMetrics();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchBadgeMetrics() {
- fetchWebApi(false).then(
- (webservices) => {
- if (this.mounted) {
- const domain = webservices.find((d) => d.path === 'api/project_badges');
- const ws = domain && domain.actions.find((w) => w.key === 'measure');
- const param = ws && ws.params && ws.params.find((p) => p.key === 'metric');
- if (param && param.possibleValues) {
- this.setState({ badgeMetrics: param.possibleValues });
- }
- }
- },
- () => {}
- );
- }
-
- getColorOptions = () => {
- return ['white', 'black', 'orange'].map((color) => ({
- label: translate('overview.badges.options.colors', color),
- value: color,
- }));
- };
-
- getFormatOptions = () => {
- return ['md', 'url'].map((format) => ({
- label: translate('overview.badges.options.formats', format),
- value: format as BadgeFormats,
- }));
- };
-
- getMetricOptions = () => {
- return this.state.badgeMetrics.map((key) => {
- const metric = this.props.metrics[key];
- return {
- value: key,
- label: metric ? getLocalizedMetricName(metric) : key,
- };
- });
- };
-
- handleFormatChange = ({ value }: { value: BadgeFormats }) => {
- this.props.updateOptions({ format: value });
- };
-
- handleMetricChange = ({ value }: { value: string }) => {
- this.props.updateOptions({ metric: value });
- };
-
- renderBadgeType = (type: BadgeType, options: BadgeOptions) => {
- if (type === BadgeType.measure) {
- const metricOptions = this.getMetricOptions();
- return (
- <>
- <label className="spacer-right" htmlFor="badge-metric">
- {translate('overview.badges.metric')}:
- </label>
- <Select
- className="input-medium it__metric-badge-select"
- inputId="badge-metric"
- isSearchable={false}
- onChange={this.handleMetricChange}
- options={metricOptions}
- value={metricOptions.find((o) => o.value === options.metric)}
- />
- </>
- );
- }
- return null;
- };
-
- render() {
- const { className, options, type } = this.props;
- const formatOptions = this.getFormatOptions();
- return (
- <div className={className}>
- {this.renderBadgeType(type, options)}
-
- <label
- className={classNames('spacer-right', {
- 'spacer-top': type !== BadgeType.qualityGate,
- })}
- htmlFor="badge-format"
- >
- {translate('format')}:
- </label>
- <Select
- className="input-medium"
- inputId="badge-format"
- isSearchable={false}
- onChange={this.handleFormatChange}
- options={formatOptions}
- value={formatOptions.find((o) => o.value === options.format)}
- defaultValue={formatOptions.find((o) => o.value === 'md')}
- />
- </div>
- );
- }
-}
-
-export default withMetricsContext(BadgeParams);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import {
- getProjectBadgesToken,
- renewProjectBadgesToken,
-} from '../../../../../../api/project-badges';
-import CodeSnippet from '../../../../../../components/common/CodeSnippet';
-import { Button } from '../../../../../../components/controls/buttons';
-import { Alert } from '../../../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../../../components/ui/DeferredSpinner';
-import { getBranchLikeQuery } from '../../../../../../helpers/branch-like';
-import { translate } from '../../../../../../helpers/l10n';
-import { BranchLike } from '../../../../../../types/branch-like';
-import { MetricKey } from '../../../../../../types/metrics';
-import { Component } from '../../../../../../types/types';
-import BadgeButton from './BadgeButton';
-import BadgeParams from './BadgeParams';
-import './styles.css';
-import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';
-
-interface Props {
- branchLike?: BranchLike;
- component: Component;
-}
-
-interface State {
- isRenewing: boolean;
- token: string;
- selectedType: BadgeType;
- badgeOptions: BadgeOptions;
-}
-
-export default class ProjectBadges extends React.PureComponent<Props, State> {
- mounted = false;
- headingNodeRef = React.createRef<HTMLHeadingElement>();
- state: State = {
- isRenewing: false,
- token: '',
- selectedType: BadgeType.measure,
- badgeOptions: { metric: MetricKey.alert_status },
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchToken();
- if (this.headingNodeRef.current) {
- this.headingNodeRef.current.focus();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- async fetchToken() {
- const {
- component: { key },
- } = this.props;
- const token = await getProjectBadgesToken(key).catch(() => '');
- if (this.mounted) {
- this.setState({ token });
- }
- }
-
- handleSelectBadge = (selectedType: BadgeType) => {
- this.setState({ selectedType });
- };
-
- handleUpdateOptions = (options: Partial<BadgeOptions>) => {
- this.setState((state) => ({
- badgeOptions: { ...state.badgeOptions, ...options },
- }));
- };
-
- handleRenew = async () => {
- const {
- component: { key },
- } = this.props;
-
- this.setState({ isRenewing: true });
- await renewProjectBadgesToken(key).catch(() => {});
- await this.fetchToken();
- if (this.mounted) {
- this.setState({ isRenewing: false });
- }
- };
-
- render() {
- const {
- branchLike,
- component: { key: project, qualifier, configuration },
- } = this.props;
- const { isRenewing, selectedType, badgeOptions, token } = this.state;
- const fullBadgeOptions = {
- project,
- ...badgeOptions,
- ...getBranchLikeQuery(branchLike),
- };
- const canRenew = configuration?.showSettings;
-
- return (
- <div className="display-flex-column">
- <h3 tabIndex={-1} ref={this.headingNodeRef}>
- {translate('overview.badges.get_badge', qualifier)}
- </h3>
- <p className="big-spacer-bottom">{translate('overview.badges.description', qualifier)}</p>
- <BadgeButton
- onClick={this.handleSelectBadge}
- selected={BadgeType.measure === selectedType}
- type={BadgeType.measure}
- url={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token)}
- />
- <p className="huge-spacer-bottom spacer-top">
- {translate('overview.badges', BadgeType.measure, 'description', qualifier)}
- </p>
- <BadgeButton
- onClick={this.handleSelectBadge}
- selected={BadgeType.qualityGate === selectedType}
- type={BadgeType.qualityGate}
- url={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token)}
- />
- <p className="huge-spacer-bottom spacer-top">
- {translate('overview.badges', BadgeType.qualityGate, 'description', qualifier)}
- </p>
- <BadgeParams
- className="big-spacer-bottom display-flex-column"
- options={badgeOptions}
- type={selectedType}
- updateOptions={this.handleUpdateOptions}
- />
- {isRenewing ? (
- <div className="spacer-top spacer-bottom display-flex-row display-flex-justify-center">
- <DeferredSpinner className="spacer-top spacer-bottom" loading={isRenewing} />
- </div>
- ) : (
- <CodeSnippet isOneLine snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} />
- )}
-
- <Alert variant="warning">
- <p>
- {translate('overview.badges.leak_warning')}{' '}
- {canRenew && translate('overview.badges.renew.description')}
- </p>
- {canRenew && (
- <Button
- disabled={isRenewing}
- className="spacer-top it__project-info-renew-badge"
- onClick={this.handleRenew}
- >
- {translate('overview.badges.renew')}
- </Button>
- )}
- </Alert>
- </div>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import selectEvent from 'react-select-event';
-import { getProjectBadgesToken } from '../../../../../../../api/project-badges';
-import { mockBranch } from '../../../../../../../helpers/mocks/branch-like';
-import { mockComponent } from '../../../../../../../helpers/mocks/component';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { Location } from '../../../../../../../helpers/urls';
-import { ComponentQualifier } from '../../../../../../../types/component';
-import { MetricKey } from '../../../../../../../types/metrics';
-import ProjectBadges from '../ProjectBadges';
-import { BadgeType } from '../utils';
-
-jest.mock('../../../../../../../helpers/urls', () => ({
- getHostUrl: () => 'host',
- getPathUrlAsString: (l: Location) => l.pathname,
- getProjectUrl: () => ({ pathname: '/dashboard' } as Location),
-}));
-
-jest.mock('../../../../../../../api/project-badges', () => ({
- getProjectBadgesToken: jest.fn().mockResolvedValue('foo'),
- renewProjectBadgesToken: jest.fn().mockResolvedValue({}),
-}));
-
-jest.mock('../../../../../../../api/web-api', () => ({
- fetchWebApi: () =>
- Promise.resolve([
- {
- path: 'api/project_badges',
- actions: [
- {
- key: 'measure',
- // eslint-disable-next-line local-rules/use-metrickey-enum
- params: [{ key: 'metric', possibleValues: ['alert_status', 'coverage'] }],
- },
- ],
- },
- ]),
-}));
-
-it('should renew token', async () => {
- const user = userEvent.setup();
- jest.mocked(getProjectBadgesToken).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar');
- renderProjectBadges({
- component: mockComponent({ configuration: { showSettings: true } }),
- });
-
- expect(
- await screen.findByText(`overview.badges.get_badge.${ComponentQualifier.Project}`)
- ).toHaveFocus();
-
- expect(screen.getByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)).toHaveAttribute(
- 'src',
- 'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo'
- );
-
- expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
- 'src',
- 'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo'
- );
-
- await user.click(screen.getByText('overview.badges.renew'));
-
- expect(
- await screen.findByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)
- ).toHaveAttribute(
- 'src',
- 'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=bar'
- );
-
- expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
- 'src',
- 'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=bar'
- );
-});
-
-it('should update params', async () => {
- renderProjectBadges({
- component: mockComponent({ configuration: { showSettings: true } }),
- });
-
- expect(
- await screen.findByText(
- '[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo)](/dashboard)'
- )
- ).toBeInTheDocument();
-
- await selectEvent.select(screen.getByLabelText('format:'), [
- 'overview.badges.options.formats.url',
- ]);
-
- expect(
- screen.getByText(
- 'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo'
- )
- ).toBeInTheDocument();
-
- await selectEvent.select(screen.getByLabelText('overview.badges.metric:'), MetricKey.coverage);
-
- expect(
- screen.getByText(
- `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.coverage}&token=foo`
- )
- ).toBeInTheDocument();
-});
-
-function renderProjectBadges(props: Partial<ProjectBadges['props']> = {}) {
- return renderComponent(
- <ProjectBadges
- branchLike={mockBranch()}
- component={mockComponent({ key: 'foo', qualifier: ComponentQualifier.Project })}
- {...props}
- />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { Location } from '../../../../../../../components/hoc/withRouter';
-import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from '../utils';
-
-jest.mock('../../../../../../../helpers/urls', () => ({
- ...jest.requireActual('../../../../../../../helpers/urls'),
- getHostUrl: () => 'host',
- getPathUrlAsString: (o: Location) => `host${o.pathname}${o.search}`,
-}));
-
-const options: BadgeOptions = {
- branch: 'master',
- metric: 'alert_status',
- project: 'foo',
-};
-
-describe('#getBadgeUrl', () => {
- it('should generate correct quality gate badge links', () => {
- expect(getBadgeUrl(BadgeType.qualityGate, options, 'foo')).toBe(
- 'host/api/project_badges/quality_gate?branch=master&project=foo&token=foo'
- );
- });
-
- it('should generate correct measures badge links', () => {
- expect(getBadgeUrl(BadgeType.measure, options, 'foo')).toBe(
- 'host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo'
- );
- });
-
- it('should ignore undefined parameters', () => {
- expect(getBadgeUrl(BadgeType.measure, { metric: 'alert_status' }, 'foo')).toBe(
- 'host/api/project_badges/measure?metric=alert_status&token=foo'
- );
- });
-
- it('should force metric parameters', () => {
- expect(getBadgeUrl(BadgeType.measure, {}, 'foo')).toBe(
- 'host/api/project_badges/measure?metric=alert_status&token=foo'
- );
- });
-});
-
-describe('#getBadgeSnippet', () => {
- it('should generate a correct markdown image', () => {
- expect(getBadgeSnippet(BadgeType.measure, { ...options, format: 'md' }, 'foo')).toBe(
- '[![alert_status](host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo)](host/dashboard?id=foo&branch=master)'
- );
- });
-});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.badges-list {
- display: flex;
- justify-content: space-around;
- justify-content: space-evenly;
- flex-wrap: nowrap;
-}
-
-.button.badge-button {
- display: flex;
- justify-content: center;
- padding: var(--gridSize);
- min-width: 146px;
- height: 116px;
- background-color: var(--barBackgroundColor);
- border: solid 1px var(--barBorderColor);
- border-radius: 3px;
- transition: all 0.3s ease;
-}
-
-.button.badge-button:hover,
-.button.badge-button:focus,
-.button.badge-button:active {
- background-color: var(--barBackgroundColor);
- border-color: var(--blue);
-}
-
-.button.badge-button.selected {
- background-color: var(--lightBlue);
- border-color: var(--darkBlue);
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { getLocalizedMetricName } from '../../../../../../helpers/l10n';
-import { omitNil } from '../../../../../../helpers/request';
-import { getHostUrl, getPathUrlAsString, getProjectUrl } from '../../../../../../helpers/urls';
-
-export type BadgeColors = 'white' | 'black' | 'orange';
-export type BadgeFormats = 'md' | 'url';
-
-export interface BadgeOptions {
- branch?: string;
- format?: BadgeFormats;
- project?: string;
- metric?: string;
- pullRequest?: string;
-}
-
-export enum BadgeType {
- measure = 'measure',
- qualityGate = 'quality_gate',
-}
-
-export function getBadgeSnippet(type: BadgeType, options: BadgeOptions, token: string) {
- const url = getBadgeUrl(type, options, token);
- const { branch, format = 'md', metric = 'alert_status', project } = options;
-
- if (format === 'url') {
- return url;
- }
-
- let label;
- let projectUrl;
-
- switch (type) {
- case BadgeType.measure:
- label = getLocalizedMetricName({ key: metric });
- break;
- case BadgeType.qualityGate:
- default:
- label = 'Quality gate';
- break;
- }
-
- if (project) {
- projectUrl = getPathUrlAsString(getProjectUrl(project, branch), false);
- }
-
- const mdImage = `![${label}](${url})`;
- return projectUrl ? `[${mdImage}](${projectUrl})` : mdImage;
-}
-
-export function getBadgeUrl(
- type: BadgeType,
- { branch, project, metric = 'alert_status', pullRequest }: BadgeOptions,
- token: string
-) {
- switch (type) {
- case BadgeType.qualityGate:
- return `${getHostUrl()}/api/project_badges/quality_gate?${new URLSearchParams(
- omitNil({ branch, project, pullRequest, token })
- ).toString()}`;
- case BadgeType.measure:
- default:
- return `${getHostUrl()}/api/project_badges/measure?${new URLSearchParams(
- omitNil({ branch, project, metric, pullRequest, token })
- ).toString()}`;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { ClipboardButton } from '../../../../../../components/controls/clipboard';
-import { translate } from '../../../../../../helpers/l10n';
-
-export interface MetaKeyProps {
- componentKey: string;
- qualifier: string;
-}
-
-export default function MetaKey({ componentKey, qualifier }: MetaKeyProps) {
- return (
- <>
- <h3 id="project-key">{translate('overview.project_key', qualifier)}</h3>
- <div className="display-flex-center">
- <input
- className="overview-key"
- aria-labelledby="project-key"
- readOnly
- type="text"
- value={componentKey}
- />
- <ClipboardButton
- aria-label={translate('overview.project_key.click_to_copy')}
- className="little-spacer-left"
- copyValue={componentKey}
- />
- </div>
- </>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { ClearButton } from '../../../../../../components/controls/buttons';
-import ProjectLinkIcon from '../../../../../../components/icons/ProjectLinkIcon';
-import { getLinkName } from '../../../../../../helpers/projectLinks';
-import { ProjectLink } from '../../../../../../types/types';
-import isValidUri from '../../../../../utils/isValidUri';
-
-interface Props {
- iconOnly?: boolean;
- link: ProjectLink;
-}
-
-interface State {
- expanded: boolean;
-}
-
-export default class MetaLink extends React.PureComponent<Props, State> {
- state = { expanded: false };
-
- handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
- event.preventDefault();
- this.setState(({ expanded }) => ({ expanded: !expanded }));
- };
-
- handleCollapse = () => {
- this.setState({ expanded: false });
- };
-
- handleSelect = (event: React.MouseEvent<HTMLInputElement>) => {
- event.currentTarget.select();
- };
-
- render() {
- const { iconOnly, link } = this.props;
- const linkTitle = getLinkName(link);
- const isValid = isValidUri(link.url);
- return (
- <li>
- <a
- className="link-no-underline"
- href={isValid ? link.url : undefined}
- onClick={isValid ? undefined : this.handleClick}
- rel="nofollow noreferrer noopener"
- target="_blank"
- title={linkTitle}
- >
- <ProjectLinkIcon className="little-spacer-right" type={link.type} />
- {!iconOnly && linkTitle}
- </a>
- {this.state.expanded && (
- <div className="little-spacer-top display-flex-center">
- <input
- className="overview-key width-80"
- onClick={this.handleSelect}
- readOnly
- type="text"
- value={link.url}
- />
- <ClearButton className="little-spacer-left" onClick={this.handleCollapse} />
- </div>
- )}
- </li>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { getProjectLinks } from '../../../../../../api/projectLinks';
-import { translate } from '../../../../../../helpers/l10n';
-import { orderLinks } from '../../../../../../helpers/projectLinks';
-import { LightComponent, ProjectLink } from '../../../../../../types/types';
-import MetaLink from './MetaLink';
-
-interface Props {
- component: LightComponent;
-}
-
-interface State {
- links?: ProjectLink[];
-}
-
-export default class MetaLinks extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = {};
-
- componentDidMount() {
- this.mounted = true;
- this.loadLinks();
- }
-
- componentDidUpdate(prevProps: Props) {
- if (prevProps.component.key !== this.props.component.key) {
- this.loadLinks();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- loadLinks = () =>
- getProjectLinks(this.props.component.key).then(
- (links) => {
- if (this.mounted) {
- this.setState({ links });
- }
- },
- () => {}
- );
-
- render() {
- const { links } = this.state;
-
- if (!links || links.length === 0) {
- return null;
- }
-
- const orderedLinks = orderLinks(links);
-
- return (
- <div className="big-padded bordered-bottom">
- <h3>{translate('overview.external_links')}</h3>
- <ul className="project-info-list">
- {orderedLinks.map((link) => (
- <MetaLink key={link.id} link={link} />
- ))}
- </ul>
- </div>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import Link from '../../../../../../components/common/Link';
-import { translate } from '../../../../../../helpers/l10n';
-import { getQualityGateUrl } from '../../../../../../helpers/urls';
-
-interface Props {
- qualityGate: { isDefault?: boolean; name: string };
-}
-
-export default function MetaQualityGate({ qualityGate }: Props) {
- return (
- <>
- <h3>{translate('project.info.quality_gate')}</h3>
-
- <ul className="project-info-list">
- <li>
- {qualityGate.isDefault && (
- <span className="note spacer-right">({translate('default')})</span>
- )}
- <Link to={getQualityGateUrl(qualityGate.name)}>{qualityGate.name}</Link>
- </li>
- </ul>
- </>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { searchRules } from '../../../../../../api/rules';
-import Link from '../../../../../../components/common/Link';
-import Tooltip from '../../../../../../components/controls/Tooltip';
-import { translate, translateWithParameters } from '../../../../../../helpers/l10n';
-import { getQualityProfileUrl } from '../../../../../../helpers/urls';
-import { Languages } from '../../../../../../types/languages';
-import { ComponentQualityProfile, Dict } from '../../../../../../types/types';
-import withLanguagesContext from '../../../../languages/withLanguagesContext';
-
-interface Props {
- headerClassName?: string;
- languages: Languages;
- profiles: ComponentQualityProfile[];
-}
-
-interface State {
- deprecatedByKey: Dict<number>;
-}
-
-export class MetaQualityProfiles extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { deprecatedByKey: {} };
-
- componentDidMount() {
- this.mounted = true;
- this.loadDeprecatedRules();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- loadDeprecatedRules() {
- const existingProfiles = this.props.profiles.filter((p) => !p.deleted);
- const requests = existingProfiles.map((profile) =>
- this.loadDeprecatedRulesForProfile(profile.key)
- );
- Promise.all(requests).then(
- (responses) => {
- if (this.mounted) {
- const deprecatedByKey: Dict<number> = {};
- responses.forEach((count, i) => {
- const profileKey = existingProfiles[i].key;
- deprecatedByKey[profileKey] = count;
- });
- this.setState({ deprecatedByKey });
- }
- },
- () => {}
- );
- }
-
- loadDeprecatedRulesForProfile(profileKey: string) {
- const data = {
- activation: 'true',
- ps: 1,
- qprofile: profileKey,
- statuses: 'DEPRECATED',
- };
- return searchRules(data).then((r) => r.paging.total);
- }
-
- getDeprecatedRulesCount(profile: { key: string }) {
- const count = this.state.deprecatedByKey[profile.key];
- return count || 0;
- }
-
- renderProfile(profile: ComponentQualityProfile) {
- const languageFromStore = this.props.languages[profile.language];
- const languageName = languageFromStore ? languageFromStore.name : profile.language;
-
- const inner = (
- <div className="text-ellipsis">
- <span className="spacer-right">({languageName})</span>
- {profile.deleted ? (
- profile.name
- ) : (
- <Link to={getQualityProfileUrl(profile.name, profile.language)}>
- <span
- aria-label={translateWithParameters(
- 'overview.link_to_x_profile_y',
- languageName,
- profile.name
- )}
- >
- {profile.name}
- </span>
- </Link>
- )}
- </div>
- );
-
- if (profile.deleted) {
- const tooltip = translateWithParameters('overview.deleted_profile', profile.name);
- return (
- <Tooltip key={profile.key} overlay={tooltip}>
- <li className="project-info-deleted-profile">{inner}</li>
- </Tooltip>
- );
- }
-
- const count = this.getDeprecatedRulesCount(profile);
-
- if (count > 0) {
- const tooltip = translateWithParameters('overview.deprecated_profile', count);
- return (
- <Tooltip key={profile.key} overlay={tooltip}>
- <li className="project-info-deprecated-rules">{inner}</li>
- </Tooltip>
- );
- }
-
- return <li key={profile.key}>{inner}</li>;
- }
-
- render() {
- const { headerClassName, profiles } = this.props;
-
- return (
- <>
- <h3 className={headerClassName}>{translate('overview.quality_profiles')}</h3>
-
- <ul className="project-info-list">
- {profiles.map((profile) => this.renderProfile(profile))}
- </ul>
- </>
- );
- }
-}
-
-export default withLanguagesContext(MetaQualityProfiles);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import DrilldownLink from '../../../../../../components/shared/DrilldownLink';
-import SizeRating from '../../../../../../components/ui/SizeRating';
-import { translate, translateWithParameters } from '../../../../../../helpers/l10n';
-import { formatMeasure, localizeMetric } from '../../../../../../helpers/measures';
-import { ComponentQualifier } from '../../../../../../types/component';
-import { MetricKey } from '../../../../../../types/metrics';
-import { Component, Measure } from '../../../../../../types/types';
-
-export interface MetaSizeProps {
- component: Component;
- measures: Measure[];
-}
-
-export default function MetaSize({ component, measures }: MetaSizeProps) {
- const isApp = component.qualifier === ComponentQualifier.Application;
- const ncloc = measures.find((measure) => measure.metric === MetricKey.ncloc);
- const projects = isApp
- ? measures.find((measure) => measure.metric === MetricKey.projects)
- : undefined;
-
- return (
- <>
- <div className="display-flex-row display-inline-flex-baseline">
- <h3>{localizeMetric(MetricKey.ncloc)}</h3>
- <span className="spacer-left small">({translate('project.info.main_branch')})</span>
- </div>
- <div className="display-flex-center">
- {ncloc && ncloc.value ? (
- <>
- <DrilldownLink className="huge" component={component.key} metric={MetricKey.ncloc}>
- <span
- aria-label={translateWithParameters(
- 'project.info.see_more_info_on_x_locs',
- ncloc.value
- )}
- >
- {formatMeasure(ncloc.value, 'SHORT_INT')}
- </span>
- </DrilldownLink>
-
- <span className="spacer-left">
- <SizeRating value={Number(ncloc.value)} />
- </span>
- </>
- ) : (
- <span>0</span>
- )}
-
- {isApp && (
- <span className="huge-spacer-left display-inline-flex-center">
- {projects ? (
- <DrilldownLink component={component.key} metric={MetricKey.projects}>
- <span className="big">{formatMeasure(projects.value, 'SHORT_INT')}</span>
- </DrilldownLink>
- ) : (
- <span className="big">0</span>
- )}
- <span className="little-spacer-left text-muted">
- {translate('metric.projects.name')}
- </span>
- </span>
- )}
- </div>
- </>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { setApplicationTags, setProjectTags } from '../../../../../../api/components';
-import { ButtonLink } from '../../../../../../components/controls/buttons';
-import Dropdown from '../../../../../../components/controls/Dropdown';
-import TagsList from '../../../../../../components/tags/TagsList';
-import { PopupPlacement } from '../../../../../../components/ui/popups';
-import { translate } from '../../../../../../helpers/l10n';
-import { ComponentQualifier } from '../../../../../../types/component';
-import { Component } from '../../../../../../types/types';
-import MetaTagsSelector from './MetaTagsSelector';
-
-interface Props {
- component: Component;
- onComponentChange: (changes: {}) => void;
-}
-
-export default class MetaTags extends React.PureComponent<Props> {
- canUpdateTags = () => {
- const { configuration } = this.props.component;
- return configuration && configuration.showSettings;
- };
-
- setTags = (values: string[]) => {
- const { component } = this.props;
-
- if (component.qualifier === ComponentQualifier.Application) {
- return setApplicationTags({
- application: component.key,
- tags: values.join(','),
- });
- }
-
- return setProjectTags({
- project: component.key,
- tags: values.join(','),
- });
- };
-
- handleSetProjectTags = (values: string[]) => {
- this.setTags(values).then(
- () => this.props.onComponentChange({ tags: values }),
- () => {}
- );
- };
-
- render() {
- const tags = this.props.component.tags || [];
-
- return this.canUpdateTags() ? (
- <div className="big-spacer-top project-info-tags">
- <Dropdown
- closeOnClick={false}
- closeOnClickOutside
- overlay={
- <MetaTagsSelector selectedTags={tags} setProjectTags={this.handleSetProjectTags} />
- }
- overlayPlacement={PopupPlacement.BottomLeft}
- >
- <ButtonLink stopPropagation>
- <TagsList allowUpdate tags={tags.length ? tags : [translate('no_tags')]} />
- </ButtonLink>
- </Dropdown>
- </div>
- ) : (
- <div className="big-spacer-top project-info-tags">
- <TagsList
- allowUpdate={false}
- className="note"
- tags={tags.length ? tags : [translate('no_tags')]}
- />
- </div>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { difference, without } from 'lodash';
-import * as React from 'react';
-import { searchProjectTags } from '../../../../../../api/components';
-import TagsSelector from '../../../../../../components/tags/TagsSelector';
-
-interface Props {
- selectedTags: string[];
- setProjectTags: (tags: string[]) => void;
-}
-
-interface State {
- searchResult: string[];
-}
-
-const LIST_SIZE = 10;
-
-export default class MetaTagsSelector extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { searchResult: [] };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- onSearch = (query: string) => {
- return searchProjectTags({
- q: query,
- ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
- }).then(
- ({ tags }) => {
- if (this.mounted) {
- this.setState({ searchResult: tags });
- }
- },
- () => {}
- );
- };
-
- onSelect = (tag: string) => {
- this.props.setProjectTags([...this.props.selectedTags, tag]);
- };
-
- onUnselect = (tag: string) => {
- this.props.setProjectTags(without(this.props.selectedTags, tag));
- };
-
- render() {
- const availableTags = difference(this.state.searchResult, this.props.selectedTags);
- return (
- <TagsSelector
- listSize={LIST_SIZE}
- onSearch={this.onSearch}
- onSelect={this.onSelect}
- onUnselect={this.onUnselect}
- selectedTags={this.props.selectedTags}
- tags={availableTags}
- />
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { screen } from '@testing-library/react';
-import * as React from 'react';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { ComponentQualifier } from '../../../../../../../types/component';
-import MetaKey, { MetaKeyProps } from '../MetaKey';
-
-it('should render correctly', () => {
- renderMetaKey();
- expect(
- screen.getByLabelText(`overview.project_key.${ComponentQualifier.Project}`)
- ).toBeInTheDocument();
- expect(
- screen.getByRole('button', { name: 'overview.project_key.click_to_copy' })
- ).toBeInTheDocument();
-});
-
-function renderMetaKey(props: Partial<MetaKeyProps> = {}) {
- return renderComponent(
- <MetaKey componentKey="foo" qualifier={ComponentQualifier.Project} {...props} />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { screen } from '@testing-library/react';
-import * as React from 'react';
-import { searchRules } from '../../../../../../../api/rules';
-import {
- mockLanguage,
- mockPaging,
- mockQualityProfile,
-} from '../../../../../../../helpers/testMocks';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { SearchRulesResponse } from '../../../../../../../types/coding-rules';
-import { Dict } from '../../../../../../../types/types';
-import { MetaQualityProfiles } from '../MetaQualityProfiles';
-
-jest.mock('../../../../../../../api/rules', () => {
- return {
- searchRules: jest.fn().mockResolvedValue({
- total: 10,
- }),
- };
-});
-
-it('should render correctly', async () => {
- const totals: Dict<number> = {
- js: 0,
- ts: 10,
- css: 0,
- };
- jest
- .mocked(searchRules)
- .mockImplementation(({ qprofile }: { qprofile: string }): Promise<SearchRulesResponse> => {
- return Promise.resolve({
- rules: [],
- paging: mockPaging({
- total: totals[qprofile],
- }),
- });
- });
-
- renderMetaQualityprofiles();
-
- expect(await screen.findByText('overview.deleted_profile.javascript')).toBeInTheDocument();
- expect(screen.getByText('overview.deprecated_profile.10')).toBeInTheDocument();
-});
-
-function renderMetaQualityprofiles(overrides: Partial<MetaQualityProfiles['props']> = {}) {
- return renderComponent(
- <MetaQualityProfiles
- languages={{ css: mockLanguage() }}
- profiles={[
- { ...mockQualityProfile({ key: 'js', name: 'javascript' }), deleted: true },
- { ...mockQualityProfile({ key: 'ts', name: 'typescript' }), deleted: false },
- {
- ...mockQualityProfile({
- key: 'css',
- name: 'style',
- language: 'css',
- languageName: 'CSS',
- }),
- deleted: false,
- },
- ]}
- {...overrides}
- />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import {
- searchProjectTags,
- setApplicationTags,
- setProjectTags,
-} from '../../../../../../../api/components';
-import { mockComponent } from '../../../../../../../helpers/mocks/component';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { ComponentQualifier } from '../../../../../../../types/component';
-import MetaTags from '../MetaTags';
-
-jest.mock('../../../../../../../api/components', () => ({
- setApplicationTags: jest.fn().mockResolvedValue(true),
- setProjectTags: jest.fn().mockResolvedValue(true),
- searchProjectTags: jest.fn(),
-}));
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-it('should render without tags and admin rights', async () => {
- renderMetaTags();
-
- expect(await screen.findByText('no_tags')).toBeInTheDocument();
- expect(screen.queryByRole('button')).not.toBeInTheDocument();
-});
-
-it('should allow to edit tags for a project', async () => {
- const user = userEvent.setup();
- jest.mocked(searchProjectTags).mockResolvedValue({ tags: ['best', 'useless'] });
-
- const onComponentChange = jest.fn();
- const component = mockComponent({
- key: 'my-second-project',
- tags: ['foo', 'bar'],
- configuration: {
- showSettings: true,
- },
- name: 'MySecondProject',
- });
-
- renderMetaTags({ component, onComponentChange });
-
- expect(await screen.findByText('foo, bar')).toBeInTheDocument();
- expect(screen.getByRole('button')).toBeInTheDocument();
-
- await user.click(screen.getByRole('button', { name: 'tags_list_x.foo, bar' }));
-
- expect(await screen.findByText('best')).toBeInTheDocument();
-
- await user.click(screen.getByText('best'));
- expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo', 'bar', 'best'] });
-
- onComponentChange.mockClear();
-
- /*
- * Since we're not actually updating the tags, we're back to having the foo, bar only
- */
- await user.click(screen.getByText('bar'));
- expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo'] });
-
- expect(setProjectTags).toHaveBeenCalled();
- expect(setApplicationTags).not.toHaveBeenCalled();
-});
-
-it('should set tags for an app', async () => {
- const user = userEvent.setup();
-
- renderMetaTags({
- component: mockComponent({
- configuration: {
- showSettings: true,
- },
- qualifier: ComponentQualifier.Application,
- }),
- });
-
- await user.click(screen.getByRole('button', { name: 'tags_list_x.no_tags' }));
-
- await user.click(screen.getByText('best'));
-
- expect(setProjectTags).not.toHaveBeenCalled();
- expect(setApplicationTags).toHaveBeenCalled();
-});
-
-function renderMetaTags(overrides: Partial<MetaTags['props']> = {}) {
- const component = mockComponent({
- configuration: {
- showSettings: false,
- },
- });
-
- return renderComponent(
- <MetaTags component={component} onComponentChange={jest.fn()} {...overrides} />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import NotificationsList from '../../../../../../apps/account/notifications/NotificationsList';
-import {
- withNotifications,
- WithNotificationsProps,
-} from '../../../../../../components/hoc/withNotifications';
-import { Alert } from '../../../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../../../components/ui/DeferredSpinner';
-import { translate } from '../../../../../../helpers/l10n';
-import { Component } from '../../../../../../types/types';
-
-interface Props {
- component: Component;
-}
-
-export function ProjectNotifications(props: WithNotificationsProps & Props) {
- const { channels, component, loading, notifications, perProjectTypes } = props;
- const heading = React.useRef<HTMLHeadingElement>(null);
-
- React.useEffect(() => {
- if (heading.current) {
- // a11y: provide focus to the heading when the info drawer page is opened.
- heading.current.focus();
- }
- }, [heading]);
-
- const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => {
- props.addNotification({ project: component.key, channel, type });
- };
-
- const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => {
- props.removeNotification({
- project: component.key,
- channel,
- type,
- });
- };
-
- const getCheckboxId = (type: string, channel: string) => {
- return `project-notification-${component.key}-${type}-${channel}`;
- };
-
- const projectNotifications = notifications.filter(
- (n) => n.project && n.project === component.key
- );
-
- return (
- <>
- <h3 tabIndex={-1} ref={heading}>
- {translate('project.info.notifications')}
- </h3>
-
- <Alert className="spacer-top" variant="info">
- {translate('notification.dispatcher.information')}
- </Alert>
-
- <DeferredSpinner loading={loading}>
- <table className="data zebra notifications-table">
- <thead>
- <tr>
- <th aria-label={translate('project')} />
- {channels.map((channel) => (
- <th className="text-center" key={channel}>
- <h4>{translate('notification.channel', channel)}</h4>
- </th>
- ))}
- </tr>
- </thead>
-
- <NotificationsList
- channels={channels}
- checkboxId={getCheckboxId}
- notifications={projectNotifications}
- onAdd={handleAddNotification}
- onRemove={handleRemoveNotification}
- project
- types={perProjectTypes}
- />
- </table>
- </DeferredSpinner>
- </>
- );
-}
-
-export default withNotifications(ProjectNotifications);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import { getNotifications } from '../../../../../../../api/notifications';
-import { mockComponent } from '../../../../../../../helpers/mocks/component';
-import { mockNotification } from '../../../../../../../helpers/testMocks';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import {
- NotificationGlobalType,
- NotificationProjectType,
-} from '../../../../../../../types/notifications';
-import ProjectNotifications from '../ProjectNotifications';
-
-jest.mock('../../../../../../../api/notifications', () => ({
- addNotification: jest.fn().mockResolvedValue(undefined),
- removeNotification: jest.fn().mockResolvedValue(undefined),
- getNotifications: jest.fn(),
-}));
-
-beforeAll(() => {
- jest.mocked(getNotifications).mockResolvedValue({
- channels: ['channel1'],
- globalTypes: [NotificationGlobalType.MyNewIssues],
- notifications: [
- mockNotification({}),
- mockNotification({ type: NotificationProjectType.NewAlerts }),
- ],
- perProjectTypes: [NotificationProjectType.NewAlerts, NotificationProjectType.NewIssues],
- });
-});
-
-it('should render correctly', async () => {
- const user = userEvent.setup();
- renderProjectNotifications();
-
- expect(await screen.findByText('notification.channel.channel1')).toBeInTheDocument();
- expect(
- screen.getByLabelText(
- 'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
- )
- ).toBeChecked();
-
- expect(
- screen.getByLabelText(
- 'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
- )
- ).not.toBeChecked();
-
- // Toggle New Alerts
- await user.click(
- screen.getByLabelText(
- 'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
- )
- );
-
- expect(
- screen.getByLabelText(
- 'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
- )
- ).not.toBeChecked();
-
- // Toggle New Issues
- await user.click(
- screen.getByLabelText(
- 'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
- )
- );
-
- expect(
- screen.getByLabelText(
- 'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
- )
- ).toBeChecked();
-});
-
-function renderProjectNotifications() {
- return renderComponent(
- <ProjectNotifications component={mockComponent({ key: 'foo', name: 'Foo' })} />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import classNames from 'classnames';
-import { orderBy } from 'lodash';
-import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { getBranches } from '../../../../../../api/branches';
-import { getRegulatoryReportUrl } from '../../../../../../api/regulatory-report';
-import DocLink from '../../../../../../components/common/DocLink';
-import Select, { LabelValueSelectOption } from '../../../../../../components/controls/Select';
-import { ButtonLink } from '../../../../../../components/controls/buttons';
-import { Alert } from '../../../../../../components/ui/Alert';
-import {
- getBranchLikeDisplayName,
- getBranchLikeKey,
- isMainBranch,
-} from '../../../../../../helpers/branch-like';
-import { translate } from '../../../../../../helpers/l10n';
-import { BranchLike } from '../../../../../../types/branch-like';
-import { Component } from '../../../../../../types/types';
-
-interface Props {
- component: Pick<Component, 'key' | 'name'>;
- branchLike?: BranchLike;
- onClose: () => void;
-}
-
-interface State {
- downloadStarted: boolean;
- selectedBranch: string;
- branchOptions: LabelValueSelectOption[];
-}
-
-export default class RegulatoryReport extends React.PureComponent<Props, State> {
- constructor(props: Props) {
- super(props);
- this.state = {
- downloadStarted: false,
- selectedBranch: '',
- branchOptions: [],
- };
- }
-
- componentDidMount() {
- const { component, branchLike } = this.props;
- getBranches(component.key)
- .then((data) => {
- const availableBranches = data.filter(
- (br) => br.analysisDate && (isMainBranch(br) || br.excludedFromPurge)
- );
- const mainBranch = availableBranches.find(isMainBranch);
- const otherBranchSorted = orderBy(
- availableBranches.filter((b) => !isMainBranch(b)),
- (b) => b.name
- );
- const sortedBranch = mainBranch ? [mainBranch, ...otherBranchSorted] : otherBranchSorted;
- const options = sortedBranch.map((br) => {
- return {
- value: getBranchLikeDisplayName(br),
- label: getBranchLikeDisplayName(br),
- };
- });
-
- let selectedBranch = '';
- if (
- branchLike &&
- availableBranches.find((br) => getBranchLikeKey(br) === getBranchLikeKey(branchLike))
- ) {
- selectedBranch = getBranchLikeDisplayName(branchLike);
- } else if (mainBranch) {
- selectedBranch = getBranchLikeDisplayName(mainBranch);
- }
- this.setState({ selectedBranch, branchOptions: options });
- })
- .catch(() => {
- this.setState({ branchOptions: [] });
- });
- }
-
- onBranchSelect = (newOption: LabelValueSelectOption) => {
- this.setState({ selectedBranch: newOption.value, downloadStarted: false });
- };
-
- render() {
- const { component, onClose } = this.props;
- const { downloadStarted, selectedBranch, branchOptions } = this.state;
- const isDownloadButtonDisabled = downloadStarted || !selectedBranch;
-
- return (
- <>
- <div className="modal-head">
- <h2>{translate('regulatory_report.page')}</h2>
- </div>
- <div className="modal-body">
- <p>{translate('regulatory_report.description1')}</p>
- <div className="markdown">
- <ul>
- <li>{translate('regulatory_report.bullet_point1')}</li>
- <li>{translate('regulatory_report.bullet_point2')}</li>
- <li>{translate('regulatory_report.bullet_point3')}</li>
- </ul>
- </div>
- <p>{translate('regulatory_report.description2')}</p>
- {branchOptions.length > 0 ? (
- <>
- <div className="modal-field big-spacer-top">
- <label htmlFor="regulatory-report-branch-select">
- {translate('regulatory_page.select_branch')}
- </label>
- <Select
- className="width-100"
- inputId="regulatory-report-branch-select"
- id="regulatory-report-branch-select-input"
- onChange={this.onBranchSelect}
- options={branchOptions}
- value={branchOptions.find((o) => o.value === selectedBranch)}
- />
- </div>
- <Alert variant="info">
- <div>
- {translate('regulatory_page.available_branches_info.only_keep_when_inactive')}
- </div>
- <div>
- <FormattedMessage
- id="regulatory_page.available_branches_info.more_info"
- defaultMessage={translate('regulatory_page.available_branches_info.more_info')}
- values={{
- doc_link: (
- <DocLink to="/analyzing-source-code/branches/branch-analysis/#inactive-branches">
- {translate('regulatory_page.available_branches_info.more_info.doc_link')}
- </DocLink>
- ),
- }}
- />
- </div>
- </Alert>
- </>
- ) : (
- <div className="big-spacer-top">
- <Alert variant="warning">
- <div>{translate('regulatory_page.no_available_branch')}</div>
- </Alert>
- </div>
- )}
- <div className="modal-field big-spacer-top">
- {downloadStarted && (
- <div>
- <p>{translate('regulatory_page.download_start.sentence')}</p>
- </div>
- )}
- </div>
- </div>
- <div className="modal-foot">
- <a
- className={classNames('button button-primary big-spacer-right', {
- disabled: isDownloadButtonDisabled,
- })}
- download={[component.name, selectedBranch, 'regulatory report.zip']
- .filter((s) => !!s)
- .join(' - ')}
- onClick={() => this.setState({ downloadStarted: true })}
- href={getRegulatoryReportUrl(component.key, selectedBranch)}
- target="_blank"
- rel="noopener noreferrer"
- aria-disabled={isDownloadButtonDisabled}
- >
- {translate('download_verb')}
- </a>
- <ButtonLink onClick={onClose}>{translate('close')}</ButtonLink>
- </div>
- </>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { translate } from '../../../../../../helpers/l10n';
-import { Component } from '../../../../../../types/types';
-import Modal from '../../../../../../components/controls/Modal';
-import RegulatoryReport from './RegulatoryReport';
-import ClickEventBoundary from '../../../../../../components/controls/ClickEventBoundary';
-import { BranchLike } from '../../../../../../types/branch-like';
-
-interface Props {
- component: Component;
- branchLike?: BranchLike;
- onClose: () => void;
-}
-
-export default function RegulatoryReportModal(props: Props) {
- const { component, branchLike } = props;
- return (
- <Modal contentLabel={translate('regulatory_report.page')} onRequestClose={props.onClose}>
- <ClickEventBoundary>
- <form>
- <RegulatoryReport component={component} branchLike={branchLike} onClose={props.onClose} />
- </form>
- </ClickEventBoundary>
- </Modal>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import BranchesServiceMock from '../../../../../../../api/mocks/BranchesServiceMock';
-import { mockBranch, mockMainBranch } from '../../../../../../../helpers/mocks/branch-like';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { BranchLike } from '../../../../../../../types/branch-like';
-import RegulatoryReport from '../RegulatoryReport';
-
-let handler: BranchesServiceMock;
-
-beforeAll(() => {
- handler = new BranchesServiceMock();
-});
-
-afterEach(() => handler.reset());
-
-describe('RegulatoryReport tests', () => {
- it('should open the regulatory report page', async () => {
- const user = userEvent.setup();
- renderRegulatoryReportApp();
- expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
- expect(screen.getByText('regulatory_report.description1')).toBeInTheDocument();
- expect(screen.getByText('regulatory_report.description2')).toBeInTheDocument();
- expect(
- screen.getByText('regulatory_page.available_branches_info.only_keep_when_inactive')
- ).toBeInTheDocument();
- expect(
- screen.getByText('regulatory_page.available_branches_info.more_info')
- ).toBeInTheDocument();
-
- const branchSelect = screen.getByRole('combobox', { name: 'regulatory_page.select_branch' });
- expect(branchSelect).toBeInTheDocument();
-
- await user.click(branchSelect);
- await user.keyboard('[ArrowDown][Enter]');
-
- const downloadButton = screen.getByRole('link', { name: 'download_verb' });
- expect(downloadButton).toBeInTheDocument();
-
- expect(screen.queryByText('regulatory_page.download_start.sentence')).not.toBeInTheDocument();
- await user.click(downloadButton);
- expect(screen.getByText('regulatory_page.download_start.sentence')).toBeInTheDocument();
- });
-
- it('should display warning message if there is no available branch', async () => {
- handler.emptyBranches();
- renderRegulatoryReportApp();
-
- expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
-
- expect(screen.getByText('regulatory_page.no_available_branch')).toBeInTheDocument();
-
- const downloadButton = screen.getByRole('link', { name: 'download_verb' });
- expect(downloadButton).toBeInTheDocument();
- expect(downloadButton).toHaveClass('disabled');
- });
-
- it('should automatically select passed branch if compatible', async () => {
- const compatibleBranch = mockBranch({ name: 'compatible-branch' });
- handler.addBranch(compatibleBranch);
- renderRegulatoryReportApp(compatibleBranch);
-
- expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
-
- const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
- expect(downloadButton).toBeInTheDocument();
- expect(downloadButton).not.toHaveClass('disabled');
- expect(downloadButton.href).toContain(compatibleBranch.name);
- });
-
- it('should automatically select main branch if present and passed branch is not compatible', async () => {
- handler.emptyBranches();
- const mainBranch = mockMainBranch({ name: 'main' });
- const notCompatibleBranch = mockBranch({
- name: 'not-compatible-branch',
- excludedFromPurge: false,
- });
- handler.addBranch(mainBranch);
- handler.addBranch(notCompatibleBranch);
- renderRegulatoryReportApp(notCompatibleBranch);
-
- expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
-
- const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
- expect(downloadButton).toBeInTheDocument();
- expect(downloadButton).not.toHaveClass('disabled');
- expect(downloadButton.href).toContain(mainBranch.name);
- });
-});
-
-function renderRegulatoryReportApp(branchLike?: BranchLike) {
- renderComponent(
- <RegulatoryReport
- component={{ key: '', name: '' }}
- branchLike={branchLike}
- onClose={() => {}}
- />
- );
-}
import projectBranchesRoutes from '../../apps/projectBranches/routes';
import ProjectDeletionApp from '../../apps/projectDeletion/App';
import projectDumpRoutes from '../../apps/projectDump/routes';
+import projectInfoRoutes from '../../apps/projectInformation/routes';
import ProjectKeyApp from '../../apps/projectKey/ProjectKeyApp';
import ProjectLinksApp from '../../apps/projectLinks/ProjectLinksApp';
import projectQualityGateRoutes from '../../apps/projectQualityGate/routes';
<Route path="security_hotspots" element={<SecurityHotspotsApp />} />
{projectQualityGateRoutes()}
{projectQualityProfilesRoutes()}
+ {projectInfoRoutes()}
{tutorialsRoutes()}
</Route>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import MetaLink from '../../../app/components/nav/component/projectInformation/meta/MetaLink';
import Link from '../../../components/common/Link';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import DateFromNow from '../../../components/intl/DateFromNow';
import { orderLinks } from '../../../helpers/projectLinks';
import { getProjectUrl } from '../../../helpers/urls';
import { MyProject, ProjectLink } from '../../../types/types';
+import MetaLink from '../../projectInformation/meta/MetaLink';
interface Props {
project: MyProject;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { Card, LargeCenteredLayout, PageContentFontWrapper, Title } from 'design-system';
+import * as React from 'react';
+import { getMeasures } from '../../api/measures';
+import withAvailableFeatures, {
+ WithAvailableFeaturesProps,
+} from '../../app/components/available-features/withAvailableFeatures';
+import withComponentContext from '../../app/components/componentContext/withComponentContext';
+import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
+import withMetricsContext from '../../app/components/metrics/withMetricsContext';
+import { translate } from '../../helpers/l10n';
+import { BranchLike } from '../../types/branch-like';
+import { ComponentQualifier } from '../../types/component';
+import { Feature } from '../../types/features';
+import { MetricKey } from '../../types/metrics';
+import { Component, Dict, Measure, Metric } from '../../types/types';
+import { CurrentUser, isLoggedIn } from '../../types/users';
+import AboutProject from './about/AboutProject';
+import ProjectBadges from './badges/ProjectBadges';
+import ProjectNotifications from './notifications/ProjectNotifications';
+import RegulatoryReport from './projectRegulatoryReport/RegulatoryReport';
+
+interface Props extends WithAvailableFeaturesProps {
+ branchLike?: BranchLike;
+ component: Component;
+ currentUser: CurrentUser;
+ onComponentChange: (changes: {}) => void;
+ metrics: Dict<Metric>;
+}
+
+interface State {
+ measures?: Measure[];
+}
+
+export class ProjectInformationApp extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {};
+
+ componentDidMount() {
+ this.mounted = true;
+ this.loadMeasures();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadMeasures = () => {
+ const {
+ component: { key },
+ } = this.props;
+
+ return getMeasures({
+ component: key,
+ metricKeys: [MetricKey.ncloc, MetricKey.projects].join(),
+ }).then((measures) => {
+ if (this.mounted) {
+ this.setState({ measures });
+ }
+ });
+ };
+
+ render() {
+ const { branchLike, component, currentUser, metrics } = this.props;
+ const { measures } = this.state;
+
+ const canConfigureNotifications =
+ isLoggedIn(currentUser) && component.qualifier === ComponentQualifier.Project;
+ const canUseBadges =
+ metrics !== undefined &&
+ (component.qualifier === ComponentQualifier.Application ||
+ component.qualifier === ComponentQualifier.Project);
+ const regulatoryReportFeatureEnabled = this.props.hasFeature(Feature.RegulatoryReport);
+ const isApp = component.qualifier === ComponentQualifier.Application;
+
+ return (
+ <main>
+ <LargeCenteredLayout>
+ <PageContentFontWrapper>
+ <div className="overview sw-my-6 sw-body-sm">
+ <Title className="sw-mb-12">
+ {translate(isApp ? 'application' : 'project', 'info.title')}
+ </Title>
+ <div className="sw-grid sw-grid-cols-[488px_minmax(0,_2fr)] sw-gap-x-12 sw-gap-y-3 sw-auto-rows-min">
+ <div className="sw-row-span-3">
+ <Card>
+ <AboutProject
+ component={component}
+ measures={measures}
+ onComponentChange={this.props.onComponentChange}
+ />
+ </Card>
+ </div>
+
+ {canConfigureNotifications && (
+ <Card>
+ <ProjectNotifications component={component} />
+ </Card>
+ )}
+ {canUseBadges && (
+ <Card>
+ <ProjectBadges branchLike={branchLike} component={component} />
+ </Card>
+ )}
+ {component.qualifier === ComponentQualifier.Project &&
+ regulatoryReportFeatureEnabled && (
+ <Card>
+ <RegulatoryReport
+ component={component}
+ branchLike={branchLike}
+ onClose={() => {}}
+ />
+ </Card>
+ )}
+ </div>
+ </div>
+ </PageContentFontWrapper>
+ </LargeCenteredLayout>
+ </main>
+ );
+ }
+}
+
+export default withComponentContext(
+ withCurrentUserContext(withMetricsContext(withAvailableFeatures(ProjectInformationApp)))
+);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
+import { translate } from '../../../helpers/l10n';
+import { ComponentQualifier } from '../../../types/component';
+import { Component, Measure } from '../../../types/types';
+import MetaKey from '../meta/MetaKey';
+import MetaLinks from '../meta/MetaLinks';
+import MetaQualityGate from '../meta/MetaQualityGate';
+import MetaQualityProfiles from '../meta/MetaQualityProfiles';
+import MetaSize from '../meta/MetaSize';
+import MetaTags from '../meta/MetaTags';
+
+export interface AboutProjectProps {
+ component: Component;
+ measures?: Measure[];
+ onComponentChange: (changes: {}) => void;
+}
+
+export function AboutProject(props: AboutProjectProps) {
+ const { component, measures = [] } = props;
+
+ const heading = React.useRef<HTMLHeadingElement>(null);
+ const isApp = component.qualifier === ComponentQualifier.Application;
+
+ React.useEffect(() => {
+ if (heading.current) {
+ // a11y: provide focus to the heading when the Project Information is opened.
+ heading.current.focus();
+ }
+ }, [heading]);
+
+ return (
+ <>
+ <div>
+ <h2 className="big-padded bordered-bottom" tabIndex={-1} ref={heading}>
+ {translate(isApp ? 'application' : 'project', 'info.title')}
+ </h2>
+ </div>
+
+ <div className="overflow-y-auto">
+ <div className="big-padded bordered-bottom">
+ <div className="display-flex-center">
+ <h3 className="spacer-right">{translate('project.info.description')}</h3>
+ {component.visibility && (
+ <PrivacyBadgeContainer
+ qualifier={component.qualifier}
+ visibility={component.visibility}
+ />
+ )}
+ </div>
+
+ {component.description && (
+ <p className="it__project-description">{component.description}</p>
+ )}
+
+ <MetaTags component={component} onComponentChange={props.onComponentChange} />
+ </div>
+
+ <div className="big-padded bordered-bottom it__project-loc-value">
+ <MetaSize component={component} measures={measures} />
+ </div>
+
+ {!isApp &&
+ (component.qualityGate ||
+ (component.qualityProfiles && component.qualityProfiles.length > 0)) && (
+ <div className="big-padded bordered-bottom">
+ {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
+
+ {component.qualityProfiles && component.qualityProfiles.length > 0 && (
+ <MetaQualityProfiles
+ headerClassName={component.qualityGate ? 'big-spacer-top' : undefined}
+ profiles={component.qualityProfiles}
+ />
+ )}
+ </div>
+ )}
+
+ {!isApp && <MetaLinks component={component} />}
+
+ <div className="big-padded bordered-bottom">
+ <MetaKey componentKey={component.key} qualifier={component.qualifier} />
+ </div>
+ </div>
+ </>
+ );
+}
+
+export default AboutProject;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import classNames from 'classnames';
+import * as React from 'react';
+import { Button } from '../../../components/controls/buttons';
+import { translate } from '../../../helpers/l10n';
+import { BadgeType } from './utils';
+
+interface Props {
+ onClick: (type: BadgeType) => void;
+ selected: boolean;
+ type: BadgeType;
+ url: string;
+}
+
+export default class BadgeButton extends React.PureComponent<Props> {
+ handleClick = () => {
+ this.props.onClick(this.props.type);
+ };
+
+ render() {
+ const { selected, type, url } = this.props;
+ const width = type !== BadgeType.measure ? '128px' : undefined;
+ return (
+ <Button className={classNames('badge-button', { selected })} onClick={this.handleClick}>
+ <img alt={translate('overview.badges', type, 'alt')} src={url} width={width} />
+ </Button>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import classNames from 'classnames';
+import * as React from 'react';
+import { fetchWebApi } from '../../../api/web-api';
+import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
+import Select from '../../../components/controls/Select';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { Dict, Metric } from '../../../types/types';
+import { BadgeFormats, BadgeOptions, BadgeType } from './utils';
+
+interface Props {
+ className?: string;
+ metrics: Dict<Metric>;
+ options: BadgeOptions;
+ type: BadgeType;
+ updateOptions: (options: Partial<BadgeOptions>) => void;
+}
+
+interface State {
+ badgeMetrics: string[];
+}
+
+export class BadgeParams extends React.PureComponent<Props> {
+ mounted = false;
+
+ state: State = { badgeMetrics: [] };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchBadgeMetrics();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchBadgeMetrics() {
+ fetchWebApi(false).then(
+ (webservices) => {
+ if (this.mounted) {
+ const domain = webservices.find((d) => d.path === 'api/project_badges');
+ const ws = domain && domain.actions.find((w) => w.key === 'measure');
+ const param = ws && ws.params && ws.params.find((p) => p.key === 'metric');
+ if (param && param.possibleValues) {
+ this.setState({ badgeMetrics: param.possibleValues });
+ }
+ }
+ },
+ () => {}
+ );
+ }
+
+ getColorOptions = () => {
+ return ['white', 'black', 'orange'].map((color) => ({
+ label: translate('overview.badges.options.colors', color),
+ value: color,
+ }));
+ };
+
+ getFormatOptions = () => {
+ return ['md', 'url'].map((format) => ({
+ label: translate('overview.badges.options.formats', format),
+ value: format as BadgeFormats,
+ }));
+ };
+
+ getMetricOptions = () => {
+ return this.state.badgeMetrics.map((key) => {
+ const metric = this.props.metrics[key];
+ return {
+ value: key,
+ label: metric ? getLocalizedMetricName(metric) : key,
+ };
+ });
+ };
+
+ handleFormatChange = ({ value }: { value: BadgeFormats }) => {
+ this.props.updateOptions({ format: value });
+ };
+
+ handleMetricChange = ({ value }: { value: string }) => {
+ this.props.updateOptions({ metric: value });
+ };
+
+ renderBadgeType = (type: BadgeType, options: BadgeOptions) => {
+ if (type === BadgeType.measure) {
+ const metricOptions = this.getMetricOptions();
+ return (
+ <>
+ <label className="spacer-right" htmlFor="badge-metric">
+ {translate('overview.badges.metric')}:
+ </label>
+ <Select
+ className="input-medium it__metric-badge-select"
+ inputId="badge-metric"
+ isSearchable={false}
+ onChange={this.handleMetricChange}
+ options={metricOptions}
+ value={metricOptions.find((o) => o.value === options.metric)}
+ />
+ </>
+ );
+ }
+ return null;
+ };
+
+ render() {
+ const { className, options, type } = this.props;
+ const formatOptions = this.getFormatOptions();
+ return (
+ <div className={className}>
+ {this.renderBadgeType(type, options)}
+
+ <label
+ className={classNames('spacer-right', {
+ 'spacer-top': type !== BadgeType.qualityGate,
+ })}
+ htmlFor="badge-format"
+ >
+ {translate('format')}:
+ </label>
+ <Select
+ className="input-medium"
+ inputId="badge-format"
+ isSearchable={false}
+ onChange={this.handleFormatChange}
+ options={formatOptions}
+ value={formatOptions.find((o) => o.value === options.format)}
+ defaultValue={formatOptions.find((o) => o.value === 'md')}
+ />
+ </div>
+ );
+ }
+}
+
+export default withMetricsContext(BadgeParams);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { getProjectBadgesToken, renewProjectBadgesToken } from '../../../api/project-badges';
+import CodeSnippet from '../../../components/common/CodeSnippet';
+import { Button } from '../../../components/controls/buttons';
+import { Alert } from '../../../components/ui/Alert';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey } from '../../../types/metrics';
+import { Component } from '../../../types/types';
+import BadgeButton from './BadgeButton';
+import BadgeParams from './BadgeParams';
+import './styles.css';
+import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';
+
+interface Props {
+ branchLike?: BranchLike;
+ component: Component;
+}
+
+interface State {
+ isRenewing: boolean;
+ token: string;
+ selectedType: BadgeType;
+ badgeOptions: BadgeOptions;
+}
+
+export default class ProjectBadges extends React.PureComponent<Props, State> {
+ mounted = false;
+ headingNodeRef = React.createRef<HTMLHeadingElement>();
+ state: State = {
+ isRenewing: false,
+ token: '',
+ selectedType: BadgeType.measure,
+ badgeOptions: { metric: MetricKey.alert_status },
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchToken();
+ if (this.headingNodeRef.current) {
+ this.headingNodeRef.current.focus();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ async fetchToken() {
+ const {
+ component: { key },
+ } = this.props;
+ const token = await getProjectBadgesToken(key).catch(() => '');
+ if (this.mounted) {
+ this.setState({ token });
+ }
+ }
+
+ handleSelectBadge = (selectedType: BadgeType) => {
+ this.setState({ selectedType });
+ };
+
+ handleUpdateOptions = (options: Partial<BadgeOptions>) => {
+ this.setState((state) => ({
+ badgeOptions: { ...state.badgeOptions, ...options },
+ }));
+ };
+
+ handleRenew = async () => {
+ const {
+ component: { key },
+ } = this.props;
+
+ this.setState({ isRenewing: true });
+ await renewProjectBadgesToken(key).catch(() => {});
+ await this.fetchToken();
+ if (this.mounted) {
+ this.setState({ isRenewing: false });
+ }
+ };
+
+ render() {
+ const {
+ branchLike,
+ component: { key: project, qualifier, configuration },
+ } = this.props;
+ const { isRenewing, selectedType, badgeOptions, token } = this.state;
+ const fullBadgeOptions = {
+ project,
+ ...badgeOptions,
+ ...getBranchLikeQuery(branchLike),
+ };
+ const canRenew = configuration?.showSettings;
+
+ return (
+ <div className="display-flex-column">
+ <h3 tabIndex={-1} ref={this.headingNodeRef}>
+ {translate('overview.badges.get_badge', qualifier)}
+ </h3>
+ <p className="big-spacer-bottom">{translate('overview.badges.description', qualifier)}</p>
+ <BadgeButton
+ onClick={this.handleSelectBadge}
+ selected={BadgeType.measure === selectedType}
+ type={BadgeType.measure}
+ url={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token)}
+ />
+ <p className="huge-spacer-bottom spacer-top">
+ {translate('overview.badges', BadgeType.measure, 'description', qualifier)}
+ </p>
+ <BadgeButton
+ onClick={this.handleSelectBadge}
+ selected={BadgeType.qualityGate === selectedType}
+ type={BadgeType.qualityGate}
+ url={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token)}
+ />
+ <p className="huge-spacer-bottom spacer-top">
+ {translate('overview.badges', BadgeType.qualityGate, 'description', qualifier)}
+ </p>
+ <BadgeParams
+ className="big-spacer-bottom display-flex-column"
+ options={badgeOptions}
+ type={selectedType}
+ updateOptions={this.handleUpdateOptions}
+ />
+ {isRenewing ? (
+ <div className="spacer-top spacer-bottom display-flex-row display-flex-justify-center">
+ <DeferredSpinner className="spacer-top spacer-bottom" loading={isRenewing} />
+ </div>
+ ) : (
+ <CodeSnippet isOneLine snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} />
+ )}
+
+ <Alert variant="warning">
+ <p>
+ {translate('overview.badges.leak_warning')}{' '}
+ {canRenew && translate('overview.badges.renew.description')}
+ </p>
+ {canRenew && (
+ <Button
+ disabled={isRenewing}
+ className="spacer-top it__project-info-renew-badge"
+ onClick={this.handleRenew}
+ >
+ {translate('overview.badges.renew')}
+ </Button>
+ )}
+ </Alert>
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import selectEvent from 'react-select-event';
+import { getProjectBadgesToken } from '../../../../api/project-badges';
+import { mockBranch } from '../../../../helpers/mocks/branch-like';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { Location } from '../../../../helpers/urls';
+import { ComponentQualifier } from '../../../../types/component';
+import { MetricKey } from '../../../../types/metrics';
+import ProjectBadges from '../ProjectBadges';
+import { BadgeType } from '../utils';
+
+jest.mock('../../../../helpers/urls', () => ({
+ getHostUrl: () => 'host',
+ getPathUrlAsString: (l: Location) => l.pathname,
+ getProjectUrl: () => ({ pathname: '/dashboard' } as Location),
+}));
+
+jest.mock('../../../../api/project-badges', () => ({
+ getProjectBadgesToken: jest.fn().mockResolvedValue('foo'),
+ renewProjectBadgesToken: jest.fn().mockResolvedValue({}),
+}));
+
+jest.mock('../../../../api/web-api', () => ({
+ fetchWebApi: () =>
+ Promise.resolve([
+ {
+ path: 'api/project_badges',
+ actions: [
+ {
+ key: 'measure',
+ // eslint-disable-next-line local-rules/use-metrickey-enum
+ params: [{ key: 'metric', possibleValues: ['alert_status', 'coverage'] }],
+ },
+ ],
+ },
+ ]),
+}));
+
+it('should renew token', async () => {
+ const user = userEvent.setup();
+ jest.mocked(getProjectBadgesToken).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar');
+ renderProjectBadges({
+ component: mockComponent({ configuration: { showSettings: true } }),
+ });
+
+ expect(
+ await screen.findByText(`overview.badges.get_badge.${ComponentQualifier.Project}`)
+ ).toHaveFocus();
+
+ expect(screen.getByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)).toHaveAttribute(
+ 'src',
+ 'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo'
+ );
+
+ expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
+ 'src',
+ 'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo'
+ );
+
+ await user.click(screen.getByText('overview.badges.renew'));
+
+ expect(
+ await screen.findByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)
+ ).toHaveAttribute(
+ 'src',
+ 'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=bar'
+ );
+
+ expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
+ 'src',
+ 'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=bar'
+ );
+});
+
+it('should update params', async () => {
+ renderProjectBadges({
+ component: mockComponent({ configuration: { showSettings: true } }),
+ });
+
+ expect(
+ await screen.findByText(
+ '[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo)](/dashboard)'
+ )
+ ).toBeInTheDocument();
+
+ await selectEvent.select(screen.getByLabelText('format:'), [
+ 'overview.badges.options.formats.url',
+ ]);
+
+ expect(
+ screen.getByText(
+ 'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo'
+ )
+ ).toBeInTheDocument();
+
+ await selectEvent.select(screen.getByLabelText('overview.badges.metric:'), MetricKey.coverage);
+
+ expect(
+ screen.getByText(
+ `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.coverage}&token=foo`
+ )
+ ).toBeInTheDocument();
+});
+
+function renderProjectBadges(props: Partial<ProjectBadges['props']> = {}) {
+ return renderComponent(
+ <ProjectBadges
+ branchLike={mockBranch()}
+ component={mockComponent({ key: 'foo', qualifier: ComponentQualifier.Project })}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { Location } from '../../../../components/hoc/withRouter';
+import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from '../utils';
+
+jest.mock('../../../../helpers/urls', () => ({
+ ...jest.requireActual('../../../../helpers/urls'),
+ getHostUrl: () => 'host',
+ getPathUrlAsString: (o: Location) => `host${o.pathname}${o.search}`,
+}));
+
+const options: BadgeOptions = {
+ branch: 'master',
+ metric: 'alert_status',
+ project: 'foo',
+};
+
+describe('#getBadgeUrl', () => {
+ it('should generate correct quality gate badge links', () => {
+ expect(getBadgeUrl(BadgeType.qualityGate, options, 'foo')).toBe(
+ 'host/api/project_badges/quality_gate?branch=master&project=foo&token=foo'
+ );
+ });
+
+ it('should generate correct measures badge links', () => {
+ expect(getBadgeUrl(BadgeType.measure, options, 'foo')).toBe(
+ 'host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo'
+ );
+ });
+
+ it('should ignore undefined parameters', () => {
+ expect(getBadgeUrl(BadgeType.measure, { metric: 'alert_status' }, 'foo')).toBe(
+ 'host/api/project_badges/measure?metric=alert_status&token=foo'
+ );
+ });
+
+ it('should force metric parameters', () => {
+ expect(getBadgeUrl(BadgeType.measure, {}, 'foo')).toBe(
+ 'host/api/project_badges/measure?metric=alert_status&token=foo'
+ );
+ });
+});
+
+describe('#getBadgeSnippet', () => {
+ it('should generate a correct markdown image', () => {
+ expect(getBadgeSnippet(BadgeType.measure, { ...options, format: 'md' }, 'foo')).toBe(
+ '[![alert_status](host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo)](host/dashboard?id=foo&branch=master)'
+ );
+ });
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+.badges-list {
+ display: flex;
+ justify-content: space-around;
+ justify-content: space-evenly;
+ flex-wrap: nowrap;
+}
+
+.button.badge-button {
+ display: flex;
+ justify-content: center;
+ padding: var(--gridSize);
+ min-width: 146px;
+ height: 116px;
+ background-color: var(--barBackgroundColor);
+ border: solid 1px var(--barBorderColor);
+ border-radius: 3px;
+ transition: all 0.3s ease;
+}
+
+.button.badge-button:hover,
+.button.badge-button:focus,
+.button.badge-button:active {
+ background-color: var(--barBackgroundColor);
+ border-color: var(--blue);
+}
+
+.button.badge-button.selected {
+ background-color: var(--lightBlue);
+ border-color: var(--darkBlue);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { getLocalizedMetricName } from '../../../helpers/l10n';
+import { omitNil } from '../../../helpers/request';
+import { getHostUrl, getPathUrlAsString, getProjectUrl } from '../../../helpers/urls';
+
+export type BadgeColors = 'white' | 'black' | 'orange';
+export type BadgeFormats = 'md' | 'url';
+
+export interface BadgeOptions {
+ branch?: string;
+ format?: BadgeFormats;
+ project?: string;
+ metric?: string;
+ pullRequest?: string;
+}
+
+export enum BadgeType {
+ measure = 'measure',
+ qualityGate = 'quality_gate',
+}
+
+export function getBadgeSnippet(type: BadgeType, options: BadgeOptions, token: string) {
+ const url = getBadgeUrl(type, options, token);
+ const { branch, format = 'md', metric = 'alert_status', project } = options;
+
+ if (format === 'url') {
+ return url;
+ }
+
+ let label;
+ let projectUrl;
+
+ switch (type) {
+ case BadgeType.measure:
+ label = getLocalizedMetricName({ key: metric });
+ break;
+ case BadgeType.qualityGate:
+ default:
+ label = 'Quality gate';
+ break;
+ }
+
+ if (project) {
+ projectUrl = getPathUrlAsString(getProjectUrl(project, branch), false);
+ }
+
+ const mdImage = `![${label}](${url})`;
+ return projectUrl ? `[${mdImage}](${projectUrl})` : mdImage;
+}
+
+export function getBadgeUrl(
+ type: BadgeType,
+ { branch, project, metric = 'alert_status', pullRequest }: BadgeOptions,
+ token: string
+) {
+ switch (type) {
+ case BadgeType.qualityGate:
+ return `${getHostUrl()}/api/project_badges/quality_gate?${new URLSearchParams(
+ omitNil({ branch, project, pullRequest, token })
+ ).toString()}`;
+ case BadgeType.measure:
+ default:
+ return `${getHostUrl()}/api/project_badges/measure?${new URLSearchParams(
+ omitNil({ branch, project, metric, pullRequest, token })
+ ).toString()}`;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { ClipboardButton } from '../../../components/controls/clipboard';
+import { translate } from '../../../helpers/l10n';
+
+export interface MetaKeyProps {
+ componentKey: string;
+ qualifier: string;
+}
+
+export default function MetaKey({ componentKey, qualifier }: MetaKeyProps) {
+ return (
+ <>
+ <h3 id="project-key">{translate('overview.project_key', qualifier)}</h3>
+ <div className="display-flex-center">
+ <input
+ className="overview-key"
+ aria-labelledby="project-key"
+ readOnly
+ type="text"
+ value={componentKey}
+ />
+ <ClipboardButton
+ aria-label={translate('overview.project_key.click_to_copy')}
+ className="little-spacer-left"
+ copyValue={componentKey}
+ />
+ </div>
+ </>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import isValidUri from '../../../app/utils/isValidUri';
+import { ClearButton } from '../../../components/controls/buttons';
+import ProjectLinkIcon from '../../../components/icons/ProjectLinkIcon';
+import { getLinkName } from '../../../helpers/projectLinks';
+import { ProjectLink } from '../../../types/types';
+
+interface Props {
+ iconOnly?: boolean;
+ link: ProjectLink;
+}
+
+interface State {
+ expanded: boolean;
+}
+
+export default class MetaLink extends React.PureComponent<Props, State> {
+ state = { expanded: false };
+
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.setState(({ expanded }) => ({ expanded: !expanded }));
+ };
+
+ handleCollapse = () => {
+ this.setState({ expanded: false });
+ };
+
+ handleSelect = (event: React.MouseEvent<HTMLInputElement>) => {
+ event.currentTarget.select();
+ };
+
+ render() {
+ const { iconOnly, link } = this.props;
+ const linkTitle = getLinkName(link);
+ const isValid = isValidUri(link.url);
+ return (
+ <li>
+ <a
+ className="link-no-underline"
+ href={isValid ? link.url : undefined}
+ onClick={isValid ? undefined : this.handleClick}
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ title={linkTitle}
+ >
+ <ProjectLinkIcon className="little-spacer-right" type={link.type} />
+ {!iconOnly && linkTitle}
+ </a>
+ {this.state.expanded && (
+ <div className="little-spacer-top display-flex-center">
+ <input
+ className="overview-key width-80"
+ onClick={this.handleSelect}
+ readOnly
+ type="text"
+ value={link.url}
+ />
+ <ClearButton className="little-spacer-left" onClick={this.handleCollapse} />
+ </div>
+ )}
+ </li>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { getProjectLinks } from '../../../api/projectLinks';
+import { translate } from '../../../helpers/l10n';
+import { orderLinks } from '../../../helpers/projectLinks';
+import { LightComponent, ProjectLink } from '../../../types/types';
+import MetaLink from './MetaLink';
+
+interface Props {
+ component: LightComponent;
+}
+
+interface State {
+ links?: ProjectLink[];
+}
+
+export default class MetaLinks extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {};
+
+ componentDidMount() {
+ this.mounted = true;
+ this.loadLinks();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.component.key !== this.props.component.key) {
+ this.loadLinks();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadLinks = () =>
+ getProjectLinks(this.props.component.key).then(
+ (links) => {
+ if (this.mounted) {
+ this.setState({ links });
+ }
+ },
+ () => {}
+ );
+
+ render() {
+ const { links } = this.state;
+
+ if (!links || links.length === 0) {
+ return null;
+ }
+
+ const orderedLinks = orderLinks(links);
+
+ return (
+ <div className="big-padded bordered-bottom">
+ <h3>{translate('overview.external_links')}</h3>
+ <ul className="project-info-list">
+ {orderedLinks.map((link) => (
+ <MetaLink key={link.id} link={link} />
+ ))}
+ </ul>
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Link from '../../../components/common/Link';
+import { translate } from '../../../helpers/l10n';
+import { getQualityGateUrl } from '../../../helpers/urls';
+
+interface Props {
+ qualityGate: { isDefault?: boolean; name: string };
+}
+
+export default function MetaQualityGate({ qualityGate }: Props) {
+ return (
+ <>
+ <h3>{translate('project.info.quality_gate')}</h3>
+
+ <ul className="project-info-list">
+ <li>
+ {qualityGate.isDefault && (
+ <span className="note spacer-right">({translate('default')})</span>
+ )}
+ <Link to={getQualityGateUrl(qualityGate.name)}>{qualityGate.name}</Link>
+ </li>
+ </ul>
+ </>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { searchRules } from '../../../api/rules';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
+import Link from '../../../components/common/Link';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { getQualityProfileUrl } from '../../../helpers/urls';
+import { Languages } from '../../../types/languages';
+import { ComponentQualityProfile, Dict } from '../../../types/types';
+
+interface Props {
+ headerClassName?: string;
+ languages: Languages;
+ profiles: ComponentQualityProfile[];
+}
+
+interface State {
+ deprecatedByKey: Dict<number>;
+}
+
+export class MetaQualityProfiles extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { deprecatedByKey: {} };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.loadDeprecatedRules();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadDeprecatedRules() {
+ const existingProfiles = this.props.profiles.filter((p) => !p.deleted);
+ const requests = existingProfiles.map((profile) =>
+ this.loadDeprecatedRulesForProfile(profile.key)
+ );
+ Promise.all(requests).then(
+ (responses) => {
+ if (this.mounted) {
+ const deprecatedByKey: Dict<number> = {};
+ responses.forEach((count, i) => {
+ const profileKey = existingProfiles[i].key;
+ deprecatedByKey[profileKey] = count;
+ });
+ this.setState({ deprecatedByKey });
+ }
+ },
+ () => {}
+ );
+ }
+
+ loadDeprecatedRulesForProfile(profileKey: string) {
+ const data = {
+ activation: 'true',
+ ps: 1,
+ qprofile: profileKey,
+ statuses: 'DEPRECATED',
+ };
+ return searchRules(data).then((r) => r.paging.total);
+ }
+
+ getDeprecatedRulesCount(profile: { key: string }) {
+ const count = this.state.deprecatedByKey[profile.key];
+ return count || 0;
+ }
+
+ renderProfile(profile: ComponentQualityProfile) {
+ const languageFromStore = this.props.languages[profile.language];
+ const languageName = languageFromStore ? languageFromStore.name : profile.language;
+
+ const inner = (
+ <div className="text-ellipsis">
+ <span className="spacer-right">({languageName})</span>
+ {profile.deleted ? (
+ profile.name
+ ) : (
+ <Link to={getQualityProfileUrl(profile.name, profile.language)}>
+ <span
+ aria-label={translateWithParameters(
+ 'overview.link_to_x_profile_y',
+ languageName,
+ profile.name
+ )}
+ >
+ {profile.name}
+ </span>
+ </Link>
+ )}
+ </div>
+ );
+
+ if (profile.deleted) {
+ const tooltip = translateWithParameters('overview.deleted_profile', profile.name);
+ return (
+ <Tooltip key={profile.key} overlay={tooltip}>
+ <li className="project-info-deleted-profile">{inner}</li>
+ </Tooltip>
+ );
+ }
+
+ const count = this.getDeprecatedRulesCount(profile);
+
+ if (count > 0) {
+ const tooltip = translateWithParameters('overview.deprecated_profile', count);
+ return (
+ <Tooltip key={profile.key} overlay={tooltip}>
+ <li className="project-info-deprecated-rules">{inner}</li>
+ </Tooltip>
+ );
+ }
+
+ return <li key={profile.key}>{inner}</li>;
+ }
+
+ render() {
+ const { headerClassName, profiles } = this.props;
+
+ return (
+ <>
+ <h3 className={headerClassName}>{translate('overview.quality_profiles')}</h3>
+
+ <ul className="project-info-list">
+ {profiles.map((profile) => this.renderProfile(profile))}
+ </ul>
+ </>
+ );
+ }
+}
+
+export default withLanguagesContext(MetaQualityProfiles);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import DrilldownLink from '../../../components/shared/DrilldownLink';
+import SizeRating from '../../../components/ui/SizeRating';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure, localizeMetric } from '../../../helpers/measures';
+import { ComponentQualifier } from '../../../types/component';
+import { MetricKey } from '../../../types/metrics';
+import { Component, Measure } from '../../../types/types';
+
+export interface MetaSizeProps {
+ component: Component;
+ measures: Measure[];
+}
+
+export default function MetaSize({ component, measures }: MetaSizeProps) {
+ const isApp = component.qualifier === ComponentQualifier.Application;
+ const ncloc = measures.find((measure) => measure.metric === MetricKey.ncloc);
+ const projects = isApp
+ ? measures.find((measure) => measure.metric === MetricKey.projects)
+ : undefined;
+
+ return (
+ <>
+ <div className="display-flex-row display-inline-flex-baseline">
+ <h3>{localizeMetric(MetricKey.ncloc)}</h3>
+ <span className="spacer-left small">({translate('project.info.main_branch')})</span>
+ </div>
+ <div className="display-flex-center">
+ {ncloc && ncloc.value ? (
+ <>
+ <DrilldownLink className="huge" component={component.key} metric={MetricKey.ncloc}>
+ <span
+ aria-label={translateWithParameters(
+ 'project.info.see_more_info_on_x_locs',
+ ncloc.value
+ )}
+ >
+ {formatMeasure(ncloc.value, 'SHORT_INT')}
+ </span>
+ </DrilldownLink>
+
+ <span className="spacer-left">
+ <SizeRating value={Number(ncloc.value)} />
+ </span>
+ </>
+ ) : (
+ <span>0</span>
+ )}
+
+ {isApp && (
+ <span className="huge-spacer-left display-inline-flex-center">
+ {projects ? (
+ <DrilldownLink component={component.key} metric={MetricKey.projects}>
+ <span className="big">{formatMeasure(projects.value, 'SHORT_INT')}</span>
+ </DrilldownLink>
+ ) : (
+ <span className="big">0</span>
+ )}
+ <span className="little-spacer-left text-muted">
+ {translate('metric.projects.name')}
+ </span>
+ </span>
+ )}
+ </div>
+ </>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { setApplicationTags, setProjectTags } from '../../../api/components';
+import Dropdown from '../../../components/controls/Dropdown';
+import { ButtonLink } from '../../../components/controls/buttons';
+import TagsList from '../../../components/tags/TagsList';
+import { PopupPlacement } from '../../../components/ui/popups';
+import { translate } from '../../../helpers/l10n';
+import { ComponentQualifier } from '../../../types/component';
+import { Component } from '../../../types/types';
+import MetaTagsSelector from './MetaTagsSelector';
+
+interface Props {
+ component: Component;
+ onComponentChange: (changes: {}) => void;
+}
+
+export default class MetaTags extends React.PureComponent<Props> {
+ canUpdateTags = () => {
+ const { configuration } = this.props.component;
+ return configuration && configuration.showSettings;
+ };
+
+ setTags = (values: string[]) => {
+ const { component } = this.props;
+
+ if (component.qualifier === ComponentQualifier.Application) {
+ return setApplicationTags({
+ application: component.key,
+ tags: values.join(','),
+ });
+ }
+
+ return setProjectTags({
+ project: component.key,
+ tags: values.join(','),
+ });
+ };
+
+ handleSetProjectTags = (values: string[]) => {
+ this.setTags(values).then(
+ () => this.props.onComponentChange({ tags: values }),
+ () => {}
+ );
+ };
+
+ render() {
+ const tags = this.props.component.tags || [];
+
+ return this.canUpdateTags() ? (
+ <div className="big-spacer-top project-info-tags">
+ <Dropdown
+ closeOnClick={false}
+ closeOnClickOutside
+ overlay={
+ <MetaTagsSelector selectedTags={tags} setProjectTags={this.handleSetProjectTags} />
+ }
+ overlayPlacement={PopupPlacement.BottomLeft}
+ >
+ <ButtonLink stopPropagation>
+ <TagsList allowUpdate tags={tags.length ? tags : [translate('no_tags')]} />
+ </ButtonLink>
+ </Dropdown>
+ </div>
+ ) : (
+ <div className="big-spacer-top project-info-tags">
+ <TagsList
+ allowUpdate={false}
+ className="note"
+ tags={tags.length ? tags : [translate('no_tags')]}
+ />
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { difference, without } from 'lodash';
+import * as React from 'react';
+import { searchProjectTags } from '../../../api/components';
+import TagsSelector from '../../../components/tags/TagsSelector';
+
+interface Props {
+ selectedTags: string[];
+ setProjectTags: (tags: string[]) => void;
+}
+
+interface State {
+ searchResult: string[];
+}
+
+const LIST_SIZE = 10;
+
+export default class MetaTagsSelector extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { searchResult: [] };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ onSearch = (query: string) => {
+ return searchProjectTags({
+ q: query,
+ ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
+ }).then(
+ ({ tags }) => {
+ if (this.mounted) {
+ this.setState({ searchResult: tags });
+ }
+ },
+ () => {}
+ );
+ };
+
+ onSelect = (tag: string) => {
+ this.props.setProjectTags([...this.props.selectedTags, tag]);
+ };
+
+ onUnselect = (tag: string) => {
+ this.props.setProjectTags(without(this.props.selectedTags, tag));
+ };
+
+ render() {
+ const availableTags = difference(this.state.searchResult, this.props.selectedTags);
+ return (
+ <TagsSelector
+ listSize={LIST_SIZE}
+ onSearch={this.onSearch}
+ onSelect={this.onSelect}
+ onUnselect={this.onUnselect}
+ selectedTags={this.props.selectedTags}
+ tags={availableTags}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import * as React from 'react';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import MetaKey, { MetaKeyProps } from '../MetaKey';
+
+it('should render correctly', () => {
+ renderMetaKey();
+ expect(
+ screen.getByLabelText(`overview.project_key.${ComponentQualifier.Project}`)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'overview.project_key.click_to_copy' })
+ ).toBeInTheDocument();
+});
+
+function renderMetaKey(props: Partial<MetaKeyProps> = {}) {
+ return renderComponent(
+ <MetaKey componentKey="foo" qualifier={ComponentQualifier.Project} {...props} />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import * as React from 'react';
+import { searchRules } from '../../../../api/rules';
+import { mockLanguage, mockPaging, mockQualityProfile } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { SearchRulesResponse } from '../../../../types/coding-rules';
+import { Dict } from '../../../../types/types';
+import { MetaQualityProfiles } from '../MetaQualityProfiles';
+
+jest.mock('../../../../api/rules', () => {
+ return {
+ searchRules: jest.fn().mockResolvedValue({
+ total: 10,
+ }),
+ };
+});
+
+it('should render correctly', async () => {
+ const totals: Dict<number> = {
+ js: 0,
+ ts: 10,
+ css: 0,
+ };
+ jest
+ .mocked(searchRules)
+ .mockImplementation(({ qprofile }: { qprofile: string }): Promise<SearchRulesResponse> => {
+ return Promise.resolve({
+ rules: [],
+ paging: mockPaging({
+ total: totals[qprofile],
+ }),
+ });
+ });
+
+ renderMetaQualityprofiles();
+
+ expect(await screen.findByText('overview.deleted_profile.javascript')).toBeInTheDocument();
+ expect(screen.getByText('overview.deprecated_profile.10')).toBeInTheDocument();
+});
+
+function renderMetaQualityprofiles(overrides: Partial<MetaQualityProfiles['props']> = {}) {
+ return renderComponent(
+ <MetaQualityProfiles
+ languages={{ css: mockLanguage() }}
+ profiles={[
+ { ...mockQualityProfile({ key: 'js', name: 'javascript' }), deleted: true },
+ { ...mockQualityProfile({ key: 'ts', name: 'typescript' }), deleted: false },
+ {
+ ...mockQualityProfile({
+ key: 'css',
+ name: 'style',
+ language: 'css',
+ languageName: 'CSS',
+ }),
+ deleted: false,
+ },
+ ]}
+ {...overrides}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { searchProjectTags, setApplicationTags, setProjectTags } from '../../../../api/components';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import MetaTags from '../MetaTags';
+
+jest.mock('../../../../api/components', () => ({
+ setApplicationTags: jest.fn().mockResolvedValue(true),
+ setProjectTags: jest.fn().mockResolvedValue(true),
+ searchProjectTags: jest.fn(),
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('should render without tags and admin rights', async () => {
+ renderMetaTags();
+
+ expect(await screen.findByText('no_tags')).toBeInTheDocument();
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+});
+
+it('should allow to edit tags for a project', async () => {
+ const user = userEvent.setup();
+ jest.mocked(searchProjectTags).mockResolvedValue({ tags: ['best', 'useless'] });
+
+ const onComponentChange = jest.fn();
+ const component = mockComponent({
+ key: 'my-second-project',
+ tags: ['foo', 'bar'],
+ configuration: {
+ showSettings: true,
+ },
+ name: 'MySecondProject',
+ });
+
+ renderMetaTags({ component, onComponentChange });
+
+ expect(await screen.findByText('foo, bar')).toBeInTheDocument();
+ expect(screen.getByRole('button')).toBeInTheDocument();
+
+ await user.click(screen.getByRole('button', { name: 'tags_list_x.foo, bar' }));
+
+ expect(await screen.findByText('best')).toBeInTheDocument();
+
+ await user.click(screen.getByText('best'));
+ expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo', 'bar', 'best'] });
+
+ onComponentChange.mockClear();
+
+ /*
+ * Since we're not actually updating the tags, we're back to having the foo, bar only
+ */
+ await user.click(screen.getByText('bar'));
+ expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo'] });
+
+ expect(setProjectTags).toHaveBeenCalled();
+ expect(setApplicationTags).not.toHaveBeenCalled();
+});
+
+it('should set tags for an app', async () => {
+ const user = userEvent.setup();
+
+ renderMetaTags({
+ component: mockComponent({
+ configuration: {
+ showSettings: true,
+ },
+ qualifier: ComponentQualifier.Application,
+ }),
+ });
+
+ await user.click(screen.getByRole('button', { name: 'tags_list_x.no_tags' }));
+
+ await user.click(screen.getByText('best'));
+
+ expect(setProjectTags).not.toHaveBeenCalled();
+ expect(setApplicationTags).toHaveBeenCalled();
+});
+
+function renderMetaTags(overrides: Partial<MetaTags['props']> = {}) {
+ const component = mockComponent({
+ configuration: {
+ showSettings: false,
+ },
+ });
+
+ return renderComponent(
+ <MetaTags component={component} onComponentChange={jest.fn()} {...overrides} />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import {
+ withNotifications,
+ WithNotificationsProps,
+} from '../../../components/hoc/withNotifications';
+import { Alert } from '../../../components/ui/Alert';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { translate } from '../../../helpers/l10n';
+import { Component } from '../../../types/types';
+import NotificationsList from '../../account/notifications/NotificationsList';
+
+interface Props {
+ component: Component;
+}
+
+export function ProjectNotifications(props: WithNotificationsProps & Props) {
+ const { channels, component, loading, notifications, perProjectTypes } = props;
+ const heading = React.useRef<HTMLHeadingElement>(null);
+
+ React.useEffect(() => {
+ if (heading.current) {
+ // a11y: provide focus to the heading when the info drawer page is opened.
+ heading.current.focus();
+ }
+ }, [heading]);
+
+ const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => {
+ props.addNotification({ project: component.key, channel, type });
+ };
+
+ const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => {
+ props.removeNotification({
+ project: component.key,
+ channel,
+ type,
+ });
+ };
+
+ const getCheckboxId = (type: string, channel: string) => {
+ return `project-notification-${component.key}-${type}-${channel}`;
+ };
+
+ const projectNotifications = notifications.filter(
+ (n) => n.project && n.project === component.key
+ );
+
+ return (
+ <div>
+ <h3 tabIndex={-1} ref={heading}>
+ {translate('project.info.notifications')}
+ </h3>
+
+ <Alert className="spacer-top" variant="info">
+ {translate('notification.dispatcher.information')}
+ </Alert>
+
+ <DeferredSpinner loading={loading}>
+ <table className="data zebra notifications-table">
+ <thead>
+ <tr>
+ <th aria-label={translate('project')} />
+ {channels.map((channel) => (
+ <th className="text-center" key={channel}>
+ <h4>{translate('notification.channel', channel)}</h4>
+ </th>
+ ))}
+ </tr>
+ </thead>
+
+ <NotificationsList
+ channels={channels}
+ checkboxId={getCheckboxId}
+ notifications={projectNotifications}
+ onAdd={handleAddNotification}
+ onRemove={handleRemoveNotification}
+ project
+ types={perProjectTypes}
+ />
+ </table>
+ </DeferredSpinner>
+ </div>
+ );
+}
+
+export default withNotifications(ProjectNotifications);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { getNotifications } from '../../../../api/notifications';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockNotification } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { NotificationGlobalType, NotificationProjectType } from '../../../../types/notifications';
+import ProjectNotifications from '../ProjectNotifications';
+
+jest.mock('../../../../api/notifications', () => ({
+ addNotification: jest.fn().mockResolvedValue(undefined),
+ removeNotification: jest.fn().mockResolvedValue(undefined),
+ getNotifications: jest.fn(),
+}));
+
+beforeAll(() => {
+ jest.mocked(getNotifications).mockResolvedValue({
+ channels: ['channel1'],
+ globalTypes: [NotificationGlobalType.MyNewIssues],
+ notifications: [
+ mockNotification({}),
+ mockNotification({ type: NotificationProjectType.NewAlerts }),
+ ],
+ perProjectTypes: [NotificationProjectType.NewAlerts, NotificationProjectType.NewIssues],
+ });
+});
+
+it('should render correctly', async () => {
+ const user = userEvent.setup();
+ renderProjectNotifications();
+
+ expect(await screen.findByText('notification.channel.channel1')).toBeInTheDocument();
+ expect(
+ screen.getByLabelText(
+ 'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
+ )
+ ).toBeChecked();
+
+ expect(
+ screen.getByLabelText(
+ 'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
+ )
+ ).not.toBeChecked();
+
+ // Toggle New Alerts
+ await user.click(
+ screen.getByLabelText(
+ 'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
+ )
+ );
+
+ expect(
+ screen.getByLabelText(
+ 'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
+ )
+ ).not.toBeChecked();
+
+ // Toggle New Issues
+ await user.click(
+ screen.getByLabelText(
+ 'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
+ )
+ );
+
+ expect(
+ screen.getByLabelText(
+ 'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
+ )
+ ).toBeChecked();
+});
+
+function renderProjectNotifications() {
+ return renderComponent(
+ <ProjectNotifications component={mockComponent({ key: 'foo', name: 'Foo' })} />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import classNames from 'classnames';
+import { orderBy } from 'lodash';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { getBranches } from '../../../api/branches';
+import { getRegulatoryReportUrl } from '../../../api/regulatory-report';
+import DocLink from '../../../components/common/DocLink';
+import Select, { LabelValueSelectOption } from '../../../components/controls/Select';
+import { ButtonLink } from '../../../components/controls/buttons';
+import { Alert } from '../../../components/ui/Alert';
+import {
+ getBranchLikeDisplayName,
+ getBranchLikeKey,
+ isMainBranch,
+} from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
+import { Component } from '../../../types/types';
+
+interface Props {
+ component: Pick<Component, 'key' | 'name'>;
+ branchLike?: BranchLike;
+ onClose: () => void;
+}
+
+interface State {
+ downloadStarted: boolean;
+ selectedBranch: string;
+ branchOptions: LabelValueSelectOption[];
+}
+
+export default class RegulatoryReport extends React.PureComponent<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ downloadStarted: false,
+ selectedBranch: '',
+ branchOptions: [],
+ };
+ }
+
+ componentDidMount() {
+ const { component, branchLike } = this.props;
+ getBranches(component.key)
+ .then((data) => {
+ const availableBranches = data.filter(
+ (br) => br.analysisDate && (isMainBranch(br) || br.excludedFromPurge)
+ );
+ const mainBranch = availableBranches.find(isMainBranch);
+ const otherBranchSorted = orderBy(
+ availableBranches.filter((b) => !isMainBranch(b)),
+ (b) => b.name
+ );
+ const sortedBranch = mainBranch ? [mainBranch, ...otherBranchSorted] : otherBranchSorted;
+ const options = sortedBranch.map((br) => {
+ return {
+ value: getBranchLikeDisplayName(br),
+ label: getBranchLikeDisplayName(br),
+ };
+ });
+
+ let selectedBranch = '';
+ if (
+ branchLike &&
+ availableBranches.find((br) => getBranchLikeKey(br) === getBranchLikeKey(branchLike))
+ ) {
+ selectedBranch = getBranchLikeDisplayName(branchLike);
+ } else if (mainBranch) {
+ selectedBranch = getBranchLikeDisplayName(mainBranch);
+ }
+ this.setState({ selectedBranch, branchOptions: options });
+ })
+ .catch(() => {
+ this.setState({ branchOptions: [] });
+ });
+ }
+
+ onBranchSelect = (newOption: LabelValueSelectOption) => {
+ this.setState({ selectedBranch: newOption.value, downloadStarted: false });
+ };
+
+ render() {
+ const { component, onClose } = this.props;
+ const { downloadStarted, selectedBranch, branchOptions } = this.state;
+ const isDownloadButtonDisabled = downloadStarted || !selectedBranch;
+
+ return (
+ <>
+ <div className="modal-head">
+ <h2>{translate('regulatory_report.page')}</h2>
+ </div>
+ <div className="modal-body">
+ <p>{translate('regulatory_report.description1')}</p>
+ <div className="markdown">
+ <ul>
+ <li>{translate('regulatory_report.bullet_point1')}</li>
+ <li>{translate('regulatory_report.bullet_point2')}</li>
+ <li>{translate('regulatory_report.bullet_point3')}</li>
+ </ul>
+ </div>
+ <p>{translate('regulatory_report.description2')}</p>
+ {branchOptions.length > 0 ? (
+ <>
+ <div className="modal-field big-spacer-top">
+ <label htmlFor="regulatory-report-branch-select">
+ {translate('regulatory_page.select_branch')}
+ </label>
+ <Select
+ className="width-100"
+ inputId="regulatory-report-branch-select"
+ id="regulatory-report-branch-select-input"
+ onChange={this.onBranchSelect}
+ options={branchOptions}
+ value={branchOptions.find((o) => o.value === selectedBranch)}
+ />
+ </div>
+ <Alert variant="info">
+ <div>
+ {translate('regulatory_page.available_branches_info.only_keep_when_inactive')}
+ </div>
+ <div>
+ <FormattedMessage
+ id="regulatory_page.available_branches_info.more_info"
+ defaultMessage={translate('regulatory_page.available_branches_info.more_info')}
+ values={{
+ doc_link: (
+ <DocLink to="/analyzing-source-code/branches/branch-analysis/#inactive-branches">
+ {translate('regulatory_page.available_branches_info.more_info.doc_link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </div>
+ </Alert>
+ </>
+ ) : (
+ <div className="big-spacer-top">
+ <Alert variant="warning">
+ <div>{translate('regulatory_page.no_available_branch')}</div>
+ </Alert>
+ </div>
+ )}
+ <div className="modal-field big-spacer-top">
+ {downloadStarted && (
+ <div>
+ <p>{translate('regulatory_page.download_start.sentence')}</p>
+ </div>
+ )}
+ </div>
+ </div>
+ <div className="modal-foot">
+ <a
+ className={classNames('button button-primary big-spacer-right', {
+ disabled: isDownloadButtonDisabled,
+ })}
+ download={[component.name, selectedBranch, 'regulatory report.zip']
+ .filter((s) => !!s)
+ .join(' - ')}
+ onClick={() => this.setState({ downloadStarted: true })}
+ href={getRegulatoryReportUrl(component.key, selectedBranch)}
+ target="_blank"
+ rel="noopener noreferrer"
+ aria-disabled={isDownloadButtonDisabled}
+ >
+ {translate('download_verb')}
+ </a>
+ <ButtonLink onClick={onClose}>{translate('close')}</ButtonLink>
+ </div>
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import ClickEventBoundary from '../../../components/controls/ClickEventBoundary';
+import Modal from '../../../components/controls/Modal';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
+import { Component } from '../../../types/types';
+import RegulatoryReport from './RegulatoryReport';
+
+interface Props {
+ component: Component;
+ branchLike?: BranchLike;
+ onClose: () => void;
+}
+
+export default function RegulatoryReportModal(props: Props) {
+ const { component, branchLike } = props;
+ return (
+ <Modal contentLabel={translate('regulatory_report.page')} onRequestClose={props.onClose}>
+ <ClickEventBoundary>
+ <form>
+ <RegulatoryReport component={component} branchLike={branchLike} onClose={props.onClose} />
+ </form>
+ </ClickEventBoundary>
+ </Modal>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
+import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { BranchLike } from '../../../../types/branch-like';
+import RegulatoryReport from '../RegulatoryReport';
+
+let handler: BranchesServiceMock;
+
+beforeAll(() => {
+ handler = new BranchesServiceMock();
+});
+
+afterEach(() => handler.reset());
+
+describe('RegulatoryReport tests', () => {
+ it('should open the regulatory report page', async () => {
+ const user = userEvent.setup();
+ renderRegulatoryReportApp();
+ expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+ expect(screen.getByText('regulatory_report.description1')).toBeInTheDocument();
+ expect(screen.getByText('regulatory_report.description2')).toBeInTheDocument();
+ expect(
+ screen.getByText('regulatory_page.available_branches_info.only_keep_when_inactive')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('regulatory_page.available_branches_info.more_info')
+ ).toBeInTheDocument();
+
+ const branchSelect = screen.getByRole('combobox', { name: 'regulatory_page.select_branch' });
+ expect(branchSelect).toBeInTheDocument();
+
+ await user.click(branchSelect);
+ await user.keyboard('[ArrowDown][Enter]');
+
+ const downloadButton = screen.getByRole('link', { name: 'download_verb' });
+ expect(downloadButton).toBeInTheDocument();
+
+ expect(screen.queryByText('regulatory_page.download_start.sentence')).not.toBeInTheDocument();
+ await user.click(downloadButton);
+ expect(screen.getByText('regulatory_page.download_start.sentence')).toBeInTheDocument();
+ });
+
+ it('should display warning message if there is no available branch', async () => {
+ handler.emptyBranches();
+ renderRegulatoryReportApp();
+
+ expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+
+ expect(screen.getByText('regulatory_page.no_available_branch')).toBeInTheDocument();
+
+ const downloadButton = screen.getByRole('link', { name: 'download_verb' });
+ expect(downloadButton).toBeInTheDocument();
+ expect(downloadButton).toHaveClass('disabled');
+ });
+
+ it('should automatically select passed branch if compatible', async () => {
+ const compatibleBranch = mockBranch({ name: 'compatible-branch' });
+ handler.addBranch(compatibleBranch);
+ renderRegulatoryReportApp(compatibleBranch);
+
+ expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+
+ const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
+ expect(downloadButton).toBeInTheDocument();
+ expect(downloadButton).not.toHaveClass('disabled');
+ expect(downloadButton.href).toContain(compatibleBranch.name);
+ });
+
+ it('should automatically select main branch if present and passed branch is not compatible', async () => {
+ handler.emptyBranches();
+ const mainBranch = mockMainBranch({ name: 'main' });
+ const notCompatibleBranch = mockBranch({
+ name: 'not-compatible-branch',
+ excludedFromPurge: false,
+ });
+ handler.addBranch(mainBranch);
+ handler.addBranch(notCompatibleBranch);
+ renderRegulatoryReportApp(notCompatibleBranch);
+
+ expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+
+ const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
+ expect(downloadButton).toBeInTheDocument();
+ expect(downloadButton).not.toHaveClass('disabled');
+ expect(downloadButton.href).toContain(mainBranch.name);
+ });
+});
+
+function renderRegulatoryReportApp(branchLike?: BranchLike) {
+ renderComponent(
+ <RegulatoryReport
+ component={{ key: '', name: '' }}
+ branchLike={branchLike}
+ onClose={() => {}}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { Route } from 'react-router-dom';
+import ProjectInformationApp from './ProjectInformationApp';
+
+const routes = () => <Route path="project/info" element={<ProjectInformationApp />} />;
+
+export default routes;