* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
+import { inRange } from 'lodash';
import tw from 'twin.macro';
import { getProp, themeColor, themeContrast } from '../helpers/theme';
import { SizeLabel } from '../types/measures';
export interface Props {
size?: 'xs' | 'sm' | 'md';
- value: SizeLabel;
+ value: number;
}
const SIZE_MAPPING = {
};
export function SizeIndicator({ size = 'sm', value }: Props) {
+ let letter: SizeLabel;
+ if (inRange(value, 0, 1000)) {
+ letter = 'XS';
+ } else if (inRange(value, 1000, 10000)) {
+ letter = 'S';
+ } else if (inRange(value, 10000, 100000)) {
+ letter = 'M';
+ } else if (inRange(value, 100000, 500000)) {
+ letter = 'L';
+ } else {
+ letter = 'XL';
+ }
+
return (
<StyledContainer aria-hidden="true" size={SIZE_MAPPING[size]}>
- {value}
+ {letter}
</StyledContainer>
);
}
const StyledContainer = styled.div<{ size: string }>`
width: ${getProp('size')};
height: ${getProp('size')};
- font-size: ${({ size }) => (size === '2rem' ? '0.875rem' : '0.75rem')};
+ font-size: ${({ size }) => (size === '2rem' ? '0.875rem' : `calc(${size}/2)`)};
color: ${themeContrast('sizeIndicator')};
background-color: ${themeColor('sizeIndicator')};
import { SizeLabel } from '../../types/measures';
import { SizeIndicator } from '../SizeIndicator';
-it.each(['XS', 'S', 'M', 'L', 'XL'])(
- 'should display SizeIndicator with size',
- (value: SizeLabel) => {
- setupWithProps({ value });
- expect(screen.getByText(value)).toBeInTheDocument();
- }
-);
+it.each([
+ [100, 'XS'],
+ [1100, 'S'],
+ [20_000, 'M'],
+ [200_000, 'L'],
+ [1_000_000, 'XL'],
+])('should display SizeIndicator with size', (value: number, letter: SizeLabel) => {
+ setupWithProps({ value });
+ expect(screen.getByText(letter)).toBeInTheDocument();
+});
function setupWithProps(props: Partial<FCProps<typeof SizeIndicator>> = {}) {
- return render(<SizeIndicator value="XS" {...props} />);
+ return render(<SizeIndicator value={0} {...props} />);
}
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.getByText('/project/info?id=my-project')).toBeInTheDocument();
+ expect(screen.getByText('/project/information?id=my-project')).toBeInTheDocument();
});
function renderComponentNav(props: Partial<ComponentNavProps> = {}) {
*/
import * as React from 'react';
import Link from '../../../components/common/Link';
+import MetaLink from '../../../components/common/MetaLink';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import DateFromNow from '../../../components/intl/DateFromNow';
import Level from '../../../components/ui/Level';
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;
import withMetricsContext from '../../app/components/metrics/withMetricsContext';
import { translate } from '../../helpers/l10n';
import { BranchLike } from '../../types/branch-like';
-import { ComponentQualifier } from '../../types/component';
+import { ComponentQualifier, isApplication, isProject } from '../../types/component';
import { Feature } from '../../types/features';
import { MetricKey } from '../../types/metrics';
import { Component, Dict, Measure, Metric } from '../../types/types';
const { branchLike, component, currentUser, metrics } = this.props;
const { measures } = this.state;
- const canConfigureNotifications =
- isLoggedIn(currentUser) && component.qualifier === ComponentQualifier.Project;
+ const canConfigureNotifications = isLoggedIn(currentUser) && isProject(component.qualifier);
const canUseBadges =
metrics !== undefined &&
- (component.qualifier === ComponentQualifier.Application ||
- component.qualifier === ComponentQualifier.Project);
+ (isApplication(component.qualifier) || isProject(component.qualifier));
const regulatoryReportFeatureEnabled = this.props.hasFeature(Feature.RegulatoryReport);
- const isApp = component.qualifier === ComponentQualifier.Application;
+ const isApp = isApplication(component.qualifier);
return (
<main>
--- /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 ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
+import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../helpers/testSelector';
+import routes from '../routes';
+
+const componentsMock = new ComponentsServiceMock();
+
+const ui = {
+ projectPageTitle: byRole('heading', { name: 'project.info.title' }),
+ applicationPageTitle: byRole('heading', { name: 'application.info.title' }),
+ qualityGateList: byRole('list', { name: 'project.info.quality_gate' }),
+ qualityProfilesList: byRole('list', { name: 'project.info.qualit_profiles' }),
+ link: byRole('link'),
+ tags: byRole('generic', { name: /tags:/ }),
+ size: byRole('link', { name: /project.info.see_more_info_on_x_locs/ }),
+ newKeyInput: byRole('textbox'),
+ updateInputButton: byRole('button', { name: 'update_verb' }),
+ resetInputButton: byRole('button', { name: 'reset_verb' }),
+};
+
+afterEach(() => {
+ componentsMock.reset();
+});
+
+it('can update project key', async () => {
+ renderProjectInformationApp();
+ expect(await ui.projectPageTitle.find()).toBeInTheDocument();
+});
+
+function renderProjectInformationApp() {
+ return renderAppWithComponentContext(
+ 'project/information',
+ routes,
+ {},
+ { component: componentsMock.components[0].component }
+ );
+}
* 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 classNames from 'classnames';
+import { BasicSeparator, SubTitle } from 'design-system';
+import React, { PropsWithChildren, useContext, useEffect, useState } from 'react';
+import { getProjectLinks } from '../../../api/projectLinks';
+import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
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';
+import { ComponentQualifier, Visibility } from '../../../types/component';
+import { Component, Measure, ProjectLink } from '../../../types/types';
+import { isLoggedIn } from '../../../types/users';
+import MetaDescription from './components/MetaDescription';
+import MetaHome from './components/MetaHome';
+import MetaKey from './components/MetaKey';
+import MetaLinks from './components/MetaLinks';
+import MetaQualityGate from './components/MetaQualityGate';
+import MetaQualityProfiles from './components/MetaQualityProfiles';
+import MetaSize from './components/MetaSize';
+import MetaTags from './components/MetaTags';
+import MetaVisibility from './components/MetaVisibility';
export interface AboutProjectProps {
component: Component;
onComponentChange: (changes: {}) => void;
}
-export function AboutProject(props: AboutProjectProps) {
+export default function AboutProject(props: AboutProjectProps) {
+ const { currentUser } = useContext(CurrentUserContext);
const { component, measures = [] } = props;
-
- const heading = React.useRef<HTMLHeadingElement>(null);
const isApp = component.qualifier === ComponentQualifier.Application;
+ const [links, setLinks] = useState<ProjectLink[] | undefined>(undefined);
- React.useEffect(() => {
- if (heading.current) {
- // a11y: provide focus to the heading when the Project Information is opened.
- heading.current.focus();
+ useEffect(() => {
+ if (!isApp) {
+ getProjectLinks(component.key).then(
+ (links) => setLinks(links),
+ () => {}
+ );
}
- }, [heading]);
+ }, [component.key, isApp]);
return (
<>
<div>
- <h2 className="big-padded bordered-bottom" tabIndex={-1} ref={heading}>
- {translate(isApp ? 'application' : 'project', 'info.title')}
- </h2>
+ <SubTitle>{translate(isApp ? 'application' : 'project', 'about.title')}</SubTitle>
</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}
+ {!isApp &&
+ (component.qualityGate ||
+ (component.qualityProfiles && component.qualityProfiles.length > 0)) && (
+ <ProjectInformationSection className="sw-pt-0">
+ {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
+
+ {component.qualityProfiles && component.qualityProfiles.length > 0 && (
+ <MetaQualityProfiles
+ headerClassName={component.qualityGate ? 'big-spacer-top' : undefined}
+ profiles={component.qualityProfiles}
/>
)}
- </div>
+ </ProjectInformationSection>
+ )}
- {component.description && (
- <p className="it__project-description">{component.description}</p>
- )}
+ <ProjectInformationSection>
+ <MetaKey componentKey={component.key} qualifier={component.qualifier} />
+ </ProjectInformationSection>
- <MetaTags component={component} onComponentChange={props.onComponentChange} />
- </div>
+ {component.visibility === Visibility.Private && (
+ <ProjectInformationSection>
+ <MetaVisibility qualifier={component.qualifier} visibility={component.visibility} />
+ </ProjectInformationSection>
+ )}
- <div className="big-padded bordered-bottom it__project-loc-value">
- <MetaSize component={component} measures={measures} />
- </div>
+ <ProjectInformationSection>
+ <MetaDescription description={component.description} isApp={isApp} />
+ </ProjectInformationSection>
- {!isApp &&
- (component.qualityGate ||
- (component.qualityProfiles && component.qualityProfiles.length > 0)) && (
- <div className="big-padded bordered-bottom">
- {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
+ <ProjectInformationSection>
+ <MetaTags component={component} onComponentChange={props.onComponentChange} />
+ </ProjectInformationSection>
- {component.qualityProfiles && component.qualityProfiles.length > 0 && (
- <MetaQualityProfiles
- headerClassName={component.qualityGate ? 'big-spacer-top' : undefined}
- profiles={component.qualityProfiles}
- />
- )}
- </div>
- )}
+ <ProjectInformationSection>
+ <MetaSize component={component} measures={measures} />
+ </ProjectInformationSection>
- {!isApp && <MetaLinks component={component} />}
+ {!isApp && links && links.length > 0 && (
+ <ProjectInformationSection last={!isLoggedIn(currentUser)}>
+ <MetaLinks links={links} />
+ </ProjectInformationSection>
+ )}
- <div className="big-padded bordered-bottom">
- <MetaKey componentKey={component.key} qualifier={component.qualifier} />
- </div>
- </div>
+ {isLoggedIn(currentUser) && (
+ <ProjectInformationSection last>
+ <MetaHome componentKey={component.key} currentUser={currentUser} isApp={isApp} />
+ </ProjectInformationSection>
+ )}
</>
);
}
-export default AboutProject;
+interface ProjectInformationSectionProps {
+ last?: boolean;
+ className?: string;
+}
+
+function ProjectInformationSection(props: PropsWithChildren<ProjectInformationSectionProps>) {
+ const { children, className, last = false } = props;
+ return (
+ <>
+ <div className={classNames('sw-py-6', className)}>{children}</div>
+ {!last && <BasicSeparator />}
+ </>
+ );
+}
--- /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 { TextMuted } from 'design-system';
+import * as React from 'react';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+ description?: string;
+ isApp?: boolean;
+}
+
+export default function MetaDescription({ description, isApp }: Props) {
+ return (
+ <>
+ <h3>{translate('project.info.description')}</h3>
+ {description ? (
+ <p className="it__project-description">{description}</p>
+ ) : (
+ <TextMuted text={translate(isApp ? 'application' : 'project', 'info.empty_description')} />
+ )}
+ </>
+ );
+}
--- /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 { Checkbox, HelperHintIcon } from 'design-system';
+import React, { useContext } from 'react';
+import { setHomePage } from '../../../../api/users';
+import { CurrentUserContext } from '../../../../app/components/current-user/CurrentUserContext';
+import HelpTooltip from '../../../../components/controls/HelpTooltip';
+import { DEFAULT_HOMEPAGE } from '../../../../components/controls/HomePageSelect';
+import { translate } from '../../../../helpers/l10n';
+import { isSameHomePage } from '../../../../helpers/users';
+import { HomePage, LoggedInUser, isLoggedIn } from '../../../../types/users';
+
+export interface MetaHomeProps {
+ componentKey: string;
+ currentUser: LoggedInUser;
+ isApp?: boolean;
+}
+
+export default function MetaHome({ componentKey, currentUser, isApp }: MetaHomeProps) {
+ const { updateCurrentUserHomepage } = useContext(CurrentUserContext);
+ const currentPage: HomePage = {
+ component: componentKey,
+ type: 'PROJECT',
+ branch: undefined,
+ };
+
+ const setCurrentUserHomepage = async (homepage: HomePage) => {
+ if (isLoggedIn(currentUser)) {
+ await setHomePage(homepage);
+
+ updateCurrentUserHomepage(homepage);
+ }
+ };
+
+ const handleClick = (value: boolean) => {
+ setCurrentUserHomepage(value ? currentPage : DEFAULT_HOMEPAGE);
+ };
+
+ return (
+ <>
+ <div className="sw-flex sw-items-center">
+ <h3>{translate('project.info.make_home.title')}</h3>
+ <HelpTooltip
+ className="sw-ml-1"
+ overlay={
+ <p className="sw-max-w-abs-250">
+ {translate(isApp ? 'application' : 'project', 'info.make_home.tooltip')}
+ </p>
+ }
+ >
+ <HelperHintIcon />
+ </HelpTooltip>
+ </div>
+ <Checkbox
+ checked={
+ currentUser.homepage !== undefined && isSameHomePage(currentUser.homepage, currentPage)
+ }
+ onCheck={handleClick}
+ >
+ <span className="sw-ml-2">
+ {translate(isApp ? 'application' : 'project', 'info.make_home.label')}
+ </span>
+ </Checkbox>
+ </>
+ );
+}
--- /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 { ClipboardIconButton, CodeSnippet, HelperHintIcon } from 'design-system';
+import * as React from 'react';
+import HelpTooltip from '../../../../components/controls/HelpTooltip';
+import { translate } from '../../../../helpers/l10n';
+
+interface MetaKeyProps {
+ componentKey: string;
+ qualifier: string;
+}
+
+export default function MetaKey({ componentKey, qualifier }: MetaKeyProps) {
+ return (
+ <>
+ <div className="sw-flex sw-items-center">
+ <h3>{translate('overview.project_key', qualifier)}</h3>
+ <HelpTooltip
+ className="sw-ml-1"
+ overlay={
+ <p className="sw-max-w-abs-250">
+ {translate('overview.project_key.tooltip', qualifier)}
+ </p>
+ }
+ >
+ <HelperHintIcon />
+ </HelpTooltip>
+ </div>
+ <div className="sw-w-full">
+ <div className="sw-flex sw-gap-2 sw-items-center sw-min-w-0">
+ <CodeSnippet
+ className="sw-min-w-0"
+ isOneLine
+ noCopy
+ highlight={false}
+ snippet={componentKey}
+ />
+ <ClipboardIconButton copyValue={componentKey} />
+ </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.
+ */
+import React from 'react';
+import MetaLink from '../../../../components/common/MetaLink';
+import { translate } from '../../../../helpers/l10n';
+import { orderLinks } from '../../../../helpers/projectLinks';
+import { ProjectLink } from '../../../../types/types';
+
+interface Props {
+ links: ProjectLink[];
+}
+
+export default function MetaLinks({ links }: Props) {
+ const orderedLinks = orderLinks(links);
+
+ return (
+ <>
+ <h3>{translate('overview.external_links')}</h3>
+ <ul className="project-info-list">
+ {orderedLinks.map((link) => (
+ <MetaLink miui key={link.id} link={link} />
+ ))}
+ </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 { Link } from 'design-system';
+import * as React from 'react';
+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 id="quality-gate-header">{translate('project.info.quality_gate')}</h3>
+
+ <ul className="project-info-list" aria-labelledby="quality-gate-header">
+ <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 { Badge, Link } from 'design-system';
+import React, { useContext, useEffect } from 'react';
+import { searchRules } from '../../../../api/rules';
+import { LanguagesContext } from '../../../../app/components/languages/LanguagesContext';
+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;
+ profiles: ComponentQualityProfile[];
+}
+
+export function MetaQualityProfiles({ headerClassName, profiles }: Props) {
+ const [deprecatedByKey, setDeprecatedByKey] = React.useState<Dict<number>>({});
+ const languages = useContext(LanguagesContext);
+
+ useEffect(() => {
+ const existingProfiles = profiles.filter((p) => !p.deleted);
+ const requests = existingProfiles.map((profile) => {
+ const data = {
+ activation: 'true',
+ ps: 1,
+ qprofile: profile.key,
+ statuses: 'DEPRECATED',
+ };
+ return searchRules(data).then((r) => r.paging.total);
+ });
+ Promise.all(requests).then(
+ (responses) => {
+ const deprecatedByKey: Dict<number> = {};
+ responses.forEach((count, i) => {
+ const profileKey = existingProfiles[i].key;
+ deprecatedByKey[profileKey] = count;
+ });
+ setDeprecatedByKey(deprecatedByKey);
+ },
+ () => {}
+ );
+ }, [profiles]);
+
+ return (
+ <>
+ <h3 className={headerClassName} id="quality-profiles-list">
+ {translate('overview.quality_profiles')}
+ </h3>
+ <ul className="project-info-list" aria-labelledby="quality-profiles-list">
+ {profiles.map((profile) => (
+ <ProfileItem
+ key={profile.key}
+ profile={profile}
+ languages={languages}
+ deprecatedByKey={deprecatedByKey}
+ />
+ ))}
+ </ul>
+ </>
+ );
+}
+
+function ProfileItem({
+ profile,
+ languages,
+ deprecatedByKey,
+}: {
+ profile: ComponentQualityProfile;
+ languages: Languages;
+ deprecatedByKey: Dict<number>;
+}) {
+ const languageFromStore = languages[profile.language];
+ const languageName = languageFromStore ? languageFromStore.name : profile.language;
+ const count = deprecatedByKey[profile.key] || 0;
+
+ return (
+ <li>
+ <div className="sw-grid sw-grid-cols-[1fr_3fr]">
+ <span>{languageName}</span>
+ <div>
+ {profile.deleted ? (
+ <Tooltip
+ key={profile.key}
+ overlay={translateWithParameters('overview.deleted_profile', profile.name)}
+ >
+ <div className="project-info-deleted-profile">{profile.name}</div>
+ </Tooltip>
+ ) : (
+ <>
+ <Link to={getQualityProfileUrl(profile.name, profile.language)}>
+ <span
+ aria-label={translateWithParameters(
+ 'overview.link_to_x_profile_y',
+ languageName,
+ profile.name
+ )}
+ >
+ {profile.name}
+ </span>
+ </Link>
+ {count > 0 && (
+ <Tooltip
+ key={profile.key}
+ overlay={translateWithParameters('overview.deprecated_profile', count)}
+ >
+ <span>
+ <Badge className="sw-ml-6" variant="deleted">
+ {translate('deprecated')}
+ </Badge>
+ </span>
+ </Tooltip>
+ )}
+ </>
+ )}
+ </div>
+ </div>
+ </li>
+ );
+}
+
+export default 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 { DrilldownLink, SizeIndicator } from 'design-system';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+import { formatMeasure, localizeMetric } from '../../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../../helpers/urls';
+import { ComponentQualifier } from '../../../../types/component';
+import { MetricKey } from '../../../../types/metrics';
+import { Component, Measure } from '../../../../types/types';
+
+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;
+ const url = getComponentDrilldownUrl({
+ componentKey: component.key,
+ metric: MetricKey.ncloc,
+ listView: true,
+ });
+
+ return (
+ <>
+ <div className="sw-flex sw-items-center">
+ <h3>{localizeMetric(MetricKey.ncloc)}</h3>
+ <span className="sw-ml-1 small">({translate('project.info.main_branch')})</span>
+ </div>
+ <div className="sw-flex sw-items-center">
+ {ncloc && ncloc.value ? (
+ <>
+ <DrilldownLink className="huge" to={url}>
+ <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">
+ <SizeIndicator value={Number(ncloc.value)} size="xs" />
+ </span>
+ </>
+ ) : (
+ <span>0</span>
+ )}
+
+ {isApp && (
+ <span className="huge-spacer-left display-inline-flex-center">
+ {projects ? (
+ <DrilldownLink to={url}>
+ <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 { Tags, TagsSelector } from 'design-system';
+import { difference, without } from 'lodash';
+import React, { useState } from 'react';
+import { searchProjectTags, setApplicationTags, setProjectTags } from '../../../../api/components';
+import Tooltip from '../../../../components/controls/Tooltip';
+import { PopupPlacement } from '../../../../components/ui/popups';
+import { translate } from '../../../../helpers/l10n';
+import { ComponentQualifier } from '../../../../types/component';
+import { Component } from '../../../../types/types';
+
+interface Props {
+ component: Component;
+ onComponentChange: (changes: {}) => void;
+}
+
+export default function MetaTags(props: Props) {
+ const [open, setOpen] = React.useState(false);
+
+ const canUpdateTags = () => {
+ const { configuration } = props.component;
+ return configuration && configuration.showSettings;
+ };
+
+ const setTags = (values: string[]) => {
+ const { component } = props;
+
+ if (component.qualifier === ComponentQualifier.Application) {
+ return setApplicationTags({
+ application: component.key,
+ tags: values.join(','),
+ });
+ }
+
+ return setProjectTags({
+ project: component.key,
+ tags: values.join(','),
+ });
+ };
+
+ const handleSetProjectTags = (values: string[]) => {
+ setTags(values).then(
+ () => props.onComponentChange({ tags: values }),
+ () => {}
+ );
+ };
+
+ const tags = props.component.tags || [];
+
+ return (
+ <>
+ <h3>{translate('tags')}</h3>
+ <Tags
+ allowUpdate={canUpdateTags()}
+ ariaTagsListLabel={translate('tags')}
+ className="js-issue-edit-tags"
+ emptyText={translate('no_tags')}
+ overlay={<MetaTagsSelector selectedTags={tags} setProjectTags={handleSetProjectTags} />}
+ popupPlacement={PopupPlacement.Bottom}
+ tags={tags}
+ tagsToDisplay={2}
+ tooltip={Tooltip}
+ open={open}
+ onClose={() => setOpen(false)}
+ />
+ </>
+ );
+}
+
+interface MetaTagsSelectorProps {
+ selectedTags: string[];
+ setProjectTags: (tags: string[]) => void;
+}
+
+const LIST_SIZE = 10;
+
+function MetaTagsSelector({ selectedTags, setProjectTags }: MetaTagsSelectorProps) {
+ const [searchResult, setSearchResult] = useState<string[]>([]);
+ const availableTags = difference(searchResult, selectedTags);
+
+ const onSearch = (query: string) => {
+ return searchProjectTags({
+ q: query,
+ ps: Math.min(selectedTags.length - 1 + LIST_SIZE, 100),
+ }).then(
+ ({ tags }) => setSearchResult(tags),
+ () => {}
+ );
+ };
+
+ const onSelect = (tag: string) => {
+ setProjectTags([...selectedTags, tag]);
+ };
+
+ const onUnselect = (tag: string) => {
+ setProjectTags(without(selectedTags, tag));
+ };
+
+ return (
+ <TagsSelector
+ headerLabel={translate('tags')}
+ searchInputAriaLabel={translate('search.search_for_tags')}
+ clearIconAriaLabel={translate('clear')}
+ createElementLabel={translate('issue.create_tag')}
+ noResultsLabel={translate('no_results')}
+ onSearch={onSearch}
+ onSelect={onSelect}
+ onUnselect={onUnselect}
+ selectedTags={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 * as React from 'react';
+import PrivacyBadgeContainer from '../../../../components/common/PrivacyBadgeContainer';
+import { translate } from '../../../../helpers/l10n';
+import { Visibility } from '../../../../types/component';
+
+interface Props {
+ qualifier: string;
+ visibility: Visibility;
+}
+
+export default function MetaVisibility({ qualifier, visibility }: Props) {
+ return (
+ <>
+ <h3>{translate('visibility')}</h3>
+ <PrivacyBadgeContainer qualifier={qualifier} visibility={visibility} />
+ </>
+ );
+}
--- /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 from '../MetaKey';
+
+it('should render correctly', () => {
+ renderMetaKey();
+ expect(
+ screen.getByText(`overview.project_key.${ComponentQualifier.Project}`)
+ ).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument();
+});
+
+function renderMetaKey(props: Partial<Parameters<typeof MetaKey>[0]> = {}) {
+ 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 { LanguagesContext } from '../../../../../app/components/languages/LanguagesContext';
+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<Parameters<typeof MetaQualityProfiles>[0]> = {}
+) {
+ return renderComponent(
+ <LanguagesContext.Provider value={{ css: mockLanguage() }}>
+ <MetaQualityProfiles
+ 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}
+ />
+ </LanguagesContext.Provider>
+ );
+}
--- /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 { act, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { 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().mockResolvedValue({ tags: ['best', 'useless'] }),
+}));
+
+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();
+
+ 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 act(() => user.click(screen.getByRole('button', { name: 'foo, bar +' })));
+
+ expect(await screen.findByRole('checkbox', { name: 'best' })).toBeInTheDocument();
+
+ await user.click(screen.getByRole('checkbox', { name: '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.getByRole('checkbox', { name: '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 act(() => user.click(screen.getByRole('button', { name: 'no_tags +' })));
+
+ await user.click(await screen.findByRole('checkbox', { name: 'best' }));
+
+ expect(setProjectTags).not.toHaveBeenCalled();
+ expect(setApplicationTags).toHaveBeenCalled();
+});
+
+function renderMetaTags(overrides: Partial<Parameters<typeof MetaTags>[0]> = {}) {
+ 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 { 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 { CloseIcon, InputField, InteractiveIcon, Link } from 'design-system';
+import React, { useState } from 'react';
+import isValidUri from '../../app/utils/isValidUri';
+import { translate } from '../../helpers/l10n';
+import { getLinkName } from '../../helpers/projectLinks';
+import { ProjectLink } from '../../types/types';
+import { ClearButton } from '../controls/buttons';
+import ProjectLinkIcon from '../icons/ProjectLinkIcon';
+
+interface Props {
+ iconOnly?: boolean;
+ link: ProjectLink;
+ // TODO Remove this prop when all links are migrated to the new design
+ miui?: boolean;
+}
+
+export default function MetaLink({ iconOnly, link, miui }: Props) {
+ const [expanded, setExpanded] = useState(false);
+
+ const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ setExpanded((expanded) => !expanded);
+ };
+
+ const handleCollapse = () => {
+ setExpanded(false);
+ };
+
+ const handleSelect = (event: React.MouseEvent<HTMLInputElement>) => {
+ event.currentTarget.select();
+ };
+
+ const linkTitle = getLinkName(link);
+ const isValid = isValidUri(link.url);
+ return miui ? (
+ <li>
+ <Link
+ isExternal
+ to={link.url}
+ preventDefault={!isValid}
+ onClick={isValid ? undefined : handleClick}
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ icon={<ProjectLinkIcon miui className="little-spacer-right" type={link.type} />}
+ >
+ {!iconOnly && linkTitle}
+ </Link>
+
+ {expanded && (
+ <div className="little-spacer-top display-flex-center">
+ <InputField
+ className="overview-key width-80"
+ onClick={handleSelect}
+ readOnly
+ type="text"
+ value={link.url}
+ />
+ <InteractiveIcon
+ Icon={CloseIcon}
+ aria-label={translate('hide')}
+ className="sw-ml-1"
+ onClick={handleCollapse}
+ />
+ </div>
+ )}
+ </li>
+ ) : (
+ <li>
+ <a
+ className="link-no-underline"
+ href={isValid ? link.url : undefined}
+ onClick={isValid ? undefined : handleClick}
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ title={linkTitle}
+ >
+ <ProjectLinkIcon className="little-spacer-right" type={link.type} />
+ {!iconOnly && linkTitle}
+ </a>
+ {expanded && (
+ <div className="little-spacer-top display-flex-center">
+ <input
+ className="overview-key width-80"
+ onClick={handleSelect}
+ readOnly
+ type="text"
+ value={link.url}
+ />
+ <ClearButton className="little-spacer-left" onClick={handleCollapse} />
+ </div>
+ )}
+ </li>
+ );
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { LinkExternalIcon } from '@primer/octicons-react';
+import { HomeIcon } from 'design-system';
import * as React from 'react';
import BugTrackerIcon from './BugTrackerIcon';
import ContinuousIntegrationIcon from './ContinuousIntegrationIcon';
interface ProjectLinkIconProps {
type: string;
+ miui?: boolean;
}
-export default function ProjectLinkIcon({ type, ...iconProps }: IconProps & ProjectLinkIconProps) {
- switch (type) {
- case 'issue':
- return <BugTrackerIcon {...iconProps} />;
- case 'homepage':
- return <HouseIcon {...iconProps} />;
- case 'ci':
- return <ContinuousIntegrationIcon {...iconProps} />;
- case 'scm':
- return <SCMIcon {...iconProps} />;
- default:
- return <DetachIcon {...iconProps} />;
- }
+export default function ProjectLinkIcon({
+ miui,
+ type,
+ ...iconProps
+}: IconProps & ProjectLinkIconProps) {
+ const getIcon = (): any => {
+ switch (type) {
+ case 'issue':
+ return BugTrackerIcon;
+ case 'homepage':
+ return miui ? HomeIcon : HouseIcon;
+ case 'ci':
+ return ContinuousIntegrationIcon;
+ case 'scm':
+ return SCMIcon;
+ default:
+ return miui ? LinkExternalIcon : DetachIcon;
+ }
+ };
+
+ const Icon = getIcon();
+
+ return <Icon {...iconProps} />;
}
project.info.title=Project Information
application.info.title=Application Information
+project.about.title=About this Project
+application.about.title=About this Application
project.info.description=Description
+project.info.empty_description=No description added for this project.
+application.info.empty_description=No description added for this application.
project.info.quality_gate=Quality Gate used
project.info.to_notifications=Set notifications
project.info.notifications=Notifications
project.info.main_branch=Main branch
project.info.see_more_info_on_x_locs=See more information on your {0} lines of code
+project.info.make_home.title=Use as homepage
+project.info.make_home.label=Make this project my homepage
+application.info.make_home.label=Make this application my homepage
+project.info.make_home.tooltip=This means you'll be redirected to this project whenever you log in to sonarqube or click on the top-left SonarQube logo.
+application.info.make_home.tooltip=This means you'll be redirected to this application whenever you log in to sonarqube or click on the top-left SonarQube logo.
+overview.project_key.tooltip.TRK=Your project key is a unique identifier for your project. If you are using Maven, make sure the key matches the "groupId:artifactId" format.
+overview.project_key.tooltip.APP=Your application key is a unique identifier for your application.
#------------------------------------------------------------------------------
#