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 page
try-new-ui
Shivanand Sonnad 2025-03-04 12:07:03 +00:00 committed by Harness
parent efcefd3c10
commit 285cb1b9ba
31 changed files with 1574 additions and 161 deletions

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryPackageType } from '@ar/common/types'
import versionFactory from './VersionFactory'
import type { 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 })
}

View File

@ -15,9 +15,13 @@
*/
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 { Parent, RepositoryPackageType } from '@ar/common/types'
import type { PageType, Parent, RepositoryPackageType } from '@ar/common/types'
export interface VersionDetailsHeaderProps<T> {
data: T
@ -39,6 +43,15 @@ export interface VersionListTableProps {
parent: Parent
}
export interface ArtifactActionProps {
data: RegistryArtifactMetadata | ArtifactSummary
pageType: PageType
repoKey: string
artifactKey: string
readonly?: boolean
onClose?: () => void
}
export abstract class VersionStep<T> {
protected abstract packageType: RepositoryPackageType
protected abstract allowedVersionDetailsTabs: VersionDetailsTab[]
@ -56,4 +69,6 @@ export abstract class VersionStep<T> {
abstract renderVersionDetailsHeader(props: VersionDetailsHeaderProps<T>): JSX.Element
abstract renderVersionDetailsTab(props: VersionDetailsTabProps): JSX.Element
abstract renderArtifactActions(props: ArtifactActionProps): JSX.Element
}

View File

@ -38,7 +38,7 @@ export default function getARRouteDefinitions(routeParams: Record<string, string
toARRepositoryDetails: params => `/${params?.repositoryIdentifier}`,
toARRepositoryDetailsTab: params => `/${params?.repositoryIdentifier}/${params?.tab}`,
toARRepositoryWebhookDetails: params => `/${params?.repositoryIdentifier}/webhooks/${params?.webhookIdentifier}`,
toARArtifacts: () => `/${routeParams?.repositoryIdentifier}?tab=packages`,
toARArtifacts: () => `/${routeParams?.repositoryIdentifier}/packages`,
toARArtifactDetails: params => `/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}`,
toARVersionDetails: params =>
`/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}`,

View File

@ -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;
}

View File

@ -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

View File

@ -15,45 +15,46 @@
*/
import React, { useState } from 'react'
import { Menu, Position } from '@blueprintjs/core'
import { Button, ButtonVariation } from '@harnessio/uicore'
import DeleteRepositoryMenuItem from './DeleteRepository'
import EditRepositoryMenuItem from './EditRepository'
import SetupClientMenuItem from './SetupClient'
import { PageType } from '@ar/common/types'
import ActionButton from '@ar/components/ActionButton/ActionButton'
import SetupClientMenuItem from './SetupClientMenuItem'
import type { ArtifactActionProps } from './types'
import DeleteArtifactMenuItem from './DeleteArtifactMenuItem'
import css from './ArtifactActions.module.scss'
export default function ArtifactActions({ data, repoKey }: ArtifactActionProps): JSX.Element {
const [menuOpen, setMenuOpen] = useState(false)
export default function ArtifactActions({
data,
repoKey,
artifactKey,
pageType,
readonly,
onClose
}: ArtifactActionProps): JSX.Element {
const [open, setOpen] = useState(false)
return (
<Button
variation={ButtonVariation.ICON}
icon="Options"
tooltip={
<Menu
className={css.optionsMenu}
onClick={e => {
e.stopPropagation()
}}>
<DeleteRepositoryMenuItem data={data} repoKey={repoKey} />
<EditRepositoryMenuItem data={data} repoKey={repoKey} />
<SetupClientMenuItem data={data} repoKey={repoKey} />
</Menu>
}
tooltipProps={{
interactionKind: 'click',
onInteraction: nextOpenState => {
setMenuOpen(nextOpenState)
},
isOpen: menuOpen,
position: Position.BOTTOM
}}
onClick={e => {
e.stopPropagation()
setMenuOpen(true)
}}
/>
<ActionButton isOpen={open} setOpen={setOpen}>
<DeleteArtifactMenuItem
artifactKey={artifactKey}
repoKey={repoKey}
data={data}
pageType={pageType}
readonly={readonly}
onClose={() => {
setOpen(false)
onClose?.()
}}
/>
{pageType === PageType.Table && (
<SetupClientMenuItem
data={data}
pageType={pageType}
readonly={readonly}
onClose={() => setOpen(false)}
artifactKey={artifactKey}
repoKey={repoKey}
/>
)}
</ActionButton>
)
}

View File

@ -18,23 +18,34 @@ import React from 'react'
import { useHistory } from 'react-router-dom'
import { useStrings } from '@ar/frameworks/strings'
import { queryClient } from '@ar/utils/queryClient'
import { useParentComponents, useRoutes } from '@ar/hooks'
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
import { PermissionIdentifier, ResourceType } from '@ar/common/permissionTypes'
import useDeleteRepositoryModal from '@ar/pages/repository-details/hooks/useDeleteRepositoryModal/useDeleteRepositoryModal'
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 { RbacMenuItem } = useParentComponents()
const history = useHistory()
const routes = useRoutes()
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,
onSuccess: handleAfterDeleteRepository
})
@ -46,14 +57,15 @@ export default function DeleteRepositoryMenuItem({ repoKey }: ArtifactActionProp
return (
<RbacMenuItem
icon="code-delete"
text={getString('artifactList.table.actions.deleteRepository')}
text={getString('artifactList.table.actions.deleteArtifact')}
onClick={handleDeleteService}
disabled={readonly}
permission={{
resource: {
resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: repoKey
resourceIdentifier: artifactKey
},
permission: PermissionIdentifier.DELETE_ARTIFACT_REGISTRY
permission: PermissionIdentifier.DELETE_ARTIFACT
}}
/>
)

View File

@ -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
}}
/>
</>
)
}

