Disable activation on scim profiles

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-18 17:14:43 +04:00
parent 13aef29ddf
commit 9941a6a66a
12 changed files with 163 additions and 287 deletions

View File

@@ -1,6 +1,6 @@
import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { ActionDropdown, Avatar, Breadcrumb, Card, DropdownItem, IconTrashCan, useConfirm } from "@probo/ui";
import { ActionDropdown, Avatar, Badge, Breadcrumb, Card, DropdownItem, IconTrashCan, useConfirm } from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { useNavigate } from "react-router";
import { graphql } from "relay-runtime";
@@ -18,9 +18,8 @@ export const personPageQuery = graphql`
... on Profile {
id
fullName
identity @required(action: THROW) {
email
}
emailAddress
source
canDelete: permission(action: "iam:membership-profile:delete")
...PersonFormFragment
}
@@ -100,9 +99,11 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
<div className="flex items-center gap-6">
<Avatar name={person.fullName} size="xl" />
<div>
<div className="text-2xl">{person.fullName}</div>
<div className="text-lg text-txt-secondary">{person.identity.email}</div>
<div className="flex items-center gap-2">
<span className="text-2xl">{person.fullName}</span>
<Badge variant="info">{person.source}</Badge>
</div>
<div className="text-lg text-txt-secondary">{person.emailAddress}</div>
</div>
</div>
{person.canDelete && (

View File

@@ -29,14 +29,12 @@ const fragment = graphql`
source
state
fullName
emailAddress
membership @required(action: THROW) {
id
role
canUpdate: permission(action: "iam:membership:update")
}
identity @required(action: THROW) {
email
}
lastInvitation: pendingInvitations(first: 1, orderBy: { field: CREATED_AT, direction: DESC })
@required(action: THROW)
@connection(key: "PeopleListItem_lastInvitation") {
@@ -44,8 +42,6 @@ const fragment = graphql`
edges {
node {
id
expiresAt
acceptedAt
createdAt
}
}
@@ -193,87 +189,85 @@ export function PeopleListItem(props: {
};
return (
<>
<Tr to={`/organizations/${organizationId}/people/${profile.id}`}>
<Td className={clsx(
<Tr to={`/organizations/${organizationId}/people/${profile.id}`}>
<Td className={clsx(
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
>
<span className="font-semibold">{profile.fullName}</span>
</Td>
<Td>
<Badge variant={profile.state === "INACTIVE" ? "neutral" : "success"}>{profile.state}</Badge>
</Td>
<Td className={clsx(
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
>
<div className="flex items-center gap-2">
{profile.emailAddress}
<Badge variant="info">{profile.source}</Badge>
</div>
</Td>
<Td
noLink
className={clsx(
"pr-4",
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
>
<Select
disabled={!profile.membership.canUpdate || isUpdatingRole}
value={profile.membership.role}
onValueChange={role => void handleUpdateRole(role)}
>
<span className="font-semibold">{profile.fullName}</span>
</Td>
<Td>
<Badge variant={profile.state === "INACTIVE" ? "neutral" : "success"}>{profile.state}</Badge>
</Td>
<Td className={clsx(
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
>
<div className="flex items-center gap-2">
{profile.identity.email}
<Badge variant="info">{profile.source}</Badge>
</div>
</Td>
<Td
noLink
className={clsx(
"pr-4",
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
{availableRoles.includes("OWNER") && (
<Option value="OWNER">{__("Owner")}</Option>
)}
>
<Select
disabled={!profile.membership.canUpdate || isUpdatingRole}
value={profile.membership.role}
onValueChange={role => void handleUpdateRole(role)}
>
{availableRoles.includes("OWNER") && (
<Option value="OWNER">{__("Owner")}</Option>
)}
{availableRoles.includes("ADMIN") && (
<Option value="ADMIN">{__("Admin")}</Option>
)}
{availableRoles.includes("VIEWER") && (
<Option value="VIEWER">{__("Viewer")}</Option>
)}
{availableRoles.includes("AUDITOR") && (
<Option value="AUDITOR">{__("Auditor")}</Option>
)}
{availableRoles.includes("EMPLOYEE") && (
<Option value="EMPLOYEE">{__("Employee")}</Option>
)}
</Select>
</Td>
<Td className={clsx(
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
>
{new Date(profile.createdAt).toLocaleDateString()}
</Td>
<Td noLink width={160} className="text-end">
<ActionDropdown>
{isInactive && (
<DropdownItem
onClick={handleInvite}
icon={IconMail}
>
{lastInvitation ? __("Resend activation mail") : __("Send activation mail")}
</DropdownItem>
)}
{profile.canDelete && (
<DropdownItem
onClick={handleRemove}
variant="danger"
icon={IconTrashCan}
>
{__("Remove person")}
</DropdownItem>
)}
</ActionDropdown>
</Td>
</Tr>
</>
{availableRoles.includes("ADMIN") && (
<Option value="ADMIN">{__("Admin")}</Option>
)}
{availableRoles.includes("VIEWER") && (
<Option value="VIEWER">{__("Viewer")}</Option>
)}
{availableRoles.includes("AUDITOR") && (
<Option value="AUDITOR">{__("Auditor")}</Option>
)}
{availableRoles.includes("EMPLOYEE") && (
<Option value="EMPLOYEE">{__("Employee")}</Option>
)}
</Select>
</Td>
<Td className={clsx(
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
>
{new Date(profile.createdAt).toLocaleDateString()}
</Td>
<Td noLink width={160} className="text-end">
<ActionDropdown>
{isInactive && profile.source !== "SCIM" && profile.canInvite && (
<DropdownItem
onClick={handleInvite}
icon={IconMail}
>
{lastInvitation ? __("Resend activation mail") : __("Send activation mail")}
</DropdownItem>
)}
{profile.canDelete && profile.source !== "SCIM" && (
<DropdownItem
onClick={handleRemove}
variant="danger"
icon={IconTrashCan}
>
{__("Remove person")}
</DropdownItem>
)}
</ActionDropdown>
</Td>
</Tr>
);
}

View File

@@ -23,6 +23,7 @@ const fragment = graphql`
id
fullName
emailAddress
source
membership @required(action: THROW) {
role
}
@@ -268,7 +269,7 @@ export function PersonFormLoader(props: { fragmentRef: PersonFormFragment$key })
return (
<PersonForm
id={person.id}
disabled={!person.canUpdate}
disabled={!person.canUpdate || person.source === "SCIM"}
defaultValues={
{
kind: person.kind,