-
Notifications
You must be signed in to change notification settings - Fork 1
feat: [AH-2867]: Frontend support to add tag to docker and helm artifact #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
319fe19
770a0e9
b069cd1
4460059
f0a9395
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -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<HTMLDivElement>(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); | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove leftover 🔧 Proposed fix- console.log('allTags', allTags);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||
| 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 ( | ||||
| <Formik initialValues={{ tagNames: [] as string[] }} enableReinitialize onSubmit={() => undefined}> | ||||
| {formik => ( | ||||
| <ModalDialog | ||||
| title={getString('versionList.actions.addTag')} | ||||
| isOpen={true} | ||||
| onClose={handleClose} | ||||
| showOverlay={loading} | ||||
| footer={ | ||||
| <Layout.Horizontal spacing="large"> | ||||
| <Button | ||||
| variation={ButtonVariation.PRIMARY} | ||||
| onClick={() => { | ||||
| const currentInput = | ||||
| inputWrapperRef.current?.querySelector('input')?.value?.trim() ?? '' | ||||
| handleSubmit(formik.values.tagNames, currentInput) | ||||
| }} | ||||
| disabled={loading}> | ||||
| {getString('add')} | ||||
| </Button> | ||||
| <Button variation={ButtonVariation.SECONDARY} onClick={handleClose} disabled={loading}> | ||||
| {getString('cancel')} | ||||
| </Button> | ||||
| </Layout.Horizontal> | ||||
| }> | ||||
| <div ref={inputWrapperRef} className={css.addTagInputWrapper}> | ||||
| <PatternInput | ||||
| name="tagNames" | ||||
| label={getString('versionList.table.columns.tags')} | ||||
| placeholder={getString('versionList.actions.addTagPlaceholder')} | ||||
| disabled={loading} | ||||
| /> | ||||
| </div> | ||||
| </ModalDialog> | ||||
| )} | ||||
| </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 ( | ||||
| <RbacMenuItem | ||||
| icon="plus" | ||||
| text={getString('versionList.actions.addTag')} | ||||
| onClick={handleClick} | ||||
| disabled={readonly} | ||||
| permission={{ | ||||
| resource: { | ||||
| resourceType: ResourceType.ARTIFACT_REGISTRY, | ||||
| resourceIdentifier: repoKey | ||||
| }, | ||||
| permission: PermissionIdentifier.UPLOAD_ARTIFACT | ||||
| }} | ||||
| /> | ||||
| ) | ||||
| } | ||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| () => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <AddTagModalContent | ||||||||||||||||||||||||||||||||||||||||||||||||||
| artifactKey={artifactKey} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| repoKey={repoKey} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| versionKey={versionKey} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| hideModal={hideAddTagModal} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose={closeMenu} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| [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) && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <AddTagMenuItem | ||||||||||||||||||||||||||||||||||||||||||||||||||
| artifactKey={artifactKey} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| repoKey={repoKey} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| versionKey={versionKey} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data={data} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pageType={pageType} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly={readonly} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose={closeMenu} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| openAddTagModal={showAddTagModal} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+209
to
+220
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hide Add Tag in public sessions for consistency. Other mutating actions are gated by Suggested guard- {isAllowed(VersionAction.AddTag) && (
+ {!isCurrentSessionPublic && isAllowed(VersionAction.AddTag) && (
<AddTagMenuItem
artifactKey={artifactKey}
repoKey={repoKey}
versionKey={versionKey}
data={data}
pageType={pageType}
readonly={readonly}
onClose={closeMenu}
openAddTagModal={showAddTagModal}
/>
)}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| </ActionButton> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stylelint error for
:globalneeds a fix.The current selector trips
selector-pseudo-class-no-unknown, so the lint step will fail unless the rule is configured to allow:global.✅ One quick fix (local suppression)
.addTagInputWrapper { + /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ :global(.bp3-input) { min-width: 400px; max-width: 400px; } }📝 Committable suggestion
🧰 Tools
🪛 Stylelint (17.3.0)
[error] 19-19: Unexpected unknown pseudo-class selector ":global" (selector-pseudo-class-no-unknown)
(selector-pseudo-class-no-unknown)
🤖 Prompt for AI Agents