Skip to content

Commit 5645325

Browse files
committed
feat: Add BulkTriggerSidebar component and integrate it into DeployImageContent for improved image selection handling
1 parent 8fc1c44 commit 5645325

File tree

3 files changed

+258
-199
lines changed

3 files changed

+258
-199
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { SyntheticEvent, useMemo } from 'react'
2+
3+
import {
4+
API_STATUS_CODES,
5+
BULK_DEPLOY_ACTIVE_IMAGE_TAG,
6+
BULK_DEPLOY_LATEST_IMAGE_TAG,
7+
CD_MATERIAL_SIDEBAR_TABS,
8+
CDMaterialSidebarType,
9+
CDMaterialType,
10+
CommonNodeAttr,
11+
DeploymentNodeType,
12+
Icon,
13+
SelectPicker,
14+
SelectPickerOptionType,
15+
stopPropagation,
16+
stringComparatorBySortOrder,
17+
Tooltip,
18+
TriggerBlockType,
19+
} from '@devtron-labs/devtron-fe-common-lib'
20+
21+
import { BulkCDDetailType } from '@Components/ApplicationGroup/AppGroup.types'
22+
import { BULK_CD_MESSAGING } from '@Components/ApplicationGroup/Constants'
23+
import { importComponentFromFELibrary } from '@Components/common'
24+
25+
import { getIsMaterialApproved } from '../cdMaterials.utils'
26+
import { BulkTriggerSidebarProps } from './types'
27+
import { getIsExceptionUser } from './utils'
28+
29+
const RuntimeParamTabs = importComponentFromFELibrary('RuntimeParamTabs', null, 'function')
30+
const PolicyEnforcementMessage = importComponentFromFELibrary('PolicyEnforcementMessage')
31+
const TriggerBlockedError = importComponentFromFELibrary('TriggerBlockedError', null, 'function')
32+
33+
const BulkTriggerSidebar = ({
34+
appId,
35+
stageType,
36+
appInfoMap,
37+
selectedTagName,
38+
handleTagChange,
39+
changeApp,
40+
handleSidebarTabChange,
41+
currentSidebarTab,
42+
}: BulkTriggerSidebarProps) => {
43+
const isPreOrPostCD = stageType === DeploymentNodeType.PRECD || stageType === DeploymentNodeType.POSTCD
44+
45+
const tagOptions: SelectPickerOptionType<string>[] = useMemo(() => {
46+
const tagNames = new Set<string>()
47+
Object.values(appInfoMap).forEach((app) => {
48+
app.materialResponse?.appReleaseTagNames?.forEach((tag) => tagNames.add(tag))
49+
})
50+
51+
return [BULK_DEPLOY_LATEST_IMAGE_TAG, BULK_DEPLOY_ACTIVE_IMAGE_TAG].concat(
52+
Array.from(tagNames)
53+
.sort(stringComparatorBySortOrder)
54+
.map((tag) => ({ label: tag, value: tag })),
55+
)
56+
}, [appInfoMap])
57+
58+
const selectedTagOption = useMemo(() => {
59+
const selectedTag = tagOptions.find((option) => option.value === selectedTagName)
60+
const areMultipleTagsPresent = Object.values(appInfoMap).some((appDetails) => {
61+
const selectedImage = appDetails.materialResponse?.materials?.find(
62+
(material: CDMaterialType) => material.isSelected,
63+
)
64+
65+
if (!selectedImage) {
66+
return false
67+
}
68+
69+
return !selectedImage.imageReleaseTags?.some((tagDetails) => tagDetails.tagName === selectedTagName)
70+
})
71+
72+
if (areMultipleTagsPresent || !selectedTag) {
73+
return { label: 'Multiple Tags', value: '' }
74+
}
75+
76+
return selectedTag
77+
}, [selectedTagName, tagOptions, appInfoMap])
78+
79+
const sortedAppValues = useMemo(
80+
() => Object.values(appInfoMap).sort((a, b) => stringComparatorBySortOrder(a.appName, b.appName)),
81+
[appInfoMap],
82+
)
83+
84+
const getHandleAppChange = (newAppId: number) => (e: SyntheticEvent) => {
85+
stopPropagation(e)
86+
87+
if ('key' in e && e.key !== 'Enter' && e.key !== ' ') {
88+
return
89+
}
90+
91+
changeApp(newAppId)
92+
}
93+
94+
const renderDeploymentWithoutApprovalWarning = (app: BulkCDDetailType) => {
95+
const isExceptionUser = getIsExceptionUser(app.materialResponse)
96+
97+
if (!isExceptionUser) {
98+
return null
99+
}
100+
101+
const selectedMaterial: CDMaterialType = app.materialResponse?.materials?.find(
102+
(mat: CDMaterialType) => mat.isSelected,
103+
)
104+
105+
if (!selectedMaterial || getIsMaterialApproved(selectedMaterial?.userApprovalMetadata)) {
106+
return null
107+
}
108+
109+
return (
110+
<div className="flex left dc__gap-4 mb-4">
111+
<Icon name="ic-warning" color={null} size={14} />
112+
<p className="m-0 fs-12 lh-16 fw-4 cy-7">Non-approved image selected</p>
113+
</div>
114+
)
115+
}
116+
117+
const renderAppWarningAndErrors = (app: BulkCDDetailType) => {
118+
const isAppSelected = app.appId === appId
119+
// We don't support cd for mandatory plugins
120+
const blockedPluginNodeType: CommonNodeAttr['type'] =
121+
stageType === DeploymentNodeType.PRECD ? 'PRECD' : 'POSTCD'
122+
123+
if (app.materialError?.code === API_STATUS_CODES.UNAUTHORIZED) {
124+
return (
125+
<div className="flex left dc__gap-4">
126+
<Icon name="ic-locked" color="Y500" size={12} />
127+
<span className="cy-7 fw-4 fs-12 dc__truncate">{BULK_CD_MESSAGING.unauthorized.title}</span>
128+
</div>
129+
)
130+
}
131+
132+
if (app.isTriggerBlockedDueToPlugin) {
133+
return (
134+
<PolicyEnforcementMessage
135+
consequence={app.consequence}
136+
configurePluginURL={app.configurePluginURL}
137+
nodeType={blockedPluginNodeType}
138+
shouldRenderAdditionalInfo={isAppSelected}
139+
/>
140+
)
141+
}
142+
143+
if (app.triggerBlockedInfo?.blockedBy === TriggerBlockType.MANDATORY_TAG) {
144+
return <TriggerBlockedError stageType={stageType} />
145+
}
146+
147+
if (!!app.warningMessage && !app.showPluginWarning) {
148+
return (
149+
<div className="flex left top dc__gap-4">
150+
<Icon name="ic-warning" color={null} size={14} />
151+
<span className="fw-4 fs-12 cy-7 dc__truncate">{app.warningMessage}</span>
152+
</div>
153+
)
154+
}
155+
156+
if (app.showPluginWarning) {
157+
return (
158+
<PolicyEnforcementMessage
159+
consequence={app.consequence}
160+
configurePluginURL={app.configurePluginURL}
161+
nodeType={blockedPluginNodeType}
162+
shouldRenderAdditionalInfo={isAppSelected}
163+
/>
164+
)
165+
}
166+
167+
return null
168+
}
169+
170+
return (
171+
<div className="flexbox-col h-100 dc__overflow-auto bg__primary">
172+
<div className="dc__position-sticky dc__top-0 pt-12 bg__primary dc__zi-1">
173+
{!!(RuntimeParamTabs && isPreOrPostCD) && (
174+
<div className="px-16 pb-8">
175+
<RuntimeParamTabs
176+
tabs={CD_MATERIAL_SIDEBAR_TABS}
177+
initialTab={currentSidebarTab}
178+
onChange={handleSidebarTabChange}
179+
hasError={{
180+
[CDMaterialSidebarType.PARAMETERS]:
181+
appInfoMap[+appId]?.deployViewState?.runtimeParamsErrorState &&
182+
!appInfoMap[+appId].deployViewState.runtimeParamsErrorState.isValid,
183+
}}
184+
/>
185+
</div>
186+
)}
187+
188+
{currentSidebarTab === CDMaterialSidebarType.IMAGE && (
189+
<>
190+
<span className="px-16">Select image by release tag</span>
191+
<div className="tag-selection-dropdown px-16 pt-6 pb-12">
192+
<SelectPicker
193+
name="bulk-cd-trigger__select-tag"
194+
inputId="bulk-cd-trigger__select-tag"
195+
isSearchable
196+
options={tagOptions}
197+
value={selectedTagOption}
198+
icon={<Icon name="ic-tag" size={16} color={null} />}
199+
onChange={handleTagChange}
200+
isDisabled={false}
201+
// Not changing it for backward compatibility for automation
202+
classNamePrefix="build-config__select-repository-containing-code"
203+
autoFocus
204+
/>
205+
</div>
206+
</>
207+
)}
208+
<div className="dc__border-bottom py-8 px-16 w-100">
209+
<span className="fw-6 fs-13 cn-7">APPLICATIONS</span>
210+
</div>
211+
</div>
212+
213+
{sortedAppValues.map((appDetails) => (
214+
<div
215+
key={`app-${appDetails.appId}`}
216+
className={`p-16 dc__border-bottom-n1 cursor w-100 dc__tab-focus ${
217+
appDetails.appId === appId ? 'bg__tertiary' : ''
218+
}`}
219+
role="button"
220+
tabIndex={0}
221+
onClick={getHandleAppChange(appDetails.appId)}
222+
onKeyDown={getHandleAppChange(appDetails.appId)}
223+
>
224+
<Tooltip content={appDetails.appName}>
225+
<span className="lh-20 cn-9 fw-6 fs-13 dc__truncate">{appDetails.appName}</span>
226+
</Tooltip>
227+
{renderDeploymentWithoutApprovalWarning(appDetails)}
228+
{renderAppWarningAndErrors(appDetails)}
229+
</div>
230+
))}
231+
</div>
232+
)
233+
}
234+
235+
export default BulkTriggerSidebar

0 commit comments

Comments
 (0)