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 { 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
|
||||
}
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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 { 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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}}
|
||||
/>
|
||||
)
|
|
@ -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'
|
||||
|
||||
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,
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
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?
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ table:
|
|||
actions:
|
||||
editRepository: Edit Registry
|
||||
deleteRepository: Delete Registry
|
||||
deleteArtifact: Delete Artifact
|
||||
VulnerabilityStatus:
|
||||
scanned: Scanned
|
||||
nonScanned: Not Scanned
|
||||
|
|
|
@ -52,7 +52,7 @@ export const MockGetArtifactsByRegistryResponse: GetAllArtifactsByRegistryOkResp
|
|||
}
|
||||
],
|
||||
itemCount: 1,
|
||||
pageCount: 1,
|
||||
pageCount: 3,
|
||||
pageIndex: 0,
|
||||
pageSize: 50
|
||||
},
|
||||
|
|
|
@ -52,7 +52,7 @@ export const MockGetGenericArtifactsByRegistryResponse: GetAllArtifactsByRegistr
|
|||
}
|
||||
],
|
||||
itemCount: 0,
|
||||
pageCount: 0,
|
||||
pageCount: 2,
|
||||
pageIndex: 0,
|
||||
pageSize: 50
|
||||
},
|
||||
|
|
|
@ -52,7 +52,7 @@ export const MockGetHelmArtifactsByRegistryResponse: GetAllArtifactsByRegistryOk
|
|||
}
|
||||
],
|
||||
itemCount: 1,
|
||||
pageCount: 1,
|
||||
pageCount: 2,
|
||||
pageIndex: 0,
|
||||
pageSize: 50
|
||||
},
|
||||
|
|
|
@ -52,7 +52,7 @@ export const MockGetMavenArtifactsByRegistryResponse: GetAllArtifactsByRegistryO
|
|||
}
|
||||
],
|
||||
itemCount: 0,
|
||||
pageCount: 0,
|
||||
pageCount: 2,
|
||||
pageIndex: 0,
|
||||
pageSize: 50
|
||||
},
|
||||
|
|
|
@ -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} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 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} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
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} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { 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} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { 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} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue