@@ -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; |
@@ -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')}; | |||
`; |
@@ -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™ 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> | |||
); |
@@ -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')} | |||
/> | |||
); |
@@ -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> | |||
); |
@@ -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> | |||
)} | |||
</> | |||
); |
@@ -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> |
@@ -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> | |||
) : ( |
@@ -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` |
@@ -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} /> | |||
)} |
@@ -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> | |||
); |
@@ -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> |
@@ -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() { |
@@ -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')} |
@@ -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')} |
@@ -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 |
@@ -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); |