feat(approvals): visualization of deprovisioned scim users

This commit is contained in:
Daniel Hougaard
2025-08-13 02:06:01 +04:00
parent b464941fbc
commit 2501c57030
18 changed files with 332 additions and 75 deletions
@@ -35,6 +35,7 @@ export type Approver = {
type: ApproverType;
sequence?: number;
approvalsRequired?: number;
isOrgMembershipActive: boolean;
};
export type Bypasser = {
@@ -81,6 +82,7 @@ export type TAccessApprovalRequest = {
name: string;
approvals: number;
approvers: {
isOrgMembershipActive: boolean;
userId: string;
sequence?: number;
approvalsRequired?: number;
@@ -96,6 +98,7 @@ export type TAccessApprovalRequest = {
};
reviewers: {
isOrgMembershipActive: boolean;
userId: string;
status: string;
}[];
@@ -166,7 +169,7 @@ export type TCreateAccessPolicyDTO = {
projectSlug: string;
name?: string;
environments: string[];
approvers?: Approver[];
approvers?: Omit<Approver, "isOrgMembershipActive">[];
bypassers?: Bypasser[];
approvals?: number;
secretPath: string;
@@ -178,7 +181,7 @@ export type TCreateAccessPolicyDTO = {
export type TUpdateAccessPolicyDTO = {
id: string;
name?: string;
approvers?: Approver[];
approvers?: Omit<Approver, "isOrgMembershipActive">[];
bypassers?: Bypasser[];
secretPath?: string;
environments?: string[];
@@ -20,6 +20,7 @@ export enum ApproverType {
}
export type Approver = {
isOrgMembershipActive: boolean;
id: string;
type: ApproverType;
};
@@ -49,7 +50,7 @@ export type TCreateSecretPolicyDTO = {
name?: string;
environments: string[];
secretPath: string;
approvers?: Approver[];
approvers?: Omit<Approver, "isOrgMembershipActive">[];
bypassers?: Bypasser[];
approvals?: number;
enforcementLevel: EnforcementLevel;
@@ -59,7 +60,7 @@ export type TCreateSecretPolicyDTO = {
export type TUpdateSecretPolicyDTO = {
id: string;
name?: string;
approvers?: Approver[];
approvers?: Omit<Approver, "isOrgMembershipActive">[];
bypassers?: Bypasser[];
secretPath?: string;
approvals?: number;
@@ -53,6 +53,7 @@ export type TSecretApprovalRequest = {
firstName: string;
lastName: string;
username: string;
isOrgMembershipActive: boolean;
}[];
workspace: string;
environment: string;
@@ -62,6 +63,7 @@ export type TSecretApprovalRequest = {
status: "open" | "close";
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
approvers: {
isOrgMembershipActive: boolean;
userId: string;
email: string;
firstName: string;
+1
View File
@@ -83,6 +83,7 @@ export type TProjectMembership = {
export type TWorkspaceUser = {
id: string;
user: {
isOrgMembershipActive: boolean;
email: string;
username: string;
firstName: string;
@@ -3,7 +3,10 @@ import {
faBan,
faCheck,
faHourglass,
faTriangleExclamation
faQuestionCircle,
faTriangleExclamation,
faUser,
faUserSlash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms";
@@ -33,7 +36,7 @@ import { EnforcementLevel } from "@app/hooks/api/policies/enums";
import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
import { groupBy } from "@app/lib/fn/array";
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
const getReviewedStatusSymbol = (status?: ApprovalStatus, isOrgMembershipActive?: boolean) => {
if (status === ApprovalStatus.APPROVED)
return (
<Badge variant="success" className="flex h-4 items-center justify-center">
@@ -46,6 +49,16 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
<FontAwesomeIcon icon={faBan} size="xs" />
</Badge>
);
if (!isOrgMembershipActive) {
return (
// Can't do a tooltip here because nested tooltips doesn't work properly as of yet.
// TODO(daniel): Fix nested tooltips in the future.
<Badge variant="danger" className="flex h-4 items-center justify-center">
<FontAwesomeIcon icon={faUserSlash} size="xs" />
</Badge>
);
}
return (
<Badge variant="primary" className="flex h-4 items-center justify-center">
<FontAwesomeIcon icon={faHourglass} size="xs" />
@@ -81,6 +94,7 @@ export const ReviewAccessRequestModal = ({
}) => {
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
const [bypassApproval, setBypassApproval] = useState(false);
const [bypassReason, setBypassReason] = useState("");
const { currentWorkspace } = useWorkspace();
const { data: groupMemberships = [] } = useListWorkspaceGroups(currentWorkspace?.id || "");
@@ -215,7 +229,10 @@ export const ReviewAccessRequestModal = ({
const approvers = approversBySequence?.map((approverChain) => {
const reviewers = request.policy.approvers
.filter((el) => (el.sequence || 1) === approverChain.sequence)
.map((el) => ({ ...el, status: reviewesGroupById?.[el.userId]?.[0]?.status }));
.map((el) => ({
...el,
status: reviewesGroupById?.[el.userId]?.[0]?.status
}));
const hasApproved =
reviewers.filter((el) => el.status === "approved").length >=
(approverChain?.approvals || 1);
@@ -383,12 +400,31 @@ export const ReviewAccessRequestModal = ({
</div>
)}
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
<GenericFieldLabel className="col-span-2" label="Users">
{approver?.user
?.map(
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
)
.join(", ")}
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
{Boolean(approver.user.length) && (
<div className="flex flex-row flex-wrap gap-2">
{approver?.user?.map((el) => {
const member = approverSequence?.membersGroupById?.[el.id]?.[0];
if (!member) return null;
return member.user.isOrgMembershipActive ? (
<span key={el.id}>{member.user.username}</span>
) : (
<span className="opacity-40" key={el.id}>
{member.user.username}{" "}
<span className="text-xs">
<Tooltip content="This user has been deactivated and no longer has an active organization membership.">
<div>
(Inactive){" "}
<FontAwesomeIcon size="xs" icon={faQuestionCircle} />
</div>
</Tooltip>
</span>
</span>
);
})}
</div>
)}
</GenericFieldLabel>
<GenericFieldLabel className="col-span-2" label="Groups">
{approver?.group
@@ -413,8 +449,18 @@ export const ReviewAccessRequestModal = ({
key={`reviewer-${idx + 1}`}
className="flex items-center gap-2 px-2 py-2 text-sm"
>
<div className="flex-1">{el.username}</div>
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
<div
className={twMerge(
"flex-1",
!el.isOrgMembershipActive && "opacity-40"
)}
>
{el.username}
</div>
{getReviewedStatusSymbol(
el?.status as ApprovalStatus,
el.isOrgMembershipActive
)}
</div>
))}
</div>
@@ -3,6 +3,7 @@ import {
faClipboardCheck,
faEdit,
faEllipsisV,
faQuestionCircle,
faTrash,
faUser,
faUserGroup
@@ -19,6 +20,7 @@ import {
GenericFieldLabel,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge";
@@ -86,10 +88,9 @@ export const ApprovalPolicyRow = ({
return entityInSameSequence?.map((el) => {
return {
sequence: el.sequence || policy.approvals,
userLabels: members
?.filter((member) => el.user.find((i) => i.id === member.user.id))
.map((member) => getMemberLabel(member))
.join(", "),
users: members.filter((member) => el.user.find((i) => i.id === member.user.id)),
groupLabels: groups
?.filter(({ group }) => el.group.find((i) => i.id === group.id))
.map(({ group }) => group.name)
@@ -212,7 +213,27 @@ export const ApprovalPolicyRow = ({
)}
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
{el.userLabels}
{Boolean(el.users.length) && (
<div className="flex flex-row flex-wrap gap-2">
{el.users.map((u, i) => {
return u.user.isOrgMembershipActive ? (
<span key={u.id}>{getMemberLabel(u)}</span>
) : (
<span className="opacity-40" key={u.id}>
{getMemberLabel(u)}{" "}
<span className="text-xs">
<Tooltip content="This user has been deactivated and no longer has an active organization membership.">
<div>
(Inactive){" "}
<FontAwesomeIcon size="xs" icon={faQuestionCircle} />
</div>
</Tooltip>
</span>
</span>
);
})}
</div>
)}
</GenericFieldLabel>
<GenericFieldLabel className="col-span-2" icon={faUserGroup} label="Groups">
{el.groupLabels}
@@ -8,7 +8,8 @@ import {
faCodeBranch,
faComment,
faFolder,
faHourglass
faHourglass,
faUserSlash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -85,6 +86,7 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
return <FontAwesomeIcon icon={faCheck} size="xs" className="text-green" />;
if (status === ApprovalStatus.REJECTED)
return <FontAwesomeIcon icon={faBan} size="xs" className="text-red" />;
return <FontAwesomeIcon icon={faHourglass} size="xs" className="text-yellow" />;
};
@@ -162,15 +164,21 @@ export const SecretApprovalRequestChanges = ({
secretApprovalRequestDetails.policy.bypassers.some(({ userId }) => userId === userSession.id);
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
Record<string, { status: ApprovalStatus; comment: string }>
Record<string, { status: ApprovalStatus; comment: string; isOrgMembershipActive: boolean }>
>(
(prev, curr) => ({
...prev,
[curr.userId]: { status: curr.status, comment: curr.comment }
[curr.userId]: {
status: curr.status,
comment: curr.comment,
isOrgMembershipActive: curr.isOrgMembershipActive
}
}),
{}
);
console.log("reviewedUsers", reviewedUsers);
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus, comment: string) => {
try {
await updateSecretApprovalRequestStatus({
@@ -524,7 +532,7 @@ export const SecretApprovalRequestChanges = ({
</div>
</div>
<div className="sticky top-0 z-[51] w-1/5 cursor-default pt-2" style={{ minWidth: "240px" }}>
<div className="text-sm text-bunker-300">Reviewers</div>
<div className="text-sm text-bunker-300">Reviewerssss</div>
<div className="mt-2 flex flex-col space-y-2 text-sm">
{secretApprovalRequestDetails?.policy?.approvers
.filter(
@@ -533,26 +541,50 @@ export const SecretApprovalRequestChanges = ({
)
.map((requiredApprover) => {
const reviewer = reviewedUsers?.[requiredApprover.userId];
const isOrgMembershipActive = requiredApprover.isOrgMembershipActive;
console.log("isOrgMembershipActive", isOrgMembershipActive);
return (
<div
className="flex flex-nowrap items-center justify-between space-x-2 rounded border border-mineshaft-600 bg-mineshaft-800 px-2 py-1"
key={`required-approver-${requiredApprover.userId}`}
>
<Tooltip
content={
requiredApprover.firstName
? `${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""}`
: undefined
}
position="left"
sideOffset={10}
<div
className={twMerge(
"flex items-center gap-1 text-sm",
!isOrgMembershipActive && "opacity-40"
)}
>
<div className="flex text-sm">
<div>{requiredApprover?.email}</div>
<span className="text-red">*</span>
</div>
</Tooltip>
<div>
<Tooltip
content={
requiredApprover.firstName
? `${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""}`
: undefined
}
position="left"
sideOffset={10}
>
<div className="flex">
<div>{requiredApprover?.email}</div>
<span className="text-red">*</span>
</div>
</Tooltip>
{!isOrgMembershipActive && (
<Tooltip
className="relative !z-[500]"
content="This user has been deactivated and no longer has an active organization membership."
>
<FontAwesomeIcon
icon={faUserSlash}
size="xs"
className="text-mineshaft-300"
/>
</Tooltip>
)}
</div>
<div className="flex items-center">
{reviewer?.comment && (
<Tooltip className="max-w-lg break-words" content={reviewer.comment}>
<FontAwesomeIcon
@@ -562,9 +594,21 @@ export const SecretApprovalRequestChanges = ({
/>
</Tooltip>
)}
<Tooltip content={`Status: ${reviewer?.status || ApprovalStatus.PENDING}`}>
{getReviewedStatusSymbol(reviewer?.status)}
</Tooltip>
<div className="flex gap-2">
<Tooltip
className="relative !z-[500]"
content={
<span className="text-sm">
Status:{" "}
<span className="capitalize">
{reviewer?.status || ApprovalStatus.PENDING}
</span>
</span>
}
>
{getReviewedStatusSymbol(reviewer?.status)}
</Tooltip>
</div>
</div>
</div>
);
@@ -578,20 +622,43 @@ export const SecretApprovalRequestChanges = ({
)
.map((reviewer) => {
const status = reviewedUsers?.[reviewer.userId].status;
const isOrgMembershipActive = reviewer.isOrgMembershipActive;
return (
<div
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
className="flex flex-nowrap items-center justify-between space-x-2 rounded bg-mineshaft-800 px-2 py-1"
key={`required-approver-${reviewer.userId}`}
>
<div className="flex-grow text-sm">
<Tooltip content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}>
<span>{reviewer?.email} </span>
<div
className={twMerge(
"flex items-center gap-1 text-sm",
!isOrgMembershipActive && "opacity-40"
)}
>
<Tooltip
className="relative !z-[500]"
content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}
>
<div className="flex">
<span>{reviewer?.email} </span>
</div>
</Tooltip>
<span className="text-red">*</span>
{!isOrgMembershipActive && (
<Tooltip
className="relative !z-[500]"
content="This user has been deactivated and no longer has an active organization membership."
>
<FontAwesomeIcon
icon={faUserSlash}
size="xs"
className="text-mineshaft-300"
/>
</Tooltip>
)}
</div>
<div>
{reviewer.comment && (
<Tooltip content={reviewer.comment}>
<Tooltip className="relative !z-[500]" content={reviewer.comment}>
<FontAwesomeIcon
icon={faComment}
size="xs"
@@ -599,7 +666,15 @@ export const SecretApprovalRequestChanges = ({
/>
</Tooltip>
)}
<Tooltip content={status || ApprovalStatus.PENDING}>
<Tooltip
className="relative !z-[500]"
content={
<span className="text-sm">
Status:{" "}
<span className="capitalize">{status || ApprovalStatus.PENDING}</span>
</span>
}
>
{getReviewedStatusSymbol(status)}
</Tooltip>
</div>