From 319fe1961bb83ffdb8602ab71194d6a1199f7028 Mon Sep 17 00:00:00 2001 From: shivang Date: Mon, 23 Feb 2026 15:45:17 +0530 Subject: [PATCH 1/5] feat: [AH-2867]: Frontend support to add tag to docker and helm artifact --- .../MultiTagsInput/MultiTagsInput.module.scss | 5 + .../DockerVersion/DockerVersionType.tsx | 3 +- .../HelmVersion/HelmVersionType.tsx | 3 +- .../VersionActions/AddTagMenuItem.module.scss | 23 +++ .../VersionActions/AddTagMenuItem.tsx | 183 ++++++++++++++++++ .../VersionActions/VersionActions.tsx | 41 +++- .../components/VersionActions/types.ts | 3 +- .../version-list/strings/strings.en.yaml | 4 + web/src/ar/strings/types.ts | 4 + 9 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.module.scss create mode 100644 web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.tsx diff --git a/web/src/ar/components/MultiTagsInput/MultiTagsInput.module.scss b/web/src/ar/components/MultiTagsInput/MultiTagsInput.module.scss index 9e204b77de..597053ebd9 100644 --- a/web/src/ar/components/MultiTagsInput/MultiTagsInput.module.scss +++ b/web/src/ar/components/MultiTagsInput/MultiTagsInput.module.scss @@ -15,9 +15,11 @@ */ .mutliTagsInput { + &.hidePopover { & :global(.bp3-overlay) { display: none; + } & :global(.bp3-input) { @@ -27,15 +29,18 @@ &.readonly { & :global(.bp3-input) { cursor: default; + } } & :global(.bp3-tag) { background-color: #0280ff12 !important; border: 0.5px solid #0278d5; color: #0278d5 !important; + &:hover { background-color: #0281ff29 !important; + } } } 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..186a3a85c3 --- /dev/null +++ b/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.module.scss @@ -0,0 +1,23 @@ +/* + * 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) { + min-width: 400px; + max-width: 400px; + } +} 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..fabaa68895 --- /dev/null +++ b/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.tsx @@ -0,0 +1,183 @@ +/* + * 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, { useState } from 'react' +import { Formik } from 'formik' +import { Button, ButtonVariation, Layout, ModalDialog, useToaster } from '@harnessio/uicore' + +import { useParentComponents } from '@ar/hooks' +import { useStrings } from '@ar/frameworks/strings' +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 + accountId: string + getApiBaseUrl: (url: string) => string + getCustomHeaders: () => Record + hideModal: () => void + onClose?: () => void +} + +export function AddTagModalContent({ + artifactKey, + repoKey, + versionKey, + accountId, + getApiBaseUrl, + getCustomHeaders, + hideModal, + onClose +}: AddTagModalContentProps): JSX.Element { + const { getString } = useStrings() + const { showSuccess, showError } = useToaster() + const [loading, setLoading] = useState(false) + + const handleSubmit = async ( + tagNamesRaw: string[] | (string | { label: string; value: string })[] + ) => { + const trimmed = normalizeTagNames(tagNamesRaw) + if (trimmed.length === 0) { + showError(getString('validationMessages.entityRequired', { entity: 'Tag name' })) + return + } + setLoading(true) + try { + const base = getApiBaseUrl('') + .replace(/\/api\/v1\/?$/, '') + .replace(/\/har\/api\/v1\/?$/, '') + .replace(/\/har\/?$/, '') + const url = + `${base}/har/api/v2/oci/tags?account_identifier=${encodeURIComponent(accountId || '')}®istry_identifier=${encodeURIComponent(repoKey)}` + const headers: Record = { + 'Content-Type': 'application/json', + ...getCustomHeaders() + } + const res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ + package: artifactKey, + version: versionKey, + tags: trimmed + }) + }) + if (!res.ok) { + const errText = await res.text() + throw new Error(errText || res.statusText) + } + showSuccess(getString('versionList.messages.addTagSuccess')) + hideModal() + onClose?.() + window.dispatchEvent(new CustomEvent('ar-refresh-artifact-list')) + } catch (e) { + showError((e as Error)?.message || getString('versionList.messages.addTagFailed')) + } finally { + setLoading(false) + } + } + + const handleClose = () => { + hideModal() + onClose?.() + } + + return ( + {}} + > + {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..79f7c2ff5b 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,8 @@ 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 { useParentUtils } from '@ar/hooks/useParentUtils' import { useStrings } from '@ar/frameworks/strings' import ActionButton from '@ar/components/ActionButton/ActionButton' import CopyMenuItem from '@ar/components/MenuItemTypes/CopyMenuItem' @@ -34,6 +35,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, @@ -50,14 +52,37 @@ export default function VersionActions({ }: VersionActionProps): JSX.Element { const [open, setOpen] = useState(false) const routes = useRoutes() - const { isCurrentSessionPublic } = useAppStore() + const { isCurrentSessionPublic, scope } = useAppStore() const { getString } = useStrings() + const { useModalHook } = useParentHooks() + const { getApiBaseUrl, getCustomHeaders } = useParentUtils() 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, scope?.accountId] + ) + const isAllowed = (action: VersionAction): boolean => { if (!allowedActions) return true return allowedActions.includes(action) @@ -179,6 +204,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 From 770a0e91b297f5fc3485320cd36b41319f2f783e Mon Sep 17 00:00:00 2001 From: shivang Date: Tue, 24 Feb 2026 12:58:59 +0530 Subject: [PATCH 2/5] feat: [AH-2867]: frontend support to add tag for docker and helm --- .../MultiTagsInput/MultiTagsInput.module.scss | 5 ---- .../pages/artifact-list/ArtifactListPage.tsx | 8 +++++- .../VersionActions/AddTagMenuItem.tsx | 25 ++++++++----------- .../VersionActions/VersionActions.tsx | 9 ++++++- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/web/src/ar/components/MultiTagsInput/MultiTagsInput.module.scss b/web/src/ar/components/MultiTagsInput/MultiTagsInput.module.scss index 597053ebd9..9e204b77de 100644 --- a/web/src/ar/components/MultiTagsInput/MultiTagsInput.module.scss +++ b/web/src/ar/components/MultiTagsInput/MultiTagsInput.module.scss @@ -15,11 +15,9 @@ */ .mutliTagsInput { - &.hidePopover { & :global(.bp3-overlay) { display: none; - } & :global(.bp3-input) { @@ -29,18 +27,15 @@ &.readonly { & :global(.bp3-input) { cursor: default; - } } & :global(.bp3-tag) { background-color: #0280ff12 !important; border: 0.5px solid #0278d5; color: #0278d5 !important; - &:hover { background-color: #0281ff29 !important; - } } } diff --git a/web/src/ar/pages/artifact-list/ArtifactListPage.tsx b/web/src/ar/pages/artifact-list/ArtifactListPage.tsx index c85c071394..bdbdb990d8 100644 --- a/web/src/ar/pages/artifact-list/ArtifactListPage.tsx +++ b/web/src/ar/pages/artifact-list/ArtifactListPage.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useMemo, useRef } from 'react' +import React, { useEffect, useMemo, useRef } from 'react' import classNames from 'classnames' import { flushSync } from 'react-dom' import { Expander } from '@blueprintjs/core' @@ -89,6 +89,12 @@ function ArtifactListPage(): JSX.Element { package_type: packageTypes }) + useEffect(() => { + const handler = () => refetch() + window.addEventListener('ar-refresh-artifact-list', handler) + return () => window.removeEventListener('ar-refresh-artifact-list', handler) + }, [refetch]) + const handleClearAllFilters = (): void => { flushSync(searchRef.current.clear) replaceQueryParams({ diff --git a/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.tsx b/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.tsx index fabaa68895..e6f71ccba2 100644 --- a/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.tsx +++ b/web/src/ar/pages/version-details/components/VersionActions/AddTagMenuItem.tsx @@ -20,6 +20,7 @@ import { Button, ButtonVariation, Layout, ModalDialog, useToaster } from '@harne import { 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' @@ -27,9 +28,7 @@ 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[] { +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 ?? '')) @@ -62,9 +61,7 @@ export function AddTagModalContent({ const { showSuccess, showError } = useToaster() const [loading, setLoading] = useState(false) - const handleSubmit = async ( - tagNamesRaw: string[] | (string | { label: string; value: string })[] - ) => { + const handleSubmit = async (tagNamesRaw: string[] | (string | { label: string; value: string })[]) => { const trimmed = normalizeTagNames(tagNamesRaw) if (trimmed.length === 0) { showError(getString('validationMessages.entityRequired', { entity: 'Tag name' })) @@ -76,8 +73,9 @@ export function AddTagModalContent({ .replace(/\/api\/v1\/?$/, '') .replace(/\/har\/api\/v1\/?$/, '') .replace(/\/har\/?$/, '') - const url = - `${base}/har/api/v2/oci/tags?account_identifier=${encodeURIComponent(accountId || '')}®istry_identifier=${encodeURIComponent(repoKey)}` + const url = `${base}/har/api/v2/oci/tags?account_identifier=${encodeURIComponent( + accountId || '' + )}®istry_identifier=${encodeURIComponent(repoKey)}` const headers: Record = { 'Content-Type': 'application/json', ...getCustomHeaders() @@ -99,6 +97,8 @@ export function AddTagModalContent({ hideModal() onClose?.() window.dispatchEvent(new CustomEvent('ar-refresh-artifact-list')) + queryClient.invalidateQueries(['GetAllHarnessArtifacts']) + queryClient.invalidateQueries(['ListVersions']) } catch (e) { showError((e as Error)?.message || getString('versionList.messages.addTagFailed')) } finally { @@ -112,11 +112,7 @@ export function AddTagModalContent({ } return ( - {}} - > + undefined}> {formik => ( handleSubmit(formik.values.tagNames)} - disabled={loading} - > + disabled={loading}> {getString('add')} @@ -140,7 +125,7 @@ export function AddTagModalContent({ }> -
+