View File

@ -25,10 +25,10 @@ import { useSetupClientModal } from '@ar/pages/repository-details/hooks/useSetup
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 { RbacMenuItem } = useParentComponents()
const artifactKey = data.imageName || ''
const [showSetupClientModal] = useSetupClientModal({
repoKey,
@ -41,6 +41,7 @@ export default function SetupClientMenuItem({ data, repoKey }: ArtifactActionPro
icon="setup-client"
text={getString('actions.setupClient')}
onClick={showSetupClientModal}
disabled={readonly}
permission={{
resource: {
resourceType: ResourceType.ARTIFACT_REGISTRY,

View File

@ -14,9 +14,14 @@
* 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 {
data: ArtifactSummary
data: ArtifactSummary | RegistryArtifactMetadata
artifactKey: string
repoKey: string
pageType: PageType
readonly?: boolean
onClose?: () => void
}

View File

@ -21,11 +21,12 @@ import type { ArtifactSummary } from '@harnessio/react-har-service-client'
import { useDecodedParams } from '@ar/hooks'
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 WeeklyDownloads from '@ar/components/PageTitle/WeeklyDownloads'
import CreatedAndModifiedAt from '@ar/components/PageTitle/CreatedAndModifiedAt'
import NameAndDescription from '@ar/components/PageTitle/NameAndDescription'
import ArtifactActionsWidget from '@ar/frameworks/Version/ArtifactActionsWidget'
import SetupClientButton from '@ar/components/SetupClientButton/SetupClientButton'
import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon'
@ -69,6 +70,13 @@ function ArtifactDetailsHeaderContent(props: ArtifactDetailsHeaderContentProps):
artifactIdentifier={artifactIdentifier}
packageType={packageType as RepositoryPackageType}
/>
<ArtifactActionsWidget
packageType={packageType as RepositoryPackageType}
data={data as ArtifactSummary}
repoKey={repositoryIdentifier}
artifactKey={artifactIdentifier}
pageType={PageType.Details}
/>
</Layout.Horizontal>
</Layout.Vertical>
</Layout.Horizontal>

View File

@ -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 }
}

View File

@ -3,3 +3,7 @@ downloadsThisWeek: Downloads This Week
artifactLabelInputPlaceholder: Type input and hit enter
labelsUpdated: Artifact labels updated successfully!
totalDownloads: Total Downloads
artifactDeleted: Artifact deleted successfully!
deleteArtifactModal:
title: Delete Artifact
contentText: Are you sure you want to delete the artifact?

View File

@ -34,7 +34,7 @@
div[class*='TableV2--cells'],
div[class*='TableV2--header'] {
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;
}
}

View File

@ -23,6 +23,7 @@ import type { ListRegistryArtifact, RegistryArtifactMetadata } from '@harnessio/
import { useStrings } from '@ar/frameworks/strings'
import { useParentHooks } from '@ar/hooks'
import {
RegistryArtifactActionsCell,
RegistryArtifactDownloadsCell,
RegistryArtifactLatestUpdatedCell,
RegistryArtifactNameCell,
@ -95,6 +96,12 @@ export default function RegistryArtifactListTable(props: RegistryArtifactListTab
accessor: 'latestVersion',
Cell: RegistryArtifactLatestUpdatedCell,
serverSortProps: getServerSortProps('latestVersion')
},
{
Header: '',
accessor: 'actions',
Cell: RegistryArtifactActionsCell,
disableSortBy: true
}
].filter(Boolean) as unknown as Column<RegistryArtifactMetadata>[]
}, [currentOrder, currentSort, getString, onClickLabel])

View File

@ -28,8 +28,9 @@ import { useStrings } from '@ar/frameworks/strings'
import TableCells from '@ar/components/TableCells/TableCells'
import LabelsPopover from '@ar/components/LabelsPopover/LabelsPopover'
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 ArtifactActionsWidget from '@ar/frameworks/Version/ArtifactActionsWidget'
import { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
type CellTypeWithActions<D extends Record<string, any>, V = any> = TableInstance<D> & {
@ -143,3 +144,16 @@ export const RegistryArtifactLatestUpdatedCell: CellType = ({ row }) => {
</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}
/>
)
}

View File

@ -21,6 +21,7 @@ table:
actions:
editRepository: Edit Registry
deleteRepository: Delete Registry
deleteArtifact: Delete Artifact
VulnerabilityStatus:
scanned: Scanned
nonScanned: Not Scanned

View File

@ -52,7 +52,7 @@ export const MockGetArtifactsByRegistryResponse: GetAllArtifactsByRegistryOkResp
}
],
itemCount: 1,
pageCount: 1,
pageCount: 3,
pageIndex: 0,
pageSize: 50
},

