]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16565 Add token expiration field to Onboarding forms
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Fri, 8 Jul 2022 09:18:45 +0000 (11:18 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 8 Jul 2022 20:02:47 +0000 (20:02 +0000)
server/sonar-web/src/main/js/app/styles/init/misc.css
server/sonar-web/src/main/js/components/tutorials/components/EditTokenModal.tsx
server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx
server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.tsx
server/sonar-web/src/main/js/components/tutorials/manual/__tests__/TokenStep-test.tsx
server/sonar-web/src/main/js/components/tutorials/manual/__tests__/__snapshots__/TokenStep-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index b5269470f8ad00063d52bba60aa0ce807fb5bd24..2d476444af1bcd8ec452b48ea982573883ed31c7 100644 (file)
@@ -358,9 +358,14 @@ th.huge-spacer-right {
   width: 10% !important;
 }
 
+.abs-width-100 {
+  width: 100px !important;
+}
+
 .abs-width-150 {
   width: 150px !important;
 }
+
 .abs-width-240 {
   width: 240px !important;
 }
index f60578becaa1e473f261584a6eb89241e3ac621c..6f09c905b844ebf91f49bfc2a1be4b6354030932 100644 (file)
@@ -27,15 +27,23 @@ 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 { TokenType } from '../../../types/token';
+import {
+  computeTokenExpirationDate,
+  EXPIRATION_OPTIONS,
+  getAvailableExpirationOptions
+} from '../../../helpers/tokens';
+import { TokenExpiration, TokenType } from '../../../types/token';
 import { Component } from '../../../types/types';
 import { LoggedInUser } from '../../../types/users';
+import Select from '../../controls/Select';
 import { getUniqueTokenName } from '../utils';
 
 interface State {
   loading: boolean;
   token?: string;
   tokenName: string;
+  tokenExpiration: TokenExpiration;
+  tokenExpirationOptions: { value: TokenExpiration; label: string }[];
 }
 
 interface Props {
@@ -48,12 +56,15 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = {
     loading: true,
-    tokenName: ''
+    tokenName: '',
+    tokenExpiration: TokenExpiration.OneMonth,
+    tokenExpirationOptions: EXPIRATION_OPTIONS
   };
 
   componentDidMount() {
     this.mounted = true;
     this.getTokensAndName();
+    this.getTokenExpirationOptions();
   }
 
   componentWillUnmount() {
@@ -73,16 +84,26 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
     }
   };
 
+  getTokenExpirationOptions = async () => {
+    const tokenExpirationOptions = await getAvailableExpirationOptions();
+    if (tokenExpirationOptions && this.mounted) {
+      this.setState({ tokenExpirationOptions });
+    }
+  };
+
   getNewToken = async () => {
     const {
       component: { key }
     } = this.props;
-    const { tokenName } = this.state;
+    const { tokenName, tokenExpiration } = this.state;
 
     const { token } = await generateToken({
       name: tokenName,
       type: TokenType.Project,
-      projectKey: key
+      projectKey: key,
+      ...(tokenExpiration !== TokenExpiration.NoExpiration && {
+        expirationDate: computeTokenExpirationDate(tokenExpiration)
+      })
     });
 
     if (this.mounted) {
@@ -93,12 +114,16 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
     }
   };
 
-  handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+  handleTokenNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     this.setState({
       tokenName: event.target.value
     });
   };
 
+  handleTokenExpirationChange = ({ value }: { value: TokenExpiration }) => {
+    this.setState({ tokenExpiration: value });
+  };
+
   handleTokenRevoke = async () => {
     const { tokenName } = this.state;
 
@@ -115,7 +140,7 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { loading, token, tokenName } = this.state;
+    const { loading, token, tokenName, tokenExpiration, tokenExpirationOptions } = this.state;
 
     const header = translate('onboarding.token.generate_project_token');
 
@@ -161,25 +186,49 @@ export default class EditTokenModal extends React.PureComponent<Props, State> {
                   </Alert>
                 </>
               ) : (
-                <div className="big-spacer-top">
+                <div className="big-spacer-top display-flex-center">
                   {loading ? (
                     <DeferredSpinner />
                   ) : (
                     <>
-                      <input
-                        className="input-super-large spacer-right text-middle"
-                        onChange={this.handleChange}
-                        placeholder={translate('onboarding.token.generate_token.placeholder')}
-                        required={true}
-                        type="text"
-                        value={tokenName}
-                      />
-                      <Button
-                        className="text-middle"
-                        disabled={!tokenName}
-                        onClick={this.getNewToken}>
-                        {translate('onboarding.token.generate')}
-                      </Button>
+                      <div className="display-flex-column">
+                        <label className="text-bold little-spacer-bottom" htmlFor="token-name">
+                          {translate('onboarding.token.generate_token.placeholder')}
+                        </label>
+                        <input
+                          className="input-large spacer-right text-middle"
+                          onChange={this.handleTokenNameChange}
+                          required={true}
+                          id="token-name"
+                          type="text"
+                          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>
index 6e7c2f8354bd0efb44d7f3ba11a52045549a1f9a..8ad792ee98104c68909fd339dcd61836fdf5edbd 100644 (file)
@@ -48,6 +48,18 @@ jest.mock('../../utils', () => ({
   getUniqueTokenName: jest.fn().mockReturnValue('lightsaber-9000')
 }));
 
+jest.mock('../../../../api/settings', () => {
+  return {
+    ...jest.requireActual('../../../../api/settings'),
+    getAllValues: jest.fn().mockResolvedValue([
+      {
+        key: 'sonar.auth.token.max.allowed.lifetime',
+        value: 'No expiration'
+      }
+    ])
+  };
+});
+
 beforeEach(() => {
   jest.clearAllMocks();
 });
@@ -97,7 +109,7 @@ it('should handle change on user input', () => {
   const wrapper = shallowRender();
   const instance = wrapper.instance();
 
-  instance.handleChange(mockEvent({ target: { value: 'my-token' } }));
+  instance.handleTokenNameChange(mockEvent({ target: { value: 'my-token' } }));
   expect(wrapper.state('tokenName')).toBe('my-token');
 });
 
index e0f47ce24252aae17d11b53f75f403525902c0f3..80052f5a3f227015501d4178420ae83a0729e96f 100644 (file)
@@ -25,9 +25,15 @@ import { Button, DeleteButton, SubmitButton } from '../../../components/controls
 import Radio from '../../../components/controls/Radio';
 import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
 import { translate } from '../../../helpers/l10n';
-import { TokenType, UserToken } from '../../../types/token';
+import {
+  computeTokenExpirationDate,
+  EXPIRATION_OPTIONS,
+  getAvailableExpirationOptions
+} from '../../../helpers/tokens';
+import { TokenExpiration, TokenType, UserToken } from '../../../types/token';
 import { LoggedInUser } from '../../../types/users';
 import DocumentationTooltip from '../../common/DocumentationTooltip';
+import Select from '../../controls/Select';
 import AlertErrorIcon from '../../icons/AlertErrorIcon';
 import Step from '../components/Step';
 import { getUniqueTokenName } from '../utils';
@@ -50,6 +56,8 @@ interface State {
   tokenName?: string;
   token?: string;
   tokens?: UserToken[];
+  tokenExpiration: TokenExpiration;
+  tokenExpirationOptions: { value: TokenExpiration; label: string }[];
 }
 
 const TOKEN_FORMAT_REGEX = /^[_a-z0-9]+$/;
@@ -63,7 +71,9 @@ export default class TokenStep extends React.PureComponent<Props, State> {
       existingToken: '',
       loading: false,
       selection: 'generate',
-      tokenName: props.initialTokenName
+      tokenName: props.initialTokenName,
+      tokenExpiration: TokenExpiration.OneMonth,
+      tokenExpirationOptions: EXPIRATION_OPTIONS
     };
   }
 
@@ -72,6 +82,11 @@ export default class TokenStep extends React.PureComponent<Props, State> {
     const { currentUser, initialTokenName } = this.props;
     const { tokenName } = this.state;
 
+    const tokenExpirationOptions = await getAvailableExpirationOptions();
+    if (tokenExpirationOptions && this.mounted) {
+      this.setState({ tokenExpirationOptions });
+    }
+
     const tokens = await getTokens(currentUser.login).catch(() => {
       /* noop */
     });
@@ -104,9 +119,13 @@ export default class TokenStep extends React.PureComponent<Props, State> {
     this.setState({ tokenName: event.target.value });
   };
 
+  handleTokenExpirationChange = ({ value }: { value: TokenExpiration }) => {
+    this.setState({ tokenExpiration: value });
+  };
+
   handleTokenGenerate = async (event: React.FormEvent<HTMLFormElement>) => {
     event.preventDefault();
-    const { tokenName } = this.state;
+    const { tokenName, tokenExpiration } = this.state;
     const { projectKey } = this.props;
 
     if (tokenName) {
@@ -115,7 +134,10 @@ export default class TokenStep extends React.PureComponent<Props, State> {
         const { token } = await generateToken({
           name: tokenName,
           type: TokenType.Project,
-          projectKey
+          projectKey,
+          ...(tokenExpiration !== TokenExpiration.NoExpiration && {
+            expirationDate: computeTokenExpirationDate(tokenExpiration)
+          })
         });
         if (this.mounted) {
           this.setState({ loading: false, token });
@@ -159,57 +181,83 @@ export default class TokenStep extends React.PureComponent<Props, State> {
     }
   };
 
-  renderGenerateOption = () => (
-    <div>
-      {this.state.tokens !== undefined && this.state.tokens.length > 0 ? (
-        <Radio
-          checked={this.state.selection === 'generate'}
-          onCheck={this.handleModeChange}
-          value="generate">
-          {translate('onboarding.token.generate_project_token')}
-        </Radio>
-      ) : (
-        translate('onboarding.token.generate_project_token')
-      )}
-      {this.state.selection === 'generate' && (
-        <div className="big-spacer-top">
-          <form className="display-flex-column" onSubmit={this.handleTokenGenerate}>
-            <label className="h3" htmlFor="generate-token-input">
-              {translate('onboarding.token.generate_project_token.label')}
-              <DocumentationTooltip
-                className="spacer-left"
-                content={translate('onboarding.token.generate_project_token.help')}
-                links={[
-                  {
-                    href: '/documentation/user-guide/user-token/',
-                    label: translate('learn_more')
-                  }
-                ]}
-              />
-            </label>
-            <div>
-              <input
-                id="generate-token-input"
-                autoFocus={true}
-                className="input-super-large spacer-right spacer-top text-middle"
-                onChange={this.handleTokenNameChange}
-                required={true}
-                type="text"
-                value={this.state.tokenName || ''}
-              />
-              {this.state.loading ? (
-                <i className="spinner text-middle" />
-              ) : (
-                <SubmitButton className="text-middle spacer-top" disabled={!this.state.tokenName}>
-                  {translate('onboarding.token.generate')}
-                </SubmitButton>
-              )}
-            </div>
-          </form>
-        </div>
-      )}
-    </div>
-  );
+  renderGenerateOption = () => {
+    const {
+      loading,
+      selection,
+      tokens,
+      tokenName,
+      tokenExpiration,
+      tokenExpirationOptions
+    } = this.state;
+    return (
+      <div>
+        {tokens !== undefined && tokens.length > 0 ? (
+          <Radio
+            checked={selection === 'generate'}
+            onCheck={this.handleModeChange}
+            value="generate">
+            {translate('onboarding.token.generate_project_token')}
+          </Radio>
+        ) : (
+          translate('onboarding.token.generate_project_token')
+        )}
+        {selection === 'generate' && (
+          <div className="big-spacer-top">
+            <form className="display-flex-center" onSubmit={this.handleTokenGenerate}>
+              <div className="display-flex-column">
+                <label className="h3" htmlFor="generate-token-input">
+                  {translate('onboarding.token.generate_project_token.label')}
+                  <DocumentationTooltip
+                    className="spacer-left"
+                    content={translate('onboarding.token.generate_project_token.help')}
+                    links={[
+                      {
+                        href: '/documentation/user-guide/user-token/',
+                        label: translate('learn_more')
+                      }
+                    ]}
+                  />
+                </label>
+                <input
+                  id="generate-token-input"
+                  autoFocus={true}
+                  className="input-super-large spacer-right spacer-top text-middle"
+                  onChange={this.handleTokenNameChange}
+                  required={true}
+                  type="text"
+                  value={tokenName || ''}
+                />
+              </div>
+              <div className="display-flex-column spacer-left big-spacer-right">
+                <label htmlFor="token-select-expiration" className="h3">
+                  {translate('users.tokens.expires_in')}
+                </label>
+                <div className="display-flex-center">
+                  <Select
+                    id="token-select-expiration"
+                    className="spacer-top abs-width-100 spacer-right"
+                    isSearchable={false}
+                    onChange={this.handleTokenExpirationChange}
+                    options={tokenExpirationOptions}
+                    value={tokenExpirationOptions.find(option => option.value === tokenExpiration)}
+                  />
+
+                  {loading ? (
+                    <i className="spinner text-middle" />
+                  ) : (
+                    <SubmitButton className="text-middle spacer-top" disabled={!tokenName}>
+                      {translate('onboarding.token.generate')}
+                    </SubmitButton>
+                  )}
+                </div>
+              </div>
+            </form>
+          </div>
+        )}
+      </div>
+    );
+  };
 
   renderUseExistingOption = () => {
     const { existingToken } = this.state;
index b3b26676f0aa9a8d375e1bdab5b011cd849c1585..77a80a105293f378b44da3867a57ddce4d1b0a00 100644 (file)
@@ -30,6 +30,18 @@ jest.mock('../../../../api/user-tokens', () => ({
   revokeToken: jest.fn().mockResolvedValue(null)
 }));
 
+jest.mock('../../../../api/settings', () => {
+  return {
+    ...jest.requireActual('../../../../api/settings'),
+    getAllValues: jest.fn().mockResolvedValue([
+      {
+        key: 'sonar.auth.token.max.allowed.lifetime',
+        value: 'No expiration'
+      }
+    ])
+  };
+});
+
 it('sets an initial token name', async () => {
   (getTokens as jest.Mock).mockResolvedValueOnce([{ name: 'fôo' }]);
   const wrapper = shallowRender({ initialTokenName: 'fôo' });
index e5217b8c6f20aa7d4c239967aa52bde3c8793cbb..2c05c134d6ac929748e78b626702ec1385fba2f4 100644 (file)
@@ -35,28 +35,30 @@ exports[`generates token 1`] = `
             className="big-spacer-top"
           >
             <form
-              className="display-flex-column"
+              className="display-flex-center"
               onSubmit={[Function]}
             >
-              <label
-                className="h3"
-                htmlFor="generate-token-input"
+              <div
+                className="display-flex-column"
               >
-                onboarding.token.generate_project_token.label
-                <DocumentationTooltip
-                  className="spacer-left"
-                  content="onboarding.token.generate_project_token.help"
-                  links={
-                    Array [
-                      Object {
-                        "href": "/documentation/user-guide/user-token/",
-                        "label": "learn_more",
-                      },
-                    ]
-                  }
-                />
-              </label>
-              <div>
+                <label
+                  className="h3"
+                  htmlFor="generate-token-input"
+                >
+                  onboarding.token.generate_project_token.label
+                  <DocumentationTooltip
+                    className="spacer-left"
+                    content="onboarding.token.generate_project_token.help"
+                    links={
+                      Array [
+                        Object {
+                          "href": "/documentation/user-guide/user-token/",
+                          "label": "learn_more",
+                        },
+                      ]
+                    }
+                  />
+                </label>
                 <input
                   autoFocus={true}
                   className="input-super-large spacer-right spacer-top text-middle"
@@ -66,12 +68,58 @@ exports[`generates token 1`] = `
                   type="text"
                   value=""
                 />
-                <SubmitButton
-                  className="text-middle spacer-top"
-                  disabled={true}
+              </div>
+              <div
+                className="display-flex-column spacer-left big-spacer-right"
+              >
+                <label
+                  className="h3"
+                  htmlFor="token-select-expiration"
+                >
+                  users.tokens.expires_in
+                </label>
+                <div
+                  className="display-flex-center"
                 >
-                  onboarding.token.generate
-                </SubmitButton>
+                  <Select
+                    className="spacer-top abs-width-100 spacer-right"
+                    id="token-select-expiration"
+                    isSearchable={false}
+                    onChange={[Function]}
+                    options={
+                      Array [
+                        Object {
+                          "label": "users.tokens.expiration.30",
+                          "value": 30,
+                        },
+                        Object {
+                          "label": "users.tokens.expiration.90",
+                          "value": 90,
+                        },
+                        Object {
+                          "label": "users.tokens.expiration.365",
+                          "value": 365,
+                        },
+                        Object {
+                          "label": "users.tokens.expiration.0",
+                          "value": 0,
+                        },
+                      ]
+                    }
+                    value={
+                      Object {
+                        "label": "users.tokens.expiration.30",
+                        "value": 30,
+                      }
+                    }
+                  />
+                  <SubmitButton
+                    className="text-middle spacer-top"
+                    disabled={true}
+                  >
+                    onboarding.token.generate
+                  </SubmitButton>
+                </div>
               </div>
             </form>
           </div>
@@ -146,28 +194,30 @@ exports[`generates token 2`] = `
             className="big-spacer-top"
           >
             <form
-              className="display-flex-column"
+              className="display-flex-center"
               onSubmit={[Function]}
             >
-              <label
-                className="h3"
-                htmlFor="generate-token-input"
+              <div
+                className="display-flex-column"
               >
-                onboarding.token.generate_project_token.label
-                <DocumentationTooltip
-                  className="spacer-left"
-                  content="onboarding.token.generate_project_token.help"
-                  links={
-                    Array [
-                      Object {
-                        "href": "/documentation/user-guide/user-token/",
-                        "label": "learn_more",
-                      },
-                    ]
-                  }
-                />
-              </label>
-              <div>
+                <label
+                  className="h3"
+                  htmlFor="generate-token-input"
+                >
+                  onboarding.token.generate_project_token.label
+                  <DocumentationTooltip
+                    className="spacer-left"
+                    content="onboarding.token.generate_project_token.help"
+                    links={
+                      Array [
+                        Object {
+                          "href": "/documentation/user-guide/user-token/",
+                          "label": "learn_more",
+                        },
+                      ]
+                    }
+                  />
+                </label>
                 <input
                   autoFocus={true}
                   className="input-super-large spacer-right spacer-top text-middle"
@@ -177,9 +227,55 @@ exports[`generates token 2`] = `
                   type="text"
                   value="my token"
                 />
-                <i
-                  className="spinner text-middle"
-                />
+              </div>
+              <div
+                className="display-flex-column spacer-left big-spacer-right"
+              >
+                <label
+                  className="h3"
+                  htmlFor="token-select-expiration"
+                >
+                  users.tokens.expires_in
+                </label>
+                <div
+                  className="display-flex-center"
+                >
+                  <Select
+                    className="spacer-top abs-width-100 spacer-right"
+                    id="token-select-expiration"
+                    isSearchable={false}
+                    onChange={[Function]}
+                    options={
+                      Array [
+                        Object {
+                          "label": "users.tokens.expiration.30",
+                          "value": 30,
+                        },
+                        Object {
+                          "label": "users.tokens.expiration.90",
+                          "value": 90,
+                        },
+                        Object {
+                          "label": "users.tokens.expiration.365",
+                          "value": 365,
+                        },
+                        Object {
+                          "label": "users.tokens.expiration.0",
+                          "value": 0,
+                        },
+                      ]
+                    }
+                    value={
+                      Object {
+                        "label": "users.tokens.expiration.30",
+                        "value": 30,
+                      }
+                    }
+                  />
+                  <i
+                    className="spinner text-middle"
+                  />
+                </div>
               </div>
             </form>
           </div>
@@ -475,28 +571,30 @@ exports[`revokes token 3`] = `
             className="big-spacer-top"
           >
             <form
-              className="display-flex-column"
+              className="display-flex-center"
               onSubmit={[Function]}
             >
-              <label
-                className="h3"
-                htmlFor="generate-token-input"
+              <div
+                className="display-flex-column"
               >
-                onboarding.token.generate_project_token.label
-                <DocumentationTooltip
-                  className="spacer-left"
-                  content="onboarding.token.generate_project_token.help"
-                  links={
-                    Array [
-                      Object {
-                        "href": "/documentation/user-guide/user-token/",
-                        "label": "learn_more",
-                      },
-                    ]
-                  }
-                />
-              </label>
-              <div>
+                <label
+                  className="h3"
+                  htmlFor="generate-token-input"
+                >
+                  onboarding.token.generate_project_token.label
+                  <DocumentationTooltip
+                    className="spacer-left"
+                    content="onboarding.token.generate_project_token.help"
+                    links={
+                      Array [
+                        Object {
+                          "href": "/documentation/user-guide/user-token/",
+                          "label": "learn_more",
+                        },
+                      ]
+                    }
+                  />
+                </label>
                 <input
                   autoFocus={true}
                   className="input-super-large spacer-right spacer-top text-middle"
@@ -506,12 +604,58 @@ exports[`revokes token 3`] = `
                   type="text"
                   value=""
                 />
-                <SubmitButton
-                  className="text-middle spacer-top"
-                  disabled={true}
+              </div>
+              <div
+                className="display-flex-column spacer-left big-spacer-right"
+              >
+                <label
+                  className="h3"
+                  htmlFor="token-select-expiration"
                 >
-                  onboarding.token.generate
-                </SubmitButton>
+                  users.tokens.expires_in
+                </label>
+                <div
+                  className="display-flex-center"
+                >
+                  <Select
+                    className="spacer-top abs-width-100 spacer-right"
+                    id="token-select-expiration"
+                    isSearchable={false}
+                    onChange={[Function]}
+                    options={
+                      Array [
+                        Object {
+                          "label": "users.tokens.expiration.30",
+                          "value": 30,
+                        },
+                        Object {
+                          "label": "users.tokens.expiration.90",
+                          "value": 90,
+                        },
+                        Object {
+                          "label": "users.tokens.expiration.365",
+                          "value": 365,
+                        },
+                        Object {
+                          "label": "users.tokens.expiration.0",
+                          "value": 0,
+                        },
+                      ]
+                    }
+                    value={
+                      Object {
+                        "label": "users.tokens.expiration.30",
+                        "value": 30,
+                      }
+                    }
+                  />
+                  <SubmitButton
+                    className="text-middle spacer-top"
+                    disabled={true}
+                  >
+                    onboarding.token.generate
+                  </SubmitButton>
+                </div>
               </div>
             </form>
           </div>
index 23dac66b829430615457895286270c3a1e4148b0..2815bb88e1c391f324dfbe6589c302be68c2dd09 100644 (file)
@@ -3475,6 +3475,7 @@ onboarding.token.text.user_account=user account
 onboarding.token.generate=Generate
 onboarding.token.placeholder=Enter a name for your token
 onboarding.token.generate_token=Generate a token
+onboarding.token.generate_token.placeholder=Token name
 onboarding.token.generate_project_token=Generate a project token
 onboarding.token.generate_project_token.label=Token name
 onboarding.token.use_existing_token=Use existing token