mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 23:27:14 +00:00
Merge remote-tracking branch 'origin/main' into feat/add-cloudflare-app-connection-and-sync
This commit is contained in:
@@ -89,7 +89,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectSlug: z.string().trim(),
|
projectSlug: z.string().trim(),
|
||||||
authorProjectMembershipId: z.string().trim().optional(),
|
authorUserId: z.string().trim().optional(),
|
||||||
envSlug: z.string().trim().optional()
|
envSlug: z.string().trim().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -143,7 +143,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
|
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
|
||||||
projectSlug: req.query.projectSlug,
|
projectSlug: req.query.projectSlug,
|
||||||
authorProjectMembershipId: req.query.authorProjectMembershipId,
|
authorUserId: req.query.authorUserId,
|
||||||
envSlug: req.query.envSlug,
|
envSlug: req.query.envSlug,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
environment: z.string().trim().optional(),
|
environment: z.string().trim().optional(),
|
||||||
committer: z.string().trim().optional(),
|
committer: z.string().trim().optional(),
|
||||||
|
search: z.string().trim().optional(),
|
||||||
status: z.nativeEnum(RequestState).optional(),
|
status: z.nativeEnum(RequestState).optional(),
|
||||||
limit: z.coerce.number().default(20),
|
limit: z.coerce.number().default(20),
|
||||||
offset: z.coerce.number().default(0)
|
offset: z.coerce.number().default(0)
|
||||||
@@ -66,13 +67,14 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
userId: z.string().nullable().optional()
|
userId: z.string().nullable().optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
}).array()
|
}).array(),
|
||||||
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
|
const { approvals, totalCount } = await server.services.secretApprovalRequest.getSecretApprovals({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@@ -80,7 +82,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
...req.query,
|
...req.query,
|
||||||
projectId: req.query.workspaceId
|
projectId: req.query.workspaceId
|
||||||
});
|
});
|
||||||
return { approvals };
|
return { approvals, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -725,16 +725,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
)
|
)
|
||||||
|
|
||||||
.where(`${TableName.Environment}.projectId`, projectId)
|
.where(`${TableName.Environment}.projectId`, projectId)
|
||||||
.where(`${TableName.AccessApprovalPolicy}.deletedAt`, null)
|
|
||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
||||||
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"));
|
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"))
|
||||||
|
.select(db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"));
|
||||||
|
|
||||||
const formattedRequests = sqlNestRelationships({
|
const formattedRequests = sqlNestRelationships({
|
||||||
data: accessRequests,
|
data: accessRequests,
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: (doc) => ({
|
parentMapper: (doc) => ({
|
||||||
...AccessApprovalRequestsSchema.parse(doc)
|
...AccessApprovalRequestsSchema.parse(doc),
|
||||||
|
isPolicyDeleted: Boolean(doc.policyDeletedAt)
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
@@ -751,7 +752,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
(req) =>
|
(req) =>
|
||||||
!req.privilegeId &&
|
!req.privilegeId &&
|
||||||
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) &&
|
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) &&
|
||||||
req.status === ApprovalStatus.PENDING
|
req.status === ApprovalStatus.PENDING &&
|
||||||
|
!req.isPolicyDeleted
|
||||||
);
|
);
|
||||||
|
|
||||||
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
|
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
|
||||||
@@ -759,7 +761,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
(req) =>
|
(req) =>
|
||||||
req.privilegeId ||
|
req.privilegeId ||
|
||||||
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) ||
|
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) ||
|
||||||
req.status !== ApprovalStatus.PENDING
|
req.status !== ApprovalStatus.PENDING ||
|
||||||
|
req.isPolicyDeleted
|
||||||
);
|
);
|
||||||
|
|
||||||
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
|
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
|
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
authorProjectMembershipId,
|
authorUserId,
|
||||||
envSlug,
|
envSlug,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
@@ -300,8 +300,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||||
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
||||||
|
|
||||||
if (authorProjectMembershipId) {
|
if (authorUserId) {
|
||||||
requests = requests.filter((request) => request.requestedByUserId === actorId);
|
requests = requests.filter((request) => request.requestedByUserId === authorUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (envSlug) {
|
if (envSlug) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export type TCreateAccessApprovalRequestDTO = {
|
|||||||
|
|
||||||
export type TListApprovalRequestsDTO = {
|
export type TListApprovalRequestsDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
authorProjectMembershipId?: string;
|
authorUserId?: string;
|
||||||
envSlug?: string;
|
envSlug?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type TFindQueryFilter = {
|
|||||||
committer?: string;
|
committer?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||||
@@ -314,7 +315,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||||
)
|
)
|
||||||
.andWhere((bd) => void bd.where(`${TableName.SecretApprovalPolicy}.deletedAt`, null))
|
|
||||||
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
||||||
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
||||||
.count("status")
|
.count("status")
|
||||||
@@ -340,13 +340,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findByProjectId = async (
|
const findByProjectId = async (
|
||||||
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
|
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
||||||
// this is the place u wanna look at.
|
// this is the place u wanna look at.
|
||||||
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
||||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
.join(
|
.join(
|
||||||
@@ -435,7 +435,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||||
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||||
)
|
)
|
||||||
.orderBy("createdAt", "desc");
|
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
|
||||||
|
.as("inner");
|
||||||
|
|
||||||
|
const query = (tx || db)
|
||||||
|
.select("*")
|
||||||
|
.select(db.raw("count(*) OVER() as total_count"))
|
||||||
|
.from(innerQuery)
|
||||||
|
.orderBy("createdAt", "desc") as typeof innerQuery;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
void query.where((qb) => {
|
||||||
|
void qb
|
||||||
|
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
|
||||||
|
db.ref("firstName").withSchema("committerUser"),
|
||||||
|
db.ref("lastName").withSchema("committerUser"),
|
||||||
|
`%${search}%`
|
||||||
|
])
|
||||||
|
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
|
||||||
|
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
|
||||||
|
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const docs = await (tx || db)
|
const docs = await (tx || db)
|
||||||
.with("w", query)
|
.with("w", query)
|
||||||
@@ -443,6 +466,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.from<Awaited<typeof query>[number]>("w")
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
.where("w.rank", ">=", offset)
|
.where("w.rank", ">=", offset)
|
||||||
.andWhere("w.rank", "<", offset + limit);
|
.andWhere("w.rank", "<", offset + limit);
|
||||||
|
|
||||||
|
// @ts-expect-error knex does not infer
|
||||||
|
const totalCount = Number(docs[0]?.total_count || 0);
|
||||||
|
|
||||||
const formattedDoc = sqlNestRelationships({
|
const formattedDoc = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "id",
|
key: "id",
|
||||||
@@ -504,23 +531,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
return formattedDoc.map((el) => ({
|
return {
|
||||||
|
approvals: formattedDoc.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
||||||
}));
|
})),
|
||||||
|
totalCount
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindSAR" });
|
throw new DatabaseError({ error, name: "FindSAR" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByProjectIdBridgeSecretV2 = async (
|
const findByProjectIdBridgeSecretV2 = async (
|
||||||
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
|
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
||||||
// this is the place u wanna look at.
|
// this is the place u wanna look at.
|
||||||
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
||||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
.join(
|
.join(
|
||||||
@@ -609,14 +639,42 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||||
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||||
)
|
)
|
||||||
.orderBy("createdAt", "desc");
|
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
|
||||||
|
.as("inner");
|
||||||
|
|
||||||
|
const query = (tx || db)
|
||||||
|
.select("*")
|
||||||
|
.select(db.raw("count(*) OVER() as total_count"))
|
||||||
|
.from(innerQuery)
|
||||||
|
.orderBy("createdAt", "desc") as typeof innerQuery;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
void query.where((qb) => {
|
||||||
|
void qb
|
||||||
|
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
|
||||||
|
db.ref("firstName").withSchema("committerUser"),
|
||||||
|
db.ref("lastName").withSchema("committerUser"),
|
||||||
|
`%${search}%`
|
||||||
|
])
|
||||||
|
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
|
||||||
|
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
|
||||||
|
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankOffset = offset + 1;
|
||||||
const docs = await (tx || db)
|
const docs = await (tx || db)
|
||||||
.with("w", query)
|
.with("w", query)
|
||||||
.select("*")
|
.select("*")
|
||||||
.from<Awaited<typeof query>[number]>("w")
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
.where("w.rank", ">=", offset)
|
.where("w.rank", ">=", rankOffset)
|
||||||
.andWhere("w.rank", "<", offset + limit);
|
.andWhere("w.rank", "<", rankOffset + limit);
|
||||||
|
|
||||||
|
// @ts-expect-error knex does not infer
|
||||||
|
const totalCount = Number(docs[0]?.total_count || 0);
|
||||||
|
|
||||||
const formattedDoc = sqlNestRelationships({
|
const formattedDoc = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "id",
|
key: "id",
|
||||||
@@ -682,10 +740,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
return formattedDoc.map((el) => ({
|
return {
|
||||||
|
approvals: formattedDoc.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
||||||
}));
|
})),
|
||||||
|
totalCount
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindSAR" });
|
throw new DatabaseError({ error, name: "FindSAR" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
environment,
|
environment,
|
||||||
committer,
|
committer,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
search
|
||||||
}: TListApprovalsDTO) => {
|
}: TListApprovalsDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
@@ -208,6 +209,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
if (shouldUseSecretV2Bridge) {
|
if (shouldUseSecretV2Bridge) {
|
||||||
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
|
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
|
||||||
projectId,
|
projectId,
|
||||||
@@ -216,19 +218,21 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
status,
|
status,
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
search
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
|
||||||
|
return secretApprovalRequestDAL.findByProjectId({
|
||||||
projectId,
|
projectId,
|
||||||
committer,
|
committer,
|
||||||
environment,
|
environment,
|
||||||
status,
|
status,
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
search
|
||||||
});
|
});
|
||||||
return approvals;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecretApprovalDetails = async ({
|
const getSecretApprovalDetails = async ({
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export type TListApprovalsDTO = {
|
|||||||
committer?: string;
|
committer?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TSecretApprovalDetailsDTO = {
|
export type TSecretApprovalDetailsDTO = {
|
||||||
|
|||||||
+2247
File diff suppressed because it is too large
Load Diff
-2242
File diff suppressed because it is too large
Load Diff
+7
-46
@@ -1,3 +1,7 @@
|
|||||||
|
* {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
#navbar .max-w-8xl {
|
#navbar .max-w-8xl {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-bottom: 1px solid #ebebeb;
|
border-bottom: 1px solid #ebebeb;
|
||||||
@@ -26,24 +30,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar li > div.mt-2 {
|
#sidebar li > div.mt-2 {
|
||||||
border-radius: 0;
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar li > a.text-primary {
|
#sidebar li > a.text-primary {
|
||||||
border-radius: 0;
|
|
||||||
background-color: #FBFFCC;
|
background-color: #FBFFCC;
|
||||||
border-left: 4px solid #EFFF33;
|
border-left: 4px solid #EFFF33;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar li > a.mt-2 {
|
#sidebar li > a.mt-2 {
|
||||||
border-radius: 0;
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar li > a.leading-6 {
|
#sidebar li > a.leading-6 {
|
||||||
border-radius: 0;
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,65 +68,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#content-area .mt-8 .block{
|
#content-area .mt-8 .block{
|
||||||
border-radius: 0;
|
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
background-color: #FCFBFA;
|
background-color: #FCFBFA;
|
||||||
border-color: #ebebeb;
|
border-color: #ebebeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #content-area:hover .mt-8 .block:hover{
|
/* #content-area:hover .mt-8 .block:hover{
|
||||||
border-radius: 0;
|
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
background-color: #FDFFE5;
|
background-color: #FDFFE5;
|
||||||
border-color: #EFFF33;
|
border-color: #EFFF33;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
#content-area .mt-8 .rounded-xl{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-8 .rounded-lg{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-6 .rounded-xl{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-6 .rounded-lg{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-6 .rounded-md{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-8 .rounded-md{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area div.my-4{
|
#content-area div.my-4{
|
||||||
border-radius: 0;
|
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content-area div.flex-1 {
|
/* #content-area div.flex-1 {
|
||||||
/* text-transform: uppercase; */
|
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
} */
|
||||||
|
|
||||||
#content-area button {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area a {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .not-prose {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .eyebrow {
|
/* .eyebrow {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
||||||
className,
|
className,
|
||||||
isDisabled ? "pointer-events-none opacity-50" : ""
|
isDisabled ? "pointer-events-none cursor-not-allowed opacity-50" : ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
|
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faArrowRightToBracket, faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import { PolicyType } from "@app/hooks/api/policies/enums";
|
import { PolicyType } from "@app/hooks/api/policies/enums";
|
||||||
|
|
||||||
export const policyDetails: Record<PolicyType, { name: string; className: string }> = {
|
export const policyDetails: Record<
|
||||||
|
PolicyType,
|
||||||
|
{ name: string; className: string; icon: IconDefinition }
|
||||||
|
> = {
|
||||||
[PolicyType.AccessPolicy]: {
|
[PolicyType.AccessPolicy]: {
|
||||||
className: "bg-lime-900 text-lime-100",
|
className: "bg-green/20 text-green",
|
||||||
name: "Access Policy"
|
name: "Access Policy",
|
||||||
|
icon: faArrowRightToBracket
|
||||||
},
|
},
|
||||||
[PolicyType.ChangePolicy]: {
|
[PolicyType.ChangePolicy]: {
|
||||||
className: "bg-indigo-900 text-indigo-100",
|
className: "bg-yellow/20 text-yellow",
|
||||||
name: "Change Policy"
|
name: "Change Policy",
|
||||||
|
icon: faEdit
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequests
|
|||||||
const fetchApprovalRequests = async ({
|
const fetchApprovalRequests = async ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
envSlug,
|
envSlug,
|
||||||
authorProjectMembershipId
|
authorUserId
|
||||||
}: TGetAccessApprovalRequestsDTO) => {
|
}: TGetAccessApprovalRequestsDTO) => {
|
||||||
const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
|
const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
|
||||||
"/api/v1/access-approvals/requests",
|
"/api/v1/access-approvals/requests",
|
||||||
{ params: { projectSlug, envSlug, authorProjectMembershipId } }
|
{ params: { projectSlug, envSlug, authorUserId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.requests.map((request) => ({
|
return data.requests.map((request) => ({
|
||||||
@@ -109,12 +109,12 @@ export const useGetAccessRequestsCount = ({
|
|||||||
export const useGetAccessApprovalPolicies = ({
|
export const useGetAccessApprovalPolicies = ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
envSlug,
|
envSlug,
|
||||||
authorProjectMembershipId,
|
authorUserId,
|
||||||
options = {}
|
options = {}
|
||||||
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
|
queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
|
||||||
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }),
|
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorUserId }),
|
||||||
...options,
|
...options,
|
||||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||||
});
|
});
|
||||||
@@ -122,16 +122,13 @@ export const useGetAccessApprovalPolicies = ({
|
|||||||
export const useGetAccessApprovalRequests = ({
|
export const useGetAccessApprovalRequests = ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
envSlug,
|
envSlug,
|
||||||
authorProjectMembershipId,
|
authorUserId,
|
||||||
options = {}
|
options = {}
|
||||||
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(
|
queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, authorUserId),
|
||||||
projectSlug,
|
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorUserId }),
|
||||||
envSlug,
|
|
||||||
authorProjectMembershipId
|
|
||||||
),
|
|
||||||
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }),
|
|
||||||
...options,
|
...options,
|
||||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
enabled: Boolean(projectSlug) && (options?.enabled ?? true),
|
||||||
|
placeholderData: (previousData) => previousData
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export type TCreateAccessRequestDTO = {
|
|||||||
export type TGetAccessApprovalRequestsDTO = {
|
export type TGetAccessApprovalRequestsDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
envSlug?: string;
|
envSlug?: string;
|
||||||
authorProjectMembershipId?: string;
|
authorUserId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetAccessPolicyApprovalCountDTO = {
|
export type TGetAccessPolicyApprovalCountDTO = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
@@ -25,10 +25,11 @@ export const secretApprovalRequestKeys = {
|
|||||||
status,
|
status,
|
||||||
committer,
|
committer,
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit,
|
||||||
|
search
|
||||||
}: TGetSecretApprovalRequestList) =>
|
}: TGetSecretApprovalRequestList) =>
|
||||||
[
|
[
|
||||||
{ workspaceId, environment, status, committer, offset, limit },
|
{ workspaceId, environment, status, committer, offset, limit, search },
|
||||||
"secret-approval-requests"
|
"secret-approval-requests"
|
||||||
] as const,
|
] as const,
|
||||||
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
|
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
|
||||||
@@ -118,23 +119,25 @@ const fetchSecretApprovalRequestList = async ({
|
|||||||
committer,
|
committer,
|
||||||
status = "open",
|
status = "open",
|
||||||
limit = 20,
|
limit = 20,
|
||||||
offset
|
offset = 0,
|
||||||
|
search = ""
|
||||||
}: TGetSecretApprovalRequestList) => {
|
}: TGetSecretApprovalRequestList) => {
|
||||||
const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequest[] }>(
|
const { data } = await apiRequest.get<{
|
||||||
"/api/v1/secret-approval-requests",
|
approvals: TSecretApprovalRequest[];
|
||||||
{
|
totalCount: number;
|
||||||
|
}>("/api/v1/secret-approval-requests", {
|
||||||
params: {
|
params: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
committer,
|
committer,
|
||||||
status,
|
status,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
search
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return data.approvals;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetSecretApprovalRequests = ({
|
export const useGetSecretApprovalRequests = ({
|
||||||
@@ -143,31 +146,32 @@ export const useGetSecretApprovalRequests = ({
|
|||||||
options = {},
|
options = {},
|
||||||
status,
|
status,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
|
offset = 0,
|
||||||
|
search,
|
||||||
committer
|
committer
|
||||||
}: TGetSecretApprovalRequestList & TReactQueryOptions) =>
|
}: TGetSecretApprovalRequestList & TReactQueryOptions) =>
|
||||||
useInfiniteQuery({
|
useQuery({
|
||||||
initialPageParam: 0,
|
|
||||||
queryKey: secretApprovalRequestKeys.list({
|
queryKey: secretApprovalRequestKeys.list({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
committer,
|
committer,
|
||||||
status
|
status,
|
||||||
|
limit,
|
||||||
|
search,
|
||||||
|
offset
|
||||||
}),
|
}),
|
||||||
queryFn: ({ pageParam }) =>
|
queryFn: () =>
|
||||||
fetchSecretApprovalRequestList({
|
fetchSecretApprovalRequestList({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
status,
|
status,
|
||||||
committer,
|
committer,
|
||||||
limit,
|
limit,
|
||||||
offset: pageParam
|
offset,
|
||||||
|
search
|
||||||
}),
|
}),
|
||||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||||
getNextPageParam: (lastPage, pages) => {
|
placeholderData: (previousData) => previousData
|
||||||
if (lastPage.length && lastPage.length < limit) return undefined;
|
|
||||||
|
|
||||||
return lastPage?.length !== 0 ? pages.length * limit : undefined;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchSecretApprovalRequestDetails = async ({
|
const fetchSecretApprovalRequestDetails = async ({
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export type TGetSecretApprovalRequestList = {
|
|||||||
committer?: string;
|
committer?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecretApprovalRequestCount = {
|
export type TGetSecretApprovalRequestCount = {
|
||||||
|
|||||||
@@ -352,9 +352,9 @@ export const ProjectLayout = () => {
|
|||||||
secretApprovalReqCount?.open ||
|
secretApprovalReqCount?.open ||
|
||||||
accessApprovalRequestCount?.pendingCount
|
accessApprovalRequestCount?.pendingCount
|
||||||
) && (
|
) && (
|
||||||
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
|
<Badge variant="primary" className="ml-1.5">
|
||||||
{pendingRequestsCount}
|
{pendingRequestsCount}
|
||||||
</span>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v2/Badge";
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
@@ -45,21 +43,7 @@ export const SecretApprovalsPage = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Approval Workflows"
|
title="Approval Workflows"
|
||||||
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
|
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
|
||||||
Documentation
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faArrowUpRightFromSquare}
|
|
||||||
className="mb-[0.06rem] ml-1 text-xs"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</PageHeader>
|
|
||||||
<Tabs defaultValue={defaultTab}>
|
<Tabs defaultValue={defaultTab}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={TabSection.SecretApprovalRequests}>
|
<Tab value={TabSection.SecretApprovalRequests}>
|
||||||
|
|||||||
+229
-77
@@ -2,15 +2,25 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBan,
|
||||||
|
faBookOpen,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
faClipboardCheck,
|
||||||
faLock,
|
faLock,
|
||||||
faPlus
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faSearch,
|
||||||
|
faStopwatch,
|
||||||
|
faUser,
|
||||||
|
IconDefinition
|
||||||
} 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 { formatDistance } from "date-fns";
|
import { format, formatDistance } from "date-fns";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +31,8 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v2/Badge";
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
@@ -32,7 +44,12 @@ import {
|
|||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
||||||
import {
|
import {
|
||||||
accessApprovalKeys,
|
accessApprovalKeys,
|
||||||
@@ -48,28 +65,21 @@ import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
|
|||||||
import { RequestAccessModal } from "./components/RequestAccessModal";
|
import { RequestAccessModal } from "./components/RequestAccessModal";
|
||||||
import { ReviewAccessRequestModal } from "./components/ReviewAccessModal";
|
import { ReviewAccessRequestModal } from "./components/ReviewAccessModal";
|
||||||
|
|
||||||
const generateRequestText = (request: TAccessApprovalRequest, userId: string) => {
|
const generateRequestText = (request: TAccessApprovalRequest) => {
|
||||||
const { isTemporary } = request;
|
const { isTemporary } = request;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<div>
|
<div>
|
||||||
Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
|
Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
|
||||||
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
<code className="mx-1 rounded bg-mineshaft-600 px-1.5 py-0.5 font-mono text-[13px] text-mineshaft-200">
|
||||||
{request.policy.secretPath}
|
{request.policy.secretPath}
|
||||||
</code>
|
</code>{" "}
|
||||||
in
|
in{" "}
|
||||||
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
<code className="mx-1 rounded bg-mineshaft-600 px-1.5 py-0.5 font-mono text-[13px] text-mineshaft-200">
|
||||||
{request.environmentName}
|
{request.environmentName}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
{request.requestedByUserId === userId && (
|
|
||||||
<span className="text-xs text-gray-500">
|
|
||||||
<Badge className="ml-1">Requested By You</Badge>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -120,30 +130,64 @@ export const AccessApprovalRequest = ({
|
|||||||
projectSlug
|
projectSlug
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: requests, refetch: refetchRequests } = useGetAccessApprovalRequests({
|
const {
|
||||||
|
data: requests,
|
||||||
|
refetch: refetchRequests,
|
||||||
|
isPending: areRequestsPending
|
||||||
|
} = useGetAccessApprovalRequests({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
authorProjectMembershipId: requestedByFilter,
|
authorUserId: requestedByFilter,
|
||||||
envSlug: envFilter
|
envSlug: envFilter
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
|
||||||
|
initPerPage: getUserTablePreference("accessRequestsTable", PreferenceKey.PerPage, 20)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("accessRequestsTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
const filteredRequests = useMemo(() => {
|
const filteredRequests = useMemo(() => {
|
||||||
|
let accessRequests: typeof requests;
|
||||||
|
|
||||||
if (statusFilter === "open")
|
if (statusFilter === "open")
|
||||||
return requests?.filter(
|
accessRequests = requests?.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
!request.policy.deletedAt &&
|
!request.policy.deletedAt &&
|
||||||
!request.isApproved &&
|
!request.isApproved &&
|
||||||
!request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
!request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
||||||
);
|
);
|
||||||
if (statusFilter === "close")
|
if (statusFilter === "close")
|
||||||
return requests?.filter(
|
accessRequests = requests?.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
request.policy.deletedAt ||
|
request.policy.deletedAt ||
|
||||||
request.isApproved ||
|
request.isApproved ||
|
||||||
request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
||||||
);
|
);
|
||||||
|
|
||||||
return requests;
|
return (
|
||||||
}, [requests, statusFilter, requestedByFilter, envFilter]);
|
accessRequests?.filter((request) => {
|
||||||
|
const { environmentName, requestedByUser } = request;
|
||||||
|
|
||||||
|
const searchValue = search.trim().toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
environmentName?.toLowerCase().includes(searchValue) ||
|
||||||
|
`${requestedByUser?.email ?? ""} ${requestedByUser?.firstName ?? ""} ${requestedByUser?.lastName ?? ""}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchValue)
|
||||||
|
);
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
}, [requests, statusFilter, requestedByFilter, envFilter, search]);
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount: filteredRequests.length,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
const generateRequestDetails = useCallback(
|
const generateRequestDetails = useCallback(
|
||||||
(request: TAccessApprovalRequest) => {
|
(request: TAccessApprovalRequest) => {
|
||||||
@@ -162,9 +206,15 @@ export const AccessApprovalRequest = ({
|
|||||||
const canBypass =
|
const canBypass =
|
||||||
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
|
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
|
||||||
|
|
||||||
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
let displayData: {
|
||||||
|
label: string;
|
||||||
|
type: "primary" | "danger" | "success";
|
||||||
|
tooltipContent?: string;
|
||||||
|
icon: IconDefinition | null;
|
||||||
|
} = {
|
||||||
label: "",
|
label: "",
|
||||||
type: "primary"
|
type: "primary",
|
||||||
|
icon: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const isExpired =
|
const isExpired =
|
||||||
@@ -172,20 +222,42 @@ export const AccessApprovalRequest = ({
|
|||||||
request.isApproved &&
|
request.isApproved &&
|
||||||
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
||||||
|
|
||||||
if (isExpired) displayData = { label: "Access Expired", type: "danger" };
|
if (isExpired)
|
||||||
else if (isAccepted) displayData = { label: "Access Granted", type: "success" };
|
displayData = {
|
||||||
else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" };
|
label: "Access Expired",
|
||||||
|
type: "danger",
|
||||||
|
icon: faStopwatch,
|
||||||
|
tooltipContent: request.privilege?.temporaryAccessEndTime
|
||||||
|
? `Expired ${format(request.privilege.temporaryAccessEndTime, "M/d/yyyy h:mm aa")}`
|
||||||
|
: undefined
|
||||||
|
};
|
||||||
|
else if (isAccepted)
|
||||||
|
displayData = {
|
||||||
|
label: "Access Granted",
|
||||||
|
type: "success",
|
||||||
|
icon: faCheck,
|
||||||
|
tooltipContent: `Granted ${format(request.updatedAt, "M/d/yyyy h:mm aa")}`
|
||||||
|
};
|
||||||
|
else if (isRejectedByAnyone)
|
||||||
|
displayData = {
|
||||||
|
label: "Rejected",
|
||||||
|
type: "danger",
|
||||||
|
icon: faBan,
|
||||||
|
tooltipContent: `Rejected ${format(request.updatedAt, "M/d/yyyy h:mm aa")}`
|
||||||
|
};
|
||||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||||
displayData = {
|
displayData = {
|
||||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||||
}`,
|
}`,
|
||||||
type: "primary"
|
type: "primary",
|
||||||
|
icon: faClipboardCheck
|
||||||
};
|
};
|
||||||
} else if (!isReviewedByUser)
|
} else if (!isReviewedByUser)
|
||||||
displayData = {
|
displayData = {
|
||||||
label: "Review Required",
|
label: "Review Required",
|
||||||
type: "primary"
|
type: "primary",
|
||||||
|
icon: faClipboardCheck
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -225,16 +297,42 @@ export const AccessApprovalRequest = ({
|
|||||||
[generateRequestDetails, membersGroupById, user, setSelectedRequest, handlePopUpOpen]
|
[generateRequestDetails, membersGroupById, user, setSelectedRequest, handlePopUpOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFiltered = Boolean(search || envFilter || requestedByFilter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key="approval-changes-list"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
className="rounded-md text-gray-300"
|
||||||
|
>
|
||||||
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6 flex items-end justify-between">
|
<div className="flex items-start gap-1">
|
||||||
<div className="flex flex-col">
|
<p className="text-xl font-semibold text-mineshaft-100">Access Requests</p>
|
||||||
<span className="text-xl font-semibold text-mineshaft-100">Access Requests</span>
|
<a
|
||||||
<div className="mt-2 text-sm text-bunker-300">
|
href="https://infisical.com/docs/documentation/platform/access-controls/access-requests"
|
||||||
Request access to secrets in sensitive environments and folders.
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
<span>Docs</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">
|
||||||
|
Request and review access to secrets in sensitive environments and folders
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="To submit Access Requests, your project needs to create Access Request policies first."
|
content="To submit Access Requests, your project needs to create Access Request policies first."
|
||||||
isDisabled={policiesLoading || !!policies?.length}
|
isDisabled={policiesLoading || !!policies?.length}
|
||||||
@@ -247,25 +345,23 @@ export const AccessApprovalRequest = ({
|
|||||||
}
|
}
|
||||||
handlePopUpOpen("requestAccess");
|
handlePopUpOpen("requestAccess");
|
||||||
}}
|
}}
|
||||||
|
colorSchema="secondary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
isDisabled={policiesLoading || !policies?.length}
|
isDisabled={policiesLoading || !policies?.length}
|
||||||
>
|
>
|
||||||
Request access
|
Request Access
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Input
|
||||||
|
value={search}
|
||||||
<AnimatePresence>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
<motion.div
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
key="approval-changes-list"
|
placeholder="Search approval requests by requesting user or environment..."
|
||||||
transition={{ duration: 0.1 }}
|
className="flex-1"
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
containerClassName="mb-4"
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
/>
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||||
className="rounded-md text-gray-300"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 p-4 px-8">
|
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -273,17 +369,19 @@ export const AccessApprovalRequest = ({
|
|||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
if (evt.key === "Enter") setStatusFilter("open");
|
if (evt.key === "Enter") setStatusFilter("open");
|
||||||
}}
|
}}
|
||||||
className={
|
className={twMerge(
|
||||||
statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
"font-medium",
|
||||||
}
|
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
||||||
{!!requestCount && requestCount?.pendingCount} Pending
|
{!!requestCount && requestCount?.pendingCount} Pending
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={twMerge(
|
||||||
statusFilter === "open" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
"font-medium",
|
||||||
}
|
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||||
|
)}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setStatusFilter("close")}
|
onClick={() => setStatusFilter("close")}
|
||||||
@@ -292,7 +390,7 @@ export const AccessApprovalRequest = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||||
{!!requestCount && requestCount.finalizedCount} Completed
|
{!!requestCount && requestCount.finalizedCount} Closed
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow justify-end space-x-8">
|
<div className="flex flex-grow justify-end space-x-8">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -300,14 +398,20 @@ export const AccessApprovalRequest = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
className="text-bunker-300"
|
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
||||||
>
|
>
|
||||||
Environments
|
Environments
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select an environment</DropdownMenuLabel>
|
align="end"
|
||||||
|
sideOffset={1}
|
||||||
|
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
|
Select an Environment
|
||||||
|
</DropdownMenuLabel>
|
||||||
{currentWorkspace?.environments.map(({ slug, name }) => (
|
{currentWorkspace?.environments.map(({ slug, name }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||||
@@ -337,15 +441,27 @@ export const AccessApprovalRequest = ({
|
|||||||
Requested By
|
Requested By
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
|
align="end"
|
||||||
|
sideOffset={1}
|
||||||
|
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
|
Select Requesting User
|
||||||
|
</DropdownMenuLabel>
|
||||||
{members?.map(({ user: membershipUser, id }) => (
|
{members?.map(({ user: membershipUser, id }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setRequestedByFilter((state) => (state === id ? undefined : id))
|
setRequestedByFilter((state) =>
|
||||||
|
state === membershipUser.id ? undefined : membershipUser.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
key={`request-filter-member-${id}`}
|
key={`request-filter-member-${id}`}
|
||||||
icon={requestedByFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={
|
||||||
|
requestedByFilter === membershipUser.id && (
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} />
|
||||||
|
)
|
||||||
|
}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
>
|
>
|
||||||
{membershipUser.username}
|
{membershipUser.username}
|
||||||
@@ -357,19 +473,26 @@ export const AccessApprovalRequest = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
||||||
{filteredRequests?.length === 0 && (
|
{filteredRequests?.length === 0 && !isFiltered && (
|
||||||
<div className="py-12">
|
<div className="py-12">
|
||||||
<EmptyState title="No more access requests pending." />
|
<EmptyState
|
||||||
|
title={`No ${statusFilter === "open" ? "Pending" : "Closed"} Access Requests`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{Boolean(!filteredRequests?.length && isFiltered && !areRequestsPending) && (
|
||||||
|
<div className="py-12">
|
||||||
|
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!filteredRequests?.length &&
|
{!!filteredRequests?.length &&
|
||||||
filteredRequests?.map((request) => {
|
filteredRequests?.slice(offset, perPage * page).map((request) => {
|
||||||
const details = generateRequestDetails(request);
|
const details = generateRequestDetails(request);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={request.id}
|
key={request.id}
|
||||||
className="flex w-full cursor-pointer px-8 py-4 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
className="flex w-full cursor-pointer border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => handleSelectRequest(request)}
|
onClick={() => handleSelectRequest(request)}
|
||||||
@@ -379,14 +502,18 @@ export const AccessApprovalRequest = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex w-full flex-col justify-between">
|
<div className="flex w-full flex-col justify-between">
|
||||||
<div className="mb-1 flex w-full items-center">
|
<div className="mb-1 flex w-full items-center">
|
||||||
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
<FontAwesomeIcon
|
||||||
{generateRequestText(request, user.id)}
|
icon={faLock}
|
||||||
|
size="xs"
|
||||||
|
className="mr-1.5 text-mineshaft-300"
|
||||||
|
/>
|
||||||
|
{generateRequestText(request)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs leading-3 text-gray-500">
|
||||||
{membersGroupById?.[request.requestedByUserId]?.user && (
|
{membersGroupById?.[request.requestedByUserId]?.user && (
|
||||||
<>
|
<>
|
||||||
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
|
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
|
||||||
@@ -397,21 +524,45 @@ export const AccessApprovalRequest = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{request.requestedByUserId === user.id && (
|
||||||
|
<div className="flex items-center gap-1.5 whitespace-nowrap text-xs text-bunker-300">
|
||||||
|
<FontAwesomeIcon icon={faUser} size="sm" />
|
||||||
|
<span>Requested By You</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Tooltip content={details.displayData.tooltipContent}>
|
||||||
<div>
|
<div>
|
||||||
<Badge variant={details.displayData.type}>
|
<Badge
|
||||||
{details.displayData.label}
|
className="flex items-center gap-1.5 whitespace-nowrap"
|
||||||
|
variant={details.displayData.type}
|
||||||
|
>
|
||||||
|
{details.displayData.icon && (
|
||||||
|
<FontAwesomeIcon icon={details.displayData.icon} />
|
||||||
|
)}
|
||||||
|
<span>{details.displayData.label}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{Boolean(filteredRequests.length) && (
|
||||||
|
<Pagination
|
||||||
|
className="border-none"
|
||||||
|
count={filteredRequests.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{!!policies && (
|
{!!policies && (
|
||||||
<RequestAccessModal
|
<RequestAccessModal
|
||||||
policies={policies}
|
policies={policies}
|
||||||
@@ -452,6 +603,7 @@ export const AccessApprovalRequest = ({
|
|||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+302
-63
@@ -1,11 +1,19 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faChevronDown,
|
|
||||||
faFileShield,
|
faFileShield,
|
||||||
faPlus
|
faFilter,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faSearch
|
||||||
} 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 { AnimatePresence, motion } from "framer-motion";
|
||||||
|
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";
|
||||||
@@ -19,8 +27,9 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
Modal,
|
IconButton,
|
||||||
ModalContent,
|
Input,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -38,7 +47,12 @@ import {
|
|||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useDeleteAccessApprovalPolicy,
|
useDeleteAccessApprovalPolicy,
|
||||||
useDeleteSecretApprovalPolicy,
|
useDeleteSecretApprovalPolicy,
|
||||||
@@ -47,6 +61,7 @@ import {
|
|||||||
useListWorkspaceGroups
|
useListWorkspaceGroups
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
|
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { PolicyType } from "@app/hooks/api/policies/enums";
|
import { PolicyType } from "@app/hooks/api/policies/enums";
|
||||||
import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
|
import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
|
||||||
|
|
||||||
@@ -57,6 +72,18 @@ interface IProps {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PolicyOrderBy {
|
||||||
|
Name = "name",
|
||||||
|
Environment = "environment",
|
||||||
|
SecretPath = "secret-path",
|
||||||
|
Type = "type"
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyFilters = {
|
||||||
|
type: null | PolicyType;
|
||||||
|
environmentIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => {
|
const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => {
|
||||||
const { data: accessPolicies, isPending: isAccessPoliciesLoading } = useGetAccessApprovalPolicies(
|
const { data: accessPolicies, isPending: isAccessPoliciesLoading } = useGetAccessApprovalPolicies(
|
||||||
{
|
{
|
||||||
@@ -112,11 +139,79 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
currentWorkspace
|
currentWorkspace
|
||||||
);
|
);
|
||||||
|
|
||||||
const [filterType, setFilterType] = useState<string | null>(null);
|
const [filters, setFilters] = useState<PolicyFilters>({
|
||||||
|
type: null,
|
||||||
|
environmentIds: []
|
||||||
|
});
|
||||||
|
|
||||||
const filteredPolicies = useMemo(() => {
|
const {
|
||||||
return filterType ? policies.filter((policy) => policy.policyType === filterType) : policies;
|
search,
|
||||||
}, [policies, filterType]);
|
setSearch,
|
||||||
|
setPage,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
offset,
|
||||||
|
orderDirection,
|
||||||
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
setOrderDirection,
|
||||||
|
toggleOrderDirection
|
||||||
|
} = usePagination<PolicyOrderBy>(PolicyOrderBy.Name, {
|
||||||
|
initPerPage: getUserTablePreference("approvalPoliciesTable", PreferenceKey.PerPage, 20)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("approvalPoliciesTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredPolicies = useMemo(
|
||||||
|
() =>
|
||||||
|
policies
|
||||||
|
.filter(({ policyType, environment, name, secretPath }) => {
|
||||||
|
if (filters.type && policyType !== filters.type) return false;
|
||||||
|
|
||||||
|
if (filters.environmentIds.length && !filters.environmentIds.includes(environment.id))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const searchValue = search.trim().toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
name.toLowerCase().includes(searchValue) ||
|
||||||
|
environment.name.toLowerCase().includes(searchValue) ||
|
||||||
|
(secretPath ?? "*").toLowerCase().includes(searchValue)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const [policyOne, policyTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||||
|
|
||||||
|
switch (orderBy) {
|
||||||
|
case PolicyOrderBy.Type:
|
||||||
|
return policyOne.policyType
|
||||||
|
.toLowerCase()
|
||||||
|
.localeCompare(policyTwo.policyType.toLowerCase());
|
||||||
|
case PolicyOrderBy.Environment:
|
||||||
|
return policyOne.environment.name
|
||||||
|
.toLowerCase()
|
||||||
|
.localeCompare(policyTwo.environment.name.toLowerCase());
|
||||||
|
case PolicyOrderBy.SecretPath:
|
||||||
|
return (policyOne.secretPath ?? "*")
|
||||||
|
.toLowerCase()
|
||||||
|
.localeCompare((policyTwo.secretPath ?? "*").toLowerCase());
|
||||||
|
case PolicyOrderBy.Name:
|
||||||
|
default:
|
||||||
|
return policyOne.name.toLowerCase().localeCompare(policyTwo.name.toLowerCase());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[policies, filters, search, orderBy, orderDirection]
|
||||||
|
);
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount: filteredPolicies.length,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
|
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
|
||||||
const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
|
const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
|
||||||
@@ -151,16 +246,57 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isTableFiltered = filters.type !== null || Boolean(filters.environmentIds.length);
|
||||||
|
|
||||||
|
const handleSort = (column: PolicyOrderBy) => {
|
||||||
|
if (column === orderBy) {
|
||||||
|
toggleOrderDirection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrderBy(column);
|
||||||
|
setOrderDirection(OrderByDirection.ASC);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClassName = (col: PolicyOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||||
|
|
||||||
|
const getColSortIcon = (col: PolicyOrderBy) =>
|
||||||
|
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key="approval-changes-list"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
className="rounded-md text-gray-300"
|
||||||
|
>
|
||||||
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6 flex items-end justify-between">
|
<div className="flex items-start gap-1">
|
||||||
<div className="flex flex-col">
|
<p className="text-xl font-semibold text-mineshaft-100">Policies</p>
|
||||||
<span className="text-xl font-semibold text-mineshaft-100">Policies</span>
|
<a
|
||||||
<div className="mt-2 text-sm text-bunker-300">
|
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||||
Implement granular policies for access requests and secrets management.
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
<span>Docs</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">
|
||||||
|
Implement granular policies for access requests and secrets management
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.SecretApproval}
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
@@ -174,6 +310,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
}
|
}
|
||||||
handlePopUpOpen("policyForm");
|
handlePopUpOpen("policyForm");
|
||||||
}}
|
}}
|
||||||
|
colorSchema="secondary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
@@ -182,41 +319,54 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<TableContainer>
|
<Input
|
||||||
<Table>
|
value={search}
|
||||||
<THead>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
<Tr>
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
<Th>Name</Th>
|
placeholder="Search policies by name, type, environment or secret path..."
|
||||||
<Th>Environment</Th>
|
className="flex-1"
|
||||||
<Th>Secret Path</Th>
|
/>
|
||||||
<Th>
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<IconButton
|
||||||
|
ariaLabel="Filter findings"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
size="sm"
|
||||||
className="text-xs font-semibold uppercase text-bunker-300"
|
className={twMerge(
|
||||||
rightIcon={
|
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||||
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
isTableFiltered && "border-primary/50 text-primary"
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
Type
|
<FontAwesomeIcon icon={faFilter} />
|
||||||
</Button>
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select a type</DropdownMenuLabel>
|
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>Policy Type</DropdownMenuLabel>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setFilterType(null)}
|
onClick={() =>
|
||||||
icon={!filterType && <FontAwesomeIcon icon={faCheckCircle} />}
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
type: null
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
icon={!filters && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setFilterType(PolicyType.AccessPolicy)}
|
onClick={() =>
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
type: PolicyType.AccessPolicy
|
||||||
|
}))
|
||||||
|
}
|
||||||
icon={
|
icon={
|
||||||
filterType === PolicyType.AccessPolicy && (
|
filters.type === PolicyType.AccessPolicy && (
|
||||||
<FontAwesomeIcon icon={faCheckCircle} />
|
<FontAwesomeIcon icon={faCheckCircle} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -225,9 +375,14 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
Access Policy
|
Access Policy
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setFilterType(PolicyType.ChangePolicy)}
|
onClick={() =>
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
type: PolicyType.ChangePolicy
|
||||||
|
}))
|
||||||
|
}
|
||||||
icon={
|
icon={
|
||||||
filterType === PolicyType.ChangePolicy && (
|
filters.type === PolicyType.ChangePolicy && (
|
||||||
<FontAwesomeIcon icon={faCheckCircle} />
|
<FontAwesomeIcon icon={faCheckCircle} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -235,25 +390,110 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
>
|
>
|
||||||
Change Policy
|
Change Policy
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuLabel>Environment</DropdownMenuLabel>
|
||||||
|
{currentWorkspace.environments.map((env) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
environmentIds: prev.environmentIds.includes(env.id)
|
||||||
|
? prev.environmentIds.filter((i) => i !== env.id)
|
||||||
|
: [...prev.environmentIds, env.id]
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
key={env.id}
|
||||||
|
icon={
|
||||||
|
filters.environmentIds.includes(env.id) && (
|
||||||
|
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<span className="capitalize">{env.name}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Name
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(PolicyOrderBy.Name)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(PolicyOrderBy.Name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Name)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th />
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Environment
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(PolicyOrderBy.Environment)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(PolicyOrderBy.Environment)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Environment)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Secret Path
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(PolicyOrderBy.SecretPath)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(PolicyOrderBy.SecretPath)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.SecretPath)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Type
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(PolicyOrderBy.Type)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(PolicyOrderBy.Type)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Type)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isPoliciesLoading && (
|
{isPoliciesLoading && (
|
||||||
<TableSkeleton columns={6} innerKey="secret-policies" className="bg-mineshaft-700" />
|
<TableSkeleton
|
||||||
|
columns={5}
|
||||||
|
innerKey="secret-policies"
|
||||||
|
className="bg-mineshaft-700"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{!isPoliciesLoading && !filteredPolicies?.length && (
|
{!isPoliciesLoading && !policies?.length && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={6}>
|
<Td colSpan={5}>
|
||||||
<EmptyState title="No policies found" icon={faFileShield} />
|
<EmptyState title="No Policies Found" icon={faFileShield} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
{!!currentWorkspace &&
|
{!!currentWorkspace &&
|
||||||
filteredPolicies?.map((policy) => (
|
filteredPolicies
|
||||||
|
?.slice(offset, perPage * page)
|
||||||
|
.map((policy) => (
|
||||||
<ApprovalPolicyRow
|
<ApprovalPolicyRow
|
||||||
policy={policy}
|
policy={policy}
|
||||||
key={policy.id}
|
key={policy.id}
|
||||||
@@ -265,20 +505,21 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
))}
|
))}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{Boolean(!filteredPolicies.length && policies.length && !isPoliciesLoading) && (
|
||||||
|
<EmptyState title="No Policies Match Search" icon={faSearch} />
|
||||||
|
)}
|
||||||
|
{Boolean(filteredPolicies.length) && (
|
||||||
|
<Pagination
|
||||||
|
count={filteredPolicies.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<Modal
|
</div>
|
||||||
isOpen={popUp.policyForm.isOpen}
|
</motion.div>
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("policyForm", isOpen)}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
className="max-w-3xl"
|
|
||||||
title={
|
|
||||||
popUp.policyForm.data
|
|
||||||
? `Edit ${popUp?.policyForm?.data?.name || "Policy"}`
|
|
||||||
: "Create Policy"
|
|
||||||
}
|
|
||||||
id="policy-form"
|
|
||||||
>
|
|
||||||
<AccessPolicyForm
|
<AccessPolicyForm
|
||||||
projectId={currentWorkspace.id}
|
projectId={currentWorkspace.id}
|
||||||
projectSlug={currentWorkspace.slug}
|
projectSlug={currentWorkspace.slug}
|
||||||
@@ -287,8 +528,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
members={members}
|
members={members}
|
||||||
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deletePolicy.isOpen}
|
isOpen={popUp.deletePolicy.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
@@ -301,6 +540,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
|
text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
|
||||||
/>
|
/>
|
||||||
</div>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+35
-13
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { RefObject, useMemo, useRef, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -110,20 +112,20 @@ const formSchema = z
|
|||||||
|
|
||||||
type TFormSchema = z.infer<typeof formSchema>;
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const AccessPolicyForm = ({
|
const Form = ({
|
||||||
isOpen,
|
|
||||||
onToggle,
|
onToggle,
|
||||||
members = [],
|
members = [],
|
||||||
projectId,
|
projectId,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
editValues
|
editValues,
|
||||||
}: Props) => {
|
modalContainer,
|
||||||
|
isEditMode
|
||||||
|
}: Props & { modalContainer: RefObject<HTMLDivElement>; isEditMode: boolean }) => {
|
||||||
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
|
||||||
watch,
|
watch,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
@@ -188,13 +190,8 @@ export const AccessPolicyForm = ({
|
|||||||
const { data: groups } = useListWorkspaceGroups(projectId);
|
const { data: groups } = useListWorkspaceGroups(projectId);
|
||||||
|
|
||||||
const environments = currentWorkspace?.environments || [];
|
const environments = currentWorkspace?.environments || [];
|
||||||
const isEditMode = Boolean(editValues);
|
|
||||||
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
|
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen || !isEditMode) reset({});
|
|
||||||
}, [isOpen, isEditMode]);
|
|
||||||
|
|
||||||
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
|
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
|
||||||
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
|
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
|
||||||
|
|
||||||
@@ -387,6 +384,7 @@ export const AccessPolicyForm = ({
|
|||||||
setDraggedItem(null);
|
setDraggedItem(null);
|
||||||
setDragOverItem(null);
|
setDragOverItem(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
@@ -572,7 +570,7 @@ export const AccessPolicyForm = ({
|
|||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
>
|
>
|
||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
menuPortalTarget={document.getElementById("policy-form")}
|
menuPortalTarget={modalContainer.current}
|
||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
isMulti
|
isMulti
|
||||||
placeholder="Select members..."
|
placeholder="Select members..."
|
||||||
@@ -602,7 +600,7 @@ export const AccessPolicyForm = ({
|
|||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
>
|
>
|
||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
menuPortalTarget={document.getElementById("policy-form")}
|
menuPortalTarget={modalContainer.current}
|
||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
isMulti
|
isMulti
|
||||||
placeholder="Select groups..."
|
placeholder="Select groups..."
|
||||||
@@ -813,3 +811,27 @@ export const AccessPolicyForm = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AccessPolicyForm = ({ isOpen, onToggle, editValues, ...props }: Props) => {
|
||||||
|
const modalContainer = useRef<HTMLDivElement>(null);
|
||||||
|
const isEditMode = Boolean(editValues);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-3xl"
|
||||||
|
ref={modalContainer}
|
||||||
|
title={isEditMode ? "Edit Policy" : "Create Policy"}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
{...props}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onToggle={onToggle}
|
||||||
|
editValues={editValues}
|
||||||
|
modalContainer={modalContainer}
|
||||||
|
isEditMode={isEditMode}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+49
-44
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
GenericFieldLabel,
|
||||||
|
IconButton,
|
||||||
Td,
|
Td,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
@@ -80,11 +82,11 @@ export const ApprovalPolicyRow = ({
|
|||||||
userLabels: members
|
userLabels: members
|
||||||
?.filter((member) => el.user.find((i) => i.id === member.user.id))
|
?.filter((member) => el.user.find((i) => i.id === member.user.id))
|
||||||
.map((member) => getMemberLabel(member))
|
.map((member) => getMemberLabel(member))
|
||||||
.join(","),
|
.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)
|
||||||
.join(","),
|
.join(", "),
|
||||||
approvals: el.approvals
|
approvals: el.approvals
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -102,36 +104,47 @@ export const ApprovalPolicyRow = ({
|
|||||||
}}
|
}}
|
||||||
onClick={() => setIsExpanded.toggle()}
|
onClick={() => setIsExpanded.toggle()}
|
||||||
>
|
>
|
||||||
<Td>{policy.name}</Td>
|
<Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td>
|
||||||
<Td>{policy.environment.slug}</Td>
|
<Td>{policy.environment.name}</Td>
|
||||||
<Td>{policy.secretPath || "*"}</Td>
|
<Td>{policy.secretPath || "*"}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Badge className={policyDetails[policy.policyType].className}>
|
<Badge
|
||||||
{policyDetails[policy.policyType].name}
|
className={twMerge(
|
||||||
|
policyDetails[policy.policyType].className,
|
||||||
|
"flex w-min items-center gap-1.5 whitespace-nowrap"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={policyDetails[policy.policyType].icon} />
|
||||||
|
<span>{policyDetails[policy.policyType].name}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
||||||
<div className="flex items-center justify-center transition-transform duration-300 ease-in-out hover:scale-125 hover:text-primary-400 data-[state=open]:scale-125 data-[state=open]:text-primary-400">
|
<DropdownMenuTrigger asChild>
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
<IconButton
|
||||||
</div>
|
ariaLabel="Options"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="w-6"
|
||||||
|
variant="plain"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent sideOffset={2} align="end" className="min-w-[12rem] p-1">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.SecretApproval}
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEdit();
|
onEdit();
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
>
|
>
|
||||||
Edit Policy
|
Edit Policy
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -143,16 +156,12 @@ export const ApprovalPolicyRow = ({
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
|
||||||
isAllowed
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelete();
|
onDelete();
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
>
|
>
|
||||||
Delete Policy
|
Delete Policy
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -162,45 +171,41 @@ export const ApprovalPolicyRow = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
{isExpanded && (
|
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={5} className="rounded bg-mineshaft-900">
|
<Td colSpan={6} className="!border-none p-0">
|
||||||
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
|
<div
|
||||||
|
className={`w-full overflow-hidden bg-mineshaft-900/75 transition-all duration-500 ease-in-out ${
|
||||||
|
isExpanded ? "thin-scrollbar max-h-[26rem] !overflow-y-auto opacity-100" : "max-h-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="mb-4 border-b-2 border-mineshaft-500 pb-2">Approvers</div>
|
||||||
{labels?.map((el, index) => (
|
{labels?.map((el, index) => (
|
||||||
<div
|
<div
|
||||||
key={`approval-list-${index + 1}`}
|
key={`approval-list-${index + 1}`}
|
||||||
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-700 p-4"
|
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-800 p-4"
|
||||||
>
|
>
|
||||||
<div>
|
<div className="my-auto mr-8 flex h-8 w-8 items-center justify-center rounded border border-mineshaft-400 bg-bunker-500/50 text-white">
|
||||||
<div className="mr-8 flex h-8 w-8 items-center justify-center border border-bunker-300 bg-bunker-800 text-white">
|
<div>{index + 1}</div>
|
||||||
<div className="text-lg">{index + 1}</div>
|
|
||||||
</div>
|
</div>
|
||||||
{index !== labels.length - 1 && (
|
{index !== labels.length - 1 && (
|
||||||
<div className="absolute bottom-0 left-8 h-6 border-r border-gray-400" />
|
<div className="absolute bottom-0 left-8 h-[1.25rem] border-r border-mineshaft-400" />
|
||||||
)}
|
)}
|
||||||
{index !== 0 && (
|
{index !== 0 && (
|
||||||
<div className="absolute left-8 top-0 h-4 border-r border-gray-400" />
|
<div className="absolute left-8 top-0 h-[1.25rem] border-r border-mineshaft-400" />
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div className="grid flex-grow grid-cols-3">
|
<div className="grid flex-grow grid-cols-3">
|
||||||
<div>
|
<GenericFieldLabel label="Users">{el.userLabels}</GenericFieldLabel>
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
|
<GenericFieldLabel label="Groups">{el.groupLabels}</GenericFieldLabel>
|
||||||
<div>{el.userLabels || "-"}</div>
|
<GenericFieldLabel label="Approvals Required">{el.approvals}</GenericFieldLabel>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
|
|
||||||
<div>{el.groupLabels || "-"}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
|
|
||||||
<div>{el.approvals || "-"}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+137
-45
@@ -1,14 +1,19 @@
|
|||||||
import { Fragment, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faCodeBranch
|
faCodeBranch,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faSearch
|
||||||
} 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 { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { formatDistance } from "date-fns";
|
import { formatDistance } from "date-fns";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -18,6 +23,8 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
Skeleton
|
Skeleton
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
@@ -28,6 +35,12 @@ import {
|
|||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { usePagination } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetSecretApprovalRequestCount,
|
useGetSecretApprovalRequestCount,
|
||||||
useGetSecretApprovalRequests,
|
useGetSecretApprovalRequests,
|
||||||
@@ -52,18 +65,41 @@ export const SecretApprovalRequest = () => {
|
|||||||
const [usingUrlRequestId, setUsingUrlRequestId] = useState(false);
|
const [usingUrlRequestId, setUsingUrlRequestId] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: secretApprovalRequests,
|
debouncedSearch: debouncedSearchFilter,
|
||||||
isFetchingNextPage: isFetchingNextApprovalRequest,
|
search: searchFilter,
|
||||||
fetchNextPage: fetchNextApprovalRequest,
|
setSearch: setSearchFilter,
|
||||||
hasNextPage: hasNextApprovalPage,
|
setPage,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
} = usePagination("", {
|
||||||
|
initPerPage: getUserTablePreference("changeRequestsTable", PreferenceKey.PerPage, 20)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("changeRequestsTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
isPending: isApprovalRequestLoading,
|
isPending: isApprovalRequestLoading,
|
||||||
refetch
|
refetch
|
||||||
} = useGetSecretApprovalRequests({
|
} = useGetSecretApprovalRequests({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
environment: envFilter,
|
environment: envFilter,
|
||||||
committer: committerFilter
|
committer: committerFilter,
|
||||||
|
search: debouncedSearchFilter,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const totalApprovalCount = data?.totalCount ?? 0;
|
||||||
|
const secretApprovalRequests = data?.approvals ?? [];
|
||||||
|
|
||||||
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
||||||
useGetSecretApprovalRequestCount({ workspaceId });
|
useGetSecretApprovalRequestCount({ workspaceId });
|
||||||
const { user: userSession } = useUser();
|
const { user: userSession } = useUser();
|
||||||
@@ -88,8 +124,9 @@ export const SecretApprovalRequest = () => {
|
|||||||
refetch();
|
refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRequestListEmpty =
|
const isRequestListEmpty = !isApprovalRequestLoading && secretApprovalRequests?.length === 0;
|
||||||
!isApprovalRequestLoading && secretApprovalRequests?.pages[0]?.length === 0;
|
|
||||||
|
const isFiltered = Boolean(searchFilter || envFilter || committerFilter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
@@ -116,7 +153,38 @@ export const SecretApprovalRequest = () => {
|
|||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
className="rounded-md text-gray-300"
|
className="rounded-md text-gray-300"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 p-4 px-8">
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start gap-1">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Change Requests</p>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
<span>Docs</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">Review pending and closed change requests</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search change requests by author, environment or policy path..."
|
||||||
|
className="flex-1"
|
||||||
|
containerClassName="mb-4"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -124,17 +192,19 @@ export const SecretApprovalRequest = () => {
|
|||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
if (evt.key === "Enter") setStatusFilter("open");
|
if (evt.key === "Enter") setStatusFilter("open");
|
||||||
}}
|
}}
|
||||||
className={
|
className={twMerge(
|
||||||
statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
"font-medium",
|
||||||
}
|
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||||
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
|
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={twMerge(
|
||||||
statusFilter === "open" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
"font-medium",
|
||||||
}
|
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||||
|
)}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setStatusFilter("close")}
|
onClick={() => setStatusFilter("close")}
|
||||||
@@ -152,13 +222,21 @@ export const SecretApprovalRequest = () => {
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
className={envFilter ? "text-white" : "text-bunker-300"}
|
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
rightIcon={
|
||||||
|
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Environments
|
Environments
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select an environment</DropdownMenuLabel>
|
align="end"
|
||||||
|
sideOffset={1}
|
||||||
|
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
|
Select an Environment
|
||||||
|
</DropdownMenuLabel>
|
||||||
{currentWorkspace?.environments.map(({ slug, name }) => (
|
{currentWorkspace?.environments.map(({ slug, name }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||||
@@ -188,8 +266,14 @@ export const SecretApprovalRequest = () => {
|
|||||||
Author
|
Author
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
|
align="end"
|
||||||
|
sideOffset={1}
|
||||||
|
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
|
Select an Author
|
||||||
|
</DropdownMenuLabel>
|
||||||
{members?.map(({ user, id }) => (
|
{members?.map(({ user, id }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -210,14 +294,14 @@ export const SecretApprovalRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
||||||
{isRequestListEmpty && (
|
{isRequestListEmpty && !isFiltered && (
|
||||||
<div className="py-12">
|
<div className="py-12">
|
||||||
<EmptyState title="No more requests pending." />
|
<EmptyState
|
||||||
|
title={`No ${statusFilter === "open" ? "Open" : "Closed"} Change Requests`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{secretApprovalRequests?.pages?.map((group, i) => (
|
{secretApprovalRequests.map((secretApproval) => {
|
||||||
<Fragment key={`secret-approval-request-${i + 1}`}>
|
|
||||||
{group?.map((secretApproval) => {
|
|
||||||
const {
|
const {
|
||||||
id: reqId,
|
id: reqId,
|
||||||
commits,
|
commits,
|
||||||
@@ -233,7 +317,7 @@ export const SecretApprovalRequest = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={reqId}
|
key={reqId}
|
||||||
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
|
className="flex flex-col border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setSelectedApprovalId(secretApproval.id)}
|
onClick={() => setSelectedApprovalId(secretApproval.id)}
|
||||||
@@ -241,14 +325,18 @@ export const SecretApprovalRequest = () => {
|
|||||||
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
|
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-1">
|
<div className="mb-1 text-sm">
|
||||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
<FontAwesomeIcon
|
||||||
|
icon={faCodeBranch}
|
||||||
|
size="sm"
|
||||||
|
className="mr-1.5 text-mineshaft-300"
|
||||||
|
/>
|
||||||
{secretApproval.isReplicated
|
{secretApproval.isReplicated
|
||||||
? `${commits.length} secret pending import`
|
? `${commits.length} secret pending import`
|
||||||
: generateCommitText(commits)}
|
: generateCommitText(commits)}
|
||||||
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
|
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs leading-3 text-gray-500">
|
||||||
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
||||||
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
||||||
{committerUser?.email})
|
{committerUser?.email})
|
||||||
@@ -257,9 +345,24 @@ export const SecretApprovalRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Fragment>
|
{Boolean(
|
||||||
))}
|
!secretApprovalRequests.length && isFiltered && !isApprovalRequestLoading
|
||||||
{(isFetchingNextApprovalRequest || isApprovalRequestLoading) && (
|
) && (
|
||||||
|
<div className="py-12">
|
||||||
|
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{Boolean(totalApprovalCount) && (
|
||||||
|
<Pagination
|
||||||
|
className="border-none"
|
||||||
|
count={totalApprovalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isApprovalRequestLoading && (
|
||||||
<div>
|
<div>
|
||||||
{Array.apply(0, Array(3)).map((_x, index) => (
|
{Array.apply(0, Array(3)).map((_x, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -276,18 +379,7 @@ export const SecretApprovalRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{hasNextApprovalPage && (
|
</div>
|
||||||
<Button
|
|
||||||
className="mt-4 text-sm"
|
|
||||||
isFullWidth
|
|
||||||
variant="star"
|
|
||||||
isLoading={isFetchingNextApprovalRequest}
|
|
||||||
isDisabled={isFetchingNextApprovalRequest || !hasNextApprovalPage}
|
|
||||||
onClick={() => fetchNextApprovalRequest()}
|
|
||||||
>
|
|
||||||
{hasNextApprovalPage ? "Load More" : "End of history"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
+7
-10
@@ -56,27 +56,24 @@ export const generateCommitText = (commits: { op: CommitType }[] = [], isReplica
|
|||||||
if (score[CommitType.CREATE])
|
if (score[CommitType.CREATE])
|
||||||
text.push(
|
text.push(
|
||||||
<span key="created-commit">
|
<span key="created-commit">
|
||||||
{score[CommitType.CREATE]} secret{score[CommitType.CREATE] !== 1 && "s"}
|
{score[CommitType.CREATE]} Secret{score[CommitType.CREATE] !== 1 && "s"}
|
||||||
<span style={{ color: "#60DD00" }}> created</span>
|
<span className="text-green-600"> Created</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
if (score[CommitType.UPDATE])
|
if (score[CommitType.UPDATE])
|
||||||
text.push(
|
text.push(
|
||||||
<span key="updated-commit">
|
<span key="updated-commit">
|
||||||
{Boolean(text.length) && ","}
|
{Boolean(text.length) && ", "}
|
||||||
{score[CommitType.UPDATE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
{score[CommitType.UPDATE]} Secret{score[CommitType.UPDATE] !== 1 && "s"}
|
||||||
<span style={{ color: "#F8EB30" }} className="text-orange-600">
|
<span className="text-yellow-600"> Updated</span>
|
||||||
{" "}
|
|
||||||
updated
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
if (score[CommitType.DELETE])
|
if (score[CommitType.DELETE])
|
||||||
text.push(
|
text.push(
|
||||||
<span className="deleted-commit">
|
<span className="deleted-commit">
|
||||||
{Boolean(text.length) && "and"}
|
{Boolean(text.length) && "and"}
|
||||||
{score[CommitType.DELETE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
{score[CommitType.DELETE]} Secret{score[CommitType.DELETE] !== 1 && "s"}
|
||||||
<span style={{ color: "#F83030" }}> deleted</span>
|
<span className="text-red-600"> Deleted</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
return text;
|
return text;
|
||||||
|
|||||||
+8
-2
@@ -37,7 +37,10 @@ const formSchema = z.object({
|
|||||||
policyArns: z.string().trim().optional(),
|
policyArns: z.string().trim().optional(),
|
||||||
tags: z
|
tags: z
|
||||||
.array(
|
.array(
|
||||||
z.object({ key: z.string().trim().min(1).max(128), value: z.string().trim().min(1).max(256) })
|
z.object({
|
||||||
|
key: z.string().trim().min(1).max(128),
|
||||||
|
value: z.string().trim().min(1).max(256)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
}),
|
}),
|
||||||
@@ -52,7 +55,10 @@ const formSchema = z.object({
|
|||||||
policyArns: z.string().trim().optional(),
|
policyArns: z.string().trim().optional(),
|
||||||
tags: z
|
tags: z
|
||||||
.array(
|
.array(
|
||||||
z.object({ key: z.string().trim().min(1).max(128), value: z.string().trim().min(1).max(256) })
|
z.object({
|
||||||
|
key: z.string().trim().min(1).max(128),
|
||||||
|
value: z.string().trim().min(1).max(256)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
|||||||
+3
-4
@@ -26,7 +26,7 @@ const formSchema = z.object({
|
|||||||
policyArns: z.string().trim().optional(),
|
policyArns: z.string().trim().optional(),
|
||||||
tags: z
|
tags: z
|
||||||
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
|
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
|
||||||
.optional(),
|
.optional()
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
|
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
|
||||||
@@ -97,7 +97,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
|||||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||||
inputs: {
|
inputs: {
|
||||||
...(dynamicSecret.inputs as TForm["inputs"])
|
...(dynamicSecret.inputs as TForm["inputs"])
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
|
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
|
||||||
@@ -125,8 +125,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
|||||||
defaultTTL,
|
defaultTTL,
|
||||||
inputs,
|
inputs,
|
||||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||||
usernameTemplate:
|
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||||
!usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
Reference in New Issue
Block a user