Переглянути джерело

SONAR-19613 Migrate About this project to MIUI

tags/10.2.0.77647
Viktor Vorona 1 рік тому
джерело
коміт
4e7d35f613
27 змінених файлів з 898 додано та 670 видалено
  1. 17
    3
      server/sonar-web/design-system/src/components/SizeIndicator.tsx
  2. 11
    8
      server/sonar-web/design-system/src/components/__tests__/SizeIndicator-test.tsx
  3. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
  4. 1
    1
      server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx
  5. 4
    6
      server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx
  6. 56
    0
      server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx
  7. 81
    57
      server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx
  8. 40
    0
      server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx
  9. 83
    0
      server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaHome.tsx
  10. 60
    0
      server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx
  11. 43
    0
      server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx
  12. 5
    5
      server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx
  13. 140
    0
      server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityProfiles.tsx
  14. 19
    14
      server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx
  15. 131
    0
      server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaTags.tsx
  16. 8
    20
      server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx
  17. 6
    8
      server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaKey-test.tsx
  18. 28
    24
      server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx
  19. 14
    15
      server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaTags-test.tsx
  20. 0
    84
      server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLink.tsx
  21. 0
    84
      server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLinks.tsx
  22. 0
    151
      server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityProfiles.tsx
  23. 0
    93
      server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTags.tsx
  24. 0
    83
      server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTagsSelector.tsx
  25. 113
    0
      server/sonar-web/src/main/js/components/common/MetaLink.tsx
  26. 26
    13
      server/sonar-web/src/main/js/components/icons/ProjectLinkIcon.tsx
  27. 11
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 17
- 3
server/sonar-web/design-system/src/components/SizeIndicator.tsx Переглянути файл

@@ -18,13 +18,14 @@
* 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 = {
@@ -34,9 +35,22 @@ 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>
);
}
@@ -44,7 +58,7 @@ export function SizeIndicator({ size = 'sm', value }: Props) {
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')};


+ 11
- 8
server/sonar-web/design-system/src/components/__tests__/SizeIndicator-test.tsx Переглянути файл

@@ -25,14 +25,17 @@ import { FCProps } from '../../types/misc';
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} />);
}

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx Переглянути файл

