mirror of
https://github.com/harness/drone.git
synced 2025-05-17 09:30:00 +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',
|
lang = 'en',
|
||||||
on401 = handle401,
|
on401 = handle401,
|
||||||
children,
|
children,
|
||||||
hooks = {},
|
hooks
|
||||||
components = {}
|
|
||||||
}: AppProps) {
|
}: AppProps) {
|
||||||
const [strings, setStrings] = useState<LanguageRecord>()
|
const [strings, setStrings] = useState<LanguageRecord>()
|
||||||
const token = useAPIToken()
|
const token = useAPIToken()
|
||||||
const getRequestOptions = useCallback(
|
const getRequestOptions = useCallback(
|
||||||
(): Partial<RequestInit> => buildResfulReactRequestOptions(hooks.useGetToken?.() || token),
|
(): Partial<RequestInit> => buildResfulReactRequestOptions(hooks?.useGetToken?.() || token),
|
||||||
[token, hooks]
|
[token, hooks]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -42,7 +41,7 @@ const App: React.FC<AppProps> = React.memo(function App({
|
|||||||
return strings ? (
|
return strings ? (
|
||||||
<StringsContextProvider initialStrings={strings}>
|
<StringsContextProvider initialStrings={strings}>
|
||||||
<AppErrorBoundary>
|
<AppErrorBoundary>
|
||||||
<AppContextProvider value={{ standalone, space, routes, lang, on401, hooks, components }}>
|
<AppContextProvider value={{ standalone, space, routes, lang, on401, hooks }}>
|
||||||
<RestfulProvider
|
<RestfulProvider
|
||||||
base={standalone ? '/' : '/scm'}
|
base={standalone ? '/' : '/scm'}
|
||||||
requestOptions={getRequestOptions}
|
requestOptions={getRequestOptions}
|
||||||
|
@ -11,8 +11,7 @@ const AppContext = React.createContext<AppContextProps>({
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
setAppContext: noop,
|
setAppContext: noop,
|
||||||
routes,
|
routes,
|
||||||
hooks: {},
|
hooks: {}
|
||||||
components: {}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AppContextProvider: React.FC<{ value: AppProps }> = React.memo(function AppContextProvider({
|
export const AppContextProvider: React.FC<{ value: AppProps }> = React.memo(function AppContextProvider({
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import type React from 'react'
|
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 { SCMRoutes } from 'RouteDefinitions'
|
||||||
|
import type { Unknown } from 'utils/Utils'
|
||||||
import type { LangLocale } from './framework/strings/languageLoader'
|
import type { LangLocale } from './framework/strings/languageLoader'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,63 +40,7 @@ export interface AppProps {
|
|||||||
on401?: () => void
|
on401?: () => void
|
||||||
|
|
||||||
/** React Hooks that Harness Platform passes down. Note: Pass only hooks that your app need */
|
/** React Hooks that Harness Platform passes down. Note: Pass only hooks that your app need */
|
||||||
hooks: Partial<AppPropsHook>
|
hooks: Partial<{
|
||||||
|
useGetToken: Unknown
|
||||||
/** 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
|
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
@ -6,38 +6,16 @@ export interface SCMPathProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SCMQueryProps {
|
export interface SCMQueryProps {
|
||||||
branch?: string
|
query?: string
|
||||||
filePath?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pathProps: Readonly<Required<SCMPathProps>> = {
|
export const pathProps: Readonly<Required<SCMPathProps>> = {
|
||||||
space: ':space',
|
space: ':space',
|
||||||
repoName: ':repoName',
|
repoName: ':repoName',
|
||||||
gitRef: ':gitRef*',
|
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 {
|
export interface SCMRoutes {
|
||||||
toSignIn: () => string
|
toSignIn: () => string
|
||||||
toSignUp: () => string
|
toSignUp: () => string
|
||||||
@ -56,22 +34,7 @@ export interface SCMRoutes {
|
|||||||
export const routes: SCMRoutes = {
|
export const routes: SCMRoutes = {
|
||||||
toSignIn: (): string => '/signin',
|
toSignIn: (): string => '/signin',
|
||||||
toSignUp: (): string => '/signup',
|
toSignUp: (): string => '/signup',
|
||||||
toSCMRepositoriesListing: ({ space }: { space: string }) => {
|
toSCMRepositoriesListing: ({ space }: { space: string }) => `/${space}`,
|
||||||
const [accountId, orgIdentifier, projectIdentifier] = space.split('/')
|
toSCMRepository: ({ repoPath, gitRef, resourcePath }: { repoPath: string; gitRef?: string; resourcePath?: string }) =>
|
||||||
return `/account/${accountId}/code/${orgIdentifier}/${projectIdentifier}`
|
`/${repoPath}/${gitRef ? '/' + gitRef : ''}${resourcePath ? '/~/' + resourcePath : ''}`
|
||||||
},
|
|
||||||
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 : ''
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,21 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
|||||||
<Route path={routes.toSignUp()}>
|
<Route path={routes.toSignUp()}>
|
||||||
<SignUp />
|
<SignUp />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={routes.toSCMRepositoriesListing(pathProps)} exact>
|
<Route path={routes.toSCMRepositoriesListing({ space: pathProps.space })} exact>
|
||||||
<RepositoriesListing />
|
<RepositoriesListing />
|
||||||
</Route>
|
</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 />
|
<Repository />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
|
import { routes } from 'RouteDefinitions'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import './bootstrap.scss'
|
import './bootstrap.scss'
|
||||||
|
|
||||||
@ -8,4 +9,4 @@ import './bootstrap.scss'
|
|||||||
// Also being used in when generating proper URLs inside the app.
|
// Also being used in when generating proper URLs inside the app.
|
||||||
window.STRIP_SCM_PREFIX = true
|
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,
|
SUGGESTED_BRANCH_NAMES,
|
||||||
Unknown
|
Unknown
|
||||||
} from 'utils/Utils'
|
} from 'utils/Utils'
|
||||||
import type { RepositoryDTO, CreateRepositoryBody } from 'types/SCMTypes'
|
|
||||||
import { isGitBranchNameValid } from 'utils/GitUtils'
|
import { isGitBranchNameValid } from 'utils/GitUtils'
|
||||||
|
import type { TypesRepository, OpenapiCreateRepositoryRequest } from 'services/scm'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import css from './NewRepoModalButton.module.scss'
|
import css from './NewRepoModalButton.module.scss'
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ export interface NewRepoModalButtonProps extends Omit<ButtonProps, 'onClick' | '
|
|||||||
modalTitle: string
|
modalTitle: string
|
||||||
submitButtonTitle?: string
|
submitButtonTitle?: string
|
||||||
cancelButtonTitle?: string
|
cancelButtonTitle?: string
|
||||||
onSubmit: (data: RepositoryDTO) => void
|
onSubmit: (data: TypesRepository) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
|
export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
|
||||||
@ -88,7 +88,7 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
|
|||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const [branchName, setBranchName] = useState(DEFAULT_BRANCH_NAME)
|
const [branchName, setBranchName] = useState(DEFAULT_BRANCH_NAME)
|
||||||
const { showError } = useToaster()
|
const { showError } = useToaster()
|
||||||
const { mutate: createRepo, loading: submitLoading } = useMutate<RepositoryDTO>({
|
const { mutate: createRepo, loading: submitLoading } = useMutate<TypesRepository>({
|
||||||
verb: 'POST',
|
verb: 'POST',
|
||||||
path: `/api/v1/repos?spacePath=${space}`,
|
path: `/api/v1/repos?spacePath=${space}`,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
@ -129,7 +129,7 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
|
|||||||
pathName: get(formData, 'name', '').trim(),
|
pathName: get(formData, 'name', '').trim(),
|
||||||
readme: get(formData, 'addReadme', false),
|
readme: get(formData, 'addReadme', false),
|
||||||
spaceId: standalone ? space : 0
|
spaceId: standalone ? space : 0
|
||||||
} as CreateRepositoryBody)
|
} as OpenapiCreateRepositoryRequest)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
hideModal()
|
hideModal()
|
||||||
onSubmit(response)
|
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 RequireField<T, K extends keyof T> = T & Required<Pick<T, K>>
|
||||||
|
|
||||||
type Module =
|
declare module 'lang-map' {
|
||||||
| 'ci'
|
const languages: { languages: (name: string) => string[] }
|
||||||
| 'cd'
|
export default languages
|
||||||
| 'cf'
|
}
|
||||||
| 'cv'
|
|
||||||
| 'ce'
|
declare module 'react-join' {
|
||||||
| ':module(ci)'
|
const ReactJoin: React.FC<{ separator: JSX.Element }>
|
||||||
| ':module(cd)'
|
export default ReactJoin
|
||||||
| ':module(cf)'
|
}
|
||||||
| ':module'
|
|
||||||
| ':module(cv)'
|
|
||||||
|
@ -10,59 +10,59 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Color,
|
Color,
|
||||||
Pagination,
|
Pagination,
|
||||||
Icon
|
Icon,
|
||||||
|
TextInput
|
||||||
} from '@harness/uicore'
|
} from '@harness/uicore'
|
||||||
import type { CellProps, Column } from 'react-table'
|
import type { CellProps, Column } from 'react-table'
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import { useGet } from 'restful-react'
|
import { useGet } from 'restful-react'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory, useParams } from 'react-router-dom'
|
||||||
import { useStrings } from 'framework/strings'
|
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 { 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 { useAppContext } from 'AppContext'
|
||||||
import emptyStateImage from './images/empty-state.svg'
|
import emptyStateImage from './images/empty-state.svg'
|
||||||
import css from './RepositoriesListing.module.scss'
|
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 { getString } = useStrings()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const rowContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const [nameTextWidth, setNameTextWidth] = useState(600)
|
const [nameTextWidth, setNameTextWidth] = useState(600)
|
||||||
const { space = '', routes } = useAppContext() // TODO: Proper handling `space` for standalone version
|
const [pageIndex, setPageIndex] = useState(0)
|
||||||
const {
|
const params = useParams<SCMPathProps>()
|
||||||
data: repositories,
|
const [query, setQuery] = useState<string | undefined>()
|
||||||
error,
|
const { space = params.space || '', routes } = useAppContext()
|
||||||
loading,
|
const path = useMemo(
|
||||||
refetch,
|
() =>
|
||||||
response
|
`/api/v1/spaces/${space}/+/repos?page=${pageIndex + 1}&per_page=${LIST_FETCHING_PER_PAGE}${
|
||||||
} = useGet<RepositoryDTO[]>({
|
query ? `&query=${query}` : ''
|
||||||
path: `/api/v1/spaces/${space}/+/repos`
|
}`,
|
||||||
})
|
[space, query, pageIndex]
|
||||||
|
)
|
||||||
// DOES NOT WORK !!! API URL IS NOT CONSTRUCTED PROPERLY
|
const { data: repositories, error, loading, refetch, response } = useGet<TypesRepository[]>({ path })
|
||||||
// const r = useListRepos({
|
|
||||||
// spaceRef: space
|
|
||||||
// })
|
|
||||||
// console.log({ r })
|
|
||||||
|
|
||||||
const itemCount = useMemo(() => parseInt(response?.headers?.get(X_TOTAL) || '0'), [response])
|
const itemCount = useMemo(() => parseInt(response?.headers?.get(X_TOTAL) || '0'), [response])
|
||||||
const pageCount = useMemo(() => parseInt(response?.headers?.get(X_TOTAL_PAGES) || '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 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'),
|
Header: getString('repos.name'),
|
||||||
accessor: row => row.name,
|
width: 'calc(100% - 180px)',
|
||||||
width: '75%',
|
Cell: ({ row }: CellProps<TypesRepository>) => {
|
||||||
Cell: ({ row }: CellProps<RepositoryDTO>) => {
|
|
||||||
const record = row.original
|
const record = row.original
|
||||||
return (
|
return (
|
||||||
<Container className={css.nameContainer}>
|
<Container className={css.nameContainer}>
|
||||||
<Layout.Horizontal spacing="small" style={{ flexGrow: 1 }}>
|
<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}>
|
<Text className={css.repoName} width={nameTextWidth} lineClamp={2}>
|
||||||
{record.name}
|
{record.name}
|
||||||
</Text>
|
</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'),
|
Header: getString('repos.updated'),
|
||||||
id: 'menu',
|
width: '180px',
|
||||||
accessor: row => row.updated,
|
Cell: ({ row }: CellProps<TypesRepository>) => {
|
||||||
width: '15%',
|
|
||||||
Cell: ({ row }: CellProps<RepositoryDTO>) => {
|
|
||||||
return (
|
return (
|
||||||
<Layout.Horizontal style={{ alignItems: 'center' }}>
|
<Layout.Horizontal style={{ alignItems: 'center' }}>
|
||||||
<Text color={Color.BLACK} lineClamp={1} rightIconProps={{ size: 10 }} width={120}>
|
<Text color={Color.BLACK} lineClamp={1} rightIconProps={{ size: 10 }} width={120}>
|
||||||
{formatDate(row.original.updated)}
|
{formatDate(row.original.updated as number)}
|
||||||
</Text>
|
</Text>
|
||||||
{row.original.isPublic === false ? <Icon name="lock" size={10} /> : undefined}
|
{row.original.isPublic === false ? <Icon name="lock" size={10} /> : undefined}
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
@ -103,12 +93,11 @@ export default function Repos(): JSX.Element {
|
|||||||
disableSortBy: true
|
disableSortBy: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[history, module, nameTextWidth] // eslint-disable-line react-hooks/exhaustive-deps
|
[nameTextWidth, getString]
|
||||||
)
|
)
|
||||||
const [pageIndex, setPageIndex] = useState(0)
|
|
||||||
const onResize = useCallback((): void => {
|
const onResize = useCallback((): void => {
|
||||||
if (ref.current) {
|
if (rowContainerRef.current) {
|
||||||
setNameTextWidth((ref.current.closest('div[role="cell"]') as HTMLDivElement)?.offsetWidth - 100)
|
setNameTextWidth((rowContainerRef.current.closest('div[role="cell"]') as HTMLDivElement)?.offsetWidth - 100)
|
||||||
}
|
}
|
||||||
}, [setNameTextWidth])
|
}, [setNameTextWidth])
|
||||||
const NewRepoButton = (
|
const NewRepoButton = (
|
||||||
@ -118,18 +107,7 @@ export default function Repos(): JSX.Element {
|
|||||||
text={getString('newRepo')}
|
text={getString('newRepo')}
|
||||||
variation={ButtonVariation.PRIMARY}
|
variation={ButtonVariation.PRIMARY}
|
||||||
icon="plus"
|
icon="plus"
|
||||||
onSubmit={_data => {
|
onSubmit={repoInfo => history.push(routes.toSCMRepository({ repoPath: repoInfo.path as string }))}
|
||||||
// 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('/') })
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -145,14 +123,12 @@ export default function Repos(): JSX.Element {
|
|||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
<PageHeader title={getString('repositories')} />
|
<PageHeader title={getString('repositories')} />
|
||||||
<PageBody
|
<PageBody
|
||||||
loading={loading}
|
loading={loading && query === undefined}
|
||||||
className={cx({ [css.withError]: !!error })}
|
className={cx({ [css.withError]: !!error })}
|
||||||
error={error ? getErrorMessage(error) : null}
|
error={error ? getErrorMessage(error) : null}
|
||||||
retryOnError={() => {
|
retryOnError={() => refetch()}
|
||||||
refetch()
|
|
||||||
}}
|
|
||||||
noData={{
|
noData={{
|
||||||
when: () => repositories?.length === 0,
|
when: () => repositories?.length === 0 && query === undefined,
|
||||||
image: emptyStateImage,
|
image: emptyStateImage,
|
||||||
message: getString('repos.noDataMessage'),
|
message: getString('repos.noDataMessage'),
|
||||||
button: NewRepoButton
|
button: NewRepoButton
|
||||||
@ -161,24 +137,25 @@ export default function Repos(): JSX.Element {
|
|||||||
<Layout.Horizontal spacing="large">
|
<Layout.Horizontal spacing="large">
|
||||||
{NewRepoButton}
|
{NewRepoButton}
|
||||||
<FlexExpander />
|
<FlexExpander />
|
||||||
{/* TODO: Search is not yet supported by backend */}
|
<TextInput
|
||||||
{/* <TextInput placeholder={getString('search')} leftIcon="search" style={{ width: 350 }} autoFocus /> */}
|
placeholder={getString('search')}
|
||||||
|
leftIcon={loading && query !== undefined ? 'steps-spinner' : 'search'}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
autoFocus
|
||||||
|
onInput={event => {
|
||||||
|
setQuery(event.currentTarget.value || '')
|
||||||
|
setPageIndex(0)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
<Container margin={{ top: 'medium' }}>
|
<Container margin={{ top: 'medium' }}>
|
||||||
<Table<RepositoryDTO>
|
<Table<TypesRepository>
|
||||||
rowDataTestID={(_, index: number) => `scm-repo-${index}`}
|
rowDataTestID={(_, index: number) => `scm-repo-${index}`}
|
||||||
className={css.table}
|
className={css.table}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={repositories || []}
|
data={repositories || []}
|
||||||
onRowClick={data => {
|
onRowClick={repoInfo => {
|
||||||
// TODO: Remove this when backend fixes https://harness.slack.com/archives/C03Q1Q4C9J8/p1666521412586789
|
history.push(routes.toSCMRepository({ repoPath: repoInfo.path as string }))
|
||||||
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)
|
|
||||||
}}
|
}}
|
||||||
getRowClassName={row => cx(css.row, !row.original.description && css.noDesc)}
|
getRowClassName={row => cx(css.row, !row.original.description && css.noDesc)}
|
||||||
/>
|
/>
|
||||||
@ -193,7 +170,6 @@ export default function Repos(): JSX.Element {
|
|||||||
pageCount={pageCount}
|
pageCount={pageCount}
|
||||||
pageIndex={pageIndex}
|
pageIndex={pageIndex}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
pageSizeOptions={[5, 10, 20, 40]}
|
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
|
@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom'
|
|||||||
import { useGet } from 'restful-react'
|
import { useGet } from 'restful-react'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import type { SCMPathProps } from 'RouteDefinitions'
|
import type { SCMPathProps } from 'RouteDefinitions'
|
||||||
import type { RepositoryDTO } from 'types/SCMTypes'
|
import type { TypesRepository } from 'services/scm'
|
||||||
import { getErrorMessage } from 'utils/Utils'
|
import { getErrorMessage } from 'utils/Utils'
|
||||||
import { RepositoryContent } from './RepositoryContent/RepositoryContent'
|
import { RepositoryContent } from './RepositoryContent/RepositoryContent'
|
||||||
import { RepositoryHeader } from './RepositoryHeader/RepositoryHeader'
|
import { RepositoryHeader } from './RepositoryHeader/RepositoryHeader'
|
||||||
@ -13,7 +13,7 @@ import css from './Repository.module.scss'
|
|||||||
export default function Repository(): JSX.Element {
|
export default function Repository(): JSX.Element {
|
||||||
const { space: spaceFromParams, repoName, gitRef = '', resourcePath = '' } = useParams<SCMPathProps>()
|
const { space: spaceFromParams, repoName, gitRef = '', resourcePath = '' } = useParams<SCMPathProps>()
|
||||||
const { space = spaceFromParams || '' } = useAppContext()
|
const { space = spaceFromParams || '' } = useAppContext()
|
||||||
const { data, error, loading } = useGet<RepositoryDTO>({
|
const { data, error, loading } = useGet<TypesRepository>({
|
||||||
path: `/api/v1/repos/${space}/${repoName}/+/`
|
path: `/api/v1/repos/${space}/${repoName}/+/`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -4,12 +4,13 @@ import {
|
|||||||
Layout,
|
Layout,
|
||||||
Button,
|
Button,
|
||||||
FlexExpander,
|
FlexExpander,
|
||||||
TextInput,
|
// TextInput,
|
||||||
ButtonVariation,
|
ButtonVariation,
|
||||||
Text,
|
Text,
|
||||||
DropDown,
|
DropDown,
|
||||||
Icon,
|
Icon,
|
||||||
Color
|
Color,
|
||||||
|
SelectOption
|
||||||
} from '@harness/uicore'
|
} from '@harness/uicore'
|
||||||
import ReactJoin from 'react-join'
|
import ReactJoin from 'react-join'
|
||||||
import { Link, useHistory } from 'react-router-dom'
|
import { Link, useHistory } from 'react-router-dom'
|
||||||
@ -17,14 +18,13 @@ import { uniq } from 'lodash-es'
|
|||||||
import { useGet } from 'restful-react'
|
import { useGet } from 'restful-react'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import type { RepoBranch } from 'services/scm'
|
import type { RepoBranch, TypesRepository } from 'services/scm'
|
||||||
import type { RepositoryDTO } from 'types/SCMTypes'
|
|
||||||
import css from './ContentHeader.module.scss'
|
import css from './ContentHeader.module.scss'
|
||||||
|
|
||||||
interface ContentHeaderProps {
|
interface ContentHeaderProps {
|
||||||
gitRef?: string
|
gitRef?: string
|
||||||
resourcePath?: string
|
resourcePath?: string
|
||||||
repoMetadata: RepositoryDTO
|
repoMetadata: TypesRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContentHeader({ repoMetadata, gitRef, resourcePath = '' }: ContentHeaderProps): JSX.Element {
|
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.defaultBranch].concat(gitRef ? gitRef : []),
|
||||||
[repoMetadata, 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(() => {
|
useEffect(() => {
|
||||||
if (data?.length) {
|
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 => ({
|
uniq(defaultBranches.concat(data.map(e => e.name) as string[])).map(_branch => ({
|
||||||
label: _branch,
|
label: _branch,
|
||||||
value: _branch
|
value: _branch
|
||||||
}))
|
})) as SelectOption[]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [data, defaultBranches])
|
}, [data, defaultBranches])
|
||||||
@ -65,7 +67,7 @@ export function ContentHeader({ repoMetadata, gitRef, resourcePath = '' }: Conte
|
|||||||
setBranch(switchBranch as string)
|
setBranch(switchBranch as string)
|
||||||
history.push(
|
history.push(
|
||||||
routes.toSCMRepository({
|
routes.toSCMRepository({
|
||||||
repoPath: repoMetadata.path,
|
repoPath: repoMetadata.path as string,
|
||||||
gitRef: switchBranch as string,
|
gitRef: switchBranch as string,
|
||||||
resourcePath // TODO: Handle 404 when resourcePath does not exist in newly switched branch
|
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>
|
<Container>
|
||||||
<Layout.Horizontal spacing="small">
|
<Layout.Horizontal spacing="small">
|
||||||
<Link to={routes.toSCMRepository({ repoPath: repoMetadata.path })}>
|
<Link to={routes.toSCMRepository({ repoPath: repoMetadata.path as string })}>
|
||||||
<Icon name="main-folder" />
|
<Icon name="main-folder" />
|
||||||
</Link>
|
</Link>
|
||||||
<Text color={Color.GREY_900}>/</Text>
|
<Text color={Color.GREY_900}>/</Text>
|
||||||
@ -86,7 +88,11 @@ export function ContentHeader({ repoMetadata, gitRef, resourcePath = '' }: Conte
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={path + index}
|
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>
|
<Text color={Color.GREY_900}>{path}</Text>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
@ -17,15 +17,14 @@ import ReactTimeago from 'react-timeago'
|
|||||||
import { Link, useHistory } from 'react-router-dom'
|
import { Link, useHistory } from 'react-router-dom'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { useAppContext } from 'AppContext'
|
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 { formatDate } from 'utils/Utils'
|
||||||
import { findReadmeInfo, GitIcon, isFile } from 'utils/GitUtils'
|
import { findReadmeInfo, GitIcon, isFile } from 'utils/GitUtils'
|
||||||
import type { RepositoryDTO } from 'types/SCMTypes'
|
|
||||||
import { Readme } from './Readme'
|
import { Readme } from './Readme'
|
||||||
import css from './FolderContent.module.scss'
|
import css from './FolderContent.module.scss'
|
||||||
|
|
||||||
interface FolderContentProps {
|
interface FolderContentProps {
|
||||||
repoMetadata: RepositoryDTO
|
repoMetadata: TypesRepository
|
||||||
gitRef?: string
|
gitRef?: string
|
||||||
contentInfo: OpenapiGetContentOutput
|
contentInfo: OpenapiGetContentOutput
|
||||||
}
|
}
|
||||||
@ -106,7 +105,7 @@ export function FolderContent({ repoMetadata, contentInfo, gitRef }: FolderConte
|
|||||||
onRowClick={data => {
|
onRowClick={data => {
|
||||||
history.push(
|
history.push(
|
||||||
routes.toSCMRepository({
|
routes.toSCMRepository({
|
||||||
repoPath: repoMetadata.path,
|
repoPath: repoMetadata.path as string,
|
||||||
gitRef,
|
gitRef,
|
||||||
resourcePath: data.path
|
resourcePath: data.path
|
||||||
})
|
})
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Container, Color, Layout, Button, FlexExpander, ButtonVariation, Heading } from '@harness/uicore'
|
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 { useGet } from 'restful-react'
|
||||||
import { useStrings } from 'framework/strings'
|
// import { useStrings } from 'framework/strings'
|
||||||
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
||||||
import { useAppContext } from 'AppContext'
|
// import { useAppContext } from 'AppContext'
|
||||||
import type { OpenapiContentInfo, OpenapiGetContentOutput, RepoFileContent } from 'services/scm'
|
import type { OpenapiContentInfo, OpenapiGetContentOutput, RepoFileContent, TypesRepository } from 'services/scm'
|
||||||
import { GitIcon } from 'utils/GitUtils'
|
import { GitIcon } from 'utils/GitUtils'
|
||||||
import type { RepositoryDTO } from 'types/SCMTypes'
|
import type {} from 'services/scm'
|
||||||
import css from './Readme.module.scss'
|
import css from './Readme.module.scss'
|
||||||
|
|
||||||
interface FolderContentProps {
|
interface FolderContentProps {
|
||||||
metadata: RepositoryDTO
|
metadata: TypesRepository
|
||||||
gitRef?: string
|
gitRef?: string
|
||||||
readmeInfo: OpenapiContentInfo
|
readmeInfo: OpenapiContentInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Readme({ metadata, gitRef, readmeInfo }: FolderContentProps): JSX.Element {
|
export function Readme({ metadata, gitRef, readmeInfo }: FolderContentProps): JSX.Element {
|
||||||
const { getString } = useStrings()
|
// const { getString } = useStrings()
|
||||||
const history = useHistory()
|
// const history = useHistory()
|
||||||
const { routes } = useAppContext()
|
// const { routes } = useAppContext()
|
||||||
|
|
||||||
const { data /*error, loading, refetch, response */ } = useGet<OpenapiGetContentOutput>({
|
const { data /*error, loading, refetch, response */ } = useGet<OpenapiGetContentOutput>({
|
||||||
path: `/api/v1/repos/${metadata.path}/+/content/${readmeInfo.path}?include_commit=false${
|
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 React from 'react'
|
||||||
import { Container } from '@harness/uicore'
|
import { Container } from '@harness/uicore'
|
||||||
import { useGet } from 'restful-react'
|
import { useGet } from 'restful-react'
|
||||||
import type { RepositoryDTO } from 'types/SCMTypes'
|
import type { OpenapiGetContentOutput, TypesRepository } from 'services/scm'
|
||||||
import type { OpenapiGetContentOutput } from 'services/scm'
|
|
||||||
import { isDir, isFile } from 'utils/GitUtils'
|
import { isDir, isFile } from 'utils/GitUtils'
|
||||||
import { ContentHeader } from './ContentHeader/ContentHeader'
|
import { ContentHeader } from './ContentHeader/ContentHeader'
|
||||||
import { FolderContent } from './FolderContent/FolderContent'
|
import { FolderContent } from './FolderContent/FolderContent'
|
||||||
@ -12,7 +11,7 @@ import css from './RepositoryContent.module.scss'
|
|||||||
interface RepositoryContentProps {
|
interface RepositoryContentProps {
|
||||||
gitRef?: string
|
gitRef?: string
|
||||||
resourcePath?: string
|
resourcePath?: string
|
||||||
repoMetadata: RepositoryDTO
|
repoMetadata: TypesRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepositoryContent({ repoMetadata, gitRef, resourcePath }: RepositoryContentProps): JSX.Element {
|
export function RepositoryContent({ repoMetadata, gitRef, resourcePath }: RepositoryContentProps): JSX.Element {
|
||||||
|
@ -5,13 +5,13 @@ import { PopoverInteractionKind } from '@blueprintjs/core'
|
|||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { ButtonRoleProps } from 'utils/Utils'
|
import { ButtonRoleProps } from 'utils/Utils'
|
||||||
import { GitIcon } from 'utils/GitUtils'
|
import { GitIcon } from 'utils/GitUtils'
|
||||||
import type { RepositoryDTO } from 'types/SCMTypes'
|
import type { TypesRepository } from 'services/scm'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import type { SCMPathProps } from 'RouteDefinitions'
|
import type { SCMPathProps } from 'RouteDefinitions'
|
||||||
import css from './RepositoryHeader.module.scss'
|
import css from './RepositoryHeader.module.scss'
|
||||||
|
|
||||||
interface RepositoryHeaderProps {
|
interface RepositoryHeaderProps {
|
||||||
repoMetadata: RepositoryDTO
|
repoMetadata: TypesRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepositoryHeader({ repoMetadata }: RepositoryHeaderProps): JSX.Element {
|
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} /> */}
|
<Icon name="main-chevron-right" size={10} color={Color.GREY_500} /> */}
|
||||||
<Link to={routes.toSCMRepositoriesListing({ space })}>{getString('repositories')}</Link>
|
<Link to={routes.toSCMRepositoriesListing({ space })}>{getString('repositories')}</Link>
|
||||||
<Icon name="main-chevron-right" size={10} color={Color.GREY_500} />
|
<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>
|
</Layout.Horizontal>
|
||||||
<Container padding={{ top: 'medium', bottom: 'medium' }}>
|
<Container padding={{ top: 'medium', bottom: 'medium' }}>
|
||||||
<Text
|
<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 {
|
export enum GitContentType {
|
||||||
FILE = 'file',
|
FILE = 'file',
|
||||||
DIR = 'dir',
|
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 { get } from 'lodash-es'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import langMap from 'lang-map'
|
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 DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a'
|
||||||
export const X_TOTAL = 'x-total'
|
export const X_TOTAL = 'x-total'
|
||||||
export const X_TOTAL_PAGES = 'x-total-pages'
|
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 type Unknown = any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
export const DEFAULT_BRANCH_NAME = 'main'
|
export const DEFAULT_BRANCH_NAME = 'main'
|
||||||
export const REGEX_VALID_REPO_NAME = /^[A-Za-z0-9_.-][A-Za-z0-9 _.-]*$/
|
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.
|
/** This utility shows a toaster without being bound to any component.
|
||||||
* It's useful to show cross-page/component messages */
|
* It's useful to show cross-page/component messages */
|
||||||
@ -24,8 +22,7 @@ export function showToaster(message: string, props?: Partial<IToastProps>): IToa
|
|||||||
return toaster
|
return toaster
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
export const getErrorMessage = (error: Unknown): string =>
|
||||||
export const getErrorMessage = (error: any): string =>
|
|
||||||
get(error, 'data.error', get(error, 'data.message', error?.message))
|
get(error, 'data.error', get(error, 'data.message', error?.message))
|
||||||
|
|
||||||
export const MonacoEditorOptions = {
|
export const MonacoEditorOptions = {
|
||||||
@ -81,101 +78,6 @@ export function formatDate(timestamp: number | string, dateStyle = 'medium'): st
|
|||||||
}).format(new Date(timestamp))
|
}).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
|
* Make any HTML element as a clickable button with keyboard accessibility
|
||||||
* support (hit Enter/Space will trigger click event)
|
* support (hit Enter/Space will trigger click event)
|
||||||
|
Loading…
Reference in New Issue
Block a user