View File

@ -52,7 +52,7 @@ export const MockGetGenericArtifactsByRegistryResponse: GetAllArtifactsByRegistr
}
],
itemCount: 0,
pageCount: 0,
pageCount: 2,
pageIndex: 0,
pageSize: 50
},

View File

@ -52,7 +52,7 @@ export const MockGetHelmArtifactsByRegistryResponse: GetAllArtifactsByRegistryOk
}
],
itemCount: 1,
pageCount: 1,
pageCount: 2,
pageIndex: 0,
pageSize: 50
},

View File

@ -52,7 +52,7 @@ export const MockGetMavenArtifactsByRegistryResponse: GetAllArtifactsByRegistryO
}
],
itemCount: 0,
pageCount: 0,
pageCount: 2,
pageIndex: 0,
pageSize: 50
},

View File

@ -19,11 +19,13 @@ import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client
import { String } from '@ar/frameworks/strings'
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 {
VersionDetailsHeaderProps,
VersionDetailsTabProps,
VersionListTableProps,
type ArtifactActionProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
} from '@ar/frameworks/Version/Version'
@ -72,4 +74,8 @@ export class DockerVersionType extends VersionStep<ArtifactVersionSummary> {
return <String stringID="tabNotFound" />
}
}
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
return <ArtifactActions {...props} />
}
}

View File

@ -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/+'
})
})
})
})

