Add configurable compliance portal commitment cards
The compliance portal home page rendered security-commitment cards from a hardcoded placeholder POJO. Back them with real, per-organization data that admins configure in the console and the portal loads over the trust center GraphQL API. Model two entities under the trust center: a commitment group (title, description, rank) and a commitment card (icon, eyebrow, title, description, rank). The card icon is a curated enum mapped to a Phosphor icon in the portal. New entities adopt the compliance_portal_ prefix as the start of the broader rename away from trust_center_ naming. Expose the groups and cards read-only on the public trust API and with full CRUD on the console API, add a Commitments tab to the compliance page, and replace the placeholder section with a Relay-driven one. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"heroDescription": "Welcome to our Compliance Portal. Find our security documentation and certifications here.",
|
||||
"sections": {
|
||||
"compliance": "Compliance",
|
||||
"securityCommitments": "Security Commitments",
|
||||
"trustedBy": "Trusted by",
|
||||
"recentUpdates": "Recent updates"
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"heroDescription": "Bienvenue dans notre Compliance Portal. Retrouvez ici notre documentation et nos certifications de sécurité.",
|
||||
"sections": {
|
||||
"compliance": "Conformité",
|
||||
"securityCommitments": "Engagements de sécurité",
|
||||
"trustedBy": "Ils nous font confiance",
|
||||
"recentUpdates": "Mises à jour récentes"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import type { SecurityCommitmentGroupListItem_group$key } from "./__generated__/SecurityCommitmentGroupListItem_group.graphql";
|
||||
import { SecurityCommitmentListItem } from "./SecurityCommitmentListItem";
|
||||
import { securityCommitments } from "./variants";
|
||||
|
||||
const fragment = graphql`
|
||||
fragment SecurityCommitmentGroupListItem_group on CompliancePortalCommitmentGroup {
|
||||
title
|
||||
description
|
||||
commitments(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...SecurityCommitmentListItem_commitment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface SecurityCommitmentGroupListItemProps {
|
||||
groupKey: SecurityCommitmentGroupListItem_group$key;
|
||||
// Only the first group carries the section eyebrow, matching the design.
|
||||
showEyebrow: boolean;
|
||||
}
|
||||
|
||||
export function SecurityCommitmentGroupListItem({ groupKey, showEyebrow }: SecurityCommitmentGroupListItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const group = useFragment(fragment, groupKey);
|
||||
const slots = securityCommitments();
|
||||
|
||||
const commitments = group.commitments.edges.map(edge => edge.node);
|
||||
|
||||
if (commitments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={slots.group()}>
|
||||
<div className={slots.groupHeader()}>
|
||||
{showEyebrow && (
|
||||
<Text size={1} color="gold">
|
||||
{t("home.sections.securityCommitments")}
|
||||
</Text>
|
||||
)}
|
||||
<Text size={2} weight="medium" color="neutral" highContrast>
|
||||
{group.title}
|
||||
</Text>
|
||||
<Text size={2} color="neutral">
|
||||
{group.description}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={slots.grid()}>
|
||||
{commitments.map(commitment => (
|
||||
<SecurityCommitmentListItem key={commitment.id} commitmentKey={commitment} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import { CommitmentCard } from "#/components/CommitmentCard/CommitmentCard";
|
||||
|
||||
import type { SecurityCommitmentListItem_commitment$key } from "./__generated__/SecurityCommitmentListItem_commitment.graphql";
|
||||
import { CommitmentIcon } from "./commitmentIcons";
|
||||
|
||||
const fragment = graphql`
|
||||
fragment SecurityCommitmentListItem_commitment on CompliancePortalCommitment {
|
||||
icon
|
||||
eyebrow
|
||||
title
|
||||
description
|
||||
}
|
||||
`;
|
||||
|
||||
interface SecurityCommitmentListItemProps {
|
||||
commitmentKey: SecurityCommitmentListItem_commitment$key;
|
||||
}
|
||||
|
||||
export function SecurityCommitmentListItem({ commitmentKey }: SecurityCommitmentListItemProps) {
|
||||
const commitment = useFragment(fragment, commitmentKey);
|
||||
|
||||
return (
|
||||
<CommitmentCard
|
||||
icon={<CommitmentIcon icon={commitment.icon} size={32} weight="light" />}
|
||||
eyebrow={<Text size={1} color="gold">{commitment.eyebrow}</Text>}
|
||||
title={(
|
||||
<Text size={4} weight="medium" color="neutral" highContrast>
|
||||
{commitment.title}
|
||||
</Text>
|
||||
)}
|
||||
description={<Text size={2} color="neutral">{commitment.description}</Text>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -18,54 +18,70 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import { CommitmentCard } from "#/components/CommitmentCard/CommitmentCard";
|
||||
import { InlineErrorCard } from "#/components/errors/InlineErrorCard";
|
||||
|
||||
import { SECURITY_COMMITMENT_GROUPS } from "./securityCommitments";
|
||||
import type { SecurityCommitmentsSection_trustCenter$key } from "./__generated__/SecurityCommitmentsSection_trustCenter.graphql";
|
||||
import { SecurityCommitmentGroupListItem } from "./SecurityCommitmentGroupListItem";
|
||||
import { securityCommitments } from "./variants";
|
||||
|
||||
// "Security Commitments" section.
|
||||
//
|
||||
// TODO: This section renders placeholder data from a local POJO
|
||||
// (./securityCommitments.ts) because there is no backend / DB structure for it
|
||||
// yet. Replace it with a relay-driven fragment (and i18n copy) once available.
|
||||
export function SecurityCommitmentsSection() {
|
||||
// @throwOnFieldError makes a field error in this fragment throw at the read
|
||||
// below, where the section's ErrorBoundary contains it. See
|
||||
// contrib/claude/error-handling.md.
|
||||
const securityCommitmentsSectionFragment = graphql`
|
||||
fragment SecurityCommitmentsSection_trustCenter on TrustCenter @throwOnFieldError {
|
||||
commitmentGroups(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...SecurityCommitmentGroupListItem_group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface SecurityCommitmentsSectionProps {
|
||||
trustCenterKey: SecurityCommitmentsSection_trustCenter$key;
|
||||
}
|
||||
|
||||
// "Security Commitments" section: stacked groups, each a header above a grid of
|
||||
// commitment cards. Wraps its data-reading content in a boundary so a load
|
||||
// failure degrades to an inline error instead of taking down the page.
|
||||
export function SecurityCommitmentsSection({ trustCenterKey }: SecurityCommitmentsSectionProps) {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(
|
||||
<div className="w-full py-8">
|
||||
<InlineErrorCard onRetry={() => window.location.reload()} />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<SecurityCommitmentsSectionContent trustCenterKey={trustCenterKey} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function SecurityCommitmentsSectionContent({ trustCenterKey }: SecurityCommitmentsSectionProps) {
|
||||
const data = useFragment(securityCommitmentsSectionFragment, trustCenterKey);
|
||||
const slots = securityCommitments();
|
||||
|
||||
const groups = data.commitmentGroups.edges.map(edge => edge.node);
|
||||
|
||||
if (groups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={slots.root()}>
|
||||
{SECURITY_COMMITMENT_GROUPS.map(group => (
|
||||
<div key={group.title} className={slots.group()}>
|
||||
<div className={slots.groupHeader()}>
|
||||
{group.eyebrow != null && (
|
||||
<Text size={1} color="gold">
|
||||
{group.eyebrow}
|
||||
</Text>
|
||||
)}
|
||||
<Text size={2} weight="medium" color="neutral" highContrast>
|
||||
{group.title}
|
||||
</Text>
|
||||
<Text size={2} color="neutral">
|
||||
{group.description}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={slots.grid()}>
|
||||
{group.items.map(item => (
|
||||
<CommitmentCard
|
||||
key={item.title}
|
||||
icon={<item.Icon size={32} weight="light" />}
|
||||
eyebrow={<Text size={1} color="gold">{item.eyebrow}</Text>}
|
||||
title={(
|
||||
<Text size={4} weight="medium" color="neutral" highContrast>
|
||||
{item.title}
|
||||
</Text>
|
||||
)}
|
||||
description={<Text size={2} color="neutral">{item.description}</Text>}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{groups.map((group, index) => (
|
||||
<SecurityCommitmentGroupListItem
|
||||
key={group.id}
|
||||
groupKey={group}
|
||||
showEyebrow={index === 0}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { Icon, IconProps } from "@phosphor-icons/react";
|
||||
import {
|
||||
BellIcon,
|
||||
BugIcon,
|
||||
CertificateIcon,
|
||||
CloudIcon,
|
||||
CodeIcon,
|
||||
DatabaseIcon,
|
||||
EyeIcon,
|
||||
EyeSlashIcon,
|
||||
FingerprintIcon,
|
||||
GavelIcon,
|
||||
GlobeIcon,
|
||||
HardDrivesIcon,
|
||||
HeartbeatIcon,
|
||||
KeyIcon,
|
||||
LockIcon,
|
||||
LockKeyIcon,
|
||||
ShieldCheckIcon,
|
||||
ShieldWarningIcon,
|
||||
SirenIcon,
|
||||
UsersIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { createElement } from "react";
|
||||
|
||||
// Maps the CompliancePortalCommitmentIcon enum (coredata) to the Phosphor icon
|
||||
// rendered on each commitment card. Keep in sync with the console icon picker
|
||||
// (see apps/console .../commitments/_lib/commitmentIcons.ts).
|
||||
const COMMITMENT_ICONS: Record<string, Icon> = {
|
||||
LOCK_KEY: LockKeyIcon,
|
||||
EYE_SLASH: EyeSlashIcon,
|
||||
FINGERPRINT: FingerprintIcon,
|
||||
SHIELD_WARNING: ShieldWarningIcon,
|
||||
SHIELD_CHECK: ShieldCheckIcon,
|
||||
SIREN: SirenIcon,
|
||||
KEY: KeyIcon,
|
||||
LOCK: LockIcon,
|
||||
CLOUD: CloudIcon,
|
||||
DATABASE: DatabaseIcon,
|
||||
GLOBE: GlobeIcon,
|
||||
EYE: EyeIcon,
|
||||
USERS: UsersIcon,
|
||||
CERTIFICATE: CertificateIcon,
|
||||
GAVEL: GavelIcon,
|
||||
HEARTBEAT: HeartbeatIcon,
|
||||
BELL: BellIcon,
|
||||
BUG: BugIcon,
|
||||
CODE: CodeIcon,
|
||||
SERVER: HardDrivesIcon,
|
||||
};
|
||||
|
||||
// Renders the Phosphor icon mapped from a CompliancePortalCommitmentIcon enum
|
||||
// value. Declared at module scope (and built via createElement) so the icon
|
||||
// component is never created during a parent's render.
|
||||
export function CommitmentIcon({ icon, ...props }: { icon: string } & IconProps) {
|
||||
const resolved: Icon = COMMITMENT_ICONS[icon] ?? LockKeyIcon;
|
||||
return createElement(resolved, props);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { Icon } from "@phosphor-icons/react";
|
||||
import { EyeSlashIcon, FingerprintIcon, LockKeyIcon, ShieldWarningIcon, SirenIcon } from "@phosphor-icons/react";
|
||||
|
||||
export interface SecurityCommitmentItem {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
Icon: Icon;
|
||||
}
|
||||
|
||||
export interface SecurityCommitmentGroup {
|
||||
// Only the first group carries the section eyebrow in the design.
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
items: SecurityCommitmentItem[];
|
||||
}
|
||||
|
||||
// TODO: This content is placeholder data. There is no backend / DB structure for
|
||||
// security commitments yet — replace this POJO with relay-sourced data (and move
|
||||
// the copy into i18n) once the schema exists.
|
||||
export const SECURITY_COMMITMENT_GROUPS: SecurityCommitmentGroup[] = [
|
||||
{
|
||||
eyebrow: "Security Commitments",
|
||||
title: "Data Protection",
|
||||
description:
|
||||
"Your data is protected with industry-leading encryption and strict privacy controls, from storage to transit to processing.",
|
||||
items: [
|
||||
{
|
||||
eyebrow: "Data Protection",
|
||||
title: "Encrypted at rest. Encrypted in transit. Always.",
|
||||
description:
|
||||
"Customer data is protected with AES-256 at rest and TLS 1.3 in transit. Keys rotated on a fixed schedule.",
|
||||
Icon: LockKeyIcon,
|
||||
},
|
||||
{
|
||||
eyebrow: "Privacy",
|
||||
title: "Customer data stays customer data.",
|
||||
description:
|
||||
"PII is masked in non-production environments. Access requires SSO with hardware-backed keys.",
|
||||
Icon: EyeSlashIcon,
|
||||
},
|
||||
{
|
||||
eyebrow: "Identity",
|
||||
title: "Phishing-resistant authentication, by default.",
|
||||
description:
|
||||
"Every employee authenticates via WebAuthn. Production access is just-in-time and recorded.",
|
||||
Icon: FingerprintIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Operational Security",
|
||||
description:
|
||||
"We maintain continuous uptime and rapid incident response so you can rely on our platform around the clock.",
|
||||
items: [
|
||||
{
|
||||
eyebrow: "Threat Detection",
|
||||
title: "Anomalies do not wait. Neither do we.",
|
||||
description:
|
||||
"24/7 SIEM monitoring with automated triage. Critical incidents page on-call within 90 seconds.",
|
||||
Icon: ShieldWarningIcon,
|
||||
},
|
||||
{
|
||||
eyebrow: "Incident Response",
|
||||
title: "Issues are caught fast. And handled.",
|
||||
description:
|
||||
"Security incidents are investigated within hours. Customers notified within 24 hours of a confirmed breach.",
|
||||
Icon: SirenIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -39,6 +39,7 @@ export const homePageQuery = graphql`
|
||||
...OrganizationContactInfo_organization
|
||||
}
|
||||
...ComplianceFrameworksSection_trustCenter
|
||||
...SecurityCommitmentsSection_trustCenter
|
||||
...TrustedBySection_trustCenter
|
||||
...RecentUpdatesSection_trustCenter
|
||||
}
|
||||
@@ -66,7 +67,7 @@ export function HomePage({ queryRef }: HomePageProps) {
|
||||
<div className="flex w-full flex-col items-center px-8">
|
||||
<div className="flex w-full max-w-5xl flex-col">
|
||||
<ComplianceFrameworksSection trustCenterKey={currentTrustCenter} />
|
||||
<SecurityCommitmentsSection />
|
||||
<SecurityCommitmentsSection trustCenterKey={currentTrustCenter} />
|
||||
<TrustedBySection trustCenterKey={currentTrustCenter} />
|
||||
<RecentUpdatesSection trustCenterKey={currentTrustCenter} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user