Skip to content

Commit 04f3a63

Browse files
Merge pull request #1310 from devtron-labs/bulk-source-change
feat: Ability to change branch for all selected applications during bulk build from Application Groups
2 parents b5dbc29 + 3081d3c commit 04f3a63

File tree

8 files changed

+327
-5
lines changed

8 files changed

+327
-5
lines changed

src/components/ApplicationGroup/Constants.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,16 @@ export const CREATE_GROUP_TABS = {
167167
selectedApps: 'Selected applications',
168168
allApps: 'Add/Remove applications',
169169
}
170+
171+
export const GetBranchChangeStatus = (statusText: string): BulkResponseStatus => {
172+
switch (statusText) {
173+
case BULK_VIRTUAL_RESPONSE_STATUS.pass:
174+
return BulkResponseStatus.PASS
175+
case BULK_VIRTUAL_RESPONSE_STATUS.fail:
176+
return BulkResponseStatus.FAIL
177+
case BULK_VIRTUAL_RESPONSE_STATUS.unauthorized:
178+
return BulkResponseStatus.UNAUTHORIZE
179+
default:
180+
return
181+
}
182+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { CustomInput, Drawer, InfoColourBar, Progressing } from '@devtron-labs/devtron-fe-common-lib'
2+
import React, { useEffect, useRef, useState } from 'react'
3+
import { ReactComponent as Close } from '../../../../assets/icons/ic-cross.svg'
4+
import { ReactComponent as Warn } from '../../../../assets/icons/ic-warning.svg'
5+
import SourceUpdateResponseModal from './SourceUpdateResponseModal'
6+
7+
export default function BulkSourceChange({ closePopup, responseList, changeBranch, loading, selectedAppCount }) {
8+
const sourceChangeDetailRef = useRef<HTMLDivElement>(null)
9+
10+
const [showResponseModal, setShowResponseModal] = useState(false)
11+
const [inputError, setInputError] = useState('')
12+
const [branchName, setBranchName] = useState('')
13+
14+
const closeBulkCIModal = (evt) => {
15+
closePopup(evt)
16+
}
17+
18+
const escKeyPressHandler = (evt): void => {
19+
if (evt && evt.key === 'Escape' && typeof closePopup === 'function') {
20+
evt.preventDefault()
21+
closeBulkCIModal(evt)
22+
}
23+
}
24+
const outsideClickHandler = (evt): void => {
25+
if (
26+
sourceChangeDetailRef.current &&
27+
!sourceChangeDetailRef.current.contains(evt.target) &&
28+
typeof closePopup === 'function'
29+
) {
30+
closeBulkCIModal(evt)
31+
}
32+
}
33+
34+
useEffect(() => {
35+
document.addEventListener('keydown', escKeyPressHandler)
36+
return (): void => {
37+
document.removeEventListener('keydown', escKeyPressHandler)
38+
}
39+
}, [escKeyPressHandler])
40+
41+
useEffect(() => {
42+
document.addEventListener('click', outsideClickHandler)
43+
return (): void => {
44+
document.removeEventListener('click', outsideClickHandler)
45+
}
46+
}, [outsideClickHandler])
47+
48+
useEffect(() => {
49+
setShowResponseModal(responseList.length > 0)
50+
}, [responseList])
51+
52+
const updateBranch = () => {
53+
if (branchName.length === 0) {
54+
setInputError('This is required')
55+
return
56+
}
57+
changeBranch(branchName)
58+
}
59+
60+
const renderHeaderSection = (): JSX.Element | null => {
61+
return (
62+
<div className="flex flex-align-center flex-justify dc__border-bottom bcn-0 pt-16 pr-20 pb-16 pl-20">
63+
<h2 className="fs-16 fw-6 lh-1-43 m-0">Change branch for {selectedAppCount} applications</h2>
64+
<button
65+
type="button"
66+
className="dc__transparent flex icon-dim-24"
67+
disabled={loading}
68+
onClick={closeBulkCIModal}
69+
>
70+
<Close className="icon-dim-24" />
71+
</button>
72+
</div>
73+
)
74+
}
75+
76+
const renderInfoBar = (): JSX.Element => {
77+
return (
78+
<InfoColourBar
79+
message="Branch will be changed only for build pipelines with source type as ‘Branch Fixed’ or ‘Branch Regex’."
80+
classname="warn dc__no-border-radius dc__no-top-border dc__no-left-border dc__no-right-border"
81+
Icon={Warn}
82+
iconClass="warning-icon"
83+
/>
84+
)
85+
}
86+
87+
const checkInput = (): boolean => {
88+
return branchName === ''
89+
}
90+
91+
const handleInputChange = (e): void => {
92+
setBranchName(e.target.value)
93+
setInputError('')
94+
}
95+
96+
const renderForm = (): JSX.Element => {
97+
const isDisabled = checkInput()
98+
99+
return (
100+
<div className="p-20">
101+
<div className="form__row">
102+
<CustomInput
103+
labelClassName="dc__required-field"
104+
autoComplete="off"
105+
name="branch_name"
106+
disabled={false}
107+
value={branchName}
108+
error={inputError}
109+
onChange={handleInputChange}
110+
label="Change to branch"
111+
placeholder="Enter branch name"
112+
/>
113+
</div>
114+
<div className="flexbox">
115+
<button
116+
data-testid="cancel_button"
117+
className="cta cancel h-36 lh-36"
118+
type="button"
119+
onClick={closeBulkCIModal}
120+
>
121+
Cancel
122+
</button>
123+
<button
124+
data-testid="save_cluster_after_entering_cluster_details"
125+
className="cta ml-12 h-36 lh-36"
126+
onClick={updateBranch}
127+
disabled={isDisabled}
128+
>
129+
{loading ? <Progressing /> : 'Update branch'}
130+
</button>
131+
</div>
132+
</div>
133+
)
134+
}
135+
return (
136+
<Drawer position="right" width="75%" minWidth={showResponseModal ? '1024px' : '600px'} maxWidth={showResponseModal ? '1200px' : '600px'}>
137+
<div className="dc__window-bg h-100 bcn-0 bulk-ci-trigger-container" ref={sourceChangeDetailRef}>
138+
{renderHeaderSection()}
139+
{showResponseModal ? (
140+
<SourceUpdateResponseModal closePopup={closePopup} isLoading={false} responseList={responseList} />
141+
) : (
142+
<>
143+
{renderInfoBar()}
144+
{renderForm()}
145+
</>
146+
)}
147+
</div>
148+
</Drawer>
149+
)
150+
}

src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
}
2020
}
2121