@@ -67,7 +67,7 @@ it('renders correctly when the project binding is incorrect', () => {
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> = {}) {

+ 1
- 1
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx Переглянути файл

@@ -19,6 +19,7 @@
*/
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';
@@ -26,7 +27,6 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
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;

+ 4
- 6
server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx Переглянути файл

@@ -28,7 +28,7 @@ import withCurrentUserContext from '../../app/components/current-user/withCurren
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';
@@ -82,14 +82,12 @@ export class ProjectInformationApp extends React.PureComponent<Props, State> {
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>

+ 56
- 0
server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx Переглянути файл

@@ -0,0 +1,56 @@
/*
* 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 }
);
}

+ 81
- 57
server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx Переглянути файл

@@ -17,17 +17,24 @@
* 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;
@@ -35,73 +42,90 @@ export interface AboutProjectProps {
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 />}
</>
);
}

+ 40
- 0
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx Переглянути файл

@@ -0,0 +1,40 @@
/*
* 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')} />
)}
</>
);
}

+ 83
- 0
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaHome.tsx Переглянути файл

@@ -0,0 +1,83 @@
/*
* 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>
</>
);
}

+ 60
- 0
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx Переглянути файл

@@ -0,0 +1,60 @@
/*
* 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>
</>
);
}

+ 43
- 0
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx Переглянути файл

@@ -0,0 +1,43 @@
/*
* 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>
</>
);
}

server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityGate.tsx → server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx Переглянути файл

@@ -17,10 +17,10 @@
* 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 Link from '../../../components/common/Link';
import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
import { translate } from '../../../../helpers/l10n';
import { getQualityGateUrl } from '../../../../helpers/urls';

interface Props {
qualityGate: { isDefault?: boolean; name: string };
@@ -29,9 +29,9 @@ interface Props {
export default function MetaQualityGate({ qualityGate }: Props) {
return (
<>
<h3>{translate('project.info.quality_gate')}</h3>
<h3 id="quality-gate-header">{translate('project.info.quality_gate')}</h3>

<ul className="project-info-list">
<ul className="project-info-list" aria-labelledby="quality-gate-header">
<li>
{qualityGate.isDefault && (
<span className="note spacer-right">({translate('default')})</span>

+ 140
- 0
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityProfiles.tsx Переглянути файл

@@ -0,0 +1,140 @@
/*
* 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;

server/sonar-web/src/main/js/apps/projectInformation/meta/MetaSize.tsx → server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx Переглянути файл

@@ -17,16 +17,16 @@
* 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 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';
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';

export interface MetaSizeProps {
interface MetaSizeProps {
component: Component;
measures: Measure[];
}
@@ -37,17 +37,22 @@ export default function MetaSize({ component, measures }: MetaSizeProps) {
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="display-flex-row display-inline-flex-baseline">
<div className="sw-flex sw-items-center">
<h3>{localizeMetric(MetricKey.ncloc)}</h3>
<span className="spacer-left small">({translate('project.info.main_branch')})</span>
<span className="sw-ml-1 small">({translate('project.info.main_branch')})</span>
</div>
<div className="display-flex-center">
<div className="sw-flex sw-items-center">
{ncloc && ncloc.value ? (
<>
<DrilldownLink className="huge" component={component.key} metric={MetricKey.ncloc}>
<DrilldownLink className="huge" to={url}>
<span
aria-label={translateWithParameters(
'project.info.see_more_info_on_x_locs',
@@ -59,7 +64,7 @@ export default function MetaSize({ component, measures }: MetaSizeProps) {
</DrilldownLink>

<span className="spacer-left">
<SizeRating value={Number(ncloc.value)} />
<SizeIndicator value={Number(ncloc.value)} size="xs" />
</span>
</>
) : (
@@ -69,7 +74,7 @@ export default function MetaSize({ component, measures }: MetaSizeProps) {
{isApp && (
<span className="huge-spacer-left display-inline-flex-center">
{projects ? (
<DrilldownLink component={component.key} metric={MetricKey.projects}>
<DrilldownLink to={url}>
<span className="big">{formatMeasure(projects.value, 'SHORT_INT')}</span>
</DrilldownLink>
) : (

+ 131
- 0
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaTags.tsx Переглянути файл

@@ -0,0 +1,131 @@
/*
* 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}
/>
);
}

server/sonar-web/src/main/js/apps/projectInformation/meta/MetaKey.tsx → server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx Переглянути файл

@@ -18,32 +18,20 @@
* 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';
import PrivacyBadgeContainer from '../../../../components/common/PrivacyBadgeContainer';
import { translate } from '../../../../helpers/l10n';
import { Visibility } from '../../../../types/component';

export interface MetaKeyProps {
componentKey: string;
interface Props {
qualifier: string;
visibility: Visibility;
}

export default function MetaKey({ componentKey, qualifier }: MetaKeyProps) {
export default function MetaVisibility({ qualifier, visibility }: Props) {
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>
<h3>{translate('visibility')}</h3>
<PrivacyBadgeContainer qualifier={qualifier} visibility={visibility} />
</>
);
}

server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaKey-test.tsx → server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaKey-test.tsx Переглянути файл

@@ -19,21 +19,19 @@
*/
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';
import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
import { ComponentQualifier } from '../../../../../types/component';
import MetaKey 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' })
screen.getByText(`overview.project_key.${ComponentQualifier.Project}`)
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument();
});

function renderMetaKey(props: Partial<MetaKeyProps> = {}) {
function renderMetaKey(props: Partial<Parameters<typeof MetaKey>[0]> = {}) {
return renderComponent(
<MetaKey componentKey="foo" qualifier={ComponentQualifier.Project} {...props} />
);

server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx → server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx Переглянути файл

@@ -19,14 +19,15 @@
*/
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 { 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', () => {
jest.mock('../../../../../api/rules', () => {
return {
searchRules: jest.fn().mockResolvedValue({
total: 10,
@@ -57,24 +58,27 @@ it('should render correctly', async () => {
expect(screen.getByText('overview.deprecated_profile.10')).toBeInTheDocument();
});

function renderMetaQualityprofiles(overrides: Partial<MetaQualityProfiles['props']> = {}) {
function renderMetaQualityprofiles(
overrides: Partial<Parameters<typeof MetaQualityProfiles>[0]> = {}
) {
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}
/>
<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>
);
}

server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaTags-test.tsx → server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaTags-test.tsx Переглянути файл

@@ -17,19 +17,19 @@
* 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 { act, 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 { 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', () => ({
jest.mock('../../../../../api/components', () => ({
setApplicationTags: jest.fn().mockResolvedValue(true),
setProjectTags: jest.fn().mockResolvedValue(true),
searchProjectTags: jest.fn(),
searchProjectTags: jest.fn().mockResolvedValue({ tags: ['best', 'useless'] }),
}));

beforeEach(() => {
@@ -45,7 +45,6 @@ it('should render without tags and admin rights', async () => {

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({
@@ -62,11 +61,11 @@ it('should allow to edit tags for a project', async () => {
expect(await screen.findByText('foo, bar')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument();

await user.click(screen.getByRole('button', { name: 'tags_list_x.foo, bar' }));
await act(() => user.click(screen.getByRole('button', { name: 'foo, bar +' })));

expect(await screen.findByText('best')).toBeInTheDocument();
expect(await screen.findByRole('checkbox', { name: 'best' })).toBeInTheDocument();

await user.click(screen.getByText('best'));
await user.click(screen.getByRole('checkbox', { name: 'best' }));
expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo', 'bar', 'best'] });

onComponentChange.mockClear();
@@ -74,7 +73,7 @@ it('should allow to edit tags for a project', async () => {
/*
* Since we're not actually updating the tags, we're back to having the foo, bar only
*/
await user.click(screen.getByText('bar'));
await user.click(screen.getByRole('checkbox', { name: 'bar' }));
expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo'] });

expect(setProjectTags).toHaveBeenCalled();
@@ -93,15 +92,15 @@ it('should set tags for an app', async () => {
}),
});

await user.click(screen.getByRole('button', { name: 'tags_list_x.no_tags' }));
await act(() => user.click(screen.getByRole('button', { name: 'no_tags +' })));

await user.click(screen.getByText('best'));
await user.click(await screen.findByRole('checkbox', { name: 'best' }));

expect(setProjectTags).not.toHaveBeenCalled();
expect(setApplicationTags).toHaveBeenCalled();
});

function renderMetaTags(overrides: Partial<MetaTags['props']> = {}) {
function renderMetaTags(overrides: Partial<Parameters<typeof MetaTags>[0]> = {}) {
const component = mockComponent({
configuration: {
showSettings: false,

+ 0
- 84
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLink.tsx Переглянути файл

@@ -1,84 +0,0 @@
/*
* 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>
);
}
}

+ 0
- 84
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLinks.tsx Переглянути файл

@@ -1,84 +0,0 @@
/*
* 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>
);
}
}

+ 0
- 151
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityProfiles.tsx Переглянути файл

@@ -1,151 +0,0 @@
/*
* 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);

+ 0
- 93
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTags.tsx Переглянути файл

@@ -1,93 +0,0 @@
/*
* 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>
);
}
}

+ 0
- 83
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTagsSelector.tsx Переглянути файл

@@ -1,83 +0,0 @@
/*
* 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}
/>
);
}
}

+ 113
- 0
server/sonar-web/src/main/js/components/common/MetaLink.tsx Переглянути файл

@@ -0,0 +1,113 @@
/*
* 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>
);
}

+ 26
- 13
server/sonar-web/src/main/js/components/icons/ProjectLinkIcon.tsx Переглянути файл

@@ -17,6 +17,8 @@
* 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';
@@ -27,19 +29,30 @@ import SCMIcon from './SCMIcon';

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} />;
}

+ 11
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -1825,12 +1825,23 @@ projects_management.filter_by_visibility=Filter by visibility

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.

#------------------------------------------------------------------------------
#

Завантаження…
Відмінити
Зберегти