mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 05:28:23 +00:00
Merge pull request #4363 from Infisical/daniel/scim-deprovisioning-ui
feat(approvals): visualization of deprovisioned scim users
This commit is contained in:
@@ -133,6 +133,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: z
|
approvers: z
|
||||||
.object({
|
.object({
|
||||||
|
isOrgMembershipActive: z.boolean().nullable().optional(),
|
||||||
userId: z.string().nullable().optional(),
|
userId: z.string().nullable().optional(),
|
||||||
sequence: z.number().nullable().optional(),
|
sequence: z.number().nullable().optional(),
|
||||||
approvalsRequired: z.number().nullable().optional(),
|
approvalsRequired: z.number().nullable().optional(),
|
||||||
@@ -150,6 +151,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
}),
|
}),
|
||||||
reviewers: z
|
reviewers: z
|
||||||
.object({
|
.object({
|
||||||
|
isOrgMembershipActive: z.boolean().nullable().optional(),
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
status: z.string()
|
status: z.string()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -294,12 +294,13 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
approval: SecretApprovalRequestsSchema.merge(
|
approval: SecretApprovalRequestsSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
// secretPath: z.string(),
|
|
||||||
policy: z.object({
|
policy: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: approvalRequestUser.array(),
|
approvers: approvalRequestUser
|
||||||
|
.extend({ isOrgMembershipActive: z.boolean().nullable().optional() })
|
||||||
|
.array(),
|
||||||
bypassers: approvalRequestUser.array(),
|
bypassers: approvalRequestUser.array(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
enforcementLevel: z.string(),
|
enforcementLevel: z.string(),
|
||||||
@@ -309,7 +310,13 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
statusChangedByUser: approvalRequestUser.optional(),
|
statusChangedByUser: approvalRequestUser.optional(),
|
||||||
committerUser: approvalRequestUser.nullish(),
|
committerUser: approvalRequestUser.nullish(),
|
||||||
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
reviewers: approvalRequestUser
|
||||||
|
.extend({
|
||||||
|
status: z.string(),
|
||||||
|
comment: z.string().optional(),
|
||||||
|
isOrgMembershipActive: z.boolean().nullable().optional()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
commits: secretRawSchema
|
commits: secretRawSchema
|
||||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
AccessApprovalRequestsSchema,
|
AccessApprovalRequestsSchema,
|
||||||
TableName,
|
TableName,
|
||||||
TAccessApprovalRequests,
|
TAccessApprovalRequests,
|
||||||
|
TOrgMemberships,
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
TUsers
|
TUsers
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
@@ -144,6 +145,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -151,6 +153,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
bypassers: string[];
|
bypassers: string[];
|
||||||
@@ -202,6 +205,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
reviewers: {
|
reviewers: {
|
||||||
userId: string;
|
userId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}[];
|
}[];
|
||||||
approvers: (
|
approvers: (
|
||||||
| {
|
| {
|
||||||
@@ -210,6 +214,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -217,6 +222,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
bypassers: string[];
|
bypassers: string[];
|
||||||
@@ -288,6 +294,24 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
`requestedByUser.id`
|
`requestedByUser.id`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("approverOrgMembership"),
|
||||||
|
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||||
|
`approverOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("approverGroupOrgMembership"),
|
||||||
|
`${TableName.Users}.id`,
|
||||||
|
`approverGroupOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("reviewerOrgMembership"),
|
||||||
|
`${TableName.AccessApprovalRequestReviewer}.reviewerUserId`,
|
||||||
|
`reviewerOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||||
|
|
||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
@@ -300,6 +324,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
|
db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
|
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
|
||||||
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
|
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
|
||||||
|
|
||||||
|
db.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"),
|
||||||
|
db.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"),
|
||||||
|
db.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"),
|
||||||
db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
|
db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
|
||||||
)
|
)
|
||||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
@@ -396,17 +424,26 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
{
|
{
|
||||||
key: "reviewerUserId",
|
key: "reviewerUserId",
|
||||||
label: "reviewers" as const,
|
label: "reviewers" as const,
|
||||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
mapper: ({ reviewerUserId: userId, reviewerStatus: status, reviewerIsOrgMembershipActive }) =>
|
||||||
|
userId ? { userId, status, isOrgMembershipActive: reviewerIsOrgMembershipActive } : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({
|
mapper: ({
|
||||||
|
approverUserId,
|
||||||
|
approverSequence,
|
||||||
|
approvalsRequired,
|
||||||
|
approverUsername,
|
||||||
|
approverEmail,
|
||||||
|
approverIsOrgMembershipActive
|
||||||
|
}) => ({
|
||||||
userId: approverUserId,
|
userId: approverUserId,
|
||||||
sequence: approverSequence,
|
sequence: approverSequence,
|
||||||
approvalsRequired,
|
approvalsRequired,
|
||||||
email: approverEmail,
|
email: approverEmail,
|
||||||
username: approverUsername
|
username: approverUsername,
|
||||||
|
isOrgMembershipActive: approverIsOrgMembershipActive
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -417,13 +454,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
approverSequence,
|
approverSequence,
|
||||||
approvalsRequired,
|
approvalsRequired,
|
||||||
approverGroupEmail,
|
approverGroupEmail,
|
||||||
approverGroupUsername
|
approverGroupUsername,
|
||||||
|
approverGroupIsOrgMembershipActive
|
||||||
}) => ({
|
}) => ({
|
||||||
userId: approverGroupUserId,
|
userId: approverGroupUserId,
|
||||||
sequence: approverSequence,
|
sequence: approverSequence,
|
||||||
approvalsRequired,
|
approvalsRequired,
|
||||||
email: approverGroupEmail,
|
email: approverGroupEmail,
|
||||||
username: approverGroupUsername
|
username: approverGroupUsername,
|
||||||
|
isOrgMembershipActive: approverGroupIsOrgMembershipActive
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
|
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -94,6 +95,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
bypassers: string[];
|
bypassers: string[];
|
||||||
@@ -145,6 +147,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
reviewers: {
|
reviewers: {
|
||||||
userId: string;
|
userId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}[];
|
}[];
|
||||||
approvers: (
|
approvers: (
|
||||||
| {
|
| {
|
||||||
@@ -153,6 +156,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -160,6 +164,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
bypassers: string[];
|
bypassers: string[];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
|
|||||||
import {
|
import {
|
||||||
SecretApprovalRequestsSchema,
|
SecretApprovalRequestsSchema,
|
||||||
TableName,
|
TableName,
|
||||||
|
TOrgMemberships,
|
||||||
TSecretApprovalRequests,
|
TSecretApprovalRequests,
|
||||||
TSecretApprovalRequestsSecrets,
|
TSecretApprovalRequestsSecrets,
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
@@ -107,11 +108,32 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
|
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
|
||||||
`secretApprovalReviewerUser.id`
|
`secretApprovalReviewerUser.id`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("approverOrgMembership"),
|
||||||
|
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||||
|
`approverOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("approverGroupOrgMembership"),
|
||||||
|
`secretApprovalPolicyGroupApproverUser.id`,
|
||||||
|
`approverGroupOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("reviewerOrgMembership"),
|
||||||
|
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
|
||||||
|
`reviewerOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
tx.ref("userId").withSchema("approverUserGroupMembership").as("approverGroupUserId"),
|
tx.ref("userId").withSchema("approverUserGroupMembership").as("approverGroupUserId"),
|
||||||
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||||
|
tx.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"),
|
||||||
|
tx.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"),
|
||||||
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||||
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
||||||
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||||
@@ -148,6 +170,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
||||||
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
||||||
tx.ref("lastName").withSchema("secretApprovalReviewerUser").as("reviewerLastName"),
|
tx.ref("lastName").withSchema("secretApprovalReviewerUser").as("reviewerLastName"),
|
||||||
|
tx.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"),
|
||||||
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||||
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
||||||
tx.ref("projectId").withSchema(TableName.Environment),
|
tx.ref("projectId").withSchema(TableName.Environment),
|
||||||
@@ -211,9 +234,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
reviewerLastName: lastName,
|
reviewerLastName: lastName,
|
||||||
reviewerUsername: username,
|
reviewerUsername: username,
|
||||||
reviewerFirstName: firstName,
|
reviewerFirstName: firstName,
|
||||||
reviewerComment: comment
|
reviewerComment: comment,
|
||||||
|
reviewerIsOrgMembershipActive: isOrgMembershipActive
|
||||||
}) =>
|
}) =>
|
||||||
userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined
|
userId
|
||||||
|
? {
|
||||||
|
userId,
|
||||||
|
status,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
username,
|
||||||
|
comment: comment ?? "",
|
||||||
|
isOrgMembershipActive
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
@@ -223,13 +258,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
approverEmail: email,
|
approverEmail: email,
|
||||||
approverUsername: username,
|
approverUsername: username,
|
||||||
approverLastName: lastName,
|
approverLastName: lastName,
|
||||||
approverFirstName: firstName
|
approverFirstName: firstName,
|
||||||
|
approverIsOrgMembershipActive: isOrgMembershipActive
|
||||||
}) => ({
|
}) => ({
|
||||||
userId,
|
userId,
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
username
|
username,
|
||||||
|
isOrgMembershipActive
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -240,13 +277,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
approverGroupEmail: email,
|
approverGroupEmail: email,
|
||||||
approverGroupUsername: username,
|
approverGroupUsername: username,
|
||||||
approverGroupLastName: lastName,
|
approverGroupLastName: lastName,
|
||||||
approverGroupFirstName: firstName
|
approverGroupFirstName: firstName,
|
||||||
|
approverGroupIsOrgMembershipActive: isOrgMembershipActive
|
||||||
}) => ({
|
}) => ({
|
||||||
userId,
|
userId,
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
username
|
username,
|
||||||
|
isOrgMembershipActive
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
|
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
|
||||||
|
|
||||||
if (!secretApprovalRequest)
|
if (!secretApprovalRequest)
|
||||||
throw new NotFoundError({ message: `Secret approval request with ID '${id}' not found` });
|
throw new NotFoundError({ message: `Secret approval request with ID '${id}' not found` });
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
firstName: true,
|
firstName: true,
|
||||||
lastName: true,
|
lastName: true,
|
||||||
id: true
|
id: true
|
||||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
})
|
||||||
|
.merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
|
||||||
|
.extend({
|
||||||
|
isOrgMembershipActive: z.boolean()
|
||||||
|
}),
|
||||||
project: SanitizedProjectSchema.pick({ name: true, id: true }),
|
project: SanitizedProjectSchema.pick({ name: true, id: true }),
|
||||||
roles: z.array(
|
roles: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
`${TableName.ProjectRoles}.id`
|
`${TableName.ProjectRoles}.id`
|
||||||
)
|
)
|
||||||
|
.join(TableName.OrgMembership, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||||
db.ref("createdAt").withSchema(TableName.UserGroupMembership),
|
db.ref("createdAt").withSchema(TableName.UserGroupMembership),
|
||||||
@@ -176,7 +177,8 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
|
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
|
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole),
|
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||||
|
db.ref("isActive").withSchema(TableName.OrgMembership)
|
||||||
)
|
)
|
||||||
.where({ isGhost: false });
|
.where({ isGhost: false });
|
||||||
|
|
||||||
@@ -192,7 +194,8 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
projectName,
|
projectName,
|
||||||
createdAt
|
createdAt,
|
||||||
|
isActive
|
||||||
}) => ({
|
}) => ({
|
||||||
isGroupMember: true,
|
isGroupMember: true,
|
||||||
id,
|
id,
|
||||||
@@ -202,7 +205,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
id: projectId,
|
id: projectId,
|
||||||
name: projectName
|
name: projectName
|
||||||
},
|
},
|
||||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost, isOrgMembershipActive: isActive },
|
||||||
createdAt
|
createdAt
|
||||||
}),
|
}),
|
||||||
key: "id",
|
key: "id",
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
|
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.join(TableName.OrgMembership, (qb) => {
|
||||||
|
qb.on(`${TableName.Users}.id`, "=", `${TableName.OrgMembership}.userId`).andOn(
|
||||||
|
`${TableName.OrgMembership}.orgId`,
|
||||||
|
"=",
|
||||||
|
`${TableName.Project}.orgId`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
.where((qb) => {
|
.where((qb) => {
|
||||||
if (filter.usernames) {
|
if (filter.usernames) {
|
||||||
void qb.whereIn("username", filter.usernames);
|
void qb.whereIn("username", filter.usernames);
|
||||||
@@ -90,7 +98,8 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
|
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
|
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||||
|
db.ref("isActive").withSchema(TableName.OrgMembership)
|
||||||
)
|
)
|
||||||
.where({ isGhost: false })
|
.where({ isGhost: false })
|
||||||
.orderBy(`${TableName.Users}.username` as "username");
|
.orderBy(`${TableName.Users}.username` as "username");
|
||||||
@@ -107,12 +116,22 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
projectName,
|
projectName,
|
||||||
createdAt
|
createdAt,
|
||||||
|
isActive
|
||||||
}) => ({
|
}) => ({
|
||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
user: {
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
id: userId,
|
||||||
|
publicKey,
|
||||||
|
isGhost,
|
||||||
|
isOrgMembershipActive: isActive
|
||||||
|
},
|
||||||
project: {
|
project: {
|
||||||
id: projectId,
|
id: projectId,
|
||||||
name: projectName
|
name: projectName
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles });
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles });
|
||||||
|
|
||||||
// projectMembers[0].project
|
|
||||||
if (includeGroupMembers) {
|
if (includeGroupMembers) {
|
||||||
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
|
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
|
||||||
const allMembers = [
|
const allMembers = [
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export type Approver = {
|
|||||||
type: ApproverType;
|
type: ApproverType;
|
||||||
sequence?: number;
|
sequence?: number;
|
||||||
approvalsRequired?: number;
|
approvalsRequired?: number;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Bypasser = {
|
export type Bypasser = {
|
||||||
@@ -82,6 +83,7 @@ export type TAccessApprovalRequest = {
|
|||||||
name: string;
|
name: string;
|
||||||
approvals: number;
|
approvals: number;
|
||||||
approvers: {
|
approvers: {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
sequence?: number;
|
sequence?: number;
|
||||||
approvalsRequired?: number;
|
approvalsRequired?: number;
|
||||||
@@ -98,6 +100,7 @@ export type TAccessApprovalRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
reviewers: {
|
reviewers: {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
status: string;
|
status: string;
|
||||||
}[];
|
}[];
|
||||||
@@ -177,7 +180,7 @@ export type TCreateAccessPolicyDTO = {
|
|||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
environments: string[];
|
environments: string[];
|
||||||
approvers?: Approver[];
|
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||||
bypassers?: Bypasser[];
|
bypassers?: Bypasser[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
@@ -190,7 +193,7 @@ export type TCreateAccessPolicyDTO = {
|
|||||||
export type TUpdateAccessPolicyDTO = {
|
export type TUpdateAccessPolicyDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
approvers?: Approver[];
|
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||||
bypassers?: Bypasser[];
|
bypassers?: Bypasser[];
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
environments?: string[];
|
environments?: string[];
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export enum ApproverType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Approver = {
|
export type Approver = {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
type: ApproverType;
|
type: ApproverType;
|
||||||
};
|
};
|
||||||
@@ -49,7 +50,7 @@ export type TCreateSecretPolicyDTO = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
environments: string[];
|
environments: string[];
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
approvers?: Approver[];
|
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||||
bypassers?: Bypasser[];
|
bypassers?: Bypasser[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
@@ -59,7 +60,7 @@ export type TCreateSecretPolicyDTO = {
|
|||||||
export type TUpdateSecretPolicyDTO = {
|
export type TUpdateSecretPolicyDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
approvers?: Approver[];
|
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||||
bypassers?: Bypasser[];
|
bypassers?: Bypasser[];
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export type TSecretApprovalRequest = {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}[];
|
}[];
|
||||||
workspace: string;
|
workspace: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
@@ -62,6 +63,7 @@ export type TSecretApprovalRequest = {
|
|||||||
status: "open" | "close";
|
status: "open" | "close";
|
||||||
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
|
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
|
||||||
approvers: {
|
approvers: {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export type TProjectMembership = {
|
|||||||
export type TWorkspaceUser = {
|
export type TWorkspaceUser = {
|
||||||
id: string;
|
id: string;
|
||||||
user: {
|
user: {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,6 @@
|
|||||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
|
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
enum EnforceAuthType {
|
enum EnforceAuthType {
|
||||||
SAML = "saml",
|
SAML = "saml",
|
||||||
|
|||||||
+67
-11
@@ -4,7 +4,9 @@ import {
|
|||||||
faCheck,
|
faCheck,
|
||||||
faEdit,
|
faEdit,
|
||||||
faHourglass,
|
faHourglass,
|
||||||
faTriangleExclamation
|
faTriangleExclamation,
|
||||||
|
faUser,
|
||||||
|
faUserSlash
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
@@ -37,7 +39,7 @@ import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
|
|||||||
import { groupBy } from "@app/lib/fn/array";
|
import { groupBy } from "@app/lib/fn/array";
|
||||||
import { EditAccessRequestModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal";
|
import { EditAccessRequestModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal";
|
||||||
|
|
||||||
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
const getReviewedStatusSymbol = (status?: ApprovalStatus, isOrgMembershipActive?: boolean) => {
|
||||||
if (status === ApprovalStatus.APPROVED)
|
if (status === ApprovalStatus.APPROVED)
|
||||||
return (
|
return (
|
||||||
<Badge variant="success" className="flex h-4 items-center justify-center">
|
<Badge variant="success" className="flex h-4 items-center justify-center">
|
||||||
@@ -50,6 +52,17 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
|||||||
<FontAwesomeIcon icon={faBan} size="xs" />
|
<FontAwesomeIcon icon={faBan} size="xs" />
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!isOrgMembershipActive) {
|
||||||
|
return (
|
||||||
|
// Can't do a tooltip here because nested tooltips doesn't work properly as of yet.
|
||||||
|
// TODO(daniel): Fix nested tooltips in the future.
|
||||||
|
|
||||||
|
<Badge className="flex h-4 items-center justify-center bg-mineshaft-400/50 text-bunker-300">
|
||||||
|
<FontAwesomeIcon size="xs" icon={faUserSlash} />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Badge variant="primary" className="flex h-4 items-center justify-center">
|
<Badge variant="primary" className="flex h-4 items-center justify-center">
|
||||||
<FontAwesomeIcon icon={faHourglass} size="xs" />
|
<FontAwesomeIcon icon={faHourglass} size="xs" />
|
||||||
@@ -87,6 +100,7 @@ export const ReviewAccessRequestModal = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
|
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
|
||||||
const [bypassApproval, setBypassApproval] = useState(false);
|
const [bypassApproval, setBypassApproval] = useState(false);
|
||||||
|
|
||||||
const [bypassReason, setBypassReason] = useState("");
|
const [bypassReason, setBypassReason] = useState("");
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { data: groupMemberships = [] } = useListWorkspaceGroups(currentWorkspace?.id || "");
|
const { data: groupMemberships = [] } = useListWorkspaceGroups(currentWorkspace?.id || "");
|
||||||
@@ -192,6 +206,7 @@ export const ReviewAccessRequestModal = ({
|
|||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
if (acc.length && acc[acc.length - 1].sequence === curr.sequence) {
|
if (acc.length && acc[acc.length - 1].sequence === curr.sequence) {
|
||||||
acc[acc.length - 1][curr.type]?.push(curr);
|
acc[acc.length - 1][curr.type]?.push(curr);
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +218,7 @@ export const ReviewAccessRequestModal = ({
|
|||||||
? { user: [curr], group: [], sequence, approvals }
|
? { user: [curr], group: [], sequence, approvals }
|
||||||
: { group: [curr], user: [], sequence, approvals }
|
: { group: [curr], user: [], sequence, approvals }
|
||||||
);
|
);
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
[] as {
|
[] as {
|
||||||
@@ -216,7 +232,10 @@ export const ReviewAccessRequestModal = ({
|
|||||||
const approvers = approversBySequence?.map((approverChain) => {
|
const approvers = approversBySequence?.map((approverChain) => {
|
||||||
const reviewers = request.policy.approvers
|
const reviewers = request.policy.approvers
|
||||||
.filter((el) => (el.sequence || 1) === approverChain.sequence)
|
.filter((el) => (el.sequence || 1) === approverChain.sequence)
|
||||||
.map((el) => ({ ...el, status: reviewesGroupById?.[el.userId]?.[0]?.status }));
|
.map((el) => ({
|
||||||
|
...el,
|
||||||
|
status: reviewesGroupById?.[el.userId]?.[0]?.status
|
||||||
|
}));
|
||||||
const hasApproved =
|
const hasApproved =
|
||||||
reviewers.filter((el) => el.status === "approved").length >=
|
reviewers.filter((el) => el.status === "approved").length >=
|
||||||
(approverChain?.approvals || 1);
|
(approverChain?.approvals || 1);
|
||||||
@@ -410,12 +429,39 @@ export const ReviewAccessRequestModal = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||||
<GenericFieldLabel className="col-span-2" label="Users">
|
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
|
||||||
{approver?.user
|
{Boolean(approver.user.length) && (
|
||||||
?.map(
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
|
{approver?.user?.map((el, idx) => {
|
||||||
)
|
const member = approverSequence?.membersGroupById?.[el.id]?.[0];
|
||||||
.join(", ")}
|
if (!member) return null;
|
||||||
|
|
||||||
|
return member.user.isOrgMembershipActive ? (
|
||||||
|
<div className="flex items-center" key={member.user.id}>
|
||||||
|
<span>{member.user.username}</span>
|
||||||
|
{idx < approver.user.length - 1 && ","}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center" key={member.user.id}>
|
||||||
|
<span className="flex items-center opacity-40">
|
||||||
|
{member.user.username}
|
||||||
|
<span className="text-xs">
|
||||||
|
<Tooltip content="This user has been deactivated and no longer has an active organization membership.">
|
||||||
|
<div>
|
||||||
|
<Badge className="pointer-events-none ml-1 mr-auto flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||||
|
<FontAwesomeIcon icon={faBan} />
|
||||||
|
Inactive
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{idx < approver.user.length - 1 && ","}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
<GenericFieldLabel className="col-span-2" label="Groups">
|
<GenericFieldLabel className="col-span-2" label="Groups">
|
||||||
{approver?.group
|
{approver?.group
|
||||||
@@ -440,8 +486,18 @@ export const ReviewAccessRequestModal = ({
|
|||||||
key={`reviewer-${idx + 1}`}
|
key={`reviewer-${idx + 1}`}
|
||||||
className="flex items-center gap-2 px-2 py-2 text-sm"
|
className="flex items-center gap-2 px-2 py-2 text-sm"
|
||||||
>
|
>
|
||||||
<div className="flex-1">{el.username}</div>
|
<div
|
||||||
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
|
className={twMerge(
|
||||||
|
"flex-1",
|
||||||
|
!el.isOrgMembershipActive && "opacity-40"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{el.username}
|
||||||
|
</div>
|
||||||
|
{getReviewedStatusSymbol(
|
||||||
|
el?.status as ApprovalStatus,
|
||||||
|
el.isOrgMembershipActive
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+35
-8
@@ -43,6 +43,8 @@ import {
|
|||||||
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
|
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
|
||||||
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||||
|
|
||||||
|
import { PolicyMemberOption } from "./PolicyMemberOption";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
onToggle: (isOpen: boolean) => void;
|
onToggle: (isOpen: boolean) => void;
|
||||||
@@ -59,7 +61,11 @@ const formSchema = z
|
|||||||
secretPath: z.string().trim().min(1),
|
secretPath: z.string().trim().min(1),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
userApprovers: z
|
userApprovers: z
|
||||||
.object({ type: z.literal(ApproverType.User), id: z.string() })
|
.object({
|
||||||
|
type: z.literal(ApproverType.User),
|
||||||
|
id: z.string(),
|
||||||
|
isOrgMembershipActive: z.boolean().optional()
|
||||||
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
groupApprovers: z
|
groupApprovers: z
|
||||||
@@ -67,7 +73,11 @@ const formSchema = z
|
|||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
userBypassers: z
|
userBypassers: z
|
||||||
.object({ type: z.literal(BypasserType.User), id: z.string() })
|
.object({
|
||||||
|
type: z.literal(BypasserType.User),
|
||||||
|
id: z.string(),
|
||||||
|
isOrgMembershipActive: z.boolean().optional()
|
||||||
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
groupBypassers: z
|
groupBypassers: z
|
||||||
@@ -80,7 +90,11 @@ const formSchema = z
|
|||||||
sequenceApprovers: z
|
sequenceApprovers: z
|
||||||
.object({
|
.object({
|
||||||
user: z
|
user: z
|
||||||
.object({ type: z.literal(ApproverType.User), id: z.string() })
|
.object({
|
||||||
|
type: z.literal(ApproverType.User),
|
||||||
|
id: z.string(),
|
||||||
|
isOrgMembershipActive: z.boolean().optional()
|
||||||
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
group: z
|
group: z
|
||||||
@@ -139,7 +153,11 @@ const Form = ({
|
|||||||
userApprovers:
|
userApprovers:
|
||||||
editValues?.approvers
|
editValues?.approvers
|
||||||
?.filter((approver) => approver.type === ApproverType.User)
|
?.filter((approver) => approver.type === ApproverType.User)
|
||||||
.map(({ id, type }) => ({ id, type: type as ApproverType.User })) || [],
|
.map(({ id, type, isOrgMembershipActive }) => ({
|
||||||
|
id,
|
||||||
|
type: type as ApproverType.User,
|
||||||
|
isOrgMembershipActive
|
||||||
|
})) || [],
|
||||||
groupApprovers:
|
groupApprovers:
|
||||||
editValues?.approvers
|
editValues?.approvers
|
||||||
?.filter((approver) => approver.type === ApproverType.Group)
|
?.filter((approver) => approver.type === ApproverType.Group)
|
||||||
@@ -235,7 +253,9 @@ const Form = ({
|
|||||||
...data,
|
...data,
|
||||||
approvers: sequenceApprovers?.flatMap((approvers, index) =>
|
approvers: sequenceApprovers?.flatMap((approvers, index) =>
|
||||||
approvers.user
|
approvers.user
|
||||||
.map((el) => ({ ...el, sequence: index + 1 }) as Approver)
|
.map(
|
||||||
|
(el) => ({ ...el, sequence: index + 1 }) as Omit<Approver, "isOrgMembershipActive">
|
||||||
|
)
|
||||||
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
|
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
|
||||||
),
|
),
|
||||||
approvalsRequired: sequenceApprovers?.map((el, index) => ({
|
approvalsRequired: sequenceApprovers?.map((el, index) => ({
|
||||||
@@ -291,7 +311,9 @@ const Form = ({
|
|||||||
...data,
|
...data,
|
||||||
approvers: sequenceApprovers?.flatMap((approvers, index) =>
|
approvers: sequenceApprovers?.flatMap((approvers, index) =>
|
||||||
approvers.user
|
approvers.user
|
||||||
.map((el) => ({ ...el, sequence: index + 1 }) as Approver)
|
.map(
|
||||||
|
(el) => ({ ...el, sequence: index + 1 }) as Omit<Approver, "isOrgMembershipActive">
|
||||||
|
)
|
||||||
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
|
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
|
||||||
),
|
),
|
||||||
approvalsRequired: sequenceApprovers?.map((el, index) => ({
|
approvalsRequired: sequenceApprovers?.map((el, index) => ({
|
||||||
@@ -329,7 +351,8 @@ const Form = ({
|
|||||||
() =>
|
() =>
|
||||||
members.map((member) => ({
|
members.map((member) => ({
|
||||||
id: member.user.id,
|
id: member.user.id,
|
||||||
type: ApproverType.User
|
type: ApproverType.User,
|
||||||
|
isOrgMembershipActive: member.user.isOrgMembershipActive
|
||||||
})),
|
})),
|
||||||
[members]
|
[members]
|
||||||
);
|
);
|
||||||
@@ -347,7 +370,8 @@ const Form = ({
|
|||||||
() =>
|
() =>
|
||||||
members.map((member) => ({
|
members.map((member) => ({
|
||||||
id: member.user.id,
|
id: member.user.id,
|
||||||
type: BypasserType.User
|
type: BypasserType.User,
|
||||||
|
isOrgMembershipActive: member.user.isOrgMembershipActive
|
||||||
})),
|
})),
|
||||||
[members]
|
[members]
|
||||||
);
|
);
|
||||||
@@ -608,6 +632,7 @@ const Form = ({
|
|||||||
isMulti
|
isMulti
|
||||||
placeholder="Select members..."
|
placeholder="Select members..."
|
||||||
options={memberOptions}
|
options={memberOptions}
|
||||||
|
components={{ Option: PolicyMemberOption }}
|
||||||
getOptionValue={(option) => option.id}
|
getOptionValue={(option) => option.id}
|
||||||
getOptionLabel={(option) => {
|
getOptionLabel={(option) => {
|
||||||
const member = members?.find((m) => m.user.id === option.id);
|
const member = members?.find((m) => m.user.id === option.id);
|
||||||
@@ -685,6 +710,7 @@ const Form = ({
|
|||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
isMulti
|
isMulti
|
||||||
placeholder="Select members..."
|
placeholder="Select members..."
|
||||||
|
components={{ Option: PolicyMemberOption }}
|
||||||
options={memberOptions}
|
options={memberOptions}
|
||||||
getOptionValue={(option) => option.id}
|
getOptionValue={(option) => option.id}
|
||||||
getOptionLabel={(option) => {
|
getOptionLabel={(option) => {
|
||||||
@@ -783,6 +809,7 @@ const Form = ({
|
|||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
isMulti
|
isMulti
|
||||||
placeholder="Select members..."
|
placeholder="Select members..."
|
||||||
|
components={{ Option: PolicyMemberOption }}
|
||||||
options={bypasserMemberOptions}
|
options={bypasserMemberOptions}
|
||||||
getOptionValue={(option) => option.id}
|
getOptionValue={(option) => option.id}
|
||||||
getOptionLabel={(option) => {
|
getOptionLabel={(option) => {
|
||||||
|
|||||||
+34
-5
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
|
faBan,
|
||||||
faClipboardCheck,
|
faClipboardCheck,
|
||||||
faEdit,
|
faEdit,
|
||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
GenericFieldLabel,
|
GenericFieldLabel,
|
||||||
IconButton,
|
IconButton,
|
||||||
Td,
|
Td,
|
||||||
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v2/Badge";
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
@@ -86,10 +88,9 @@ export const ApprovalPolicyRow = ({
|
|||||||
return entityInSameSequence?.map((el) => {
|
return entityInSameSequence?.map((el) => {
|
||||||
return {
|
return {
|
||||||
sequence: el.sequence || policy.approvals,
|
sequence: el.sequence || policy.approvals,
|
||||||
userLabels: members
|
|
||||||
?.filter((member) => el.user.find((i) => i.id === member.user.id))
|
users: members.filter((member) => el.user.find((i) => i.id === member.user.id)),
|
||||||
.map((member) => getMemberLabel(member))
|
|
||||||
.join(", "),
|
|
||||||
groupLabels: groups
|
groupLabels: groups
|
||||||
?.filter(({ group }) => el.group.find((i) => i.id === group.id))
|
?.filter(({ group }) => el.group.find((i) => i.id === group.id))
|
||||||
.map(({ group }) => group.name)
|
.map(({ group }) => group.name)
|
||||||
@@ -212,7 +213,35 @@ export const ApprovalPolicyRow = ({
|
|||||||
)}
|
)}
|
||||||
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||||
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
|
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
|
||||||
{el.userLabels}
|
{Boolean(el.users.length) && (
|
||||||
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
|
{el.users.map((u, idx) => {
|
||||||
|
return u.user.isOrgMembershipActive ? (
|
||||||
|
<div className="flex items-center" key={u.id}>
|
||||||
|
<span>{getMemberLabel(u)}</span>
|
||||||
|
{idx < el.users.length - 1 && ","}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center" key={u.id}>
|
||||||
|
<span className="flex items-center opacity-40">
|
||||||
|
{getMemberLabel(u)}
|
||||||
|
<span className="text-xs">
|
||||||
|
<Tooltip content="This user has been deactivated and no longer has an active organization membership.">
|
||||||
|
<div>
|
||||||
|
<Badge className="pointer-events-none ml-1 mr-auto flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||||
|
<FontAwesomeIcon icon={faBan} />
|
||||||
|
Inactive
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{idx < el.users.length - 1 && ","}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
<GenericFieldLabel className="col-span-2" icon={faUserGroup} label="Groups">
|
<GenericFieldLabel className="col-span-2" icon={faUserGroup} label="Groups">
|
||||||
{el.groupLabels}
|
{el.groupLabels}
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
import { components, OptionProps } from "react-select";
|
||||||
|
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Badge } from "@app/components/v2";
|
||||||
|
import { BypasserType } from "@app/hooks/api/accessApproval/types";
|
||||||
|
import { ApproverType } from "@app/hooks/api/secretApproval/types";
|
||||||
|
|
||||||
|
export const PolicyMemberOption = ({
|
||||||
|
isSelected,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: OptionProps<{
|
||||||
|
id: string;
|
||||||
|
type: BypasserType | ApproverType;
|
||||||
|
isOrgMembershipActive?: boolean;
|
||||||
|
}>) => {
|
||||||
|
return (
|
||||||
|
<components.Option isSelected={isSelected} {...props}>
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<p
|
||||||
|
className={twMerge("truncate", !props.data.isOrgMembershipActive && "text-mineshaft-400")}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
{!props.data.isOrgMembershipActive && (
|
||||||
|
<Badge className="pointer-events-none ml-1 mr-auto flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||||
|
<FontAwesomeIcon icon={faBan} />
|
||||||
|
Inactive
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isSelected && (
|
||||||
|
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</components.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
+94
-27
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
@@ -8,7 +9,8 @@ import {
|
|||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
faComment,
|
faComment,
|
||||||
faFolder,
|
faFolder,
|
||||||
faHourglass
|
faHourglass,
|
||||||
|
faUserSlash
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -85,6 +87,7 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
|||||||
return <FontAwesomeIcon icon={faCheck} size="xs" className="text-green" />;
|
return <FontAwesomeIcon icon={faCheck} size="xs" className="text-green" />;
|
||||||
if (status === ApprovalStatus.REJECTED)
|
if (status === ApprovalStatus.REJECTED)
|
||||||
return <FontAwesomeIcon icon={faBan} size="xs" className="text-red" />;
|
return <FontAwesomeIcon icon={faBan} size="xs" className="text-red" />;
|
||||||
|
|
||||||
return <FontAwesomeIcon icon={faHourglass} size="xs" className="text-yellow" />;
|
return <FontAwesomeIcon icon={faHourglass} size="xs" className="text-yellow" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,11 +165,15 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
secretApprovalRequestDetails.policy.bypassers.some(({ userId }) => userId === userSession.id);
|
secretApprovalRequestDetails.policy.bypassers.some(({ userId }) => userId === userSession.id);
|
||||||
|
|
||||||
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||||
Record<string, { status: ApprovalStatus; comment: string }>
|
Record<string, { status: ApprovalStatus; comment: string; isOrgMembershipActive: boolean }>
|
||||||
>(
|
>(
|
||||||
(prev, curr) => ({
|
(prev, curr) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[curr.userId]: { status: curr.status, comment: curr.comment }
|
[curr.userId]: {
|
||||||
|
status: curr.status,
|
||||||
|
comment: curr.comment,
|
||||||
|
isOrgMembershipActive: curr.isOrgMembershipActive
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
@@ -533,26 +540,44 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
)
|
)
|
||||||
.map((requiredApprover) => {
|
.map((requiredApprover) => {
|
||||||
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||||
|
const { isOrgMembershipActive } = requiredApprover;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-nowrap items-center justify-between space-x-2 rounded border border-mineshaft-600 bg-mineshaft-800 px-2 py-1"
|
className="flex flex-nowrap items-center justify-between space-x-2 rounded border border-mineshaft-600 bg-mineshaft-800 px-2 py-1"
|
||||||
key={`required-approver-${requiredApprover.userId}`}
|
key={`required-approver-${requiredApprover.userId}`}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<div
|
||||||
content={
|
className={twMerge(
|
||||||
requiredApprover.firstName
|
"flex items-center gap-1 text-sm",
|
||||||
? `${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""}`
|
!isOrgMembershipActive && "opacity-40"
|
||||||
: undefined
|
)}
|
||||||
}
|
|
||||||
position="left"
|
|
||||||
sideOffset={10}
|
|
||||||
>
|
>
|
||||||
<div className="flex text-sm">
|
<Tooltip
|
||||||
<div>{requiredApprover?.email}</div>
|
content={
|
||||||
<span className="text-red">*</span>
|
!isOrgMembershipActive
|
||||||
</div>
|
? "This user has been deactivated and no longer has an active organization membership."
|
||||||
</Tooltip>
|
: requiredApprover.firstName
|
||||||
<div>
|
? `${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
position="left"
|
||||||
|
sideOffset={10}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div>{requiredApprover?.email}</div>
|
||||||
|
<span className="text-red">*</span>
|
||||||
|
{!isOrgMembershipActive && (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUserSlash}
|
||||||
|
size="xs"
|
||||||
|
className="ml-1 text-mineshaft-300"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
{reviewer?.comment && (
|
{reviewer?.comment && (
|
||||||
<Tooltip className="max-w-lg break-words" content={reviewer.comment}>
|
<Tooltip className="max-w-lg break-words" content={reviewer.comment}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
@@ -562,9 +587,21 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={`Status: ${reviewer?.status || ApprovalStatus.PENDING}`}>
|
<div className="flex gap-2">
|
||||||
{getReviewedStatusSymbol(reviewer?.status)}
|
<Tooltip
|
||||||
</Tooltip>
|
className="relative !z-[500]"
|
||||||
|
content={
|
||||||
|
<span className="text-sm">
|
||||||
|
Status:{" "}
|
||||||
|
<span className="capitalize">
|
||||||
|
{reviewer?.status || ApprovalStatus.PENDING}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{getReviewedStatusSymbol(reviewer?.status)}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -578,20 +615,42 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
)
|
)
|
||||||
.map((reviewer) => {
|
.map((reviewer) => {
|
||||||
const status = reviewedUsers?.[reviewer.userId].status;
|
const status = reviewedUsers?.[reviewer.userId].status;
|
||||||
|
const { isOrgMembershipActive } = reviewer;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
className="flex flex-nowrap items-center justify-between space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||||
key={`required-approver-${reviewer.userId}`}
|
key={`required-approver-${reviewer.userId}`}
|
||||||
>
|
>
|
||||||
<div className="flex-grow text-sm">
|
<div
|
||||||
<Tooltip content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}>
|
className={twMerge(
|
||||||
<span>{reviewer?.email} </span>
|
"flex items-center gap-1 text-sm",
|
||||||
|
!isOrgMembershipActive && "opacity-40"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
className="relative !z-[500]"
|
||||||
|
content={
|
||||||
|
!isOrgMembershipActive
|
||||||
|
? "This user has been deactivated and no longer has an active organization membership."
|
||||||
|
: `${reviewer.firstName || ""} ${reviewer.lastName || ""}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span>{reviewer?.email} </span>
|
||||||
|
{!isOrgMembershipActive && (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUserSlash}
|
||||||
|
size="xs"
|
||||||
|
className="ml-1 text-mineshaft-300"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="text-red">*</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{reviewer.comment && (
|
{reviewer.comment && (
|
||||||
<Tooltip content={reviewer.comment}>
|
<Tooltip className="relative !z-[500]" content={reviewer.comment}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faComment}
|
icon={faComment}
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -599,7 +658,15 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={status || ApprovalStatus.PENDING}>
|
<Tooltip
|
||||||
|
className="relative !z-[500]"
|
||||||
|
content={
|
||||||
|
<span className="text-sm">
|
||||||
|
Status:{" "}
|
||||||
|
<span className="capitalize">{status || ApprovalStatus.PENDING}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
{getReviewedStatusSymbol(status)}
|
{getReviewedStatusSymbol(status)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user