mirror of
https://github.com/harness/drone.git
synced 2025-05-21 11:29:52 +08:00
470 lines
22 KiB
TypeScript
470 lines
22 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
import { useGet } from 'restful-react'
|
|
import {
|
|
ButtonSize,
|
|
ButtonVariation,
|
|
Container,
|
|
FlexExpander,
|
|
Heading,
|
|
Layout,
|
|
StringSubstitute,
|
|
Tabs,
|
|
Utils
|
|
} from '@harnessio/uicore'
|
|
import { Icon } from '@harnessio/icons'
|
|
import { Color } from '@harnessio/design-system'
|
|
import { Document, Page, pdfjs } from 'react-pdf'
|
|
import { Render, Match, Truthy, Falsy, Case, Else } from 'react-jsx-match'
|
|
import { Link, useHistory } from 'react-router-dom'
|
|
import { SourceCodeViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
|
import type { OpenapiContentInfo, RepoFileContent, TypesCommit } from 'services/code'
|
|
import {
|
|
decodeGitContent,
|
|
findMarkdownInfo,
|
|
GitCommitAction,
|
|
GitInfoProps,
|
|
isRefATag,
|
|
makeDiffRefs
|
|
} from 'utils/GitUtils'
|
|
import { filenameToLanguage, permissionProps, LIST_FETCHING_LIMIT, RenameDetails, FileSection } from 'utils/Utils'
|
|
import { useAppContext } from 'AppContext'
|
|
import { LatestCommitForFile } from 'components/LatestCommit/LatestCommit'
|
|
import { useCommitModal } from 'components/CommitModalButton/CommitModalButton'
|
|
import { useStrings } from 'framework/strings'
|
|
import { getConfig } from 'services/config'
|
|
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
|
import { PlainButton } from 'components/PlainButton/PlainButton'
|
|
import { CommitsView } from 'components/CommitsView/CommitsView'
|
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
|
import { FileCategory, RepoContentExtended, useFileContentViewerDecision } from 'utils/FileUtils'
|
|
import { useDownloadRawFile } from 'hooks/useDownloadRawFile'
|
|
import { usePageIndex } from 'hooks/usePageIndex'
|
|
import { Readme } from '../FolderContent/Readme'
|
|
import { GitBlame } from './GitBlame'
|
|
import RenameContentHistory from './RenameContentHistory'
|
|
import css from './FileContent.module.scss'
|
|
|
|
export function FileContent({
|
|
repoMetadata,
|
|
gitRef,
|
|
resourcePath,
|
|
resourceContent,
|
|
commitRef
|
|
}: Pick<GitInfoProps, 'repoMetadata' | 'gitRef' | 'resourcePath' | 'resourceContent' | 'commitRef'>) {
|
|
const { routes } = useAppContext()
|
|
const { getString } = useStrings()
|
|
const downloadFile = useDownloadRawFile()
|
|
const { category, isText, isFileTooLarge, isViewable, filename, extension, size, base64Data, rawURL } =
|
|
useFileContentViewerDecision({ repoMetadata, gitRef, resourcePath, resourceContent })
|
|
const history = useHistory()
|
|
const [activeTab, setActiveTab] = React.useState<string>(FileSection.CONTENT)
|
|
const content = useMemo(
|
|
() => decodeGitContent((resourceContent?.content as RepoFileContent)?.data),
|
|
[resourceContent?.content]
|
|
)
|
|
const markdownInfo = useMemo(() => findMarkdownInfo(resourceContent), [resourceContent])
|
|
const [openDeleteFileModal] = useCommitModal({
|
|
repoMetadata,
|
|
gitRef,
|
|
resourcePath,
|
|
commitAction: GitCommitAction.DELETE,
|
|
commitTitlePlaceHolder: getString('deleteFile').replace('__path__', resourcePath),
|
|
onSuccess: (_commitInfo, newBranch) => {
|
|
if (newBranch) {
|
|
history.replace(
|
|
routes.toCODECompare({
|
|
repoPath: repoMetadata.path as string,
|
|
diffRefs: makeDiffRefs(repoMetadata?.default_branch as string, newBranch)
|
|
})
|
|
)
|
|
} else {
|
|
history.push(
|
|
routes.toCODERepository({
|
|
repoPath: repoMetadata.path as string,
|
|
gitRef
|
|
})
|
|
)
|
|
}
|
|
}
|
|
})
|
|
|
|
const { standalone } = useAppContext()
|
|
const { hooks } = useAppContext()
|
|
const space = useGetSpaceParam()
|
|
const permPushResult = hooks?.usePermissionTranslate?.(
|
|
{
|
|
resource: {
|
|
resourceType: 'CODE_REPOSITORY'
|
|
},
|
|
permissions: ['code_repo_push']
|
|
},
|
|
[space]
|
|
)
|
|
const permsFinal = useMemo(() => {
|
|
const perms = permissionProps(permPushResult, standalone)
|
|
if (isRefATag(gitRef) && perms) {
|
|
return { tooltip: perms.tooltip, disabled: true }
|
|
}
|
|
|
|
if (isRefATag(gitRef)) {
|
|
return { tooltip: getString('editNotAllowed'), disabled: true }
|
|
} else if (perms?.disabled) {
|
|
return { disabled: perms.disabled, tooltip: perms.tooltip }
|
|
}
|
|
return { disabled: isRefATag(gitRef) || false, tooltip: undefined }
|
|
}, [permPushResult, gitRef]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
const [pdfWidth, setPdfWidth] = useState<number>(700)
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
const [numPages, setNumPages] = useState<number>()
|
|
|
|
useEffect(() => {
|
|
if (ref.current) {
|
|
const width = Math.min(Math.max(ref.current.clientWidth - 100, 700), 1800)
|
|
|
|
if (pdfWidth !== width) {
|
|
setPdfWidth(width)
|
|
}
|
|
}
|
|
}, [pdfWidth, ref.current?.clientWidth])
|
|
|
|
const [page] = usePageIndex()
|
|
const { data: commits } = useGet<{ commits: TypesCommit[]; rename_details: RenameDetails[] }>({
|
|
path: `/api/v1/repos/${repoMetadata?.path}/+/commits`,
|
|
queryParams: {
|
|
limit: LIST_FETCHING_LIMIT,
|
|
page,
|
|
git_ref: commitRef || gitRef || repoMetadata?.default_branch,
|
|
path: resourcePath
|
|
},
|
|
lazy: !repoMetadata
|
|
})
|
|
const editButtonDisabled = useMemo(() => permsFinal.disabled || !isText, [permsFinal.disabled, isText])
|
|
const editAsText = useMemo(
|
|
() => editButtonDisabled && !isFileTooLarge && category === FileCategory.OTHER,
|
|
[editButtonDisabled, isFileTooLarge, category]
|
|
)
|
|
|
|
return (
|
|
<Container className={css.tabsContainer} ref={ref}>
|
|
<Tabs
|
|
id="fileTabs"
|
|
selectedTabId={activeTab}
|
|
defaultSelectedTabId={FileSection.CONTENT}
|
|
large={false}
|
|
onChange={(id: string) => setActiveTab(id)}
|
|
tabList={[
|
|
{
|
|
id: FileSection.CONTENT,
|
|
title: getString('content'),
|
|
panel: (
|
|
<Container className={css.fileContent}>
|
|
<Layout.Vertical spacing="small" style={{ maxWidth: '100%' }}>
|
|
<LatestCommitForFile
|
|
repoMetadata={repoMetadata}
|
|
latestCommit={resourceContent.latest_commit}
|
|
standaloneStyle
|
|
size={size}
|
|
/>
|
|
<Container className={css.container} background={Color.WHITE}>
|
|
<Layout.Horizontal padding="small" className={css.heading}>
|
|
<Heading level={5} color={Color.BLACK}>
|
|
{resourceContent.name}
|
|
</Heading>
|
|
<FlexExpander />
|
|
<Layout.Horizontal spacing="xsmall" style={{ alignItems: 'center' }}>
|
|
<PlainButton
|
|
withoutCurrentColor
|
|
size={ButtonSize.SMALL}
|
|
variation={ButtonVariation.TERTIARY}
|
|
iconProps={{ size: 16 }}
|
|
text={getString(editAsText ? 'editAsText' : 'edit')}
|
|
icon="code-edit"
|
|
tooltipProps={{ isDark: true }}
|
|
tooltip={permsFinal.tooltip}
|
|
disabled={editButtonDisabled && !editAsText}
|
|
onClick={() => {
|
|
history.push(
|
|
routes.toCODEFileEdit({
|
|
repoPath: repoMetadata.path as string,
|
|
gitRef,
|
|
resourcePath
|
|
})
|
|
)
|
|
}}
|
|
/>
|
|
<OptionsMenuButton
|
|
isDark={true}
|
|
icon="Options"
|
|
iconProps={{ size: 14 }}
|
|
style={{ padding: '5px' }}
|
|
width="145px"
|
|
items={[
|
|
{
|
|
hasIcon: true,
|
|
iconName: 'arrow-right',
|
|
text: getString('viewRaw'),
|
|
onClick: () => {
|
|
const url = standalone
|
|
? rawURL.replace(/^\/code/, '')
|
|
: getConfig(rawURL).replace('//', '/')
|
|
window.open(url, '_blank')
|
|
}
|
|
},
|
|
'-',
|
|
{
|
|
hasIcon: true,
|
|
iconName: 'cloud-download',
|
|
text: getString('download'),
|
|
onClick: () => downloadFile({ repoMetadata, resourcePath, gitRef, filename })
|
|
},
|
|
{
|
|
hasIcon: true,
|
|
iconName: 'code-copy',
|
|
iconSize: 16,
|
|
text: getString('copy'),
|
|
onClick: () => Utils.copy(content)
|
|
},
|
|
{
|
|
hasIcon: true,
|
|
iconName: 'code-delete',
|
|
iconSize: 16,
|
|
title: getString(isRefATag(gitRef) ? 'deleteNotAllowed' : 'delete'),
|
|
disabled: isRefATag(gitRef),
|
|
text: getString('delete'),
|
|
onClick: openDeleteFileModal
|
|
}
|
|
]}
|
|
/>
|
|
</Layout.Horizontal>
|
|
</Layout.Horizontal>
|
|
|
|
<Render
|
|
when={
|
|
(resourceContent?.content as RepoFileContent)?.data ||
|
|
(resourceContent?.content as RepoContentExtended)?.target ||
|
|
(resourceContent?.content as RepoContentExtended)?.url
|
|
}>
|
|
<Container className={css.content}>
|
|
<Match expr={isViewable}>
|
|
<Falsy>
|
|
<Center>
|
|
rawURL: {rawURL}
|
|
<Link
|
|
to={rawURL} // TODO: Link component generates wrong copy link
|
|
onClick={e => {
|
|
Utils.stopEvent(e)
|
|
downloadFile({ repoMetadata, resourcePath, gitRef, filename })
|
|
}}>
|
|
<Layout.Horizontal spacing="small">
|
|
<Icon name="cloud-download" size={16} />
|
|
<span>{getString('download')}</span>
|
|
</Layout.Horizontal>
|
|
</Link>
|
|
</Center>
|
|
</Falsy>
|
|
<Truthy>
|
|
<Match expr={isFileTooLarge}>
|
|
<Truthy>
|
|
<Center>
|
|
<Match expr={category}>
|
|
<Case val={FileCategory.PDF}>
|
|
<Document
|
|
file={rawURL}
|
|
options={{
|
|
// TODO: Configure this to use a local worker/webpack loader
|
|
cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
|
|
cMapPacked: true
|
|
}}
|
|
onLoadSuccess={({ numPages: nextNumPages }) => setNumPages(nextNumPages)}>
|
|
{Array.from(new Array(numPages), (_el, index) => (
|
|
<Page
|
|
loading=""
|
|
width={pdfWidth}
|
|
key={`page_${index + 1}`}
|
|
pageNumber={index + 1}
|
|
renderTextLayer={false}
|
|
renderAnnotationLayer={false}
|
|
/>
|
|
))}
|
|
</Document>
|
|
</Case>
|
|
<Case val={FileCategory.AUDIO}>
|
|
<audio controls>
|
|
<source src={rawURL} />
|
|
</audio>
|
|
</Case>
|
|
<Case val={FileCategory.VIDEO}>
|
|
<video controls height={500}>
|
|
<source src={rawURL} />
|
|
</video>
|
|
</Case>
|
|
<Else>
|
|
<StringSubstitute
|
|
str={getString('fileTooLarge')}
|
|
vars={{
|
|
download: (
|
|
<Link
|
|
to={rawURL} // TODO: Link component generates wrong copy link
|
|
onClick={e => {
|
|
Utils.stopEvent(e)
|
|
downloadFile({ repoMetadata, resourcePath, gitRef, filename })
|
|
}}>
|
|
<Layout.Horizontal spacing="small" padding={{ left: 'small' }}>
|
|
<Icon name="cloud-download" size={16} />
|
|
<span>{getString('clickHereToDownload')}</span>
|
|
</Layout.Horizontal>
|
|
</Link>
|
|
)
|
|
}}
|
|
/>
|
|
</Else>
|
|
</Match>
|
|
</Center>
|
|
</Truthy>
|
|
<Falsy>
|
|
<Match expr={markdownInfo}>
|
|
<Truthy>
|
|
<Readme
|
|
metadata={repoMetadata}
|
|
readmeInfo={markdownInfo as OpenapiContentInfo}
|
|
contentOnly
|
|
maxWidth="calc(100vw - 346px)"
|
|
gitRef={gitRef}
|
|
/>
|
|
</Truthy>
|
|
<Falsy>
|
|
<Center>
|
|
<Match expr={category}>
|
|
<Case val={FileCategory.SVG}>
|
|
<img
|
|
src={`data:image/svg+xml;base64,${base64Data}`}
|
|
alt={filename}
|
|
style={{ maxWidth: '100%', maxHeight: '100%' }}
|
|
/>
|
|
</Case>
|
|
<Case val={FileCategory.IMAGE}>
|
|
<img
|
|
src={`data:image/${extension};base64,${base64Data}`}
|
|
alt={filename}
|
|
style={{ maxWidth: '100%', maxHeight: '100%' }}
|
|
/>
|
|
</Case>
|
|
<Case val={FileCategory.PDF}>
|
|
<Document
|
|
file={`data:application/pdf;base64,${base64Data}`}
|
|
options={{
|
|
// TODO: Configure this to use a local worker/webpack loader
|
|
cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
|
|
cMapPacked: true
|
|
}}
|
|
onLoadSuccess={({ numPages: nextNumPages }) => setNumPages(nextNumPages)}>
|
|
{Array.from(new Array(numPages), (_el, index) => (
|
|
<Page
|
|
loading=""
|
|
width={pdfWidth}
|
|
key={`page_${index + 1}`}
|
|
pageNumber={index + 1}
|
|
renderTextLayer={false}
|
|
renderAnnotationLayer={false}
|
|
/>
|
|
))}
|
|
</Document>
|
|
</Case>
|
|
<Case val={FileCategory.AUDIO}>
|
|
<audio controls>
|
|
<source src={`data:audio/${extension};base64,${base64Data}`} />
|
|
</audio>
|
|
</Case>
|
|
<Case val={FileCategory.VIDEO}>
|
|
<video controls height={500}>
|
|
<source src={`data:video/${extension};base64,${base64Data}`} />
|
|
</video>
|
|
</Case>
|
|
<Case val={FileCategory.TEXT}>
|
|
<SourceCodeViewer
|
|
language={filenameToLanguage(filename)}
|
|
source={decodeGitContent(base64Data)}
|
|
/>
|
|
</Case>
|
|
<Case val={FileCategory.SUBMODULE}>
|
|
<SourceCodeViewer
|
|
language={filenameToLanguage(filename)}
|
|
source={decodeGitContent(base64Data)}
|
|
/>
|
|
</Case>
|
|
<Case val={FileCategory.SYMLINK}>
|
|
<SourceCodeViewer
|
|
language={filenameToLanguage(filename)}
|
|
source={decodeGitContent(base64Data)}
|
|
/>
|
|
</Case>
|
|
</Match>
|
|
</Center>
|
|
</Falsy>
|
|
</Match>
|
|
</Falsy>
|
|
</Match>
|
|
</Truthy>
|
|
</Match>
|
|
</Container>
|
|
</Render>
|
|
</Container>
|
|
</Layout.Vertical>
|
|
</Container>
|
|
)
|
|
},
|
|
{
|
|
id: FileSection.BLAME,
|
|
title: getString('blame'),
|
|
panel: (
|
|
<Container className={css.gitBlame}>
|
|
{[resourcePath + gitRef].map(key => (
|
|
<GitBlame repoMetadata={repoMetadata} resourcePath={resourcePath} gitRef={gitRef} key={key} />
|
|
))}
|
|
</Container>
|
|
)
|
|
},
|
|
{
|
|
id: FileSection.HISTORY,
|
|
title: getString('history'),
|
|
panel: (
|
|
<>
|
|
{repoMetadata && !!commits?.commits?.length && (
|
|
<>
|
|
<Container className={css.gitCommit}>
|
|
<CommitsView
|
|
commits={commits.commits}
|
|
repoMetadata={repoMetadata}
|
|
emptyTitle={getString('noCommits')}
|
|
emptyMessage={getString('noCommitsMessage')}
|
|
showFileHistoryIcons={true}
|
|
resourcePath={resourcePath}
|
|
setActiveTab={setActiveTab}
|
|
/>
|
|
</Container>
|
|
<Container className={css.gitHistory}>
|
|
{commits?.rename_details && repoMetadata ? (
|
|
<RenameContentHistory
|
|
rename_details={commits.rename_details}
|
|
repoMetadata={repoMetadata}
|
|
setActiveTab={setActiveTab}
|
|
/>
|
|
) : null}
|
|
</Container>
|
|
</>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
]}
|
|
/>
|
|
</Container>
|
|
)
|
|
}
|
|
|
|
const Center: React.FC = ({ children }) => (
|
|
<Container flex={{ align: 'center-center' }} style={{ width: '100%', height: '100%' }} padding={{ right: 'large' }}>
|
|
{children}
|
|
</Container>
|
|
)
|