Skip to content

Commit 9f9bf5c

Browse files
Merge pull request #260 from devtron-labs/feat/approve-material
feat: approval material
2 parents 3dcaf81 + 93a45fa commit 9f9bf5c

File tree

8 files changed

+212
-17
lines changed

8 files changed

+212
-17
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "0.2.6-beta-4",
3+
"version": "0.2.6-beta-10",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Common/Common.service.ts

Lines changed: 144 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,18 @@ import {
3232
CDMaterialFilterQuery,
3333
ImagePromotionMaterialInfo,
3434
EnvironmentListHelmResponse,
35+
UserGroupApproverType,
36+
ImageApprovalPolicyUserGroupDataType,
37+
ImageApprovalPolicyType,
38+
ImageApprovalUsersInfoDTO,
39+
UserApprovalMetadataType,
40+
UserApprovalConfigType,
41+
CDMaterialListModalServiceUtilProps,
3542
} from './Types'
3643
import { ApiResourceType } from '../Pages'
44+
import { getIsManualApprovalSpecific, sanitizeUserApprovalConfig, stringComparatorBySortOrder } from '@Shared/Helpers'
45+
import { API_TOKEN_PREFIX } from '@Shared/constants'
46+
import { DefaultUserKey } from '@Shared/types'
3747

