feat: [AH-1192]: Tree view implementation (#3736)

* feat: [AH-1192]: fix failing PR checks
* feat: [AH-1192]: rebase with main and fix bugs
* feat: [AH-1192]: Implement digest list view in tree view (#3733)

* feat: [AH-1192]: do not encode query params
* feat: [AH-1192]: Implement digest list view in tree view
* feat: [AH-1192]: Support version list and details in tree view (#3727)

* feat: [AH-1192]: resolve PR comments
* feat: [AH-1192]: Support version list and details in tree view
* feat: [AH-1192]: Implement artifact list tree view and artifact tree node details view (#3694)

* feat: [AH-1192]: add factory implementation for artifact details node view
* feat: [AH-1192]: Implement artifact list tree view and artifact tree node details view
* feat: [AH-1192]: implement a repository list and details route in tree view (#3685)

* feat: [AH-1192]: refactor refetch logic
* feat: [AH-1192]: fix failing unit tests
* feat: [AH-1192]: implement a repository list and details route in tree view
* feat: [AH-1192]: Implement landing page for
This commit is contained in:
Shivanand Sonnad 2025-04-25 16:58:26 +00:00 committed by Harness
parent 92f986122f
commit 38e2ec8308
90 changed files with 2927 additions and 155 deletions

View File

@ -77,6 +77,7 @@ module.exports = {
'monaco-editor': '<rootDir>/node_modules/react-monaco-editor',
'\\.(jpg|jpeg|png|gif|svg|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/scripts/jest/file-mock.js',
'\\.svg?.url': '<rootDir>/scripts/jest/file-mock.js',
'@uiw/react-markdown-preview': '<rootDir>/node_modules/@uiw/react-markdown-preview/dist/markdown.min.js',
...pathsToModuleNameMapper(compilerOptions.paths)
},

View File

@ -135,5 +135,6 @@ export interface MFEAppProps {
export enum FeatureFlags {
HAR_TRIGGERS = 'HAR_TRIGGERS',
HAR_NUGET_PACKAGE_TYPE_ENABLED = 'HAR_NUGET_PACKAGE_TYPE_ENABLED',
HAR_RPM_PACKAGE_TYPE_ENABLED = 'HAR_RPM_PACKAGE_TYPE_ENABLED'
HAR_RPM_PACKAGE_TYPE_ENABLED = 'HAR_RPM_PACKAGE_TYPE_ENABLED',
HAR_TREE_VIEW_ENABLED = 'HAR_TREE_VIEW_ENABLED'
}

View File

@ -19,13 +19,14 @@ import { Page } from '@harnessio/uicore'
import { QueryClientProvider } from '@tanstack/react-query'
import { StringsContextProvider } from '@ar/frameworks/strings/StringsContextProvider'
import { AppStoreContext } from '@ar/contexts/AppStoreContext'
import { AppStoreContext, RepositoryListViewTypeEnum } from '@ar/contexts/AppStoreContext'
import ParentProvider from '@ar/contexts/ParentProvider'
import type { ParentProviderProps } from '@ar/contexts/ParentProvider'
import { queryClient } from '@ar/utils/queryClient'
import { Parent } from '@ar/common/types'
import strings from '@ar/strings/strings.en.yaml'
import { PreferenceScope } from '@ar/constants'
import type { MFEAppProps } from '@ar/MFEAppTypes'
import DefaultNavComponent from '@ar/__mocks__/components/DefaultNavComponent'
import AppErrorBoundary from '@ar/components/AppErrorBoundary/AppErrorBoundary'
@ -60,6 +61,10 @@ export default function ChildApp(props: PropsWithChildren<MFEAppProps>): React.R
const { ModalProvider } = customComponents
const appStoreData = React.useContext(parentContextObj.appStoreContext)
const { usePreferenceStore } = customHooks
const { preference: repositoryListViewType, setPreference: setRepositoryListViewType } = usePreferenceStore<
RepositoryListViewTypeEnum | undefined
>(PreferenceScope.USER, 'RepositoryListViewType')
useOpenApiClient({ on401, customUtils })
@ -81,6 +86,8 @@ export default function ChildApp(props: PropsWithChildren<MFEAppProps>): React.R
matchPath,
baseUrl: renderUrl,
scope: { ...scope, ...customScope },
repositoryListViewType: repositoryListViewType || RepositoryListViewTypeEnum.LIST,
setRepositoryListViewType,
parent
}}>
<StringsContextProvider initialStrings={strings}>

View File

@ -20,11 +20,18 @@ import type { PropsWithChildren } from 'react'
import { Parent } from '@ar/common/types'
import { useAppStore, useDecodedParams, useDeepCompareEffect } from '@ar/hooks'
export default function ParentSyncProvider(props: PropsWithChildren<unknown>) {
const pathParams = useDecodedParams<Record<string, unknown>>()
interface ParentSyncProviderProps {
onLoad?: (pathParams: Record<string, string>) => void
}
export default function ParentSyncProvider(props: PropsWithChildren<ParentSyncProviderProps>) {
const pathParams = useDecodedParams<Record<string, string>>()
const { updateAppStore, parent } = useAppStore()
useDeepCompareEffect(() => {
if (typeof props.onLoad === 'function') {
props.onLoad(pathParams)
}
if (typeof updateAppStore === 'function' && parent !== Parent.Enterprise) {
updateAppStore({
repositoryIdentifier: pathParams?.repositoryIdentifier as string,

View File

@ -24,15 +24,16 @@ import ParentSyncProvider from './ParentSyncProvider'
interface RouteProviderProps extends RouteProps {
enabled?: boolean
onLoad?: (pathParams: Record<string, string>) => void
}
function RouteProvider(props: PropsWithChildren<RouteProviderProps>) {
const { children, enabled = true, ...rest } = props
const { children, enabled = true, onLoad, ...rest } = props
const { ModalProvider } = useParentComponents()
if (!enabled) return <></>
return (
<Route {...rest}>
<ParentSyncProvider>
<ParentSyncProvider onLoad={onLoad}>
<ModalProvider>{children}</ModalProvider>
</ParentSyncProvider>
</Route>

View File

@ -0,0 +1,48 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type PropsWithChildren } from 'react'
import { Spinner } from '@blueprintjs/core'
import { PageError, Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import TreeNode from './TreeNode'
interface TreeBodyProps {
loading?: boolean
error?: string
retryOnError?: () => void
isEmpty?: boolean
emptyDataMessage?: string
}
export default function TreeBody(props: PropsWithChildren<TreeBodyProps>) {
const { loading, error, retryOnError, emptyDataMessage, isEmpty } = props
const { getString } = useStrings()
if (loading) return <Spinner size={Spinner.SIZE_SMALL} />
if (error) return <PageError message={error} onClick={retryOnError} />
if (isEmpty)
return (
<TreeNode
disabled
level={1}
isLastChild
heading={<Text>{emptyDataMessage ?? getString('noResultsFound')}</Text>}
/>
)
return <>{props.children}</>
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Button, ButtonSize, ButtonVariation } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import TreeNode, { TreeNodeProps } from './TreeNode'
export default function TreeLoadMoreNode(props: Omit<TreeNodeProps, 'heading'>) {
const { getString } = useStrings()
return (
<TreeNode
disabled
level={props.level}
heading={
<Button
variation={ButtonVariation.LINK}
size={ButtonSize.SMALL}
minimal
onClick={props.onClick}
disabled={props.disabled}>
{getString('loadMore')}
</Button>
}
/>
)
}

View File

@ -0,0 +1,115 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useRef, useState, type PropsWithChildren } from 'react'
import classNames from 'classnames'
import { Icon } from '@harnessio/icons'
import { Container } from '@harnessio/uicore'
import childImage from './images/child.svg?url'
import lastChildImage from './images/last-child.svg?url'
import lineImage from './images/line.svg?url'
import type { NodeSpec } from './TreeViewContext'
import css from './TreeView.module.scss'
export enum NodeTypeEnum {
File = 'File',
Folder = 'Folder'
}
export interface TreeNodeProps<T = unknown> extends React.HTMLAttributes<HTMLLIElement> {
heading: string | React.ReactNode
isActive?: boolean
isOpen?: boolean
nodeType?: NodeTypeEnum
level?: number
compact?: boolean
disabled?: boolean
onClick?: () => void
actionElement?: React.ReactNode
alwaysShowAction?: boolean
isLastChild?: boolean
parentNodeLevels?: Array<NodeSpec<T>>
}
export default function TreeNode<T>(props: PropsWithChildren<TreeNodeProps<T>>) {
const {
isOpen,
nodeType = NodeTypeEnum.File,
level = 0,
onClick,
compact = true,
disabled,
isActive,
actionElement,
alwaysShowAction,
parentNodeLevels = [],
isLastChild = false,
className,
...rest
} = props
const ref = useRef<HTMLLIElement>(null)
const [open, setOpen] = useState(isOpen)
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
if (open && ref.current) {
ref.current.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' })
}
}, [])
return (
<li
data-level={2}
data-last-child={isLastChild}
ref={ref}
className={classNames(css.treeNode, className)}
{...rest}>
<Container
tabIndex={0}
onMouseEnter={() => setIsMounted(true)}
onMouseLeave={() => setIsMounted(false)}
className={classNames(css.header, {
[css.active]: isActive,
[css.disabled]: disabled
})}
onClick={() => {
if (disabled) return
if (nodeType === NodeTypeEnum.Folder) setOpen(!open)
onClick?.()
}}>
{parentNodeLevels.slice(1).map((_each, indx) => {
const img = _each.isLastChild ? undefined : lineImage
return <div key={indx} className={css.levelImg} style={{ backgroundImage: `url(${img})` }}></div>
})}
{level > 0 && (
<div
className={css.levelImg}
style={{ backgroundImage: `url(${isLastChild ? lastChildImage : childImage})` }}></div>
)}
{nodeType === NodeTypeEnum.Folder && <Icon name={open ? 'chevron-down' : 'chevron-right'} />}
<Container padding={compact ? 'xsmall' : 'small'} className={css.headingContent}>
{props.heading}
</Container>
{actionElement && (isMounted || alwaysShowAction || isActive) && <Container>{actionElement}</Container>}
</Container>
{open && <Container>{props.children}</Container>}
</li>
)
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Layout, Text } from '@harnessio/uicore'
import { Icon, type IconName } from '@harnessio/icons'
import { Color, FontVariation } from '@harnessio/design-system'
import { useStrings } from '@ar/frameworks/strings'
import { RepositoryConfigType } from '@ar/common/types'
import css from './TreeView.module.scss'
interface TreeNodeContentProps {
icon?: IconName
iconSize?: number
label: string
downloads?: number
size?: string
artifacts?: number
type?: RepositoryConfigType
compact?: boolean
}
export default function TreeNodeContent(props: TreeNodeContentProps) {
const { icon, iconSize, label, type, downloads, artifacts, size, compact } = props
const { getString } = useStrings()
return (
<Layout.Horizontal className={css.treeNodeContent} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
{icon && <Icon name={icon} size={iconSize} />}
<Layout.Vertical className={css.labelContainer} spacing="xsmall">
<Text font={{ variation: FontVariation.BODY }} lineClamp={1}>
{label}
</Text>
{!compact && (
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} spacing="xsmall">
{type && (
<>
<Text font={{ variation: FontVariation.SMALL }} color={Color.GREY_400} lineClamp={1}>
{type === RepositoryConfigType.VIRTUAL
? getString('badges.artifactRegistry')
: getString('badges.upstreamProxy')}
</Text>
</>
)}
{size !== undefined && (
<Text
icon="dot"
iconProps={{ size: 8, color: Color.GREY_400 }}
font={{ variation: FontVariation.SMALL }}
color={Color.GREY_400}
lineClamp={1}>
{size}
</Text>
)}
{artifacts !== undefined && (
<Text
icon="dot"
iconProps={{ size: 8, color: Color.GREY_400 }}
rightIcon="store-artifact-bundle"
rightIconProps={{ size: 12 }}
color={Color.GREY_400}
font={{ variation: FontVariation.SMALL }}
lineClamp={1}>
{artifacts}
</Text>
)}
{downloads !== undefined && (
<Text
icon="dot"
iconProps={{ size: 8, color: Color.GREY_400 }}
rightIcon="download-box"
rightIconProps={{ size: 12 }}
color={Color.GREY_400}
font={{ variation: FontVariation.SMALL }}
lineClamp={1}>
{downloads.toLocaleString()}
</Text>
)}
</Layout.Horizontal>
)}
</Layout.Vertical>
</Layout.Horizontal>
)
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type PropsWithChildren } from 'react'
import classNames from 'classnames'
import css from './TreeView.module.scss'
export default function TreeNodeList(
props: PropsWithChildren<React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>>
) {
return (
<ul {...props} className={classNames(props.className, css.treeList)}>
{props.children}
</ul>
)
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import classNames from 'classnames'
import { ExpandingSearchInput, ExpandingSearchInputProps } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import TreeNode, { TreeNodeProps } from './TreeNode'
import css from './TreeView.module.scss'
interface TreeNodeSearchInputProps extends ExpandingSearchInputProps {
level?: number
treeNodeProps?: Omit<TreeNodeProps, 'heading'>
}
export default function TreeNodeSearchInput(props: TreeNodeSearchInputProps) {
const { level = 0, treeNodeProps, className, ...rest } = props
const { getString } = useStrings()
return (
<TreeNode
className={classNames(className, css.stickyNode)}
disabled
level={level}
heading={<ExpandingSearchInput alwaysExpanded placeholder={getString('search')} width="100%" {...rest} />}
{...treeNodeProps}
/>
)
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.treeList {
list-style: none;
margin: 0;
padding: 0;
}
.treeNodeCollapseContainer {
border: none;
}
.headingContent {
flex: 1;
}
.actionElement {
display: none;
&.alwaysShowAction {
display: block;
}
}
.treeNode {
& > :first-child {
padding-left: var(--spacing-large) !important;
padding-right: var(--spacing-small) !important;
}
& .header {
display: flex;
align-items: center;
gap: var(--spacing-xsmall);
&:hover,
&.active {
background-color: var(--primary-2);
cursor: pointer;
& .actionElement {
display: block;
}
}
&.disabled {
cursor: auto;
&:hover {
background-color: var(--grey-100);
}
}
}
& .levelImg {
width: var(--spacing-large);
align-self: stretch;
background-size: 100% 100%;
background-repeat: no-repeat;
background-color: transparent;
}
}
.treeNodeContent {
.labelContainer {
margin-left: var(--spacing-small) !important;
}
}
.stickyNode {
position: sticky;
top: 0;
background-color: var(--white);
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const actionElement: string
export declare const active: string
export declare const alwaysShowAction: string
export declare const disabled: string
export declare const header: string
export declare const headingContent: string
export declare const labelContainer: string
export declare const levelImg: string
export declare const stickyNode: string
export declare const treeList: string
export declare const treeNode: string
export declare const treeNodeCollapseContainer: string
export declare const treeNodeContent: string

View File

@ -0,0 +1,35 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createContext } from 'react'
import { noop } from 'lodash-es'
export interface NodeSpec<T = unknown> {
data: T
isLastChild?: boolean
}
interface TreeViewContextValue {
activePath: string
setActivePath: (path: string) => void
compact?: boolean
}
export const TreeViewContext = createContext<TreeViewContextValue>({
activePath: '',
setActivePath: noop,
compact: false
})

View File

@ -0,0 +1,4 @@
<svg height="100%" width="100%" xmlns="http://www.w3.org/2000/svg">
<line x1="50%" y1="0" x2="50%" y2="100%" stroke="#d9dae5" stroke-width="1"/>
<line x1="50%" y1="50%" x2="100%" y2="50%" stroke="#d9dae5" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 234 B

View File

@ -0,0 +1,4 @@
<svg height="100%" width="100%" xmlns="http://www.w3.org/2000/svg">
<line x1="50%" y1="0" x2="50%" y2="50%" stroke="#d9dae5" stroke-width="1"/>
<line x1="50%" y1="50%" x2="100%" y2="50%" stroke="#d9dae5" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

View File

@ -0,0 +1,3 @@
<svg height="100%" width="100%" xmlns="http://www.w3.org/2000/svg">
<line x1="50%" y1="0" x2="50%" y2="100%" stroke="#d9dae5" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 153 B

View File

@ -15,14 +15,22 @@
*/
import { createContext } from 'react'
import { noop } from 'lodash-es'
import type { AppstoreContext, Scope } from '@ar/MFEAppTypes'
import { Parent } from '@ar/common/types'
export enum RepositoryListViewTypeEnum {
LIST = 'list',
DIRECTORY = 'directory'
}
export interface ModuleAppStoreContextProps extends AppstoreContext, Record<string, unknown> {
baseUrl: string
matchPath: string
scope: Scope & Record<string, string>
parent: Parent
repositoryListViewType: RepositoryListViewTypeEnum
setRepositoryListViewType: (type: RepositoryListViewTypeEnum) => void
}
export const AppStoreContext = createContext<ModuleAppStoreContextProps>({
@ -33,5 +41,7 @@ export const AppStoreContext = createContext<ModuleAppStoreContextProps>({
matchPath: '',
scope: {},
accountInfo: {},
parent: Parent.OSS
parent: Parent.OSS,
repositoryListViewType: RepositoryListViewTypeEnum.LIST,
setRepositoryListViewType: noop
})

View File

@ -15,6 +15,7 @@
*/
import type { IconName } from '@harnessio/icons'
import type { RegistryMetadata } from '@harnessio/react-har-service-client'
import type { UpstreamRepositoryURLInputSource } from '@ar/pages/upstream-proxy-details/types'
import type { FormikFowardRef, RepositoryPackageType, RepositoryConfigType, PageType, Scanners } from '@ar/common/types'
import type { StringKeys } from '../strings'
@ -48,6 +49,11 @@ export interface RepositoryDetailsHeaderProps<T> {
type: RepositoryConfigType
}
export interface RepositoryTreeNodeProps {
data: RegistryMetadata
isLastChild?: boolean
}
export abstract class RepositoryStep<T, U = unknown> {
protected abstract packageType: RepositoryPackageType
protected abstract repositoryName: string
@ -132,4 +138,8 @@ export abstract class RepositoryStep<T, U = unknown> {
abstract renderRepositoryDetailsHeader(props: RepositoryDetailsHeaderProps<U>): JSX.Element
abstract renderRedirectPage(): JSX.Element
abstract renderTreeNodeView(props: RepositoryTreeNodeProps): JSX.Element
abstract renderTreeNodeDetails(): JSX.Element
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryPackageType } from '@ar/common/types'
import repositoryFactory from './RepositoryFactory'
import type { RepositoryAbstractFactory } from './RepositoryAbstractFactory'
interface RepositoryTreeNodeDetailsWidgetProps {
packageType: RepositoryPackageType
factory?: RepositoryAbstractFactory
}
export default function RepositoryTreeNodeDetailsWidget(props: RepositoryTreeNodeDetailsWidgetProps): JSX.Element {
const { packageType, factory = repositoryFactory } = props
const { getString } = useStrings()
const repositoryType = factory?.getRepositoryType(packageType as RepositoryPackageType)
if (!repositoryType) {
return <Text intent="warning">{getString('stepNotFound')}</Text>
}
return repositoryType.renderTreeNodeDetails()
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryPackageType } from '@ar/common/types'
import repositoryFactory from './RepositoryFactory'
import type { RepositoryTreeNodeProps } from './Repository'
import type { RepositoryAbstractFactory } from './RepositoryAbstractFactory'
interface RepositoryTreeNodeViewWidgetProps extends RepositoryTreeNodeProps {
packageType: RepositoryPackageType
factory?: RepositoryAbstractFactory
}
export default function RepositoryTreeNodeViewWidget(props: RepositoryTreeNodeViewWidgetProps): JSX.Element {
const { packageType, factory = repositoryFactory, ...rest } = props
const { getString } = useStrings()
const repositoryType = factory?.getRepositoryType(packageType as RepositoryPackageType)
if (!repositoryType) {
return <Text intent="warning">{getString('stepNotFound')}</Text>
}
return repositoryType.renderTreeNodeView({ ...rest })
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryPackageType } from '@ar/common/types'
import versionFactory from './VersionFactory'
import type { VersionAbstractFactory } from './VersionAbstractFactory'
interface ArtifactTreeNodeDetailsWidgetProps {
packageType: RepositoryPackageType
factory?: VersionAbstractFactory
}
export default function ArtifactTreeNodeDetailsWidget(props: ArtifactTreeNodeDetailsWidgetProps): JSX.Element {
const { packageType, factory = versionFactory } = props
const { getString } = useStrings()
const repositoryType = factory?.getVersionType(packageType as RepositoryPackageType)
if (!repositoryType) {
return <Text intent="warning">{getString('stepNotFound')}</Text>
}
return repositoryType.renderArtifactTreeNodeDetails()
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryPackageType } from '@ar/common/types'
import versionFactory from './VersionFactory'
import type { ArtifactTreeNodeViewProps } from './Version'
import type { VersionAbstractFactory } from './VersionAbstractFactory'
interface ArtifactTreeNodeViewWidgetProps extends ArtifactTreeNodeViewProps {
packageType: RepositoryPackageType
factory?: VersionAbstractFactory
}
export default function ArtifactTreeNodeViewWidget(props: ArtifactTreeNodeViewWidgetProps): JSX.Element {
const { packageType, factory = versionFactory, ...rest } = props
const { getString } = useStrings()
const repositoryType = factory?.getVersionType(packageType as RepositoryPackageType)
if (!repositoryType) {
return <Text intent="warning">{getString('stepNotFound')}</Text>
}
return repositoryType.renderArtifactTreeNodeView({ ...rest })
}

View File

@ -21,10 +21,12 @@ import type {
ArtifactVersionMetadata,
ArtifactVersionSummary,
ListArtifactVersion,
RegistryArtifactMetadata
RegistryArtifactMetadata,
RegistryMetadata
} from '@harnessio/react-har-service-client'
import type { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
import type { NodeSpec } from '@ar/components/TreeView/TreeViewContext'
import type { PageType, Parent, RepositoryPackageType } from '@ar/common/types'
import type { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
export interface VersionDetailsHeaderProps<T> {
data: T
@ -69,6 +71,23 @@ export interface ArtifactRowSubComponentProps {
data: ArtifactMetadata
}
export interface ArtifactTreeNodeViewProps {
data: RegistryArtifactMetadata
parentNodeLevels: Array<NodeSpec<RegistryMetadata>>
isLastChild?: boolean
}
export interface VersionTreeNodeViewProps {
data: ArtifactVersionMetadata
artifactIdentifier: string
parentNodeLevels: Array<NodeSpec<RegistryMetadata | RegistryArtifactMetadata>>
isLastChild?: boolean
}
export interface VersionTreeNodeDetailsProps {
data: ArtifactVersionSummary
}
export abstract class VersionStep<T> {
protected abstract packageType: RepositoryPackageType
protected abstract allowedVersionDetailsTabs: VersionDetailsTab[]
@ -97,4 +116,12 @@ export abstract class VersionStep<T> {
abstract renderVersionActions(props: VersionActionProps): JSX.Element
abstract renderArtifactRowSubComponent(props: ArtifactRowSubComponentProps): JSX.Element
abstract renderArtifactTreeNodeView(props: ArtifactTreeNodeViewProps): JSX.Element
abstract renderArtifactTreeNodeDetails(): JSX.Element
abstract renderVersionTreeNodeView(props: VersionTreeNodeViewProps): JSX.Element
abstract renderVersionTreeNodeDetails(props: VersionTreeNodeDetailsProps): JSX.Element
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryPackageType } from '@ar/common/types'
import versionFactory from './VersionFactory'
import type { VersionTreeNodeDetailsProps } from './Version'
import type { VersionAbstractFactory } from './VersionAbstractFactory'
interface VersionTreeNodeDetailsWidgetProps extends VersionTreeNodeDetailsProps {
packageType: RepositoryPackageType
factory?: VersionAbstractFactory
}
export default function VersionTreeNodeDetailsWidget(props: VersionTreeNodeDetailsWidgetProps): JSX.Element {
const { packageType, factory = versionFactory, ...rest } = props
const { getString } = useStrings()
const repositoryType = factory?.getVersionType(packageType as RepositoryPackageType)
if (!repositoryType) {
return <Text intent="warning">{getString('stepNotFound')}</Text>
}
return repositoryType.renderVersionTreeNodeDetails({ ...rest })
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryPackageType } from '@ar/common/types'
import versionFactory from './VersionFactory'
import type { VersionTreeNodeViewProps } from './Version'
import type { VersionAbstractFactory } from './VersionAbstractFactory'
interface VersionTreeNodeViewWidgetProps extends VersionTreeNodeViewProps {
packageType: RepositoryPackageType
factory?: VersionAbstractFactory
}
export default function VersionTreeNodeViewWidget(props: VersionTreeNodeViewWidgetProps): JSX.Element {
const { packageType, factory = versionFactory, ...rest } = props
const { getString } = useStrings()
const repositoryType = factory?.getVersionType(packageType as RepositoryPackageType)
if (!repositoryType) {
return <Text intent="warning">{getString('stepNotFound')}</Text>
}
return repositoryType.renderVersionTreeNodeView({ ...rest })
}

View File

@ -15,6 +15,8 @@
*/
import { defaultTo, isEmpty } from 'lodash-es'
import { routeDefinitionWithMode } from '@ar/routes/utils'
import type { ARRouteDefinitionsReturn } from '@ar/routes/RouteDefinitions'
export default function getARRouteDefinitions(routeParams: Record<string, string>): ARRouteDefinitionsReturn {
@ -34,17 +36,34 @@ export default function getARRouteDefinitions(routeParams: Record<string, string
}
return '/redirect'
},
toARRepositories: () => '/',
toARRepositoryDetails: params => `/${params?.repositoryIdentifier}`,
toARRepositoryDetailsTab: params => `/${params?.repositoryIdentifier}/${params?.tab}`,
toARRepositoryWebhookDetails: params => `/${params?.repositoryIdentifier}/webhooks/${params?.webhookIdentifier}`,
toARArtifacts: () => `/${routeParams?.repositoryIdentifier}/packages`,
toARArtifactDetails: params => `/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}`,
toARVersionDetails: params =>
`/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}`,
toARRepositories: routeDefinitionWithMode(() => '/'),
toARRepositoryDetails: routeDefinitionWithMode(params => `/${params?.repositoryIdentifier}`),
toARRepositoryDetailsTab: routeDefinitionWithMode(params => `/${params?.repositoryIdentifier}/${params?.tab}`),
toARArtifacts: routeDefinitionWithMode(() => `/${routeParams?.repositoryIdentifier}/packages`),
toARArtifactDetails: routeDefinitionWithMode(
params => `/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}`
),
toARVersionDetails: routeDefinitionWithMode(
params =>
`/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}`
),
// anything random, as this route will not be used in gitness
toARVersionDetailsTab: params =>
`/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}`,
toARVersionDetailsTab: routeDefinitionWithMode(params => {
let route = `/${params.repositoryIdentifier}/artifacts/${params.artifactIdentifier}/versions/${params.versionIdentifier}`
if (params.orgIdentifier) route += `/orgs/${params.orgIdentifier}`
if (params.projectIdentifier) route += `/projects/${params.projectIdentifier}`
if (params.sourceId && params.artifactId) {
route += `/artifact-sources/${params.sourceId}/artifacts/${params.artifactId}`
}
if (params.pipelineIdentifier && params.executionIdentifier) {
route += `/pipelines/${params.pipelineIdentifier}/executions/${params.executionIdentifier}`
}
route += `/${params.versionTab}`
return route
}),
toARRepositoryWebhookDetails: routeDefinitionWithMode(
params => `/${params?.repositoryIdentifier}/webhooks/${params?.webhookIdentifier}`
),
toARRepositoryWebhookDetailsTab: params =>
`/${params?.repositoryIdentifier}/webhooks/${params?.webhookIdentifier}/${params?.tab}`
}

View File

@ -26,3 +26,4 @@ export { useParentContextObj } from './useParentContextObj'
export { useLicenseStore } from './useLicenseStore'
export { useFeatureFlags, useFeatureFlag } from './useFeatureFlag'
export { useGetUpstreamRepositoryPackageTypes } from './useGetUpstreamRepositoryPackageTypes'
export { useGetRepositoryListViewType } from './useGetRepositoryListViewType'

View File

@ -0,0 +1,29 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RepositoryListViewTypeEnum } from '@ar/contexts/AppStoreContext'
import { useAppStore } from './useAppStore'
import { useFeatureFlags } from './useFeatureFlag'
export function useGetRepositoryListViewType() {
const { repositoryListViewType } = useAppStore()
const { HAR_TREE_VIEW_ENABLED } = useFeatureFlags()
if (!HAR_TREE_VIEW_ENABLED) {
return RepositoryListViewTypeEnum.LIST
}
return repositoryListViewType || RepositoryListViewTypeEnum.LIST
}

View File

@ -17,7 +17,7 @@
import { useMemo } from 'react'
import { mapValues } from 'lodash-es'
import { encodePathParams, normalizePath } from '@ar/routes/utils'
import { encodePathParams, IRouteOptions, normalizePath } from '@ar/routes/utils'
import { ARRouteDefinitionsReturn, routeDefinitions } from '@ar/routes/RouteDefinitions'
import { useAppStore } from './useAppStore'
@ -32,11 +32,11 @@ export function useRoutes(isRouteDestinationRendering = false): ARRouteDefinitio
const transformedRouteDefinitions: ARRouteDefinitionsReturn = useMemo(() => {
const finalRouteDefinitions =
typeof getRouteDefinitions === 'function' ? getRouteDefinitions(routeParams) : routeDefinitions
return mapValues(finalRouteDefinitions, route => (params: any = {}) => {
return mapValues(finalRouteDefinitions, route => (params: any = {}, options?: IRouteOptions) => {
const transformedParams: any = Object.keys(params).reduce((acc, curr) => {
return { ...acc, [curr]: encodePathParams(params[curr]) }
}, {})
return normalizePath(`${prefixUrl}/${route(transformedParams)}`)
return normalizePath(`${prefixUrl}/${route(transformedParams, options)}`)
})
}, [prefixUrl])

View File

@ -0,0 +1,30 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.cardContainer {
width: 60% !important;
min-width: 1040px;
padding: var(--spacing-7) !important;
background-color: var(--white);
}
.gridContainer {
align-items: center;
display: grid;
grid-template-columns: max-content auto;
row-gap: var(--spacing-medium);
column-gap: 30px;
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const cardContainer: string
export declare const gridContainer: string

View File

@ -0,0 +1,94 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react'
import { omit } from 'lodash-es'
import { useHistory } from 'react-router-dom'
import type { IconName } from '@harnessio/icons'
import type { RegistryArtifactMetadata, RegistryMetadata } from '@harnessio/react-har-service-client'
import { useParentHooks, useRoutes } from '@ar/hooks'
import TreeNode, { NodeTypeEnum } from '@ar/components/TreeView/TreeNode'
import TreeNodeContent from '@ar/components/TreeView/TreeNodeContent'
import { PageType, type RepositoryPackageType } from '@ar/common/types'
import { TreeViewContext } from '@ar/components/TreeView/TreeViewContext'
import type { ArtifactTreeNodeViewProps } from '@ar/frameworks/Version/Version'
import ArtifactActionsWidget from '@ar/frameworks/Version/ArtifactActionsWidget'
import VersionListTreeView from '@ar/pages/version-list/components/VersionListTreeView/VersionListTreeView'
interface IArtifactTreeNode extends ArtifactTreeNodeViewProps {
icon: IconName
iconSize?: number
level?: number
}
export default function ArtifactTreeNode(props: IArtifactTreeNode) {
const { data, icon, iconSize = 24, level = 1, isLastChild, parentNodeLevels } = props
const { setActivePath, activePath, compact } = useContext(TreeViewContext)
const { useQueryParams } = useParentHooks()
const queryParams = useQueryParams<Record<string, string>>()
const routes = useRoutes()
const history = useHistory()
const path = `${data.registryIdentifier}/${data.name}`
return (
<TreeNode<RegistryMetadata | RegistryArtifactMetadata>
key={path}
id={path}
level={level}
nodeType={NodeTypeEnum.Folder}
compact={compact}
isLastChild={isLastChild}
parentNodeLevels={parentNodeLevels}
isOpen={activePath.includes(path)}
isActive={activePath === path}
onClick={() => {
setActivePath(path)
history.push(
routes.toARArtifactDetails(
{
repositoryIdentifier: data.registryIdentifier,
artifactIdentifier: data.name
},
{ queryParams: omit(queryParams, 'digest') }
)
)
}}
heading={
<TreeNodeContent
icon={icon}
iconSize={iconSize}
label={data.name}
downloads={data.downloadsCount}
compact={compact}
/>
}
actionElement={
<ArtifactActionsWidget
packageType={data.packageType as RepositoryPackageType}
data={data}
repoKey={data.registryIdentifier}
artifactKey={data.name}
pageType={PageType.Table}
/>
}>
<VersionListTreeView
parentNodeLevels={[...parentNodeLevels, { data, isLastChild }]}
registryIdentifier={data.registryIdentifier}
artifactIdentifier={data.name}
/>
</TreeNode>
)
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react'
import type { RepositoryPackageType } from '@ar/common/types'
import ArtifactTreeNodeDetailsWidget from '@ar/frameworks/Version/ArtifactTreeNodeDetailsWidget'
import { ArtifactProviderContext } from '../../context/ArtifactProvider'
export default function ArtifactTreeNodeDetails() {
const { data } = useContext(ArtifactProviderContext)
if (!data) return null
const { packageType } = data
return <ArtifactTreeNodeDetailsWidget packageType={packageType as RepositoryPackageType} />
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react'
import { FontVariation } from '@harnessio/design-system'
import { Card, Container, Layout, Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import { DEFAULT_DATE_TIME_FORMAT } from '@ar/constants'
import { getReadableDateTime } from '@ar/common/dateUtils'
import { LabelValueTypeEnum } from '@ar/pages/version-details/components/LabelValueContent/type'
import { LabelValueContent } from '@ar/pages/version-details/components/LabelValueContent/LabelValueContent'
import { ArtifactProviderContext } from '../../context/ArtifactProvider'
import css from './ArtifactTreeNode.module.scss'
export default function ArtifactTreeNodeDetailsContent(): JSX.Element {
const { data } = useContext(ArtifactProviderContext)
const { getString } = useStrings()
if (!data) return <></>
return (
<Layout.Vertical spacing="small" padding="large">
<Text font={{ variation: FontVariation.CARD_TITLE }}>{getString('details')}</Text>
<Card className={css.cardContainer}>
<Container className={css.gridContainer}>
<LabelValueContent
label={getString('versionDetails.overview.generalInformation.name')}
value={data.imageName}
type={LabelValueTypeEnum.Text}
/>
<LabelValueContent
label={getString('versionDetails.overview.generalInformation.downloads')}
value={data.downloadsCount}
type={LabelValueTypeEnum.Text}
/>
<LabelValueContent
label={getString('versionDetails.overview.generalInformation.createdAt')}
value={getReadableDateTime(Number(data.createdAt), DEFAULT_DATE_TIME_FORMAT)}
type={LabelValueTypeEnum.Text}
/>
<LabelValueContent
label={getString('versionDetails.overview.generalInformation.modifiedAt')}
value={getReadableDateTime(Number(data.modifiedAt), DEFAULT_DATE_TIME_FORMAT)}
type={LabelValueTypeEnum.Text}
/>
</Container>
</Card>
</Layout.Vertical>
)
}

View File

@ -18,8 +18,9 @@ import React, { createContext, type FC, type PropsWithChildren } from 'react'
import { ArtifactSummary, useGetArtifactSummaryQuery } from '@harnessio/react-har-service-client'
import { PageError, PageSpinner } from '@harnessio/uicore'
import { useGetSpaceRef } from '@ar/hooks'
import { encodeRef } from '@ar/hooks/useGetSpaceRef'
import { useDecodedParams, useGetSpaceRef } from '@ar/hooks'
import type { ArtifactDetailsPathParams } from '@ar/routes/types'
export interface ArtifactProviderProps {
data: ArtifactSummary | undefined
@ -29,12 +30,13 @@ export interface ArtifactProviderProps {
export const ArtifactProviderContext = createContext<ArtifactProviderProps>({} as ArtifactProviderProps)
const ArtifactProvider: FC<PropsWithChildren<{ repoKey: string; artifact: string }>> = ({
const ArtifactProvider: FC<PropsWithChildren<{ repoKey?: string; artifact?: string }>> = ({
children,
repoKey,
artifact
}): JSX.Element => {
const spaceRef = useGetSpaceRef(repoKey)
const { repositoryIdentifier, artifactIdentifier } = useDecodedParams<ArtifactDetailsPathParams>()
const spaceRef = useGetSpaceRef(repoKey ?? repositoryIdentifier)
const {
data,
isFetching: loading,
@ -42,7 +44,7 @@ const ArtifactProvider: FC<PropsWithChildren<{ repoKey: string; artifact: string
refetch
} = useGetArtifactSummaryQuery({
registry_ref: spaceRef,
artifact: encodeRef(artifact)
artifact: encodeRef(artifact ?? artifactIdentifier)
})
const responseData = data?.content?.data

View File

@ -0,0 +1,119 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
import {
type Error,
getAllArtifactsByRegistry,
type GetAllArtifactsByRegistryOkResponse,
type RegistryMetadata
} from '@harnessio/react-har-service-client'
import { DEFAULT_PAGE_SIZE } from '@ar/constants'
import { useStrings } from '@ar/frameworks/strings'
import TreeBody from '@ar/components/TreeView/TreeBody'
import { useGetSpaceRef, useParentHooks } from '@ar/hooks'
import type { RepositoryPackageType } from '@ar/common/types'
import TreeNodeList from '@ar/components/TreeView/TreeNodeList'
import TreeLoadMoreNode from '@ar/components/TreeView/TreeLoadMoreNode'
import type { NodeSpec } from '@ar/components/TreeView/TreeViewContext'
import TreeNodeSearchInput from '@ar/components/TreeView/TreeNodeSearchInput'
import ArtifactTreeNodeViewWidget from '@ar/frameworks/Version/ArtifactTreeNodeViewWidget'
import {
type RegistryArtifactListPageQueryParams,
useRegistryArtifactListQueryParamOptions
} from '../RegistryArtifactListTable/utils'
interface ArtifactListTreeViewProps {
registryIdentifier: string
parentNodeLevels: Array<NodeSpec<RegistryMetadata>>
}
export default function ArtifactListTreeView(props: ArtifactListTreeViewProps) {
const { registryIdentifier } = props
const { useQueryParams, useUpdateQueryParams } = useParentHooks()
const { updateQueryParams } = useUpdateQueryParams<Partial<RegistryArtifactListPageQueryParams>>()
const queryParams = useQueryParams<RegistryArtifactListPageQueryParams>(useRegistryArtifactListQueryParamOptions())
const { artifactSearchTerm } = queryParams
const { getString } = useStrings()
const registryRef = useGetSpaceRef(registryIdentifier)
const { data, isLoading, error, hasNextPage, fetchNextPage, refetch, isFetchingNextPage } = useInfiniteQuery<
GetAllArtifactsByRegistryOkResponse,
Error,
GetAllArtifactsByRegistryOkResponse,
Array<string | undefined>
>({
queryKey: ['artifactList', registryIdentifier, artifactSearchTerm],
queryFn: ({ pageParam = 0 }) =>
getAllArtifactsByRegistry({
registry_ref: registryRef,
queryParams: {
page: pageParam,
size: DEFAULT_PAGE_SIZE,
search_term: artifactSearchTerm
},
stringifyQueryParamsOptions: {
arrayFormat: 'repeat'
}
}),
getNextPageParam: lastPage => {
const totalPages = lastPage.content.data.pageCount ?? 0
const lastPageNumber = lastPage.content.data.pageIndex ?? -1
const nextPage = lastPageNumber + 1
return nextPage < totalPages ? nextPage : undefined
}
})
const { list: artifactList, count: totalArtifacts } = useMemo(() => {
const list = data?.pages.flatMap(page => page.content.data.artifacts) || []
const count = data?.pages[0].content.data.itemCount || 0
return { list, count }
}, [data])
return (
<TreeNodeList>
{totalArtifacts > DEFAULT_PAGE_SIZE && (
<TreeNodeSearchInput
level={1}
defaultValue={artifactSearchTerm}
onChange={val => {
updateQueryParams({ artifactSearchTerm: val })
}}
/>
)}
<TreeBody
loading={isLoading}
error={error?.message}
retryOnError={refetch}
isEmpty={!artifactList.length}
emptyDataMessage={getString('artifactList.table.noArtifactsTitle')}>
{artifactList.map((each, indx) => (
<ArtifactTreeNodeViewWidget
key={each.name}
packageType={each.packageType as RepositoryPackageType}
data={each}
isLastChild={indx === artifactList.length - 1}
parentNodeLevels={props.parentNodeLevels}
/>
))}
{hasNextPage && <TreeLoadMoreNode level={1} onClick={() => fetchNextPage()} disabled={isFetchingNextPage} />}
</TreeBody>
</TreeNodeList>
)
}

View File

@ -26,6 +26,7 @@ export type RegistryArtifactListPageQueryParams = {
size: number
sort: string[]
searchTerm?: string
artifactSearchTerm?: string
isDeployedArtifacts: boolean
packageTypes: RepositoryPackageType[]
repositoryKey?: string

View File

@ -0,0 +1,138 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext, useState } from 'react'
import { useHistory } from 'react-router-dom'
import {
type ArtifactVersionMetadata,
type DockerManifestDetails,
type RegistryArtifactMetadata,
type RegistryMetadata,
useGetDockerArtifactManifestsQuery
} from '@harnessio/react-har-service-client'
import { useGetSpaceRef, useParentHooks, useRoutes } from '@ar/hooks'
import { encodeRef } from '@ar/hooks/useGetSpaceRef'
import TreeNode, { NodeTypeEnum } from '@ar/components/TreeView/TreeNode'
import TreeBody from '@ar/components/TreeView/TreeBody'
import TreeNodeList from '@ar/components/TreeView/TreeNodeList'
import TreeNodeContent from '@ar/components/TreeView/TreeNodeContent'
import { type NodeSpec, TreeViewContext } from '@ar/components/TreeView/TreeViewContext'
import { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
import { getShortDigest } from '../../utils'
interface DigestListTreeViewProps {
registryIdentifier: string
artifactIdentifier: string
versionIdentifier: string
parentNodeLevels: Array<NodeSpec<RegistryMetadata | RegistryArtifactMetadata | ArtifactVersionMetadata>>
}
export default function DigestListTreeView(props: DigestListTreeViewProps) {
const { registryIdentifier, artifactIdentifier, versionIdentifier, parentNodeLevels } = props
const [page] = useState(0)
const [searchTerm] = useState('')
const routes = useRoutes()
const history = useHistory()
const { setActivePath, activePath, compact } = useContext(TreeViewContext)
const { useQueryParams } = useParentHooks()
const queryParams = useQueryParams<Record<string, string>>()
const registryRef = useGetSpaceRef(registryIdentifier)
const {
data,
refetch,
isFetching: loading,
error
} = useGetDockerArtifactManifestsQuery({
registry_ref: registryRef,
artifact: encodeRef(artifactIdentifier),
version: versionIdentifier,
queryParams: {
page,
size: 100,
search_term: searchTerm
},
stringifyQueryParamsOptions: {
arrayFormat: 'repeat'
}
})
const manifestList = data?.content.data.manifests || []
return (
<TreeNodeList>
<TreeBody
loading={loading}
error={error?.message}
retryOnError={refetch}
isEmpty={!manifestList.length}
emptyDataMessage="digestList.table.noDigestTitle">
{manifestList.map((each, idx) => {
const path = `${registryIdentifier}/${artifactIdentifier}/${versionIdentifier}/${each.digest}`
const isLastChild = idx === manifestList.length - 1
return (
<TreeNode<
| RegistryMetadata
| RegistryArtifactMetadata
| ArtifactVersionMetadata
| ArtifactVersionMetadata
| DockerManifestDetails
>
key={path}
id={path}
level={3}
compact={compact}
nodeType={NodeTypeEnum.File}
isOpen={activePath.includes(path)}
isActive={activePath === path}
isLastChild={isLastChild}
parentNodeLevels={parentNodeLevels}
onClick={() => {
setActivePath(path)
history.push(
routes.toARVersionDetailsTab(
{
repositoryIdentifier: registryIdentifier,
artifactIdentifier,
versionIdentifier,
versionTab: VersionDetailsTab.OVERVIEW
},
{
queryParams: {
...queryParams,
digest: each.digest
}
}
)
)
}}
heading={
<TreeNodeContent
icon="file"
iconSize={20}
size={each.size}
compact={compact}
label={getShortDigest(each.digest)}
downloads={each.downloadsCount}
/>
}
/>
)
})}
</TreeBody>
</TreeNodeList>
)
}

View File

@ -23,6 +23,7 @@ import type {
RepositoryActionsProps,
RepositoryConfigurationFormProps,
RepositoryDetailsHeaderProps,
RepositoryTreeNodeProps,
RepositoySetupClientProps
} from '@ar/frameworks/RepositoryStep/Repository'
@ -38,12 +39,14 @@ import {
} from '@ar/pages/upstream-proxy-details/types'
import type { Repository, VirtualRegistryRequest } from '@ar/pages/repository-details/types'
import RepositoryDetails from '../RepositoryDetails'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import DockerRedirectPage from './DockerRedirectPage/DockerRedirectPage'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryTreeNode from '../components/RepositoryTreeNode/RepositoryTreeNode'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
import DockerRedirectPage from './DockerRedirectPage/DockerRedirectPage'
export class DockerRepositoryType extends RepositoryStep<VirtualRegistryRequest> {
protected packageType = RepositoryPackageType.DOCKER
@ -131,4 +134,12 @@ export class DockerRepositoryType extends RepositoryStep<VirtualRegistryRequest>
renderRedirectPage(): JSX.Element {
return <DockerRedirectPage />
}
renderTreeNodeView(props: RepositoryTreeNodeProps): JSX.Element {
return <RepositoryTreeNode {...props} icon={this.repositoryIcon} />
}
renderTreeNodeDetails(): JSX.Element {
return <RepositoryDetails />
}
}

View File

@ -24,15 +24,19 @@ import {
RepositoryConfigurationFormProps,
RepositoryDetailsHeaderProps,
RepositoryStep,
RepositoryTreeNodeProps,
RepositoySetupClientProps
} from '@ar/frameworks/RepositoryStep/Repository'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RepositoryDetails from '../RepositoryDetails'
import type { Repository, VirtualRegistryRequest } from '../types'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RedirectPageView from '../components/RedirectPageView/RedirectPageView'
import RepositoryTreeNode from '../components/RepositoryTreeNode/RepositoryTreeNode'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
export class GenericRepositoryType extends RepositoryStep<VirtualRegistryRequest> {
protected packageType = RepositoryPackageType.GENERIC
@ -89,4 +93,12 @@ export class GenericRepositoryType extends RepositoryStep<VirtualRegistryRequest
renderRedirectPage(): JSX.Element {
return <RedirectPageView />
}
renderTreeNodeView(props: RepositoryTreeNodeProps): JSX.Element {
return <RepositoryTreeNode {...props} icon={this.repositoryIcon} iconSize={20} />
}
renderTreeNodeDetails(): JSX.Element {
return <RepositoryDetails />
}
}

View File

@ -23,6 +23,7 @@ import type {
RepositoryActionsProps,
RepositoryConfigurationFormProps,
RepositoryDetailsHeaderProps,
RepositoryTreeNodeProps,
RepositoySetupClientProps
} from '@ar/frameworks/RepositoryStep/Repository'
@ -38,12 +39,14 @@ import {
} from '@ar/pages/upstream-proxy-details/types'
import type { Repository, VirtualRegistryRequest } from '@ar/pages/repository-details/types'
import RepositoryDetails from '../RepositoryDetails'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RedirectPageView from '../components/RedirectPageView/RedirectPageView'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryTreeNode from '../components/RepositoryTreeNode/RepositoryTreeNode'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RedirectPageView from '../components/RedirectPageView/RedirectPageView'
export class HelmRepositoryType extends RepositoryStep<VirtualRegistryRequest> {
protected packageType = RepositoryPackageType.HELM
@ -128,4 +131,12 @@ export class HelmRepositoryType extends RepositoryStep<VirtualRegistryRequest> {
renderRedirectPage(): JSX.Element {
return <RedirectPageView />
}
renderTreeNodeView(props: RepositoryTreeNodeProps): JSX.Element {
return <RepositoryTreeNode {...props} icon={this.repositoryIcon} />
}
renderTreeNodeDetails(): JSX.Element {
return <RepositoryDetails />
}
}

View File

@ -28,6 +28,7 @@ import {
type RepositoryConfigurationFormProps,
type RepositoryDetailsHeaderProps,
RepositoryStep,
type RepositoryTreeNodeProps,
type RepositoySetupClientProps
} from '@ar/frameworks/RepositoryStep/Repository'
import {
@ -36,10 +37,12 @@ import {
UpstreamRepositoryURLInputSource
} from '@ar/pages/upstream-proxy-details/types'
import RepositoryDetails from '../RepositoryDetails'
import type { Repository, VirtualRegistryRequest } from '../types'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RedirectPageView from '../components/RedirectPageView/RedirectPageView'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryTreeNode from '../components/RepositoryTreeNode/RepositoryTreeNode'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
@ -127,4 +130,12 @@ export class MavenRepositoryType extends RepositoryStep<VirtualRegistryRequest>
renderRedirectPage(): JSX.Element {
return <RedirectPageView />
}
renderTreeNodeView(props: RepositoryTreeNodeProps): JSX.Element {
return <RepositoryTreeNode {...props} icon={this.repositoryIcon} />
}
renderTreeNodeDetails(): JSX.Element {
return <RepositoryDetails />
}
}

View File

@ -28,6 +28,7 @@ import {
type RepositoryConfigurationFormProps,
type RepositoryDetailsHeaderProps,
RepositoryStep,
type RepositoryTreeNodeProps,
type RepositoySetupClientProps
} from '@ar/frameworks/RepositoryStep/Repository'
import {
@ -36,10 +37,12 @@ import {
UpstreamRepositoryURLInputSource
} from '@ar/pages/upstream-proxy-details/types'
import RepositoryDetails from '../RepositoryDetails'
import type { Repository, VirtualRegistryRequest } from '../types'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RedirectPageView from '../components/RedirectPageView/RedirectPageView'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryTreeNode from '../components/RepositoryTreeNode/RepositoryTreeNode'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
@ -127,4 +130,12 @@ export class NpmRepositoryType extends RepositoryStep<VirtualRegistryRequest> {
renderRedirectPage(): JSX.Element {
return <RedirectPageView />
}
renderTreeNodeView(props: RepositoryTreeNodeProps): JSX.Element {
return <RepositoryTreeNode {...props} icon={this.repositoryIcon} />
}
renderTreeNodeDetails(): JSX.Element {
return <RepositoryDetails />
}
}

View File

@ -28,6 +28,7 @@ import {
type RepositoryConfigurationFormProps,
type RepositoryDetailsHeaderProps,
RepositoryStep,
type RepositoryTreeNodeProps,
type RepositoySetupClientProps
} from '@ar/frameworks/RepositoryStep/Repository'
import {
@ -36,10 +37,12 @@ import {
UpstreamRepositoryURLInputSource
} from '@ar/pages/upstream-proxy-details/types'
import RepositoryDetails from '../RepositoryDetails'
import type { Repository, VirtualRegistryRequest } from '../types'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RedirectPageView from '../components/RedirectPageView/RedirectPageView'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryTreeNode from '../components/RepositoryTreeNode/RepositoryTreeNode'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
@ -127,4 +130,12 @@ export class NuGetRepositoryType extends RepositoryStep<VirtualRegistryRequest>
renderRedirectPage(): JSX.Element {
return <RedirectPageView />
}
renderTreeNodeView(props: RepositoryTreeNodeProps): JSX.Element {
return <RepositoryTreeNode {...props} icon={this.repositoryIcon} />
}
renderTreeNodeDetails(): JSX.Element {
return <RepositoryDetails />
}
}

View File

@ -28,6 +28,7 @@ import {
type RepositoryConfigurationFormProps,
type RepositoryDetailsHeaderProps,
RepositoryStep,
type RepositoryTreeNodeProps,
type RepositoySetupClientProps
} from '@ar/frameworks/RepositoryStep/Repository'
import {
@ -36,10 +37,12 @@ import {
UpstreamRepositoryURLInputSource
} from '@ar/pages/upstream-proxy-details/types'
import RepositoryDetails from '../RepositoryDetails'
import type { Repository, VirtualRegistryRequest } from '../types'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RedirectPageView from '../components/RedirectPageView/RedirectPageView'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryTreeNode from '../components/RepositoryTreeNode/RepositoryTreeNode'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
@ -127,4 +130,12 @@ export class PythonRepositoryType extends RepositoryStep<VirtualRegistryRequest>
renderRedirectPage(): JSX.Element {
return <RedirectPageView />
}
renderTreeNodeView(props: RepositoryTreeNodeProps): JSX.Element {
return <RepositoryTreeNode {...props} icon={this.repositoryIcon} />
}
renderTreeNodeDetails(): JSX.Element {
return <RepositoryDetails />
}
}

View File

@ -28,6 +28,7 @@ import {
type RepositoryConfigurationFormProps,
type RepositoryDetailsHeaderProps,
RepositoryStep,
type RepositoryTreeNodeProps,
type RepositoySetupClientProps
} from '@ar/frameworks/RepositoryStep/Repository'
import {
@ -36,10 +37,12 @@ import {
UpstreamRepositoryURLInputSource
} from '@ar/pages/upstream-proxy-details/types'
import RepositoryDetails from '../RepositoryDetails'
import type { Repository, VirtualRegistryRequest } from '../types'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RedirectPageView from '../components/RedirectPageView/RedirectPageView'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryTreeNode from '../components/RepositoryTreeNode/RepositoryTreeNode'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
@ -124,4 +127,12 @@ export class RPMRepositoryType extends RepositoryStep<VirtualRegistryRequest> {
renderRedirectPage(): JSX.Element {
return <RedirectPageView />
}
renderTreeNodeView(props: RepositoryTreeNodeProps): JSX.Element {
return <RepositoryTreeNode {...props} icon={this.repositoryIcon} />
}
renderTreeNodeDetails(): JSX.Element {
return <RepositoryDetails />
}
}

View File

@ -23,13 +23,17 @@ import { Button, ButtonVariation, Container, Layout, Tab, Tabs } from '@harnessi
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryDetailsPathParams } from '@ar/routes/types'
import RouteProvider from '@ar/components/RouteProvider/RouteProvider'
import { useDecodedParams, useFeatureFlags, useParentComponents, useRoutes } from '@ar/hooks'
import {
useDecodedParams,
useFeatureFlags,
useGetRepositoryListViewType,
useParentComponents,
useRoutes
} from '@ar/hooks'
import { PermissionIdentifier, ResourceType } from '@ar/common/permissionTypes'
import { RepositoryConfigType, RepositoryPackageType } from '@ar/common/types'
import RepositoryDetailsHeaderWidget from '@ar/frameworks/RepositoryStep/RepositoryDetailsHeaderWidget'
import { repositoryDetailsPathProps, repositoryDetailsTabPathProps } from '@ar/routes/RouteDestinations'
import { RepositoryDetailsTab } from './constants'
import { RepositoryDetailsTab, RepositoryDetailsTabs } from './constants'
import RepositoryDetailsTabPage from './RepositoryDetailsTabPage'
import { RepositoryProviderContext } from './context/RepositoryProvider'
import css from './RepositoryDetailsPage.module.scss'
@ -37,7 +41,7 @@ import css from './RepositoryDetailsPage.module.scss'
export default function RepositoryDetails(): JSX.Element | null {
const { RbacButton } = useParentComponents()
const { getString } = useStrings()
const { HAR_TRIGGERS } = useFeatureFlags()
const featureFlags = useFeatureFlags()
const pathParams = useDecodedParams<RepositoryDetailsPathParams>()
const { repositoryIdentifier } = pathParams
const [activeTab, setActiveTab] = useState('')
@ -46,6 +50,7 @@ export default function RepositoryDetails(): JSX.Element | null {
const routeDefinitions = useRoutes(true)
const history = useHistory()
const routes = useRoutes()
const repositoryListViewType = useGetRepositoryListViewType()
const { isDirty, data, isUpdating } = useContext(RepositoryProviderContext)
@ -90,22 +95,17 @@ export default function RepositoryDetails(): JSX.Element | null {
if (!data) return null
const isNotUpstreamRegistry = data.config.type !== RepositoryConfigType.UPSTREAM
return (
<>
<RepositoryDetailsHeaderWidget
data={data}
packageType={data.packageType as RepositoryPackageType}
type={data.config.type as RepositoryConfigType}
/>
<Container className={css.tabsContainer}>
<Tabs id="repositoryTabDetails" selectedTabId={activeTab} onChange={handleTabChange}>
<Tab id={RepositoryDetailsTab.PACKAGES} title={getString('repositoryDetails.tabs.packages')} />
<Tab id={RepositoryDetailsTab.CONFIGURATION} title={getString('repositoryDetails.tabs.configuration')} />
{HAR_TRIGGERS && isNotUpstreamRegistry && (
<Tab id={RepositoryDetailsTab.WEBHOOKS} title={getString('repositoryDetails.tabs.webhooks')} />
)}
{RepositoryDetailsTabs.filter(each => !each.featureFlag || featureFlags[each.featureFlag])
.filter(each => !each.packageType || each.packageType === data.packageType)
.filter(each => !each.type || each.type === data.config.type)
.filter(each => !each.mode || each.mode === repositoryListViewType)
.map(each => (
<Tab key={each.value} id={each.value} title={getString(each.label)} />
))}
<Expander />
{activeTab === RepositoryDetailsTab.CONFIGURATION && renderActionBtns()}
</Tabs>

View File

@ -15,6 +15,7 @@
*/
import React from 'react'
import RepositoryHeader from './RepositoryHeader'
import RepositoryDetails from './RepositoryDetails'
import RepositoryProvider from './context/RepositoryProvider'
@ -23,6 +24,7 @@ import './RepositoryFactory'
export default function RepositoryDetailsPage(): JSX.Element {
return (
<RepositoryProvider>
<RepositoryHeader />
<RepositoryDetails />
</RepositoryProvider>
)

View File

@ -0,0 +1,34 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react'
import type { RepositoryConfigType, RepositoryPackageType } from '@ar/common/types'
import RepositoryDetailsHeaderWidget from '@ar/frameworks/RepositoryStep/RepositoryDetailsHeaderWidget'
import { RepositoryProviderContext } from './context/RepositoryProvider'
export default function RepositoryHeader() {
const { data } = useContext(RepositoryProviderContext)
if (!data) return null
return (
<RepositoryDetailsHeaderWidget
data={data}
packageType={data.packageType as RepositoryPackageType}
type={data.config.type as RepositoryConfigType}
/>
)
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react'
import { defaultTo, omit } from 'lodash-es'
import { useHistory } from 'react-router-dom'
import type { IconName } from '@harnessio/icons'
import { useParentHooks, useRoutes } from '@ar/hooks'
import TreeNode, { NodeTypeEnum } from '@ar/components/TreeView/TreeNode'
import TreeNodeContent from '@ar/components/TreeView/TreeNodeContent'
import { TreeViewContext } from '@ar/components/TreeView/TreeViewContext'
import type { RepositoryTreeNodeProps } from '@ar/frameworks/RepositoryStep/Repository'
import RepositoryActionsWidget from '@ar/frameworks/RepositoryStep/RepositoryActionsWidget'
import { PageType, type RepositoryConfigType, type RepositoryPackageType } from '@ar/common/types'
import ArtifactListTreeView from '@ar/pages/artifact-list/components/ArtifactListTreeView/ArtifactListTreeView'
import { RepositoryDetailsTab } from '../../constants'
interface IRepositoryTreeNode extends RepositoryTreeNodeProps {
icon: IconName
iconSize?: number
level?: number
}
export default function RepositoryTreeNode(props: IRepositoryTreeNode) {
const { data, icon, iconSize = 24, level = 0, isLastChild } = props
const { setActivePath, activePath, compact } = useContext(TreeViewContext)
const { useQueryParams } = useParentHooks()
const queryParams = useQueryParams<Record<string, string>>()
const routes = useRoutes()
const history = useHistory()
const path = data.identifier
return (
<TreeNode
key={path}
id={path}
level={level}
compact={compact}
parentNodeLevels={[]}
nodeType={NodeTypeEnum.Folder}
isOpen={activePath.includes(path)}
isActive={activePath === path}
isLastChild={isLastChild}
onClick={() => {
setActivePath(path)
history.push(
routes.toARRepositoryDetailsTab(
{
repositoryIdentifier: data.identifier,
tab: RepositoryDetailsTab.CONFIGURATION
},
{ queryParams: omit(queryParams, 'digest') }
)
)
}}
heading={
<TreeNodeContent
icon={icon}
iconSize={iconSize}
label={data.identifier}
type={data.type as RepositoryConfigType}
artifacts={defaultTo(data.artifactsCount, 0)}
downloads={defaultTo(data.downloadsCount, 0)}
size={data.registrySize}
compact={compact}
/>
}
actionElement={
<RepositoryActionsWidget
packageType={data.packageType as RepositoryPackageType}
readonly={false}
data={data}
type={data.type as RepositoryConfigType}
pageType={PageType.Table}
/>
}>
<ArtifactListTreeView registryIdentifier={data.identifier} parentNodeLevels={[{ data, isLastChild }]} />
</TreeNode>
)
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react'
import type { RepositoryPackageType } from '@ar/common/types'
import RepositoryTreeNodeDetailsWidget from '@ar/frameworks/RepositoryStep/RepositoryTreeNodeDetailsWidget'
import { RepositoryProviderContext } from '../../context/RepositoryProvider'
export default function RepositoryTreeNodeDetails() {
const { data } = useContext(RepositoryProviderContext)
if (!data) return null
const { packageType } = data
return <RepositoryTreeNodeDetailsWidget packageType={packageType as RepositoryPackageType} />
}

View File

@ -17,7 +17,10 @@
import type { IconName } from '@harnessio/icons'
import type { Scanner } from '@harnessio/react-har-service-client'
import type { Scanners } from '@ar/common/types'
import { FeatureFlags } from '@ar/MFEAppTypes'
import type { StringKeys } from '@ar/frameworks/strings'
import { RepositoryListViewTypeEnum } from '@ar/contexts/AppStoreContext'
import { RepositoryConfigType, RepositoryPackageType, Scanners } from '@ar/common/types'
export enum RepositoryDetailsTab {
PACKAGES = 'packages',
@ -44,3 +47,30 @@ export const ContainerScannerConfig: Record<Scanners, ScannerConfigSpec> = {
value: 'GRYPE'
}
}
interface RepositoryDetailsTabSpec {
label: StringKeys
value: RepositoryDetailsTab
packageType?: RepositoryPackageType
type?: RepositoryConfigType
mode?: RepositoryListViewTypeEnum
featureFlag?: FeatureFlags
}
export const RepositoryDetailsTabs: RepositoryDetailsTabSpec[] = [
{
label: 'repositoryDetails.tabs.packages',
value: RepositoryDetailsTab.PACKAGES,
mode: RepositoryListViewTypeEnum.LIST
},
{
label: 'repositoryDetails.tabs.configuration',
value: RepositoryDetailsTab.CONFIGURATION
},
{
label: 'repositoryDetails.tabs.webhooks',
value: RepositoryDetailsTab.WEBHOOKS,
featureFlag: FeatureFlags.HAR_TRIGGERS,
type: RepositoryConfigType.VIRTUAL
}
]

View File

@ -19,6 +19,11 @@
background-color: var(--primary-bg) !important;
}
.treeViewPageBody {
--page-header-height: 145px;
display: flex;
}
.pageHeader {
height: var(--har-repository-list-page-header-height) !important;
background-color: var(--white) !important;

View File

@ -20,3 +20,4 @@ export declare const pageBody: string
export declare const pageHeader: string
export declare const subHeader: string
export declare const subHeaderItems: string
export declare const treeViewPageBody: string

View File

@ -17,13 +17,29 @@
import React, { useMemo, useRef } from 'react'
import { flushSync } from 'react-dom'
import { Expander } from '@blueprintjs/core'
import { ExpandingSearchInput, HarnessDocTooltip, Page, Button, ButtonVariation } from '@harnessio/uicore'
import {
ExpandingSearchInput,
HarnessDocTooltip,
Page,
Button,
ButtonVariation,
GridListToggle,
Views
} from '@harnessio/uicore'
import type { ExpandingSearchInputHandle } from '@harnessio/uicore'
import { useGetAllRegistriesQuery } from '@harnessio/react-har-service-client'
import { useStrings } from '@ar/frameworks/strings'
import { DEFAULT_PAGE_INDEX, PreferenceScope } from '@ar/constants'
import { useParentComponents, useParentHooks, useGetSpaceRef } from '@ar/hooks'
import { RepositoryListViewTypeEnum } from '@ar/contexts/AppStoreContext'
import {
useParentComponents,
useParentHooks,
useGetSpaceRef,
useFeatureFlags,
useAppStore,
useGetRepositoryListViewType
} from '@ar/hooks'
import PackageTypeSelector from '@ar/components/PackageTypeSelector/PackageTypeSelector'
import { CreateRepository } from './components/CreateRepository/CreateRepository'
@ -39,7 +55,10 @@ function RepositoryListPage(): JSX.Element {
const { getString } = useStrings()
const { NGBreadcrumbs } = useParentComponents()
const { useQueryParams, useUpdateQueryParams, usePreferenceStore } = useParentHooks()
const { HAR_TREE_VIEW_ENABLED } = useFeatureFlags()
const { updateQueryParams } = useUpdateQueryParams<Partial<ArtifactRepositoryListPageQueryParams>>()
const { setRepositoryListViewType } = useAppStore()
const repositoryListViewType = useGetRepositoryListViewType()
const spaceRef = useGetSpaceRef()
const queryParamOptions = useArtifactRepositoriesQueryParamOptions()
@ -130,6 +149,16 @@ function RepositoryListPage(): JSX.Element {
defaultValue={searchTerm}
ref={searchRef}
/>
{HAR_TREE_VIEW_ENABLED && (
<GridListToggle
initialSelectedView={repositoryListViewType === RepositoryListViewTypeEnum.LIST ? Views.LIST : Views.GRID}
icons={{ left: 'SplitView' }}
onViewToggle={newView => {
if (newView === Views.LIST) return
setRepositoryListViewType(RepositoryListViewTypeEnum.DIRECTORY)
}}
/>
)}
</div>
</Page.SubHeader>
<Page.Body

View File

@ -0,0 +1,101 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Expander } from '@blueprintjs/core'
import { HarnessDocTooltip, Page, GridListToggle, Views } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import { DEFAULT_PAGE_INDEX } from '@ar/constants'
import { RepositoryListViewTypeEnum } from '@ar/contexts/AppStoreContext'
import { useAppStore, useGetRepositoryListViewType, useParentComponents, useParentHooks } from '@ar/hooks'
import PackageTypeSelector from '@ar/components/PackageTypeSelector/PackageTypeSelector'
import TableFilterCheckbox from '@ar/components/TableFilterCheckbox/TableFilterCheckbox'
import { useArtifactRepositoriesQueryParamOptions } from './utils'
import type { ArtifactRepositoryListPageQueryParams } from './utils'
import { CreateRepository } from './components/CreateRepository/CreateRepository'
import RepositoryTypeSelector from './components/RepositoryTypeSelector/RepositoryTypeSelector'
import RepositoryListTreeView from './components/RepositoryListTreeView/RepositoryListTreeView'
import css from './RepositoryListPage.module.scss'
function RepositoryListTreeViewPage(): JSX.Element {
const { getString } = useStrings()
const { NGBreadcrumbs } = useParentComponents()
const { useQueryParams, useUpdateQueryParams } = useParentHooks()
const { updateQueryParams } = useUpdateQueryParams<Partial<ArtifactRepositoryListPageQueryParams>>()
const { setRepositoryListViewType } = useAppStore()
const repositoryListViewType = useGetRepositoryListViewType()
const queryParamOptions = useArtifactRepositoriesQueryParamOptions()
const queryParams = useQueryParams<ArtifactRepositoryListPageQueryParams>(queryParamOptions)
const { repositoryTypes, configType, compact } = queryParams
return (
<>
<Page.Header
className={css.pageHeader}
title={
<div className="ng-tooltip-native">
<h2 data-tooltip-id="artifactRepositoriesPageHeading">{getString('repositoryList.pageHeading')}</h2>
<HarnessDocTooltip tooltipId="artifactRepositoriesPageHeading" useStandAlone={true} />
</div>
}
breadcrumbs={<NGBreadcrumbs links={[]} />}
/>
<Page.SubHeader className={css.subHeader}>
<div className={css.subHeaderItems}>
<CreateRepository />
<RepositoryTypeSelector
value={configType}
onChange={val => {
updateQueryParams({ configType: val, page: DEFAULT_PAGE_INDEX })
}}
/>
<PackageTypeSelector
value={repositoryTypes}
onChange={val => {
updateQueryParams({ repositoryTypes: val, page: DEFAULT_PAGE_INDEX })
}}
/>
<Expander />
<TableFilterCheckbox
value={compact}
label={getString('repositoryList.compact')}
disabled={false}
onChange={val => {
updateQueryParams({ compact: val })
}}
/>
<GridListToggle
initialSelectedView={repositoryListViewType === RepositoryListViewTypeEnum.LIST ? Views.LIST : Views.GRID}
icons={{ left: 'SplitView' }}
onViewToggle={newView => {
if (newView === Views.GRID) return
setRepositoryListViewType(RepositoryListViewTypeEnum.LIST)
}}
/>
</div>
</Page.SubHeader>
<Page.Body className={css.treeViewPageBody}>
<RepositoryListTreeView />
</Page.Body>
</>
)
}
export default RepositoryListTreeViewPage

View File

@ -0,0 +1,55 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.treeViewPageContainer {
--tree-view-page-content-height: calc(var(--page-height, var(--page-min-height)) - var(--page-header-height, 64px));
--tree-view-width: 25%;
--har-repository-details-page-header-height: 0px;
--har-version-details-page-header-height: 0px;
width: 100% !important;
height: var(--tree-view-page-content-height);
}
.treeViewContainer {
width: var(--tree-view-width);
height: var(--tree-view-page-content-height);
overflow: auto;
border-right: 1px solid var(--grey-200);
background-color: var(--white) !important;
padding: var(--spacing-0) var(--spacing-0) var(--spacing-small) !important;
}
.treeViewPageContentContainer {
position: relative;
background-color: var(--primary-bg) !important;
width: calc(100% - var(--tree-view-width));
height: var(--tree-view-page-content-height);
overflow: auto;
padding: var(--spacing-0);
border-top: 1px solid var(--grey-200);
}
.sortingDropDown {
cursor: pointer;
& :global([class*='DropDown--dropdownButton--']) {
column-gap: var(--spacing-small) !important;
}
}
.searchInput {
padding: var(--spacing-xsmall) !important;
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const searchInput: string
export declare const sortingDropDown: string
export declare const treeViewContainer: string
export declare const treeViewPageContainer: string
export declare const treeViewPageContentContainer: string

View File

@ -0,0 +1,222 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo, useState } from 'react'
import { compact as lodashCompact } from 'lodash-es'
import { Switch } from 'react-router-dom'
import { useInfiniteQuery } from '@tanstack/react-query'
import { FontVariation } from '@harnessio/design-system'
import { Container, DropDown, Layout, Text } from '@harnessio/uicore'
import { type Error, getAllRegistries, type GetAllRegistriesOkResponse } from '@harnessio/react-har-service-client'
import { useStrings } from '@ar/frameworks/strings'
import { useGetSpaceRef, useParentHooks, useRoutes } from '@ar/hooks'
import TreeBody from '@ar/components/TreeView/TreeBody'
import TreeNode from '@ar/components/TreeView/TreeNode'
import type { RepositoryPackageType } from '@ar/common/types'
import TreeNodeList from '@ar/components/TreeView/TreeNodeList'
import RouteProvider from '@ar/components/RouteProvider/RouteProvider'
import TreeLoadMoreNode from '@ar/components/TreeView/TreeLoadMoreNode'
import RepositoryProvider from '@ar/pages/repository-details/context/RepositoryProvider'
import {
artifactDetailsPathProps,
repositoryDetailsPathProps,
versionDetailsPathParams
} from '@ar/routes/RouteDestinations'
import TreeNodeSearchInput from '@ar/components/TreeView/TreeNodeSearchInput'
import { TreeViewContext } from '@ar/components/TreeView/TreeViewContext'
import VersionProvider from '@ar/pages/version-details/context/VersionProvider'
import ArtifactProvider from '@ar/pages/artifact-details/context/ArtifactProvider'
import type { DockerVersionDetailsQueryParams } from '@ar/pages/version-details/DockerVersion/types'
import RepositoryTreeNodeViewWidget from '@ar/frameworks/RepositoryStep/RepositoryTreeNodeViewWidget'
import VersionTreeNodeDetails from '@ar/pages/version-details/components/VersionTreeNode/VersionTreeNodeDetails'
import ArtifactTreeNodeDetails from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNodeDetails'
import RepositoryTreeNodeDetails from '@ar/pages/repository-details/components/RepositoryTreeNode/RepositoryTreeNodeDetails'
import { TreeViewSortingOptions } from '../../constants'
import { useArtifactRepositoriesQueryParamOptions, type ArtifactRepositoryListPageQueryParams } from '../../utils'
import css from './RepositoryListTreeView.module.scss'
export default function RepositoryListTreeView() {
const routeDefinitions = useRoutes(true)
const { getString } = useStrings()
const spaceRef = useGetSpaceRef()
const [initialised, setInitialised] = useState(false)
const { useQueryParams, useUpdateQueryParams } = useParentHooks()
const { digest } = useQueryParams<DockerVersionDetailsQueryParams>()
const [activePath, setActivePath] = useState('')
const queryParamOptions = useArtifactRepositoriesQueryParamOptions()
const { updateQueryParams } = useUpdateQueryParams<Partial<ArtifactRepositoryListPageQueryParams>>()
const { registrySearchTerm, compact, repositoryTypes, configType, treeSort } =
useQueryParams<ArtifactRepositoryListPageQueryParams>(queryParamOptions)
const [sortField, sortOrder] = treeSort?.split(',') || []
const { data, isFetching, error, hasNextPage, fetchNextPage, refetch, isFetchingNextPage } = useInfiniteQuery<
GetAllRegistriesOkResponse,
Error,
GetAllRegistriesOkResponse,
Array<string | Record<string, unknown>>
>({
queryKey: [
'registryList',
{
sortField,
sortOrder,
registrySearchTerm,
configType,
repositoryTypes
}
],
queryFn: ({ pageParam = 0 }) =>
getAllRegistries({
space_ref: spaceRef,
queryParams: {
page: pageParam,
size: 20,
sort_field: sortField,
sort_order: sortOrder,
package_type: repositoryTypes,
search_term: registrySearchTerm,
type: configType
},
stringifyQueryParamsOptions: {
arrayFormat: 'repeat'
}
}),
getNextPageParam: lastPage => {
const totalPages = lastPage.content.data.pageCount ?? 0
const lastPageNumber = lastPage.content.data.pageIndex ?? -1
const nextPage = lastPageNumber + 1
return nextPage < totalPages ? nextPage : undefined
}
})
const handleUpdateActivePath = (values: Record<string, string>) => {
const initialActivePath = lodashCompact([
values.repositoryIdentifier,
values.artifactIdentifier,
values.versionIdentifier,
digest
]).join('/')
setActivePath(initialActivePath)
setInitialised(true)
}
const { list: repositoryList, count: repositoryCount } = useMemo(() => {
const list = data?.pages.flatMap(page => page.content.data.registries) || []
const count = data?.pages[0].content.data.itemCount || 0
return { list, count }
}, [data])
return (
<TreeViewContext.Provider value={{ activePath, setActivePath, compact }}>
<Layout.Horizontal className={css.treeViewPageContainer}>
<Container className={css.treeViewContainer}>
<TreeNodeList>
<TreeNodeSearchInput
className={css.searchInput}
defaultValue={registrySearchTerm}
onChange={val => {
updateQueryParams({ registrySearchTerm: val })
}}
treeNodeProps={{
alwaysShowAction: true,
actionElement: (
<DropDown
icon="main-sort"
className={css.sortingDropDown}
items={TreeViewSortingOptions}
value={treeSort}
onChange={option => {
const selectedOption = TreeViewSortingOptions.find(each => each.label === option.label)
if (!selectedOption) return
const val = selectedOption.key
const dir = selectedOption.dir
updateQueryParams({ treeSort: [val, dir].join(',') })
}}
usePortal
/>
)
}}
/>
{!!repositoryCount && (
<TreeNode
disabled
alwaysShowAction
compact={compact}
heading={
<Text font={{ variation: FontVariation.BODY, weight: 'semi-bold' }}>
{getString('repositoryList.registryCount', { count: repositoryCount })}
</Text>
}
/>
)}
<TreeBody
loading={isFetching || !initialised}
error={error?.message}
retryOnError={refetch}
isEmpty={!repositoryList.length}>
{repositoryList.map((registry, idx) => (
<RepositoryTreeNodeViewWidget
key={registry.identifier}
data={registry}
packageType={registry.packageType as RepositoryPackageType}
isLastChild={idx === repositoryList.length - 1}
/>
))}
{hasNextPage && <TreeLoadMoreNode onClick={() => fetchNextPage()} disabled={isFetchingNextPage} />}
</TreeBody>
</TreeNodeList>
</Container>
<Container className={css.treeViewPageContentContainer}>
<Switch>
<RouteProvider exact onLoad={handleUpdateActivePath} path={[routeDefinitions.toARRepositories()]}>
{/* TODO: Implement a default page for this path */}
<></>
</RouteProvider>
<RouteProvider
exact
onLoad={handleUpdateActivePath}
path={[routeDefinitions.toARArtifactDetails({ ...artifactDetailsPathProps })]}>
<ArtifactProvider>
<ArtifactTreeNodeDetails />
</ArtifactProvider>
</RouteProvider>
<RouteProvider
onLoad={handleUpdateActivePath}
path={[routeDefinitions.toARVersionDetails({ ...versionDetailsPathParams })]}>
<VersionProvider>
<VersionTreeNodeDetails />
</VersionProvider>
</RouteProvider>
<RouteProvider
onLoad={handleUpdateActivePath}
path={[routeDefinitions.toARRepositoryDetails({ ...repositoryDetailsPathProps })]}>
<RepositoryProvider>
<RepositoryTreeNodeDetails />
</RepositoryProvider>
</RouteProvider>
</Switch>
</Container>
</Layout.Horizontal>
</TreeViewContext.Provider>
)
}

View File

@ -31,6 +31,7 @@ export default function RepositoryTypeSelector(props: RepositoryTypeSelectorProp
return (
<DropDown
width={180}
usePortal
buttonTestId="registry-type-select"
items={RepositoryConfigTypes.filter(each => !each.disabled).map(each => ({
...each,

View File

@ -0,0 +1,28 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface TreeViewSortingOption {
value: string
label: string
dir?: string
}
export const TreeViewSortingOptions = [
{ value: 'lastModified,DESC', label: 'Newest', key: 'lastModified', dir: 'DESC' },
{ value: 'lastModified,ASC', label: 'Oldest', key: 'lastModified', dir: 'ASC' },
{ value: 'identifier,ASC', label: 'Name (A->Z, 0->9)', key: 'identifier', dir: 'ASC' },
{ value: 'identifier,DESC', label: 'Name (Z->A, 9->0)', key: 'identifier', dir: 'DESC' }
]

View File

@ -1,6 +1,7 @@
pageHeading: Artifact Registries
newRepository: New Artifact Registry
newRegistry: New Registry
registryCount: 'Total {{count}} Registries'
artifactRegistry:
label: Artifact Registry
subLabel: Manage internal packages and external dependencies through a unified registry.
@ -11,6 +12,7 @@ selectEnvironments: Environments
selectPackageTypes: Package Types
selectRegistryType: Registry Type
selectLabels: '{{ $.artifactList.table.columns.tags }}'
compact: Compact View
deleteModal:
title: '{{ $.artifactList.table.actions.deleteRepository }}'
contentText: Are you sure you want to delete the registry?

View File

@ -29,8 +29,11 @@ type GetArtifactRepositoryQueryParams = {
size: number
sort: string[]
searchTerm?: string
registrySearchTerm?: string
compact: boolean
repositoryTypes: RepositoryPackageType[]
configType?: RepositoryConfigType
treeSort?: string
}
export type ArtifactRepositoryListPageQueryParams = Omit<
@ -46,7 +49,8 @@ export const useArtifactRepositoriesQueryParamOptions =
page: DEFAULT_PAGE_INDEX,
size: DEFAULT_PAGE_SIZE,
sort: DEFAULT_PIPELINE_LIST_TABLE_SORT,
repositoryTypes: []
repositoryTypes: [],
compact: false
},
{ ignoreEmptyString: false }
)

View File

@ -19,12 +19,13 @@ import { FontVariation } from '@harnessio/design-system'
import { Card, Container, Layout, Page, Text } from '@harnessio/uicore'
import { useGetDockerArtifactDetailsQuery } from '@harnessio/react-har-service-client'
import { Parent } from '@ar/common/types'
import { useStrings } from '@ar/frameworks/strings'
import { encodeRef } from '@ar/hooks/useGetSpaceRef'
import { DEFAULT_DATE_TIME_FORMAT } from '@ar/constants'
import type { VersionDetailsPathParams } from '@ar/routes/types'
import { getReadableDateTime } from '@ar/common/dateUtils'
import { useDecodedParams, useGetSpaceRef, useParentHooks } from '@ar/hooks'
import { useAppStore, useDecodedParams, useGetSpaceRef, useParentHooks } from '@ar/hooks'
import type { DockerVersionDetailsQueryParams } from './types'
import { VersionOverviewCard } from '../components/OverviewCards/types'
@ -40,6 +41,7 @@ export default function DockerVersionOverviewContent(): JSX.Element {
const { useQueryParams } = useParentHooks()
const { digest } = useQueryParams<DockerVersionDetailsQueryParams>()
const spaceRef = useGetSpaceRef()
const { parent } = useAppStore()
const {
data,
@ -70,15 +72,17 @@ export default function DockerVersionOverviewContent(): JSX.Element {
retryOnError={() => refetch()}>
{response && (
<Layout.Vertical className={css.cardContainer} spacing="medium" flex={{ alignItems: 'flex-start' }}>
<VersionOverviewCards
cards={[
VersionOverviewCard.DEPLOYMENT,
VersionOverviewCard.BUILD,
VersionOverviewCard.SECURITY_TESTS,
VersionOverviewCard.SUPPLY_CHAIN
]}
digest={digest}
/>
{parent === Parent.Enterprise && (
<VersionOverviewCards
cards={[
VersionOverviewCard.DEPLOYMENT,
VersionOverviewCard.BUILD,
VersionOverviewCard.SECURITY_TESTS,
VersionOverviewCard.SUPPLY_CHAIN
]}
digest={digest}
/>
)}
<Card
data-testid="general-information-card"
title={getString('versionDetails.overview.generalInformation.title')}

View File

@ -18,30 +18,38 @@ import React from 'react'
import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client'
import { String } from '@ar/frameworks/strings'
import { PageType, RepositoryPackageType } from '@ar/common/types'
import { NodeTypeEnum } from '@ar/components/TreeView/TreeNode'
import DigestListPage from '@ar/pages/digest-list/DigestListPage'
import { PageType, RepositoryPackageType } from '@ar/common/types'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import ArtifactTreeNode from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNode'
import DigestListTreeView from '@ar/pages/digest-list/components/DigestListTreeView/DigestListTreeView'
import DockerVersionListTable from '@ar/pages/version-list/DockerVersion/VersionListTable/DockerVersionListTable'
import ArtifactTreeNodeDetailsContent from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNodeDetailsContent'
import {
type ArtifactActionProps,
ArtifactRowSubComponentProps,
type VersionActionProps,
type ArtifactTreeNodeViewProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
VersionStep,
type VersionTreeNodeViewProps
} from '@ar/frameworks/Version/Version'
import DockerVersionHeader from './DockerVersionHeader'
import { VersionAction } from '../components/VersionActions/types'
import DockerArtifactSSCAContent from './DockerArtifactSSCAContent'
import VersionActions from '../components/VersionActions/VersionActions'
import DockerVersionOverviewContent from './DockerVersionOverviewContent'
import DockerArtifactDetailsContent from './DockerArtifactDetailsContent'
import VersionTreeNode from '../components/VersionTreeNode/VersionTreeNode'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import DockerArtifactSecurityTestsContent from './DockerArtifactSecurityTestsContent'
import DockerVersionOSSContent from './DockerVersionOSSContent/DockerVersionOSSContent'
import DockerDeploymentsContent from './DockerDeploymentsContent/DockerDeploymentsContent'
import VersionActions from '../components/VersionActions/VersionActions'
import { VersionAction } from '../components/VersionActions/types'
import DockerVersionTreeNodeDetailsContent from './components/DockerVersionTreeNodeDetailsContent/DockerVersionTreeNodeDetailsContent'
export class DockerVersionType extends VersionStep<ArtifactVersionSummary> {
protected packageType = RepositoryPackageType.DOCKER
@ -111,4 +119,30 @@ export class DockerVersionType extends VersionStep<ArtifactVersionSummary> {
<DigestListPage repoKey={props.data.registryIdentifier} artifact={props.data.name} version={props.data.version} />
)
}
renderArtifactTreeNodeView(props: ArtifactTreeNodeViewProps): JSX.Element {
return <ArtifactTreeNode {...props} icon="store-artifact-bundle" />
}
renderArtifactTreeNodeDetails(): JSX.Element {
return <ArtifactTreeNodeDetailsContent />
}
renderVersionTreeNodeView(props: VersionTreeNodeViewProps): JSX.Element {
const { data, parentNodeLevels, isLastChild } = props
return (
<VersionTreeNode {...props} icon="container" nodeType={NodeTypeEnum.Folder}>
<DigestListTreeView
registryIdentifier={props.data.registryIdentifier}
artifactIdentifier={props.artifactIdentifier}
versionIdentifier={props.data.name}
parentNodeLevels={[...parentNodeLevels, { data, isLastChild }]}
/>
</VersionTreeNode>
)
}
renderVersionTreeNodeDetails(): JSX.Element {
return <DockerVersionTreeNodeDetailsContent />
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.cardContainer {
width: 60% !important;
min-width: 1040px;
padding: var(--spacing-7) !important;
background-color: var(--white);
}
.gridContainer {
align-items: center;
display: grid;
grid-template-columns: max-content auto;
row-gap: var(--spacing-medium);
column-gap: 30px;
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const cardContainer: string
export declare const gridContainer: string

View File

@ -0,0 +1,66 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react'
import { FontVariation } from '@harnessio/design-system'
import { Card, Container, Layout, Text } from '@harnessio/uicore'
import { useParentHooks } from '@ar/hooks'
import { useStrings } from '@ar/frameworks/strings'
import { VersionProviderContext } from '@ar/pages/version-details/context/VersionProvider'
import { LabelValueTypeEnum } from '@ar/pages/version-details/components/LabelValueContent/type'
import VersionDetailsTabs from '@ar/pages/version-details/components/VersionDetailsTabs/VersionDetailsTabs'
import { LabelValueContent } from '@ar/pages/version-details/components/LabelValueContent/LabelValueContent'
import type { DockerVersionDetailsQueryParams } from '../../types'
import css from './DockerVersionTreeNodeDetailsContent.module.scss'
export default function DockerVersionTreeNodeDetailsContent(): JSX.Element {
const { useQueryParams } = useParentHooks()
const { getString } = useStrings()
const { data } = useContext(VersionProviderContext)
const { digest } = useQueryParams<DockerVersionDetailsQueryParams>()
if (!data) return <></>
if (!digest) {
return (
<Layout.Vertical spacing="small" padding="large">
<Text font={{ variation: FontVariation.CARD_TITLE }}>{getString('details')}</Text>
<Card className={css.cardContainer}>
<Container className={css.gridContainer}>
<LabelValueContent
label={getString('versionDetails.overview.generalInformation.name')}
value={data.imageName}
type={LabelValueTypeEnum.Text}
/>
<LabelValueContent
label={getString('versionDetails.overview.generalInformation.version')}
value={data.version}
type={LabelValueTypeEnum.Text}
/>
<LabelValueContent
label={getString('versionDetails.overview.generalInformation.packageType')}
value={getString('packageTypes.dockerPackage')}
type={LabelValueTypeEnum.PackageType}
icon="docker-step"
/>
</Container>
</Card>
</Layout.Vertical>
)
}
return <VersionDetailsTabs />
}

View File

@ -20,10 +20,12 @@ import {
type ArtifactActionProps,
ArtifactRowSubComponentProps,
type VersionActionProps,
type ArtifactTreeNodeViewProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
VersionStep,
type VersionTreeNodeViewProps
} from '@ar/frameworks/Version/Version'
import { String } from '@ar/frameworks/strings'
import { PageType, RepositoryPackageType } from '@ar/common/types'
@ -32,9 +34,13 @@ import VersionListTable, {
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import ArtifactTreeNode from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNode'
import ArtifactTreeNodeDetailsContent from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNodeDetailsContent'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import GenericOverviewPage from './pages/overview/OverviewPage'
import OSSContentPage from './pages/oss-details/OSSContentPage'
import VersionTreeNode from '../components/VersionTreeNode/VersionTreeNode'
import VersionDetailsTabs from '../components/VersionDetailsTabs/VersionDetailsTabs'
import GenericArtifactDetailsPage from './pages/artifact-details/GenericArtifactDetailsPage'
import VersionDetailsHeaderContent from '../components/VersionDetailsHeaderContent/VersionDetailsHeaderContent'
import VersionFilesProvider from '../context/VersionFilesProvider'
@ -113,4 +119,20 @@ export class GenericVersionType extends VersionStep<ArtifactVersionSummary> {
</VersionFilesProvider>
)
}
renderArtifactTreeNodeView(props: ArtifactTreeNodeViewProps): JSX.Element {
return <ArtifactTreeNode {...props} icon="store-artifact-bundle" />
}
renderArtifactTreeNodeDetails(): JSX.Element {
return <ArtifactTreeNodeDetailsContent />
}
renderVersionTreeNodeView(props: VersionTreeNodeViewProps): JSX.Element {
return <VersionTreeNode {...props} icon="container" />
}
renderVersionTreeNodeDetails(): JSX.Element {
return <VersionDetailsTabs />
}
}

View File

@ -24,19 +24,26 @@ import VersionListTable, {
CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import ArtifactTreeNode from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNode'
import ArtifactTreeNodeDetailsContent from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNodeDetailsContent'
import {
type ArtifactActionProps,
type VersionActionProps,
type ArtifactTreeNodeViewProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
VersionStep,
type VersionTreeNodeViewProps
} from '@ar/frameworks/Version/Version'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import HelmVersionOverviewContent from './HelmVersionOverviewContent'
import HelmArtifactDetailsContent from './HelmArtifactDetailsContent'
import VersionActions from '../components/VersionActions/VersionActions'
import VersionTreeNode from '../components/VersionTreeNode/VersionTreeNode'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import HelmVersionOSSContent from './HelmVersionOSSContent/HelmVersionOSSContent'
import VersionDetailsTabs from '../components/VersionDetailsTabs/VersionDetailsTabs'
import VersionDetailsHeaderContent from '../components/VersionDetailsHeaderContent/VersionDetailsHeaderContent'
import { VersionAction } from '../components/VersionActions/types'
@ -105,4 +112,20 @@ export class HelmVersionType extends VersionStep<ArtifactVersionSummary> {
renderArtifactRowSubComponent(): JSX.Element {
return <></>
}
renderArtifactTreeNodeView(props: ArtifactTreeNodeViewProps): JSX.Element {
return <ArtifactTreeNode {...props} icon="store-artifact-bundle" />
}
renderArtifactTreeNodeDetails(): JSX.Element {
return <ArtifactTreeNodeDetailsContent />
}
renderVersionTreeNodeView(props: VersionTreeNodeViewProps): JSX.Element {
return <VersionTreeNode {...props} icon="container" />
}
renderVersionTreeNodeDetails(): JSX.Element {
return <VersionDetailsTabs />
}
}

View File

@ -20,6 +20,8 @@ import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client
import { String } from '@ar/frameworks/strings'
import { PageType, RepositoryPackageType } from '@ar/common/types'
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
import ArtifactTreeNode from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNode'
import ArtifactTreeNodeDetailsContent from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNodeDetailsContent'
import VersionListTable, {
type CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
@ -27,17 +29,21 @@ import {
type ArtifactActionProps,
ArtifactRowSubComponentProps,
type VersionActionProps,
type ArtifactTreeNodeViewProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
VersionStep,
type VersionTreeNodeViewProps
} from '@ar/frameworks/Version/Version'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import OSSContentPage from './pages/oss-details/OSSContentPage'
import VersionFilesProvider from '../context/VersionFilesProvider'
import VersionTreeNode from '../components/VersionTreeNode/VersionTreeNode'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import MavenArtifactOverviewPage from './pages/overview/MavenArtifactOverviewPage'
import VersionDetailsTabs from '../components/VersionDetailsTabs/VersionDetailsTabs'
import MavenArtifactDetailsPage from './pages/artifact-details/MavenArtifactDetailsPage'
import ArtifactFilesContent from '../components/ArtifactFileListTable/ArtifactFilesContent'
import VersionDetailsHeaderContent from '../components/VersionDetailsHeaderContent/VersionDetailsHeaderContent'
@ -115,4 +121,20 @@ export class MavenVersionType extends VersionStep<ArtifactVersionSummary> {
</VersionFilesProvider>
)
}
renderArtifactTreeNodeView(props: ArtifactTreeNodeViewProps): JSX.Element {
return <ArtifactTreeNode {...props} icon="store-artifact-bundle" />
}
renderArtifactTreeNodeDetails(): JSX.Element {
return <ArtifactTreeNodeDetailsContent />
}
renderVersionTreeNodeView(props: VersionTreeNodeViewProps): JSX.Element {
return <VersionTreeNode {...props} icon="container" />
}
renderVersionTreeNodeDetails(): JSX.Element {
return <VersionDetailsTabs />
}
}

View File

@ -22,6 +22,8 @@ import { String } from '@ar/frameworks/strings'
import { PageType, RepositoryPackageType } from '@ar/common/types'
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import ArtifactTreeNode from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNode'
import ArtifactTreeNodeDetailsContent from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNodeDetailsContent'
import VersionListTable, {
type CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
@ -29,15 +31,19 @@ import {
type ArtifactActionProps,
ArtifactRowSubComponentProps,
type VersionActionProps,
type ArtifactTreeNodeViewProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
VersionStep,
type VersionTreeNodeViewProps
} from '@ar/frameworks/Version/Version'
import VersionActions from '../components/VersionActions/VersionActions'
import VersionTreeNode from '../components/VersionTreeNode/VersionTreeNode'
import NpmVersionOverviewPage from './pages/overview/NpmVersionOverviewPage'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import VersionDetailsTabs from '../components/VersionDetailsTabs/VersionDetailsTabs'
import NpmVersionArtifactDetailsPage from './pages/artifact-dertails/NpmVersionArtifactDetailsPage'
import VersionDetailsHeaderContent from '../components/VersionDetailsHeaderContent/VersionDetailsHeaderContent'
import VersionFilesProvider from '../context/VersionFilesProvider'
@ -123,4 +129,20 @@ export class NpmVersionType extends VersionStep<ArtifactVersionSummary> {
</VersionFilesProvider>
)
}
renderArtifactTreeNodeView(props: ArtifactTreeNodeViewProps): JSX.Element {
return <ArtifactTreeNode {...props} icon="store-artifact-bundle" />
}
renderArtifactTreeNodeDetails(): JSX.Element {
return <ArtifactTreeNodeDetailsContent />
}
renderVersionTreeNodeView(props: VersionTreeNodeViewProps): JSX.Element {
return <VersionTreeNode {...props} icon="container" />
}
renderVersionTreeNodeDetails(): JSX.Element {
return <VersionDetailsTabs />
}
}

View File

@ -22,22 +22,28 @@ import { String } from '@ar/frameworks/strings'
import { PageType, RepositoryPackageType } from '@ar/common/types'
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import ArtifactTreeNode from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNode'
import ArtifactTreeNodeDetailsContent from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNodeDetailsContent'
import VersionListTable, {
type CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
import {
type ArtifactActionProps,
ArtifactRowSubComponentProps,
type ArtifactRowSubComponentProps,
type ArtifactTreeNodeViewProps,
type VersionActionProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
VersionStep,
type VersionTreeNodeViewProps
} from '@ar/frameworks/Version/Version'
import VersionActions from '../components/VersionActions/VersionActions'
import NuGetVersionOverviewPage from './pages/overview/NuGetVersionOverviewPage'
import VersionTreeNode from '../components/VersionTreeNode/VersionTreeNode'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import NuGetVersionOverviewPage from './pages/overview/NuGetVersionOverviewPage'
import VersionDetailsTabs from '../components/VersionDetailsTabs/VersionDetailsTabs'
import NuGetVersionArtifactDetailsPage from './pages/artifact-dertails/NuGetVersionArtifactDetailsPage'
import VersionDetailsHeaderContent from '../components/VersionDetailsHeaderContent/VersionDetailsHeaderContent'
import VersionFilesProvider from '../context/VersionFilesProvider'
@ -123,4 +129,20 @@ export class NuGetVersionType extends VersionStep<ArtifactVersionSummary> {
</VersionFilesProvider>
)
}
renderArtifactTreeNodeView(props: ArtifactTreeNodeViewProps): JSX.Element {
return <ArtifactTreeNode {...props} icon="store-artifact-bundle" />
}
renderArtifactTreeNodeDetails(): JSX.Element {
return <ArtifactTreeNodeDetailsContent />
}
renderVersionTreeNodeView(props: VersionTreeNodeViewProps): JSX.Element {
return <VersionTreeNode {...props} icon="container" />
}
renderVersionTreeNodeDetails(): JSX.Element {
return <VersionDetailsTabs />
}
}

View File

@ -22,22 +22,28 @@ import { String } from '@ar/frameworks/strings'
import { PageType, RepositoryPackageType } from '@ar/common/types'
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import ArtifactTreeNode from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNode'
import ArtifactTreeNodeDetailsContent from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNodeDetailsContent'
import VersionListTable, {
type CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
import {
type ArtifactActionProps,
ArtifactRowSubComponentProps,
type ArtifactRowSubComponentProps,
type ArtifactTreeNodeViewProps,
type VersionActionProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
VersionStep,
type VersionTreeNodeViewProps
} from '@ar/frameworks/Version/Version'
import VersionActions from '../components/VersionActions/VersionActions'
import VersionTreeNode from '../components/VersionTreeNode/VersionTreeNode'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import PythonVersionOverviewPage from './pages/overview/PythonVersionOverviewPage'
import VersionDetailsTabs from '../components/VersionDetailsTabs/VersionDetailsTabs'
import PythonVersionArtifactDetailsPage from './pages/artifact-dertails/PythonVersionArtifactDetailsPage'
import VersionDetailsHeaderContent from '../components/VersionDetailsHeaderContent/VersionDetailsHeaderContent'
import VersionFilesProvider from '../context/VersionFilesProvider'
@ -124,4 +130,20 @@ export class PythonVersionType extends VersionStep<ArtifactVersionSummary> {
</VersionFilesProvider>
)
}
renderArtifactTreeNodeView(props: ArtifactTreeNodeViewProps): JSX.Element {
return <ArtifactTreeNode {...props} icon="store-artifact-bundle" />
}
renderArtifactTreeNodeDetails(): JSX.Element {
return <ArtifactTreeNodeDetailsContent />
}
renderVersionTreeNodeView(props: VersionTreeNodeViewProps): JSX.Element {
return <VersionTreeNode {...props} icon="container" />
}
renderVersionTreeNodeDetails(): JSX.Element {
return <VersionDetailsTabs />
}
}

View File

@ -22,17 +22,21 @@ import { String } from '@ar/frameworks/strings'
import { PageType, RepositoryPackageType } from '@ar/common/types'
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import ArtifactTreeNode from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNode'
import ArtifactTreeNodeDetailsContent from '@ar/pages/artifact-details/components/ArtifactTreeNode/ArtifactTreeNodeDetailsContent'
import VersionListTable, {
type CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
import {
type ArtifactActionProps,
ArtifactRowSubComponentProps,
type ArtifactRowSubComponentProps,
type ArtifactTreeNodeViewProps,
type VersionActionProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
VersionStep,
VersionTreeNodeViewProps
} from '@ar/frameworks/Version/Version'
import VersionFilesProvider from '../context/VersionFilesProvider'
@ -44,6 +48,8 @@ import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import ArtifactFilesContent from '../components/ArtifactFileListTable/ArtifactFilesContent'
import RPMVersionArtifactDetailsPage from './pages/artifact-dertails/RPMVersionArtifactDetailsPage'
import VersionDetailsHeaderContent from '../components/VersionDetailsHeaderContent/VersionDetailsHeaderContent'
import VersionTreeNode from '../components/VersionTreeNode/VersionTreeNode'
import VersionDetailsTabs from '../components/VersionDetailsTabs/VersionDetailsTabs'
export class RPMVersionType extends VersionStep<ArtifactVersionSummary> {
protected packageType = RepositoryPackageType.RPM
@ -133,4 +139,20 @@ export class RPMVersionType extends VersionStep<ArtifactVersionSummary> {
</VersionFilesProvider>
)
}
renderArtifactTreeNodeView(props: ArtifactTreeNodeViewProps): JSX.Element {
return <ArtifactTreeNode {...props} icon="store-artifact-bundle" />
}
renderArtifactTreeNodeDetails(): JSX.Element {
return <ArtifactTreeNodeDetailsContent />
}
renderVersionTreeNodeView(props: VersionTreeNodeViewProps): JSX.Element {
return <VersionTreeNode {...props} icon="container" />
}
renderVersionTreeNodeDetails(): JSX.Element {
return <VersionDetailsTabs />
}
}

View File

@ -38,7 +38,6 @@ import {
} from '@ar/routes/RouteDestinations'
import { VersionDetailsTab, VersionDetailsTabList } from './constants'
import type { DockerVersionDetailsQueryParams } from '../../DockerVersion/types'
import css from './VersionDetailsTab.module.scss'
export default function VersionDetailsTabs(): JSX.Element {
@ -49,15 +48,18 @@ export default function VersionDetailsTabs(): JSX.Element {
const history = useHistory()
const { getString } = useStrings()
const routeDefinitions = useRoutes(true)
const { parent } = useAppStore()
const { data } = useContext(VersionProviderContext)
const pathParams = useDecodedParams<VersionDetailsPathParams>()
const { digest } = useQueryParams<DockerVersionDetailsQueryParams>()
const queryParams = useQueryParams<Record<string, string>>()
const { orgIdentifier, projectIdentifier } = scope
const tabList = useMemo(() => {
const versionType = versionFactory?.getVersionType(data?.packageType)
if (!versionType) return []
return VersionDetailsTabList.filter(each => versionType.getAllowedVersionDetailsTab().includes(each.value))
return VersionDetailsTabList.filter(each => !each.parent || each.parent === parent).filter(each =>
versionType.getAllowedVersionDetailsTab().includes(each.value)
)
}, [data])
const handleTabChange = useCallback(
@ -66,44 +68,50 @@ export default function VersionDetailsTabs(): JSX.Element {
let newRoute
switch (nextTab) {
case VersionDetailsTab.SUPPLY_CHAIN:
newRoute = routes.toARVersionDetailsTab({
versionIdentifier: pathParams.versionIdentifier,
artifactIdentifier: pathParams.artifactIdentifier,
repositoryIdentifier: pathParams.repositoryIdentifier,
versionTab: nextTab,
sourceId: data?.sscaArtifactSourceId,
artifactId: data?.sscaArtifactId,
orgIdentifier: !orgIdentifier ? DEFAULT_ORG : undefined,
projectIdentifier: !projectIdentifier ? DEFAULT_PROJECT : undefined
})
newRoute = routes.toARVersionDetailsTab(
{
versionIdentifier: pathParams.versionIdentifier,
artifactIdentifier: pathParams.artifactIdentifier,
repositoryIdentifier: pathParams.repositoryIdentifier,
versionTab: nextTab,
sourceId: data?.sscaArtifactSourceId,
artifactId: data?.sscaArtifactId,
orgIdentifier: !orgIdentifier ? DEFAULT_ORG : undefined,
projectIdentifier: !projectIdentifier ? DEFAULT_PROJECT : undefined
},
{ queryParams }
)
break
case VersionDetailsTab.SECURITY_TESTS:
newRoute = routes.toARVersionDetailsTab({
versionIdentifier: pathParams.versionIdentifier,
artifactIdentifier: pathParams.artifactIdentifier,
repositoryIdentifier: pathParams.repositoryIdentifier,
versionTab: nextTab,
executionIdentifier: data?.stoExecutionId,
pipelineIdentifier: data?.stoPipelineId,
orgIdentifier: !orgIdentifier ? DEFAULT_ORG : undefined,
projectIdentifier: !projectIdentifier ? DEFAULT_PROJECT : undefined
})
newRoute = routes.toARVersionDetailsTab(
{
versionIdentifier: pathParams.versionIdentifier,
artifactIdentifier: pathParams.artifactIdentifier,
repositoryIdentifier: pathParams.repositoryIdentifier,
versionTab: nextTab,
executionIdentifier: data?.stoExecutionId,
pipelineIdentifier: data?.stoPipelineId,
orgIdentifier: !orgIdentifier ? DEFAULT_ORG : undefined,
projectIdentifier: !projectIdentifier ? DEFAULT_PROJECT : undefined
},
{ queryParams }
)
break
default:
newRoute = routes.toARVersionDetailsTab({
versionIdentifier: pathParams.versionIdentifier,
artifactIdentifier: pathParams.artifactIdentifier,
repositoryIdentifier: pathParams.repositoryIdentifier,
versionTab: nextTab
})
newRoute = routes.toARVersionDetailsTab(
{
versionIdentifier: pathParams.versionIdentifier,
artifactIdentifier: pathParams.artifactIdentifier,
repositoryIdentifier: pathParams.repositoryIdentifier,
versionTab: nextTab
},
{ queryParams }
)
break
}
if (digest) {
newRoute = `${newRoute}?digest=${digest}`
}
history.push(newRoute)
},
[digest]
[queryParams]
)
if (!data) return <></>

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { Parent } from '@ar/common/types'
import type { StringsMap } from '@ar/frameworks/strings'
export enum VersionDetailsTab {
@ -30,6 +31,7 @@ interface VersionDetailsTabListItem {
label: keyof StringsMap
value: VersionDetailsTab
disabled?: boolean
parent?: Parent
}
export const VersionDetailsTabList: VersionDetailsTabListItem[] = [
@ -43,19 +45,23 @@ export const VersionDetailsTabList: VersionDetailsTabListItem[] = [
},
{
label: 'versionDetails.tabs.supplyChain',
value: VersionDetailsTab.SUPPLY_CHAIN
value: VersionDetailsTab.SUPPLY_CHAIN,
parent: Parent.Enterprise
},
{
label: 'versionDetails.tabs.securityTests',
value: VersionDetailsTab.SECURITY_TESTS
value: VersionDetailsTab.SECURITY_TESTS,
parent: Parent.Enterprise
},
{
label: 'versionDetails.tabs.deployments',
value: VersionDetailsTab.DEPLOYMENTS
value: VersionDetailsTab.DEPLOYMENTS,
parent: Parent.Enterprise
},
{
label: 'versionDetails.tabs.code',
value: VersionDetailsTab.CODE,
disabled: true
disabled: true,
parent: Parent.Enterprise
}
]

View File

@ -0,0 +1,108 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { PropsWithChildren, useContext } from 'react'
import { omit } from 'lodash-es'
import { useHistory } from 'react-router-dom'
import type { IconName } from '@harnessio/icons'
import type {
ArtifactVersionMetadata,
RegistryArtifactMetadata,
RegistryMetadata
} from '@harnessio/react-har-service-client'
import { useParentHooks, useRoutes } from '@ar/hooks'
import TreeNode, { NodeTypeEnum } from '@ar/components/TreeView/TreeNode'
import TreeNodeContent from '@ar/components/TreeView/TreeNodeContent'
import { PageType, type RepositoryPackageType } from '@ar/common/types'
import { TreeViewContext } from '@ar/components/TreeView/TreeViewContext'
import VersionActionsWidget from '@ar/frameworks/Version/VersionActionsWidget'
import type { VersionTreeNodeViewProps } from '@ar/frameworks/Version/Version'
import { VersionDetailsTab } from '../VersionDetailsTabs/constants'
interface IVersionTreeNode extends VersionTreeNodeViewProps {
icon: IconName
iconSize?: number
level?: number
nodeType?: NodeTypeEnum
}
export default function VersionTreeNode(props: PropsWithChildren<IVersionTreeNode>) {
const {
data,
icon,
iconSize = 24,
level = 2,
nodeType = NodeTypeEnum.File,
artifactIdentifier,
isLastChild,
parentNodeLevels
} = props
const { setActivePath, activePath, compact } = useContext(TreeViewContext)
const routes = useRoutes()
const history = useHistory()
const { useQueryParams } = useParentHooks()
const queryParams = useQueryParams<Record<string, string>>()
const path = `${data.registryIdentifier}/${artifactIdentifier}/${data.name}`
return (
<TreeNode<RegistryMetadata | RegistryArtifactMetadata | ArtifactVersionMetadata>
key={path}
id={path}
level={level}
nodeType={nodeType}
compact={compact}
isOpen={activePath.includes(path)}
isActive={activePath === path}
isLastChild={isLastChild}
parentNodeLevels={parentNodeLevels}
onClick={() => {
setActivePath(path)
history.push(
routes.toARVersionDetailsTab(
{
repositoryIdentifier: data.registryIdentifier,
artifactIdentifier: artifactIdentifier,
versionIdentifier: data.name,
versionTab: VersionDetailsTab.OVERVIEW
},
{ queryParams: omit(queryParams, 'digest') }
)
)
}}
heading={
<TreeNodeContent
icon={icon}
iconSize={iconSize}
label={data.name}
size={data.size}
downloads={data.downloadsCount}
compact={compact}
/>
}
actionElement={
<VersionActionsWidget
pageType={PageType.Table}
data={data}
repoKey={data.registryIdentifier}
artifactKey={artifactIdentifier}
versionKey={data.name}
packageType={data.packageType as RepositoryPackageType}
/>
}>
{nodeType === NodeTypeEnum.Folder && props.children}
</TreeNode>
)
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react'
import type { RepositoryPackageType } from '@ar/common/types'
import VersionTreeNodeDetailsWidget from '@ar/frameworks/Version/VersionTreeNodeDetailsWidget'
import { VersionProviderContext } from '../../context/VersionProvider'
export default function VersionTreeNodeDetails() {
const { data } = useContext(VersionProviderContext)
if (!data) return <></>
const { packageType } = data
return <VersionTreeNodeDetailsWidget packageType={packageType as RepositoryPackageType} data={data} />
}

View File

@ -19,7 +19,8 @@ import { PageError, PageSpinner } from '@harnessio/uicore'
import { ArtifactVersionSummary, useGetArtifactVersionSummaryQuery } from '@harnessio/react-har-service-client'
import { encodeRef } from '@ar/hooks/useGetSpaceRef'
import { useGetSpaceRef, useParentHooks } from '@ar/hooks'
import type { VersionDetailsPathParams } from '@ar/routes/types'
import { useDecodedParams, useGetSpaceRef, useParentHooks } from '@ar/hooks'
import type { DockerVersionDetailsQueryParams } from '../DockerVersion/types'
@ -32,9 +33,9 @@ interface VersionProviderProps {
export const VersionProviderContext = createContext<VersionProviderProps>({} as VersionProviderProps)
interface VersionProviderSpcs {
repoKey: string
artifactKey: string
versionKey: string
repoKey?: string
artifactKey?: string
versionKey?: string
}
const VersionProvider: FC<PropsWithChildren<VersionProviderSpcs>> = ({
@ -43,10 +44,11 @@ const VersionProvider: FC<PropsWithChildren<VersionProviderSpcs>> = ({
artifactKey,
versionKey
}): JSX.Element => {
const spaceRef = useGetSpaceRef(repoKey)
const { useQueryParams } = useParentHooks()
const { digest } = useQueryParams<DockerVersionDetailsQueryParams>()
const { repositoryIdentifier, artifactIdentifier, versionIdentifier } = useDecodedParams<VersionDetailsPathParams>()
const spaceRef = useGetSpaceRef(repoKey ?? repositoryIdentifier)
const {
data,
isFetching: loading,
@ -54,8 +56,8 @@ const VersionProvider: FC<PropsWithChildren<VersionProviderSpcs>> = ({
refetch
} = useGetArtifactVersionSummaryQuery({
registry_ref: spaceRef,
artifact: encodeRef(artifactKey),
version: versionKey,
artifact: encodeRef(artifactKey ?? artifactIdentifier),
version: versionKey ?? versionIdentifier,
queryParams: {
digest
}

View File

@ -50,6 +50,8 @@ overview:
downloads: '{{ $.repositoryList.table.columns.downloads }}'
uploadedBy: Uploaded At
createdAndLastModifiedAt: Created | Last modified
createdAt: Created At
modifiedAt: Modified At
description: Description
pullCommand: Pull command
repository: Repository

View File

@ -0,0 +1,124 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
import {
type Error,
getAllArtifactVersions,
type GetAllArtifactVersionsOkResponse,
type RegistryArtifactMetadata,
type RegistryMetadata
} from '@harnessio/react-har-service-client'
import { DEFAULT_PAGE_SIZE } from '@ar/constants'
import { useStrings } from '@ar/frameworks/strings'
import { encodeRef } from '@ar/hooks/useGetSpaceRef'
import TreeBody from '@ar/components/TreeView/TreeBody'
import { useGetSpaceRef, useParentHooks } from '@ar/hooks'
import type { RepositoryPackageType } from '@ar/common/types'
import TreeNodeList from '@ar/components/TreeView/TreeNodeList'
import type { NodeSpec } from '@ar/components/TreeView/TreeViewContext'
import TreeLoadMoreNode from '@ar/components/TreeView/TreeLoadMoreNode'
import TreeNodeSearchInput from '@ar/components/TreeView/TreeNodeSearchInput'
import VersionTreeNodeViewWidget from '@ar/frameworks/Version/VersionTreeNodeViewWidget'
import { useVersionListQueryParamOptions, VersionListPageQueryParams } from '../../utils'
interface VersiontListTreeViewProps {
registryIdentifier: string
artifactIdentifier: string
parentNodeLevels: Array<NodeSpec<RegistryMetadata | RegistryArtifactMetadata>>
}
export default function VersionListTreeView(props: VersiontListTreeViewProps) {
const { registryIdentifier, artifactIdentifier, parentNodeLevels } = props
const { useQueryParams, useUpdateQueryParams } = useParentHooks()
const { updateQueryParams } = useUpdateQueryParams<Partial<VersionListPageQueryParams>>()
const queryParams = useQueryParams<VersionListPageQueryParams>(useVersionListQueryParamOptions())
const { versionSearchTerm } = queryParams
const { getString } = useStrings()
const registryRef = useGetSpaceRef(registryIdentifier)
const { data, isLoading, error, hasNextPage, fetchNextPage, refetch, isFetchingNextPage } = useInfiniteQuery<
GetAllArtifactVersionsOkResponse,
Error,
GetAllArtifactVersionsOkResponse,
Array<string | undefined>
>({
queryKey: ['versionList', registryIdentifier, artifactIdentifier, versionSearchTerm],
queryFn: ({ pageParam = 0 }) =>
getAllArtifactVersions({
registry_ref: registryRef,
artifact: encodeRef(artifactIdentifier),
queryParams: {
page: pageParam,
size: DEFAULT_PAGE_SIZE,
search_term: versionSearchTerm
},
stringifyQueryParamsOptions: {
arrayFormat: 'repeat'
}
}),
getNextPageParam: lastPage => {
const totalPages = lastPage.content.data.pageCount ?? 0
const lastPageNumber = lastPage.content.data.pageIndex ?? -1
return lastPageNumber < totalPages - 1 ? lastPageNumber + 1 : undefined
}
})
const { list: versionList, count: totalVersions } = useMemo(() => {
const list = data?.pages.flatMap(page => page.content.data.artifactVersions ?? []) || []
const count = data?.pages[0].content.data.itemCount || 0
return { list, count }
}, [data])
return (
<TreeNodeList>
{totalVersions > DEFAULT_PAGE_SIZE && (
<TreeNodeSearchInput
level={2}
defaultValue={versionSearchTerm}
onChange={val => {
updateQueryParams({ versionSearchTerm: val })
}}
/>
)}
<TreeBody
loading={isLoading}
error={error?.message}
retryOnError={refetch}
isEmpty={!versionList.length}
emptyDataMessage={getString('versionList.table.noVersionsTitle')}>
{versionList.map((each, idx) => (
<VersionTreeNodeViewWidget
key={each.name}
packageType={each.packageType as RepositoryPackageType}
artifactIdentifier={artifactIdentifier}
data={{
...each,
registryIdentifier
}}
parentNodeLevels={parentNodeLevels}
isLastChild={idx === versionList.length - 1}
/>
))}
{hasNextPage && <TreeLoadMoreNode level={2} onClick={() => fetchNextPage()} disabled={isFetchingNextPage} />}
</TreeBody>
</TreeNodeList>
)
}

View File

@ -30,6 +30,7 @@ type GetVersionListQueryParams = {
size: number
sort: string[]
searchTerm: string
versionSearchTerm?: string
isDeployedArtifacts: boolean
packageTypes: RepositoryPackageType[]
repositoryKey: string

View File

@ -15,6 +15,7 @@
*/
import { defaultTo, isEmpty } from 'lodash-es'
import type {
ArtifactDetailsPathParams,
RedirectPageQueryParams,
@ -25,19 +26,20 @@ import type {
VersionDetailsPathParams,
VersionDetailsTabPathParams
} from './types'
import { IRouteOptions, routeDefinitionWithMode } from './utils'
export interface ARRouteDefinitionsReturn {
toAR: () => string
toARRedirect: (params?: RedirectPageQueryParams) => string
toARRepositories: () => string
toARRepositoryDetails: (params: RepositoryDetailsPathParams) => string
toARRepositoryDetailsTab: (params: RepositoryDetailsTabPathParams) => string
toARArtifacts: () => string
toARArtifactDetails: (params: ArtifactDetailsPathParams) => string
toARVersionDetails: (params: VersionDetailsPathParams) => string
toARVersionDetailsTab: (params: VersionDetailsTabPathParams) => string
toARRepositoryWebhookDetails: (params: RepositoryWebhookDetailsPathParams) => string
toARRepositoryWebhookDetailsTab: (params: RepositoryWebhookDetailsTabPathParams) => string
toARRedirect: (params?: RedirectPageQueryParams, options?: IRouteOptions) => string
toARRepositories: (_?: unknown, options?: IRouteOptions) => string
toARRepositoryDetails: (params: RepositoryDetailsPathParams, options?: IRouteOptions) => string
toARRepositoryDetailsTab: (params: RepositoryDetailsTabPathParams, options?: IRouteOptions) => string
toARArtifacts: (_?: unknown, options?: IRouteOptions) => string
toARArtifactDetails: (params: ArtifactDetailsPathParams, options?: IRouteOptions) => string
toARVersionDetails: (params: VersionDetailsPathParams, options?: IRouteOptions) => string
toARVersionDetailsTab: (params: VersionDetailsTabPathParams, options?: IRouteOptions) => string
toARRepositoryWebhookDetails: (params: RepositoryWebhookDetailsPathParams, options?: IRouteOptions) => string
toARRepositoryWebhookDetailsTab: (params: RepositoryWebhookDetailsTabPathParams, options?: IRouteOptions) => string
}
export const routeDefinitions: ARRouteDefinitionsReturn = {
@ -55,14 +57,20 @@ export const routeDefinitions: ARRouteDefinitionsReturn = {
}
return '/redirect'
},
toARRepositories: () => '/registries',
toARRepositoryDetails: params => `/registries/${params?.repositoryIdentifier}`,
toARRepositoryDetailsTab: params => `/registries/${params?.repositoryIdentifier}/${params?.tab}`,
toARArtifacts: () => '/artifacts',
toARArtifactDetails: params => `/registries/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}`,
toARVersionDetails: params =>
`/registries/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}`,
toARVersionDetailsTab: params => {
toARRepositories: routeDefinitionWithMode(() => '/registries'),
toARRepositoryDetails: routeDefinitionWithMode(params => `/registries/${params?.repositoryIdentifier}`),
toARRepositoryDetailsTab: routeDefinitionWithMode(
params => `/registries/${params?.repositoryIdentifier}/${params.tab}`
),
toARArtifacts: routeDefinitionWithMode(() => '/artifacts'),
toARArtifactDetails: routeDefinitionWithMode(
params => `/registries/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}`
),
toARVersionDetails: routeDefinitionWithMode(
params =>
`/registries/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}`
),
toARVersionDetailsTab: routeDefinitionWithMode(params => {
let route = `/registries/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}`
if (params.orgIdentifier) route += `/orgs/${params.orgIdentifier}`
if (params.projectIdentifier) route += `/projects/${params.projectIdentifier}`
@ -74,7 +82,7 @@ export const routeDefinitions: ARRouteDefinitionsReturn = {
}
route += `/${params.versionTab}`
return route
},
}),
toARRepositoryWebhookDetails: params =>
`/registries/${params?.repositoryIdentifier}/webhooks/${params?.webhookIdentifier}`,
toARRepositoryWebhookDetailsTab: params =>

View File

@ -18,8 +18,9 @@ import React from 'react'
import { Redirect, Switch } from 'react-router-dom'
import { Parent } from '@ar/common/types'
import { useAppStore, useRoutes } from '@ar/hooks'
import { useAppStore, useGetRepositoryListViewType, useRoutes } from '@ar/hooks'
import RedirectPage from '@ar/pages/redirect-page/RedirectPage'
import { RepositoryListViewTypeEnum } from '@ar/contexts/AppStoreContext'
import type { WebhookDetailsTab } from '@ar/pages/webhook-details/constants'
import type { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
@ -34,6 +35,7 @@ import type {
} from './types'
const RepositoryListPage = React.lazy(() => import('@ar/pages/repository-list/RepositoryListPage'))
const RepositoryListTreeViewPage = React.lazy(() => import('@ar/pages/repository-list/RepositoryListTreeViewPage'))
const RepositoryDetailsPage = React.lazy(() => import('@ar/pages/repository-details/RepositoryDetailsPage'))
const ArtifactListPage = React.lazy(() => import('@ar/pages/artifact-list/ArtifactListPage'))
const ArtifactDetailsPage = React.lazy(() => import('@ar/pages/artifact-details/ArtifactDetailsPage'))
@ -51,7 +53,7 @@ export const repositoryDetailsTabPathProps: RepositoryDetailsTabPathParams = {
tab: ':tab' as RepositoryDetailsTab
}
const artifactDetailsPathProps: ArtifactDetailsPathParams = {
export const artifactDetailsPathProps: ArtifactDetailsPathParams = {
...repositoryDetailsPathProps,
artifactIdentifier: ':artifactIdentifier'
}
@ -102,6 +104,9 @@ export const repositoryWebhookDetailsTabPathParams: RepositoryWebhookDetailsTabP
const RouteDestinations = (): JSX.Element => {
const routes = useRoutes(true)
const { parent } = useAppStore()
const repositoryListViewType = useGetRepositoryListViewType()
const shouldUseSeperateVersionDetailsRoute =
parent === Parent.Enterprise || repositoryListViewType === RepositoryListViewTypeEnum.DIRECTORY
return (
<Switch>
<RouteProvider exact path={routes.toAR()}>
@ -110,27 +115,38 @@ const RouteDestinations = (): JSX.Element => {
<RouteProvider exact path={routes.toARRedirect()}>
<RedirectPage />
</RouteProvider>
<RouteProvider exact path={routes.toARRepositories()}>
<RepositoryListPage />
</RouteProvider>
<RouteProvider exact path={routes.toARArtifacts()}>
<ArtifactListPage />
</RouteProvider>
{repositoryListViewType === RepositoryListViewTypeEnum.DIRECTORY && (
<RouteProvider path={routes.toARRepositories()}>
<RepositoryListTreeViewPage />
</RouteProvider>
)}
{repositoryListViewType === RepositoryListViewTypeEnum.LIST && (
<RouteProvider exact path={routes.toARRepositories()}>
<RepositoryListPage />
</RouteProvider>
)}
{/* IF Enterprise then will use different route for version details page
* IF repositoryListViewType = DIRECTORY then will use different route for version details page
* IF OSS then will use version details as sub route for artifact details page
*/}
<RouteProvider
exact={parent === Parent.Enterprise}
exact={shouldUseSeperateVersionDetailsRoute}
path={routes.toARArtifactDetails({ ...artifactDetailsPathProps })}>
<>
<ArtifactDetailsPage />
{parent === Parent.OSS && (
<Switch>
<RouteProvider exact path={routes.toARVersionDetails({ ...versionDetailsPathParams })}>
<RouteProvider path={routes.toARVersionDetails({ ...versionDetailsPathParams })}>
<OSSVersionDetailsPage />
</RouteProvider>
</Switch>
)}
</>
</RouteProvider>
{parent === Parent.Enterprise && (
{shouldUseSeperateVersionDetailsRoute && (
<RouteProvider path={routes.toARVersionDetails({ ...versionDetailsPathParams })}>
<VersionDetailsPage />
</RouteProvider>

View File

@ -15,6 +15,7 @@
*/
import { defaultTo } from 'lodash-es'
import QueryString from 'qs'
export function normalizePath(url: string): string {
return url.replace(/\/{2,}/g, '/')
@ -24,3 +25,19 @@ export const encodePathParams = (val?: string): string => {
if (typeof val === 'string' && val.startsWith(':')) return val
return encodeURIComponent(defaultTo(val, ''))
}
export interface IRouteOptions {
queryParams?: Record<string, string>
}
export function routeDefinitionWithMode<T>(fn: (params: T) => string) {
return (params: T, options?: IRouteOptions) => {
const { queryParams } = options || {}
let route = fn(params)
if (queryParams) {
const queryString = QueryString.stringify(queryParams, { encode: false })
route = `${route}?${queryString}`
}
return route
}
}

View File

@ -11,6 +11,7 @@ prodCount: '{{count}} Prod'
nonProdCount: '{{count}} Non-Prod'
copy: Copy
view: View
loadMore: Load More
noMatchingFilterData: No results found matching the filter criteria
stepNotFound: Step not found
tabNotFound: Tab not found
@ -48,6 +49,7 @@ createdAt: Created At
modifiedAt: Modified At
plaintext: Plain Text
encrypted: Encrypted
details: Details
tags:
latest: LATEST
latestVersion: LATEST VERSION

View File

@ -107,11 +107,13 @@ export interface StringsMap {
'repositoryDetails.upstreamProxiesSelectList.selectedList.note.message': string
'repositoryList.artifactRegistry.label': string
'repositoryList.artifactRegistry.subLabel': string
'repositoryList.compact': string
'repositoryList.deleteModal.contentText': string
'repositoryList.deleteModal.title': string
'repositoryList.newRegistry': string
'repositoryList.newRepository': string
'repositoryList.pageHeading': string
'repositoryList.registryCount': string
'repositoryList.selectEnvironments': string
'repositoryList.selectLabels': string
'repositoryList.selectPackageTypes': string
@ -219,11 +221,13 @@ export interface StringsMap {
'versionDetails.overview.generalInformation.buildHost': string
'versionDetails.overview.generalInformation.buildTime': string
'versionDetails.overview.generalInformation.createdAndLastModifiedAt': string
'versionDetails.overview.generalInformation.createdAt': string
'versionDetails.overview.generalInformation.description': string
'versionDetails.overview.generalInformation.digest': string
'versionDetails.overview.generalInformation.downloads': string
'versionDetails.overview.generalInformation.homepage': string
'versionDetails.overview.generalInformation.license': string
'versionDetails.overview.generalInformation.modifiedAt': string
'versionDetails.overview.generalInformation.name': string
'versionDetails.overview.generalInformation.packageType': string
'versionDetails.overview.generalInformation.packager': string
@ -342,6 +346,7 @@ export interface StringsMap {
delete: string
description: string
descriptionPlaceholder: string
details: string
discard: string
download: string
encrypted: string
@ -352,6 +357,7 @@ export interface StringsMap {
harnessAI: string
id: string
lastUpdated: string
loadMore: string
loading: string
modifiedAt: string
moduleName: string

View File

@ -26,7 +26,7 @@ import type { FeatureFlags, Scope } from '@ar/MFEAppTypes'
import { Parent } from '@ar/common/types'
import { ModalProvider } from '@ar/__mocks__/hooks'
import type { UseStringsReturn } from '@ar/frameworks/strings'
import { AppStoreContext } from '@ar/contexts/AppStoreContext'
import { AppStoreContext, RepositoryListViewTypeEnum } from '@ar/contexts/AppStoreContext'
import ParentProvider from '@ar/contexts/ParentProvider'
import type { ParentProviderProps } from '@ar/contexts/ParentProvider'
import { StringsContextProvider } from '@ar/frameworks/strings/StringsContextProvider'
@ -82,7 +82,9 @@ export default function ArTestWrapper(props: PropsWithChildren<TestWrapperProps>
matchPath,
scope,
parent,
updateAppStore: noop
updateAppStore: noop,
repositoryListViewType: RepositoryListViewTypeEnum.LIST,
setRepositoryListViewType: noop
}}>
<StringsContextProvider initialStrings={stringsData} getString={getString}>
<ParentProvider