From d17f1fa0ac2b38f1e94f2e7e5d72f9a747062c8b Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 6 Oct 2022 17:43:42 +0200 Subject: [PATCH] SONAR-17436 Improve SL connection UX --- .../public/images/SonarLint-connection-ok.png | Bin 0 -> 6308 bytes .../images/SonarLint-connection-request.png | Bin 0 -> 6605 bytes server/sonar-web/public/images/check.svg | 3 + server/sonar-web/public/images/cross.svg | 3 + .../src/main/js/api/mocks/UserTokensMock.ts | 10 ++ .../js/app/components/SonarLintConnection.css | 20 ++- .../js/app/components/SonarLintConnection.tsx | 165 ++++++++++++++---- .../__tests__/SonarLintConnection-test.tsx | 39 +++-- .../resources/org/sonar/l10n/core.properties | 29 ++- 9 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 server/sonar-web/public/images/SonarLint-connection-ok.png create mode 100644 server/sonar-web/public/images/SonarLint-connection-request.png create mode 100644 server/sonar-web/public/images/check.svg create mode 100644 server/sonar-web/public/images/cross.svg diff --git a/server/sonar-web/public/images/SonarLint-connection-ok.png b/server/sonar-web/public/images/SonarLint-connection-ok.png new file mode 100644 index 0000000000000000000000000000000000000000..e4071d0e2f0964c2c654ecc23f4d4ff2d708996a GIT binary patch literal 6308 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 LNkvXXu0mjfNXpqo literal 0 HcmV?d00001 diff --git a/server/sonar-web/public/images/check.svg b/server/sonar-web/public/images/check.svg new file mode 100644 index 00000000000..30051d62119 --- /dev/null +++ b/server/sonar-web/public/images/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/server/sonar-web/public/images/cross.svg b/server/sonar-web/public/images/cross.svg new file mode 100644 index 00000000000..9ca7781bec9 --- /dev/null +++ b/server/sonar-web/public/images/cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts b/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts index 9a10d6e29e0..cd21d0fc443 100644 --- a/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts @@ -40,6 +40,7 @@ const defaultTokens = [ export default class UserTokensMock { tokens: Array & UserToken>; + failGeneration = false; constructor() { this.tokens = cloneDeep(defaultTokens); @@ -66,6 +67,11 @@ export default class UserTokensMock { projectKey: string; expirationDate?: string; }) => { + if (this.failGeneration) { + this.failGeneration = false; + return Promise.reject('x_x'); + } + const token = { name, login, @@ -96,6 +102,10 @@ export default class UserTokensMock { return Promise.resolve(); }; + failNextTokenGeneration = () => { + this.failGeneration = true; + }; + getTokens = () => { return cloneDeep(this.tokens); }; diff --git a/server/sonar-web/src/main/js/app/components/SonarLintConnection.css b/server/sonar-web/src/main/js/app/components/SonarLintConnection.css index 8ff95e9993c..75072217d93 100644 --- a/server/sonar-web/src/main/js/app/components/SonarLintConnection.css +++ b/server/sonar-web/src/main/js/app/components/SonarLintConnection.css @@ -29,7 +29,25 @@ } .sonarlint-connection-content { - min-width: 500px; + min-width: 600px; width: 40%; margin: 0 auto; } + +.sonarlint-connection-page ol { + list-style: inside decimal; +} + +.sonarlint-connection-page .field-label { + display: inline-block; + width: 150px; + color: var(--secondFontColor); + text-align: start; + flex-shrink: 0; +} + +.sonarlint-connection-page .sonarlint-token-value { + background-color: var(--codeBackground); + border: 1px solid var(--barBorderColor); + padding: calc(var(--gridSize) / 2) var(--gridSize); +} diff --git a/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx b/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx index 6483aa50fd8..77cb25dc997 100644 --- a/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx +++ b/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx @@ -18,10 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { generateToken, getTokens } from '../../api/user-tokens'; +import Link from '../../components/common/Link'; import { Button } from '../../components/controls/buttons'; +import { ClipboardButton } from '../../components/controls/clipboard'; import { whenLoggedIn } from '../../components/hoc/whenLoggedIn'; +import CheckIcon from '../../components/icons/CheckIcon'; import { translate, translateWithParameters } from '../../helpers/l10n'; import { portIsValid, sendUserToken } from '../../helpers/sonarlint'; import { @@ -29,13 +33,23 @@ import { getAvailableExpirationOptions, getNextTokenName } from '../../helpers/tokens'; +import { NewUserToken, TokenExpiration } from '../../types/token'; import { LoggedInUser } from '../../types/users'; import './SonarLintConnection.css'; +enum Status { + request, + tokenError, + connectionError, + success +} + interface Props { currentUser: LoggedInUser; } +const TOKEN_PREFIX = 'SonarLint'; + const getNextAvailableTokenName = async (login: string, tokenNameBase: string) => { const tokens = await getTokens(login); @@ -46,15 +60,14 @@ async function computeExpirationDate() { const options = await getAvailableExpirationOptions(); const maxOption = options[options.length - 1]; - return maxOption.value ? computeTokenExpirationDate(maxOption.value) : undefined; + return computeTokenExpirationDate(maxOption.value || TokenExpiration.OneYear); } export function SonarLintConnection({ currentUser }: Props) { const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const [success, setSuccess] = React.useState(false); - const [error, setError] = React.useState(''); - const [newTokenName, setNewTokenName] = React.useState(''); + const [status, setStatus] = React.useState(Status.request); + const [newToken, setNewToken] = React.useState(undefined); const port = parseInt(searchParams.get('port') ?? '0', 10); const ideName = searchParams.get('ideName') ?? translate('sonarlint-connection.unspecified-ide'); @@ -69,20 +82,24 @@ export function SonarLintConnection({ currentUser }: Props) { const { login } = currentUser; const authorize = React.useCallback(async () => { - const newTokenName = await getNextAvailableTokenName(login, `sonarlint-${ideName}`); + const newTokenName = await getNextAvailableTokenName(login, `${TOKEN_PREFIX}-${ideName}`); const expirationDate = await computeExpirationDate(); - const token = await generateToken({ name: newTokenName, login, expirationDate }); + const token = await generateToken({ name: newTokenName, login, expirationDate }).catch( + () => undefined + ); + + if (!token) { + setStatus(Status.tokenError); + return; + } + + setNewToken(token); try { await sendUserToken(port, token); - setSuccess(true); - setNewTokenName(newTokenName); - } catch (error) { - if (error instanceof Error) { - setError(error.message); - } else { - setError('-'); - } + setStatus(Status.success); + } catch (_) { + setStatus(Status.connectionError); } }, [port, ideName, login]); @@ -90,31 +107,119 @@ export function SonarLintConnection({ currentUser }: Props) {
-

{translate('sonarlint-connection.title')}

- {error && ( + {status === Status.request && ( <> -

{translate('sonarlint-connection.error')}

-

{error}

+

+ {translate('sonarlint-connection.request.title')} +

+ +

+ {translateWithParameters('sonarlint-connection.request.description', ideName)} +

+

+ {translate('sonarlint-connection.request.description2')} +

+ + )} - {success && ( -

- {translateWithParameters('sonarlint-connection.success', newTokenName)} -

- )} - {!error && !success && ( + {status === Status.tokenError && ( <> -

- {translateWithParameters('sonarlint-connection.description', ideName)} + +

+ {translate('sonarlint-connection.token-error.title')} +

+

+ {translate('sonarlint-connection.token-error.description')}

- {translate('sonarlint-connection.description2')} + + {translate('sonarlint-connection.token-error.description2.link')} + + ) + }} + />

+ + )} - + {status === Status.connectionError && newToken && ( + <> + +

+ {translate('sonarlint-connection.connection-error.title')} +

+

+ {translate('sonarlint-connection.connection-error.description')} +

+
+ + {translate('sonarlint-connection.connection-error.token-name')} + + {newToken.name} +
+
+
+ + {translate('sonarlint-connection.connection-error.token-value')} + + {newToken.token} + +
+
+ {translate('sonarlint-connection.connection-error.next-steps')} +
+
    +
  1. {translate('sonarlint-connection.connection-error.step1')}
  2. +
  3. {translate('sonarlint-connection.connection-error.step2')}
  4. +
+ + )} + + {status === Status.success && newToken && ( + <> +

+ {translate('sonarlint-connection.success.title')} +

+ +

+ {translateWithParameters('sonarlint-connection.success.description', newToken.name)} +

+
+ {translate('sonarlint-connection.success.last-step')} +
+
+ {translate('sonarlint-connection.success.step')} +
)}
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/SonarLintConnection-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/SonarLintConnection-test.tsx index 0cec3892d3c..a023d6d4896 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/SonarLintConnection-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/SonarLintConnection-test.tsx @@ -74,28 +74,47 @@ it('should allow the user to accept the binding request', async () => { renderSonarLintConnection(); expect( - await screen.findByRole('heading', { name: 'sonarlint-connection.title' }) + await screen.findByRole('heading', { name: 'sonarlint-connection.request.title' }) ).toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: 'sonarlint-connection.action' })); + await user.click(screen.getByRole('button', { name: 'sonarlint-connection.request.action' })); expect( - await screen.findByText( - 'sonarlint-connection.success.sonarlint-sonarlint-connection.unspecified-ide' - ) + await screen.findByText('sonarlint-connection.success.description', { exact: false }) ).toBeInTheDocument(); }); -it('should handle errors on binding', async () => { - (sendUserToken as jest.Mock).mockRejectedValueOnce(new Error('error message')); +it('should handle token generation errors', async () => { + tokenMock.failNextTokenGeneration(); const user = userEvent.setup(); renderSonarLintConnection(); - await user.click(await screen.findByRole('button', { name: 'sonarlint-connection.action' })); + await user.click( + await screen.findByRole('button', { name: 'sonarlint-connection.request.action' }) + ); - expect(await screen.findByText('sonarlint-connection.error')).toBeInTheDocument(); - expect(await screen.findByText('error message')).toBeInTheDocument(); + expect( + await screen.findByText('sonarlint-connection.token-error.description') + ).toBeInTheDocument(); +}); + +it('should handle connection errors', async () => { + (sendUserToken as jest.Mock).mockRejectedValueOnce(new Error('')); + + const user = userEvent.setup(); + renderSonarLintConnection(); + + await user.click( + await screen.findByRole('button', { name: 'sonarlint-connection.request.action' }) + ); + + expect( + await screen.findByText('sonarlint-connection.connection-error.description') + ).toBeInTheDocument(); + + const tokenValue = tokenMock.getLastToken()?.token ?? ''; + expect(await screen.findByText(tokenValue)).toBeInTheDocument(); }); it('should require authentication if user is not logged in', () => { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 735c071564a..225ba753801 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2759,13 +2759,30 @@ promotion.sonarlint.content=Take advantage of the whole ecosystem by using Sonar # #------------------------------------------------------------------------------ -sonarlint-connection.title=SonarLint binding -sonarlint-connection.description=An instance of SonarLint for {0} requests access to this SonarQube instance. +sonarlint-connection.request.title=Allow SonarLint connection? +sonarlint-connection.request.description=SonarLint for {0} is requesting access to SonarQube. +sonarlint-connection.request.description2=Do you allow SonarLint to connect? This will create a token and share it with SonarLint. +sonarlint-connection.request.action=Allow connection + +sonarlint-connection.token-error.title=Token generation failed +sonarlint-connection.token-error.description=SonarQube was not able to generate a token. +sonarlint-connection.token-error.description2=Go back to your IDE and start again, or go to the {link} of your SonarQube account to create a new user token manually. +sonarlint-connection.token-error.description2.link=Security section + +sonarlint-connection.connection-error.title=Token created +sonarlint-connection.connection-error.description=The following token was created: +sonarlint-connection.connection-error.token-name=Token name +sonarlint-connection.connection-error.token-value=Token value +sonarlint-connection.connection-error.next-steps=Next steps +sonarlint-connection.connection-error.step1=Copy the above token. +sonarlint-connection.connection-error.step2=Go back to your IDE and paste the token in SonarLint. + +sonarlint-connection.success.title=SonarLint connection is almost ready! +sonarlint-connection.success.description=A new '{0}' token was created and sent to SonarLint in your IDE. +sonarlint-connection.success.last-step=Last step +sonarlint-connection.success.step=Go back to your IDE to complete the setup. + sonarlint-connection.unspecified-ide=an unspecified IDE -sonarlint-connection.description2=Click on the button below to allow it to connect to this SonarQube instance. This will create a token and share it with SonarLint. -sonarlint-connection.action=Allow SonarLint to connect to this instance -sonarlint-connection.error=Something went wrong. Make sure your IDE is still running and try again. -sonarlint-connection.success=A new '{0}' token was created and successfully sent to SonarLint in your IDE. #------------------------------------------------------------------------------ # -- 2.39.5