Browse Source

SONAR-21298 Showcase Echoes' Link and LinkStandalone components in a few places

tags/10.5.0.89998
David Cho-Lerat 2 months ago
parent
commit
b702895d0e

+ 32
- 0
server/sonar-web/design-system/src/components/Link.tsx View File

@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { css } from '@emotion/react';
import styled from '@emotion/styled';
import React, { HTMLAttributeAnchorTarget } from 'react';
@@ -26,6 +27,17 @@ import { themeBorder, themeColor } from '../helpers/theme';
import { TooltipWrapperInner } from './Tooltip';
import { OpenNewTabIcon } from './icons/OpenNewTabIcon';

/** @deprecated Use LinkProps from Echoes instead.
*
* Some of the props have changed or been renamed:
* - `blurAfterClick` is now `shouldBlurAfterClick`
* - ~`disabled`~ doesn't exist anymore, a disabled link is just a regular text
* - `forceExternal` is now `isExternal`
* - `icon` is now `iconLeft` and can only be used with LinkStandalone
* - `preventDefault` is now `shouldPreventDefault`
* - `showExternalIcon` is now `hasExternalIcon`
* - `stopPropagation` is now `shouldStopPropagation`
*/
export interface LinkProps extends RouterLinkProps {
blurAfterClick?: boolean;
disabled?: boolean;
@@ -109,6 +121,8 @@ const ExternalIcon = styled(OpenNewTabIcon)`
color: ${themeColor('linkExternalIcon')};
`;

/** @deprecated Use either Link or LinkStandalone from Echoes, or react-router-dom's Link instead.
*/
export const BaseLink = React.forwardRef(BaseLinkWithRef);

const StyledBaseLink = styled(BaseLink)`
@@ -150,6 +164,8 @@ const StyledBaseLink = styled(BaseLink)`
`};
`;

/** @deprecated Use either Link or LinkStandalone from Echoes instead.
*/
export const NakedLink = styled(BaseLink)`
border-bottom: none;
padding-bottom: 1px;
@@ -167,6 +183,8 @@ export const NakedLink = styled(BaseLink)`
}`};
`;

/** @deprecated Use either Link or LinkStandalone from Echoes instead.
*/
export const DrilldownLink = styled(StyledBaseLink)`
${tw`sw-heading-lg`}
${tw`sw-tracking-tight`}
@@ -184,6 +202,8 @@ export const DrilldownLink = styled(StyledBaseLink)`

DrilldownLink.displayName = 'DrilldownLink';

/** @deprecated Use either Link or LinkStandalone from Echoes instead.
*/
export const HoverLink = styled(StyledBaseLink)`
text-decoration: none;

@@ -203,6 +223,8 @@ export const HoverLink = styled(StyledBaseLink)`
`;
HoverLink.displayName = 'HoverLink';

/** @deprecated Use either Link or LinkStandalone from Echoes instead.
*/
export const LinkBox = styled(StyledBaseLink)`
text-decoration: none;

@@ -215,6 +237,8 @@ export const LinkBox = styled(StyledBaseLink)`
`;
LinkBox.displayName = 'LinkBox';

/** @deprecated Use either Link or LinkStandalone from Echoes instead.
*/
export const DiscreetLinkBox = styled(StyledBaseLink)`
text-decoration: none;

@@ -229,11 +253,15 @@ export const DiscreetLinkBox = styled(StyledBaseLink)`
`;
LinkBox.displayName = 'DiscreetLinkBox';

/** @deprecated Use either Link or LinkStandalone from Echoes instead.
*/
export const DiscreetLink = styled(HoverLink)`
--border: ${themeBorder('default', 'linkDiscreet')};
`;
DiscreetLink.displayName = 'DiscreetLink';

/** @deprecated Use either Link or LinkStandalone from Echoes instead.
*/
export const ContentLink = styled(HoverLink)`
--color: ${themeColor('pageTitle')};
--border: ${themeBorder('default', 'contentLinkBorder')};
@@ -249,6 +277,8 @@ export const ContentLink = styled(HoverLink)`
`;
ContentLink.displayName = 'ContentLink';

/** @deprecated Use either Link or LinkStandalone from Echoes instead.
*/
export const StandoutLink = styled(StyledBaseLink)`
${tw`sw-font-semibold`}
${tw`sw-no-underline`}
@@ -267,6 +297,8 @@ export const StandoutLink = styled(StyledBaseLink)`
`;
StandoutLink.displayName = 'StandoutLink';

/** @deprecated Use either Link or LinkStandalone from Echoes instead.
*/
export const IssueIndicatorLink = styled(BaseLink)`
color: ${themeColor('codeLineMeta')};
text-decoration: none;

+ 34
- 12
server/sonar-web/src/main/js/app/components/GlobalFooter.tsx View File

