]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21384 Migrate the user tokens modal to MIUI
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Wed, 3 Jan 2024 18:04:47 +0000 (19:04 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 15 Jan 2024 20:03:06 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx
server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx
server/sonar-web/src/main/js/apps/users/components/TokensFormNewToken.tsx

index b12ec3fbfa0355ff0d2b442b02894da75cbefda7..44209c8012dc50e2e4da1edc335126183c835aac 100644 (file)
@@ -316,7 +316,7 @@ describe('security page', () => {
       expect(
         await screen.findByText(`users.tokens.new_token_created.${newTokenName}`),
       ).toBeInTheDocument();
-      expect(screen.getByRole('button', { name: 'copy_to_clipboard' })).toBeInTheDocument();
+      expect(screen.getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument();
 
       const lastTokenCreated = tokenMock.getLastToken();
       // eslint-disable-next-line jest/no-conditional-in-test
index 33a0be26b466c6c9c923a173b73b51b7cd1daa83..7057c0b9bd767230629b9082a51e064f9baa308e 100644 (file)
@@ -58,6 +58,7 @@ const ui = {
   scmAddButton: byRole('button', { name: 'add_verb' }),
   createUserDialogButton: byRole('button', { name: 'create' }),
   cancelButton: byRole('button', { name: 'cancel' }),
+  closeButton: byRole('button', { name: 'close' }),
   reloadButton: byRole('button', { name: 'reload' }),
   doneButton: byRole('button', { name: 'done' }),
   changeButton: byRole('button', { name: 'change_verb' }),
@@ -107,7 +108,7 @@ const ui = {
   unselectedFilter: byRole('radio', { name: 'unselected' }),
 
   getGroups: () => within(ui.dialogGroups.get()).getAllByRole('checkbox'),
-  dialogTokens: byRole('dialog', { name: 'users.tokens' }),
+  dialogTokens: byRole('dialog', { name: /users.user_X_tokens/ }),
   dialogPasswords: byRole('dialog', { name: 'my_profile.password.title' }),
   dialogUpdateUser: byRole('dialog', { name: 'users.update_user' }),
   dialogCreateUser: byRole('dialog', { name: 'users.create_user' }),
@@ -136,6 +137,7 @@ const ui = {
   githubProvisioningSuccess: byText(/synchronization_successful/),
   githubProvisioningWarning: byText(/synchronization_successful.with_warning/),
   githubProvisioningAlert: byText(/synchronization_failed_short/),
+  expiresInSelector: byRole('combobox', { name: 'users.tokens.expires_in' }),
 };
 
 beforeEach(() => {
@@ -568,27 +570,38 @@ describe('in manage mode', () => {
 
     const getTokensList = () => ui.dialogTokens.byRole('row').getAll();
 
-    expect(getTokensList()).toHaveLength(3);
+    expect(getTokensList()).toHaveLength(3); // header + 2 mocked tokens
 
     await user.type(ui.tokenNameInput.get(), 'test');
     await user.click(ui.generateButton.get());
 
-    // Not deleted because there is already token with name test
+    // Not created because there is already a token with the name "test"
     expect(screen.queryByText('users.tokens.new_token_created.test')).not.toBeInTheDocument();
-    expect(getTokensList()).toHaveLength(3);
+    expect(getTokensList()).toHaveLength(3); // header + 2 mocked tokens
 
     expect(ui.sureButton.query()).not.toBeInTheDocument();
     await user.click(ui.revokeButton('test').get());
     expect(await ui.sureButton.find()).toBeInTheDocument();
     await user.click(ui.sureButton.get());
 
-    expect(getTokensList()).toHaveLength(2);
+    expect(getTokensList()).toHaveLength(2); // header + "local-scanner" token
+    expect(screen.queryByText('users.no_tokens')).not.toBeInTheDocument();
 
+    expect(ui.sureButton.query()).not.toBeInTheDocument();
+    await user.click(ui.revokeButton('local-scanner').get());
+    expect(await ui.sureButton.find()).toBeInTheDocument();
+    await user.click(ui.sureButton.get());
+
+    expect(getTokensList()).toHaveLength(2); // header + "No tokens"
+    expect(await screen.findByText('users.no_tokens')).toBeInTheDocument();
+
+    await selectEvent.select(ui.expiresInSelector.get(), 'users.tokens.expiration.0');
     await user.click(ui.generateButton.get());
-    expect(getTokensList()).toHaveLength(3);
+    expect(getTokensList()).toHaveLength(2); // header + "test" token
+    expect(screen.queryByText('users.no_tokens')).not.toBeInTheDocument();
     expect(await screen.findByText('users.tokens.new_token_created.test')).toBeInTheDocument();
 
-    await user.click(ui.doneButton.get());
+    await user.click(ui.closeButton.get());
     expect(ui.dialogTokens.query()).not.toBeInTheDocument();
   });
 
@@ -700,16 +713,17 @@ it('accessibility', async () => {
   await user.click(ui.cancelButton.get());
 
   // user tokens dialog should be accessible
-  user.click(
+  await user.click(
     await ui.aliceRow
       .byRole('button', {
         name: 'users.update_tokens_for_x.Alice Merveille',
       })
       .find(),
   );
+
   expect(await ui.dialogTokens.find()).toBeInTheDocument();
   await expect(await ui.dialogTokens.find()).toHaveNoA11yViolations();
-  await user.click(ui.doneButton.get());
+  await user.click(ui.closeButton.get());
 
   // user password dialog should be accessible
   await user.click(await ui.aliceUpdateButton.find());
index 64f279e5af5375c541d2959caa9f65b5a2b4000f..c270565839b27bc5b9b27528b9fa8af577c3a4e4 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,
+  ContentCell,
+  GreySeparator,
+  InputField,
+  InputSelect,
+  Table,
+  TableRow,
+} from 'design-system';
 import { isEmpty } from 'lodash';
 import * as React from 'react';
 import { getScannableProjects } from '../../../api/components';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import Select, { LabelValueSelectOption } from '../../../components/controls/Select';
-import { SubmitButton } from '../../../components/controls/buttons';
+import { LabelValueSelectOption } from '../../../components/controls/Select';
 import Spinner from '../../../components/ui/Spinner';
 import { translate } from '../../../helpers/l10n';
 import {
@@ -39,13 +48,15 @@ import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem';
 import TokensFormNewToken from './TokensFormNewToken';
 
 interface Props {
+  currentUser: CurrentUser;
   deleteConfirmation: TokenDeleteConfirmation;
-  login: string;
   displayTokenTypeInput: boolean;
-  currentUser: CurrentUser;
+  login: string;
 }
 
-export function TokensForm(props: Props) {
+const COLUMN_WIDTHS = ['auto', 'auto', 'auto', 'auto', 'auto', 'auto', '5%'];
+
+export function TokensForm(props: Readonly<Props>) {
   const { currentUser, deleteConfirmation, displayTokenTypeInput, login } = props;
   const { data: tokens, isLoading: loading } = useUserTokensQuery(login);
   const [newToken, setNewToken] = React.useState<{ name: string; token: string }>();
@@ -53,9 +64,11 @@ export function TokensForm(props: Props) {
   const [newTokenType, setNewTokenType] = React.useState<TokenType>();
   const [projects, setProjects] = React.useState<LabelValueSelectOption[]>([]);
   const [selectedProject, setSelectedProject] = React.useState<LabelValueSelectOption>();
+
   const [newTokenExpiration, setNewTokenExpiration] = React.useState<TokenExpiration>(
     TokenExpiration.OneMonth,
   );
+
   const [tokenExpirationOptions, setTokenExpirationOptions] =
     React.useState<{ value: TokenExpiration; label: string }[]>(EXPIRATION_OPTIONS);
 
@@ -70,6 +83,7 @@ export function TokensForm(props: Props) {
         value: TokenType.Global,
       });
     }
+
     if (!isEmpty(projects)) {
       value.unshift({
         label: translate('users.tokens', TokenType.Project),
@@ -100,7 +114,9 @@ export function TokensForm(props: Props) {
             label: project.name,
             value: project.key,
           }));
+
           setProjects(projects);
+
           setSelectedProject(projects.length === 1 ? projects[0] : undefined);
         })
         .catch(() => {});
@@ -111,8 +127,8 @@ export function TokensForm(props: Props) {
     event.preventDefault();
 
     generate({
-      name: newTokenName,
       login,
+      name: newTokenName,
       type: newTokenType,
       ...(newTokenType === TokenType.Project &&
         selectedProject !== undefined && {
@@ -141,6 +157,7 @@ export function TokensForm(props: Props) {
     if (generating || newTokenName.length <= 0) {
       return true;
     }
+
     if (newTokenType === TokenType.Project) {
       return !selectedProject?.value;
     }
@@ -152,16 +169,16 @@ export function TokensForm(props: Props) {
     setNewTokenName(evt.currentTarget.value);
   };
 
-  const handleNewTokenTypeChange = ({ value }: { value: TokenType }) => {
-    setNewTokenType(value);
+  const handleNewTokenTypeChange = (newTokenType: { value: unknown } | null) => {
+    setNewTokenType(newTokenType?.value as TokenType);
   };
 
   const handleProjectChange = (selectedProject: LabelValueSelectOption) => {
     setSelectedProject(selectedProject);
   };
 
-  const handleNewTokenExpirationChange = ({ value }: { value: TokenExpiration }) => {
-    setNewTokenExpiration(value);
+  const handleNewTokenExpirationChange = (newTokenExpiration: { value: unknown } | null) => {
+    setNewTokenExpiration(newTokenExpiration?.value as TokenExpiration);
   };
 
   const customSpinner = (
@@ -172,17 +189,30 @@ export function TokensForm(props: Props) {
     </tr>
   );
 
+  const tableHeader = (
+    <TableRow>
+      <ContentCell>{translate('name')}</ContentCell>
+      <ContentCell>{translate('my_account.token_type')}</ContentCell>
+      <ContentCell>{translate('my_account.project_name')}</ContentCell>
+      <ContentCell>{translate('my_account.tokens_last_usage')}</ContentCell>
+      <ContentCell>{translate('created')}</ContentCell>
+      <ContentCell>{translate('my_account.tokens.expiration')}</ContentCell>
+    </TableRow>
+  );
+
   return (
     <>
-      <h3 className="spacer-bottom">{translate('users.tokens.generate')}</h3>
-      <form autoComplete="off" className="display-flex-center" onSubmit={handleGenerateToken}>
-        <div className="display-flex-column input-large spacer-right ">
-          <label htmlFor="token-name" className="text-bold">
+      <h3 className="sw-mb-2">{translate('users.tokens.generate')}</h3>
+
+      <form autoComplete="off" className="sw-flex sw-items-center" onSubmit={handleGenerateToken}>
+        <div className="sw-flex sw-flex-col sw-mr-2">
+          <label htmlFor="token-name" className="sw-font-bold">
             {translate('users.tokens.name')}
           </label>
-          <input
+
+          <InputField
+            className="sw-mt-2 sw-w-auto it__token-name"
             id="token-name"
-            className="spacer-top it__token-name"
             maxLength={100}
             onChange={handleNewTokenChange}
             placeholder={translate('users.tokens.enter_name')}
@@ -191,15 +221,17 @@ export function TokensForm(props: Props) {
             value={newTokenName}
           />
         </div>
+
         {displayTokenTypeInput && (
           <>
-            <div className="display-flex-column input-large spacer-right">
-              <label htmlFor="token-select-type" className="text-bold">
+            <div className="sw-flex sw-flex-col sw-mr-2">
+              <label htmlFor="token-select-type" className="sw-font-bold">
                 {translate('users.tokens.type')}
               </label>
-              <Select
+
+              <InputSelect
+                className="sw-mt-2 it__token-type"
                 inputId="token-select-type"
-                className="spacer-top it__token-type"
                 isSearchable={false}
                 onChange={handleNewTokenTypeChange}
                 options={tokenTypeOptions}
@@ -211,14 +243,16 @@ export function TokensForm(props: Props) {
                 }
               />
             </div>
+
             {newTokenType === TokenType.Project && (
-              <div className="input-large spacer-right display-flex-column">
-                <label htmlFor="token-select-project" className="text-bold">
+              <div className="sw-flex sw-flex-col sw-mr-2">
+                <label htmlFor="token-select-project" className="sw-font-bold">
                   {translate('users.tokens.project')}
                 </label>
-                <Select
+
+                <InputSelect
+                  className="sw-mt-2 it__project"
                   inputId="token-select-project"
-                  className="spacer-top it__project"
                   onChange={handleProjectChange}
                   options={projects}
                   placeholder={translate('users.tokens.select_project')}
@@ -228,62 +262,62 @@ export function TokensForm(props: Props) {
             )}
           </>
         )}
-        <div className="display-flex-column input-medium spacer-right ">
-          <label htmlFor="token-select-expiration" className="text-bold">
+
+        <div className="sw-flex sw-flex-col sw-mr-2">
+          <label htmlFor="token-select-expiration" className="sw-font-bold">
             {translate('users.tokens.expires_in')}
           </label>
-          <Select
+
+          <InputSelect
+            className="sw-mt-2"
             inputId="token-select-expiration"
-            className="spacer-top"
             isSearchable={false}
             onChange={handleNewTokenExpirationChange}
             options={tokenExpirationOptions}
             value={tokenExpirationOptions.find((option) => option.value === newTokenExpiration)}
           />
         </div>
-        <SubmitButton
+
+        <ButtonPrimary
           className="it__generate-token"
-          style={{ marginTop: 'auto' }}
           disabled={isSubmitButtonDisabled()}
+          style={{ marginTop: 'auto' }}
+          type="submit"
         >
           {translate('users.generate')}
-        </SubmitButton>
+        </ButtonPrimary>
       </form>
+
       {newToken && <TokensFormNewToken token={newToken} />}
 
-      <table className="data zebra big-spacer-top fixed">
-        <thead>
-          <tr>
-            <th>{translate('name')}</th>
-            <th>{translate('my_account.token_type')}</th>
-            <th>{translate('my_account.project_name')}</th>
-            <th>{translate('my_account.tokens_last_usage')}</th>
-            <th className="text-right">{translate('created')}</th>
-            <th className="text-right">{translate('my_account.tokens.expiration')}</th>
-            <th className="text-right">{translate('actions')}</th>
-          </tr>
-        </thead>
-        <tbody>
-          <Spinner customSpinner={customSpinner} loading={!!loading}>
-            {tokens && tokens.length <= 0 ? (
-              <tr>
-                <td className="note" colSpan={7}>
-                  {translate('users.no_tokens')}
-                </td>
-              </tr>
-            ) : (
-              tokens?.map((token) => (
-                <TokensFormItem
-                  deleteConfirmation={deleteConfirmation}
-                  key={token.name}
-                  login={login}
-                  token={token}
-                />
-              ))
-            )}
-          </Spinner>
-        </tbody>
-      </table>
+      <GreySeparator className="sw-mb-4 sw-mt-6" />
+
+      <Table
+        className="sw-min-h-40 sw-w-full"
+        columnCount={COLUMN_WIDTHS.length}
+        columnWidths={COLUMN_WIDTHS}
+        header={tableHeader}
+        noHeaderTopBorder
+      >
+        <Spinner customSpinner={customSpinner} loading={!!loading}>
+          {tokens && tokens.length <= 0 ? (
+            <TableRow>
+              <ContentCell className="sw-body-lg" colSpan={7}>
+                {translate('users.no_tokens')}
+              </ContentCell>
+            </TableRow>
+          ) : (
+            tokens?.map((token) => (
+              <TokensFormItem
+                deleteConfirmation={deleteConfirmation}
+                key={token.name}
+                login={login}
+                token={token}
+              />
+            ))
+          )}
+        </Spinner>
+      </Table>
     </>
   );
 }
index d8037816bc398ac43b7cd3201e6ab2b597e12c2f..a2b8b75ad90ba63707d6d2070309b6379a89f81f 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 {
+  ContentCell,
+  DangerButtonSecondary,
+  FlagWarningIcon,
+  TableRow,
+  themeColor,
+} from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { Button } from '../../../components/controls/buttons';
-import WarningIcon from '../../../components/icons/WarningIcon';
 import DateFormatter from '../../../components/intl/DateFormatter';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import Spinner from '../../../components/ui/Spinner';
@@ -38,7 +45,7 @@ interface Props {
   token: UserToken;
 }
 
-export default function TokensFormItem(props: Props) {
+export default function TokensFormItem(props: Readonly<Props>) {
   const { token, deleteConfirmation, login } = props;
   const [showConfirmation, setShowConfirmation] = React.useState(false);
   const { mutateAsync, isLoading } = useRevokeTokenMutation();
@@ -55,45 +62,70 @@ export default function TokensFormItem(props: Props) {
     }
   };
 
+  const className = classNames('sw-mr-2', {
+    'sw-text-gray-400': token.isExpired,
+  });
+
   return (
-    <tr className={classNames({ 'text-muted-2': token.isExpired })}>
-      <td title={token.name} className="hide-overflow nowrap">
-        {token.name}
-        {token.isExpired && (
-          <div className="spacer-top text-warning">
-            <WarningIcon className="little-spacer-right" />
-            {translate('my_account.tokens.expired')}
-          </div>
-        )}
-      </td>
-      <td title={translate('users.tokens', token.type)} className="hide-overflow thin">
+    <TableRow>
+      <ContentCell
+        className={classNames('sw-flex-col sw-items-center sw-w-64', className)}
+        title={token.name}
+      >
+        <div className="sw-w-full sw-truncate">
+          {token.name}
+
+          {token.isExpired && (
+            <StyledSpan tokenIsExpired>
+              <div className="sw-mt-1">
+                <FlagWarningIcon className="sw-mr-1" />
+
+                {translate('my_account.tokens.expired')}
+              </div>
+            </StyledSpan>
+          )}
+        </div>
+      </ContentCell>
+
+      <ContentCell className={className} title={translate('users.tokens', token.type)}>
         {translate('users.tokens', token.type, 'short')}
-      </td>
-      <td title={token.project?.name} className="hide-overflow">
-        {token.project?.name}
-      </td>
-      <td className="thin nowrap">
+      </ContentCell>
+
+      <ContentCell className={classNames('sw-w-32', className)} title={token.project?.name}>
+        <div className="sw-w-full sw-truncate">{token.project?.name}</div>
+      </ContentCell>
+
+      <ContentCell className={className}>
         <DateFromNow date={token.lastConnectionDate} hourPrecision />
-      </td>
-      <td className="thin nowrap text-right">
+      </ContentCell>
+
+      <ContentCell className={className}>
         <DateFormatter date={token.createdAt} long />
-      </td>
-      <td className={classNames('thin nowrap text-right', { 'text-warning': token.isExpired })}>
-        {token.expirationDate ? <DateFormatter date={token.expirationDate} long /> : '–'}
-      </td>
-      <td className="thin nowrap text-right">
+      </ContentCell>
+
+      <ContentCell className={className}>
+        {token.expirationDate ? (
+          <StyledSpan tokenIsExpired={token.isExpired}>
+            <DateFormatter date={token.expirationDate} long />
+          </StyledSpan>
+        ) : (
+          '–'
+        )}
+      </ContentCell>
+
+      <ContentCell>
         {token.isExpired && (
-          <Button
-            className="button-red input-small"
+          <DangerButtonSecondary
             disabled={isLoading}
             onClick={handleRevoke}
             aria-label={translateWithParameters('users.tokens.remove_label', token.name)}
           >
-            <Spinner className="little-spacer-right" loading={isLoading}>
+            <Spinner className="sw-mr-1" loading={isLoading}>
               {translate('remove')}
             </Spinner>
-          </Button>
+          </DangerButtonSecondary>
         )}
+
         {!token.isExpired && deleteConfirmation === 'modal' && (
           <ConfirmButton
             confirmButtonText={translate('yes')}
@@ -109,33 +141,40 @@ export default function TokensFormItem(props: Props) {
             onConfirm={handleRevoke}
           >
             {({ onClick }) => (
-              <Button
-                className="button-red input-small"
+              <DangerButtonSecondary
                 disabled={isLoading}
                 onClick={onClick}
                 aria-label={translateWithParameters('users.tokens.revoke_label', token.name)}
               >
                 {translate('users.tokens.revoke')}
-              </Button>
+              </DangerButtonSecondary>
             )}
           </ConfirmButton>
         )}
+
         {!token.isExpired && deleteConfirmation === 'inline' && (
-          <Button
-            className="button-red input-small"
-            disabled={isLoading}
+          <DangerButtonSecondary
             aria-label={
               showConfirmation
                 ? translate('users.tokens.sure')
                 : translateWithParameters('users.tokens.revoke_label', token.name)
             }
+            disabled={isLoading}
             onClick={handleClick}
           >
-            <Spinner className="little-spacer-right" loading={isLoading} />
+            <Spinner className="sw-mr-1" loading={isLoading} />
+
             {showConfirmation ? translate('users.tokens.sure') : translate('users.tokens.revoke')}
-          </Button>
+          </DangerButtonSecondary>
         )}
-      </td>
-    </tr>
+      </ContentCell>
+    </TableRow>
   );
 }
+
+const StyledSpan = styled.span<{
+  tokenIsExpired?: boolean;
+}>`
+  color: ${({ tokenIsExpired }) =>
+    tokenIsExpired ? themeColor('iconWarning') : themeColor('pageContent')};
+`;
index adfcd152228f19647b415465842251111ff1ae19..0b0bdcb570215ccea83907a03091c6ceb5a88f3d 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 { Modal } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import Modal from '../../../components/controls/Modal';
-import { ResetButtonLink } from '../../../components/controls/buttons';
 import { translate } from '../../../helpers/l10n';
 import { RestUserDetailed } from '../../../types/users';
 import TokensForm from './TokensForm';
@@ -30,26 +30,23 @@ interface Props {
   onClose: () => void;
 }
 
-export default function TokensFormModal(props: Props) {
+export default function TokensFormModal(props: Readonly<Props>) {
   const { user } = props;
 
   return (
-    <Modal size="large" contentLabel={translate('users.tokens')} onRequestClose={props.onClose}>
-      <header className="modal-head">
-        <h2>
-          <FormattedMessage
-            defaultMessage={translate('users.user_X_tokens')}
-            id="users.user_X_tokens"
-            values={{ user: <em>{user.name}</em> }}
-          />
-        </h2>
-      </header>
-      <div className="modal-body modal-container">
-        <TokensForm deleteConfirmation="inline" login={user.login} displayTokenTypeInput={false} />
-      </div>
-      <footer className="modal-foot">
-        <ResetButtonLink onClick={props.onClose}>{translate('done')}</ResetButtonLink>
-      </footer>
-    </Modal>
+    <Modal
+      body={
+        <TokensForm deleteConfirmation="inline" displayTokenTypeInput={false} login={user.login} />
+      }
+      headerTitle={
+        <FormattedMessage
+          defaultMessage={translate('users.user_X_tokens')}
+          id="users.user_X_tokens"
+          values={{ user: <em>{user.name}</em> }}
+        />
+      }
+      isLarge
+      onClose={props.onClose}
+    />
   );
 }
index e078d8d00e0283e4a73e0753640bb746041d0d46..11b9ec3d7416001a77f7ee01d831adf22bd3d284 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 { ClipboardIconButton, CodeSnippet, FlagMessage } from 'design-system';
 import * as React from 'react';
-import { ClipboardButton } from '../../../components/controls/clipboard';
-import { Alert } from '../../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 
 interface Props {
   token: { name: string; token: string };
 }
 
-export default function TokensFormNewToken({ token }: Props) {
+export default function TokensFormNewToken({ token }: Readonly<Props>) {
   return (
-    <div className="panel panel-white big-spacer-top">
-      <Alert variant="warning">
+    <div className="sw-mt-4">
+      <FlagMessage variant="success">
         {translateWithParameters('users.tokens.new_token_created', token.name)}
-      </Alert>
-      <ClipboardButton copyValue={token.token} />
-      <code aria-label={translate('users.new_token')} className="big-spacer-left text-success">
-        {token.token}
-      </code>
+      </FlagMessage>
+
+      <div aria-label={translate('users.new_token')} className="sw-flex sw-items-center sw-mt-3">
+        <CodeSnippet className="sw-p-1" isOneLine noCopy snippet={token.token} />
+
+        <ClipboardIconButton className="sw-ml-4" copyValue={token.token} />
+      </div>
     </div>
   );
 }