3848
export const getTeamListMin = (): Promise<TeamList> => {
3949
// ignore active field
@@ -80,7 +90,35 @@ export function setImageTags(request, pipelineId: number, artifactId: number) {
8090
return post(`${ROUTES.IMAGE_TAGGING}/${pipelineId}/${artifactId}`, request)
8191
}
8292

83-
const cdMaterialListModal = (artifacts: any[], offset: number, artifactId?: number, artifactStatus?: string, disableDefaultSelection?: boolean) => {
93+
const sanitizeApprovalConfigFromApprovalMetadata = (
94+
approvalMetadata: UserApprovalMetadataType,
95+
userApprovalConfig: UserApprovalConfigType,
96+
): UserApprovalMetadataType => {
97+
if (!approvalMetadata) {
98+
return null
99+
}
100+
101+
const approvedUsersData = approvalMetadata.approvedUsersData || []
102+
const unsanitizedApprovalConfig = approvalMetadata.approvalConfig || userApprovalConfig
103+
104+
return {
105+
...approvalMetadata,
106+
approvedUsersData: approvedUsersData.map((userData) => ({
107+
...userData,
108+
userGroups: userData.userGroups?.filter((group) => !!group?.identifier && !!group?.name) ?? [],
109+
})),
110+
approvalConfig: sanitizeUserApprovalConfig(unsanitizedApprovalConfig),
111+
}
112+
}
113+
114+
const cdMaterialListModal = ({
115+
artifacts,
116+
offset,
117+
artifactId,
118+
artifactStatus,
119+
disableDefaultSelection,
120+
userApprovalConfig,
121+
}: CDMaterialListModalServiceUtilProps) => {
84122
if (!artifacts || !artifacts.length) return []
85123

86124
const markFirstSelected = offset === 0
@@ -124,7 +162,10 @@ const cdMaterialListModal = (artifacts: any[], offset: number, artifactId?: numb
124162
vulnerable: material.vulnerable,
125163
runningOnParentCd: material.runningOnParentCd,
126164
artifactStatus: artifactStatusValue,
127-
userApprovalMetadata: material.userApprovalMetadata,
165+
userApprovalMetadata: sanitizeApprovalConfigFromApprovalMetadata(
166+
material.userApprovalMetadata,
167+
userApprovalConfig,
168+
),
128169
triggeredBy: material.triggeredBy,
129170
isVirtualEnvironment: material.isVirtualEnvironment,
130171
imageComment: material.imageComment,
@@ -169,19 +210,110 @@ const cdMaterialListModal = (artifacts: any[], offset: number, artifactId?: numb
169210
return materials
170211
}
171212

213+
const getImageApprovalPolicyDetailsFromMaterialResult = (cdMaterialsResult): ImageApprovalPolicyType => {
214+
const approvalUsers: string[] = cdMaterialsResult.approvalUsers || []
215+
const userApprovalConfig = sanitizeUserApprovalConfig(cdMaterialsResult.userApprovalConfig)
216+
const isPolicyConfigured = getIsManualApprovalSpecific(userApprovalConfig)
217+
const imageApprovalUsersInfo: ImageApprovalUsersInfoDTO = cdMaterialsResult.imageApprovalUsersInfo || {}
218+
219+
const approvalUsersMap = approvalUsers.reduce(
220+
(acc, user) => {
221+
acc[user] = true
222+
return acc
223+
},
224+
{} as Record<string, true>,
225+
)
226+
227+
const specificUsersAPIToken = userApprovalConfig.specificUsers.identifiers
228+
.filter((user) => user.startsWith(API_TOKEN_PREFIX))
229+
.sort(stringComparatorBySortOrder)
230+
const specificUsersEmails = userApprovalConfig.specificUsers.identifiers
231+
.filter((user) => !user.startsWith(API_TOKEN_PREFIX) && user !== DefaultUserKey.system)
232+
.sort(stringComparatorBySortOrder)
233+
234+
const specificUsersData: ImageApprovalPolicyType['specificUsersData'] = {
235+
dataStore: userApprovalConfig.specificUsers.identifiers.reduce(
236+
(acc, email) => {
237+
acc[email] = {
238+
email,
239+
hasAccess: approvalUsersMap[email] ?? false,
240+
}
241+
return acc
242+
},
243+
{} as Record<string, UserGroupApproverType>,
244+
),
245+
requiredCount: userApprovalConfig.specificUsers.requiredCount,
246+
emails: specificUsersEmails.concat(specificUsersAPIToken),
247+
}
248+
249+
const validGroups = userApprovalConfig.userGroups.map((group) => group.identifier)
250+
251+
// Have moved from Object.keys(imageApprovalUsersInfo) to approvalUsers since backend is not filtering out the users without approval
252+
// TODO: This check should be on BE. Need to remove this once BE is updated
253+
const usersList = approvalUsers.filter((user) => user !== DefaultUserKey.system)
254+
const groupIdentifierToUsersMap = usersList.reduce(
255+
(acc, user) => {
256+
const userGroups = imageApprovalUsersInfo[user] || []
257+
userGroups.forEach((group) => {
258+
if (!acc[group.identifier]) {
259+
acc[group.identifier] = {}
260+
}
261+
acc[group.identifier][user] = true
262+
})
263+
return acc
264+
},
265+
{} as Record<string, Record<string, true>>,
266+
)
267+
268+
return {
269+
isPolicyConfigured,
270+
specificUsersData,
271+
userGroupData: userApprovalConfig.userGroups.reduce(
272+
(acc, group) => {
273+
const identifier = group.identifier
274+
// No need of handling api tokens here since they are not part of user groups
275+
const users = Object.keys(groupIdentifierToUsersMap[identifier] || {}).sort(stringComparatorBySortOrder)
276+
277+
acc[identifier] = {
278+
dataStore: users.reduce(
279+
(acc, user) => {
280+
acc[user] = {
281+
email: user,
282+
// As of now it will always be true, but UI has handled it in a way that can support false as well
283+
hasAccess: approvalUsersMap[user] ?? false,
284+
}
285+
return acc
286+
},
287+
{} as Record<string, UserGroupApproverType>,
288+
),
289+
requiredCount: group.requiredCount,
290+
emails: users,
291+
}
292+
293+
return acc
294+
},
295+
{} as Record<string, ImageApprovalPolicyUserGroupDataType>,
296+
),
297+
// Not sorting since would change them in approval info modal to name
298+
validGroups,
299+
}
300+
}
301+
172302
const processCDMaterialsApprovalInfo = (enableApproval: boolean, cdMaterialsResult): CDMaterialsApprovalInfo => {
173303
if (!enableApproval || !cdMaterialsResult) {
174304
return {
175305
approvalUsers: [],
176306
userApprovalConfig: null,
177307
canApproverDeploy: cdMaterialsResult?.canApproverDeploy ?? false,
308+
imageApprovalPolicyDetails: null,
178309
}
179310
}
180311

181312
return {
182313
approvalUsers: cdMaterialsResult.approvalUsers,
183-
userApprovalConfig: cdMaterialsResult.userApprovalConfig,
314+
userApprovalConfig: sanitizeUserApprovalConfig(cdMaterialsResult.userApprovalConfig),
184315
canApproverDeploy: cdMaterialsResult.canApproverDeploy ?? false,
316+
imageApprovalPolicyDetails: getImageApprovalPolicyDetailsFromMaterialResult(cdMaterialsResult),
185317
}
186318
}
187319

@@ -237,13 +369,14 @@ export const processCDMaterialServiceResponse = (
237369
}
238370
}
239371

240-
const materials = cdMaterialListModal(
241-
cdMaterialsResult.ci_artifacts,
242-
offset ?? 0,
243-
cdMaterialsResult.latest_wf_artifact_id,
244-
cdMaterialsResult.latest_wf_artifact_status,
372+
const materials = cdMaterialListModal({
373+
artifacts: cdMaterialsResult.ci_artifacts,
374+
offset: offset ?? 0,
375+
artifactId: cdMaterialsResult.latest_wf_artifact_id,
376+
artifactStatus: cdMaterialsResult.latest_wf_artifact_status,
245377
disableDefaultSelection,
246-
)
378+
userApprovalConfig: cdMaterialsResult.userApprovalConfig,
379+
})
247380
const approvalInfo = processCDMaterialsApprovalInfo(
248381
stage === DeploymentNodeType.CD || stage === DeploymentNodeType.APPROVAL,
249382
cdMaterialsResult,
@@ -366,8 +499,8 @@ export const getResourceGroupListRaw = (clusterId: string): Promise<ResponseType
366499
}
367500

368501
export function getNamespaceListMin(clusterIdsCsv: string): Promise<EnvironmentListHelmResponse> {
369-
const URL = `${ROUTES.NAMESPACE}/autocomplete?ids=${clusterIdsCsv}`
370-
return get(URL)
502+
const URL = `${ROUTES.NAMESPACE}/autocomplete?ids=${clusterIdsCsv}`
503+
return get(URL)
371504
}
372505
export function getWebhookEventsForEventId(eventId: string | number) {
373506
const URL = `${ROUTES.GIT_HOST_EVENT}/${eventId}`

src/Common/Types.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,12 +326,36 @@ export enum ManualApprovalType {
326326
notConfigured = 'NOT_CONFIGURED',
327327
}
328328

329+
export interface UserGroupApproverType {
330+
email: string
331+
hasAccess: boolean
332+
}
333+
334+
export interface ImageApprovalPolicyUserGroupDataType {
335+
// Mapping email to data
336+
dataStore: Record<string, UserGroupApproverType>
337+
requiredCount: number
338+
emails: string[]
339+
}
340+
341+
export interface ImageApprovalPolicyType {
342+
isPolicyConfigured: boolean
343+
specificUsersData: ImageApprovalPolicyUserGroupDataType
344+
userGroupData: Record<string, ImageApprovalPolicyUserGroupDataType>
345+
// Assuming name of groups are unique
346+
validGroups: string[]
347+
}
348+
349+
export type ImageApprovalUsersInfoDTO = Record<string, Pick<UserGroupDTO, 'identifier' | 'name'>[]>
350+
329351
// TODO: Need to verify this change for all impacting areas
330352
export interface UserApprovalConfigType {
331353
type: ManualApprovalType
332354
requiredCount: number
333355
specificUsers: {
334356
identifiers: string[]
357+
// FIXME: Remove this ? check later when time permits
358+
requiredCount?: number
335359
}
336360
userGroups: (Pick<UserGroupDTO, 'identifier'> & {
337361
requiredCount: number
@@ -356,13 +380,15 @@ interface ApprovalUserDataType {
356380
userEmail: string
357381
userId: number
358382
userResponse: number
383+
userGroups?: Pick<UserGroupDTO, 'identifier' | 'name'>[]
359384
}
360385

361386
export interface UserApprovalMetadataType {
362387
approvalRequestId: number
363388
approvalRuntimeState: number
364389
approvedUsersData: ApprovalUserDataType[]
365390
requestedUserData: ApprovalUserDataType
391+
approvalConfig?: UserApprovalConfigType
366392
}
367393

368394
export enum FilterStates {
@@ -422,6 +448,15 @@ export interface ArtifactReleaseMappingType {
422448
version: string
423449
}
424450

451+
export interface CDMaterialListModalServiceUtilProps {
452+
artifacts: any[],
453+
offset: number,
454+
artifactId?: number,
455+
artifactStatus?: string,
456+
disableDefaultSelection?: boolean,
457+
userApprovalConfig?: UserApprovalConfigType,
458+
}
459+
425460
export interface CDMaterialType {
426461
index: number
427462
id: string
@@ -643,6 +678,10 @@ export interface CDMaterialsApprovalInfo {
643678
approvalUsers: string[]
644679
userApprovalConfig: UserApprovalConfigType
645680
canApproverDeploy: boolean
681+
/**
682+
* Only available incase of approvals do'nt use in cd materials or any other flow since approvalUsers are not present there
683+
*/
684+
imageApprovalPolicyDetails: ImageApprovalPolicyType
646685
}
647686

648687
export interface CDMaterialsMetaInfo {

src/Pages/GlobalConfigurations/Authorization/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ export interface UserGroupDTO {
7474
usersCount?: number
7575
}
7676

77-
export interface UserGroupType extends Required<Pick<UserGroupDTO, 'description' | 'name' | 'usersCount'>> {
77+
export interface UserGroupType
78+
extends Required<Pick<UserGroupDTO, 'description' | 'name' | 'usersCount' | 'identifier'>> {
7879
/**
7980
* Unique id of the user group
8081
*

src/Shared/Components/CICDHistory/Artifacts.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const CIListItem = ({
7070
return (
7171
<>
7272
{type === 'deployed-artifact' && (
73-
<div className="flex dc__width-inherit">
73+
<div className="flex dc__width-inherit pb-12">
7474
<div className="w-50 text-underline-dashed-300" />
7575
<Down className="icon-dim-16 ml-8 mr-8" style={{ transform: 'rotate(90deg)' }} />
7676
<div className="w-50 text-underline-dashed-300" />

src/Shared/Components/CICDHistory/History.components.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export const GitChanges = ({
161161
) : null
162162
})}
163163
{artifact && (
164-
<div className="history-component__artifact flex left column dc__gap-12">
164+
<div className="history-component__artifact flex left column">
165165
<CIListItem
166166
type="deployed-artifact"
167167
userApprovalMetadata={userApprovalMetadata}

src/Shared/Helpers.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ import Tippy from '@tippyjs/react'
2020
import moment from 'moment'
2121
import {
2222
handleUTCTime,
23+
ManualApprovalType,
2324
mapByKey,
2425
MaterialInfo,
2526
PATTERNS,
2627
shallowEqual,
2728
SortingOrder,
29+
UserApprovalConfigType,
2830
ZERO_TIME_STRING,
2931
} from '../Common'
3032
import {
@@ -760,3 +762,23 @@ export const getFileNameFromHeaders = (headers: Headers) =>
760762
?.find((n) => n.includes('filename='))
761763
?.replace('filename=', '')
762764
.trim()
765+
766+
export const sanitizeUserApprovalConfig = (userApprovalConfig: UserApprovalConfigType): UserApprovalConfigType => ({
767+
requiredCount: userApprovalConfig?.requiredCount ?? 0,
768+
type: userApprovalConfig?.type ?? ManualApprovalType.notConfigured,
769+
specificUsers: {
770+
identifiers: userApprovalConfig?.specificUsers?.identifiers ?? [],
771+
requiredCount: userApprovalConfig?.specificUsers?.identifiers?.length ?? 0,
772+
},
773+
userGroups: userApprovalConfig?.userGroups ?? [],
774+
})
775+
776+
/**
777+
* Manual approval is considered configured only if the type is not notConfigured
778+
*/
779+
export const getIsManualApprovalConfigured = (userApprovalConfig?: Pick<UserApprovalConfigType, 'type'>) =>
780+
// Added null check for backward compatibility
781+
!!userApprovalConfig?.type && userApprovalConfig.type !== ManualApprovalType.notConfigured
782+
783+
export const getIsManualApprovalSpecific = (userApprovalConfig?: Pick<UserApprovalConfigType, 'type'>) =>
784+
getIsManualApprovalConfigured(userApprovalConfig) && userApprovalConfig.type === ManualApprovalType.specific

0 commit comments

Comments
 (0)