Refactor: Use types from SCM Service (#48)

This commit is contained in:
Tan Nhu 2022-10-27 00:57:20 -07:00 committed by GitHub
parent c8b978a6ed
commit c21b860e46
28 changed files with 130 additions and 1168 deletions

View File

@ -25,13 +25,12 @@ const App: React.FC<AppProps> = React.memo(function App({
lang = 'en',
on401 = handle401,
children,
hooks = {},
components = {}
hooks
}: AppProps) {
const [strings, setStrings] = useState<LanguageRecord>()
const token = useAPIToken()
const getRequestOptions = useCallback(
(): Partial<RequestInit> => buildResfulReactRequestOptions(hooks.useGetToken?.() || token),
(): Partial<RequestInit> => buildResfulReactRequestOptions(hooks?.useGetToken?.() || token),
[token, hooks]
)
@ -42,7 +41,7 @@ const App: React.FC<AppProps> = React.memo(function App({
return strings ? (
<StringsContextProvider initialStrings={strings}>
<AppErrorBoundary>
<AppContextProvider value={{ standalone, space, routes, lang, on401, hooks, components }}>
<AppContextProvider value={{ standalone, space, routes, lang, on401, hooks }}>
<RestfulProvider
base={standalone ? '/' : '/scm'}
requestOptions={getRequestOptions}

View File

@ -11,8 +11,7 @@ const AppContext = React.createContext<AppContextProps>({
standalone: true,
setAppContext: noop,
routes,
hooks: {},
components: {}
hooks: {}
})
export const AppContextProvider: React.FC<{ value: AppProps }> = React.memo(function AppContextProvider({

View File

@ -1,8 +1,6 @@
import type React from 'react'
import type * as History from 'history'
import type { PermissionOptionsMenuButtonProps } from 'components/Permissions/PermissionsOptionsMenuButton'
import type { Unknown } from 'utils/Utils'
import type { SCMRoutes } from 'RouteDefinitions'
import type { Unknown } from 'utils/Utils'
import type { LangLocale } from './framework/strings/languageLoader'
/**
@ -42,63 +40,7 @@ export interface AppProps {
on401?: () => void
/** React Hooks that Harness Platform passes down. Note: Pass only hooks that your app need */
hooks: Partial<AppPropsHook>
/** React Components that Harness Platform passes down. Note: Pass only components that your app need */
components: Partial<AppPropsComponent>
}
/**
* AppPathProps defines all possible URL parameters that application accepts.
*/
export interface AppPathProps {
accountId?: string
orgIdentifier?: string
projectIdentifier?: string
module?: string
policyIdentifier?: string
policySetIdentifier?: string
evaluationId?: string
repo?: string
branch?: string
}
/**
* AppPropsHook defines a collection of React Hooks that application receives from
* Platform integration.
*/
export interface AppPropsHook {
usePermission(permissionRequest: Unknown, deps?: Array<Unknown>): Array<boolean>
useGetSchemaYaml(params: Unknown, deps?: Array<Unknown>): Record<string, Unknown>
useGetToken(): Unknown
useAppStore(): Unknown
useGitSyncStore(): Unknown
useSaveToGitDialog(props: { onSuccess: Unknown; onClose: Unknown; onProgessOverlayClose: Unknown }): Unknown
useGetListOfBranchesWithStatus(props: Unknown): Unknown
useAnyEnterpriseLicense(): boolean
useCurrentEnterpriseLicense(): boolean
useLicenseStore(): Unknown
} // eslint-disable-line @typescript-eslint/no-empty-interface
/**
* AppPropsComponent defines a collection of React Components that application receives from
* Platform integration.
*/
export interface AppPropsComponent {
NGBreadcrumbs: React.FC
RbacButton: React.FC
RbacOptionsMenuButton: React.FC<PermissionOptionsMenuButtonProps>
GitSyncStoreProvider: React.FC
GitContextForm: React.FC<Unknown>
NavigationCheck: React.FC<{
when?: boolean
textProps?: {
contentText?: string
titleText?: string
confirmButtonText?: string
cancelButtonText?: string
}
navigate: (path: string) => void
shouldBlockNavigation?: (location: History.Location) => boolean
hooks: Partial<{
useGetToken: Unknown
}>
}

View File

@ -6,38 +6,16 @@ export interface SCMPathProps {
}
export interface SCMQueryProps {
branch?: string
filePath?: string
query?: string
}
export const pathProps: Readonly<Required<SCMPathProps>> = {
space: ':space',
repoName: ':repoName',
gitRef: ':gitRef*',
resourcePath: ':resourcePath'
resourcePath: ':resourcePath*'
}
// function withAccountId<T>(fn: (args: T) => string) {
// return (params: T & { accountId: string }): string => {
// const path = fn(params)
// return `/account/${params.accountId}/${path.replace(/^\//, '')}`
// }
// }
// function withQueryParams(path: string, params: Record<string, string>): string {
// return Object.entries(params).every(([key, value]) => ':' + key === value)
// ? path
// : [
// path,
// Object.entries(params)
// .reduce((value, entry) => {
// value.push(entry.join('='))
// return value
// }, [] as string[])
// .join('&')
// ].join('?')
// }
export interface SCMRoutes {
toSignIn: () => string
toSignUp: () => string
@ -56,22 +34,7 @@ export interface SCMRoutes {
export const routes: SCMRoutes = {
toSignIn: (): string => '/signin',
toSignUp: (): string => '/signup',
toSCMRepositoriesListing: ({ space }: { space: string }) => {
const [accountId, orgIdentifier, projectIdentifier] = space.split('/')
return `/account/${accountId}/code/${orgIdentifier}/${projectIdentifier}`
},
toSCMRepository: ({
repoPath,
gitRef,
resourcePath
}: {
repoPath: string
gitRef?: string
resourcePath?: string
}) => {
const [accountId, orgIdentifier, projectIdentifier, repoName] = repoPath.split('/')
return `/account/${accountId}/code/${orgIdentifier}/${projectIdentifier}/${repoName}${gitRef ? '/' + gitRef : ''}${
resourcePath ? '/~/' + resourcePath : ''
}`
}
toSCMRepositoriesListing: ({ space }: { space: string }) => `/${space}`,
toSCMRepository: ({ repoPath, gitRef, resourcePath }: { repoPath: string; gitRef?: string; resourcePath?: string }) =>
`/${repoPath}/${gitRef ? '/' + gitRef : ''}${resourcePath ? '/~/' + resourcePath : ''}`
}

View File

@ -16,10 +16,21 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
<Route path={routes.toSignUp()}>
<SignUp />
</Route>
<Route path={routes.toSCMRepositoriesListing(pathProps)} exact>
<Route path={routes.toSCMRepositoriesListing({ space: pathProps.space })} exact>
<RepositoriesListing />
</Route>
<Route path={routes.toSCMRepository(pathProps)}>
<Route
path={[
routes.toSCMRepository({
repoPath: `${pathProps.space}/${pathProps.repoName}`,
gitRef: pathProps.gitRef,
resourcePath: pathProps.resourcePath
}),
routes.toSCMRepository({
repoPath: `${pathProps.space}/${pathProps.repoName}`,
gitRef: pathProps.gitRef
})
]}>
<Repository />
</Route>
</Switch>

View File

@ -1,5 +1,6 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { routes } from 'RouteDefinitions'
import App from './App'
import './bootstrap.scss'
@ -8,4 +9,4 @@ import './bootstrap.scss'
// Also being used in when generating proper URLs inside the app.
window.STRIP_SCM_PREFIX = true
ReactDOM.render(<App standalone apiToken="" hooks={{}} components={{}} />, document.getElementById('react-root'))
ReactDOM.render(<App standalone routes={routes} hooks={{}} />, document.getElementById('react-root'))

View File

@ -1,61 +0,0 @@
.main {
max-width: 785px;
margin-bottom: var(--spacing-medium) !important;
:global(.bp3-input) {
&:disabled {
background-color: var(--grey-100);
}
}
div[class*='collapse'] {
margin-left: 0;
}
& div[class*='Collapse'] {
border-top: none;
padding: 0;
// spacing for adjacent collapsible
+ div[class*='Collapse'][class*='main'] {
margin-top: var(--spacing-medium);
}
div[class*='CollapseHeader'] {
padding-top: 0 !important;
}
span[data-icon='main-chevron-down'] {
color: var(--primary-7);
}
}
textarea {
min-height: 50px;
resize: vertical;
}
:global {
.TextInput--main {
margin: 0;
.bp3-form-group {
margin: 0;
}
margin-bottom: var(--spacing-medium);
}
}
}
.editOpen {
cursor: pointer;
margin-left: 8px !important;
position: relative;
top: -2px;
&:hover {
color: var(--primary-7) !important;
}
}
.descriptionLabel {
margin-bottom: var(--spacing-xsmall) !important;
display: flex !important;
align-items: center;
}

View File

@ -1,8 +0,0 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly main: string
readonly editOpen: string
readonly descriptionLabel: string
}
export default styles

View File

@ -1,206 +0,0 @@
import React, { useState } from 'react'
import { Container, FormInput, Icon, Label, DataTooltipInterface, HarnessDocTooltip } from '@harness/uicore'
import type { InputWithIdentifierProps } from '@harness/uicore/dist/components/InputWithIdentifier/InputWithIdentifier'
import { isEmpty } from 'lodash-es'
import { Classes, IInputGroupProps, ITagInputProps } from '@blueprintjs/core'
import cx from 'classnames'
import type { FormikProps } from 'formik'
import { useStrings } from 'framework/strings'
import type { Unknown } from 'utils/Utils'
import type {
DescriptionComponentProps,
DescriptionProps,
NameIdDescriptionProps,
NameIdDescriptionTagsDeprecatedProps,
TagsComponentProps,
TagsDeprecatedComponentProps
} from './NameIdDescriptionTagsConstants'
import css from './NameIdDescriptionTags.module.scss'
export interface NameIdDescriptionTagsProps {
identifierProps?: Omit<InputWithIdentifierProps, 'formik'>
inputGroupProps?: IInputGroupProps
descriptionProps?: DescriptionProps
tagsProps?: Partial<ITagInputProps> & {
isOption?: boolean
}
formikProps: FormikProps<Unknown>
className?: string
tooltipProps?: DataTooltipInterface
}
interface NameIdProps {
nameLabel?: string // Strong default preference for "Name" vs. Contextual Name (e.g. "Service Name") unless approved otherwise
namePlaceholder?: string
identifierProps?: Omit<InputWithIdentifierProps, 'formik'>
inputGroupProps?: IInputGroupProps
dataTooltipId?: string
}
export const NameId = (props: NameIdProps): JSX.Element => {
const { getString } = useStrings()
const { identifierProps, nameLabel = getString('name'), inputGroupProps = {} } = props
const newInputGroupProps = { placeholder: getString('namePlaceholder'), ...inputGroupProps }
return (
<FormInput.InputWithIdentifier inputLabel={nameLabel} inputGroupProps={newInputGroupProps} {...identifierProps} />
)
}
export const Description = (props: DescriptionComponentProps): JSX.Element => {
const { descriptionProps = {}, hasValue, disabled = false } = props
const { isOptional = true, ...restDescriptionProps } = descriptionProps
const { getString } = useStrings()
const [isDescriptionOpen, setDescriptionOpen] = useState<boolean>(hasValue || false)
const [isDescriptionFocus, setDescriptionFocus] = useState<boolean>(false)
return (
<Container style={{ marginBottom: isDescriptionOpen ? '0' : 'var(--spacing-medium)' }}>
<Label className={cx(Classes.LABEL, css.descriptionLabel)} data-tooltip-id={props.dataTooltipId}>
{isOptional ? getString('optionalField', { name: getString('description') }) : getString('description')}
{props.dataTooltipId ? <HarnessDocTooltip useStandAlone={true} tooltipId={props.dataTooltipId} /> : null}
{!isDescriptionOpen && (
<Icon
className={css.editOpen}
data-name="edit"
data-testid="description-edit"
size={12}
name="Edit"
onClick={() => {
setDescriptionOpen(true)
setDescriptionFocus(true)
}}
/>
)}
</Label>
{isDescriptionOpen && (
<FormInput.TextArea
data-name="description"
disabled={disabled}
autoFocus={isDescriptionFocus}
name="description"
placeholder={getString('descriptionPlaceholder')}
{...restDescriptionProps}
/>
)}
</Container>
)
}
export const Tags = (props: TagsComponentProps): JSX.Element => {
const { tagsProps, hasValue, isOptional = true } = props
const { getString } = useStrings()
const [isTagsOpen, setTagsOpen] = useState<boolean>(hasValue || false)
return (
<Container>
<Label className={cx(Classes.LABEL, css.descriptionLabel)} data-tooltip-id={props.dataTooltipId}>
{isOptional ? getString('optionalField', { name: getString('tagsLabel') }) : getString('tagsLabel')}
{props.dataTooltipId ? <HarnessDocTooltip useStandAlone={true} tooltipId={props.dataTooltipId} /> : null}
{!isTagsOpen && (
<Icon
className={css.editOpen}
data-name="edit"
data-testid="tags-edit"
size={12}
name="Edit"
onClick={() => {
setTagsOpen(true)
}}
/>
)}
</Label>
{isTagsOpen && <FormInput.KVTagInput name="tags" tagsProps={tagsProps} />}
</Container>
)
}
function TagsDeprecated(props: TagsDeprecatedComponentProps): JSX.Element {
const { hasValue } = props
const { getString } = useStrings()
const [isTagsOpen, setTagsOpen] = useState<boolean>(hasValue || false)
return (
<Container>
<Label className={cx(Classes.LABEL, css.descriptionLabel)}>
{getString('tagsLabel')}
{!isTagsOpen && (
<Icon
className={css.editOpen}
data-name="Edit"
size={12}
name="edit"
onClick={() => {
setTagsOpen(true)
}}
/>
)}
</Label>
{isTagsOpen && (
<FormInput.TagInput
name="tags"
labelFor={name => (typeof name === 'string' ? name : '')}
itemFromNewTag={newTag => newTag}
items={[]}
tagInputProps={{
noInputBorder: true,
openOnKeyDown: false,
showAddTagButton: true,
showClearAllButton: true,
allowNewTag: true
}}
/>
)}
</Container>
)
}
export function NameIdDescriptionTags(props: NameIdDescriptionTagsProps): JSX.Element {
const { getString } = useStrings()
const { className, identifierProps, descriptionProps, formikProps, inputGroupProps = {}, tooltipProps } = props
const newInputGroupProps = { placeholder: getString('namePlaceholder'), ...inputGroupProps }
return (
<Container className={cx(css.main, className)}>
<NameId identifierProps={identifierProps} inputGroupProps={newInputGroupProps} />
<Description
descriptionProps={descriptionProps}
hasValue={!!formikProps?.values.description}
dataTooltipId={tooltipProps?.dataTooltipId ? `${tooltipProps.dataTooltipId}_description` : undefined}
/>
</Container>
)
}
// Requires verification with existing tags
export function NameIdDescriptionTagsDeprecated<T>(props: NameIdDescriptionTagsDeprecatedProps<T>): JSX.Element {
const { className, identifierProps, descriptionProps, formikProps } = props
return (
<Container className={cx(css.main, className)}>
<NameId identifierProps={identifierProps} />
<Description descriptionProps={descriptionProps} hasValue={!!formikProps?.values.description} />
<TagsDeprecated hasValue={!isEmpty(formikProps?.values.tags)} />
</Container>
)
}
export function NameIdDescription(props: NameIdDescriptionProps): JSX.Element {
const { getString } = useStrings()
const { className, identifierProps, descriptionProps, formikProps, inputGroupProps = {} } = props
const newInputGroupProps = { placeholder: getString('namePlaceholder'), ...inputGroupProps }
return (
<Container className={cx(css.main, className)}>
<NameId identifierProps={identifierProps} inputGroupProps={newInputGroupProps} />
<Description descriptionProps={descriptionProps} hasValue={!!formikProps?.values.description} />
</Container>
)
}
export function DescriptionTags(props: Omit<NameIdDescriptionTagsProps, 'identifierProps'>): JSX.Element {
const { className, descriptionProps, tagsProps, formikProps } = props
return (
<Container className={cx(css.main, className)}>
<Description descriptionProps={descriptionProps} hasValue={!!formikProps?.values.description} />
<Tags tagsProps={tagsProps} isOptional={tagsProps?.isOption} hasValue={!isEmpty(formikProps?.values.tags)} />
</Container>
)
}

View File

@ -1,48 +0,0 @@
import type { TagInputProps } from '@harness/uicore'
import type { ITagInputProps, IInputGroupProps } from '@blueprintjs/core'
import type { InputWithIdentifierProps } from '@harness/uicore/dist/components/InputWithIdentifier/InputWithIdentifier'
import type { FormikProps } from 'formik'
import type { Unknown } from 'utils/Utils'
export interface DescriptionProps {
placeholder?: string
isOptional?: boolean
disabled?: boolean
}
export interface DescriptionComponentProps {
descriptionProps?: DescriptionProps
hasValue?: boolean
disabled?: boolean
dataTooltipId?: string
}
export interface TagsProps {
className?: string
}
export interface TagsComponentProps {
tagsProps?: Partial<ITagInputProps>
hasValue?: boolean
isOptional?: boolean
dataTooltipId?: string
}
export interface TagsDeprecatedComponentProps {
hasValue?: boolean
}
export interface NameIdDescriptionTagsDeprecatedProps<T> {
identifierProps?: Omit<InputWithIdentifierProps, 'formik'>
descriptionProps?: DescriptionProps
tagInputProps?: TagInputProps<T>
formikProps: FormikProps<Unknown>
className?: string
}
export interface NameIdDescriptionProps {
identifierProps?: Omit<InputWithIdentifierProps, 'formik'>
inputGroupProps?: IInputGroupProps
descriptionProps?: DescriptionProps
className?: string
formikProps: Omit<FormikProps<Unknown>, 'tags'>
}

View File

@ -37,8 +37,8 @@ import {
SUGGESTED_BRANCH_NAMES,
Unknown
} from 'utils/Utils'
import type { RepositoryDTO, CreateRepositoryBody } from 'types/SCMTypes'
import { isGitBranchNameValid } from 'utils/GitUtils'
import type { TypesRepository, OpenapiCreateRepositoryRequest } from 'services/scm'
import { useAppContext } from 'AppContext'
import css from './NewRepoModalButton.module.scss'
@ -72,7 +72,7 @@ export interface NewRepoModalButtonProps extends Omit<ButtonProps, 'onClick' | '
modalTitle: string
submitButtonTitle?: string
cancelButtonTitle?: string
onSubmit: (data: RepositoryDTO) => void
onSubmit: (data: TypesRepository) => void
}
export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
@ -88,7 +88,7 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
const { getString } = useStrings()
const [branchName, setBranchName] = useState(DEFAULT_BRANCH_NAME)
const { showError } = useToaster()
const { mutate: createRepo, loading: submitLoading } = useMutate<RepositoryDTO>({
const { mutate: createRepo, loading: submitLoading } = useMutate<TypesRepository>({
verb: 'POST',
path: `/api/v1/repos?spacePath=${space}`,
queryParams: {
@ -129,7 +129,7 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
pathName: get(formData, 'name', '').trim(),
readme: get(formData, 'addReadme', false),
spaceId: standalone ? space : 0
} as CreateRepositoryBody)
} as OpenapiCreateRepositoryRequest)
.then(response => {
hideModal()
onSubmit(response)

View File

@ -1,17 +0,0 @@
import React from 'react'
import { Button, ButtonProps } from '@harness/uicore'
import { useAppContext } from 'AppContext'
import type { Unknown } from 'utils/Utils'
interface PermissionButtonProps extends ButtonProps {
permission?: Unknown
}
export const PermissionsButton: React.FC<PermissionButtonProps> = (props: PermissionButtonProps) => {
const {
components: { RbacButton }
} = useAppContext()
const { permission, ...buttonProps } = props
return RbacButton ? <RbacButton permission={permission} {...props} /> : <Button {...buttonProps} />
}

View File

@ -1,23 +0,0 @@
import React, { AnchorHTMLAttributes, ReactElement } from 'react'
import type { IMenuItemProps } from '@blueprintjs/core'
import { OptionsMenuButton, OptionsMenuButtonProps } from 'components/OptionsMenuButton/OptionsMenuButton'
import { useAppContext } from 'AppContext'
import type { Unknown } from 'utils/Utils'
type Item = ((IMenuItemProps | PermissionsMenuItemProps) & AnchorHTMLAttributes<HTMLAnchorElement>) | '-'
interface PermissionsMenuItemProps extends IMenuItemProps {
permission?: Unknown
}
export interface PermissionOptionsMenuButtonProps extends OptionsMenuButtonProps {
items: Item[]
}
export const PermissionsOptionsMenuButton = (props: PermissionOptionsMenuButtonProps): ReactElement => {
const {
components: { RbacOptionsMenuButton }
} = useAppContext()
return RbacOptionsMenuButton ? <RbacOptionsMenuButton {...props} /> : <OptionsMenuButton {...props} />
}

View File

@ -1,15 +0,0 @@
.banner {
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
&.expiryCountdown {
background: var(--orange-50) !important;
}
&.expired {
background: var(--red-50) !important;
}
.bannerIcon {
margin-right: var(--spacing-large) !important;
}
}

View File

@ -1,9 +0,0 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly banner: string
readonly expiryCountdown: string
readonly expired: string
readonly bannerIcon: string
}
export default styles

View File

@ -1,53 +0,0 @@
import React, { ReactElement } from 'react'
import cx from 'classnames'
import moment from 'moment'
import { Container, Icon, Text } from '@harness/uicore'
import { Color } from '@harness/design-system'
import { useGetTrialInfo } from 'utils/Utils'
import { useStrings } from 'framework/strings'
import css from './TrialBanner.module.scss'
const TrialBanner = (): ReactElement => {
const trialInfo = useGetTrialInfo()
const { getString } = useStrings()
if (!trialInfo) return <></>
const { expiryTime } = trialInfo
const time = moment(trialInfo.expiryTime)
const days = Math.round(time.diff(moment.now(), 'days', true))
const expiryDate = time.format('DD MMM YYYY')
const isExpired = expiryTime !== -1 && days < 0
const expiredDays = Math.abs(days)
const expiryMessage = isExpired
? getString('banner.expired', {
days: expiredDays
})
: getString('banner.expiryCountdown', {
days
})
const bannerMessage = `Harness Policy Engine trial ${expiryMessage} on ${expiryDate}`
const bannerClassnames = cx(css.banner, isExpired ? css.expired : css.expiryCountdown)
const color = isExpired ? Color.RED_700 : Color.ORANGE_700
return (
<Container
padding="small"
intent="warning"
flex={{
justifyContent: 'start'
}}
className={bannerClassnames}
font={{
align: 'center'
}}>
<Icon name={'warning-sign'} size={15} className={css.bannerIcon} color={color} />
<Text color={color}>{bannerMessage}</Text>
</Container>
)
}
export default TrialBanner

20
web/src/global.d.ts vendored
View File

@ -54,14 +54,12 @@ declare module '*.scss'
type RequireField<T, K extends keyof T> = T & Required<Pick<T, K>>
type Module =
| 'ci'
| 'cd'
| 'cf'
| 'cv'
| 'ce'
| ':module(ci)'
| ':module(cd)'
| ':module(cf)'
| ':module'
| ':module(cv)'
declare module 'lang-map' {
const languages: { languages: (name: string) => string[] }
export default languages
}
declare module 'react-join' {
const ReactJoin: React.FC<{ separator: JSX.Element }>
export default ReactJoin
}

View File

@ -10,59 +10,59 @@ import {
Text,
Color,
Pagination,
Icon
Icon,
TextInput
} from '@harness/uicore'
import type { CellProps, Column } from 'react-table'
import cx from 'classnames'
import { useGet } from 'restful-react'
import { useHistory } from 'react-router-dom'
import { useHistory, useParams } from 'react-router-dom'
import { useStrings } from 'framework/strings'
import { formatDate, getErrorMessage, X_PER_PAGE, X_TOTAL, X_TOTAL_PAGES } from 'utils/Utils'
import { formatDate, getErrorMessage, LIST_FETCHING_PER_PAGE, X_PER_PAGE, X_TOTAL, X_TOTAL_PAGES } from 'utils/Utils'
import { NewRepoModalButton } from 'components/NewRepoModalButton/NewRepoModalButton'
import type { RepositoryDTO } from 'types/SCMTypes'
import type { TypesRepository } from 'services/scm'
import type { SCMPathProps } from 'RouteDefinitions'
import { useAppContext } from 'AppContext'
import emptyStateImage from './images/empty-state.svg'
import css from './RepositoriesListing.module.scss'
// import { useListRepos } from 'services/scm'
export default function Repos(): JSX.Element {
export default function RepositoriesListing(): JSX.Element {
const { getString } = useStrings()
const history = useHistory()
const ref = useRef<HTMLDivElement>(null)
const rowContainerRef = useRef<HTMLDivElement>(null)
const [nameTextWidth, setNameTextWidth] = useState(600)
const { space = '', routes } = useAppContext() // TODO: Proper handling `space` for standalone version
const {
data: repositories,
error,
loading,
refetch,
response
} = useGet<RepositoryDTO[]>({
path: `/api/v1/spaces/${space}/+/repos`
})
// DOES NOT WORK !!! API URL IS NOT CONSTRUCTED PROPERLY
// const r = useListRepos({
// spaceRef: space
// })
// console.log({ r })
const [pageIndex, setPageIndex] = useState(0)
const params = useParams<SCMPathProps>()
const [query, setQuery] = useState<string | undefined>()
const { space = params.space || '', routes } = useAppContext()
const path = useMemo(
() =>
`/api/v1/spaces/${space}/+/repos?page=${pageIndex + 1}&per_page=${LIST_FETCHING_PER_PAGE}${
query ? `&query=${query}` : ''
}`,
[space, query, pageIndex]
)
const { data: repositories, error, loading, refetch, response } = useGet<TypesRepository[]>({ path })
const itemCount = useMemo(() => parseInt(response?.headers?.get(X_TOTAL) || '0'), [response])
const pageCount = useMemo(() => parseInt(response?.headers?.get(X_TOTAL_PAGES) || '0'), [response])
const pageSize = useMemo(() => parseInt(response?.headers?.get(X_PER_PAGE) || '0'), [response])
const columns: Column<RepositoryDTO>[] = useMemo(
useEffect(() => {
setQuery(undefined)
setPageIndex(0)
}, [space])
const columns: Column<TypesRepository>[] = useMemo(
() => [
{
Header: getString('repos.name'),
accessor: row => row.name,
width: '75%',
Cell: ({ row }: CellProps<RepositoryDTO>) => {
width: 'calc(100% - 180px)',
Cell: ({ row }: CellProps<TypesRepository>) => {
const record = row.original
return (
<Container className={css.nameContainer}>
<Layout.Horizontal spacing="small" style={{ flexGrow: 1 }}>
<Layout.Vertical flex className={css.name} ref={ref}>
<Layout.Vertical flex className={css.name} ref={rowContainerRef}>
<Text className={css.repoName} width={nameTextWidth} lineClamp={2}>
{record.name}
</Text>
@ -77,24 +77,14 @@ export default function Repos(): JSX.Element {
)
}
},
// {
// Header: getString('status'), // TODO: Status is not yet supported by backend
// id: 'status',
// accessor: row => row.updated,
// width: '10%',
// Cell: () => <Icon name="success-tick" padding={{ left: 'small' }} />,
// disableSortBy: true
// },
{
Header: getString('repos.updated'),
id: 'menu',
accessor: row => row.updated,
width: '15%',
Cell: ({ row }: CellProps<RepositoryDTO>) => {
width: '180px',
Cell: ({ row }: CellProps<TypesRepository>) => {
return (
<Layout.Horizontal style={{ alignItems: 'center' }}>
<Text color={Color.BLACK} lineClamp={1} rightIconProps={{ size: 10 }} width={120}>
{formatDate(row.original.updated)}
{formatDate(row.original.updated as number)}
</Text>
{row.original.isPublic === false ? <Icon name="lock" size={10} /> : undefined}
</Layout.Horizontal>
@ -103,12 +93,11 @@ export default function Repos(): JSX.Element {
disableSortBy: true
}
],
[history, module, nameTextWidth] // eslint-disable-line react-hooks/exhaustive-deps
[nameTextWidth, getString]
)
const [pageIndex, setPageIndex] = useState(0)
const onResize = useCallback((): void => {
if (ref.current) {
setNameTextWidth((ref.current.closest('div[role="cell"]') as HTMLDivElement)?.offsetWidth - 100)
if (rowContainerRef.current) {
setNameTextWidth((rowContainerRef.current.closest('div[role="cell"]') as HTMLDivElement)?.offsetWidth - 100)
}
}, [setNameTextWidth])
const NewRepoButton = (
@ -118,18 +107,7 @@ export default function Repos(): JSX.Element {
text={getString('newRepo')}
variation={ButtonVariation.PRIMARY}
icon="plus"
onSubmit={_data => {
// TODO: Remove this when backend fixes https://harness.slack.com/archives/C03Q1Q4C9J8/p1666521412586789
const accountId = 'kmpySmUISimoRrJL6NL73w'
const [_accountId, orgIdentifier, projectIdentifier, repoName] = _data.path.split('/')
const url = routes.toSCMRepository({
repoPath: [accountId, orgIdentifier, projectIdentifier, repoName].join('/')
})
console.log({ _data, url, accountId, _accountId })
history.push(
routes.toSCMRepository({ repoPath: [accountId, orgIdentifier, projectIdentifier, repoName].join('/') })
)
}}
onSubmit={repoInfo => history.push(routes.toSCMRepository({ repoPath: repoInfo.path as string }))}
/>
)
@ -145,14 +123,12 @@ export default function Repos(): JSX.Element {
<Container className={css.main}>
<PageHeader title={getString('repositories')} />
<PageBody
loading={loading}
loading={loading && query === undefined}
className={cx({ [css.withError]: !!error })}
error={error ? getErrorMessage(error) : null}
retryOnError={() => {
refetch()
}}
retryOnError={() => refetch()}
noData={{
when: () => repositories?.length === 0,
when: () => repositories?.length === 0 && query === undefined,
image: emptyStateImage,
message: getString('repos.noDataMessage'),
button: NewRepoButton
@ -161,24 +137,25 @@ export default function Repos(): JSX.Element {
<Layout.Horizontal spacing="large">
{NewRepoButton}
<FlexExpander />
{/* TODO: Search is not yet supported by backend */}
{/* <TextInput placeholder={getString('search')} leftIcon="search" style={{ width: 350 }} autoFocus /> */}
<TextInput
placeholder={getString('search')}
leftIcon={loading && query !== undefined ? 'steps-spinner' : 'search'}
style={{ width: 250 }}
autoFocus
onInput={event => {
setQuery(event.currentTarget.value || '')
setPageIndex(0)
}}
/>
</Layout.Horizontal>
<Container margin={{ top: 'medium' }}>
<Table<RepositoryDTO>
<Table<TypesRepository>
rowDataTestID={(_, index: number) => `scm-repo-${index}`}
className={css.table}
columns={columns}
data={repositories || []}
onRowClick={data => {
// TODO: Remove this when backend fixes https://harness.slack.com/archives/C03Q1Q4C9J8/p1666521412586789
const accountId = 'kmpySmUISimoRrJL6NL73w'
const [_accountId, orgIdentifier, projectIdentifier, repoName] = data.path.split('/')
const url = routes.toSCMRepository({
repoPath: [accountId, orgIdentifier, projectIdentifier, repoName].join('/')
})
console.info({ data, url, accountId, _accountId })
history.push(url)
onRowClick={repoInfo => {
history.push(routes.toSCMRepository({ repoPath: repoInfo.path as string }))
}}
getRowClassName={row => cx(css.row, !row.original.description && css.noDesc)}
/>
@ -193,7 +170,6 @@ export default function Repos(): JSX.Element {
pageCount={pageCount}
pageIndex={pageIndex}
pageSize={pageSize}
pageSizeOptions={[5, 10, 20, 40]}
/>
</Container>
)}

View File

@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom'
import { useGet } from 'restful-react'
import { useAppContext } from 'AppContext'
import type { SCMPathProps } from 'RouteDefinitions'
import type { RepositoryDTO } from 'types/SCMTypes'
import type { TypesRepository } from 'services/scm'
import { getErrorMessage } from 'utils/Utils'
import { RepositoryContent } from './RepositoryContent/RepositoryContent'
import { RepositoryHeader } from './RepositoryHeader/RepositoryHeader'
@ -13,7 +13,7 @@ import css from './Repository.module.scss'
export default function Repository(): JSX.Element {
const { space: spaceFromParams, repoName, gitRef = '', resourcePath = '' } = useParams<SCMPathProps>()
const { space = spaceFromParams || '' } = useAppContext()
const { data, error, loading } = useGet<RepositoryDTO>({
const { data, error, loading } = useGet<TypesRepository>({
path: `/api/v1/repos/${space}/${repoName}/+/`
})

View File

@ -4,12 +4,13 @@ import {
Layout,
Button,
FlexExpander,
TextInput,
// TextInput,
ButtonVariation,
Text,
DropDown,
Icon,
Color
Color,
SelectOption
} from '@harness/uicore'
import ReactJoin from 'react-join'
import { Link, useHistory } from 'react-router-dom'
@ -17,14 +18,13 @@ import { uniq } from 'lodash-es'
import { useGet } from 'restful-react'
import { useStrings } from 'framework/strings'
import { useAppContext } from 'AppContext'
import type { RepoBranch } from 'services/scm'
import type { RepositoryDTO } from 'types/SCMTypes'
import type { RepoBranch, TypesRepository } from 'services/scm'
import css from './ContentHeader.module.scss'
interface ContentHeaderProps {
gitRef?: string
resourcePath?: string
repoMetadata: RepositoryDTO
repoMetadata: TypesRepository
}
export function ContentHeader({ repoMetadata, gitRef, resourcePath = '' }: ContentHeaderProps): JSX.Element {
@ -41,7 +41,9 @@ export function ContentHeader({ repoMetadata, gitRef, resourcePath = '' }: Conte
() => [repoMetadata.defaultBranch].concat(gitRef ? gitRef : []),
[repoMetadata, gitRef]
)
const [branches, setBranches] = useState(uniq(defaultBranches.map(_branch => ({ label: _branch, value: _branch }))))
const [branches, setBranches] = useState<SelectOption[]>(
uniq(defaultBranches.map(_branch => ({ label: _branch, value: _branch }))) as SelectOption[]
)
useEffect(() => {
if (data?.length) {
@ -49,7 +51,7 @@ export function ContentHeader({ repoMetadata, gitRef, resourcePath = '' }: Conte
uniq(defaultBranches.concat(data.map(e => e.name) as string[])).map(_branch => ({
label: _branch,
value: _branch
}))
})) as SelectOption[]
)
}
}, [data, defaultBranches])
@ -65,7 +67,7 @@ export function ContentHeader({ repoMetadata, gitRef, resourcePath = '' }: Conte
setBranch(switchBranch as string)
history.push(
routes.toSCMRepository({
repoPath: repoMetadata.path,
repoPath: repoMetadata.path as string,
gitRef: switchBranch as string,
resourcePath // TODO: Handle 404 when resourcePath does not exist in newly switched branch
})
@ -75,7 +77,7 @@ export function ContentHeader({ repoMetadata, gitRef, resourcePath = '' }: Conte
/>
<Container>
<Layout.Horizontal spacing="small">
<Link to={routes.toSCMRepository({ repoPath: repoMetadata.path })}>
<Link to={routes.toSCMRepository({ repoPath: repoMetadata.path as string })}>
<Icon name="main-folder" />
</Link>
<Text color={Color.GREY_900}>/</Text>
@ -86,7 +88,11 @@ export function ContentHeader({ repoMetadata, gitRef, resourcePath = '' }: Conte
return (
<Link
key={path + index}
to={routes.toSCMRepository({ repoPath: repoMetadata.path, gitRef, resourcePath: pathAtIndex })}>
to={routes.toSCMRepository({
repoPath: repoMetadata.path as string,
gitRef,
resourcePath: pathAtIndex
})}>
<Text color={Color.GREY_900}>{path}</Text>
</Link>
)

View File

@ -17,15 +17,14 @@ import ReactTimeago from 'react-timeago'
import { Link, useHistory } from 'react-router-dom'
import { useStrings } from 'framework/strings'
import { useAppContext } from 'AppContext'
import type { OpenapiContentInfo, OpenapiDirContent, OpenapiGetContentOutput } from 'services/scm'
import type { OpenapiContentInfo, OpenapiDirContent, OpenapiGetContentOutput, TypesRepository } from 'services/scm'
import { formatDate } from 'utils/Utils'
import { findReadmeInfo, GitIcon, isFile } from 'utils/GitUtils'
import type { RepositoryDTO } from 'types/SCMTypes'
import { Readme } from './Readme'
import css from './FolderContent.module.scss'
interface FolderContentProps {
repoMetadata: RepositoryDTO
repoMetadata: TypesRepository
gitRef?: string
contentInfo: OpenapiGetContentOutput
}
@ -106,7 +105,7 @@ export function FolderContent({ repoMetadata, contentInfo, gitRef }: FolderConte
onRowClick={data => {
history.push(
routes.toSCMRepository({
repoPath: repoMetadata.path,
repoPath: repoMetadata.path as string,
gitRef,
resourcePath: data.path
})

View File

@ -1,25 +1,25 @@
import React from 'react'
import { Container, Color, Layout, Button, FlexExpander, ButtonVariation, Heading } from '@harness/uicore'
import { useHistory } from 'react-router-dom'
// import { useHistory } from 'react-router-dom'
import { useGet } from 'restful-react'
import { useStrings } from 'framework/strings'
// import { useStrings } from 'framework/strings'
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
import { useAppContext } from 'AppContext'
import type { OpenapiContentInfo, OpenapiGetContentOutput, RepoFileContent } from 'services/scm'
// import { useAppContext } from 'AppContext'
import type { OpenapiContentInfo, OpenapiGetContentOutput, RepoFileContent, TypesRepository } from 'services/scm'
import { GitIcon } from 'utils/GitUtils'
import type { RepositoryDTO } from 'types/SCMTypes'
import type {} from 'services/scm'
import css from './Readme.module.scss'
interface FolderContentProps {
metadata: RepositoryDTO
metadata: TypesRepository
gitRef?: string
readmeInfo: OpenapiContentInfo
}
export function Readme({ metadata, gitRef, readmeInfo }: FolderContentProps): JSX.Element {
const { getString } = useStrings()
const history = useHistory()
const { routes } = useAppContext()
// const { getString } = useStrings()
// const history = useHistory()
// const { routes } = useAppContext()
const { data /*error, loading, refetch, response */ } = useGet<OpenapiGetContentOutput>({
path: `/api/v1/repos/${metadata.path}/+/content/${readmeInfo.path}?include_commit=false${

View File

@ -1,251 +0,0 @@
# A demo of `react-markdown`
`react-markdown` is a markdown component for React.
👉 Changes are re-rendered as you type.
👈 Try writing some markdown on the left.
## Overview
- Follows [CommonMark](https://commonmark.org)
- Optionally follows [GitHub Flavored Markdown](https://github.github.com/gfm/)
- Renders actual React elements instead of using `dangerouslySetInnerHTML`
- Lets you define your own components (to render `MyHeading` instead of `h1`)
- Has a lot of plugins
## Table of contents
```js {1,3-4} showLineNumbers
function fancyAlert(arg) {
if (arg) {
$.facebox({ div: '#foo' })
}
}
```
Here is an example of a plugin in action
([`remark-toc`](https://github.com/remarkjs/remark-toc)).
This section is replaced by an actual table of contents.
## Syntax highlighting
Here is an example of a plugin to highlight code:
[`rehype-highlight`](https://github.com/rehypejs/rehype-highlight).
```js
import React from 'react'
import ReactDOM from 'react-dom'
import ReactMarkdown from 'react-markdown'
import rehypeHighlight from 'rehype-highlight'
ReactDOM.render(
<ReactMarkdown rehypePlugins={[rehypeHighlight]}>{'# Your markdown here'}</ReactMarkdown>,
document.querySelector('#content')
)
```
Pretty neat, eh?
## GitHub flavored markdown (GFM)
For GFM, you can _also_ use a plugin:
[`remark-gfm`](https://github.com/remarkjs/react-markdown#use).
It adds support for GitHub-specific extensions to the language:
tables, strikethrough, tasklists, and literal URLs.
These features **do not work by default**.
👆 Use the toggle above to add the plugin.
| Feature | Support |
| ---------: | :------------------- |
| CommonMark | 100% |
| GFM | 100% w/ `remark-gfm` |
~~strikethrough~~
- [ ] task list
- [x] checked item
https://example.com
## HTML in markdown
⚠️ HTML in markdown is quite unsafe, but if you want to support it, you can
use [`rehype-raw`](https://github.com/rehypejs/rehype-raw).
You should probably combine it with
[`rehype-sanitize`](https://github.com/rehypejs/rehype-sanitize).
<blockquote>
👆 Use the toggle above to add the plugin.
</blockquote>
## Components
You can pass components to change things:
```js
import React from 'react'
import ReactDOM from 'react-dom'
import ReactMarkdown from 'react-markdown'
import MyFancyRule from './components/my-fancy-rule.js'
ReactDOM.render(
<ReactMarkdown
components={{
// Use h2s instead of h1s
h1: 'h2',
// Use a component instead of hrs
hr: ({ node, ...props }) => <MyFancyRule {...props} />
}}>
# Your markdown here
</ReactMarkdown>,
document.querySelector('#content')
)
```
### JSX
```jsx
import root from 'react-shadow'
import styles from './styles.css'
export default function Quote() {
return (
<root.div className="quote">
<q>There is strong shadow where there is much light.</q>
<span className="author">― Johann Wolfgang von Goethe.</span>
<style type="text/css">{styles}</style>
</root.div>
)
}
```
### Java
```java
public static void main() {
System.out.println('Hello World');
}
```
### CSS
```css
.main {
background: white;
}
```
## More info?
Much more info is available in the
[readme on GitHub](https://github.com/remarkjs/react-markdown)!
---
A component by [Espen Hovlandsdal](https://espen.codes/)
![ReactShadow](https://github.com/Wildhoney/ReactShadow/raw/master/media/logo.png)
> Utilise Shadow DOM in React with all the benefits of style encapsulation.
![Travis](http://img.shields.io/travis/Wildhoney/ReactShadow.svg?style=flat-square)
&nbsp;
![Coveralls](https://img.shields.io/coveralls/Wildhoney/ReactShadow.svg?style=flat-square)
&nbsp;
![npm](http://img.shields.io/npm/v/react-shadow.svg?style=flat-square)
&nbsp;
![License MIT](https://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat-square)
- **npm**: `npm i react-shadow`
- **yarn**: `yarn add react-shadow`
- **Heroku**: [https://react-shadow.herokuapp.com/](https://react-shadow.herokuapp.com) ([alternative](https://react-shadow-2.herokuapp.com))
<img src="https://github.com/Wildhoney/ReactShadow/raw/master/media/screenshot.png" width1="50%">
---
## Getting Started
Creating the [shadow root](https://www.w3.org/TR/shadow-dom/) is as simple as using the default export to construct a shadow root using the node name provided &ndash; for example `root.div` would create a `div` as the host element, and a shadow root as its immediate descendant &mdash; all of the child elements would then be descendants of the shadow boundary.
```jsx
import root from 'react-shadow'
import styles from './styles.css'
export default function Quote() {
return (
<root.div className="quote">
<q>There is strong shadow where there is much light.</q>
<span className="author">― Johann Wolfgang von Goethe.</span>
<style type="text/css">{styles}</style>
</root.div>
)
}
```
```java
public static void main() {
System.out.println('Hello World');
}
```
[![Edit react-shadow](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-shadow-by6bo?fontsize=14)
Applying styles requires either applying the styles directly to the component as a string, or importing the CSS documents as a string as part of your build process. You can then append the `style` component directly to your shadow boundary via your component's tree. In [the example](https://github.com/Wildhoney/ReactShadow/tree/master/example) we use the following Webpack configuration to import CSS documents as strings.
```javascript
{
test: /\.css$/,
loader: ['to-string-loader', 'css-loader']
}
```
Alternatively you can use [`styled-components`](https://www.styled-components.com/) normally, as each time a shadow boundary is created, a new `StyleSheetManager` context is also created which will encapsulate all related styles in their corresponding shadow root &mdash; to use this `import react-shadow/styled-components` instead of `import react-shadow`, likewise if you'd like to use [`emotion`](https://emotion.sh/docs/styled) you can `import react-shadow/emotion`.
```javascript
import root from 'react-shadow/styled-components'
import root from 'react-shadow/emotion'
// ...
;<root.section />
```
You may pass any props you like to the `root.*` component which will be applied directly to the host element, including event handlers and class names. There are also a handful of options that are used for the `attachShadow` invocation.
```javascript
ShadowRoot.propTypes = {
mode: PropTypes.oneOf(['open', 'closed']),
delegatesFocus: PropTypes.bool,
styleSheets: PropTypes.arrayOf(PropTypes.instanceOf(globalThis.CSSStyleSheet)),
children: PropTypes.node
}
ShadowRoot.defaultProps = {
mode: 'open',
delegatesFocus: false,
styleSheets: [],
children: null
}
```
In cases where you need the underlying element and its associated shadow boundary, you can use a standard `ref` which will be invoked with the host element &ndash; from that you can use `shadowRoot` to access its shadow root if the `mode` has been set to the default `open` value.
```javascript
const node = useRef(null)
// ...
;<root.section ref={node} />
```
Recently and at long last there has been some movement in introducing a [declarative shadow DOM](https://tomalec.github.io/declarative-shadow-dom/) which `react-shadow` _tentatively_ supports &ndash; as it's experimental, open to sudden spec changes, and React finds it difficult to rehydrate &ndash; by using the `ssr` prop.
```javascript
const node = useRef(null)
// ...
;<root.section ssr />
```

View File

@ -1,107 +0,0 @@
.tabContent {
height: calc(100vh - var(--scm-files-page-header-height)) !important;
overflow: auto;
}
.resourceContent {
background-color: #f7f7f7 !important;
.tabs {
[role='tablist'] {
background-color: #f7f7f7 !important;
padding-left: 0;
}
}
.table {
background-color: var(--white) !important;
[class*='TableV2--header'] {
[class*='variation-table-headers'] {
text-transform: none;
color: var(--grey-400);
font-weight: 500;
font-size: 13px;
}
}
.row {
height: 20px;
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
overflow: hidden;
padding-top: 5px;
padding-bottom: 25px;
margin-bottom: 0;
}
}
.fileContentContainer {
margin-top: var(--spacing-xlarge);
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
border-radius: 4px;
.fileContentHeading {
border-bottom: 1px solid var(--grey-100);
align-items: center;
padding-left: var(--spacing-large) !important;
}
}
.readmeContainer {
padding: var(--spacing-xxlarge);
}
}
.tabContent {
height: calc(100vh - var(--scm-files-page-header-height)) !important;
overflow: auto;
}
.resourceContent {
background-color: #f7f7f7 !important;
.tabs {
[role='tablist'] {
background-color: #f7f7f7 !important;
padding-left: 0;
}
}
.table {
background-color: var(--white) !important;
[class*='TableV2--header'] {
[class*='variation-table-headers'] {
text-transform: none;
color: var(--grey-400);
font-weight: 500;
font-size: 13px;
}
}
.row {
height: 20px;
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
overflow: hidden;
padding-top: 5px;
padding-bottom: 25px;
margin-bottom: 0;
}
}
.fileContentContainer {
margin-top: var(--spacing-xlarge);
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
border-radius: 4px;
.fileContentHeading {
border-bottom: 1px solid var(--grey-100);
align-items: center;
padding-left: var(--spacing-large) !important;
}
}
.readmeContainer {
padding: var(--spacing-xxlarge);
}
}

View File

@ -1,8 +1,7 @@
import React from 'react'
import { Container } from '@harness/uicore'
import { useGet } from 'restful-react'
import type { RepositoryDTO } from 'types/SCMTypes'
import type { OpenapiGetContentOutput } from 'services/scm'
import type { OpenapiGetContentOutput, TypesRepository } from 'services/scm'
import { isDir, isFile } from 'utils/GitUtils'
import { ContentHeader } from './ContentHeader/ContentHeader'
import { FolderContent } from './FolderContent/FolderContent'
@ -12,7 +11,7 @@ import css from './RepositoryContent.module.scss'
interface RepositoryContentProps {
gitRef?: string
resourcePath?: string
repoMetadata: RepositoryDTO
repoMetadata: TypesRepository
}
export function RepositoryContent({ repoMetadata, gitRef, resourcePath }: RepositoryContentProps): JSX.Element {

View File

@ -5,13 +5,13 @@ import { PopoverInteractionKind } from '@blueprintjs/core'
import { useStrings } from 'framework/strings'
import { ButtonRoleProps } from 'utils/Utils'
import { GitIcon } from 'utils/GitUtils'
import type { RepositoryDTO } from 'types/SCMTypes'
import type { TypesRepository } from 'services/scm'
import { useAppContext } from 'AppContext'
import type { SCMPathProps } from 'RouteDefinitions'
import css from './RepositoryHeader.module.scss'
interface RepositoryHeaderProps {
repoMetadata: RepositoryDTO
repoMetadata: TypesRepository
}
export function RepositoryHeader({ repoMetadata }: RepositoryHeaderProps): JSX.Element {
@ -27,7 +27,7 @@ export function RepositoryHeader({ repoMetadata }: RepositoryHeaderProps): JSX.E
<Icon name="main-chevron-right" size={10} color={Color.GREY_500} /> */}
<Link to={routes.toSCMRepositoriesListing({ space })}>{getString('repositories')}</Link>
<Icon name="main-chevron-right" size={10} color={Color.GREY_500} />
<Link to={routes.toSCMRepository({ repoPath: repoMetadata.path })}>{repoMetadata.name}</Link>
<Link to={routes.toSCMRepository({ repoPath: repoMetadata.path as string })}>{repoMetadata.name}</Link>
</Layout.Horizontal>
<Container padding={{ top: 'medium', bottom: 'medium' }}>
<Text

View File

@ -1,38 +1,3 @@
//
// TODO: Types should be auto-generated from backend
//
export interface RepositoryDTO {
created: number
createdBy: number
defaultBranch: string
description: string
forkId: number
id: number
isPublic: boolean
name: string
numClosedPulls: number
numForks: number
numOpenPulls: number
numPulls: number
path: string
pathName: string
spaceId: number
updated: number
}
export interface CreateRepositoryBody {
defaultBranch: string
description?: string
forkId?: number
gitIgnore: string
isPublic: boolean
license: string
name: string
pathName: string
readme: boolean
spaceId: number | string
}
export enum GitContentType {
FILE = 'file',
DIR = 'dir',

View File

@ -3,10 +3,8 @@ import type { editor as EDITOR } from 'monaco-editor/esm/vs/editor/editor.api'
import { get } from 'lodash-es'
import moment from 'moment'
import langMap from 'lang-map'
import { useEffect } from 'react'
import { useAppContext } from 'AppContext'
export const LIST_FETCHING_PAGE_SIZE = 20
export const LIST_FETCHING_PER_PAGE = 5
export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a'
export const X_TOTAL = 'x-total'
export const X_TOTAL_PAGES = 'x-total-pages'
@ -14,7 +12,7 @@ export const X_PER_PAGE = 'x-per-page'
export type Unknown = any // eslint-disable-line @typescript-eslint/no-explicit-any
export const DEFAULT_BRANCH_NAME = 'main'
export const REGEX_VALID_REPO_NAME = /^[A-Za-z0-9_.-][A-Za-z0-9 _.-]*$/
export const SUGGESTED_BRANCH_NAMES = ['main', 'master']
export const SUGGESTED_BRANCH_NAMES = [DEFAULT_BRANCH_NAME, 'master']
/** This utility shows a toaster without being bound to any component.
* It's useful to show cross-page/component messages */
@ -24,8 +22,7 @@ export function showToaster(message: string, props?: Partial<IToastProps>): IToa
return toaster
}
// eslint-disable-next-line
export const getErrorMessage = (error: any): string =>
export const getErrorMessage = (error: Unknown): string =>
get(error, 'data.error', get(error, 'data.message', error?.message))
export const MonacoEditorOptions = {
@ -81,101 +78,6 @@ export function formatDate(timestamp: number | string, dateStyle = 'medium'): st
}).format(new Date(timestamp))
}
export enum Editions {
ENTERPRISE = 'ENTERPRISE',
TEAM = 'TEAM',
FREE = 'FREE',
COMMUNITY = 'COMMUNITY'
}
export interface License {
accountIdentifier?: string
createdAt?: number
edition?: 'COMMUNITY' | 'FREE' | 'TEAM' | 'ENTERPRISE'
expiryTime?: number
id?: string
lastModifiedAt?: number
licenseType?: 'TRIAL' | 'PAID'
moduleType?: 'CD' | 'CI' | 'CV' | 'CF' | 'CE' | 'STO' | 'CORE' | 'PMS' | 'TEMPLATESERVICE' | 'GOVERNANCE'
premiumSupport?: boolean
selfService?: boolean
startTime?: number
status?: 'ACTIVE' | 'DELETED' | 'EXPIRED'
trialExtended?: boolean
}
export interface LicenseInformation {
[key: string]: License
}
export const findEnterprisePaid = (licenseInformation: LicenseInformation): boolean => {
return !!Object.values(licenseInformation).find(
(license: License) => license.edition === Editions.ENTERPRISE && license.licenseType === 'PAID'
)
}
export const useAnyTrialLicense = (): boolean => {
const {
hooks: { useLicenseStore = () => ({}) }
} = useAppContext()
const { licenseInformation }: { licenseInformation: LicenseInformation } = useLicenseStore()
const hasEnterprisePaid = findEnterprisePaid(licenseInformation)
if (hasEnterprisePaid) return false
const anyTrialEntitlements = Object.values(licenseInformation).find(
(license: License) => license?.edition === Editions.ENTERPRISE && license?.licenseType === 'TRIAL'
)
return !!anyTrialEntitlements
}
export const useGetTrialInfo = (): Unknown => {
const {
hooks: { useLicenseStore = () => ({}) }
} = useAppContext()
const { licenseInformation }: { licenseInformation: LicenseInformation } = useLicenseStore()
const hasEnterprisePaid = findEnterprisePaid(licenseInformation)
if (hasEnterprisePaid) return
const allEntitlements = Object.keys(licenseInformation).map(module => {
return licenseInformation[module]
})
const trialEntitlement = allEntitlements
.sort((a: License, b: License) => (b.expiryTime ?? 0) - (a.expiryTime ?? 0))
.find((license: License) => license?.edition === Editions.ENTERPRISE && license?.licenseType === 'TRIAL')
return trialEntitlement
}
export const useFindActiveEnterprise = (): boolean => {
const {
hooks: { useLicenseStore = () => ({}) }
} = useAppContext()
const { licenseInformation }: { licenseInformation: LicenseInformation } = useLicenseStore()
return Object.values(licenseInformation).some(
(license: License) => license.edition === Editions.ENTERPRISE && license.status === 'ACTIVE'
)
}
/**
* Scrolls the target element to top when any dependency changes
* @param {string} target Target element className selector
* @param {array} dependencies Dependencies to watch
* @returns {void}
*/
export const useScrollToTop = (target: string, dependencies: unknown[]): void => {
useEffect(() => {
const element = document.querySelector(`.${target}`)
if (element) {
element.scrollTop = 0
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dependencies])
}
/**
* Make any HTML element as a clickable button with keyboard accessibility
* support (hit Enter/Space will trigger click event)