feat: [CODE-215]: Move GitBlame to a separate tab + UI tweaks per Design

This commit is contained in:
“tan-nhu” 2023-04-25 17:46:40 -07:00
parent 345540594f
commit dc15e6bb18
23 changed files with 375 additions and 158 deletions

View File

@ -45,7 +45,7 @@ interface FormData {
newBranch?: string
}
interface CommitModalButtonProps extends Omit<ButtonProps, 'onClick' | 'onSubmit'>, Pick<GitInfoProps, 'repoMetadata'> {
interface CommitModalProps extends Pick<GitInfoProps, 'repoMetadata'> {
commitAction: GitCommitAction
gitRef: string
resourcePath: string
@ -57,7 +57,7 @@ interface CommitModalButtonProps extends Omit<ButtonProps, 'onClick' | 'onSubmit
onSuccess: (data: RepoCommitFilesResponse, newBranch?: string) => void
}
export const CommitModalButton: React.FC<CommitModalButtonProps> = ({
export function useCommitModal({
repoMetadata,
commitAction,
gitRef,
@ -67,9 +67,8 @@ export const CommitModalButton: React.FC<CommitModalButtonProps> = ({
disableBranchCreation = false,
payload = '',
sha,
onSuccess,
...props
}) => {
onSuccess
}: CommitModalProps) {
const ModalComponent: React.FC = () => {
const { getString } = useStrings()
const [targetBranchOption, setTargetBranchOption] = useState(CommitToGitRefOption.DIRECTLY)
@ -229,5 +228,46 @@ export const CommitModalButton: React.FC<CommitModalButtonProps> = ({
const [openModal, hideModal] = useModalHook(ModalComponent, [onSuccess, gitRef, resourcePath, commitTitlePlaceHolder])
return [openModal, hideModal]
}
interface CommitModalButtonProps extends Omit<ButtonProps, 'onClick' | 'onSubmit'>, Pick<GitInfoProps, 'repoMetadata'> {
commitAction: GitCommitAction
gitRef: string
resourcePath: string
commitTitlePlaceHolder: string
disableBranchCreation?: boolean
oldResourcePath?: string
payload?: string
sha?: string
onSuccess: (data: RepoCommitFilesResponse, newBranch?: string) => void
}
export const CommitModalButton: React.FC<CommitModalButtonProps> = ({
repoMetadata,
commitAction,
gitRef,
resourcePath,
commitTitlePlaceHolder,
oldResourcePath,
disableBranchCreation = false,
payload = '',
sha,
onSuccess,
...props
}) => {
const [openModal] = useCommitModal({
repoMetadata,
commitAction,
gitRef,
resourcePath,
commitTitlePlaceHolder,
oldResourcePath,
disableBranchCreation,
payload,
sha,
onSuccess
})
return <Button onClick={openModal} {...props} />
}

View File

@ -1,16 +1,5 @@
import React from 'react'
import {
Button,
Container,
Color,
Layout,
FlexExpander,
Text,
FontVariation,
Avatar,
ButtonVariation,
ButtonSize
} from '@harness/uicore'
import { Container, Color, Layout, FlexExpander, Text, FontVariation, Avatar } from '@harness/uicore'
import { Link } from 'react-router-dom'
import { Render } from 'react-jsx-match'
import ReactTimeago from 'react-timeago'
@ -19,9 +8,8 @@ import type { TypesCommit } from 'services/code'
import { CommitActions } from 'components/CommitActions/CommitActions'
import { useAppContext } from 'AppContext'
import { formatDate } from 'utils/Utils'
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
import type { GitInfoProps } from 'utils/GitUtils'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { useStrings } from 'framework/strings'
import css from './LatestCommit.module.scss'
interface LatestCommitProps extends Pick<GitInfoProps, 'repoMetadata'> {
@ -60,7 +48,6 @@ export function LatestCommitForFolder({ repoMetadata, latestCommit, standaloneSt
export function LatestCommitForFile({ repoMetadata, latestCommit, standaloneStyle }: LatestCommitProps) {
const { routes } = useAppContext()
const { getString } = useStrings()
const commitURL = routes.toCODECommits({
repoPath: repoMetadata.path as string,
commitRef: latestCommit?.sha as string
@ -77,22 +64,17 @@ export function LatestCommitForFile({ repoMetadata, latestCommit, standaloneStyl
{latestCommit?.author?.identity?.name || latestCommit?.author?.identity?.email}
</Text>
<PipeSeparator height={9} />
<Link to={commitURL} className={css.commitLink}>
{latestCommit?.title}
</Link>
<PipeSeparator height={9} />
<CommitActions sha={latestCommit?.sha as string} href={commitURL} />
<PipeSeparator height={9} />
<Text font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
{getString('onDate', { date: formatDate(latestCommit?.author?.when as string) })}
{formatDate(latestCommit?.author?.when as string)}
</Text>
<FlexExpander />
<Button
size={ButtonSize.SMALL}
icon={CodeIcon.History}
text={getString('history')}
variation={ButtonVariation.PRIMARY}
/>
<CommitActions sha={latestCommit?.sha as string} href={commitURL} />
</Layout.Horizontal>
</Container>
</Render>

View File

@ -14,6 +14,14 @@
}
}
.isDark {
.icon {
svg path {
fill: var(--white) !important;
}
}
}
.icon {
color: var(--white) !important;
padding: var(--spacing-xsmall) var(--spacing-small) !important;

View File

@ -13,6 +13,7 @@ type OptionsMenuItem = React.ComponentProps<typeof Menu.Item> & {
text?: string
hasIcon?: boolean
iconName?: string
iconSize?: number
}
export interface OptionsMenuButtonProps extends ButtonProps {
@ -42,14 +43,22 @@ export const OptionsMenuButton = ({
<Menu.Item
icon={
(item as OptionsMenuItem).hasIcon ? (
<Icon size={12} className={css.icon} name={(item as OptionsMenuItem).iconName as IconName} />
<Icon
size={(item as OptionsMenuItem).iconSize || 12}
className={css.icon}
name={(item as OptionsMenuItem).iconName as IconName}
/>
) : null
}
key={(item as React.ComponentProps<typeof Menu.Item>)?.text as string}
className={cx(Classes.POPOVER_DISMISS, {
[css.danger]: (item as OptionsMenuItem).isDanger,
[css.isDark]: isDark
})}
className={cx(
Classes.POPOVER_DISMISS,
{
[css.danger]: (item as OptionsMenuItem).isDanger,
[css.isDark]: isDark
},
(item as OptionsMenuItem).className
)}
{...omit(
item as IMenuItemProps & React.AnchorHTMLAttributes<HTMLAnchorElement>,
'isDanger',

View File

@ -0,0 +1,17 @@
.btn {
--border: 1px solid var(--grey-200) !important;
--background-color: var(--white) !important;
--background-color-active: var(--white) !important;
--box-shadow: none !important;
&:active,
&:hover,
&[class*='bp3-active'] {
--border: 1px solid var(--primary-7) !important;
}
.prefix {
color: var(--grey-450) !important;
font-weight: normal;
}
}

View File

@ -0,0 +1,7 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly btn: string
readonly prefix: string
}
export default styles

View File

@ -0,0 +1,5 @@
import React from 'react'
import { Button, ButtonProps } from '@harness/uicore'
import css from './PlainButton.module.scss'
export const PlainButton: React.FC<ButtonProps> = props => <Button className={css.btn} {...props} />

View File

@ -48,7 +48,7 @@ export function RepositoryPageHeader({
</Fragment>
))}
</Layout.Horizontal>
<Container padding={{ top: 'large', bottom: 'small' }}>
<Container padding={{ top: 'small', bottom: 'small' }}>
{typeof title === 'string' ? (
<Text tag="h1" font={{ variation: FontVariation.H4 }} tooltipProps={{ dataTooltipId }}>
{title}

View File

@ -17,6 +17,7 @@ export interface StringsMap {
and: string
approve: string
ascending: string
blame: string
blameCommitLine: string
botAlerts: string
branch: string
@ -95,6 +96,7 @@ export interface StringsMap {
diff: string
disableWebhookContent: string
disableWebhookTitle: string
download: string
draft: string
edit: string
editFile: string
@ -307,6 +309,7 @@ export interface StringsMap {
viewAllBranches: string
viewAllTags: string
viewCommitDetails: string
viewRaw: string
viewed: string
webhook: string
webhookAllEventsSelected: string

View File

@ -358,13 +358,13 @@ missingPerms: 'You are missing the following permission:'
createRepoPerms: 'Create / Edit Repository'
missingPermsContent: '"{PERMS}" in project "{PROJECT}"'
repositoryName: Repository name
dangerDeleteRepo: Danger, are you sure you want to delete it?
dangerDeleteRepo: Danger, are you sure you want to delete it?
repoUpdate: Repository Updated
deleteRepoText: Are you sure you want to delete the repository '{REPONAME}'?
deleteRepoTitle: Delete the repository
resolve: Resolve
generateCloneCred: + Generate Clone Credential
generateCloneText: "Please generate clone credential if its your first time"
generateCloneText: 'Please generate clone credential if its your first time'
getMyCloneTitle: Get My Clone Credential
cloneText: Your clone credentials have been generated. Please make sure to copy and store your password somewhere safe, you won't be able to see it again.
manageApiToken: Manage Api Token
@ -372,3 +372,6 @@ userName: User Name
passwordApi: Password (API Token)
firstTimeTitle: Please generate Git Credentials if its your first time to clone the repository
manageCredText: You can also manage your git credential {URL}
blame: Blame
viewRaw: View Raw
download: Download

View File

@ -37,7 +37,6 @@
height: 24px;
margin-top: var(--spacing-8);
> svg {
display: inline-block;
margin-right: 5px;

View File

@ -105,7 +105,6 @@ export default function PullRequest() {
: PullRequestSection.CONVERSATION,
[pullRequestSection]
)
// /repos/${paramsInPath.repo_ref}/pullreq/${paramsInPath.pullreq_number}/metadata
return (
<Container className={css.main}>

View File

@ -1,4 +1,6 @@
.main {
padding: var(--spacing-large) var(--spacing-xlarge) 0 var(--spacing-xlarge) !important;
div[class*='TextInput'] {
margin-bottom: 0 !important;
margin-left: 0 !important;
@ -11,7 +13,8 @@
> div {
align-items: center;
padding-bottom: var(--spacing-xlarge) !important;
padding-bottom: var(--spacing-large) !important;
// border-bottom: 1px solid var(--grey-100);
}
.btnColorFix > span[data-icon] {

View File

@ -16,3 +16,63 @@
padding: var(--spacing-xxlarge) 0 var(--spacing-large) var(--spacing-medium) !important;
}
}
.fileContent {
padding: var(--spacing-small) var(--spacing-xlarge) var(--spacing-xlarge) var(--spacing-xlarge) !important;
}
.gitBlame {
padding: var(--spacing-xlarge) !important;
}
.tabsContainer {
background-color: var(--primary-bg) !important;
> div > div[role='tablist'] {
background-color: var(--white) !important;
padding-left: var(--spacing-large) !important;
padding-right: var(--spacing-xlarge) !important;
border-bottom: 1px solid var(--grey-200) !important;
}
> div > div[role='tabpanel'] {
margin-top: 0;
}
[aria-selected='true'] {
.tabTitle,
.tabTitle:hover {
color: var(--grey-900) !important;
font-weight: 600 !important;
}
}
.tabTitle {
font-weight: 500;
color: var(--grey-700);
display: flex;
align-items: center;
height: 24px;
margin-top: var(--spacing-8);
> svg {
display: inline-block;
margin-right: 5px;
}
.count {
margin-left: var(--spacing-small);
display: inline-block;
border-radius: 8px;
font-weight: 500;
font-size: var(--font-size-small);
color: var(--primary-7) !important;
background-color: var(--primary-1) !important;
padding: 3px 6px !important;
}
}
.tabTitle:not:first-child {
margin-left: var(--spacing-8) !important;
}
}

View File

@ -4,5 +4,10 @@ declare const styles: {
readonly container: string
readonly heading: string
readonly content: string
readonly fileContent: string
readonly gitBlame: string
readonly tabsContainer: string
readonly tabTitle: string
readonly count: string
}
export default styles

View File

@ -1,21 +1,20 @@
import React, { useMemo } from 'react'
import {
Button,
ButtonSize,
ButtonVariation,
Color,
Container,
FlexExpander,
Heading,
Layout,
useToggle,
Tabs,
Utils
} from '@harness/uicore'
import { Else, Match, Render, Truthy } from 'react-jsx-match'
import { Render } from 'react-jsx-match'
import { useHistory } from 'react-router-dom'
import { SourceCodeViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
import type { OpenapiContentInfo, RepoFileContent } from 'services/code'
import {
CodeIcon,
decodeGitContent,
findMarkdownInfo,
GitCommitAction,
@ -26,13 +25,20 @@ import {
import { filenameToLanguage } from 'utils/Utils'
import { useAppContext } from 'AppContext'
import { LatestCommitForFile } from 'components/LatestCommit/LatestCommit'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { CommitModalButton } from 'components/CommitModalButton/CommitModalButton'
import { useCommitModal } from 'components/CommitModalButton/CommitModalButton'
import { useStrings } from 'framework/strings'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { PlainButton } from 'components/PlainButton/PlainButton'
import { Readme } from '../FolderContent/Readme'
import { GitBlame } from './GitBlame'
import css from './FileContent.module.scss'
enum FileSection {
CONTENT = 'content',
BLAME = 'blame',
HISTORY = 'history'
}
export function FileContent({
repoMetadata,
gitRef,
@ -42,110 +48,164 @@ export function FileContent({
const { routes } = useAppContext()
const { getString } = useStrings()
const history = useHistory()
const [showGitBlame, toggleGitBlame] = useToggle(false)
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
})
)
}
}
})
return (
<Layout.Vertical spacing="small">
<LatestCommitForFile repoMetadata={repoMetadata} latestCommit={resourceContent.latest_commit} standaloneStyle />
<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">
<Button
variation={ButtonVariation.ICON}
icon={CodeIcon.Edit}
tooltip={isRefATag(gitRef) ? getString('editNotAllowed') : getString('edit')}
tooltipProps={{ isDark: true }}
disabled={isRefATag(gitRef)}
onClick={() => {
history.push(
routes.toCODEFileEdit({
repoPath: repoMetadata.path as string,
gitRef,
resourcePath
})
)
}}
/>
<Button
variation={ButtonVariation.ICON}
tooltip={getString('copy')}
icon={CodeIcon.Copy}
tooltipProps={{ isDark: true }}
onClick={() => Utils.copy(content)}
/>
<CommitModalButton
variation={ButtonVariation.ICON}
icon={CodeIcon.Delete}
disabled={isRefATag(gitRef)}
tooltip={getString(isRefATag(gitRef) ? 'deleteNotAllowed' : 'delete')}
tooltipProps={{ isDark: true }}
repoMetadata={repoMetadata}
gitRef={gitRef}
resourcePath={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
})
)
}
}}
/>
<PipeSeparator />
<Container padding={{ left: 'small', right: 'xsmall' }}>
<Button
variation={ButtonVariation.SECONDARY}
text={showGitBlame ? 'View File' : 'Blame'}
onClick={toggleGitBlame}
/>
</Container>
</Layout.Horizontal>
</Layout.Horizontal>
<Render when={(resourceContent?.content as RepoFileContent)?.data}>
<Container className={css.content}>
<Match expr={showGitBlame}>
<Truthy>
<GitBlame repoMetadata={repoMetadata} resourcePath={resourcePath} />
</Truthy>
<Else>
<Render when={!markdownInfo}>
<SourceCodeViewer language={filenameToLanguage(resourceContent?.name)} source={content} />
</Render>
<Render when={markdownInfo}>
<Readme
metadata={repoMetadata}
readmeInfo={markdownInfo as OpenapiContentInfo}
contentOnly
maxWidth="calc(100vw - 346px)"
gitRef={gitRef}
<Container className={css.tabsContainer}>
<Tabs
id="fileTabs"
defaultSelectedTabId={FileSection.CONTENT}
large={false}
tabList={[
{
id: FileSection.CONTENT,
title: getString('content'),
panel: (
<Container className={css.fileContent}>
<Layout.Vertical spacing="small">
<LatestCommitForFile
repoMetadata={repoMetadata}
latestCommit={resourceContent.latest_commit}
standaloneStyle
/>
</Render>
</Else>
</Match>
</Container>
</Render>
</Container>
</Layout.Vertical>
<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('edit')}
icon="code-edit"
tooltip={isRefATag(gitRef) ? getString('editNotAllowed') : undefined}
tooltipProps={{ isDark: true }}
disabled={isRefATag(gitRef)}
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: () => {
window.open(
`/code/api/v1/repos/${
repoMetadata?.path
}/+/raw/${resourcePath}?${`git_ref=${gitRef}`}`,
'_blank'
)
}
},
'-',
{
hasIcon: true,
iconName: 'cloud-download',
text: getString('download'),
download: resourceContent?.name || 'download',
href: `/code/api/v1/repos/${
repoMetadata?.path
}/+/raw/${resourcePath}?${`git_ref=${gitRef}`}`
},
{
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}>
<Container className={css.content}>
<Render when={!markdownInfo}>
<SourceCodeViewer language={filenameToLanguage(resourceContent?.name)} source={content} />
</Render>
<Render when={markdownInfo}>
<Readme
metadata={repoMetadata}
readmeInfo={markdownInfo as OpenapiContentInfo}
contentOnly
maxWidth="calc(100vw - 346px)"
gitRef={gitRef}
/>
</Render>
</Container>
</Render>
</Container>
</Layout.Vertical>
</Container>
)
},
{
id: FileSection.BLAME,
title: getString('blame'),
panel: (
<Container className={css.gitBlame}>
<GitBlame repoMetadata={repoMetadata} resourcePath={resourcePath} />
</Container>
)
}
]}
/>
</Container>
)
}

View File

@ -5,9 +5,16 @@
:global {
.cm-editor {
border: none !important;
.cm-scroller {
padding: 0 !important;
}
.cm-content {
padding: 0;
}
.cm-line {
&,
* {
@ -28,8 +35,7 @@
.gitBlame {
--code-editor-border-color: var(--grey-200);
padding: 0 var(--spacing-xlarge) 0 var(--spacing-small) !important;
// padding: 0 var(--spacing-xlarge) 0 var(--spacing-small) !important;
:global {
.cm-gutter {

View File

@ -240,6 +240,7 @@ const GitBlameRenderer = React.memo(function GitBlameSourceViewer({
className={css.main}
onViewUpdate={onViewUpdate}
extensions={extensions.of([])}
maxHeight="auto"
/>
)
})

View File

@ -1,4 +1,6 @@
.folderContent {
padding: 0 var(--spacing-xlarge) var(--spacing-xlarge) var(--spacing-xlarge) !important;
.table {
background-color: var(--white) !important;

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Container, Color, Layout, Button, FlexExpander, ButtonVariation, Heading, Icon } from '@harness/uicore'
import { Container, Color, Layout, FlexExpander, ButtonVariation, Heading, Icon, ButtonSize } from '@harness/uicore'
import { Render } from 'react-jsx-match'
import { useHistory } from 'react-router-dom'
import { useGet } from 'restful-react'
@ -7,8 +7,10 @@ import cx from 'classnames'
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
import { useAppContext } from 'AppContext'
import type { OpenapiContentInfo, OpenapiGetContentOutput, RepoFileContent, TypesRepository } from 'services/code'
import { useStrings } from 'framework/strings'
import { useShowRequestError } from 'hooks/useShowRequestError'
import { CodeIcon, decodeGitContent } from 'utils/GitUtils'
import { decodeGitContent } from 'utils/GitUtils'
import { PlainButton } from 'components/PlainButton/PlainButton'
import css from './Readme.module.scss'
interface FolderContentProps {
@ -20,6 +22,7 @@ interface FolderContentProps {
}
function ReadmeViewer({ metadata, gitRef, readmeInfo, contentOnly, maxWidth }: FolderContentProps) {
const { getString } = useStrings()
const history = useHistory()
const { routes } = useAppContext()
@ -43,9 +46,13 @@ function ReadmeViewer({ metadata, gitRef, readmeInfo, contentOnly, maxWidth }: F
<Heading level={5}>{readmeInfo.name}</Heading>
<FlexExpander />
{loading && <Icon name="spinner" color={Color.PRIMARY_7} />}
<Button
variation={ButtonVariation.ICON}
icon={CodeIcon.Edit}
<PlainButton
withoutCurrentColor
size={ButtonSize.SMALL}
variation={ButtonVariation.TERTIARY}
iconProps={{ size: 16 }}
text={getString('edit')}
icon="code-edit"
onClick={() => {
history.push(
routes.toCODEFileEdit({

View File

@ -1,3 +1,5 @@
.resourceContent {
background-color: var(--primary-bg);
flex-grow: 1;
display: flex;
flex-direction: column;
}

View File

@ -13,7 +13,7 @@ export function RepositoryContent({
resourceContent
}: Pick<GitInfoProps, 'repoMetadata' | 'gitRef' | 'resourcePath' | 'resourceContent'>) {
return (
<Container padding="xlarge" className={css.resourceContent}>
<Container className={css.resourceContent}>
<ContentHeader
repoMetadata={repoMetadata}
gitRef={gitRef}

View File

@ -27,18 +27,17 @@ export function permissionProps(permResult: { disabled: boolean; tooltip: JSX.El
}
export function generateAlphaNumericHash(length: number) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
let result = ''
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
return result;
return result
}
export const dayAgoInMS = 86400000
export const getErrorMessage = (error: Unknown): string =>