View File

@ -17,6 +17,7 @@
import React from 'react'
import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client'
import {
type ArtifactActionProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
@ -28,6 +29,7 @@ import VersionListTable, {
type CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
import GenericOverviewPage from './pages/overview/OverviewPage'
import OSSContentPage from './pages/oss-details/OSSContentPage'
@ -69,4 +71,8 @@ export class GenericVersionType extends VersionStep<ArtifactVersionSummary> {
return <String stringID="tabNotFound" />
}
}
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
return <ArtifactActions {...props} />
}
}

View File

@ -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/+'
})
})
})
})

View File

@ -23,10 +23,12 @@ import { VersionListColumnEnum } from '@ar/pages/version-list/components/Version
import VersionListTable, {
CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import {
VersionDetailsHeaderProps,
VersionDetailsTabProps,
VersionListTableProps,
type ArtifactActionProps,
type VersionDetailsHeaderProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
} from '@ar/frameworks/Version/Version'
import { VersionDetailsTab } from '../components/VersionDetailsTabs/constants'
@ -70,4 +72,8 @@ export class HelmVersionType extends VersionStep<ArtifactVersionSummary> {
return <String stringID="tabNotFound" />
}
}
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
return <ArtifactActions {...props} />
}
}

View File

@ -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/+'
})
})
})
})

View File

@ -20,12 +20,14 @@ import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client
import { String } from '@ar/frameworks/strings'
import { RepositoryPackageType } from '@ar/common/types'
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import VersionListTable, {
type CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
import {
type ArtifactActionProps,
type VersionDetailsHeaderProps,
VersionDetailsTabProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
} from '@ar/frameworks/Version/Version'
@ -71,4 +73,8 @@ export class MavenVersionType extends VersionStep<ArtifactVersionSummary> {
return <String stringID="tabNotFound" />
}
}
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
return <ArtifactActions {...props} />
}
}

View File

@ -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/+'
})
})
})
})

View File

@ -21,12 +21,14 @@ import type { ArtifactVersionSummary } from '@harnessio/react-har-service-client
import { String } from '@ar/frameworks/strings'
import { RepositoryPackageType } from '@ar/common/types'
import { VersionListColumnEnum } from '@ar/pages/version-list/components/VersionListTable/types'
import ArtifactActions from '@ar/pages/artifact-details/components/ArtifactActions/ArtifactActions'
import VersionListTable, {
type CommonVersionListTableProps
} from '@ar/pages/version-list/components/VersionListTable/VersionListTable'
import {
type ArtifactActionProps,
type VersionDetailsHeaderProps,
VersionDetailsTabProps,
type VersionDetailsTabProps,
type VersionListTableProps,
VersionStep
} from '@ar/frameworks/Version/Version'
@ -77,4 +79,8 @@ export class NpmVersionType extends VersionStep<ArtifactVersionSummary> {
return <String stringID="tabNotFound" />
}
}
renderArtifactActions(props: ArtifactActionProps): JSX.Element {
return <ArtifactActions {...props} />
}
}

View File

@ -10,7 +10,10 @@
* Use the command `yarn strings` to regenerate this file.
*/
export interface StringsMap {
'artifactDetails.artifactDeleted': string
'artifactDetails.artifactLabelInputPlaceholder': string
'artifactDetails.deleteArtifactModal.contentText': string
'artifactDetails.deleteArtifactModal.title': string
'artifactDetails.downloadsThisWeek': string
'artifactDetails.labelsUpdated': string
'artifactDetails.page': string
@ -23,6 +26,7 @@ export interface StringsMap {
'artifactList.table.actions.VulnerabilityStatus.partiallyScanned': string
'artifactList.table.actions.VulnerabilityStatus.scanStatus': string
'artifactList.table.actions.VulnerabilityStatus.scanned': string
'artifactList.table.actions.deleteArtifact': string
'artifactList.table.actions.deleteRepository': string
'artifactList.table.actions.editRepository': string
'artifactList.table.allRepositories': string