mirror of https://github.com/harness/drone.git
feat: [AH-1020]: support delete artifact action on registry artifact list and artifact details page (#3497)
* feat: [AH-1020]: fix failing unit tests * feat: [AH-1020]: enode artifact key before calling api * feat: [AH-1020]: add unit test for registry artifact list tables * feat: [AH-1020]: support delete artifact action on registry artifact list and artifact details pagetry-new-ui
parent
efcefd3c10
commit
285cb1b9ba
|
@ -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 { ArtifactActionProps } from './Version'
|
||||||
|
import type { VersionAbstractFactory } from './VersionAbstractFactory'
|
||||||
|
|
||||||
|
interface ArtifactActionsWidgetProps extends ArtifactActionProps {
|
||||||
|
factory?: VersionAbstractFactory
|
||||||
|
packageType: RepositoryPackageType
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArtifactActionsWidget(props: ArtifactActionsWidgetProps): JSX.Element {
|
||||||
|
const { factory = versionFactory, packageType, ...rest } = props
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const repositoryType = factory?.getVersionType(packageType)
|
||||||
|
if (!repositoryType) {
|
||||||
|
return <Text intent="warning">{getString('stepNotFound')}</Text>
|
||||||
|
}
|
||||||
|
return repositoryType.renderArtifactActions({ ...rest })
|
||||||
|
}
|
|
@ -15,9 +15,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PaginationProps } from '@harnessio/uicore'
|
import type { PaginationProps } from '@harnessio/uicore'
|
||||||
import type { ListArtifactVersion } from '@harnessio/react-har-service-client'
|
import type {
|
||||||
|
ArtifactSummary,
|
||||||
|
ListArtifactVersion,
|
||||||
|
RegistryArtifactMetadata
|
||||||
|
} from '@harnessio/react-har-service-client'
|
||||||
import type { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
|
import type { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
|
||||||
import type { Parent, RepositoryPackageType } from '@ar/common/types'
|
import type { PageType, Parent, RepositoryPackageType } from '@ar/common/types'
|
||||||
|
|
||||||
export interface VersionDetailsHeaderProps<T> {
|
export interface VersionDetailsHeaderProps<T> {
|
||||||
data: T
|
data: T
|
||||||
|
@ -39,6 +43,15 @@ export interface VersionListTableProps {
|
||||||
parent: Parent
|
parent: Parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ArtifactActionProps {
|
||||||
|
data: RegistryArtifactMetadata | ArtifactSummary
|
||||||
|
pageType: PageType
|
||||||
|
repoKey: string
|
||||||
|
artifactKey: string
|
||||||
|
readonly?: boolean
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class VersionStep<T> {
|
export abstract class VersionStep<T> {
|
||||||
protected abstract packageType: RepositoryPackageType
|
protected abstract packageType: RepositoryPackageType
|
||||||
protected abstract allowedVersionDetailsTabs: VersionDetailsTab[]
|
protected abstract allowedVersionDetailsTabs: VersionDetailsTab[]
|
||||||
|
@ -56,4 +69,6 @@ export abstract class VersionStep<T> {
|
||||||
abstract renderVersionDetailsHeader(props: VersionDetailsHeaderProps<T>): JSX.Element
|
abstract renderVersionDetailsHeader(props: VersionDetailsHeaderProps<T>): JSX.Element
|
||||||
|
|
||||||
abstract renderVersionDetailsTab(props: VersionDetailsTabProps): JSX.Element
|
abstract renderVersionDetailsTab(props: VersionDetailsTabProps): JSX.Element
|
||||||
|
|
||||||
|
abstract renderArtifactActions(props: ArtifactActionProps): JSX.Element
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default function getARRouteDefinitions(routeParams: Record<string, string
|
||||||
toARRepositoryDetails: params => `/${params?.repositoryIdentifier}`,
|
toARRepositoryDetails: params => `/${params?.repositoryIdentifier}`,
|
||||||
toARRepositoryDetailsTab: params => `/${params?.repositoryIdentifier}/${params?.tab}`,
|
toARRepositoryDetailsTab: params => `/${params?.repositoryIdentifier}/${params?.tab}`,
|
||||||
toARRepositoryWebhookDetails: params => `/${params?.repositoryIdentifier}/webhooks/${params?.webhookIdentifier}`,
|
toARRepositoryWebhookDetails: params => `/${params?.repositoryIdentifier}/webhooks/${params?.webhookIdentifier}`,
|
||||||
toARArtifacts: () => `/${routeParams?.repositoryIdentifier}?tab=packages`,
|
toARArtifacts: () => `/${routeParams?.repositoryIdentifier}/packages`,
|
||||||
toARArtifactDetails: params => `/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}`,
|
toARArtifactDetails: params => `/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}`,
|
||||||
toARVersionDetails: params =>
|
toARVersionDetails: params =>
|
||||||
`/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}`,
|
`/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}`,
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.optionsMenu {
|
|
||||||
min-width: unset;
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 optionsMenu: string
|
|
|
@ -15,45 +15,46 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Menu, Position } from '@blueprintjs/core'
|
|
||||||
import { Button, ButtonVariation } from '@harnessio/uicore'
|
|
||||||
|
|
||||||
import DeleteRepositoryMenuItem from './DeleteRepository'
|
import { PageType } from '@ar/common/types'
|
||||||
import EditRepositoryMenuItem from './EditRepository'
|
import ActionButton from '@ar/components/ActionButton/ActionButton'
|
||||||
import SetupClientMenuItem from './SetupClient'
|
|
||||||
|
import SetupClientMenuItem from './SetupClientMenuItem'
|
||||||
import type { ArtifactActionProps } from './types'
|
import type { ArtifactActionProps } from './types'
|
||||||
|
import DeleteArtifactMenuItem from './DeleteArtifactMenuItem'
|
||||||
|
|
||||||
import css from './ArtifactActions.module.scss'
|
export default function ArtifactActions({
|
||||||
|
data,
|
||||||
export default function ArtifactActions({ data, repoKey }: ArtifactActionProps): JSX.Element {
|
repoKey,
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
artifactKey,
|
||||||
|
pageType,
|
||||||
|
readonly,
|
||||||
|
onClose
|
||||||
|
}: ArtifactActionProps): JSX.Element {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
return (
|
return (
|
||||||
<Button
|
<ActionButton isOpen={open} setOpen={setOpen}>
|
||||||
variation={ButtonVariation.ICON}
|
<DeleteArtifactMenuItem
|
||||||
icon="Options"
|
artifactKey={artifactKey}
|
||||||
tooltip={
|
repoKey={repoKey}
|
||||||
<Menu
|
data={data}
|
||||||
className={css.optionsMenu}
|
pageType={pageType}
|
||||||
onClick={e => {
|
readonly={readonly}
|
||||||
e.stopPropagation()
|
onClose={() => {
|
||||||
}}>
|
setOpen(false)
|
||||||
<DeleteRepositoryMenuItem data={data} repoKey={repoKey} />
|
onClose?.()
|
||||||
<EditRepositoryMenuItem data={data} repoKey={repoKey} />
|
}}
|
||||||
<SetupClientMenuItem data={data} repoKey={repoKey} />
|
/>
|
||||||
</Menu>
|
{pageType === PageType.Table && (
|
||||||
}
|
<SetupClientMenuItem
|
||||||
tooltipProps={{
|
data={data}
|
||||||
interactionKind: 'click',
|
pageType={pageType}
|
||||||
onInteraction: nextOpenState => {
|
readonly={readonly}
|
||||||
setMenuOpen(nextOpenState)
|
onClose={() => setOpen(false)}
|
||||||
},
|
artifactKey={artifactKey}
|
||||||
isOpen: menuOpen,
|
repoKey={repoKey}
|
||||||
position: Position.BOTTOM
|
/>
|
||||||
}}
|
)}
|
||||||
onClick={e => {
|
</ActionButton>
|
||||||
e.stopPropagation()
|
|
||||||
setMenuOpen(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,23 +18,34 @@ import React from 'react'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
import { useStrings } from '@ar/frameworks/strings'
|
import { useStrings } from '@ar/frameworks/strings'
|
||||||
|
import { queryClient } from '@ar/utils/queryClient'
|
||||||
import { useParentComponents, useRoutes } from '@ar/hooks'
|
import { useParentComponents, useRoutes } from '@ar/hooks'
|
||||||
|
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
|
||||||
import { PermissionIdentifier, ResourceType } from '@ar/common/permissionTypes'
|
import { PermissionIdentifier, ResourceType } from '@ar/common/permissionTypes'
|
||||||
import useDeleteRepositoryModal from '@ar/pages/repository-details/hooks/useDeleteRepositoryModal/useDeleteRepositoryModal'
|
|
||||||
|
|
||||||
import type { ArtifactActionProps } from './types'
|
import type { ArtifactActionProps } from './types'
|
||||||
|
import useDeleteArtifactModal from '../../hooks/useDeleteArtifactModal/useDeleteArtifactModal'
|
||||||
|
|
||||||
export default function DeleteRepositoryMenuItem({ repoKey }: ArtifactActionProps): JSX.Element {
|
export default function DeleteArtifactMenuItem(props: ArtifactActionProps): JSX.Element {
|
||||||
|
const { artifactKey, repoKey, readonly, onClose } = props
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { RbacMenuItem } = useParentComponents()
|
const { RbacMenuItem } = useParentComponents()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const routes = useRoutes()
|
const routes = useRoutes()
|
||||||
|
|
||||||
const handleAfterDeleteRepository = (): void => {
|
const handleAfterDeleteRepository = (): void => {
|
||||||
history.push(routes.toARArtifacts())
|
onClose?.()
|
||||||
|
queryClient.invalidateQueries(['GetAllArtifactsByRegistry'])
|
||||||
|
history.push(
|
||||||
|
routes.toARRepositoryDetailsTab({
|
||||||
|
repositoryIdentifier: repoKey,
|
||||||
|
tab: RepositoryDetailsTab.PACKAGES
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { triggerDelete } = useDeleteRepositoryModal({
|
const { triggerDelete } = useDeleteArtifactModal({
|
||||||
|
artifactKey,
|
||||||
repoKey,
|
repoKey,
|
||||||
onSuccess: handleAfterDeleteRepository
|
onSuccess: handleAfterDeleteRepository
|
||||||
})
|
})
|
||||||
|
@ -46,14 +57,15 @@ export default function DeleteRepositoryMenuItem({ repoKey }: ArtifactActionProp
|
||||||
return (
|
return (
|
||||||
<RbacMenuItem
|
<RbacMenuItem
|
||||||
icon="code-delete"
|
icon="code-delete"
|
||||||
text={getString('artifactList.table.actions.deleteRepository')}
|
text={getString('artifactList.table.actions.deleteArtifact')}
|
||||||
onClick={handleDeleteService}
|
onClick={handleDeleteService}
|
||||||
|
disabled={readonly}
|
||||||
permission={{
|
permission={{
|
||||||
resource: {
|
resource: {
|
||||||
resourceType: ResourceType.ARTIFACT_REGISTRY,
|
resourceType: ResourceType.ARTIFACT_REGISTRY,
|
||||||
resourceIdentifier: repoKey
|
resourceIdentifier: artifactKey
|
||||||
},
|
},
|
||||||
permission: PermissionIdentifier.DELETE_ARTIFACT_REGISTRY
|
permission: PermissionIdentifier.DELETE_ARTIFACT
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 { useHistory } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { useStrings } from '@ar/frameworks/strings'
|
|
||||||
import { useParentComponents, useRoutes } from '@ar/hooks'
|
|
||||||
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
|
|
||||||
import { PermissionIdentifier, ResourceType } from '@ar/common/permissionTypes'
|
|
||||||
|
|
||||||
import type { ArtifactActionProps } from './types'
|
|
||||||
|
|
||||||
export default function EditRepositoryMenuItem({ repoKey }: ArtifactActionProps): JSX.Element {
|
|
||||||
const { getString } = useStrings()
|
|
||||||
const { RbacMenuItem } = useParentComponents()
|
|
||||||
const history = useHistory()
|
|
||||||
const routes = useRoutes()
|
|
||||||
|
|
||||||
const handleOpenRepository = (): void => {
|
|
||||||
history.push(
|
|
||||||
routes.toARRepositoryDetailsTab({
|
|
||||||
repositoryIdentifier: repoKey,
|
|
||||||
tab: RepositoryDetailsTab.CONFIGURATION
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<RbacMenuItem
|
|
||||||
icon="edit"
|
|
||||||
text={getString('artifactList.table.actions.editRepository')}
|
|
||||||
onClick={handleOpenRepository}
|
|
||||||
permission={{
|
|
||||||
resource: {
|
|
||||||
resourceType: ResourceType.ARTIFACT_REGISTRY,
|
|
||||||
resourceIdentifier: repoKey
|
|
||||||
},
|
|
||||||
permission: PermissionIdentifier.EDIT_ARTIFACT_REGISTRY
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -25,10 +25,10 @@ import { useSetupClientModal } from '@ar/pages/repository-details/hooks/useSetup
|
||||||
|
|
||||||
import type { ArtifactActionProps } from './types'
|
import type { ArtifactActionProps } from './types'
|
||||||
|
|
||||||
export default function SetupClientMenuItem({ data, repoKey }: ArtifactActionProps): JSX.Element {
|
export default function SetupClientMenuItem(props: ArtifactActionProps): JSX.Element {
|
||||||
|
const { artifactKey, repoKey, data, readonly } = props
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { RbacMenuItem } = useParentComponents()
|
const { RbacMenuItem } = useParentComponents()
|
||||||
const artifactKey = data.imageName || ''
|
|
||||||
|
|
||||||
const [showSetupClientModal] = useSetupClientModal({
|
const [showSetupClientModal] = useSetupClientModal({
|
||||||
repoKey,
|
repoKey,
|
||||||
|
@ -41,6 +41,7 @@ export default function SetupClientMenuItem({ data, repoKey }: ArtifactActionPro
|
||||||
icon="setup-client"
|
icon="setup-client"
|
||||||
text={getString('actions.setupClient')}
|
text={getString('actions.setupClient')}
|
||||||
onClick={showSetupClientModal}
|
onClick={showSetupClientModal}
|
||||||
|
disabled={readonly}
|
||||||
permission={{
|
permission={{
|
||||||
resource: {
|
resource: {
|
||||||
resourceType: ResourceType.ARTIFACT_REGISTRY,
|
resourceType: ResourceType.ARTIFACT_REGISTRY,
|
|
@ -14,9 +14,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ArtifactSummary } from '@harnessio/react-har-service-client'
|
import type { ArtifactSummary, RegistryArtifactMetadata } from '@harnessio/react-har-service-client'
|
||||||
|
import type { PageType } from '@ar/common/types'
|
||||||
|
|
||||||
export interface ArtifactActionProps {
|
export interface ArtifactActionProps {
|
||||||
data: ArtifactSummary
|
data: ArtifactSummary | RegistryArtifactMetadata
|
||||||
|
artifactKey: string
|
||||||
repoKey: string
|
repoKey: string
|
||||||
|
pageType: PageType
|
||||||
|
readonly?: boolean
|
||||||
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,12 @@ import type { ArtifactSummary } from '@harnessio/react-har-service-client'
|
||||||
|
|
||||||
import { useDecodedParams } from '@ar/hooks'
|
import { useDecodedParams } from '@ar/hooks'
|
||||||
import { useStrings } from '@ar/frameworks/strings/String'
|
import { useStrings } from '@ar/frameworks/strings/String'
|
||||||
import type { RepositoryPackageType } from '@ar/common/types'
|
import { PageType, type RepositoryPackageType } from '@ar/common/types'
|
||||||
import type { ArtifactDetailsPathParams } from '@ar/routes/types'
|
import type { ArtifactDetailsPathParams } from '@ar/routes/types'
|
||||||
import WeeklyDownloads from '@ar/components/PageTitle/WeeklyDownloads'
|
import WeeklyDownloads from '@ar/components/PageTitle/WeeklyDownloads'
|
||||||
import CreatedAndModifiedAt from '@ar/components/PageTitle/CreatedAndModifiedAt'
|
import CreatedAndModifiedAt from '@ar/components/PageTitle/CreatedAndModifiedAt'
|
||||||
import NameAndDescription from '@ar/components/PageTitle/NameAndDescription'
|
import NameAndDescription from '@ar/components/PageTitle/NameAndDescription'
|
||||||
|
import ArtifactActionsWidget from '@ar/frameworks/Version/ArtifactActionsWidget'
|
||||||
import SetupClientButton from '@ar/components/SetupClientButton/SetupClientButton'
|
import SetupClientButton from '@ar/components/SetupClientButton/SetupClientButton'
|
||||||
|
|
||||||
import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon'
|
import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon'
|
||||||
|
@ -69,6 +70,13 @@ function ArtifactDetailsHeaderContent(props: ArtifactDetailsHeaderContentProps):
|
||||||
artifactIdentifier={artifactIdentifier}
|
artifactIdentifier={artifactIdentifier}
|
||||||
packageType={packageType as RepositoryPackageType}
|
packageType={packageType as RepositoryPackageType}
|
||||||
/>
|
/>
|
||||||
|
<ArtifactActionsWidget
|
||||||
|
packageType={packageType as RepositoryPackageType}
|
||||||
|
data={data as ArtifactSummary}
|
||||||
|
repoKey={repositoryIdentifier}
|
||||||
|
artifactKey={artifactIdentifier}
|
||||||
|
pageType={PageType.Details}
|
||||||
|
/>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* 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 { Intent } from '@blueprintjs/core'
|
||||||
|
import { getErrorInfoFromErrorObject, useToaster } from '@harnessio/uicore'
|
||||||
|
import { useDeleteArtifactMutation } from '@harnessio/react-har-service-client'
|
||||||
|
|
||||||
|
import { useGetSpaceRef, useParentHooks } from '@ar/hooks'
|
||||||
|
import { useStrings } from '@ar/frameworks/strings'
|
||||||
|
import { encodeRef } from '@ar/hooks/useGetSpaceRef'
|
||||||
|
|
||||||
|
interface useDeleteArtifactModalProps {
|
||||||
|
repoKey: string
|
||||||
|
artifactKey: string
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
export default function useDeleteArtifactModal(props: useDeleteArtifactModalProps) {
|
||||||
|
const { repoKey, onSuccess, artifactKey } = props
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { showSuccess, showError, clear } = useToaster()
|
||||||
|
const { useConfirmationDialog } = useParentHooks()
|
||||||
|
const spaceRef = useGetSpaceRef(repoKey)
|
||||||
|
|
||||||
|
const { mutateAsync: deleteArtifact } = useDeleteArtifactMutation()
|
||||||
|
|
||||||
|
const handleDeleteArtifact = async (isConfirmed: boolean): Promise<void> => {
|
||||||
|
if (isConfirmed) {
|
||||||
|
try {
|
||||||
|
const response = await deleteArtifact({
|
||||||
|
registry_ref: spaceRef,
|
||||||
|
artifact: encodeRef(artifactKey)
|
||||||
|
})
|
||||||
|
if (response.content.status === 'SUCCESS') {
|
||||||
|
clear()
|
||||||
|
showSuccess(getString('artifactDetails.artifactDeleted'))
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
showError(getErrorInfoFromErrorObject(e, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { openDialog } = useConfirmationDialog({
|
||||||
|
titleText: getString('artifactDetails.deleteArtifactModal.title'),
|
||||||
|
contentText: getString('artifactDetails.deleteArtifactModal.contentText'),
|
||||||
|
confirmButtonText: getString('delete'),
|
||||||
|
cancelButtonText: getString('cancel'),
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
onCloseDialog: handleDeleteArtifact
|
||||||
|
})
|
||||||
|
|
||||||
|
return { triggerDelete: openDialog }
|
||||||
|
}
|
|
@ -3,3 +3,7 @@ downloadsThisWeek: Downloads This Week
|
||||||
artifactLabelInputPlaceholder: Type input and hit enter
|
artifactLabelInputPlaceholder: Type input and hit enter
|
||||||
labelsUpdated: Artifact labels updated successfully!
|
labelsUpdated: Artifact labels updated successfully!
|
||||||
totalDownloads: Total Downloads
|
totalDownloads: Total Downloads
|
||||||
|
artifactDeleted: Artifact deleted successfully!
|
||||||
|
deleteArtifactModal:
|
||||||
|
title: Delete Artifact
|
||||||
|
contentText: Are you sure you want to delete the artifact?
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
div[class*='TableV2--cells'],
|
div[class*='TableV2--cells'],
|
||||||
div[class*='TableV2--header'] {
|
div[class*='TableV2--header'] {
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
grid-template-columns: minmax(var(--har-table-name-column-min-width), 1fr) 15rem 20rem 15rem;
|
grid-template-columns: minmax(var(--har-table-name-column-min-width), 1fr) 15rem 20rem 15rem 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import type { ListRegistryArtifact, RegistryArtifactMetadata } from '@harnessio/
|
||||||
import { useStrings } from '@ar/frameworks/strings'
|
import { useStrings } from '@ar/frameworks/strings'
|
||||||
import { useParentHooks } from '@ar/hooks'
|
import { useParentHooks } from '@ar/hooks'
|
||||||
import {
|
import {
|
||||||
|
RegistryArtifactActionsCell,
|
||||||
RegistryArtifactDownloadsCell,
|
RegistryArtifactDownloadsCell,
|
||||||
RegistryArtifactLatestUpdatedCell,
|
RegistryArtifactLatestUpdatedCell,
|
||||||
RegistryArtifactNameCell,
|
RegistryArtifactNameCell,
|
||||||
|
@ -95,6 +96,12 @@ export default function RegistryArtifactListTable(props: RegistryArtifactListTab
|
||||||
accessor: 'latestVersion',
|
accessor: 'latestVersion',
|
||||||
Cell: RegistryArtifactLatestUpdatedCell,
|
Cell: RegistryArtifactLatestUpdatedCell,
|
||||||
serverSortProps: getServerSortProps('latestVersion')
|
serverSortProps: getServerSortProps('latestVersion')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: '',
|
||||||
|
accessor: 'actions',
|
||||||
|
Cell: RegistryArtifactActionsCell,
|
||||||
|
disableSortBy: true
|
||||||
}
|
}
|
||||||
].filter(Boolean) as unknown as Column<RegistryArtifactMetadata>[]
|
].filter(Boolean) as unknown as Column<RegistryArtifactMetadata>[]
|
||||||
}, [currentOrder, currentSort, getString, onClickLabel])
|
}, [currentOrder, currentSort, getString, onClickLabel])
|
||||||
|
|
|
@ -28,8 +28,9 @@ import { useStrings } from '@ar/frameworks/strings'
|
||||||
import TableCells from '@ar/components/TableCells/TableCells'
|
import TableCells from '@ar/components/TableCells/TableCells'
|
||||||
import LabelsPopover from '@ar/components/LabelsPopover/LabelsPopover'
|
import LabelsPopover from '@ar/components/LabelsPopover/LabelsPopover'
|
||||||
import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon'
|
import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon'
|
||||||
import type { RepositoryPackageType } from '@ar/common/types'
|
import { PageType, type RepositoryPackageType } from '@ar/common/types'
|
||||||
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
|
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
|
||||||
|
import ArtifactActionsWidget from '@ar/frameworks/Version/ArtifactActionsWidget'
|
||||||
import { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
|
import { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
|
||||||
|
|
||||||
type CellTypeWithActions<D extends Record<string, any>, V = any> = TableInstance<D> & {
|
type CellTypeWithActions<D extends Record<string, any>, V = any> = TableInstance<D> & {
|
||||||
|
@ -143,3 +144,16 @@ export const RegistryArtifactLatestUpdatedCell: CellType = ({ row }) => {
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const RegistryArtifactActionsCell: CellType = ({ row }) => {
|
||||||
|
const { original } = row
|
||||||
|
return (
|
||||||
|
<ArtifactActionsWidget
|
||||||
|
packageType={original.packageType as RepositoryPackageType}
|
||||||
|
pageType={PageType.Table}
|
||||||
|
data={original}
|
||||||
|
repoKey={original.registryIdentifier}
|
||||||
|
artifactKey={original.name}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ table:
|
||||||
actions:
|
actions:
|
||||||
editRepository: Edit Registry
|
editRepository: Edit Registry
|
||||||
deleteRepository: Delete Registry
|
deleteRepository: Delete Registry
|
||||||
|
deleteArtifact: Delete Artifact
|
||||||
VulnerabilityStatus:
|
VulnerabilityStatus:
|
||||||
scanned: Scanned
|
scanned: Scanned
|
||||||
nonScanned: Not Scanned
|
nonScanned: Not Scanned
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const MockGetArtifactsByRegistryResponse: GetAllArtifactsByRegistryOkResp
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
itemCount: 1,
|
itemCount: 1,
|
||||||
pageCount: 1,
|
pageCount: 3,
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 50
|
pageSize: 50
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const MockGetGenericArtifactsByRegistryResponse: GetAllArtifactsByRegistr
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
pageCount: 0,
|
pageCount: 2,
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 50
|
pageSize: 50
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const MockGetHelmArtifactsByRegistryResponse: GetAllArtifactsByRegistryOk
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
itemCount: 1,
|
itemCount: 1,
|
||||||
pageCount: 1,
|
pageCount: 2,
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 50
|
pageSize: 50
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const MockGetMavenArtifactsByRegistryResponse: GetAllArtifactsByRegistryO
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
pageCount: 0,
|
pageCount: 2,
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 50
|
pageSize: 50
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,11 +19,13 @@ import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client
|
||||||
|
|
||||||
import { String } from '@ar/frameworks/strings'
|
import { String } from '@ar/frameworks/strings'
|
||||||
import { RepositoryPackageType } from '@ar/common/types'
|
import { RepositoryPackageType } from '@ar/common/types'
|
||||||
|
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
|
||||||
import DockerVersionListTable from '@ar/pages/version-list/DockerVersion/VersionListTable/DockerVersionListTable'
|
import DockerVersionListTable from '@ar/pages/version-list/DockerVersion/VersionListTable/DockerVersionListTable'
|
||||||
import {
|
import {
|
||||||
VersionDetailsHeaderProps,
|
type ArtifactActionProps,
|
||||||
VersionDetailsTabProps,
|
type VersionDetailsHeaderProps,
|
||||||
VersionListTableProps,
|
type VersionDetailsTabProps,
|
||||||
|
type VersionListTableProps,
|
||||||
VersionStep
|
VersionStep
|
||||||
} from '@ar/frameworks/Version/Version'
|
} from '@ar/frameworks/Version/Version'
|
||||||
|
|
||||||
|
@ -72,4 +74,8 @@ export class DockerVersionType extends VersionStep<ArtifactVersionSummary> {
|
||||||
return <String stringID="tabNotFound" />
|
return <String stringID="tabNotFound" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
|
||||||
|
return <ArtifactActions {...props} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* 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 userEvent from '@testing-library/user-event'
|
||||||
|
import { useGetAllArtifactsByRegistryQuery as _useGetAllArtifactsByRegistryQuery } from '@harnessio/react-har-service-client'
|
||||||
|
import { fireEvent, getByText as globalGetByText, render, waitFor } from '@testing-library/react'
|
||||||
|
|
||||||
|
import '@ar/pages/version-details/VersionFactory'
|
||||||
|
import '@ar/pages/repository-details/RepositoryFactory'
|
||||||
|
|
||||||
|
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
|
||||||
|
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
|
||||||
|
import RepositoryDetailsPage from '@ar/pages/repository-details/RepositoryDetailsPage'
|
||||||
|
import {
|
||||||
|
MockGetArtifactsByRegistryResponse,
|
||||||
|
MockGetDockerRegistryResponseWithAllData
|
||||||
|
} from '@ar/pages/repository-details/DockerRepository/__tests__/__mockData__'
|
||||||
|
import { getTableColumn } from '@ar/utils/testUtils/utils'
|
||||||
|
|
||||||
|
const useGetAllArtifactsByRegistryQuery = _useGetAllArtifactsByRegistryQuery as jest.Mock
|
||||||
|
|
||||||
|
const deleteArtifact = jest.fn().mockImplementation(() => Promise.resolve({}))
|
||||||
|
|
||||||
|
jest.mock('@harnessio/react-har-service-client', () => ({
|
||||||
|
useGetAllArtifactsByRegistryQuery: jest.fn(),
|
||||||
|
useGetRegistryQuery: jest.fn().mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
error: false,
|
||||||
|
data: MockGetDockerRegistryResponseWithAllData
|
||||||
|
})),
|
||||||
|
useDeleteArtifactMutation: jest.fn().mockImplementation(() => ({
|
||||||
|
isLoading: false,
|
||||||
|
mutateAsync: deleteArtifact
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('Test Registry Artifact List Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: MockGetArtifactsByRegistryResponse,
|
||||||
|
error: false,
|
||||||
|
refetch: jest.fn()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should render empty list if artifacts response is empty', () => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: { content: { data: { artifacts: [] } } },
|
||||||
|
error: false,
|
||||||
|
refetch: []
|
||||||
|
}))
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
const noResultsText = getByText('artifactList.table.noArtifactsTitle')
|
||||||
|
expect(noResultsText).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should render artifacts list', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
const table = container.querySelector('[class*="TableV2--table"]')
|
||||||
|
expect(table).toBeInTheDocument()
|
||||||
|
|
||||||
|
const tableRows = container.querySelectorAll('[class*="TableV2--row"]')
|
||||||
|
expect(tableRows).toHaveLength(1)
|
||||||
|
|
||||||
|
const tableData = MockGetArtifactsByRegistryResponse.content.data.artifacts
|
||||||
|
|
||||||
|
const getFirstRowColumn = (col: number) => getTableColumn(1, col) as HTMLElement
|
||||||
|
expect(globalGetByText(getFirstRowColumn(1), tableData[0].name)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(2), tableData[0].registryIdentifier)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(3), tableData[0].downloadsCount?.toString() as string)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(4), tableData[0].latestVersion)).toBeInTheDocument()
|
||||||
|
|
||||||
|
const actionBtn = getFirstRowColumn(5).querySelector('span[data-icon=Options')
|
||||||
|
await userEvent.click(actionBtn as HTMLElement)
|
||||||
|
const dialogs = document.getElementsByClassName('bp3-popover')
|
||||||
|
await waitFor(() => expect(dialogs).toHaveLength(1))
|
||||||
|
const selectPopover = dialogs[0] as HTMLElement
|
||||||
|
|
||||||
|
const items = selectPopover.getElementsByClassName('bp3-menu-item')
|
||||||
|
expect(items).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(items[0]).toHaveTextContent('artifactList.table.actions.deleteArtifact')
|
||||||
|
expect(items[1]).toHaveTextContent('actions.setupClient')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should show error message if listing api fails', async () => {
|
||||||
|
const mockRefetchFn = jest.fn().mockImplementation()
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
message: 'error message'
|
||||||
|
},
|
||||||
|
refetch: mockRefetchFn
|
||||||
|
}))
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const errorText = getByText('error message')
|
||||||
|
expect(errorText).toBeInTheDocument()
|
||||||
|
|
||||||
|
const retryBtn = getByText('Retry')
|
||||||
|
expect(retryBtn).toBeInTheDocument()
|
||||||
|
await userEvent.click(retryBtn)
|
||||||
|
expect(mockRefetchFn).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should be able to search', async () => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: { content: { data: { artifacts: [] } } },
|
||||||
|
error: false,
|
||||||
|
refetch: []
|
||||||
|
}))
|
||||||
|
const { getByText, getByPlaceholderText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchInput = getByPlaceholderText('search')
|
||||||
|
expect(searchInput).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.change(searchInput, { target: { value: 'pod' } })
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
search_term: 'pod',
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearAllFiltersBtn = getByText('clearFilters')
|
||||||
|
await userEvent.click(clearAllFiltersBtn)
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Sorting should work', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const artifactNameSortIcon = getByText('artifactList.table.columns.name').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(artifactNameSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'name',
|
||||||
|
sort_order: 'ASC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const repositorySortIcon = getByText('artifactList.table.columns.repository').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(repositorySortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'registryIdentifier',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadsSortIcon = getByText('artifactList.table.columns.downloads').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(downloadsSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'downloadsCount',
|
||||||
|
sort_order: 'ASC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastUpdatedSortIcon = getByText('artifactList.table.columns.latestVersion').nextSibling
|
||||||
|
?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(lastUpdatedSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'latestVersion',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Pagination should work', async () => {
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const nextPageBtn = getByText('Next')
|
||||||
|
await userEvent.click(nextPageBtn)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 1,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageSizeSelect = getByTestId('dropdown-button')
|
||||||
|
await userEvent.click(pageSizeSelect)
|
||||||
|
const pageSize20option = getByText('20')
|
||||||
|
await userEvent.click(pageSize20option)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 20,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should list actions', async () => {
|
||||||
|
render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const getFirstRowColumn = (col: number) => getTableColumn(1, col) as HTMLElement
|
||||||
|
|
||||||
|
// click on 3 dots action btn
|
||||||
|
const actionBtn = getFirstRowColumn(5).querySelector('span[data-icon=Options')
|
||||||
|
await userEvent.click(actionBtn as HTMLElement)
|
||||||
|
|
||||||
|
const popovers = document.getElementsByClassName('bp3-popover')
|
||||||
|
await waitFor(() => expect(popovers).toHaveLength(1))
|
||||||
|
const selectPopover = popovers[0] as HTMLElement
|
||||||
|
const deleteItem = globalGetByText(selectPopover, 'artifactList.table.actions.deleteArtifact')
|
||||||
|
|
||||||
|
// click on delete action item
|
||||||
|
await userEvent.click(deleteItem)
|
||||||
|
const dialogs = document.getElementsByClassName('bp3-dialog')
|
||||||
|
await waitFor(() => expect(dialogs).toHaveLength(1))
|
||||||
|
const deleteDialog = dialogs[0] as HTMLElement
|
||||||
|
expect(globalGetByText(deleteDialog, 'artifactDetails.deleteArtifactModal.title')).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(deleteDialog, 'artifactDetails.deleteArtifactModal.contentText')).toBeInTheDocument()
|
||||||
|
|
||||||
|
const deleteBtn = deleteDialog.querySelector('button[aria-label=delete]')
|
||||||
|
const cancelBtn = deleteDialog.querySelector('button[aria-label=cancel]')
|
||||||
|
expect(deleteBtn).toBeInTheDocument()
|
||||||
|
expect(cancelBtn).toBeInTheDocument()
|
||||||
|
|
||||||
|
// click on delete button on modal
|
||||||
|
await userEvent.click(deleteBtn!)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(deleteArtifact).toHaveBeenCalledWith({
|
||||||
|
artifact: 'podinfo-artifact/+',
|
||||||
|
registry_ref: 'undefined/docker-repo/+'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -17,6 +17,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client'
|
import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client'
|
||||||
import {
|
import {
|
||||||
|
type ArtifactActionProps,
|
||||||
type VersionDetailsHeaderProps,
|
type VersionDetailsHeaderProps,
|
||||||
type VersionDetailsTabProps,
|
type VersionDetailsTabProps,
|
||||||
type VersionListTableProps,
|
type VersionListTableProps,
|
||||||
|
@ -28,6 +29,7 @@ import VersionListTable, {
|
||||||
type CommonVersionListTableProps
|
type CommonVersionListTableProps
|
||||||
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
|
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
|
||||||
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
|
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
|
||||||
|
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
|
||||||
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
|
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
|
||||||
import GenericOverviewPage from './pages/overview/OverviewPage'
|
import GenericOverviewPage from './pages/overview/OverviewPage'
|
||||||
import OSSContentPage from './pages/oss-details/OSSContentPage'
|
import OSSContentPage from './pages/oss-details/OSSContentPage'
|
||||||
|
@ -69,4 +71,8 @@ export class GenericVersionType extends VersionStep<ArtifactVersionSummary> {
|
||||||
return <String stringID="tabNotFound" />
|
return <String stringID="tabNotFound" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
|
||||||
|
return <ArtifactActions {...props} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* 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 userEvent from '@testing-library/user-event'
|
||||||
|
import { useGetAllArtifactsByRegistryQuery as _useGetAllArtifactsByRegistryQuery } from '@harnessio/react-har-service-client'
|
||||||
|
import { fireEvent, getByText as globalGetByText, render, waitFor } from '@testing-library/react'
|
||||||
|
|
||||||
|
import '@ar/pages/version-details/VersionFactory'
|
||||||
|
import '@ar/pages/repository-details/RepositoryFactory'
|
||||||
|
|
||||||
|
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
|
||||||
|
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
|
||||||
|
import RepositoryDetailsPage from '@ar/pages/repository-details/RepositoryDetailsPage'
|
||||||
|
import {
|
||||||
|
MockGetGenericArtifactsByRegistryResponse,
|
||||||
|
MockGetGenericRegistryResponseWithAllData
|
||||||
|
} from '@ar/pages/repository-details/GenericRepository/__tests__/__mockData__'
|
||||||
|
import { getTableColumn } from '@ar/utils/testUtils/utils'
|
||||||
|
|
||||||
|
const useGetAllArtifactsByRegistryQuery = _useGetAllArtifactsByRegistryQuery as jest.Mock
|
||||||
|
|
||||||
|
const deleteArtifact = jest.fn().mockImplementation(() => Promise.resolve({}))
|
||||||
|
|
||||||
|
jest.mock('@harnessio/react-har-service-client', () => ({
|
||||||
|
useGetAllArtifactsByRegistryQuery: jest.fn(),
|
||||||
|
useGetRegistryQuery: jest.fn().mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
error: false,
|
||||||
|
data: MockGetGenericRegistryResponseWithAllData
|
||||||
|
})),
|
||||||
|
useDeleteArtifactMutation: jest.fn().mockImplementation(() => ({
|
||||||
|
isLoading: false,
|
||||||
|
mutateAsync: deleteArtifact
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('Test Registry Artifact List Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: MockGetGenericArtifactsByRegistryResponse,
|
||||||
|
error: false,
|
||||||
|
refetch: jest.fn()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should render empty list if artifacts response is empty', () => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: { content: { data: { artifacts: [] } } },
|
||||||
|
error: false,
|
||||||
|
refetch: []
|
||||||
|
}))
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
const noResultsText = getByText('artifactList.table.noArtifactsTitle')
|
||||||
|
expect(noResultsText).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should render artifacts list', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
const table = container.querySelector('[class*="TableV2--table"]')
|
||||||
|
expect(table).toBeInTheDocument()
|
||||||
|
|
||||||
|
const tableRows = container.querySelectorAll('[class*="TableV2--row"]')
|
||||||
|
expect(tableRows).toHaveLength(1)
|
||||||
|
|
||||||
|
const tableData = MockGetGenericArtifactsByRegistryResponse.content.data.artifacts
|
||||||
|
|
||||||
|
const getFirstRowColumn = (col: number) => getTableColumn(1, col) as HTMLElement
|
||||||
|
expect(globalGetByText(getFirstRowColumn(1), tableData[0].name)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(2), tableData[0].registryIdentifier)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(3), tableData[0].downloadsCount?.toString() as string)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(4), tableData[0].latestVersion)).toBeInTheDocument()
|
||||||
|
|
||||||
|
const actionBtn = getFirstRowColumn(5).querySelector('span[data-icon=Options')
|
||||||
|
await userEvent.click(actionBtn as HTMLElement)
|
||||||
|
const dialogs = document.getElementsByClassName('bp3-popover')
|
||||||
|
await waitFor(() => expect(dialogs).toHaveLength(1))
|
||||||
|
const selectPopover = dialogs[0] as HTMLElement
|
||||||
|
|
||||||
|
const items = selectPopover.getElementsByClassName('bp3-menu-item')
|
||||||
|
expect(items).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(items[0]).toHaveTextContent('artifactList.table.actions.deleteArtifact')
|
||||||
|
expect(items[1]).toHaveTextContent('actions.setupClient')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should show error message if listing api fails', async () => {
|
||||||
|
const mockRefetchFn = jest.fn().mockImplementation()
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
message: 'error message'
|
||||||
|
},
|
||||||
|
refetch: mockRefetchFn
|
||||||
|
}))
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const errorText = getByText('error message')
|
||||||
|
expect(errorText).toBeInTheDocument()
|
||||||
|
|
||||||
|
const retryBtn = getByText('Retry')
|
||||||
|
expect(retryBtn).toBeInTheDocument()
|
||||||
|
await userEvent.click(retryBtn)
|
||||||
|
expect(mockRefetchFn).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should be able to search', async () => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: { content: { data: { artifacts: [] } } },
|
||||||
|
error: false,
|
||||||
|
refetch: []
|
||||||
|
}))
|
||||||
|
const { getByText, getByPlaceholderText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchInput = getByPlaceholderText('search')
|
||||||
|
expect(searchInput).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.change(searchInput, { target: { value: 'pod' } })
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
search_term: 'pod',
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearAllFiltersBtn = getByText('clearFilters')
|
||||||
|
await userEvent.click(clearAllFiltersBtn)
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Sorting should work', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const artifactNameSortIcon = getByText('artifactList.table.columns.name').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(artifactNameSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'name',
|
||||||
|
sort_order: 'ASC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const repositorySortIcon = getByText('artifactList.table.columns.repository').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(repositorySortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'registryIdentifier',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadsSortIcon = getByText('artifactList.table.columns.downloads').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(downloadsSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'downloadsCount',
|
||||||
|
sort_order: 'ASC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastUpdatedSortIcon = getByText('artifactList.table.columns.latestVersion').nextSibling
|
||||||
|
?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(lastUpdatedSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'latestVersion',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Pagination should work', async () => {
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const nextPageBtn = getByText('Next')
|
||||||
|
await userEvent.click(nextPageBtn)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 1,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageSizeSelect = getByTestId('dropdown-button')
|
||||||
|
await userEvent.click(pageSizeSelect)
|
||||||
|
const pageSize20option = getByText('20')
|
||||||
|
await userEvent.click(pageSize20option)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 20,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should list actions', async () => {
|
||||||
|
render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const getFirstRowColumn = (col: number) => getTableColumn(1, col) as HTMLElement
|
||||||
|
|
||||||
|
// click on 3 dots action btn
|
||||||
|
const actionBtn = getFirstRowColumn(5).querySelector('span[data-icon=Options')
|
||||||
|
await userEvent.click(actionBtn as HTMLElement)
|
||||||
|
|
||||||
|
const popovers = document.getElementsByClassName('bp3-popover')
|
||||||
|
await waitFor(() => expect(popovers).toHaveLength(1))
|
||||||
|
const selectPopover = popovers[0] as HTMLElement
|
||||||
|
const deleteItem = globalGetByText(selectPopover, 'artifactList.table.actions.deleteArtifact')
|
||||||
|
|
||||||
|
// click on delete action item
|
||||||
|
await userEvent.click(deleteItem)
|
||||||
|
const dialogs = document.getElementsByClassName('bp3-dialog')
|
||||||
|
await waitFor(() => expect(dialogs).toHaveLength(1))
|
||||||
|
const deleteDialog = dialogs[0] as HTMLElement
|
||||||
|
expect(globalGetByText(deleteDialog, 'artifactDetails.deleteArtifactModal.title')).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(deleteDialog, 'artifactDetails.deleteArtifactModal.contentText')).toBeInTheDocument()
|
||||||
|
|
||||||
|
const deleteBtn = deleteDialog.querySelector('button[aria-label=delete]')
|
||||||
|
const cancelBtn = deleteDialog.querySelector('button[aria-label=cancel]')
|
||||||
|
expect(deleteBtn).toBeInTheDocument()
|
||||||
|
expect(cancelBtn).toBeInTheDocument()
|
||||||
|
|
||||||
|
// click on delete button on modal
|
||||||
|
await userEvent.click(deleteBtn!)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(deleteArtifact).toHaveBeenCalledWith({
|
||||||
|
artifact: 'artifact/+',
|
||||||
|
registry_ref: 'undefined/generic-repo/+'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -23,10 +23,12 @@ import { VersionListColumnEnum } from '@ar/pages/version-list/components/Version
|
||||||
import VersionListTable, {
|
import VersionListTable, {
|
||||||
CommonVersionListTableProps
|
CommonVersionListTableProps
|
||||||
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
|
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
|
||||||
|
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
|
||||||
import {
|
import {
|
||||||
VersionDetailsHeaderProps,
|
type ArtifactActionProps,
|
||||||
VersionDetailsTabProps,
|
type VersionDetailsHeaderProps,
|
||||||
VersionListTableProps,
|
type VersionDetailsTabProps,
|
||||||
|
type VersionListTableProps,
|
||||||
VersionStep
|
VersionStep
|
||||||
} from '@ar/frameworks/Version/Version'
|
} from '@ar/frameworks/Version/Version'
|
||||||
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
|
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
|
||||||
|
@ -70,4 +72,8 @@ export class HelmVersionType extends VersionStep<ArtifactVersionSummary> {
|
||||||
return <String stringID="tabNotFound" />
|
return <String stringID="tabNotFound" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
|
||||||
|
return <ArtifactActions {...props} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* 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 userEvent from '@testing-library/user-event'
|
||||||
|
import { useGetAllArtifactsByRegistryQuery as _useGetAllArtifactsByRegistryQuery } from '@harnessio/react-har-service-client'
|
||||||
|
import { fireEvent, getByText as globalGetByText, render, waitFor } from '@testing-library/react'
|
||||||
|
|
||||||
|
import '@ar/pages/version-details/VersionFactory'
|
||||||
|
import '@ar/pages/repository-details/RepositoryFactory'
|
||||||
|
|
||||||
|
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
|
||||||
|
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
|
||||||
|
import RepositoryDetailsPage from '@ar/pages/repository-details/RepositoryDetailsPage'
|
||||||
|
import {
|
||||||
|
MockGetHelmArtifactsByRegistryResponse,
|
||||||
|
MockGetHelmRegistryResponseWithAllData
|
||||||
|
} from '@ar/pages/repository-details/HelmRepository/__tests__/__mockData__'
|
||||||
|
import { getTableColumn } from '@ar/utils/testUtils/utils'
|
||||||
|
|
||||||
|
const useGetAllArtifactsByRegistryQuery = _useGetAllArtifactsByRegistryQuery as jest.Mock
|
||||||
|
|
||||||
|
const deleteArtifact = jest.fn().mockImplementation(() => Promise.resolve({}))
|
||||||
|
|
||||||
|
jest.mock('@harnessio/react-har-service-client', () => ({
|
||||||
|
useGetAllArtifactsByRegistryQuery: jest.fn(),
|
||||||
|
useGetRegistryQuery: jest.fn().mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
error: false,
|
||||||
|
data: MockGetHelmRegistryResponseWithAllData
|
||||||
|
})),
|
||||||
|
useDeleteArtifactMutation: jest.fn().mockImplementation(() => ({
|
||||||
|
isLoading: false,
|
||||||
|
mutateAsync: deleteArtifact
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('Test Registry Artifact List Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: MockGetHelmArtifactsByRegistryResponse,
|
||||||
|
error: false,
|
||||||
|
refetch: jest.fn()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should render empty list if artifacts response is empty', () => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: { content: { data: { artifacts: [] } } },
|
||||||
|
error: false,
|
||||||
|
refetch: []
|
||||||
|
}))
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
const noResultsText = getByText('artifactList.table.noArtifactsTitle')
|
||||||
|
expect(noResultsText).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should render artifacts list', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
const table = container.querySelector('[class*="TableV2--table"]')
|
||||||
|
expect(table).toBeInTheDocument()
|
||||||
|
|
||||||
|
const tableRows = container.querySelectorAll('[class*="TableV2--row"]')
|
||||||
|
expect(tableRows).toHaveLength(1)
|
||||||
|
|
||||||
|
const tableData = MockGetHelmArtifactsByRegistryResponse.content.data.artifacts
|
||||||
|
|
||||||
|
const getFirstRowColumn = (col: number) => getTableColumn(1, col) as HTMLElement
|
||||||
|
expect(globalGetByText(getFirstRowColumn(1), tableData[0].name)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(2), tableData[0].registryIdentifier)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(3), tableData[0].downloadsCount?.toString() as string)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(4), tableData[0].latestVersion)).toBeInTheDocument()
|
||||||
|
|
||||||
|
const actionBtn = getFirstRowColumn(5).querySelector('span[data-icon=Options')
|
||||||
|
await userEvent.click(actionBtn as HTMLElement)
|
||||||
|
const dialogs = document.getElementsByClassName('bp3-popover')
|
||||||
|
await waitFor(() => expect(dialogs).toHaveLength(1))
|
||||||
|
const selectPopover = dialogs[0] as HTMLElement
|
||||||
|
|
||||||
|
const items = selectPopover.getElementsByClassName('bp3-menu-item')
|
||||||
|
expect(items).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(items[0]).toHaveTextContent('artifactList.table.actions.deleteArtifact')
|
||||||
|
expect(items[1]).toHaveTextContent('actions.setupClient')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should show error message if listing api fails', async () => {
|
||||||
|
const mockRefetchFn = jest.fn().mockImplementation()
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
message: 'error message'
|
||||||
|
},
|
||||||
|
refetch: mockRefetchFn
|
||||||
|
}))
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const errorText = getByText('error message')
|
||||||
|
expect(errorText).toBeInTheDocument()
|
||||||
|
|
||||||
|
const retryBtn = getByText('Retry')
|
||||||
|
expect(retryBtn).toBeInTheDocument()
|
||||||
|
await userEvent.click(retryBtn)
|
||||||
|
expect(mockRefetchFn).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should be able to search', async () => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: { content: { data: { artifacts: [] } } },
|
||||||
|
error: false,
|
||||||
|
refetch: []
|
||||||
|
}))
|
||||||
|
const { getByText, getByPlaceholderText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchInput = getByPlaceholderText('search')
|
||||||
|
expect(searchInput).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.change(searchInput, { target: { value: 'pod' } })
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
search_term: 'pod',
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearAllFiltersBtn = getByText('clearFilters')
|
||||||
|
await userEvent.click(clearAllFiltersBtn)
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Sorting should work', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const artifactNameSortIcon = getByText('artifactList.table.columns.name').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(artifactNameSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'name',
|
||||||
|
sort_order: 'ASC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const repositorySortIcon = getByText('artifactList.table.columns.repository').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(repositorySortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'registryIdentifier',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadsSortIcon = getByText('artifactList.table.columns.downloads').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(downloadsSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'downloadsCount',
|
||||||
|
sort_order: 'ASC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastUpdatedSortIcon = getByText('artifactList.table.columns.latestVersion').nextSibling
|
||||||
|
?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(lastUpdatedSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'latestVersion',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Pagination should work', async () => {
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const nextPageBtn = getByText('Next')
|
||||||
|
await userEvent.click(nextPageBtn)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 1,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageSizeSelect = getByTestId('dropdown-button')
|
||||||
|
await userEvent.click(pageSizeSelect)
|
||||||
|
const pageSize20option = getByText('20')
|
||||||
|
await userEvent.click(pageSize20option)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 20,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should list actions', async () => {
|
||||||
|
render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const getFirstRowColumn = (col: number) => getTableColumn(1, col) as HTMLElement
|
||||||
|
|
||||||
|
// click on 3 dots action btn
|
||||||
|
const actionBtn = getFirstRowColumn(5).querySelector('span[data-icon=Options')
|
||||||
|
await userEvent.click(actionBtn as HTMLElement)
|
||||||
|
|
||||||
|
const popovers = document.getElementsByClassName('bp3-popover')
|
||||||
|
await waitFor(() => expect(popovers).toHaveLength(1))
|
||||||
|
const selectPopover = popovers[0] as HTMLElement
|
||||||
|
const deleteItem = globalGetByText(selectPopover, 'artifactList.table.actions.deleteArtifact')
|
||||||
|
|
||||||
|
// click on delete action item
|
||||||
|
await userEvent.click(deleteItem)
|
||||||
|
const dialogs = document.getElementsByClassName('bp3-dialog')
|
||||||
|
await waitFor(() => expect(dialogs).toHaveLength(1))
|
||||||
|
const deleteDialog = dialogs[0] as HTMLElement
|
||||||
|
expect(globalGetByText(deleteDialog, 'artifactDetails.deleteArtifactModal.title')).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(deleteDialog, 'artifactDetails.deleteArtifactModal.contentText')).toBeInTheDocument()
|
||||||
|
|
||||||
|
const deleteBtn = deleteDialog.querySelector('button[aria-label=delete]')
|
||||||
|
const cancelBtn = deleteDialog.querySelector('button[aria-label=cancel]')
|
||||||
|
expect(deleteBtn).toBeInTheDocument()
|
||||||
|
expect(cancelBtn).toBeInTheDocument()
|
||||||
|
|
||||||
|
// click on delete button on modal
|
||||||
|
await userEvent.click(deleteBtn!)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(deleteArtifact).toHaveBeenCalledWith({
|
||||||
|
artifact: 'podinfo-artifact/+',
|
||||||
|
registry_ref: 'undefined/helm-repo/+'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -20,12 +20,14 @@ import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client
|
||||||
import { String } from '@ar/frameworks/strings'
|
import { String } from '@ar/frameworks/strings'
|
||||||
import { RepositoryPackageType } from '@ar/common/types'
|
import { RepositoryPackageType } from '@ar/common/types'
|
||||||
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
|
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
|
||||||
|
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
|
||||||
import VersionListTable, {
|
import VersionListTable, {
|
||||||
type CommonVersionListTableProps
|
type CommonVersionListTableProps
|
||||||
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
|
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
|
||||||
import {
|
import {
|
||||||
|
type ArtifactActionProps,
|
||||||
type VersionDetailsHeaderProps,
|
type VersionDetailsHeaderProps,
|
||||||
VersionDetailsTabProps,
|
type VersionDetailsTabProps,
|
||||||
type VersionListTableProps,
|
type VersionListTableProps,
|
||||||
VersionStep
|
VersionStep
|
||||||
} from '@ar/frameworks/Version/Version'
|
} from '@ar/frameworks/Version/Version'
|
||||||
|
@ -71,4 +73,8 @@ export class MavenVersionType extends VersionStep<ArtifactVersionSummary> {
|
||||||
return <String stringID="tabNotFound" />
|
return <String stringID="tabNotFound" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
|
||||||
|
return <ArtifactActions {...props} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* 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 userEvent from '@testing-library/user-event'
|
||||||
|
import { useGetAllArtifactsByRegistryQuery as _useGetAllArtifactsByRegistryQuery } from '@harnessio/react-har-service-client'
|
||||||
|
import { fireEvent, getByText as globalGetByText, render, waitFor } from '@testing-library/react'
|
||||||
|
|
||||||
|
import '@ar/pages/version-details/VersionFactory'
|
||||||
|
import '@ar/pages/repository-details/RepositoryFactory'
|
||||||
|
|
||||||
|
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
|
||||||
|
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
|
||||||
|
import RepositoryDetailsPage from '@ar/pages/repository-details/RepositoryDetailsPage'
|
||||||
|
import {
|
||||||
|
MockGetMavenArtifactsByRegistryResponse,
|
||||||
|
MockGetMavenRegistryResponseWithAllData
|
||||||
|
} from '@ar/pages/repository-details/MavenRepository/__tests__/__mockData__'
|
||||||
|
import { getTableColumn } from '@ar/utils/testUtils/utils'
|
||||||
|
|
||||||
|
const useGetAllArtifactsByRegistryQuery = _useGetAllArtifactsByRegistryQuery as jest.Mock
|
||||||
|
|
||||||
|
const deleteArtifact = jest.fn().mockImplementation(() => Promise.resolve({ content: { status: 'SUCCESS' } }))
|
||||||
|
|
||||||
|
jest.mock('@harnessio/react-har-service-client', () => ({
|
||||||
|
useGetAllArtifactsByRegistryQuery: jest.fn(),
|
||||||
|
useGetRegistryQuery: jest.fn().mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
error: false,
|
||||||
|
data: MockGetMavenRegistryResponseWithAllData
|
||||||
|
})),
|
||||||
|
useDeleteArtifactMutation: jest.fn().mockImplementation(() => ({
|
||||||
|
isLoading: false,
|
||||||
|
mutateAsync: deleteArtifact
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('Test Registry Artifact List Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: MockGetMavenArtifactsByRegistryResponse,
|
||||||
|
error: false,
|
||||||
|
refetch: jest.fn()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should render empty list if artifacts response is empty', () => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: { content: { data: { artifacts: [] } } },
|
||||||
|
error: false,
|
||||||
|
refetch: []
|
||||||
|
}))
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
const noResultsText = getByText('artifactList.table.noArtifactsTitle')
|
||||||
|
expect(noResultsText).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should render artifacts list', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
const table = container.querySelector('[class*="TableV2--table"]')
|
||||||
|
expect(table).toBeInTheDocument()
|
||||||
|
|
||||||
|
const tableRows = container.querySelectorAll('[class*="TableV2--row"]')
|
||||||
|
expect(tableRows).toHaveLength(1)
|
||||||
|
|
||||||
|
const tableData = MockGetMavenArtifactsByRegistryResponse.content.data.artifacts
|
||||||
|
|
||||||
|
const getFirstRowColumn = (col: number) => getTableColumn(1, col) as HTMLElement
|
||||||
|
expect(globalGetByText(getFirstRowColumn(1), tableData[0].name)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(2), tableData[0].registryIdentifier)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(3), tableData[0].downloadsCount?.toString() as string)).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(getFirstRowColumn(4), tableData[0].latestVersion)).toBeInTheDocument()
|
||||||
|
|
||||||
|
const actionBtn = getFirstRowColumn(5).querySelector('span[data-icon=Options')
|
||||||
|
await userEvent.click(actionBtn as HTMLElement)
|
||||||
|
const dialogs = document.getElementsByClassName('bp3-popover')
|
||||||
|
await waitFor(() => expect(dialogs).toHaveLength(1))
|
||||||
|
const selectPopover = dialogs[0] as HTMLElement
|
||||||
|
|
||||||
|
const items = selectPopover.getElementsByClassName('bp3-menu-item')
|
||||||
|
expect(items).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(items[0]).toHaveTextContent('artifactList.table.actions.deleteArtifact')
|
||||||
|
expect(items[1]).toHaveTextContent('actions.setupClient')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should show error message if listing api fails', async () => {
|
||||||
|
const mockRefetchFn = jest.fn().mockImplementation()
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
message: 'error message'
|
||||||
|
},
|
||||||
|
refetch: mockRefetchFn
|
||||||
|
}))
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const errorText = getByText('error message')
|
||||||
|
expect(errorText).toBeInTheDocument()
|
||||||
|
|
||||||
|
const retryBtn = getByText('Retry')
|
||||||
|
expect(retryBtn).toBeInTheDocument()
|
||||||
|
await userEvent.click(retryBtn)
|
||||||
|
expect(mockRefetchFn).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should be able to search', async () => {
|
||||||
|
useGetAllArtifactsByRegistryQuery.mockImplementation(() => ({
|
||||||
|
isFetching: false,
|
||||||
|
data: { content: { data: { artifacts: [] } } },
|
||||||
|
error: false,
|
||||||
|
refetch: []
|
||||||
|
}))
|
||||||
|
const { getByText, getByPlaceholderText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchInput = getByPlaceholderText('search')
|
||||||
|
expect(searchInput).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.change(searchInput, { target: { value: 'pod' } })
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
search_term: 'pod',
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearAllFiltersBtn = getByText('clearFilters')
|
||||||
|
await userEvent.click(clearAllFiltersBtn)
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Sorting should work', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const artifactNameSortIcon = getByText('artifactList.table.columns.name').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(artifactNameSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'name',
|
||||||
|
sort_order: 'ASC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const repositorySortIcon = getByText('artifactList.table.columns.repository').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(repositorySortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'registryIdentifier',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadsSortIcon = getByText('artifactList.table.columns.downloads').nextSibling?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(downloadsSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'downloadsCount',
|
||||||
|
sort_order: 'ASC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastUpdatedSortIcon = getByText('artifactList.table.columns.latestVersion').nextSibling
|
||||||
|
?.firstChild as HTMLElement
|
||||||
|
await userEvent.click(lastUpdatedSortIcon)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'latestVersion',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Pagination should work', async () => {
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const nextPageBtn = getByText('Next')
|
||||||
|
await userEvent.click(nextPageBtn)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 1,
|
||||||
|
size: 50,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageSizeSelect = getByTestId('dropdown-button')
|
||||||
|
await userEvent.click(pageSizeSelect)
|
||||||
|
const pageSize20option = getByText('20')
|
||||||
|
await userEvent.click(pageSize20option)
|
||||||
|
|
||||||
|
expect(useGetAllArtifactsByRegistryQuery).toHaveBeenLastCalledWith({
|
||||||
|
registry_ref: 'undefined/abcd/+',
|
||||||
|
queryParams: {
|
||||||
|
page: 0,
|
||||||
|
size: 20,
|
||||||
|
sort_field: 'updatedAt',
|
||||||
|
sort_order: 'DESC'
|
||||||
|
},
|
||||||
|
stringifyQueryParamsOptions: { arrayFormat: 'repeat' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should list actions', async () => {
|
||||||
|
render(
|
||||||
|
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: RepositoryDetailsTab.PACKAGES }}>
|
||||||
|
<RepositoryDetailsPage />
|
||||||
|
</ArTestWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
|
const getFirstRowColumn = (col: number) => getTableColumn(1, col) as HTMLElement
|
||||||
|
|
||||||
|
// click on 3 dots action btn
|
||||||
|
const actionBtn = getFirstRowColumn(5).querySelector('span[data-icon=Options')
|
||||||
|
await userEvent.click(actionBtn as HTMLElement)
|
||||||
|
|
||||||
|
const popovers = document.getElementsByClassName('bp3-popover')
|
||||||
|
await waitFor(() => expect(popovers).toHaveLength(1))
|
||||||
|
const selectPopover = popovers[0] as HTMLElement
|
||||||
|
const deleteItem = globalGetByText(selectPopover, 'artifactList.table.actions.deleteArtifact')
|
||||||
|
|
||||||
|
// click on delete action item
|
||||||
|
await userEvent.click(deleteItem)
|
||||||
|
const dialogs = document.getElementsByClassName('bp3-dialog')
|
||||||
|
await waitFor(() => expect(dialogs).toHaveLength(1))
|
||||||
|
const deleteDialog = dialogs[0] as HTMLElement
|
||||||
|
expect(globalGetByText(deleteDialog, 'artifactDetails.deleteArtifactModal.title')).toBeInTheDocument()
|
||||||
|
expect(globalGetByText(deleteDialog, 'artifactDetails.deleteArtifactModal.contentText')).toBeInTheDocument()
|
||||||
|
|
||||||
|
const deleteBtn = deleteDialog.querySelector('button[aria-label=delete]')
|
||||||
|
const cancelBtn = deleteDialog.querySelector('button[aria-label=cancel]')
|
||||||
|
expect(deleteBtn).toBeInTheDocument()
|
||||||
|
expect(cancelBtn).toBeInTheDocument()
|
||||||
|
|
||||||
|
// click on delete button on modal
|
||||||
|
await userEvent.click(deleteBtn!)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(deleteArtifact).toHaveBeenCalledWith({
|
||||||
|
artifact: 'artifact/+',
|
||||||
|
registry_ref: 'undefined/maven-repo/+'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -21,12 +21,14 @@ import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client
|
||||||
import { String } from '@ar/frameworks/strings'
|
import { String } from '@ar/frameworks/strings'
|
||||||
import { RepositoryPackageType } from '@ar/common/types'
|
import { RepositoryPackageType } from '@ar/common/types'
|
||||||
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
|
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
|
||||||
|
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
|
||||||
import VersionListTable, {
|
import VersionListTable, {
|
||||||
type CommonVersionListTableProps
|
type CommonVersionListTableProps
|
||||||
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
|
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
|
||||||
import {
|
import {
|
||||||
|
type ArtifactActionProps,
|
||||||
type VersionDetailsHeaderProps,
|
type VersionDetailsHeaderProps,
|
||||||
VersionDetailsTabProps,
|
type VersionDetailsTabProps,
|
||||||
type VersionListTableProps,
|
type VersionListTableProps,
|
||||||
VersionStep
|
VersionStep
|
||||||
} from '@ar/frameworks/Version/Version'
|
} from '@ar/frameworks/Version/Version'
|
||||||
|
@ -77,4 +79,8 @@ export class NpmVersionType extends VersionStep<ArtifactVersionSummary> {
|
||||||
return <String stringID="tabNotFound" />
|
return <String stringID="tabNotFound" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
|
||||||
|
return <ArtifactActions {...props} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,10 @@
|
||||||
* Use the command `yarn strings` to regenerate this file.
|
* Use the command `yarn strings` to regenerate this file.
|
||||||
*/
|
*/
|
||||||
export interface StringsMap {
|
export interface StringsMap {
|
||||||
|
'artifactDetails.artifactDeleted': string
|
||||||
'artifactDetails.artifactLabelInputPlaceholder': string
|
'artifactDetails.artifactLabelInputPlaceholder': string
|
||||||
|
'artifactDetails.deleteArtifactModal.contentText': string
|
||||||
|
'artifactDetails.deleteArtifactModal.title': string
|
||||||
'artifactDetails.downloadsThisWeek': string
|
'artifactDetails.downloadsThisWeek': string
|
||||||
'artifactDetails.labelsUpdated': string
|
'artifactDetails.labelsUpdated': string
|
||||||
'artifactDetails.page': string
|
'artifactDetails.page': string
|
||||||
|
@ -23,6 +26,7 @@ export interface StringsMap {
|
||||||
'artifactList.table.actions.VulnerabilityStatus.partiallyScanned': string
|
'artifactList.table.actions.VulnerabilityStatus.partiallyScanned': string
|
||||||
'artifactList.table.actions.VulnerabilityStatus.scanStatus': string
|
'artifactList.table.actions.VulnerabilityStatus.scanStatus': string
|
||||||
'artifactList.table.actions.VulnerabilityStatus.scanned': string
|
'artifactList.table.actions.VulnerabilityStatus.scanned': string
|
||||||
|
'artifactList.table.actions.deleteArtifact': string
|
||||||
'artifactList.table.actions.deleteRepository': string
|
'artifactList.table.actions.deleteRepository': string
|
||||||
'artifactList.table.actions.editRepository': string
|
'artifactList.table.actions.editRepository': string
|
||||||
'artifactList.table.allRepositories': string
|
'artifactList.table.allRepositories': string
|
||||||
|
|
Loading…
Reference in New Issue