]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19923 Onboarding Tutorial - Token modal
authorKevin Silva <kevin.silva@sonarsource.com>
Mon, 17 Jul 2023 14:02:38 +0000 (16:02 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 21 Jul 2023 20:03:16 +0000 (20:03 +0000)
server/sonar-web/design-system/src/components/modal/Modal.tsx
server/sonar-web/design-system/src/components/modal/ModalBody.tsx
server/sonar-web/design-system/src/components/modal/__tests__/__snapshots__/ModalBody-test.tsx.snap
server/sonar-web/src/main/js/app/theme.js
server/sonar-web/src/main/js/components/tutorials/components/EditTokenModal.tsx
server/sonar-web/tailwind.base.config.js

index db54a6525558f1ab135c49553c297debfbaf56eb..c2e1ce8ed6a0aab4cb17e2e0c9a0cecbaac48f2f 100644 (file)
@@ -38,6 +38,7 @@ interface CommonProps {
   closeOnOverlayClick?: boolean;
   isLarge?: boolean;
   isOpen?: boolean;
+  isOverflowVisible?: boolean;
   isScrollable?: boolean;
   onClose: VoidFunction;
 }
@@ -77,6 +78,7 @@ export function Modal({
   closeOnOverlayClick = true,
   isLarge,
   isOpen = true,
+  isOverflowVisible = false,
   isScrollable = true,
   onClose,
   ...props
@@ -102,7 +104,9 @@ export function Modal({
           <>
             <ModalHeader description={props.headerDescription} title={props.headerTitle} />
 
-            <ModalBody isScrollable={isScrollable}>{props.body}</ModalBody>
+            <ModalBody isOverflowVisible={isOverflowVisible} isScrollable={isScrollable}>
+              {props.body}
+            </ModalBody>
 
             <ModalFooter
               loading={props.loading}
index 6b7ead1f8db767d9e776e5273c3f87542e17bdf9..213bd2d4c23672cf9213667d8f08660feeb9c06e 100644 (file)
@@ -26,11 +26,18 @@ import { themeColor } from '../../helpers/theme';
 
 interface Props {
   children: ReactNode;
+  isOverflowVisible?: boolean;
   isScrollable?: boolean;
 }
 
-export function ModalBody({ children, isScrollable = true }: Props) {
-  return <StyledMain className={classNames({ scrollable: isScrollable })}>{children}</StyledMain>;
+export function ModalBody({ children, isScrollable = true, isOverflowVisible = false }: Props) {
+  return (
+    <StyledMain
+      className={classNames({ scrollable: isScrollable, overflowVisible: isOverflowVisible })}
+    >
+      {children}
+    </StyledMain>
+  );
 }
 
 const StyledMain = styled.div`
@@ -45,4 +52,8 @@ const StyledMain = styled.div`
   &.scrollable {
     overflow-y: auto;
   }
+
+  &.overflowVisible {
+    overflow: visible;
+  }
 `;
index 4d41d0c95e2d7b2f622a48eaa23c0782bd42229a..32d0bdc498bfb98d0a467ce4a826f54d0ac323ff 100644 (file)
@@ -20,6 +20,10 @@ exports[`renders with children 1`] = `
   overflow-y: auto;
 }
 
+.emotion-0.overflowVisible {
+  overflow: visible;
+}
+
 <div>
   <div
     class="scrollable emotion-0 emotion-1"
index e0be09f66de5d7f84b8b5b046f473201e3a4a324..08f4d1f3a4d594040425b4a30e27f5109a346182 100644 (file)
@@ -258,7 +258,7 @@ module.exports = {
 
     contextbarZIndex: '420',
 
-    tooltipZIndex: '8000',
+    tooltipZIndex: '9001',
 
     dropdownMenuZIndex: '7500',
 
index a03b0b7c94fcfdc93654889a26246a9aa9f0d22a..e1d62e9745de8199a2e159ae30c13a0664f509ac 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 {
+  ButtonSecondary,
+  ClipboardIconButton,
+  DeferredSpinner,
+  DestructiveIcon,
+  FlagMessage,
+  InputField,
+  InputSelect,
+  LabelValueSelectOption,
+  Link,
+  Modal,
+  TrashIcon,
+} from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { generateToken, getTokens, revokeToken } from '../../../api/user-tokens';
-import { Button, DeleteButton } from '../../../components/controls/buttons';
-import { ClipboardIconButton } from '../../../components/controls/clipboard';
-import SimpleModal from '../../../components/controls/SimpleModal';
-import { Alert } from '../../../components/ui/Alert';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import {
-  computeTokenExpirationDate,
   EXPIRATION_OPTIONS,
+  computeTokenExpirationDate,
   getAvailableExpirationOptions,
 } from '../../../helpers/tokens';
 import { hasGlobalPermission } from '../../../helpers/users';
@@ -36,9 +45,8 @@ import { Permissions } from '../../../types/permissions';
 import { TokenExpiration, TokenType } from '../../../types/token';
 import { Component } from '../../../types/types';
 import { LoggedInUser } from '../../../types/users';
-import Link from '../../common/Link';
-import Select from '../../controls/Select';
 import { getUniqueTokenName } from '../utils';
+import { InlineSnippet } from './InlineSnippet';
 import ProjectTokenScopeInfo from './ProjectTokenScopeInfo';
 
 interface State {
@@ -130,12 +138,10 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
   };
 
   handleTokenNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.setState({
-      tokenName: event.target.value,
-    });
+    this.setState({ tokenName: event.currentTarget.value });
   };
 
-  handleTokenExpirationChange = ({ value }: { value: TokenExpiration }) => {
+  handleTokenExpirationChange = (value: TokenExpiration) => {
     this.setState({ tokenExpiration: value });
   };
 
@@ -154,118 +160,112 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
     }
   };
 
-  render() {
+  renderForm(type: TokenType) {
     const { loading, token, tokenName, tokenExpiration, tokenExpirationOptions } = this.state;
-
-    const type = this.getTokenType();
-    const header = translate('onboarding.token.generate', type);
     const intro = translate('onboarding.token.text', type);
 
     return (
-      <SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.props.onClose}>
-        {({ onCloseClick }) => (
+      <div className="sw-body-sm">
+        <FormattedMessage
+          defaultMessage={intro}
+          id={intro}
+          values={{
+            link: (
+              <Link target="_blank" to="/account/security">
+                {translate('onboarding.token.text.user_account')}
+              </Link>
+            ),
+          }}
+        />
+
+        {token ? (
           <>
-            <div className="modal-head">
-              <h2>{header}</h2>
+            <span>
+              {tokenName}
+              {': '}
+            </span>
+            <div>
+              <InlineSnippet snippet={token} />
+              <ClipboardIconButton
+                copyLabel={translate('copy_to_clipboard')}
+                className="sw-ml-2"
+                copyValue={token}
+              />
+              <DestructiveIcon
+                className="sw-ml-1"
+                Icon={TrashIcon}
+                aria-label={translate('onboarding.token.delete')}
+                onClick={this.handleTokenRevoke}
+              />
             </div>
-
-            <div className="modal-body">
-              <p className="spacer-bottom">
-                <FormattedMessage
-                  defaultMessage={intro}
-                  id={intro}
-                  values={{
-                    link: (
-                      <Link target="_blank" to="/account/security">
-                        {translate('onboarding.token.text.user_account')}
-                      </Link>
-                    ),
-                  }}
-                />
-              </p>
-
-              {token ? (
-                <>
-                  <span className="text-middle">
-                    {tokenName}
-                    {': '}
-                  </span>
-                  <div className="display-float-center">
-                    <code className="rule spacer-right">{token}</code>
-
-                    <ClipboardIconButton copyValue={token} />
-
-                    <DeleteButton
-                      aria-label={translate('onboarding.token.delete')}
-                      onClick={this.handleTokenRevoke}
+            <FlagMessage className="sw-mt-2" variant="warning">
+              {translateWithParameters('users.tokens.new_token_created', token)}
+            </FlagMessage>
+          </>
+        ) : (
+          <>
+            <div className="sw-flex sw-pt-4">
+              <DeferredSpinner loading={loading}>
+                <div className="sw-flex-col sw-mr-2">
+                  <label className="sw-block" htmlFor="token-name">
+                    {translate('onboarding.token.name.label')}
+                  </label>
+                  <InputField
+                    aria-label={translate('onboarding.token.name.label')}
+                    onChange={this.handleTokenNameChange}
+                    id="token-name"
+                    placeholder={translate('onboarding.token.name.placeholder')}
+                    value={tokenName}
+                    type="text"
+                  />
+                </div>
+                <div className="sw-flex-col">
+                  <label htmlFor="token-expiration">{translate('users.tokens.expires_in')}</label>
+                  <div className="sw-flex">
+                    <InputSelect
+                      size="medium"
+                      id="token-expiration"
+                      isSearchable={false}
+                      onChange={(data: LabelValueSelectOption<TokenExpiration>) =>
+                        this.handleTokenExpirationChange(data.value)
+                      }
+                      options={tokenExpirationOptions}
+                      value={tokenExpirationOptions.find(
+                        (option) => option.value === tokenExpiration
+                      )}
                     />
+                    <ButtonSecondary
+                      className="sw-ml-2"
+                      disabled={!tokenName}
+                      onClick={this.getNewToken}
+                    >
+                      {translate('onboarding.token.generate')}
+                    </ButtonSecondary>
                   </div>
-
-                  <Alert className="big-spacer-top" variant="warning">
-                    {translateWithParameters('users.tokens.new_token_created', token)}
-                  </Alert>
-                </>
-              ) : (
-                <>
-                  <div className="big-spacer-top display-flex-center">
-                    {loading ? (
-                      <DeferredSpinner />
-                    ) : (
-                      <>
-                        <div className="display-flex-column">
-                          <label className="text-bold little-spacer-bottom" htmlFor="token-name">
-                            {translate('onboarding.token.name.label')}
-                          </label>
-                          <input
-                            className="input-large spacer-right text-middle"
-                            onChange={this.handleTokenNameChange}
-                            required
-                            id="token-name"
-                            type="text"
-                            placeholder={translate('onboarding.token.name.placeholder')}
-                            value={tokenName}
-                          />
-                        </div>
-                        <div className="display-flex-column">
-                          <label
-                            className="text-bold little-spacer-bottom"
-                            htmlFor="token-expiration"
-                          >
-                            {translate('users.tokens.expires_in')}
-                          </label>
-                          <div className="display-flex-center">
-                            <Select
-                              id="token-expiration"
-                              className="abs-width-100 spacer-right"
-                              isSearchable={false}
-                              onChange={this.handleTokenExpirationChange}
-                              options={tokenExpirationOptions}
-                              value={tokenExpirationOptions.find(
-                                (option) => option.value === tokenExpiration
-                              )}
-                            />
-                            <Button
-                              className="text-middle"
-                              disabled={!tokenName}
-                              onClick={this.getNewToken}
-                            >
-                              {translate('onboarding.token.generate')}
-                            </Button>
-                          </div>
-                        </div>
-                      </>
-                    )}
-                  </div>
-                  {type === TokenType.Project && <ProjectTokenScopeInfo />}
-                </>
-              )}
-            </div>
-            <div className="modal-foot">
-              <Button onClick={onCloseClick}>{translate('continue')}</Button>
+                </div>
+              </DeferredSpinner>
             </div>
+            {type === TokenType.Project && <ProjectTokenScopeInfo />}
           </>
         )}
-      </SimpleModal>
+      </div>
+    );
+  }
+
+  render() {
+    const { loading } = this.state;
+    const type = this.getTokenType();
+    const header = translate('onboarding.token.generate', type);
+
+    return (
+      <Modal
+        onClose={this.props.onClose}
+        headerTitle={header}
+        isOverflowVisible
+        loading={loading}
+        body={this.renderForm(type)}
+        secondaryButtonLabel={translate('continue')}
+      />
     );
   }
 }
index b7d497c8c88a55599985d087a332c44917d638e5..5dd51df591a52ac1e164660db36ca0bac4b99dd9 100644 (file)
@@ -116,9 +116,9 @@ module.exports = {
       'core-concepts': '422',
       'global-popup': '5000',
       'dropdown-menu': '7500',
-      tooltip: '8000',
       'modal-overlay': 8500,
       modal: '9000',
+      tooltip: '9001',
     },
     extend: {
       width: {