mirror of
https://github.com/harness/drone.git
synced 2025-05-17 01:20:13 +08:00
Refactor: Use types from SCM Service (#48)
This commit is contained in:
parent
c8b978a6ed
commit
c21b860e46
@ -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}
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
}>
|
||||
}
|
||||
|
@ -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 : ''}`
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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'))
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
@ -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>
|
||||
)
|
||||
}
|
@ -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'>
|
||||
}
|
@ -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)
|
||||
|
@ -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} />
|
||||
}
|
@ -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} />
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
@ -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
20
web/src/global.d.ts
vendored
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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}/+/`
|
||||
})
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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${
|
||||
|
@ -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/)
|
||||
|
||||

|
||||
|
||||
> Utilise Shadow DOM in React with all the benefits of style encapsulation.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
- **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 – for example `root.div` would create a `div` as the host element, and a shadow root as its immediate descendant — 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');
|
||||
}
|
||||
```
|
||||
|
||||
[](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 — 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 – 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 – as it's experimental, open to sudden spec changes, and React finds it difficult to rehydrate – by using the `ssr` prop.
|
||||
|
||||
```javascript
|
||||
const node = useRef(null)
|
||||
|
||||
// ...
|
||||
|
||||
;<root.section ssr />
|
||||
```
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user