@devtron-labs/devtron-fe-common-lib 0.0.12 → 0.0.13-beta-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Common/Common.service.d.ts +10 -1
- package/dist/Common/Constants.d.ts +2 -0
- package/dist/Common/ImageTags.Types.d.ts +39 -0
- package/dist/Common/ImageTags.d.ts +4 -0
- package/dist/Common/Types.d.ts +32 -24
- package/dist/Common/index.d.ts +2 -0
- package/dist/index.js +554 -110
- package/package.json +1 -1
- package/src/Assets/Icon/ic-arrow-counter-clockwise.svg +5 -0
- package/src/Assets/Icon/ic-enter.svg +3 -0
- package/src/Assets/Icon/ic-error-exclamation.svg +6 -0
- package/src/Assets/Icon/ic-minus.svg +3 -0
- package/src/Assets/Icon/ic-pencil.svg +3 -0
- package/src/Common/Common.service.ts +18 -3
- package/src/Common/Constants.ts +2 -0
- package/src/Common/ImageTags.Types.ts +43 -0
- package/src/Common/ImageTags.tsx +586 -0
- package/src/Common/Types.ts +38 -26
- package/src/Common/index.ts +2 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { ReactComponent as Add } from '../Assets/Icon/ic-add.svg'
|
|
3
|
+
import { ReactComponent as Close } from '../Assets/Icon/ic-cross.svg'
|
|
4
|
+
import { ReactComponent as QuestionFilled } from '../Assets/Icon/ic-help.svg'
|
|
5
|
+
import { ReactComponent as Question } from '../Assets/Icon/ic-help-outline.svg'
|
|
6
|
+
|
|
7
|
+
import { ReactComponent as EditIcon } from '../Assets/Icon/ic-pencil.svg'
|
|
8
|
+
import { ReactComponent as Redo } from '../Assets/Icon/ic-arrow-counter-clockwise.svg'
|
|
9
|
+
import { ReactComponent as Minus } from '../Assets/Icon/ic-minus.svg'
|
|
10
|
+
import { toast } from 'react-toastify'
|
|
11
|
+
import { ReactComponent as Info } from '../Assets/Icon/ic-info-filled.svg'
|
|
12
|
+
import { ReactComponent as Error } from '../Assets/Icon/ic-warning.svg'
|
|
13
|
+
import { ReactComponent as Warning } from '../Assets/Icon/ic-error-exclamation.svg'
|
|
14
|
+
import { ReactComponent as Enter } from '../Assets/Icon/ic-enter.svg'
|
|
15
|
+
import { TippyTheme } from './Types'
|
|
16
|
+
import { ImageButtonType, ImageTaggingContainerType, ReleaseTag } from './ImageTags.Types'
|
|
17
|
+
import { showError, stopPropagation } from './Helper'
|
|
18
|
+
import { TippyCustomized } from './TippyCustomized'
|
|
19
|
+
import { setImageTags, getUserRole } from './Common.service'
|
|
20
|
+
import Tippy from '@tippyjs/react'
|
|
21
|
+
|
|
22
|
+
export const ImageTagsContainer = ({
|
|
23
|
+
ciPipelineId,
|
|
24
|
+
artifactId,
|
|
25
|
+
imageComment,
|
|
26
|
+
imageReleaseTags,
|
|
27
|
+
updateCurrentAppMaterial,
|
|
28
|
+
appReleaseTagNames,
|
|
29
|
+
setAppReleaseTagNames,
|
|
30
|
+
tagsEditable,
|
|
31
|
+
setTagsEditable,
|
|
32
|
+
toggleCardMode,
|
|
33
|
+
hideHardDelete,
|
|
34
|
+
forceReInit,
|
|
35
|
+
}: ImageTaggingContainerType) => {
|
|
36
|
+
const [initialTags, setInitialTags] = useState<ReleaseTag[]>(imageReleaseTags ? imageReleaseTags : [])
|
|
37
|
+
const [initialDescription, setInitialDescription] = useState(imageComment ? imageComment.comment : '')
|
|
38
|
+
const [existingTags, setExistingTags] = useState(appReleaseTagNames ? appReleaseTagNames : [])
|
|
39
|
+
const [newDescription, setNewDescription] = useState(imageComment ? imageComment.comment : '')
|
|
40
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
41
|
+
const [showTagsWarning, setShowTagsWarning] = useState(false)
|
|
42
|
+
const [displayedTags, setDisplayedTags] = useState<ReleaseTag[]>(imageReleaseTags ? imageReleaseTags : [])
|
|
43
|
+
const [tagErrorMessage, setTagErrorMessage] = useState('')
|
|
44
|
+
const [createTags, setCreateTags] = useState<ReleaseTag[]>([])
|
|
45
|
+
const [softDeleteTags, setSoftDeleteTags] = useState<ReleaseTag[]>([])
|
|
46
|
+
const [hardDeleteTags, setHardDeleteTags] = useState<ReleaseTag[]>([])
|
|
47
|
+
const [isSuperAdmin, setSuperAdmin] = useState<boolean>(false)
|
|
48
|
+
const [descriptionValidationMessage, setDescriptionValidationMessage] = useState<string>('')
|
|
49
|
+
const [textInput, setTextInput] = useState<string>('')
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
initialise()
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
reInitState()
|
|
57
|
+
}, [imageReleaseTags, imageComment, tagsEditable])
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setExistingTags(appReleaseTagNames ? appReleaseTagNames : [])
|
|
61
|
+
}, [appReleaseTagNames])
|
|
62
|
+
|
|
63
|
+
async function initialise() {
|
|
64
|
+
try {
|
|
65
|
+
const userRole = await getUserRole()
|
|
66
|
+
|
|
67
|
+
const superAdmin = userRole?.result?.roles?.includes('role:super-admin___')
|
|
68
|
+
setSuperAdmin(superAdmin)
|
|
69
|
+
} catch (err) {
|
|
70
|
+
showError(err)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const reInitState = () => {
|
|
75
|
+
if (forceReInit || !isEditing) {
|
|
76
|
+
setInitialTags(imageReleaseTags ? imageReleaseTags : [])
|
|
77
|
+
setInitialDescription(imageComment ? imageComment.comment : '')
|
|
78
|
+
setNewDescription(imageComment ? imageComment.comment : '')
|
|
79
|
+
setDisplayedTags(imageReleaseTags ? imageReleaseTags : [])
|
|
80
|
+
setIsEditing(false)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const handleEditClick = () => {
|
|
85
|
+
setIsEditing(!isEditing)
|
|
86
|
+
if (typeof toggleCardMode === 'function') {
|
|
87
|
+
toggleCardMode(artifactId)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const handleDescriptionChange = (e) => {
|
|
92
|
+
const description = e.target.value
|
|
93
|
+
description?.length > 500
|
|
94
|
+
? setDescriptionValidationMessage('comment length cannot exceed 500 characters')
|
|
95
|
+
: setDescriptionValidationMessage('')
|
|
96
|
+
setNewDescription(description)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const handleCancel = () => {
|
|
100
|
+
setDisplayedTags(initialTags)
|
|
101
|
+
setNewDescription(initialDescription)
|
|
102
|
+
setCreateTags([])
|
|
103
|
+
setSoftDeleteTags([])
|
|
104
|
+
setHardDeleteTags([])
|
|
105
|
+
handleEditClick()
|
|
106
|
+
setShowTagsWarning(false)
|
|
107
|
+
setTagErrorMessage('')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const validateTag = (lowercaseValue) : boolean => {
|
|
111
|
+
if (
|
|
112
|
+
lowercaseValue.length == 0 ||
|
|
113
|
+
lowercaseValue.length >= 128 ||
|
|
114
|
+
lowercaseValue[0] == '.' ||
|
|
115
|
+
lowercaseValue[0] == '-'
|
|
116
|
+
) {
|
|
117
|
+
setTagErrorMessage('tag name cannot be empty or exceed 128 characters or cannot start with . or -')
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
setTagErrorMessage('')
|
|
121
|
+
const isTagExistsInExistingTags = existingTags.includes(lowercaseValue)
|
|
122
|
+
let isTagExistsInDisplayedTags = false
|
|
123
|
+
for (let i = 0; i < displayedTags?.length; i++) {
|
|
124
|
+
if (displayedTags[i].tagName.toLowerCase() === lowercaseValue) isTagExistsInDisplayedTags = true
|
|
125
|
+
}
|
|
126
|
+
if (isTagExistsInExistingTags || isTagExistsInDisplayedTags || lowercaseValue === 'latest') {
|
|
127
|
+
setTagErrorMessage('This tag is already being used in this application')
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const handleTagCreate = (newValue) => {
|
|
134
|
+
const lowercaseValue = newValue.toLowerCase().trim()
|
|
135
|
+
if(validateTag(lowercaseValue)) {
|
|
136
|
+
const newTag: ReleaseTag = {
|
|
137
|
+
id: 0,
|
|
138
|
+
tagName: lowercaseValue,
|
|
139
|
+
appId: 0,
|
|
140
|
+
deleted: false,
|
|
141
|
+
artifactId: 0,
|
|
142
|
+
duplicateTag: false,
|
|
143
|
+
}
|
|
144
|
+
setCreateTags([...createTags, newTag])
|
|
145
|
+
setDisplayedTags([...displayedTags, newTag])
|
|
146
|
+
setShowTagsWarning(true)
|
|
147
|
+
setTextInput('')
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const handleTagSoftDelete = (index) => {
|
|
152
|
+
const updatedTags = [...displayedTags]
|
|
153
|
+
updatedTags[index] = {
|
|
154
|
+
...updatedTags[index],
|
|
155
|
+
deleted: !updatedTags[index].deleted,
|
|
156
|
+
}
|
|
157
|
+
const updatedTag = updatedTags[index]
|
|
158
|
+
let updatedSoftDeleteTags = [...softDeleteTags]
|
|
159
|
+
const tagIndex = updatedSoftDeleteTags.findIndex((tag) => tag.tagName === updatedTag.tagName)
|
|
160
|
+
if (tagIndex !== -1) {
|
|
161
|
+
// Tag already exists in softDeleteTags array, remove it
|
|
162
|
+
updatedSoftDeleteTags.splice(tagIndex, 1)
|
|
163
|
+
} else {
|
|
164
|
+
// Tag doesn't exist in softDeleteTags array, add it
|
|
165
|
+
updatedSoftDeleteTags.push(updatedTag)
|
|
166
|
+
}
|
|
167
|
+
setDisplayedTags(updatedTags)
|
|
168
|
+
setSoftDeleteTags(updatedSoftDeleteTags)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const handleTagHardDelete = (index) => {
|
|
172
|
+
const deletedTag = displayedTags[index]
|
|
173
|
+
const updatedCreateTags = createTags.filter((tag) => tag.tagName !== deletedTag.tagName)
|
|
174
|
+
setCreateTags(updatedCreateTags)
|
|
175
|
+
const updatedDisplayedTags = [...displayedTags]
|
|
176
|
+
updatedDisplayedTags.splice(index, 1)
|
|
177
|
+
setDisplayedTags(updatedDisplayedTags)
|
|
178
|
+
const updatedExistingTags = existingTags.filter((tag) => tag.toLowerCase() !== deletedTag.tagName.toLowerCase())
|
|
179
|
+
setExistingTags(updatedExistingTags)
|
|
180
|
+
if (deletedTag.id !== 0) {
|
|
181
|
+
const updatedHardDeleteTags = [...hardDeleteTags, deletedTag]
|
|
182
|
+
setHardDeleteTags(updatedHardDeleteTags)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const errorStateHandling = (err) => {
|
|
187
|
+
err.forEach(({ userMessage }) => {
|
|
188
|
+
const tagList = createTags.map((tags) => {
|
|
189
|
+
if (userMessage?.appReleaseTags?.includes(tags.tagName)) {
|
|
190
|
+
return {
|
|
191
|
+
...tags,
|
|
192
|
+
duplicateTag: true,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return tags
|
|
196
|
+
})
|
|
197
|
+
setCreateTags(tagList)
|
|
198
|
+
|
|
199
|
+
const displayList = displayedTags.map((obj) => {
|
|
200
|
+
const matchingTag = tagList.find((tag) => tag.tagName === obj.tagName)
|
|
201
|
+
if (matchingTag) {
|
|
202
|
+
return { ...obj, duplicateTag: matchingTag.duplicateTag }
|
|
203
|
+
}
|
|
204
|
+
return obj
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
setDisplayedTags(displayList)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const handleSave = async () => {
|
|
212
|
+
if (tagErrorMessage) return
|
|
213
|
+
|
|
214
|
+
const payload = {
|
|
215
|
+
createTags: createTags,
|
|
216
|
+
softDeleteTags: softDeleteTags,
|
|
217
|
+
imageComment: {
|
|
218
|
+
id: 0,
|
|
219
|
+
comment: newDescription.trim(),
|
|
220
|
+
artifactId: 0,
|
|
221
|
+
},
|
|
222
|
+
hardDeleteTags: hardDeleteTags,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// set loading state true
|
|
226
|
+
setImageTags(payload, ciPipelineId, artifactId)
|
|
227
|
+
.then((res) => {
|
|
228
|
+
const tags = res.result?.imageReleaseTags?.map((tag) => ({
|
|
229
|
+
id: tag.id,
|
|
230
|
+
tagName: tag.tagName,
|
|
231
|
+
deleted: tag.deleted,
|
|
232
|
+
appId: 0,
|
|
233
|
+
artifactId: 0,
|
|
234
|
+
}))
|
|
235
|
+
if (setAppReleaseTagNames) {
|
|
236
|
+
setAppReleaseTagNames(res.result?.appReleaseTags)
|
|
237
|
+
}
|
|
238
|
+
if (setTagsEditable) {
|
|
239
|
+
setTagsEditable(res.result?.tagsEditable)
|
|
240
|
+
}
|
|
241
|
+
setInitialTags(tags)
|
|
242
|
+
setInitialDescription(res.result?.imageComment?.comment)
|
|
243
|
+
setDisplayedTags(tags)
|
|
244
|
+
setNewDescription(res.result?.imageComment?.comment)
|
|
245
|
+
setCreateTags([])
|
|
246
|
+
setSoftDeleteTags([])
|
|
247
|
+
setHardDeleteTags([])
|
|
248
|
+
handleEditClick()
|
|
249
|
+
setShowTagsWarning(false)
|
|
250
|
+
setTagErrorMessage('')
|
|
251
|
+
if(updateCurrentAppMaterial)updateCurrentAppMaterial(artifactId,tags,res.result?.imageComment)
|
|
252
|
+
})
|
|
253
|
+
.catch((err) => {
|
|
254
|
+
// Fix toast message
|
|
255
|
+
if (err.errors?.[0]?.userMessage?.appReleaseTags?.length) {
|
|
256
|
+
toast.error(err.errors?.[0]?.internalMessage)
|
|
257
|
+
errorStateHandling(err.errors)
|
|
258
|
+
} else {
|
|
259
|
+
showError(err)
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const renderInfoCard = (): JSX.Element => {
|
|
265
|
+
return (
|
|
266
|
+
<TippyCustomized
|
|
267
|
+
theme={TippyTheme.white}
|
|
268
|
+
className="w-300 fcv-5"
|
|
269
|
+
placement="right"
|
|
270
|
+
Icon={QuestionFilled}
|
|
271
|
+
heading="Release tags"
|
|
272
|
+
showCloseButton={true}
|
|
273
|
+
trigger="click"
|
|
274
|
+
interactive={true}
|
|
275
|
+
additionalContent={getBuildContextAdditionalContent()}
|
|
276
|
+
>
|
|
277
|
+
<div className="icon-dim-16 fcn-5 ml-8 cursor">
|
|
278
|
+
<Question />
|
|
279
|
+
</div>
|
|
280
|
+
</TippyCustomized>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const getBuildContextAdditionalContent = () => {
|
|
285
|
+
return (
|
|
286
|
+
<div className="h-250 fs-13 dc__overflow-scroll p-12">
|
|
287
|
+
<div>Release tags allow you to tag container images with readable and relatable tags eg. v1.0.</div>
|
|
288
|
+
<ul className="pl-20 mt-8">
|
|
289
|
+
<li>
|
|
290
|
+
A release tag can only be added if a workflow has CD pipelines deploying to Production
|
|
291
|
+
environments.
|
|
292
|
+
</li>
|
|
293
|
+
<li>Multiple tags can be added to an image.</li>
|
|
294
|
+
<li>Multiple images in an application cannot have the same tag.</li>
|
|
295
|
+
<li>
|
|
296
|
+
Tags cannot be deleted once saved. Although, you can soft delete a tag if an unwanted tag has
|
|
297
|
+
been added.
|
|
298
|
+
</li>
|
|
299
|
+
</ul>
|
|
300
|
+
</div>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const creatableRef = useRef(null)
|
|
305
|
+
|
|
306
|
+
if (newDescription === '' && displayedTags.length === 0 && !isEditing) {
|
|
307
|
+
return tagsEditable ? (
|
|
308
|
+
<div className="bcn-0 mt-8 add-image-button">
|
|
309
|
+
<AddImageButton handleEditClick={handleEditClick} />
|
|
310
|
+
</div>
|
|
311
|
+
) : (
|
|
312
|
+
<div></div>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const handleKeydown = (event) => {
|
|
317
|
+
if (event.key === 'Enter' && event.target.value.trim()) {
|
|
318
|
+
handleTagCreate(event.target.value.trim())
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const setInputValue = (e) => {
|
|
323
|
+
e.target.value && validateTag(e.target.value)
|
|
324
|
+
setTextInput(e.target.value)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const enterTagCreate = (e) => {
|
|
328
|
+
stopPropagation(e)
|
|
329
|
+
if (textInput.trim()) {
|
|
330
|
+
handleTagCreate(textInput.trim())
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const onClickSave = () => {
|
|
335
|
+
if (textInput.trim()) {
|
|
336
|
+
handleTagCreate(textInput)
|
|
337
|
+
}
|
|
338
|
+
handleSave()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
<div className="mt-8">
|
|
343
|
+
{isEditing && tagsEditable ? (
|
|
344
|
+
<div className="bcn-0 dc__border-top-n1 ">
|
|
345
|
+
<div className="cn-7 mt-12 flex left">
|
|
346
|
+
<span>Release tags (eg. v1.0)</span>
|
|
347
|
+
<div className="flex row ml-0">{renderInfoCard()}</div>
|
|
348
|
+
</div>
|
|
349
|
+
<div className="mt-6 dc__position-rel-imp" data-testid="add-tag-text-area">
|
|
350
|
+
<input
|
|
351
|
+
className="form__input"
|
|
352
|
+
value={textInput}
|
|
353
|
+
onChange={setInputValue}
|
|
354
|
+
placeholder="Type a tag and press enter"
|
|
355
|
+
onKeyDown={handleKeydown}
|
|
356
|
+
onBlur={setInputValue}
|
|
357
|
+
ref={creatableRef}
|
|
358
|
+
/>
|
|
359
|
+
{textInput.length > 0 && (
|
|
360
|
+
<div
|
|
361
|
+
onClick={enterTagCreate}
|
|
362
|
+
className="dc__position-abs bcn-0 flex cursor pt-2 pb-2 pr-4 pl-4 br-2 cn-7 dc__border dc__border-bottom-2"
|
|
363
|
+
style={{
|
|
364
|
+
top: '50%',
|
|
365
|
+
fontFamily: 'monospace',
|
|
366
|
+
right: '8px',
|
|
367
|
+
transform: 'translateY(-50%)',
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
<Enter className="icon-dim-16 mr-4" />
|
|
371
|
+
Enter
|
|
372
|
+
</div>
|
|
373
|
+
)}
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
{tagErrorMessage && (
|
|
377
|
+
<div className="flex left">
|
|
378
|
+
<Error className="form__icon form__icon--error" />
|
|
379
|
+
<div className="form__error">{tagErrorMessage}</div>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
<div className="flex-wrap mt-8 flex left">
|
|
383
|
+
{displayedTags?.map((tag, index) => (
|
|
384
|
+
<ImageTagButton
|
|
385
|
+
key={tag.id}
|
|
386
|
+
text={tag?.tagName}
|
|
387
|
+
isSoftDeleted={tag?.deleted}
|
|
388
|
+
isEditing={isEditing}
|
|
389
|
+
onSoftDeleteClick={(e) => {
|
|
390
|
+
stopPropagation(e)
|
|
391
|
+
handleTagSoftDelete(index)
|
|
392
|
+
}}
|
|
393
|
+
onHardDeleteClick={(e) => {
|
|
394
|
+
stopPropagation(e)
|
|
395
|
+
handleTagHardDelete(index)
|
|
396
|
+
}}
|
|
397
|
+
tagId={tag.id}
|
|
398
|
+
softDeleteTags={softDeleteTags}
|
|
399
|
+
isSuperAdmin={isSuperAdmin}
|
|
400
|
+
duplicateTag={tag?.duplicateTag}
|
|
401
|
+
hideHardDelete={hideHardDelete}
|
|
402
|
+
/>
|
|
403
|
+
))}
|
|
404
|
+
</div>
|
|
405
|
+
{showTagsWarning && (
|
|
406
|
+
<div className="cn-7 mb-8 flex left">
|
|
407
|
+
<Info className="form__icon--info icon-dim-16 mr-2" />
|
|
408
|
+
Tags cannot be edited/removed later
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
<div className="cn-7 mt-12">Comment</div>
|
|
412
|
+
<div
|
|
413
|
+
className="flex left flex-wrap dc__gap-8 w-100 mt-6 mb-12"
|
|
414
|
+
data-testid="add-image-comment-text-area"
|
|
415
|
+
>
|
|
416
|
+
<textarea
|
|
417
|
+
value={newDescription}
|
|
418
|
+
onChange={handleDescriptionChange}
|
|
419
|
+
className="flex left flex-wrap dc__gap-8 dc__description-textarea"
|
|
420
|
+
style={{ height: '90px !important' }}
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
{descriptionValidationMessage !== '' && (
|
|
424
|
+
<div className="flex left">
|
|
425
|
+
<Error className="form__icon form__icon--error" />
|
|
426
|
+
<div className="form__error">{descriptionValidationMessage}</div>
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
<div className="w-100 flex right mt-12">
|
|
430
|
+
<button
|
|
431
|
+
className="cta flex cancel h-32 lh-32-imp"
|
|
432
|
+
type="button"
|
|
433
|
+
onClick={(e) => {
|
|
434
|
+
stopPropagation(e)
|
|
435
|
+
handleCancel()
|
|
436
|
+
}}
|
|
437
|
+
>
|
|
438
|
+
Cancel
|
|
439
|
+
</button>
|
|
440
|
+
<button
|
|
441
|
+
className="cta flex h-32 lh-32-imp ml-12"
|
|
442
|
+
data-testid="save-tags-button"
|
|
443
|
+
type="button"
|
|
444
|
+
onClick={(e) => {
|
|
445
|
+
stopPropagation(e)
|
|
446
|
+
onClickSave()
|
|
447
|
+
}}
|
|
448
|
+
>
|
|
449
|
+
Save
|
|
450
|
+
</button>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
) : (
|
|
454
|
+
<div className="top br-4 bcn-0 image-tags-container flexbox">
|
|
455
|
+
<div
|
|
456
|
+
className="flex left image-tag-left-border w-100 mt-8 mb-8 pr-10 pl-10"
|
|
457
|
+
data-testid="image-tags-container-hover"
|
|
458
|
+
>
|
|
459
|
+
<div className='w-100'>
|
|
460
|
+
{initialDescription && <div className="mb-6 fs-13 dc__word-break-all">{initialDescription}</div>}
|
|
461
|
+
<div className="flex-wrap flex left">
|
|
462
|
+
{initialTags?.map((tag, index) => (
|
|
463
|
+
<ImageTagButton
|
|
464
|
+
key={tag?.id}
|
|
465
|
+
text={tag?.tagName}
|
|
466
|
+
isSoftDeleted={tag?.deleted}
|
|
467
|
+
isEditing={isEditing}
|
|
468
|
+
onSoftDeleteClick={(e) => {
|
|
469
|
+
stopPropagation(e)
|
|
470
|
+
handleTagSoftDelete(index)
|
|
471
|
+
}}
|
|
472
|
+
onHardDeleteClick={(e) => {
|
|
473
|
+
stopPropagation(e)
|
|
474
|
+
handleTagHardDelete(index)
|
|
475
|
+
}}
|
|
476
|
+
tagId={tag.id}
|
|
477
|
+
softDeleteTags={softDeleteTags}
|
|
478
|
+
isSuperAdmin={isSuperAdmin}
|
|
479
|
+
duplicateTag={tag?.duplicateTag}
|
|
480
|
+
hideHardDelete={hideHardDelete}
|
|
481
|
+
/>
|
|
482
|
+
))}
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
<div className="mt-8 mr-6 icon-dim-16">
|
|
487
|
+
{tagsEditable && (
|
|
488
|
+
<EditIcon
|
|
489
|
+
className="icon-dim-16 image-tags-container-edit__icon cursor"
|
|
490
|
+
data-testid="edit-tags-icon"
|
|
491
|
+
onClick={(e) => {
|
|
492
|
+
stopPropagation(e)
|
|
493
|
+
handleEditClick()
|
|
494
|
+
}}
|
|
495
|
+
/>
|
|
496
|
+
)}
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export const ImageTagButton = ({
|
|
505
|
+
text,
|
|
506
|
+
isSoftDeleted,
|
|
507
|
+
isEditing,
|
|
508
|
+
onSoftDeleteClick,
|
|
509
|
+
onHardDeleteClick,
|
|
510
|
+
tagId,
|
|
511
|
+
softDeleteTags,
|
|
512
|
+
isSuperAdmin,
|
|
513
|
+
duplicateTag,
|
|
514
|
+
hideHardDelete,
|
|
515
|
+
}: ImageButtonType) => {
|
|
516
|
+
const IconComponent = isSoftDeleted ? Redo : Minus
|
|
517
|
+
const isInSoftDeleteTags = isSoftDeleted && softDeleteTags.some((tag) => tag.tagName === text)
|
|
518
|
+
const canTagBeHardDelete = tagId === 0 || (isSuperAdmin && !hideHardDelete)
|
|
519
|
+
|
|
520
|
+
const tabColor = () => {
|
|
521
|
+
if (duplicateTag) {
|
|
522
|
+
return 'cr-5 bcr-1 er-2'
|
|
523
|
+
} else if (isSoftDeleted) {
|
|
524
|
+
return 'cy-7 bcy-1 dc__strike-through ey-2'
|
|
525
|
+
} else {
|
|
526
|
+
return 'cn-9'
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<div
|
|
532
|
+
className={`br-4 en-2 bw-1 mr-8 bcn-0 mb-4 pt-2 pr-8 pb-2 pl-8 tag-class dc__word-break-all flex ${
|
|
533
|
+
isEditing ? 'icon-hover' : ''
|
|
534
|
+
} ${tabColor()}`}
|
|
535
|
+
>
|
|
536
|
+
{duplicateTag ? (
|
|
537
|
+
<Warning className="icon-dim-12 mr-4" />
|
|
538
|
+
) : (
|
|
539
|
+
<Tippy
|
|
540
|
+
className="default-tt"
|
|
541
|
+
arrow={true}
|
|
542
|
+
placement="top"
|
|
543
|
+
content={isInSoftDeleteTags ? 'Restore tag' : 'Soft delete tag'}
|
|
544
|
+
>
|
|
545
|
+
<div
|
|
546
|
+
className={`action-icon mr-4 lh-16 pt-3 ${
|
|
547
|
+
isInSoftDeleteTags || (tagId !== 0 && !isSoftDeleted) ? 'show-icon' : ''
|
|
548
|
+
}`}
|
|
549
|
+
>
|
|
550
|
+
<IconComponent
|
|
551
|
+
className={`icon-dim-12 cursor${isSoftDeleted ? 'scn-6' : 'fcn-6'} `}
|
|
552
|
+
data-testid={`${text}-tag-soft-delete`}
|
|
553
|
+
onClick={onSoftDeleteClick}
|
|
554
|
+
/>
|
|
555
|
+
</div>
|
|
556
|
+
</Tippy>
|
|
557
|
+
)}
|
|
558
|
+
{text}
|
|
559
|
+
<Tippy className="default-tt" arrow={true} placement="top" content="Remove tag">
|
|
560
|
+
<div className={`action-icon ml-4 lh-16 pt-3 ${canTagBeHardDelete ? 'show-icon' : ''}`}>
|
|
561
|
+
<Close
|
|
562
|
+
className="icon-dim-12 fcn-6 cursor"
|
|
563
|
+
data-testid={`${text}-tag-hard-delete`}
|
|
564
|
+
onClick={onHardDeleteClick}
|
|
565
|
+
/>
|
|
566
|
+
</div>
|
|
567
|
+
</Tippy>
|
|
568
|
+
</div>
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const AddImageButton = ({ handleEditClick }) => {
|
|
573
|
+
const handleClick = (e) => {
|
|
574
|
+
stopPropagation(e)
|
|
575
|
+
handleEditClick()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<div className="add-tag-button flex pt-12 pr-12" data-testid="add-tags-button" onClick={handleClick}>
|
|
580
|
+
<div className="lh-16 flex">
|
|
581
|
+
<Add className="icon-dim-16 cn-6" />
|
|
582
|
+
<span className="cn-7">Add tags/comment</span>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
)
|
|
586
|
+
}
|
package/src/Common/Types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ReactNode, CSSProperties } from 'react'
|
|
1
|
+
import React, { ReactNode, CSSProperties } from 'react'
|
|
2
2
|
import { Placement } from 'tippy.js'
|
|
3
|
+
import { ImageComment, ReleaseTag } from "./ImageTags.Types";
|
|
3
4
|
export interface ResponseType {
|
|
4
5
|
code: number
|
|
5
6
|
status: string
|
|
@@ -212,6 +213,34 @@ export interface UserApprovalMetadataType {
|
|
|
212
213
|
requestedUserData: ApprovalUserDataType
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
|
|
217
|
+
export interface CDMaterialType {
|
|
218
|
+
index: number
|
|
219
|
+
id: string
|
|
220
|
+
materialInfo: MaterialInfo[]
|
|
221
|
+
tab: CDModalTabType
|
|
222
|
+
scanEnabled: boolean
|
|
223
|
+
scanned: boolean
|
|
224
|
+
vulnerabilitiesLoading: boolean
|
|
225
|
+
lastExecution: string //timestamp
|
|
226
|
+
vulnerabilities: VulnerabilityType[]
|
|
227
|
+
vulnerable: boolean
|
|
228
|
+
deployedTime: string
|
|
229
|
+
deployedBy?: string
|
|
230
|
+
wfrId?: number
|
|
231
|
+
buildTime: string
|
|
232
|
+
image: string
|
|
233
|
+
isSelected: boolean
|
|
234
|
+
showSourceInfo: boolean
|
|
235
|
+
latest: boolean
|
|
236
|
+
runningOnParentCd?: boolean
|
|
237
|
+
userApprovalMetadata?: UserApprovalMetadataType
|
|
238
|
+
triggeredBy?: number
|
|
239
|
+
imageComment?: ImageComment
|
|
240
|
+
imageReleaseTags?: ReleaseTag[]
|
|
241
|
+
artifactStatus?: string
|
|
242
|
+
}
|
|
243
|
+
|
|
215
244
|
export interface CommonNodeAttr {
|
|
216
245
|
connectingCiPipelineId?: number
|
|
217
246
|
parents: string | number[] | string[]
|
|
@@ -268,6 +297,8 @@ export interface CommonNodeAttr {
|
|
|
268
297
|
action: any,
|
|
269
298
|
metadataField: string
|
|
270
299
|
}
|
|
300
|
+
appReleaseTagNames?: string[]
|
|
301
|
+
tagsEditable?: boolean
|
|
271
302
|
}
|
|
272
303
|
|
|
273
304
|
export interface VulnerabilityType {
|
|
@@ -297,33 +328,14 @@ export interface MaterialInfo {
|
|
|
297
328
|
type?: string
|
|
298
329
|
}
|
|
299
330
|
|
|
300
|
-
export interface CDMaterialType {
|
|
301
|
-
index: number
|
|
302
|
-
id: string
|
|
303
|
-
materialInfo: MaterialInfo[]
|
|
304
|
-
tab: CDModalTabType
|
|
305
|
-
scanEnabled: boolean
|
|
306
|
-
scanned: boolean
|
|
307
|
-
vulnerabilitiesLoading: boolean
|
|
308
|
-
lastExecution: string //timestamp
|
|
309
|
-
vulnerabilities: VulnerabilityType[]
|
|
310
|
-
vulnerable: boolean
|
|
311
|
-
deployedTime: string
|
|
312
|
-
deployedBy?: string
|
|
313
|
-
wfrId?: number
|
|
314
|
-
buildTime: string
|
|
315
|
-
image: string
|
|
316
|
-
isSelected: boolean
|
|
317
|
-
showSourceInfo: boolean
|
|
318
|
-
latest: boolean
|
|
319
|
-
runningOnParentCd?: boolean
|
|
320
|
-
userApprovalMetadata?: UserApprovalMetadataType
|
|
321
|
-
triggeredBy?: number
|
|
322
|
-
}
|
|
323
|
-
|
|
324
331
|
export interface CDMaterialResponseType {
|
|
325
332
|
approvalUsers: string[]
|
|
326
333
|
materials: any[]
|
|
327
334
|
userApprovalConfig: UserApprovalConfigType
|
|
328
335
|
requestedUserId: number
|
|
329
|
-
|
|
336
|
+
tagsEditable: boolean
|
|
337
|
+
appReleaseTagNames: string[]
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
package/src/Common/index.ts
CHANGED
|
@@ -32,3 +32,5 @@ export { default as StyledRadioGroup } from './RadioGroup/RadioGroup'
|
|
|
32
32
|
export * from './CIPipeline.Types'
|
|
33
33
|
export * from './Policy.Types'
|
|
34
34
|
export { default as DeleteComponent } from './DeleteComponentModal/DeleteComponent'
|
|
35
|
+
export * from './ImageTags'
|
|
36
|
+
export * from './ImageTags.Types'
|