mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
Git Bridge: Add git integration
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { postJSON, getJSON } from '@/infrastructure/fetch-json'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import GitLogoOrange from '@/shared/svgs/git-logo-orange'
|
||||
import TokenTable from './token-table'
|
||||
import ExposeTokenModal from './modals/expose-token-modal'
|
||||
import { Token } from '../../../../types/api'
|
||||
|
||||
export default function GitIntegrationWidget() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [tokens, setTokens] = useState<Token[]>([])
|
||||
const [showExposeTokenModal, setShowExposeTokenModal] = useState(false)
|
||||
const [secretToken, setSecretToken] = useState<string | null>(null)
|
||||
|
||||
const { runAsync, isLoading, isError, reset } = useAsync()
|
||||
|
||||
useEffect(() => {
|
||||
runAsync(getJSON('/git-bridge/personal-access-tokens'))
|
||||
.then((data: Token[]) => setTokens(data))
|
||||
.catch(debugConsole.error)
|
||||
}, [runAsync])
|
||||
|
||||
const handleCreateToken = useCallback(() => {
|
||||
runAsync(postJSON('/git-bridge/personal-access-tokens'))
|
||||
.then((data: Token & { accessToken: string }) => {
|
||||
const { accessToken, ...newToken } = data
|
||||
setTokens(prev => [...prev, newToken])
|
||||
setSecretToken(accessToken)
|
||||
setShowExposeTokenModal(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
debugConsole.error(err)
|
||||
setTimeout(() => reset(), 5000)
|
||||
})
|
||||
}, [runAsync])
|
||||
|
||||
const handleDeleteToken = useCallback((id: string) => {
|
||||
setTokens(prev => prev.filter(t => t._id !== id))
|
||||
}, [])
|
||||
|
||||
const tokenCount = tokens.length
|
||||
|
||||
return (
|
||||
<div className="settings-widget-container">
|
||||
|
||||
<div className="linking-icon-fixed-position">
|
||||
<GitLogoOrange />
|
||||
</div>
|
||||
|
||||
<div className="description-container">
|
||||
|
||||
<div className="title-row">
|
||||
<h4>{t('git_integration')}</h4>
|
||||
</div>
|
||||
|
||||
<p className="small">
|
||||
<Trans
|
||||
i18nKey="git_integration_info"
|
||||
components={[
|
||||
<a
|
||||
key="git-link"
|
||||
href="/learn/how-to/Git_Integration"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<h4 className="ui-heading">
|
||||
{t('your_git_access_tokens')}
|
||||
</h4>
|
||||
|
||||
<p className="small">
|
||||
{t('your_git_access_info')}
|
||||
</p>
|
||||
|
||||
<ul className="small">
|
||||
{tokenCount > 0 ? (
|
||||
<>
|
||||
<li>{t('your_git_access_info_bullet_1')}</li>
|
||||
<li>{t('your_git_access_info_bullet_2')}</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
<Trans
|
||||
i18nKey="your_git_access_info_bullet_3"
|
||||
components={[<strong key="strong" />]}
|
||||
/>
|
||||
</li>
|
||||
<li>{t('your_git_access_info_bullet_4')}</li>
|
||||
<li>{t('your_git_access_info_bullet_5')}</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<TokenTable
|
||||
tokens={tokens}
|
||||
onCreateToken={handleCreateToken}
|
||||
onDeleteToken={handleDeleteToken}
|
||||
/>
|
||||
|
||||
{isError && (
|
||||
<div className="notification-list">
|
||||
<Notification
|
||||
type="error"
|
||||
content={t('something_went_wrong_server')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{tokenCount === 0 && (
|
||||
<div>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
id="generate-token-button"
|
||||
onClick={handleCreateToken}
|
||||
disabled={isLoading || isError}
|
||||
>
|
||||
{t('generate_token')}
|
||||
</OLButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showExposeTokenModal && secretToken && (
|
||||
<ExposeTokenModal
|
||||
secretToken={secretToken}
|
||||
handleHide={() => {
|
||||
setShowExposeTokenModal(false)
|
||||
setSecretToken(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { deleteJSON } from '@/infrastructure/fetch-json'
|
||||
import {
|
||||
OLModal,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
handleHide: () => void
|
||||
tokenId: string
|
||||
onDeleted: (id: string) => void
|
||||
}
|
||||
|
||||
export default function DeleteTokenModal({
|
||||
show,
|
||||
handleHide,
|
||||
tokenId,
|
||||
onDeleted,
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { isLoading, isError, runAsync, reset } = useAsync()
|
||||
|
||||
const handleClose = () => {
|
||||
reset()
|
||||
handleHide()
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
runAsync(
|
||||
deleteJSON(`/git-bridge/personal-access-tokens/${tokenId}`, {
|
||||
body: { _csrf: window.csrfToken },
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
onDeleted(tokenId)
|
||||
handleClose()
|
||||
})
|
||||
.catch(debugConsole.error)
|
||||
}
|
||||
|
||||
return (
|
||||
<OLModal show={show} onHide={handleClose} backdrop="static">
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('delete_authentication_token')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<OLModalBody>
|
||||
<p>{t('delete_authentication_token_info')}</p>
|
||||
|
||||
{isError && (
|
||||
<div className="notification-list">
|
||||
<Notification
|
||||
type="error"
|
||||
content={t('something_went_wrong_server')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={handleClose} disabled={isLoading}>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
|
||||
<OLButton variant="danger" onClick={handleDelete} disabled={isLoading}>
|
||||
{t('delete_token')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import {
|
||||
OLModal,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
|
||||
|
||||
type Props = {
|
||||
secretToken: string
|
||||
handleHide: () => void
|
||||
}
|
||||
|
||||
export default function ExposeTokenModal({
|
||||
secretToken,
|
||||
handleHide,
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLModal
|
||||
show
|
||||
onHide={handleHide}
|
||||
backdrop="static"
|
||||
id="create-token-modal"
|
||||
>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>
|
||||
{t('git_authentication_token')}
|
||||
</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<OLModalBody>
|
||||
<p>
|
||||
{t('git_authentication_token_create_modal_info_1')}
|
||||
</p>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="git-bridge-copy">
|
||||
<span aria-label={t('git_authentication_token')}>
|
||||
<code>{secretToken}</code>
|
||||
</span>
|
||||
<CopyToClipboard
|
||||
content={secretToken}
|
||||
tooltipId="git-auth-token-copy"
|
||||
kind="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="git_authentication_token_create_modal_info_2"
|
||||
components={[
|
||||
<strong key="strong" />,
|
||||
<a
|
||||
key="link"
|
||||
href="/learn/how-to/Git_integration_authentication_tokens"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
</OLModalBody>
|
||||
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLRow from '@/shared/components/ol/ol-row'
|
||||
import OLCol from '@/shared/components/ol/ol-col'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import Cell from '@/features/settings/components/emails/cell'
|
||||
|
||||
type Props = {
|
||||
tokenCount: number
|
||||
limitReached: boolean
|
||||
onCreateToken: () => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export default function TokenTableFooter({
|
||||
tokenCount,
|
||||
limitReached,
|
||||
onCreateToken,
|
||||
isLoading,
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (tokenCount === 0) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="horizontal-divider" />
|
||||
<div className="affiliations-table-row-highlighted">
|
||||
<OLRow className="small">
|
||||
<OLCol lg={12}>
|
||||
<Cell>
|
||||
{limitReached ? (
|
||||
<p>{t('token_limit_reached')}</p>
|
||||
) : (
|
||||
<OLButton
|
||||
variant="link"
|
||||
className="btn-inline-link"
|
||||
onClick={onCreateToken}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('add_another_token')}
|
||||
</OLButton>
|
||||
)}
|
||||
</Cell>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLRow from '@/shared/components/ol/ol-row'
|
||||
import OLCol from '@/shared/components/ol/ol-col'
|
||||
import Cell from '@/features/settings/components/emails/cell'
|
||||
|
||||
export default function TokenTableHeader() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLRow className="small">
|
||||
<OLCol lg={4} className="linking-git-bridge-table-cell">
|
||||
<Cell><strong>{t('token')}</strong></Cell>
|
||||
</OLCol>
|
||||
|
||||
<OLCol lg={2} className="linking-git-bridge-table-cell">
|
||||
<Cell><strong>{t('created_at')}</strong></Cell>
|
||||
</OLCol>
|
||||
|
||||
<OLCol lg={2} className="linking-git-bridge-table-cell">
|
||||
<Cell><strong>{t('last_used')}</strong></Cell>
|
||||
</OLCol>
|
||||
|
||||
<OLCol lg={3} className="linking-git-bridge-table-cell">
|
||||
<Cell><strong>{t('expires')}</strong></Cell>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
|
||||
<div className="horizontal-divider" />
|
||||
<div className="horizontal-divider" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import moment from 'moment'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLRow from '@/shared/components/ol/ol-row'
|
||||
import OLCol from '@/shared/components/ol/ol-col'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import Cell from '@/features/settings/components/emails/cell'
|
||||
import { Token } from '../../../../types/api'
|
||||
|
||||
type Props = {
|
||||
token: Token
|
||||
handleDeleteClick: (id: string) => void
|
||||
}
|
||||
|
||||
function TokenTableRow({ token, handleDeleteClick }: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const created = moment(token.created_at).format('Do MMM YYYY')
|
||||
const lastUsed = token.lastUsedAt
|
||||
? moment(token.lastUsedAt).format('Do MMM YYYY')
|
||||
: t('never')
|
||||
const expires = moment(token.expiresAt).format('Do MMM YYYY')
|
||||
|
||||
const handleClick = () => handleDeleteClick(token._id)
|
||||
|
||||
return (
|
||||
<OLRow className="small">
|
||||
<OLCol lg={4} className="linking-git-bridge-table-cell">
|
||||
<Cell>{token.accessTokenPartial + '************'}</Cell>
|
||||
</OLCol>
|
||||
|
||||
<OLCol lg={2} className="linking-git-bridge-table-cell">
|
||||
<Cell>{created}</Cell>
|
||||
</OLCol>
|
||||
|
||||
<OLCol lg={2} className="linking-git-bridge-table-cell">
|
||||
<Cell>{lastUsed}</Cell>
|
||||
</OLCol>
|
||||
|
||||
<OLCol lg={3} className="linking-git-bridge-table-cell">
|
||||
<Cell>{expires}</Cell>
|
||||
</OLCol>
|
||||
|
||||
<OLCol lg={1} className="linking-git-bridge-table-cell">
|
||||
<Cell>
|
||||
<OLButton
|
||||
className="linking-git-bridge-revoke-button icon-button-small"
|
||||
variant="secondary"
|
||||
aria-label={t('remove')}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span
|
||||
className="material-symbols icon-small"
|
||||
aria-hidden="true"
|
||||
>
|
||||
delete
|
||||
</span>
|
||||
</OLButton>
|
||||
</Cell>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
)
|
||||
}
|
||||
|
||||
export default TokenTableRow
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DeleteTokenModal from './modals/delete-token-modal'
|
||||
import TokenTableHeader from './token-table-header'
|
||||
import TokenTableRow from './token-table-row'
|
||||
import TokenTableFooter from './token-table-footer'
|
||||
import { Token } from '../../../../types/api'
|
||||
|
||||
const MAX_TOKENS = 10
|
||||
|
||||
type Props = {
|
||||
tokens: Token[]
|
||||
onCreateToken: () => void
|
||||
onDeleteToken: (id: string) => void
|
||||
}
|
||||
|
||||
export default function TokenTable({
|
||||
tokens,
|
||||
onCreateToken,
|
||||
onDeleteToken,
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [selectedTokenId, setSelectedTokenId] = useState('')
|
||||
|
||||
const handleDeleteClick = (id: string) => {
|
||||
setSelectedTokenId(id)
|
||||
setShowDeleteModal(true)
|
||||
}
|
||||
|
||||
const tokenCount = tokens.length
|
||||
const limitReached = tokenCount >= MAX_TOKENS
|
||||
|
||||
return (
|
||||
<>
|
||||
{tokenCount > 0 && <TokenTableHeader />}
|
||||
|
||||
{tokens.map((token) => (
|
||||
<TokenTableRow
|
||||
key={token._id}
|
||||
token={token}
|
||||
handleDeleteClick={handleDeleteClick}
|
||||
/>
|
||||
))}
|
||||
|
||||
<TokenTableFooter
|
||||
tokenCount={tokenCount}
|
||||
limitReached={limitReached}
|
||||
onCreateToken={onCreateToken}
|
||||
isLoading={false}
|
||||
/>
|
||||
|
||||
<DeleteTokenModal
|
||||
show={showDeleteModal}
|
||||
tokenId={selectedTokenId}
|
||||
handleHide={() => setShowDeleteModal(false)}
|
||||
onDeleted={(id: string) => {
|
||||
onDeleteToken(id)
|
||||
setShowDeleteModal(false)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user