@@ -17,9 +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 styled from '@emotion/styled';
import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
import {
DiscreetLink,
FlagMessage,
LAYOUT_VIEWPORT_MIN_WIDTH,
PageContentFontWrapper,
@@ -38,7 +39,7 @@ interface GlobalFooterProps {
hideLoggedInInfo?: boolean;
}

export default function GlobalFooter({ hideLoggedInInfo }: GlobalFooterProps) {
export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooterProps>) {
const appState = React.useContext(AppStateContext);
const currentEdition = appState?.edition && getEdition(appState.edition);

@@ -53,7 +54,9 @@ export default function GlobalFooter({ hideLoggedInInfo }: GlobalFooterProps) {
<span className="sw-body-md-highlight">
{translate('footer.production_database_warning')}
</span>

<br />

<InstanceMessage message={translate('footer.production_database_explanation')} />
</p>
</FlagMessage>
@@ -64,32 +67,51 @@ export default function GlobalFooter({ hideLoggedInInfo }: GlobalFooterProps) {

<ul className="sw-flex sw-items-center sw-gap-3 sw-ml-4">
{!hideLoggedInInfo && currentEdition && <li>{currentEdition.name}</li>}

{!hideLoggedInInfo && appState?.version && (
<li className="sw-code">
{translateWithParameters('footer.version_x', appState.version)}
</li>
)}

<li>
<DiscreetLink to="https://www.gnu.org/licenses/lgpl-3.0.txt">
<LinkStandalone
highlight={LinkHighlight.CurrentColor}
to="https://www.gnu.org/licenses/lgpl-3.0.txt"
>
{translate('footer.license')}
</DiscreetLink>
</LinkStandalone>
</li>

<li>
<DiscreetLink to="https://community.sonarsource.com/c/help/sq">
<LinkStandalone
highlight={LinkHighlight.CurrentColor}
to="https://community.sonarsource.com/c/help/sq"
>
{translate('footer.community')}
</DiscreetLink>
</LinkStandalone>
</li>

<li>
<DiscreetLink to={docUrl('/')}>{translate('footer.documentation')}</DiscreetLink>
<LinkStandalone highlight={LinkHighlight.CurrentColor} to={docUrl('/')}>
{translate('footer.documentation')}
</LinkStandalone>
</li>

<li>
<DiscreetLink to={docUrl('/instance-administration/plugin-version-matrix/')}>
<LinkStandalone
highlight={LinkHighlight.CurrentColor}
to={docUrl('/instance-administration/plugin-version-matrix/')}
>
{translate('footer.plugins')}
</DiscreetLink>
</LinkStandalone>
</li>

{!hideLoggedInInfo && (
<li>
<DiscreetLink to="/web_api">{translate('footer.web_api')}</DiscreetLink>
<LinkStandalone highlight={LinkHighlight.CurrentColor} to="/web_api">
{translate('footer.web_api')}
</LinkStandalone>
</li>
)}
</ul>
@@ -100,8 +122,8 @@ export default function GlobalFooter({ hideLoggedInInfo }: GlobalFooterProps) {
}

const StyledFooter = styled.div`
background-color: ${themeColor('backgroundSecondary')};
border-top: ${themeBorder('default')};
box-sizing: border-box;
min-width: ${LAYOUT_VIEWPORT_MIN_WIDTH}px;
border-top: ${themeBorder('default')};
background-color: ${themeColor('backgroundSecondary')};
`;

+ 14
- 6
server/sonar-web/src/main/js/app/components/GlobalFooterBranding.tsx View File

@@ -17,7 +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 { DiscreetLink } from 'design-system';

import { Link, LinkHighlight } from '@sonarsource/echoes-react';
import * as React from 'react';
import { isOfficial } from '../../helpers/system';

@@ -27,21 +28,28 @@ export default function GlobalFooterBranding() {
return official ? (
<div>
SonarQube&trade; technology is powered by{' '}
<DiscreetLink to="https://www.sonarsource.com">SonarSource SA</DiscreetLink>
<Link highlight={LinkHighlight.CurrentColor} to="https://www.sonarsource.com">
SonarSource SA
</Link>
</div>
) : (
<div>
This application is based on{' '}
<DiscreetLink
<Link
highlight={LinkHighlight.CurrentColor}
to="https://www.sonarsource.com/products/sonarqube/?referrer=sonarqube"
title="SonarQube™"
>
SonarQube™
</DiscreetLink>{' '}
</Link>{' '}
but is <strong>not</strong> an official version provided by{' '}
<DiscreetLink to="https://www.sonarsource.com" title="SonarSource SA">
<Link
highlight={LinkHighlight.CurrentColor}
to="https://www.sonarsource.com"
title="SonarSource SA"
>
SonarSource SA
</DiscreetLink>
</Link>
.
</div>
);

+ 9
- 14
server/sonar-web/src/main/js/app/components/KeyboardShortcutsModal.tsx View File

@@ -18,16 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import {
ContentCell,
Key,
KeyboardHint,
Link,
Modal,
SubTitle,
Table,
TableRow,
} from 'design-system';
import { LinkStandalone } from '@sonarsource/echoes-react';
import { ContentCell, Key, KeyboardHint, Modal, SubTitle, Table, TableRow } from 'design-system';
import * as React from 'react';
import { isInput } from '../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../helpers/keycodes';
@@ -148,12 +140,14 @@ function renderSection() {
return SECTIONS.map((section) => (
<div key={section.subTitle} className="sw-mb-4">
<SubTitle>{translate(section.subTitle)}</SubTitle>

<Table columnCount={2} columnWidths={['30%', '70%']}>
{section.rows.map((row) => (
<TableRow key={row.command}>
<ContentCell className="sw-justify-center">
<KeyboardHint command={row.command} title="" />
</ContentCell>

<ContentCell>{translate(row.description)}</ContentCell>
</TableRow>
))}
@@ -195,24 +189,25 @@ export default function KeyboardShortcutsModal() {

const body = (
<>
<Link
to="/account"
<LinkStandalone
onClick={() => {
setDisplay(false);
return true;
}}
to="/account"
>
{translate('keyboard_shortcuts_modal.disable_link')}
</Link>
</LinkStandalone>

<div className="sw-mt-4">{renderSection()}</div>
</>
);

return (
<Modal
body={body}
headerTitle={title}
onClose={() => setDisplay(false)}
body={body}
secondaryButtonLabel={translate('close')}
/>
);

+ 11
- 7
server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx View File

@@ -17,7 +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 { HoverLink, TextMuted } from 'design-system';

import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
import * as React from 'react';
import Favorite from '../../../../components/controls/Favorite';
import { getComponentOverviewUrl } from '../../../../helpers/urls';
@@ -29,7 +30,7 @@ export interface BreadcrumbProps {
currentUser: CurrentUser;
}

export function Breadcrumb(props: BreadcrumbProps) {
export function Breadcrumb(props: Readonly<BreadcrumbProps>) {
const { component, currentUser } = props;

return (
@@ -48,15 +49,18 @@ export function Breadcrumb(props: BreadcrumbProps) {
qualifier={component.qualifier}
/>
)}
<HoverLink
blurAfterClick
className="js-project-link sw-flex"

<LinkStandalone
highlight={LinkHighlight.Subdued}
className="js-project-link"
key={breadcrumbElement.name}
shouldBlurAfterClick
title={breadcrumbElement.name}
to={getComponentOverviewUrl(breadcrumbElement.key, breadcrumbElement.qualifier)}
>
<TextMuted text={breadcrumbElement.name} />
</HoverLink>
{breadcrumbElement.name}
</LinkStandalone>

{isNotLast && <span className="slash-separator sw-mx-2" />}
</div>
);

+ 16
- 10
server/sonar-web/src/main/js/app/components/nav/component/branch-like/PRLink.tsx View File

@@ -17,17 +17,20 @@
* 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 { LinkStandalone } from '@sonarsource/echoes-react';
import React from 'react';
import { isPullRequest } from '../../../../../helpers/branch-like';
import { translate, translateWithParameters } from '../../../../../helpers/l10n';
import { getBaseUrl } from '../../../../../helpers/system';
import { isDefined } from '../../../../../helpers/types';
import { AlmKeys } from '../../../../../types/alm-settings';
import { BranchLike } from '../../../../../types/branch-like';
import { Component } from '../../../../../types/types';

function getPRUrlAlmKey(url = '') {
const lowerCaseUrl = url.toLowerCase();

if (lowerCaseUrl.includes(AlmKeys.GitHub)) {
return AlmKeys.GitHub;
} else if (lowerCaseUrl.includes(AlmKeys.GitLab)) {
@@ -41,29 +44,32 @@ function getPRUrlAlmKey(url = '') {
) {
return AlmKeys.Azure;
}

return undefined;
}

export default function PRLink({
currentBranchLike,
component,
}: {
}: Readonly<{
currentBranchLike: BranchLike;
component: Component;
}) {
}>) {
if (!isPullRequest(currentBranchLike)) {
return null;
}

const almKey =
component.alm?.key ||
(isPullRequest(currentBranchLike) && getPRUrlAlmKey(currentBranchLike.url));
(isPullRequest(currentBranchLike) && getPRUrlAlmKey(currentBranchLike.url)) ||
'';

return (
<>
{currentBranchLike.url !== undefined && (
<Link
icon={
almKey && (
{isDefined(currentBranchLike.url) && (
<LinkStandalone
iconLeft={
almKey !== '' && (
<img
alt={almKey}
height={16}
@@ -75,8 +81,8 @@ export default function PRLink({
key={currentBranchLike.key}
to={currentBranchLike.url}
>
{!almKey && translate('branches.see_the_pr')}
</Link>
{almKey === '' && translate('branches.see_the_pr')}
</LinkStandalone>
)}
</>
);

+ 21
- 13
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx View File

@@ -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 { LinkStandalone } from '@sonarsource/echoes-react';
import {
ButtonPrimary,
Card,
@@ -25,7 +27,6 @@ import {
FlagMessage,
FormField,
InputField,
Link,
PageContentFontWrapper,
Spinner,
SubTitle,
@@ -40,28 +41,30 @@ import Unauthorized from '../sessions/components/Unauthorized';
import { DEFAULT_ADMIN_PASSWORD } from './constants';

export interface ChangeAdminPasswordAppRendererProps {
passwordValue: string;
canAdmin?: boolean;
canSubmit?: boolean;
confirmPasswordValue: string;
location: Location;
onConfirmPasswordChange: (password: string) => void;
onPasswordChange: (password: string) => void;
onSubmit: () => void;
canAdmin?: boolean;
canSubmit?: boolean;
passwordValue: string;
submitting: boolean;
success: boolean;
location: Location;
}

const PASSWORD_FIELD_ID = 'user-password';
const CONFIRM_PASSWORD_FIELD_ID = 'confirm-user-password';

export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswordAppRendererProps) {
export default function ChangeAdminPasswordAppRenderer(
props: Readonly<ChangeAdminPasswordAppRendererProps>,
) {
const {
canAdmin,
canSubmit,
confirmPasswordValue,
passwordValue,
location,
passwordValue,
submitting,
success,
} = props;
@@ -73,24 +76,28 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
return (
<CenteredLayout>
<Helmet defer={false} title={translate('users.change_admin_password.page')} />

<PageContentFontWrapper className="sw-body-sm sw-flex sw-flex-col sw-items-center sw-justify-center">
<Card className="sw-mx-auto sw-mt-24 sw-w-abs-600 sw-flex sw-items-stretch sw-flex-col">
{success ? (
<FlagMessage className="sw-my-8" variant="success">
<div>
<p className="sw-mb-2">{translate('users.change_admin_password.form.success')}</p>

{/* We must reload because we need a refresh of the /api/navigation/global call. */}
<Link to={getReturnUrl(location)} reloadDocument>
<LinkStandalone to={getReturnUrl(location)} reloadDocument>
{translate('users.change_admin_password.form.continue_to_app')}
</Link>
</LinkStandalone>
</div>
</FlagMessage>
) : (
<>
<Title>{translate('users.change_admin_password.instance_is_at_risk')}</Title>

<DarkLabel className="sw-mb-2">
{translate('users.change_admin_password.header')}
</DarkLabel>

<p>{translate('users.change_admin_password.description')}</p>

<form
@@ -105,8 +112,8 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
</SubTitle>

<FormField
label={translate('users.change_admin_password.form.password')}
htmlFor={PASSWORD_FIELD_ID}
label={translate('users.change_admin_password.form.password')}
required
>
<InputField
@@ -122,9 +129,6 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
</FormField>

<FormField
label={translate('users.change_admin_password.form.confirm')}
htmlFor={CONFIRM_PASSWORD_FIELD_ID}
required
description={
confirmPasswordValue === passwordValue &&
passwordValue === DEFAULT_ADMIN_PASSWORD && (
@@ -133,6 +137,9 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
</FlagMessage>
)
}
htmlFor={CONFIRM_PASSWORD_FIELD_ID}
label={translate('users.change_admin_password.form.confirm')}
required
>
<InputField
id={CONFIRM_PASSWORD_FIELD_ID}
@@ -152,6 +159,7 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
type="submit"
>
<Spinner className="sw-mr-2" loading={submitting} />

{translate('update_verb')}
</ButtonPrimary>
</form>

+ 35
- 18
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx View File

@@ -17,10 +17,13 @@
* 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, BranchIcon, HoverLink, LightLabel, Note, QualifierIcon } from 'design-system';

import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
import { Badge, BranchIcon, LightLabel, Note, QualifierIcon } from 'design-system';
import * as React from 'react';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { isDefined } from '../../../helpers/types';
import { CodeScope, getComponentOverviewUrl, queryToSearch } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import {
@@ -37,8 +40,8 @@ export function getTooltip(component: ComponentMeasure) {
component.qualifier === ComponentQualifier.File ||
component.qualifier === ComponentQualifier.TestFile;

if (isFile && component.path) {
return component.path + '\n\n' + component.key;
if (isFile && isDefined(component.path)) {
return `${component.path}\n\n${component.key}`;
}

return [component.name, component.key, component.branch].filter((s) => !!s).join('\n\n');
@@ -48,23 +51,23 @@ export interface Props {
branchLike?: BranchLike;
canBrowse?: boolean;
component: ComponentMeasure;
newCodeSelected?: boolean;
previous?: ComponentMeasure;
rootComponent: ComponentMeasure;
unclickable?: boolean;
newCodeSelected?: boolean;
showIcon?: boolean;
unclickable?: boolean;
}

export default function ComponentName({
branchLike,
component,
unclickable = false,
rootComponent,
previous,
canBrowse = false,
component,
newCodeSelected,
previous,
rootComponent,
showIcon = true,
}: Props) {
unclickable = false,
}: Readonly<Props>) {
const ariaLabel = unclickable ? translate('code.parent_folder') : undefined;

if (
@@ -89,9 +92,11 @@ export default function ComponentName({
newCodeSelected,
)}
</div>

{component.branch ? (
<div className="sw-truncate sw-ml-2">
<BranchIcon className="sw-mr-1" />

<Note>{component.branch}</Note>
</div>
) : (
@@ -102,6 +107,7 @@ export default function ComponentName({
</span>
);
}

return (
<span title={getTooltip(component)} aria-label={ariaLabel}>
{renderNameWithIcon(
@@ -129,6 +135,7 @@ function renderNameWithIcon(
) {
const name = renderName(component, previous);
const codeType = newCodeSelected ? CodeScope.New : CodeScope.Overall;

if (
!unclickable &&
(isPortfolioLike(component.qualifier) ||
@@ -140,9 +147,11 @@ function renderNameWithIcon(
)
? component.branch
: undefined;

return (
<HoverLink
icon={showIcon && <QualifierIcon className="sw-mr-1" qualifier={component.qualifier} />}
<LinkStandalone
highlight={LinkHighlight.CurrentColor}
iconLeft={showIcon && <QualifierIcon className="sw-mr-2" qualifier={component.qualifier} />}
to={getComponentOverviewUrl(
component.refKey ?? component.key,
component.qualifier,
@@ -151,27 +160,32 @@ function renderNameWithIcon(
)}
>
{name}
</HoverLink>
</LinkStandalone>
);
} else if (canBrowse) {
const query = { id: rootComponent.key, ...getBranchLikeQuery(branchLike) };

if (component.key !== rootComponent.key) {
Object.assign(query, { selected: component.key });
}

return (
<HoverLink
icon={showIcon && <QualifierIcon qualifier={component.qualifier} />}
<LinkStandalone
highlight={LinkHighlight.CurrentColor}
iconLeft={showIcon && <QualifierIcon className="sw-mr-2" qualifier={component.qualifier} />}
to={{ pathname: '/code', search: queryToSearch(query) }}
>
{name}
</HoverLink>
</LinkStandalone>
);
}

return (
<span>
{showIcon && (
<QualifierIcon className="sw-mr-1 sw-align-text-bottom" qualifier={component.qualifier} />
<QualifierIcon className="sw-mr-2 sw-align-text-bottom" qualifier={component.qualifier} />
)}

{name}
</span>
);
@@ -182,13 +196,16 @@ function renderName(component: ComponentMeasure, previous: ComponentMeasure | un
component.qualifier === ComponentQualifier.Directory &&
previous &&
previous.qualifier === ComponentQualifier.Directory;

const prefix =
areBothDirs && previous !== undefined
areBothDirs && isDefined(previous)
? mostCommonPrefix([component.name + '/', previous.name + '/'])
: '';

return prefix ? (
<span>
<LightLabel>{prefix}</LightLabel>

<span>{component.name.slice(prefix.length)}</span>
</span>
) : (

+ 39
- 16
server/sonar-web/src/main/js/apps/maintenance/components/App.tsx View File

@@ -17,8 +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 styled from '@emotion/styled';
import { ButtonPrimary, Card, CenteredLayout, Link, Note, Spinner, Title } from 'design-system';
import { Link, LinkStandalone, Spinner } from '@sonarsource/echoes-react';
import { ButtonPrimary, Card, CenteredLayout, Note, Title } from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { FormattedMessage } from 'react-intl';
@@ -29,6 +31,7 @@ import DateFromNow from '../../../components/intl/DateFromNow';
import TimeFormatter from '../../../components/intl/TimeFormatter';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import { isDefined } from '../../../helpers/types';
import { getReturnUrl } from '../../../helpers/urls';

interface Props {
@@ -59,13 +62,15 @@ export default class App extends React.PureComponent<Props, State> {

componentWillUnmount() {
this.mounted = false;
if (this.interval !== undefined) {

if (isDefined(this.interval)) {
window.clearInterval(this.interval);
}
}

fetchStatus = () => {
const request = this.props.setup ? this.fetchMigrationState() : this.fetchSystemStatus();

request.catch(() => {
if (this.mounted) {
this.setState({
@@ -137,6 +142,7 @@ export default class App extends React.PureComponent<Props, State> {
return (
<>
<Helmet defaultTitle={translate('maintenance.page')} defer={false} />

<CenteredLayout className="sw-flex sw-justify-around sw-mt-32" id="bd">
<Card className="sw-body-sm sw-p-10 sw-w-abs-400" id="nonav">
{status === 'OFFLINE' && (
@@ -144,13 +150,15 @@ export default class App extends React.PureComponent<Props, State> {
<MaintenanceTitle className="text-danger">
<InstanceMessage message={translate('maintenance.is_offline')} />
</MaintenanceTitle>

<MaintenanceText>
{translate('maintenance.sonarqube_is_offline.text')}
</MaintenanceText>

<div className="sw-text-center">
<Link reloadDocument to={`${getBaseUrl()}/`}>
<LinkStandalone reloadDocument to={`${getBaseUrl()}/`}>
{translate('maintenance.try_again')}
</Link>
</LinkStandalone>
</div>
</>
)}
@@ -160,11 +168,13 @@ export default class App extends React.PureComponent<Props, State> {
<MaintenanceTitle>
<InstanceMessage message={translate('maintenance.is_up')} />
</MaintenanceTitle>

<MaintenanceText className="sw-text-center">
{translate('maintenance.all_systems_opetational')}
</MaintenanceText>

<div className="sw-text-center">
<Link to="/">{translate('layout.home')}</Link>
<LinkStandalone to="/">{translate('layout.home')}</LinkStandalone>
</div>
</>
)}
@@ -174,6 +184,7 @@ export default class App extends React.PureComponent<Props, State> {
<MaintenanceTitle>
<InstanceMessage message={translate('maintenance.is_starting')} />
</MaintenanceTitle>

<MaintenanceSpinner>
<Spinner />
</MaintenanceSpinner>
@@ -185,11 +196,13 @@ export default class App extends React.PureComponent<Props, State> {
<MaintenanceTitle className="text-danger">
<InstanceMessage message={translate('maintenance.is_down')} />
</MaintenanceTitle>

<MaintenanceText>{translate('maintenance.sonarqube_is_down.text')}</MaintenanceText>

<MaintenanceText className="sw-text-center">
<Link reloadDocument to={`${getBaseUrl()}/`}>
<LinkStandalone reloadDocument to={`${getBaseUrl()}/`}>
{translate('maintenance.try_again')}
</Link>
</LinkStandalone>
</MaintenanceText>
</>
)}
@@ -199,22 +212,21 @@ export default class App extends React.PureComponent<Props, State> {
<MaintenanceTitle>
<InstanceMessage message={translate('maintenance.is_under_maintenance')} />
</MaintenanceTitle>

<MaintenanceText>
<FormattedMessage
defaultMessage={translate('maintenance.sonarqube_is_under_maintenance.1')}
id="maintenance.sonarqube_is_under_maintenance.1"
values={{
link: (
<Link
to="https://www.sonarlint.org/?referrer=sonarqube-maintenance"
target="_blank"
>
<Link to="https://www.sonarlint.org/?referrer=sonarqube-maintenance">
{translate('maintenance.sonarqube_is_under_maintenance_link.1')}
</Link>
),
}}
/>
</MaintenanceText>

<MaintenanceText>
<FormattedMessage
defaultMessage={translate('maintenance.sonarqube_is_under_maintenance.2')}
@@ -236,8 +248,9 @@ export default class App extends React.PureComponent<Props, State> {
<MaintenanceTitle>
{translate('maintenance.database_is_up_to_date')}
</MaintenanceTitle>

<div className="sw-text-center">
<Link to="/">{translate('layout.home')}</Link>
<LinkStandalone to="/">{translate('layout.home')}</LinkStandalone>
</div>
</>
)}
@@ -245,9 +258,13 @@ export default class App extends React.PureComponent<Props, State> {
{state === 'MIGRATION_REQUIRED' && (
<>
<MaintenanceTitle>{translate('maintenance.upgrade_database')}</MaintenanceTitle>

<MaintenanceText>{translate('maintenance.upgrade_database.1')}</MaintenanceText>

<MaintenanceText>{translate('maintenance.upgrade_database.2')}</MaintenanceText>

<MaintenanceText>{translate('maintenance.upgrade_database.3')}</MaintenanceText>

<MaintenanceSpinner>
<ButtonPrimary id="start-migration" onClick={this.handleMigrateClick}>
{translate('maintenance.upgrade')}
@@ -261,6 +278,7 @@ export default class App extends React.PureComponent<Props, State> {
<MaintenanceTitle className="text-danger">
{translate('maintenance.migration_not_supported')}
</MaintenanceTitle>

<p>{translate('maintenance.migration_not_supported.text')}</p>
</>
)}
@@ -268,10 +286,12 @@ export default class App extends React.PureComponent<Props, State> {
{state === 'MIGRATION_RUNNING' && (
<>
<MaintenanceTitle>{translate('maintenance.database_migration')}</MaintenanceTitle>
{this.state.message !== undefined && (

{isDefined(this.state.message) && (
<MaintenanceText className="sw-text-center">{this.state.message}</MaintenanceText>
)}
{this.state.startedAt !== undefined && (

{isDefined(this.state.startedAt) && (
<MaintenanceText className="sw-text-center">
{translate('background_tasks.table.started')}{' '}
<DateFromNow date={this.state.startedAt} />
@@ -281,6 +301,7 @@ export default class App extends React.PureComponent<Props, State> {
</Note>
</MaintenanceText>
)}

<MaintenanceSpinner>
<Spinner />
</MaintenanceSpinner>
@@ -292,8 +313,9 @@ export default class App extends React.PureComponent<Props, State> {
<MaintenanceTitle className="text-success">
{translate('maintenance.database_is_up_to_date')}
</MaintenanceTitle>

<div className="sw-text-center">
<Link to="/">{translate('layout.home')}</Link>
<LinkStandalone to="/">{translate('layout.home')}</LinkStandalone>
</div>
</>
)}
@@ -303,6 +325,7 @@ export default class App extends React.PureComponent<Props, State> {
<MaintenanceTitle className="text-danger">
{translate('maintenance.upgrade_failed')}
</MaintenanceTitle>

<MaintenanceText>{translate('maintenance.upgrade_failed.text')}</MaintenanceText>
</>
)}
@@ -314,8 +337,8 @@ export default class App extends React.PureComponent<Props, State> {
}

const MaintenanceTitle = styled(Title)`
text-align: center;
margin-bottom: 2.5rem;
text-align: center;
`;

const MaintenanceText = styled.p`

+ 33
- 8
server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx View File

@@ -17,8 +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, Spinner } from '@sonarsource/echoes-react';
import classNames from 'classnames';
import { FlagMessage, Link, Spinner } from 'design-system';
import { FlagMessage } from 'design-system';
import * as React from 'react';
import { useComponent } from '../../../app/components/componentContext/withComponentContext';
import { translate } from '../../../helpers/l10n';
@@ -29,27 +31,30 @@ import { AnalysisErrorModal } from './AnalysisErrorModal';
import AnalysisWarningsModal from './AnalysisWarningsModal';

export interface HeaderMetaProps {
component: Component;
className?: string;
component: Component;
}

export function AnalysisStatus(props: HeaderMetaProps) {
export function AnalysisStatus(props: Readonly<HeaderMetaProps>) {
const { className, component } = props;
const { currentTask, isPending, isInProgress } = useComponent();
const { data: warnings, isLoading } = useBranchWarningQuery(component);

const [modalIsVisible, setDisplayModal] = React.useState(false);

const openModal = React.useCallback(() => {
setDisplayModal(true);
}, [setDisplayModal]);

const closeModal = React.useCallback(() => {
setDisplayModal(false);
}, [setDisplayModal]);

if (isInProgress || isPending) {
return (
<div data-test="analysis-status" className={classNames('sw-flex sw-items-center', className)}>
<div className={classNames('sw-flex sw-items-center', className)} data-test="analysis-status">
<Spinner />

<span className="sw-ml-1">
{isInProgress
? translate('project_navigation.analysis_status.in_progress')
@@ -62,12 +67,22 @@ export function AnalysisStatus(props: HeaderMetaProps) {
if (currentTask?.status === TaskStatuses.Failed) {
return (
<>
<FlagMessage data-test="analysis-status" variant="error" className={className}>
<FlagMessage className={className} data-test="analysis-status" variant="error">
<span>{translate('project_navigation.analysis_status.failed')}</span>
<Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>

{/* TODO: replace the Link below with a lighweight/discreet button component */}
{/* when it is available in Echoes */}
<Link
className="sw-ml-1"
onClick={openModal}
shouldBlurAfterClick
shouldPreventDefault
to={{}}
>
{translate('project_navigation.analysis_status.details_link')}
</Link>
</FlagMessage>

{modalIsVisible && (
<AnalysisErrorModal
component={component}
@@ -82,12 +97,22 @@ export function AnalysisStatus(props: HeaderMetaProps) {
if (!isLoading && warnings && warnings.length > 0) {
return (
<>
<FlagMessage data-test="analysis-status" variant="warning" className={className}>
<FlagMessage className={className} data-test="analysis-status" variant="warning">
<span>{translate('project_navigation.analysis_status.warnings')}</span>
<Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>

{/* TODO: replace the Link below with a lighweight/discreet button component */}
{/* when it is available in Echoes */}
<Link
className="sw-ml-1"
onClick={openModal}
shouldBlurAfterClick
shouldPreventDefault
to={{}}
>
{translate('project_navigation.analysis_status.details_link')}
</Link>
</FlagMessage>

{modalIsVisible && (
<AnalysisWarningsModal component={component} onClose={closeModal} warnings={warnings} />
)}

+ 31
- 11
server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx View File

@@ -17,7 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import styled from '@emotion/styled';
import { Link, LinkStandalone } from '@sonarsource/echoes-react';
import classNames from 'classnames';
import {
Badge,
@@ -27,7 +29,6 @@ import {
Note,
QualityGateIndicator,
SeparatorCircleIcon,
StandoutLink,
SubnavigationFlowSeparator,
Tags,
themeBorder,
@@ -42,6 +43,7 @@ import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
import Measure from '../../../../components/measure/Measure';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { formatMeasure } from '../../../../helpers/measures';
import { isDefined } from '../../../../helpers/types';
import { getProjectUrl } from '../../../../helpers/urls';
import { ComponentQualifier } from '../../../../types/component';
import { MetricKey, MetricType } from '../../../../types/metrics';
@@ -70,7 +72,7 @@ function renderFirstLine(
<>
<div className="sw-flex sw-justify-between sw-items-center ">
<div className="sw-flex sw-items-center ">
{isFavorite !== undefined && (
{isDefined(isFavorite) && (
<Favorite
className="sw-mr-2"
component={key}
@@ -82,7 +84,7 @@ function renderFirstLine(
)}

<span className="it__project-card-name" title={name}>
<StandoutLink to={getProjectUrl(key)}>{name}</StandoutLink>
<LinkStandalone to={getProjectUrl(key)}>{name}</LinkStandalone>
</span>

{qualifier === ComponentQualifier.Application && (
@@ -90,7 +92,7 @@ function renderFirstLine(
overlay={
<span>
{translate('qualifier.APP')}
{measures.projects && (
{measures.projects !== '' && (
<span>
{' ‒ '}
{translateWithParameters('x_projects_', measures.projects)}
@@ -111,7 +113,8 @@ function renderFirstLine(
</span>
</Tooltip>
</div>
{analysisDate && (

{isDefined(analysisDate) && analysisDate !== '' && (
<Tooltip overlay={qualityGateLabel}>
<span className="sw-flex sw-items-center">
<QualityGateIndicator
@@ -123,8 +126,9 @@ function renderFirstLine(
</Tooltip>
)}
</div>

<LightLabel as="div" className="sw-flex sw-items-center sw-mt-3">
{analysisDate && (
{isDefined(analysisDate) && analysisDate !== '' && (
<DateTimeFormatter date={analysisDate}>
{(formattedAnalysisDate) => (
<span className="sw-body-sm-highlight" title={formattedAnalysisDate}>
@@ -139,10 +143,12 @@ function renderFirstLine(
)}
</DateTimeFormatter>
)}

{isNewCode
? measures[MetricKey.new_lines] != null && (
<>
<SeparatorCircleIcon className="sw-mx-1" />

<div>
<span className="sw-body-sm-highlight sw-mr-1" data-key={MetricKey.new_lines}>
<Measure
@@ -151,6 +157,7 @@ function renderFirstLine(
value={measures.new_lines}
/>
</span>

<span className="sw-body-sm">{translate('metric.new_lines.name')}</span>
</div>
</>
@@ -158,6 +165,7 @@ function renderFirstLine(
: measures[MetricKey.ncloc] != null && (
<>
<SeparatorCircleIcon className="sw-mx-1" />

<div>
<span className="sw-body-sm-highlight sw-mr-1" data-key={MetricKey.ncloc}>
<Measure
@@ -166,17 +174,22 @@ function renderFirstLine(
value={measures.ncloc}
/>
</span>

<span className="sw-body-sm">{translate('metric.ncloc.name')}</span>
</div>

<SeparatorCircleIcon className="sw-mx-1" />

<span className="sw-body-sm" data-key={MetricKey.ncloc_language_distribution}>
<ProjectCardLanguages distribution={measures.ncloc_language_distribution} />
</span>
</>
)}

{tags.length > 0 && (
<>
<SeparatorCircleIcon className="sw-mx-1" />

<Tags
className="sw-body-sm"
emptyText={translate('issue.no_tag')}
@@ -199,7 +212,11 @@ function renderSecondLine(
) {
const { analysisDate, key, leakPeriodDate, measures, qualifier, isScannable } = project;

if (analysisDate && (!isNewCode || leakPeriodDate)) {
if (
isDefined(analysisDate) &&
analysisDate !== '' &&
(!isNewCode || (isDefined(leakPeriodDate) && leakPeriodDate !== ''))
) {
return (
<ProjectCardMeasures
measures={measures}
@@ -216,19 +233,20 @@ function renderSecondLine(
? translate('projects.no_new_code_period', qualifier)
: translate('projects.not_analyzed', qualifier)}
</Note>

{qualifier !== ComponentQualifier.Application &&
!analysisDate &&
(analysisDate === undefined || analysisDate === '') &&
isLoggedIn(currentUser) &&
isScannable && (
<StandoutLink className="sw-ml-2 sw-body-sm-highlight" to={getProjectUrl(key)}>
<Link className="sw-ml-2 sw-body-sm-highlight" to={getProjectUrl(key)}>
{translate('projects.configure_analysis')}
</StandoutLink>
</Link>
)}
</div>
);
}

export default function ProjectCard(props: Props) {
export default function ProjectCard(props: Readonly<Props>) {
const { currentUser, type, project } = props;
const isNewCode = type === 'leak';

@@ -240,7 +258,9 @@ export default function ProjectCard(props: Props) {
data-key={project.key}
>
{renderFirstLine(project, props.handleFavorite, isNewCode)}

<SubnavigationFlowSeparator className="sw-my-3" />

{renderSecondLine(currentUser, project, isNewCode)}
</ProjectCardWrapper>
);

+ 12
- 4
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx View File

@@ -17,7 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { FlagMessage, Link, SubTitle } from 'design-system';

import { LinkStandalone } from '@sonarsource/echoes-react';
import { FlagMessage, SubTitle } from 'design-system';
import * as React from 'react';
import { getQualityProfileExporterUrl } from '../../../api/quality-profiles';
import { translate } from '../../../helpers/l10n';
@@ -28,7 +30,7 @@ interface Props {
profile: Profile;
}

export default function ProfileExporters({ exporters, profile }: Props) {
export default function ProfileExporters({ exporters, profile }: Readonly<Props>) {
const exportersForLanguage = exporters.filter((e) => e.languages.includes(profile.language));

if (exportersForLanguage.length === 0) {
@@ -40,15 +42,21 @@ export default function ProfileExporters({ exporters, profile }: Props) {
<div>
<SubTitle>{translate('quality_profiles.exporters')}</SubTitle>
</div>

<FlagMessage className="sw-mb-4" variant="warning">
{translate('quality_profiles.exporters.deprecated')}
</FlagMessage>

<ul className="sw-flex sw-flex-col sw-gap-2">
{exportersForLanguage.map((exporter) => (
<li data-key={exporter.key} key={exporter.key}>
<Link isExternal showExternalIcon to={getQualityProfileExporterUrl(exporter, profile)}>
<LinkStandalone
hasExternalIcon
isExternal
to={getQualityProfileExporterUrl(exporter, profile)}
>
{exporter.name}
</Link>
</LinkStandalone>
</li>
))}
</ul>

+ 0
- 2
server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx View File

@@ -233,8 +233,6 @@ it('should show About page', async () => {
await user.click(ui.apiScopePet.get());
await user.click(ui.apiSidebarItem.getAt(0));
expect(screen.queryByText('about')).not.toBeInTheDocument();
await user.click(ui.title.get());
expect(await screen.findByText('about')).toBeInTheDocument();
});

function renderWebApiApp() {

+ 18
- 13
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx View File

@@ -17,13 +17,13 @@
* 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,
BasicSeparator,
Checkbox,
HelperHintIcon,
InputSearch,
Link,
SubnavigationAccordion,
SubnavigationItem,
SubnavigationSubheading,
@@ -41,13 +41,14 @@ import ApiFilterContext from './ApiFilterContext';
import RestMethodPill from './RestMethodPill';

interface Api {
name: string;
method: string;
info: OpenAPIV3.OperationObject<InternalExtension>;
method: string;
name: string;
}

interface Props {
docInfo: OpenAPIV3.InfoObject;
apisList: Api[];
docInfo: OpenAPIV3.InfoObject;
}

const METHOD_ORDER: Dict<number> = {
@@ -82,6 +83,7 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
.filter((api) => showInternal || !api.info['x-internal'])
.reduce<Record<string, Api[]>>((acc, api) => {
const subgroup = api.name.split('/')[1];

return {
...acc,
[subgroup]: [...(acc[subgroup] ?? []), api],
@@ -92,16 +94,12 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {

return (
<>
<h1 className="sw-mb-2">
<Link to="." className="sw-text-[unset] sw-border-none">
{docInfo.title}
</Link>
</h1>
<h1 className="sw-mb-2">{docInfo.title}</h1>

<InputSearch
className="sw-w-full"
placeholder={translate('api_documentation.v2.search')}
onChange={setSearch}
placeholder={translate('api_documentation.v2.search')}
value={search}
/>

@@ -109,6 +107,7 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
<Checkbox checked={showInternal} onCheck={() => setShowInternal((prev) => !prev)}>
<span className="sw-ml-2">{translate('api_documentation.show_internal_v2')}</span>
</Checkbox>

<HelpTooltip
className="sw-ml-2"
overlay={translate('api_documentation.internal_tooltip_v2')}
@@ -119,27 +118,31 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {

{Object.entries(groupedList).map(([group, apis]) => (
<SubnavigationAccordion
className="sw-mt-2"
header={group}
id={`web-api-${group}`}
initExpanded={apis.some(
({ name, method }) => name === activeApi[0] && method === activeApi[1],
)}
className="sw-mt-2"
header={group}
key={group}
id={`web-api-${group}`}
>
{sortBy(apis, (a) => [a.name, METHOD_ORDER[a.method]]).map(
({ method, name, info }, index, sorted) => {
const resourceName = getResourceFromName(name);

const previousResourceName =
index > 0 ? getResourceFromName(sorted[index - 1].name) : undefined;

const isNewResource = resourceName !== previousResourceName;

return (
<Fragment key={getApiEndpointKey(name, method)}>
{index > 0 && isNewResource && <BasicSeparator />}

{(index === 0 || isNewResource) && (
<SubnavigationSubheading>{resourceName}</SubnavigationSubheading>
)}

<SubnavigationItem
active={name === activeApi[0] && method === activeApi[1]}
onClick={handleApiClick}
@@ -148,6 +151,7 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
<div className="sw-flex sw-gap-2 sw-w-full sw-justify-between">
<div className="sw-flex sw-gap-2">
<RestMethodPill method={method} />

<div>{info.summary ?? name}</div>
</div>

@@ -158,6 +162,7 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
{translate('internal')}
</Badge>
)}

{info.deprecated && (
<Badge variant="deleted" className="sw-self-center">
{translate('deprecated')}

+ 15
- 12
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx View File

@@ -17,7 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import styled from '@emotion/styled';
import { LinkStandalone } from '@sonarsource/echoes-react';
import {
ClipboardIconButton,
DrilldownLink,
@@ -25,7 +27,6 @@ import {
InteractiveIcon,
ItemButton,
ItemLink,
Link,
MenuIcon,
Note,
PopupPlacement,
@@ -44,20 +45,19 @@ import { formatMeasure } from '../../helpers/measures';
import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
import { omitNil } from '../../helpers/request';
import { getBaseUrl } from '../../helpers/system';
import { isDefined } from '../../helpers/types';
import {
getBranchLikeUrl,
getCodeUrl,
getComponentIssuesUrl,
getComponentSecurityHotspotsUrl,
} from '../../helpers/urls';
import { DEFAULT_ISSUES_QUERY } from '../shared/utils';

import type { BranchLike } from '../../types/branch-like';
import { ComponentQualifier } from '../../types/component';
import { IssueType } from '../../types/issues';
import { MetricKey, MetricType } from '../../types/metrics';

import type { BranchLike } from '../../types/branch-like';
import type { Measure, SourceViewerFile } from '../../types/types';
import { DEFAULT_ISSUES_QUERY } from '../shared/utils';
import type { WorkspaceContextShape } from '../workspace/context';

interface Props {
@@ -142,19 +142,22 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {
>
<div className="sw-flex sw-flex-1 sw-flex-col sw-gap-1 sw-mr-5 sw-my-1">
<div className="sw-flex sw-gap-1 sw-items-center">
<Link icon={<ProjectIcon />} to={getBranchLikeUrl(project, this.props.branchLike)}>
<LinkStandalone
iconLeft={<ProjectIcon className="sw-mr-2" />}
to={getBranchLikeUrl(project, this.props.branchLike)}
>
{projectName}
</Link>
</LinkStandalone>
</div>

<div className="sw-flex sw-gap-1 sw-items-center">
<div className="sw-flex sw-gap-2 sw-items-center">
<QualifierIcon qualifier={q} />

{collapsedDirFromPath(path)}

{fileFromPath(path)}

<span className="sw-ml-1">
<span>
<ClipboardIconButton
aria-label={translate('component_viewer.copy_path_to_clipboard')}
copyValue={path}
@@ -165,7 +168,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {

{showMeasures && (
<div className="sw-flex sw-gap-6 sw-items-center">
{measures[unitTestsOrLines] && (
{isDefined(measures[unitTestsOrLines]) && (
<div className="sw-flex sw-flex-col sw-gap-1">
<Note className="it__source-viewer-header-measure-label sw-body-lg">
{translate(`metric.${unitTestsOrLines}.name`)}
@@ -177,7 +180,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {
</div>
)}

{measures.coverage !== undefined && (
{isDefined(measures.coverage) && (
<div className="sw-flex sw-flex-col sw-gap-1">
<Note className="it__source-viewer-header-measure-label sw-body-lg">
{translate('metric.coverage.name')}
@@ -189,7 +192,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {
</div>
)}

{measures.duplicationDensity !== undefined && (
{isDefined(measures.duplicationDensity) && (
<div className="sw-flex sw-flex-col sw-gap-1">
<Note className="it__source-viewer-header-measure-label sw-body-lg">
{translate('duplications')}

+ 8
- 3
server/sonar-web/src/main/js/components/common/FormattingTipsWithLink.tsx View File

@@ -17,7 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { CodeSnippet, Link } from 'design-system';

import { LinkStandalone } from '@sonarsource/echoes-react';
import { CodeSnippet } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from '../../helpers/l10n';
@@ -30,6 +32,7 @@ interface Props {
export default class FormattingTipsWithLink extends React.PureComponent<Props> {
handleClick(evt: React.SyntheticEvent<HTMLAnchorElement>) {
evt.preventDefault();

window.open(
getFormattingHelpUrl(),
'Formatting',
@@ -40,9 +43,10 @@ export default class FormattingTipsWithLink extends React.PureComponent<Props> {
render() {
return (
<div className={this.props.className}>
<Link onClick={this.handleClick} to="#">
<LinkStandalone onClick={this.handleClick} to="#">
{translate('formatting.helplink')}
</Link>
</LinkStandalone>

<p className="sw-mt-2">
<FormattedMessage
id="formatting.example.link"
@@ -50,6 +54,7 @@ export default class FormattingTipsWithLink extends React.PureComponent<Props> {
example: (
<>
<br />

<CodeSnippet
isOneLine
noCopy

+ 14
- 0
server/sonar-web/src/main/js/components/common/Link.tsx View File

@@ -17,6 +17,7 @@
* 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 Echoes from '@sonarsource/echoes-react';
import * as React from 'react';
import { Link as ReactRouterDomLink, LinkProps as ReactRouterDomLinkProps } from 'react-router-dom';
import { isWebUri } from 'valid-url';
@@ -25,6 +26,17 @@ import DetachIcon from '../icons/DetachIcon';

type OriginalLinkProps = ReactRouterDomLinkProps & React.RefAttributes<HTMLAnchorElement>;

/** @deprecated Use {@link Echoes.LinkProps | LinkProps} from Echoes instead.
*
* Some of the props have changed or been renamed:
* - `blurAfterClick` is now `shouldBlurAfterClick`
* - ~`disabled`~ doesn't exist anymore, a disabled link is just a regular text
* - `forceExternal` is now `isExternal`
* - `icon` is now `iconLeft` and can only be used with LinkStandalone
* - `preventDefault` is now `shouldPreventDefault`
* - `showExternalIcon` is now `hasExternalIcon`
* - `stopPropagation` is now `shouldStopPropagation`
*/
export interface LinkProps extends OriginalLinkProps {
size?: number;
}
@@ -66,4 +78,6 @@ function Link({ children, size, ...props }: LinkProps, ref: React.ForwardedRef<H
);
}

/** @deprecated Use either {@link Echoes.Link | Link} or {@link Echoes.LinkStandalone | LinkStandalone} from Echoes instead.
*/
export default React.forwardRef(Link);

Loading…
Cancel
Save