diff --git a/web/src/ar/pages/version-details/DockerVersion/DockerVersionType.tsx b/web/src/ar/pages/version-details/DockerVersion/DockerVersionType.tsx index ef14e0a11a..620f79e968 100644 --- a/web/src/ar/pages/version-details/DockerVersion/DockerVersionType.tsx +++ b/web/src/ar/pages/version-details/DockerVersion/DockerVersionType.tsx @@ -67,7 +67,8 @@ export class DockerVersionType extends VersionStep { VersionAction.DownloadCommand, VersionAction.ViewVersionDetails, VersionAction.Quarantine, - VersionAction.Download + VersionAction.Download, + VersionAction.AddTag ] protected allowedActionsOnVersionDetailsPage = [ diff --git a/web/src/ar/pages/version-details/HelmVersion/HelmVersionType.tsx b/web/src/ar/pages/version-details/HelmVersion/HelmVersionType.tsx index 24a32c1708..9def4ceac3 100644 --- a/web/src/ar/pages/version-details/HelmVersion/HelmVersionType.tsx +++ b/web/src/ar/pages/version-details/HelmVersion/HelmVersionType.tsx @@ -59,7 +59,8 @@ export class HelmVersionType extends VersionStep { VersionAction.DownloadCommand, VersionAction.ViewVersionDetails, VersionAction.Quarantine, - VersionAction.Download + VersionAction.Download, + VersionAction.AddTag ] protected allowedActionsOnVersionDetailsPage = [ diff --git a/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.module.scss b/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.module.scss new file mode 100644 index 0000000000..56a85fa474 --- /dev/null +++ b/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.module.scss @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Harness, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use it 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. + */ + +/* Fixed width for the tag input bar only when rendered inside Add Tag modal */ +.addTagInputWrapper { + :global(.bp3-input) { + width: min(400px, 90vw); + min-width: min(400px, 90vw); + max-width: 100%; + + box-sizing: border-box; + } +} diff --git a/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.tsx b/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.tsx new file mode 100644 index 0000000000..00b2883190 --- /dev/null +++ b/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.tsx @@ -0,0 +1,171 @@ +/* + * 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, { useRef } from 'react' +import { Formik } from 'formik' +import { Button, ButtonVariation, Layout, ModalDialog, useToaster } from '@harnessio/uicore' +import { useAddOciArtifactTagsMutation } from '@harnessio/react-har-service-client' + +import { useAppStore, useParentComponents } from '@ar/hooks' +import { useStrings } from '@ar/frameworks/strings' +import { queryClient } from '@ar/utils/queryClient' +import { ResourceType } from '@ar/common/permissionTypes' +import { PermissionIdentifier } from '@ar/common/permissionTypes' +import PatternInput from '@ar/components/Form/PatternInput/PatternInput' + +import type { VersionActionProps } from './types' +import css from './AddTagMenuItem.module.scss' + +function normalizeTagNames(value: string[] | (string | { label: string; value: string })[]): string[] { + if (!Array.isArray(value)) return [] + return value + .map(item => (typeof item === 'string' ? item : item?.value ?? '')) + .map(t => t.trim()) + .filter(Boolean) +} + +export interface AddTagModalContentProps { + artifactKey: string + repoKey: string + versionKey: string + hideModal: () => void + onClose?: () => void +} + +export function AddTagModalContent({ + artifactKey, + repoKey, + versionKey, + hideModal, + onClose +}: AddTagModalContentProps): JSX.Element { + const { scope } = useAppStore() + const { getString } = useStrings() + const { showSuccess, showError } = useToaster() + const { mutateAsync: addOciArtifactTags, isLoading: loading } = useAddOciArtifactTagsMutation() + const inputWrapperRef = useRef(null) + + const handleSubmit = async ( + tagNamesRaw: string[] | (string | { label: string; value: string })[], + currentInputValue?: string + ) => { + const committed = normalizeTagNames(tagNamesRaw) + const current = currentInputValue?.trim() ?? '' + const allTags = current ? [...committed, current] : committed + console.log('allTags', allTags); + const trimmed = normalizeTagNames(allTags) + if (trimmed.length === 0) { + showError(getString('validationMessages.entityRequired', { entity: 'Tag name' })) + return + } + try { + await addOciArtifactTags({ + queryParams: { + account_identifier: typeof scope?.accountId === 'string' ? scope.accountId : '', + org_identifier: typeof scope?.orgId === 'string' ? scope.orgId : undefined, + project_identifier: typeof scope?.projectId === 'string' ? scope.projectId : undefined, + registry_identifier: repoKey + }, + body: { + package: artifactKey, + version: versionKey, + tags: trimmed + } + }) + showSuccess(getString('versionList.messages.addTagSuccess')) + hideModal() + onClose?.() + queryClient.invalidateQueries(['GetAllHarnessArtifacts']) + queryClient.invalidateQueries(['ListVersions']) + } catch (e) { + showError((e as Error)?.message || getString('versionList.messages.addTagFailed')) + } + } + + const handleClose = () => { + hideModal() + onClose?.() + } + + return ( + undefined}> + {formik => ( + + + + + }> +
+ +
+
+ )} +
+ ) +} + +export interface AddTagMenuItemProps extends VersionActionProps { + openAddTagModal?: () => void +} + +export default function AddTagMenuItem(props: AddTagMenuItemProps): JSX.Element { + const { repoKey, readonly, onClose, openAddTagModal } = props + const { getString } = useStrings() + const { RbacMenuItem } = useParentComponents() + + const handleClick = () => { + openAddTagModal?.() + onClose?.() + } + + return ( + + ) +} diff --git a/web/src/ar/pages/version-details/components/VersionActions/VersionActions.tsx b/web/src/ar/pages/version-details/components/VersionActions/VersionActions.tsx index 16618cecf4..5980e8ea4f 100644 --- a/web/src/ar/pages/version-details/components/VersionActions/VersionActions.tsx +++ b/web/src/ar/pages/version-details/components/VersionActions/VersionActions.tsx @@ -18,7 +18,14 @@ import React, { useState } from 'react' import { get } from 'lodash-es' import { RepositoryConfigType } from '@ar/common/types' -import { useAppStore, useBulkDownloadFile, useAllowSoftDelete, useFeatureFlags, useRoutes } from '@ar/hooks' +import { + useAppStore, + useBulkDownloadFile, + useAllowSoftDelete, + useFeatureFlags, + useRoutes, + useParentHooks +} from '@ar/hooks' import { useStrings } from '@ar/frameworks/strings' import ActionButton from '@ar/components/ActionButton/ActionButton' import CopyMenuItem from '@ar/components/MenuItemTypes/CopyMenuItem' @@ -34,6 +41,7 @@ import RemoveQurantineMenuItem from './RemoveQurantineMenuItem' import DownloadVersionMenuItem from './DownloadVersionMenuItem' import SoftDeleteVersionMenuItem from './SoftDeleteVersionMenuItem' import ReEvaluateMenuItem from './ReEvaluateMenuItem' +import AddTagMenuItem, { AddTagModalContent } from './AddTagMenuItem' export default function VersionActions({ data, @@ -52,12 +60,31 @@ export default function VersionActions({ const routes = useRoutes() const { isCurrentSessionPublic } = useAppStore() const { getString } = useStrings() + const { useModalHook } = useParentHooks() const { HAR_DEPENDENCY_FIREWALL } = useFeatureFlags() const isBulkDownloadFileEnabled = useBulkDownloadFile() const allowSoftDelete = useAllowSoftDelete() const isFirewallEnabled = data.firewallMode ? data.firewallMode !== 'ALLOW' : false const allowReEvaluate = HAR_DEPENDENCY_FIREWALL && isFirewallEnabled && repoType === RepositoryConfigType.UPSTREAM + const closeMenu = () => { + setOpen(false) + onClose?.() + } + + const [showAddTagModal, hideAddTagModal] = useModalHook( + () => ( + + ), + [artifactKey, repoKey, versionKey] + ) + const isAllowed = (action: VersionAction): boolean => { if (!allowedActions) return true return allowedActions.includes(action) @@ -179,6 +206,18 @@ export default function VersionActions({ }} /> )} + {isAllowed(VersionAction.AddTag) && ( + + )} ) } diff --git a/web/src/ar/pages/version-details/components/VersionActions/types.ts b/web/src/ar/pages/version-details/components/VersionActions/types.ts index 26c0721bf7..9aca0f4b67 100644 --- a/web/src/ar/pages/version-details/components/VersionActions/types.ts +++ b/web/src/ar/pages/version-details/components/VersionActions/types.ts @@ -39,5 +39,6 @@ export enum VersionAction { ViewVersionDetails = 'viewVersionDetails', Quarantine = 'quarantine', Download = 'download', - ReEvaluate = 'reEvaluate' + ReEvaluate = 'reEvaluate', + AddTag = 'addTag' } diff --git a/web/src/ar/pages/version-list/strings/strings.en.yaml b/web/src/ar/pages/version-list/strings/strings.en.yaml index f2c5e617ea..2c9d961b8a 100644 --- a/web/src/ar/pages/version-list/strings/strings.en.yaml +++ b/web/src/ar/pages/version-list/strings/strings.en.yaml @@ -17,6 +17,8 @@ table: tags: Tags scanStatus: Evaluation Status actions: + addTag: Add Tag + addTagPlaceholder: Type tag name and press Enter deleteVersion: Delete archiveVersion: Archive restoreVersion: Restore @@ -24,5 +26,7 @@ actions: removeQuarantine: Remove From Quarantine reEvaluate: Re-Evaluate messages: + addTagSuccess: Tag added successfully + addTagFailed: Failed to add tag reEvaluateSuccess: Submitted request for Re-Evaluation successfully! reEvaluateFailed: Failed to submit request! diff --git a/web/src/ar/strings/types.ts b/web/src/ar/strings/types.ts index e9924b2c72..696480cc32 100644 --- a/web/src/ar/strings/types.ts +++ b/web/src/ar/strings/types.ts @@ -400,12 +400,16 @@ export interface StringsMap { 'versionDetails.versionArchived': string 'versionDetails.versionDeleted': string 'versionDetails.versionRestored': string + 'versionList.actions.addTag': string + 'versionList.actions.addTagPlaceholder': string 'versionList.actions.archiveVersion': string 'versionList.actions.deleteVersion': string 'versionList.actions.quarantine': string 'versionList.actions.reEvaluate': string 'versionList.actions.removeQuarantine': string 'versionList.actions.restoreVersion': string + 'versionList.messages.addTagFailed': string + 'versionList.messages.addTagSuccess': string 'versionList.messages.reEvaluateFailed': string 'versionList.messages.reEvaluateSuccess': string 'versionList.page': string