Просмотр исходного кода

Add optional personal access tokens for git bridge (#15209)

GitOrigin-RevId: 50d4c0e11728e014e81172c062a3b22fefa6286c
Alf Eaton 2 лет назад
Родитель
Сommit
749aef1c6f

+ 21 - 0
services/web/app/src/Features/Project/ProjectController.js

@@ -648,6 +648,21 @@ const ProjectController = {
             }
           )
         },
+        personalAccessTokenAssignment(cb) {
+          SplitTestHandler.getAssignment(
+            req,
+            res,
+            'personal-access-token',
+            (error, assignment) => {
+              // do not fail editor load if assignment fails
+              if (error) {
+                cb(null, { variant: 'default' })
+              } else {
+                cb(null, assignment)
+              }
+            }
+          )
+        },
         historyViewAssignment(cb) {
           SplitTestHandler.getAssignment(
             req,
@@ -716,6 +731,7 @@ const ProjectController = {
           historyViewAssignment,
           reviewPanelAssignment,
           idePageAssignment,
+          personalAccessTokenAssignment,
           projectTags,
         }
       ) => {
@@ -821,6 +837,10 @@ const ProjectController = {
               !Features.hasFeature('saas') ||
               req.query?.personal_access_token === 'true'
 
+            const optionalPersonalAccessToken =
+              !showPersonalAccessToken &&
+              personalAccessTokenAssignment.variant === 'enabled' // `?personal-access-token=enabled`
+
             const idePageReact = idePageAssignment.variant === 'react'
 
             const template =
@@ -903,6 +923,7 @@ const ProjectController = {
               isReviewPanelReact: reviewPanelAssignment.variant === 'react',
               idePageReact,
               showPersonalAccessToken,
+              optionalPersonalAccessToken,
               hasTrackChangesFeature: Features.hasFeature('track-changes'),
               projectTags,
             })

+ 22 - 1
services/web/app/src/Features/User/UserPagesController.js

@@ -10,6 +10,7 @@ const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
 const _ = require('lodash')
 const { expressify } = require('../../util/promises')
 const Features = require('../../infrastructure/Features')
+const SplitTestHandler = require('../SplitTests/SplitTestHandler')
 
 async function settingsPage(req, res) {
   const userId = SessionManager.getLoggedInUserId(req.session)
@@ -69,8 +70,27 @@ async function settingsPage(req, res) {
 
   const showPersonalAccessToken =
     !Features.hasFeature('saas') || req.query?.personal_access_token === 'true'
+
+  // if not already enabled, use a split test to determine whether to offer personal access tokens
+  let optionalPersonalAccessToken = false
+  if (!showPersonalAccessToken) {
+    try {
+      const { variant } = await SplitTestHandler.promises.getAssignment(
+        req,
+        res,
+        'personal-access-token'
+      )
+      optionalPersonalAccessToken = variant === 'enabled' // `?personal-access-token=enabled`
+    } catch (error) {
+      logger.error(
+        { err: error },
+        'Failed to get personal-access-token split test assignment'
+      )
+    }
+  }
+
   let personalAccessTokens
-  if (showPersonalAccessToken) {
+  if (showPersonalAccessToken || optionalPersonalAccessToken) {
     try {
       // require this here because module may not be included in some versions
       const PersonalAccessTokenManager = require('../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager')
@@ -133,6 +153,7 @@ async function settingsPage(req, res) {
     thirdPartyIds: UserPagesController._restructureThirdPartyIds(user),
     projectSyncSuccessMessage,
     showPersonalAccessToken,
+    optionalPersonalAccessToken,
     personalAccessTokens,
     emailAddressLimit: Settings.emailAddressLimit,
     isManagedAccount: !!req.managedBy,

+ 1 - 0
services/web/app/views/project/editor/meta.pug

@@ -36,6 +36,7 @@ meta(name="ol-showSupport", data-type="boolean" content=showSupport)
 meta(name="ol-showTemplatesServerPro", data-type="boolean" content=showTemplatesServerPro)
 meta(name="ol-showCM6SwitchAwaySurvey", data-type="boolean" content=showCM6SwitchAwaySurvey)
 meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
+meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken)
 meta(name="ol-isReviewPanelReact", data-type="boolean" content=isReviewPanelReact)
 meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature)
 meta(name="ol-mathJax3Path" content=mathJax3Path)

+ 1 - 0
services/web/app/views/user/settings.pug

@@ -23,6 +23,7 @@ block append meta
 	meta(name="ol-github" data-type="json" content=github)
 	meta(name="ol-projectSyncSuccessMessage", content=projectSyncSuccessMessage)
 	meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
+	meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken)
 	meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens)
 	meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit)
 	meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail)

+ 2 - 0
services/web/frontend/extracted-translations.json

@@ -400,9 +400,11 @@
   "git_bridge_modal_click_generate": "",
   "git_bridge_modal_description": "",
   "git_bridge_modal_enter_authentication_token": "",
+  "git_bridge_modal_git_authentication_tokens": "",
   "git_bridge_modal_see_once": "",
   "git_bridge_modal_tokens_description": "",
   "git_bridge_modal_use_previous_token": "",
+  "git_bridge_modal_you_can_also_git_clone": "",
   "git_integration": "",
   "git_integration_info": "",
   "github_commit_message_placeholder": "",

+ 4 - 4
services/web/frontend/js/features/settings/components/linking-section.tsx

@@ -31,11 +31,11 @@ function LinkingSection() {
     path: string
   }[]
 
-  const showPersonalAccessToken = getMeta(
-    'ol-showPersonalAccessToken'
-  ) as boolean
+  const showPersonalAccessTokenComponents: boolean =
+    getMeta('ol-showPersonalAccessToken') ||
+    getMeta('ol-optionalPersonalAccessToken')
 
-  const allIntegrationLinkingWidgets = showPersonalAccessToken
+  const allIntegrationLinkingWidgets = showPersonalAccessTokenComponents
     ? integrationLinkingWidgets.concat(oauth2ServerComponents)
     : integrationLinkingWidgets
 

+ 5 - 5
services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx

@@ -6,7 +6,7 @@ import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import { useShareProjectContext } from './share-project-modal'
 import { setProjectAccessLevel } from '../utils/api'
-import CopyLink from '../../../shared/components/copy-link'
+import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
 import { useProjectContext } from '../../../shared/context/project-context'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import { useUserContext } from '../../../shared/context/user-context'
@@ -265,10 +265,10 @@ function AccessToken({ token, path, tooltipId }) {
   const link = `${origin}${path}${token}`
 
   return (
-    <pre className="access-token">
-      <span>{link}</span>
-      <CopyLink link={link} tooltipId={tooltipId} />
-    </pre>
+    <div className="access-token">
+      <code>{link}</code>
+      <CopyToClipboard content={link} tooltipId={tooltipId} />
+    </div>
   )
 }
 

+ 0 - 52
services/web/frontend/js/shared/components/copy-link.tsx

@@ -1,52 +0,0 @@
-import { useCallback, useState } from 'react'
-import { Button } from 'react-bootstrap'
-import { Trans, useTranslation } from 'react-i18next'
-import Tooltip from './tooltip'
-import Icon from './icon'
-
-type CopyLinkProps = {
-  link: string
-  tooltipId: string
-}
-
-function CopyLink({ link, tooltipId }: CopyLinkProps) {
-  const { t } = useTranslation()
-
-  const [copied, setCopied] = useState(false)
-
-  const handleClick = useCallback(() => {
-    navigator.clipboard.writeText(link).then(() => {
-      setCopied(true)
-      window.setTimeout(() => {
-        setCopied(false)
-      }, 1500)
-    })
-  }, [link])
-
-  if (!navigator.clipboard?.writeText) {
-    return null
-  }
-
-  return (
-    <Tooltip
-      id={tooltipId}
-      description={copied ? 'Copied!' : <Trans i18nKey="copy" />}
-      overlayProps={{
-        delayHide: copied ? 1000 : 250,
-        shouldUpdatePosition: true,
-      }}
-    >
-      <Button
-        onClick={handleClick}
-        bsSize="xsmall"
-        bsStyle="link"
-        className="copy-button"
-        aria-label={t('copy')}
-      >
-        {copied ? <Icon type="check" /> : <Icon type="clipboard" />}
-      </Button>
-    </Tooltip>
-  )
-}
-
-export default CopyLink

+ 84 - 0
services/web/frontend/js/shared/components/copy-to-clipboard.tsx

@@ -0,0 +1,84 @@
+import { FC, memo, MouseEventHandler, useCallback, useState } from 'react'
+import { Button } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+import Tooltip from './tooltip'
+import Icon from './icon'
+
+export const CopyToClipboard = memo<{
+  content: string
+  tooltipId: string
+  kind?: 'text' | 'icon'
+}>(({ content, tooltipId, kind = 'icon' }) => {
+  const { t } = useTranslation()
+
+  const [copied, setCopied] = useState(false)
+
+  const handleClick = useCallback(() => {
+    navigator.clipboard.writeText(content).then(() => {
+      setCopied(true)
+      window.setTimeout(() => {
+        setCopied(false)
+      }, 1500)
+    })
+  }, [content])
+
+  if (!navigator.clipboard?.writeText) {
+    return null
+  }
+
+  return (
+    <Tooltip
+      id={tooltipId}
+      description={copied ? `${t('copied')}!` : t('copy')}
+      overlayProps={{
+        delayHide: copied ? 1000 : 250,
+        shouldUpdatePosition: true,
+      }}
+    >
+      <span>
+        {kind === 'text' ? (
+          <TextButton handleClick={handleClick} />
+        ) : (
+          <IconButton handleClick={handleClick} copied={copied} />
+        )}
+      </span>
+    </Tooltip>
+  )
+})
+CopyToClipboard.displayName = 'CopyToClipboard'
+
+const TextButton: FC<{
+  handleClick: MouseEventHandler<Button>
+}> = ({ handleClick }) => {
+  const { t } = useTranslation()
+
+  return (
+    <Button
+      onClick={handleClick}
+      bsSize="xsmall"
+      bsStyle="secondary"
+      className="copy-button"
+    >
+      {t('copy')}
+    </Button>
+  )
+}
+
+const IconButton: FC<{
+  handleClick: MouseEventHandler<Button>
+  copied: boolean
+}> = ({ handleClick, copied }) => {
+  const { t } = useTranslation()
+
+  return (
+    <Button
+      onClick={handleClick}
+      bsSize="xsmall"
+      bsStyle="link"
+      className="copy-button"
+      aria-label={t('copy')}
+    >
+      <Icon type={copied ? 'check' : 'clipboard'} />
+    </Button>
+  )
+}

+ 6 - 1
services/web/frontend/stories/decorators/scope.tsx

@@ -9,6 +9,7 @@ import {
   mockCompileError,
 } from '../fixtures/compile'
 import useFetchMock from '../hooks/use-fetch-mock'
+import { useMeta } from '../hooks/use-meta'
 
 const scopeWatchers: [string, (value: any) => void][] = []
 
@@ -210,7 +211,8 @@ type ScopeDecoratorOptions = {
 
 export const ScopeDecorator = (
   Story: any,
-  opts: ScopeDecoratorOptions = { mockCompileOnLoad: true }
+  opts: ScopeDecoratorOptions = { mockCompileOnLoad: true },
+  meta: Record<string, any> = {}
 ) => {
   // mock compile on load
   useFetchMock(fetchMock => {
@@ -232,6 +234,9 @@ export const ScopeDecorator = (
     return initialize()
   }, [])
 
+  // set values on window.metaAttributesCache (created in initialize, above)
+  useMeta(meta)
+
   return (
     <ContextRoot ide={ide}>
       <Story />

+ 1 - 1
services/web/frontend/stylesheets/_style_includes.less

@@ -117,6 +117,6 @@
 // TODO: find a way for modules to add styles dynamically
 @import 'modules/symbol-palette.less';
 @import 'modules/managed-users.less';
-
+@import 'modules/git-bridge-modal.less';
 @import 'modules/admin-panel.less';
 @import 'modules/overleaf-integration.less';

+ 1 - 0
services/web/frontend/stylesheets/main-style.less

@@ -148,5 +148,6 @@
 // TODO: find a way for modules to add styles dynamically
 @import 'modules/symbol-palette.less';
 @import 'modules/admin-panel.less';
+@import 'modules/git-bridge-modal.less';
 @import 'modules/managed-users.less';
 @import 'modules/overleaf-integration.less';

+ 33 - 0
services/web/frontend/stylesheets/modules/git-bridge-modal.less

@@ -0,0 +1,33 @@
+.git-bridge-copy {
+  background: @neutral-10;
+  color: @neutral-90;
+  padding: @padding-sm;
+  border-radius: @border-radius-large;
+  display: flex;
+  justify-content: center;
+  gap: @padding-sm;
+  align-items: center;
+  margin: @margin-lg 0;
+
+  & code {
+    word-break: break-word;
+  }
+}
+
+.git-bridge-optional-tokens {
+  border: 1px solid @gray-lightest;
+  border-radius: @border-radius-large;
+  padding: @padding-md;
+  margin: @margin-lg 0;
+}
+
+.git-bridge-optional-tokens-header {
+  font-family: @font-family-sans-serif;
+  font-size: 120%;
+  font-weight: bold;
+  margin-bottom: @margin-sm;
+}
+
+.git-bridge-optional-tokens-actions {
+  margin-top: @margin-sm;
+}

+ 2 - 2
services/web/locales/da.json

@@ -577,8 +577,8 @@
   "git_authentication_token": "Git autentificeringsnøgle",
   "git_authentication_token_create_modal_info_1": "Dette er din Git autentificeringsnøgle. Du skal indtaste den når du bliver spurgt om et kodeord.",
   "git_authentication_token_create_modal_info_2": "<0>Du kan kun se denne autentificeringsnøgle én gang så kopier den venligst og opbevar den sikkert.</0> For flere instruktioner omkring brugen af autentificeringsnøgler, besøg vores <1>hjælpeside</1>.",
-  "git_bridge_modal_click_generate": "Klik “Generér nøgle” for at generere din autentificeringsnøgle. Du kan også gøre det senere i dine kontoindstillinger.",
-  "git_bridge_modal_description": "Du kan <code>git</code> <code>clone</code> dit projekt med linket herunder.",
+  "git_bridge_modal_click_generate": "Klik <strong>“Generér nøgle”</strong> for at generere din autentificeringsnøgle. Du kan også gøre det senere i dine kontoindstillinger.",
+  "git_bridge_modal_description": "Du kan git clone dit projekt med linket herunder.",
   "git_bridge_modal_enter_authentication_token": "Når du bliver spurgt om en kode, indtast da din nye autentificeringsnøgle:",
   "git_bridge_modal_see_once": "Du kan kun se denne autentificeringsnøgle én gang. For at slette den eller generere en ny, gå til dine brugerindstilinger. For detalerede instruktioner og fejlsøgning, læs vores <0>hjælpeside</0>.",
   "git_bridge_modal_tokens_description": "For at git clone dit projekt har du brug for linket herunder og en git autentificeringsnøgle.",

+ 2 - 2
services/web/locales/de.json

@@ -582,8 +582,8 @@
   "git_authentication_token": "Git Anmeldungs-Token",
   "git_authentication_token_create_modal_info_1": "Das ist dein Git Anmeldungs-Token. Verwende ihn wenn Du nach einem Passwort gefragt wirst.",
   "git_authentication_token_create_modal_info_2": "<0>Du bekommst diesen Anmelde-Token nur einmal angezeigt, bitte kopiere ihn und bewahre ihn sicher auf.</0> Für weitere Anweisungen zur Verwendung von Anmelde-Tokens, besuche unsere <1>Hilfe-Seite</1>.",
-  "git_bridge_modal_click_generate": "Klicke jetzt auf Token generieren um deinen ersten Anmeldungs-Token zu erstellen. Oder erstelle ihn später in deinen Kontoeinstellungen.",
-  "git_bridge_modal_description": "Du kannst <code>git</code> <code>clone</code> für dein Projekt über den unten angezeigten Link ausführen.",
+  "git_bridge_modal_click_generate": "Klicke jetzt auf <strong>Token generieren</strong> um deinen ersten Anmeldungs-Token zu erstellen. Oder erstelle ihn später in deinen Kontoeinstellungen.",
+  "git_bridge_modal_description": "Du kannst git clone für dein Projekt über den unten angezeigten Link ausführen.",
   "git_bridge_modal_enter_authentication_token": "Wenn Du nach einem Passwort gefragt wirst, gib deinen neuen Anmeldungs-Token ein:",
   "git_bridge_modal_see_once": "Du siehst diesen Token nur einmal. Um ihn zu löschen oder einen weiteren zu generieren, besuche die Kontoeinstellungen. Für detaillierte Anweisungen und Problembehebung, besuche unsere <0>Hilfe-Seite</0>.",
   "git_bridge_modal_tokens_description": "Um einen Git-Clone deines Projekts zu erstellen, benutze folgenden Link und einen Git-Anmeldungs-Token.",

+ 4 - 2
services/web/locales/en.json

@@ -647,12 +647,14 @@
   "git_authentication_token": "Git authentication token",
   "git_authentication_token_create_modal_info_1": "This is your Git authentication token. You should enter this when prompted for a password.",
   "git_authentication_token_create_modal_info_2": "<0>You will only see this authentication token once so please copy it and keep it safe</0>. For full instructions on using authentication tokens, visit our <1>help page</1>.",
-  "git_bridge_modal_click_generate": "Click Generate token to generate your authentication token now. Or do this later in your Account Settings.",
-  "git_bridge_modal_description": "You can <code>git</code> <code>clone</code> your project using the link displayed below.",
+  "git_bridge_modal_click_generate": "Click <strong>Generate token</strong> to generate your authentication token now. Or do this later in your Account Settings.",
+  "git_bridge_modal_description": "You can git clone your project using the link displayed below.",
   "git_bridge_modal_enter_authentication_token": "When prompted for a password, enter your new authentication token:",
+  "git_bridge_modal_git_authentication_tokens": "Git authentication tokens",
   "git_bridge_modal_see_once": "You’ll only see this token once. To delete it or generate a new one, visit Account Settings. For detailed instructions and troubleshooting, read our <0>help page</0>.",
   "git_bridge_modal_tokens_description": "To git clone your project you’ll need the link below and a Git authentication token.",
   "git_bridge_modal_use_previous_token": "If you’re prompted for a password, you can use a previously generated Git authentication token. Or you can generate a new one in Account Settings. For more support, read our <0>help page</0>.",
+  "git_bridge_modal_you_can_also_git_clone": "You can also git clone your project by using the link below and a Git authentication token.",
   "git_integration": "Git Integration",
   "git_integration_info": "With Git integration, you can clone your Overleaf projects with Git. For full instructions on how to do this, read <0>our help page</0>.",
   "git_integration_lowercase": "Git integration",

+ 1 - 1
services/web/locales/fr.json

@@ -364,7 +364,7 @@
   "get_same_latex_setup": "Avec __appName__, votre configuration LaTeX vous suit partout. En travaillant avec vos collègues ou étudiant·e·s sur __appName__, vous êtes sûr·e de ne pas rencontrer d’incohérences de version ou de conflits de paquets.",
   "get_started_now": "Commencer maintenant",
   "git": "Git",
-  "git_bridge_modal_description": "Vous pouvez effectuer un <code>git</code> <code>clone</code> de votre projet en utilisant le lien ci-dessous.",
+  "git_bridge_modal_description": "Vous pouvez effectuer un git clone de votre projet en utilisant le lien ci-dessous.",
   "github_commit_message_placeholder": "Message de commit pour les changements effectués dans __appName__…",
   "github_credentials_expired": "Vos identifiants GitHub ont expiré",
   "github_for_link_shared_projects": "Vous avez accédé à ce projet par un partage de lien : celui-ci ne sera pas synchronisé à votre GitHub tant que vous n’aurez pas été invité par courriel par le propriétaire du projet.",

+ 1 - 1
services/web/locales/pt.json

@@ -261,7 +261,7 @@
   "get_same_latex_setup": "Com __appName__ você tem o mesmo aplicativo onde quer que vá. Trabalhando com seus colegas e estudantes no __appName__, você sabe que não terá problema de inconsistência ou conflito de pacotes.",
   "get_started_now": "Comece agora",
   "git": "Git",
-  "git_bridge_modal_description": "Você pode usar <code>git clone</code> no seu projeto usando o link abaixo.",
+  "git_bridge_modal_description": "Você pode usar git clone no seu projeto usando o link abaixo.",
   "github_commit_message_placeholder": "Mensagem de commit para as alterações feitas no __appName__...",
   "github_credentials_expired": "Suas credenciais de autorização do GitHub expiraram",
   "github_for_link_shared_projects": "Este projeto foi acessado por meio do compartilhamento de links e não será sincronizado com o seu Github, a menos que você seja convidado por e-mail pelo proprietário do projeto.",

+ 1 - 1
services/web/locales/zh-CN.json

@@ -362,7 +362,7 @@
   "get_same_latex_setup": "您到任何地方都可以用  __appName__ 实现LaTeX的功能。由于您和您的同事和学生可以在  __appName__ 上共同工作,不会出现版本不一致和包冲突的情况。",
   "get_started_now": "立即开始",
   "git": "Git",
-  "git_bridge_modal_description": "您可以使用下面显示的链接来<code>git</code><code>clone</code>您的项目。",
+  "git_bridge_modal_description": "您可以使用下面显示的链接来git clone您的项目。",
   "github_commit_message_placeholder": "为 __appName__ 中的更改提交信息",
   "github_credentials_expired": "您的 Github 授权凭证已过期",
   "github_for_link_shared_projects": "此项目是通过链接共享访问的,除非项目所有者通过电子邮件邀请您,否则不会与您的Github同步。",

+ 6 - 0
services/web/test/unit/src/User/UserPagesControllerTests.js

@@ -82,6 +82,11 @@ describe('UserPagesController', function () {
         getAdminEmail: sinon.stub().returns(this.adminEmail),
       },
     }
+    this.SplitTestHandler = {
+      promises: {
+        getAssignment: sinon.stub().returns('default'),
+      },
+    }
     this.UserPagesController = SandboxedModule.require(modulePath, {
       requires: {
         '@overleaf/settings': this.settings,
@@ -96,6 +101,7 @@ describe('UserPagesController', function () {
         '../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager':
           this.PersonalAccessTokenManager,
         '../Authentication/SessionManager': this.SessionManager,
+        '../SplitTests/SplitTestHandler': this.SplitTestHandler,
         request: (this.request = sinon.stub()),
       },
     })