Files
probo/apps/console/src/pages/iam/memberships/_components/MembershipCard.tsx
2026-01-17 12:34:02 -08:00

165 lines
4.3 KiB
TypeScript

import { useTranslate } from "@probo/i18n";
import {
Avatar,
Badge,
Button,
Card,
IconCheckmark1,
IconClock,
IconLock,
} from "@probo/ui";
import { Link, useNavigate } from "react-router";
import { graphql } from "relay-runtime";
import { useFragment, useMutation } from "react-relay";
import type { MembershipCardFragment$key } from "/__generated__/iam/MembershipCardFragment.graphql";
import { parseDate } from "@probo/helpers";
import { useCallback } from "react";
import type { MembershipCard_assumeMutation } from "/__generated__/iam/MembershipCard_assumeMutation.graphql";
const fragment = graphql`
fragment MembershipCardFragment on Membership {
lastSession {
id
expiresAt
}
organization @required(action: THROW) {
id
name
logoUrl
}
}
`;
const assumeOrganizationSessionMutation = graphql`
mutation MembershipCard_assumeMutation(
$input: AssumeOrganizationSessionInput!
) {
assumeOrganizationSession(input: $input) {
result {
__typename
... on OrganizationSessionCreated {
membership {
id
lastSession {
id
expiresAt
}
}
}
... on PasswordRequired {
reason
}
... on SAMLAuthenticationRequired {
reason
redirectUrl
}
}
}
}
`;
interface MembershipCardProps {
fKey: MembershipCardFragment$key;
}
export function MembershipCard(props: MembershipCardProps) {
const { fKey } = props;
const { __ } = useTranslate();
const navigate = useNavigate();
const { lastSession, organization } = useFragment<MembershipCardFragment$key>(
fragment,
fKey,
);
const isAuthenticated = !!lastSession;
const isExpired =
lastSession && parseDate(lastSession.expiresAt) < new Date();
const [assumeOrganizationSession] =
useMutation<MembershipCard_assumeMutation>(
assumeOrganizationSessionMutation,
);
const handleAssumeOrganizationSession = useCallback(() => {
assumeOrganizationSession({
variables: {
input: {
organizationId: organization.id,
},
},
onCompleted: ({ assumeOrganizationSession }) => {
if (!assumeOrganizationSession) {
throw new Error("complete mutation result is empty");
}
const { result } = assumeOrganizationSession;
switch (result.__typename) {
case "PasswordRequired":
navigate("auth/login");
break;
case "SAMLAuthenticationRequired":
window.location.href = result.redirectUrl;
break;
default:
navigate(`/organizations/${organization.id}`);
}
},
});
}, [assumeOrganizationSession, navigate, organization.id]);
const getAuthBadge = () => {
if (isAuthenticated) {
return (
<Badge variant="success" className="flex items-center gap-1">
<IconCheckmark1 size={14} />
{__("Authenticated")}
</Badge>
);
} else if (isExpired) {
return (
<Badge variant="warning" className="flex items-center gap-1">
<IconClock size={14} />
{__("Session expired")}
</Badge>
);
} else {
return (
<Badge variant="neutral" className="flex items-center gap-1">
<IconLock size={14} />
{__("Authentication required")}
</Badge>
);
}
};
return (
<Card padded className="w-full">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 hover:text-primary flex-1">
<Avatar
src={organization.logoUrl}
name={organization.name}
size="l"
/>
<div className="flex flex-col gap-1">
<h2 className="font-semibold text-xl">{organization.name}</h2>
{getAuthBadge()}
</div>
</div>
<div className="flex items-center gap-3">
{isAuthenticated ? (
<Link to={`/organizations/${organization.id}`}>
<Button variant="secondary">{__("Start")}</Button>
</Link>
) : (
<Button onClick={handleAssumeOrganizationSession}>
{__("Login")}
</Button>
)}
</div>
</div>
</Card>
);
}