]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21396 Migrate session pages to the new UI
authorJeremy Davis <jeremy.davis@sonarsource.com>
Fri, 5 Jan 2024 09:54:29 +0000 (10:54 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 5 Jan 2024 20:02:36 +0000 (20:02 +0000)
21 files changed:
server/sonar-auth-bitbucket/src/main/java/org/sonar/auth/bitbucket/BitbucketIdentityProvider.java
server/sonar-auth-bitbucket/src/test/java/org/sonar/auth/bitbucket/BitbucketIdentityProviderTest.java
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubIdentityProvider.java
server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java
server/sonar-web/design-system/src/components/buttons/ThirdPartyButton.tsx
server/sonar-web/public/images/sonar-logo-horizontal.png [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx
server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
server/sonar-web/src/main/js/apps/sessions/components/Login.css [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
server/sonar-web/src/main/js/apps/sessions/components/LoginForm.css [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/LoginForm.tsx
server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.tsx
server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.tsx
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-it.tsx
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-it.tsx.snap [deleted file]
server/sonar-web/src/main/js/helpers/testMocks.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 3aee32990718217256a1ada90a71dd6d167d9ff7..0cc02380c1197188c81e80ba6219fa9f1fe3591d 100755 (executable)
@@ -78,7 +78,7 @@ public class BitbucketIdentityProvider implements OAuth2IdentityProvider {
   @Override
   public Display getDisplay() {
     return Display.builder()
-      .setIconPath("/images/alm/bitbucket-white.svg")
+      .setIconPath("/images/alm/bitbucket.svg")
       .setBackgroundColor("#0052cc")
       .build();
   }
index 380dee89e9fea7b32b5b0427f73cecb86dc8403d..5588c76b8c33e9d2c3404afcff5fac9a23687789 100755 (executable)
@@ -42,7 +42,7 @@ public class BitbucketIdentityProviderTest {
   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");
   }
 
index bf671a80bfe969292998c4205d1479b023d5f2d7..37135fe984f70e9847e79b603ff7691f0a64b379 100644 (file)
@@ -62,7 +62,7 @@ public class GitHubIdentityProvider implements OAuth2IdentityProvider {
   @Override
   public Display getDisplay() {
     return Display.builder()
-      .setIconPath("/images/alm/github-white.svg")
+      .setIconPath("/images/alm/github.svg")
       .setBackgroundColor("#444444")
       .build();
   }
index b4495dafc4b094c5eae09f63e7326632f5a9112e..910f59161feaeb9982ff1bcdc6701c8317f639ba 100644 (file)
@@ -46,7 +46,7 @@ public class GitHubIdentityProviderTest {
   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");
   }
 
index 50c83b8ff8288eef9ca582c577ee7c980775a93e..4f85fb6f50e396c49c57550b1e2a133730e0a918 100644 (file)
@@ -28,11 +28,16 @@ interface ThirdPartyProps extends Omit<ButtonProps, 'Icon'> {
   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>
   );
diff --git a/server/sonar-web/public/images/sonar-logo-horizontal.png b/server/sonar-web/public/images/sonar-logo-horizontal.png
new file mode 100644 (file)
index 0000000..310c519
Binary files /dev/null and b/server/sonar-web/public/images/sonar-logo-horizontal.png differ
index 46eed492a2acf6462e1c41379464026930fef851..8d7d42033b3b69adce0929dc236a1e1626ff7772 100644 (file)
@@ -28,7 +28,7 @@ export default function SimpleSessionsContainer() {
       <PageTracker />
 
       <div className="global-container">
-        <div className="page-wrapper" id="container">
+        <div className="page-wrapper new-background" id="container">
           <Outlet />
         </div>
         <GlobalFooter hideLoggedInInfo />
index a2fc52f2feb666ac2f4052187fba4abf76d6a581..b12ec3fbfa0355ff0d2b442b02894da75cbefda7 100644 (file)
@@ -145,7 +145,7 @@ jest.mock('../../../api/users', () => ({
       {
         key: 'github',
         name: 'GitHub',
-        iconPath: '/images/alm/github-white.svg',
+        iconPath: '/images/alm/github.svg',
         backgroundColor: '#444444',
       },
     ],
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Login.css b/server/sonar-web/src/main/js/apps/sessions/components/Login.css
deleted file mode 100644 (file)
index 301206b..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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);
-}
index e9f2e82319ec780c2cb721899e36e88e7470917f..611756f59ce0ba0b15b2271fca88d2ed07c4c164 100644 (file)
  * 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';
 
@@ -38,42 +46,54 @@ export interface LoginProps {
   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')};
+`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginForm.css b/server/sonar-web/src/main/js/apps/sessions/components/LoginForm.css
deleted file mode 100644 (file)
index e49a4c6..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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;
-}
index 142cb826f9bd4a02fa2b4a47067f553dda36355e..5db5d5d0efdefba3403e4d25fc3f509b1d01d106 100644 (file)
  * 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;
@@ -36,6 +40,9 @@ interface State {
   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);
@@ -72,62 +79,50 @@ export default class LoginForm extends React.PureComponent<Props, State> {
   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>
index 0025d734ae9119f02092a26ba43ba1e7d7c0f3ba..92f0943b897af211cf03f5164002384d88acddeb 100644 (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 { CenteredLayout, PageContentFontWrapper } from 'design-system';
 import * as React from 'react';
 import { logOut } from '../../../api/auth';
 import RecentHistory from '../../../app/components/RecentHistory';
@@ -24,8 +25,8 @@ import { addGlobalErrorMessage } from '../../../helpers/globalMessages';
 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();
@@ -34,13 +35,13 @@ export default class Logout extends React.PureComponent {
       .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>
+  );
 }
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css b/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css
deleted file mode 100644 (file)
index 96167bf..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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);
-}
index dc66082c9b5c470f14526e8d34a987f11e8617e4..6dcdf83f8d9dc9f265671ee2db5334947a423130 100644 (file)
  * 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;
-`;
index 4162c4ea9693744ed0a950a82ffe792bf64dffdd..d94e2c0a8cc468dcbf996138394c02757570ef00 100644 (file)
  * 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>
   );
 }
index 9bc294d22fdb637b6d90e46ac2bf6c71b6e6be76..02d3bd88483495e8d60ed847be5e784fa167ed58 100644 (file)
@@ -25,6 +25,7 @@ import { getIdentityProviders } from '../../../../api/users';
 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', () => {
@@ -80,23 +81,21 @@ it('should behave correctly', async () => {
   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.
@@ -118,7 +117,7 @@ it('should behave correctly', async () => {
 });
 
 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' });
@@ -126,7 +125,7 @@ it('should not show any OAuth providers if none are configured', async () => {
 
   // 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 () => {
@@ -142,14 +141,14 @@ 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' });
@@ -161,3 +160,9 @@ function renderLoginContainer(props: Partial<LoginContainer['props']> = {}) {
     <LoginContainer location={mockLocation({ query: { return_to: '/some/path' } })} {...props} />,
   );
 }
+
+const ui = {
+  loginInput: byLabelText(/login/),
+  passwordInput: byLabelText(/password/),
+  backLink: byRole('link', { name: 'go_back' }),
+};
index a60a42cd7976c1ac313d9ed4b178ffae1d99003e..7e9d559f14f266801ea7d16a8ad7c539880fa53b 100644 (file)
@@ -78,7 +78,7 @@ it('should behave correctly', async () => {
 });
 
 it('should correctly handle a failing log out', async () => {
-  (logOut as jest.Mock).mockRejectedValueOnce(false);
+  jest.mocked(logOut).mockRejectedValueOnce(false);
   renderLogout();
 
   await waitFor(() => {
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-it.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-it.tsx.snap
deleted file mode 100644 (file)
index 96449a1..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-// 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>
-`;
index 4178e537e2eeec9c5ee312d96523447361f2ad62..0a4b5e9f32efe30c8e305eb7e574222717435af8 100644 (file)
@@ -78,7 +78,7 @@ import { CurrentUser, LoggedInUser, RestUserDetailed, User } from '../types/user
 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',
index 12833ba60af27dc15186d58e4169f73dde5360a4..83bfd80c7ee99f722146940492b817c54030d5b0 100644 (file)
@@ -2632,7 +2632,7 @@ user.x_deleted={0} (deleted)
 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