@Override
public Display getDisplay() {
return Display.builder()
- .setIconPath("/images/alm/bitbucket-white.svg")
+ .setIconPath("/images/alm/bitbucket.svg")
.setBackgroundColor("#0052cc")
.build();
}
public void check_fields() {
assertThat(underTest.getKey()).isEqualTo("bitbucket");
assertThat(underTest.getName()).isEqualTo("Bitbucket");
- assertThat(underTest.getDisplay().getIconPath()).isEqualTo("/images/alm/bitbucket-white.svg");
+ assertThat(underTest.getDisplay().getIconPath()).isEqualTo("/images/alm/bitbucket.svg");
assertThat(underTest.getDisplay().getBackgroundColor()).isEqualTo("#0052cc");
}
@Override
public Display getDisplay() {
return Display.builder()
- .setIconPath("/images/alm/github-white.svg")
+ .setIconPath("/images/alm/github.svg")
.setBackgroundColor("#444444")
.build();
}
public void check_fields() {
assertThat(underTest.getKey()).isEqualTo("github");
assertThat(underTest.getName()).isEqualTo("GitHub");
- assertThat(underTest.getDisplay().getIconPath()).isEqualTo("/images/alm/github-white.svg");
+ assertThat(underTest.getDisplay().getIconPath()).isEqualTo("/images/alm/github.svg");
assertThat(underTest.getDisplay().getBackgroundColor()).isEqualTo("#444444");
}
name: string;
}
-export function ThirdPartyButton({ children, iconPath, name, ...buttonProps }: ThirdPartyProps) {
+export function ThirdPartyButton({
+ children,
+ iconPath,
+ name,
+ ...buttonProps
+}: Readonly<ThirdPartyProps>) {
const size = 16;
return (
<ThirdPartyButtonStyled {...buttonProps}>
- <img alt={name} className="sw-mr-1" height={size} src={iconPath} width={size} />
+ <img alt={name} className="sw-mr-2" height={size} src={iconPath} width={size} />
{children}
</ThirdPartyButtonStyled>
);
<PageTracker />
<div className="global-container">
- <div className="page-wrapper" id="container">
+ <div className="page-wrapper new-background" id="container">
<Outlet />
</div>
<GlobalFooter hideLoggedInInfo />
{
key: 'github',
name: 'GitHub',
- iconPath: '/images/alm/github-white.svg',
+ iconPath: '/images/alm/github.svg',
backgroundColor: '#444444',
},
],
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.login-page {
- padding-top: 10vh;
- max-width: 300px;
- margin: 0 auto;
- align-items: center;
- display: flex;
- flex-direction: column;
-}
-
-.login-title {
- line-height: 1.5;
- font-size: 24px;
- font-weight: 300;
-}
-
-.login-message {
- width: 450px;
- background-color: var(--info50);
- border: 1px solid var(--info200);
- border-radius: 2px;
- color: rgba(0, 0, 0, 0.87);
-}
* 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 {
+ Card,
+ FlagMessage,
+ PageContentFontWrapper,
+ Spinner,
+ Title,
+ themeBorder,
+ themeColor,
+} from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { Location } from '../../../components/hoc/withRouter';
-import { Alert } from '../../../components/ui/Alert';
-import Spinner from '../../../components/ui/Spinner';
import { translate } from '../../../helpers/l10n';
import { sanitizeUserInput } from '../../../helpers/sanitize';
+import { getBaseUrl } from '../../../helpers/system';
import { getReturnUrl } from '../../../helpers/urls';
import { IdentityProvider } from '../../../types/types';
-import './Login.css';
import LoginForm from './LoginForm';
import OAuthProviders from './OAuthProviders';
location: Location;
}
-export default function Login(props: LoginProps) {
+export default function Login(props: Readonly<LoginProps>) {
const { identityProviders, loading, location, message } = props;
const returnTo = getReturnUrl(location);
const displayError = Boolean(location.query.authorizationError);
return (
- <div className="login-page" id="login_form">
- <h1 className="login-title text-center big-spacer-bottom">
- {translate('login.login_to_sonarqube')}
- </h1>
+ <div className="sw-flex sw-flex-col sw-items-center" id="login_form">
<Helmet defer={false} title={translate('login.page')} />
- {loading ? (
- <Spinner loading={loading} />
- ) : (
- <>
- {displayError && (
- <Alert className="big-spacer-bottom" display="block" variant="error">
- {translate('login.unauthorized_access_alert')}
- </Alert>
- )}
+ <img alt="" className="sw-mt-32" src={`${getBaseUrl()}/images/sonar-logo-horizontal.png`} />
+ <Card className="sw-my-14 sw-p-0 sw-w-abs-350">
+ <PageContentFontWrapper className="sw-body-md sw-flex sw-flex-col sw-items-center sw-py-8 sw-px-4">
+ <img
+ alt=""
+ className="sw-mb-6"
+ src={`${getBaseUrl()}/images/embed-doc/sq-icon.svg`}
+ width={28}
+ />
+ <Title className="sw-mb-6">{translate('login.login_to_sonarqube')}</Title>
+ <Spinner loading={loading}>
+ <>
+ {displayError && (
+ <FlagMessage className="sw-mb-6" variant="error">
+ {translate('login.unauthorized_access_alert')}
+ </FlagMessage>
+ )}
- {message && (
- <div
- className="login-message markdown big-padded spacer-top huge-spacer-bottom"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeUserInput(message) }}
- />
- )}
+ {message !== undefined && message.length > 0 && (
+ <StyledMessage
+ className="markdown sw-rounded-2 sw-p-4 sw-mb-6"
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: sanitizeUserInput(message) }}
+ />
+ )}
- {identityProviders.length > 0 && (
- <OAuthProviders identityProviders={identityProviders} returnTo={returnTo} />
- )}
+ {identityProviders.length > 0 && (
+ <OAuthProviders identityProviders={identityProviders} returnTo={returnTo} />
+ )}
- <LoginForm collapsed={identityProviders.length > 0} onSubmit={props.onSubmit} />
- </>
- )}
+ <LoginForm collapsed={identityProviders.length > 0} onSubmit={props.onSubmit} />
+ </>
+ </Spinner>
+ </PageContentFontWrapper>
+ </Card>
</div>
);
}
+
+const StyledMessage = styled.div`
+ background: ${themeColor('highlightedSection')};
+ border: ${themeBorder('default', 'highlightedSectionBorder')};
+`;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.login-form {
- width: 300px;
- margin-left: auto;
- margin-right: auto;
-}
-
-.login-input {
- width: 100% !important;
- height: auto !important;
- padding: 5px 12px !important;
- font-size: 20px;
- font-weight: 300;
-}
-
-.login-label {
- display: none;
- margin-bottom: 8px;
- font-size: 15px;
-}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ ButtonPrimary,
+ ButtonSecondary,
+ FormField,
+ InputField,
+ Link,
+ Spinner,
+} from 'design-system';
import * as React from 'react';
-import Link from '../../../components/common/Link';
-import { ButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import Spinner from '../../../components/ui/Spinner';
import { translate } from '../../../helpers/l10n';
-import './LoginForm.css';
interface Props {
collapsed?: boolean;
password: string;
}
+const LOGIN_INPUT_ID = 'login-input';
+const PASSWORD_INPUT_ID = 'password-input';
+
export default class LoginForm extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
render() {
if (this.state.collapsed) {
return (
- <div className="text-center">
- <ButtonLink
- aria-expanded={false}
- className="small js-more-options"
- onClick={this.handleMoreOptionsClick}
- >
- {translate('login.more_options')}
- </ButtonLink>
- </div>
+ <ButtonSecondary
+ className="sw-w-full sw-justify-center"
+ aria-expanded={false}
+ onClick={this.handleMoreOptionsClick}
+ >
+ {translate('login.more_options')}
+ </ButtonSecondary>
);
}
return (
- <form className="login-form" onSubmit={this.handleSubmit}>
- <div className="big-spacer-bottom">
- <label className="login-label" htmlFor="login">
- {translate('login')}
- </label>
- <input
+ <form className="sw-w-full" onSubmit={this.handleSubmit}>
+ <FormField label={translate('login')} htmlFor={LOGIN_INPUT_ID} required>
+ <InputField
autoFocus
- className="login-input"
- id="login"
+ id={LOGIN_INPUT_ID}
maxLength={255}
name="login"
onChange={this.handleLoginChange}
- placeholder={translate('login')}
required
type="text"
value={this.state.login}
+ size="full"
/>
- </div>
+ </FormField>
- <div className="big-spacer-bottom">
- <label className="login-label" htmlFor="password">
- {translate('password')}
- </label>
- <input
- className="login-input"
- id="password"
+ <FormField label={translate('password')} htmlFor={PASSWORD_INPUT_ID} required>
+ <InputField
+ id={PASSWORD_INPUT_ID}
name="password"
onChange={this.handlePwdChange}
- placeholder={translate('password')}
required
type="password"
value={this.state.password}
+ size="full"
/>
- </div>
+ </FormField>
<div>
- <div className="text-right overflow-hidden">
- <Spinner className="spacer-right" loading={this.state.loading} />
- <SubmitButton disabled={this.state.loading}>
+ <div className="sw-overflow-hidden sw-flex sw-items-center sw-justify-end sw-gap-3">
+ <Spinner loading={this.state.loading} />
+ <Link to="/">{translate('go_back')}</Link>
+ <ButtonPrimary disabled={this.state.loading} type="submit">
{translate('sessions.log_in')}
- </SubmitButton>
- <Link className="spacer-left" to="/">
- {translate('cancel')}
- </Link>
+ </ButtonPrimary>
</div>
</div>
</form>
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { CenteredLayout, PageContentFontWrapper } from 'design-system';
import * as React from 'react';
import { logOut } from '../../../api/auth';
import RecentHistory from '../../../app/components/RecentHistory';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
-export default class Logout extends React.PureComponent {
- componentDidMount() {
+export default function Logout() {
+ React.useEffect(() => {
logOut()
.then(() => {
RecentHistory.clear();
.catch(() => {
addGlobalErrorMessage(translate('login.logout_failed'));
});
- }
+ }, []);
- render() {
- return (
- <div className="page page-limited">
- <div className="text-center">{translate('logging_out')}</div>
- </div>
- );
- }
+ return (
+ <CenteredLayout>
+ <PageContentFontWrapper className="sw-body-md sw-mt-14 sw-text-center">
+ {translate('logging_out')}
+ </PageContentFontWrapper>
+ </CenteredLayout>
+ );
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.oauth-providers-help {
- position: absolute;
- top: 15px;
- right: -24px;
-}
-
-.oauth-providers + .login-form {
- padding-top: 30px;
- border-top: 1px solid var(--barBorderColor);
-}
* 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 classNames from 'classnames';
+import { BasicSeparator, ThirdPartyButton } from 'design-system';
import * as React from 'react';
import HelpTooltip from '../../../components/controls/HelpTooltip';
-import IdentityProviderLink from '../../../components/controls/IdentityProviderLink';
import { translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import { IdentityProvider } from '../../../types/types';
-import './OAuthProviders.css';
interface Props {
- className?: string;
- formatLabel?: (name: string) => React.ReactNode;
identityProviders: IdentityProvider[];
returnTo: string;
}
-export default function OAuthProviders(props: Props) {
- const formatFunction = props.formatLabel || defaultFormatLabel;
- return (
- <Container className={classNames('oauth-providers', props.className)}>
- {props.identityProviders.map((identityProvider) => (
- <OAuthProvider
- format={formatFunction}
- identityProvider={identityProvider}
- key={identityProvider.key}
- returnTo={props.returnTo}
- />
- ))}
- </Container>
+export default function OAuthProviders({ identityProviders, returnTo }: Readonly<Props>) {
+ const authenticate = React.useCallback(
+ (key: string) => {
+ // We need a real page refresh, as the login mechanism is handled on the server
+ window.location.replace(
+ `${getBaseUrl()}/sessions/init/${key}?return_to=${encodeURIComponent(returnTo)}`,
+ );
+ },
+ [returnTo],
);
-}
-
-interface ItemProps {
- format: (name: string) => React.ReactNode;
- identityProvider: IdentityProvider;
- returnTo: string;
-}
-function OAuthProvider({ format, identityProvider, returnTo }: ItemProps) {
return (
- <IdentityProviderWrapper>
- <IdentityProviderLink
- backgroundColor={identityProvider.backgroundColor}
- iconPath={identityProvider.iconPath}
- name={identityProvider.name}
- url={
- `${getBaseUrl()}/sessions/init/${identityProvider.key}` +
- `?return_to=${encodeURIComponent(returnTo)}`
- }
- >
- <span>{format(identityProvider.name)}</span>
- </IdentityProviderLink>
- {identityProvider.helpMessage && (
- <HelpTooltip className="oauth-providers-help" overlay={identityProvider.helpMessage} />
- )}
- </IdentityProviderWrapper>
+ <>
+ <div className="sw-w-full sw-flex sw-flex-col sw-gap-4" id="oauth-providers">
+ {identityProviders.map((identityProvider) => (
+ <div key={identityProvider.key}>
+ <ThirdPartyButton
+ className="sw-w-full sw-justify-center"
+ name={identityProvider.name}
+ iconPath={identityProvider.iconPath}
+ onClick={() => authenticate(identityProvider.key)}
+ >
+ <span>{translateWithParameters('login.login_with_x', identityProvider.name)}</span>
+ </ThirdPartyButton>
+ {identityProvider.helpMessage && (
+ <HelpTooltip
+ className="oauth-providers-help"
+ overlay={identityProvider.helpMessage}
+ />
+ )}
+ </div>
+ ))}
+ </div>
+ <BasicSeparator className="sw-my-6 sw-w-full" />
+ </>
);
}
-
-function defaultFormatLabel(name: string) {
- return translateWithParameters('login.login_with_x', name);
-}
-
-const Container = styled.div`
- display: inline-flex;
- flex-direction: column;
- align-items: stretch;
-`;
-
-const IdentityProviderWrapper = styled.div`
- margin-bottom: 30px;
-`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Card, CenteredLayout, Link, PageContentFontWrapper } from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
-import Link from '../../../components/common/Link';
import { getCookie } from '../../../helpers/cookies';
import { translate } from '../../../helpers/l10n';
export default function Unauthorized() {
const message = decodeURIComponent(getCookie('AUTHENTICATION-ERROR') || '');
return (
- <div className="page-wrapper-simple" id="bd">
+ <CenteredLayout id="bd">
<Helmet defer={false} title={translate('unauthorized.page')} />
- <div className="page-simple" id="nonav">
- <div className="text-center">
+ <PageContentFontWrapper className="sw-body-md sw-flex sw-justify-center" id="nonav">
+ <Card className="sw-w-abs-500 sw-my-14 sw-text-center">
<p id="unauthorized">{translate('unauthorized.message')}</p>
{Boolean(message) && (
- <p className="spacer-top">
- {translate('unauthorized.reason')} {message}
+ <p className="sw-mt-4">
+ {translate('unauthorized.reason')}
+ <br /> {message}
</p>
)}
- <div className="big-spacer-top">
+ <div className="sw-mt-8">
<Link to="/">{translate('layout.home')}</Link>
</div>
- </div>
- </div>
- </div>
+ </Card>
+ </PageContentFontWrapper>
+ </CenteredLayout>
);
}
import { addGlobalErrorMessage } from '../../../../helpers/globalMessages';
import { mockLocation } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byLabelText, byRole } from '../../../../helpers/testSelector';
import { LoginContainer } from '../LoginContainer';
jest.mock('../../../../api/users', () => {
expect(heading).toBeInTheDocument();
// OAuth provider.
- const link = screen.getByRole('link', { name: 'Github login.login_with_x.Github' });
+ const link = screen.getByRole('button', { name: 'Github login.login_with_x.Github' });
expect(link).toBeInTheDocument();
- expect(link).toHaveAttribute('href', '/sessions/init/github?return_to=%2Fsome%2Fpath');
- expect(link).toMatchSnapshot('OAuthProvider link');
// Login form collapsed by default.
- expect(screen.queryByLabelText('login')).not.toBeInTheDocument();
+ expect(ui.loginInput.query()).not.toBeInTheDocument();
// Open login form, log in.
await user.click(screen.getByRole('button', { name: 'login.more_options' }));
- const cancelLink = await screen.findByRole('link', { name: 'cancel' });
+ const cancelLink = await ui.backLink.find();
expect(cancelLink).toBeInTheDocument();
expect(cancelLink).toHaveAttribute('href', '/');
- const loginField = screen.getByLabelText('login');
- const passwordField = screen.getByLabelText('password');
+ const loginField = ui.loginInput.get();
+ const passwordField = ui.passwordInput.get();
const submitButton = screen.getByRole('button', { name: 'sessions.log_in' });
// Incorrect login.
});
it('should not show any OAuth providers if none are configured', async () => {
- (getIdentityProviders as jest.Mock).mockResolvedValueOnce({ identityProviders: [] });
+ jest.mocked(getIdentityProviders).mockResolvedValueOnce({ identityProviders: [] });
renderLoginContainer();
const heading = await screen.findByRole('heading', { name: 'login.login_to_sonarqube' });
// No OAuth providers, login form display by default.
expect(screen.queryByRole('link', { name: 'login.login_with_x' })).not.toBeInTheDocument();
- expect(screen.getByLabelText('login')).toBeInTheDocument();
+ expect(ui.loginInput.get()).toBeInTheDocument();
});
it("should show a warning if there's an authorization error", async () => {
it('should display a login message if enabled & provided', async () => {
const message = 'Welcome to SQ! Please use your Skynet credentials';
- (getLoginMessage as jest.Mock).mockResolvedValueOnce({ message });
+ jest.mocked(getLoginMessage).mockResolvedValueOnce({ message });
renderLoginContainer({});
expect(await screen.findByText(message)).toBeInTheDocument();
});
it('should handle errors', async () => {
- (getLoginMessage as jest.Mock).mockRejectedValueOnce('nope');
+ jest.mocked(getLoginMessage).mockRejectedValueOnce('nope');
renderLoginContainer({});
const heading = await screen.findByRole('heading', { name: 'login.login_to_sonarqube' });
<LoginContainer location={mockLocation({ query: { return_to: '/some/path' } })} {...props} />,
);
}
+
+const ui = {
+ loginInput: byLabelText(/login/),
+ passwordInput: byLabelText(/password/),
+ backLink: byRole('link', { name: 'go_back' }),
+};
});
it('should correctly handle a failing log out', async () => {
- (logOut as jest.Mock).mockRejectedValueOnce(false);
+ jest.mocked(logOut).mockRejectedValueOnce(false);
renderLogout();
await waitFor(() => {
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should behave correctly: OAuthProvider link 1`] = `
-<a
- class="identity-provider-link"
- href="/sessions/init/github?return_to=%2Fsome%2Fpath"
- style="background-color: rgb(0, 0, 0);"
->
- <img
- alt="Github"
- height="20"
- src="/path/icon.svg"
- width="20"
- />
- <span>
- login.login_with_x.Github
- </span>
-</a>
-`;
export function mockAlmApplication(overrides: Partial<AlmApplication> = {}): AlmApplication {
return {
backgroundColor: '#444444',
- iconPath: '/images/sonarcloud/github-white.svg',
+ iconPath: '/images/alm/github.svg',
installationUrl: 'https://github.com/apps/greg-sonarcloud/installations/new',
key: 'github',
name: 'GitHub',
login.page=Log in
login.login_to_sonarqube=Log in to SonarQube
login.login_with_x=Log in with {0}
-login.more_options=More options
+login.more_options=Manual login
login.unauthorized_access_alert=You are not authorized to access this page. Please log in with more privileges and try again.
login.with_x=With {0}
login.authentication_failed=Authentication failed