22+
.filter-divider {
23+
width: 1px;
24+
height: 20px;
25+
background-color: var(--N200);
26+
margin-left: 12px;
27+
margin-right: 12px;
28+
}
29+
2230
.bulk-ci-trigger-container {
2331
.bulk-ci-trigger {
2432
display: grid;

src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
refreshGitMaterial,
4040
triggerCDNode,
4141
triggerCINode,
42+
triggerBranchChange,
4243
} from '../../../app/service'
4344
import {
4445
createGitCommitUrl,
@@ -47,6 +48,7 @@ import {
4748
preventBodyScroll,
4849
sortObjectArrayAlphabetically,
4950
} from '../../../common'
51+
import { ReactComponent as Pencil } from '../../../../assets/icons/ic-pencil.svg'
5052
import { getWorkflows, getWorkflowStatus } from '../../AppGroup.service'
5153
import { CI_MATERIAL_EMPTY_STATE_MESSAGING, TIME_STAMP_ORDER } from '../../../app/details/triggerView/Constants'
5254
import { toast } from 'react-toastify'
@@ -59,8 +61,8 @@ import {
5961
BulkResponseStatus,
6062
ENV_TRIGGER_VIEW_GA_EVENTS,
6163
BULK_CD_RESPONSE_STATUS_TEXT,
62-
responseListOrder,
6364
BULK_VIRTUAL_RESPONSE_STATUS,
65+
GetBranchChangeStatus,
6466
} from '../../Constants'
6567
import { ReactComponent as DeployIcon } from '../../../../assets/icons/ic-nav-rocket.svg'
6668
import { ReactComponent as Close } from '../../../../assets/icons/ic-cross.svg'
@@ -88,6 +90,7 @@ import Tippy from '@tippyjs/react'
8890
import { getModuleInfo } from '../../../v2/devtronStackManager/DevtronStackManager.service'
8991
import GitCommitInfoGeneric from '../../../common/GitCommitInfoGeneric'
9092
import { getDefaultConfig } from '../../../notifications/notifications.service'
93+
import BulkSourceChange from './BulkSourceChange'
9194

9295
const ApprovalMaterialModal = importComponentFromFELibrary('ApprovalMaterialModal')
9396
const getDeployManifestDownload = importComponentFromFELibrary('getDeployManifestDownload', null, 'function')
@@ -102,6 +105,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
102105
const [pageViewType, setPageViewType] = useState<string>(ViewType.LOADING)
103106
const [isCILoading, setCILoading] = useState(false)
104107
const [isCDLoading, setCDLoading] = useState(false)
108+
const [isBranchChangeLoading, setIsBranchChangeLoading] = useState(false);
105109
const [showPreDeployment, setShowPreDeployment] = useState(false)
106110
const [showPostDeployment, setShowPostDeployment] = useState(false)
107111
const [errorCode, setErrorCode] = useState(0)
@@ -110,6 +114,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
110114
const [showApprovalModal, setShowApprovalModal] = useState(false)
111115
const [showBulkCDModal, setShowBulkCDModal] = useState(false)
112116
const [showBulkCIModal, setShowBulkCIModal] = useState(false)
117+
const [showBulkSourceChangeModal, setShowBulkSourceChangeModal] = useState(false)
113118
const [showWebhookModal, setShowWebhookModal] = useState(false)
114119
const [isWebhookPayloadLoading, setWebhookPayloadLoading] = useState(false)
115120
const [invalidateCache, setInvalidateCache] = useState(false)
@@ -176,6 +181,19 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
176181
})
177182
}
178183

184+
const preserveSelection = (_workflows: WorkflowType[]) => {
185+
if (!workflows || !_workflows) {
186+
return
187+
}
188+
const workflowMap = new Map()
189+
workflows.forEach((wf) => {
190+
workflowMap.set(wf.id, wf.isSelected)
191+
})
192+
_workflows.forEach((wf) => {
193+
wf.isSelected = workflowMap.get(wf.id)
194+
})
195+
}
196+
179197
const getWorkflowsData = async (): Promise<void> => {
180198
try {
181199
const { workflows: _workflows, filteredCIPipelines } = await getWorkflows(envId, filteredAppIds)
@@ -194,6 +212,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
194212
}),
195213
)
196214
}
215+
preserveSelection(_workflows)
197216
setWorkflows(_workflows)
198217
setFilteredCIPipelines(filteredCIPipelines)
199218
setErrorCode(0)
@@ -1032,6 +1051,41 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
10321051
setFilteredWorkflows(_workflows)
10331052
}
10341053

1054+
const changeBranch = (value): void => {
1055+
let appIds = []
1056+
let appNameMap = new Map()
1057+
selectedAppList.map((app) => {
1058+
appIds.push(app.id)
1059+
appNameMap.set(app.id, app.name)
1060+
})
1061+
setIsBranchChangeLoading(true)
1062+
triggerBranchChange(appIds, +envId, value)
1063+
.then((response: any) => {
1064+
console.log(response)
1065+
const _responseList = []
1066+
response.result.apps.map((res) => {
1067+
_responseList.push({
1068+
appId: res.appId,
1069+
appName: appNameMap.get(res.appId),
1070+
statusText: res.status,
1071+
status: GetBranchChangeStatus(res.status),
1072+
envId: +envId,
1073+
message: res.message,
1074+
})
1075+
})
1076+
updateResponseListData(_responseList)
1077+
setCDLoading(false)
1078+
setCILoading(false)
1079+
preventBodyScroll(false)
1080+
})
1081+
.catch((error) => {
1082+
showError(error)
1083+
})
1084+
.finally(() => {
1085+
setIsBranchChangeLoading(false)
1086+
})
1087+
}
1088+
10351089
const selectMaterial = (materialId, pipelineId?: number): void => {
10361090
const _workflows = [...filteredWorkflows].map((workflow) => {
10371091
const nodes = workflow.nodes.map((node) => {
@@ -1295,8 +1349,19 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
12951349
}, 100)
12961350
}
12971351

1298-
const sortResponseList = (a, b) => {
1299-
return responseListOrder[a.status] - responseListOrder[b.status]
1352+
const hideChangeSourceModal = () => {
1353+
if (responseList.length > 0) {
1354+
setPageViewType(ViewType.LOADING)
1355+
inprogressStatusTimer && clearTimeout(inprogressStatusTimer)
1356+
getWorkflowsData()
1357+
}
1358+
setIsBranchChangeLoading(false)
1359+
setShowBulkSourceChangeModal(false)
1360+
setResponseList([])
1361+
}
1362+
1363+
const onShowChangeSourceModal = () => {
1364+
setShowBulkSourceChangeModal(true)
13001365
}
13011366

13021367
const updateBulkCDInputMaterial = (cdMaterialResponse: Record<string, CDMaterialResponseType>): void => {
@@ -1380,7 +1445,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
13801445
setResponseList((prevList) => {
13811446
const resultMap = new Map(_responseList.map((data) => [data.appId, data]))
13821447
const updatedArray = prevList?.map((prevItem) => resultMap.get(prevItem.appId) || prevItem)
1383-
return (updatedArray?.length > 0 ? updatedArray : _responseList).sort(sortResponseList)
1448+
return (updatedArray?.length > 0 ? updatedArray : _responseList).sort((a, b) => sortCallback('appName', a, b))
13841449
})
13851450
}
13861451

@@ -1862,6 +1927,22 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
18621927
)
18631928
}
18641929

1930+
const renderBulkSourchChange = (): JSX.Element | null => {
1931+
if (!showBulkSourceChangeModal) {
1932+
return null
1933+
}
1934+
1935+
return (
1936+
<BulkSourceChange
1937+
closePopup={hideChangeSourceModal}
1938+
responseList={responseList}
1939+
changeBranch={changeBranch}
1940+
loading={isBranchChangeLoading}
1941+
selectedAppCount={selectedAppList.length}
1942+
/>
1943+
)
1944+
}
1945+
18651946
const renderCDMaterial = (): JSX.Element | null => {
18661947
if (showCDModal) {
18671948
let node: NodeAttr, _appID
@@ -2030,6 +2111,18 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
20302111
const _showPopupMenu = showPreDeployment || showPostDeployment
20312112
return (
20322113
<div className="flex dc__min-width-fit-content">
2114+
<button
2115+
className="dc__edit_button h-36 lh-36"
2116+
type="button"
2117+
style={{ marginRight: 'auto' }}
2118+
onClick={onShowChangeSourceModal}
2119+
>
2120+
<span className="flex dc__align-items-center">
2121+
<Pencil className="icon-dim-16 scb-5 mr-4" />
2122+
Change branch
2123+
</span>
2124+
</button>
2125+
<span className="filter-divider"></span>
20332126
<button
20342127
className="cta flex h-36 mr-12"
20352128
data-testid="bulk-build-image-button"
@@ -2157,6 +2250,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
21572250
{renderBulkCDMaterial()}
21582251
{renderBulkCIMaterial()}
21592252
{renderApprovalMaterial()}
2253+
{renderBulkSourchChange()}
21602254
</TriggerViewContext.Provider>
21612255
<div></div>
21622256
</div>

0 commit comments

Comments
 (0)