Use relay and refacto public trust center

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-08-20 23:34:50 +02:00
parent 5db9b9f787
commit d31f611e63
19 changed files with 982 additions and 498 deletions

View File

@@ -1,148 +0,0 @@
import {
Card,
Tr,
Td,
Table,
Thead,
Tbody,
Th,
Button,
IconArrowDown,
IconLock,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers";
import { FrameworkLogo } from "/components/FrameworkLogo";
import { TrustCenterAccessRequestDialog } from "./TrustCenterAccessRequestDialog";
type Audit = {
id: string;
framework: {
name: string;
};
validFrom: string;
validUntil: string | null;
state: string;
createdAt: string;
report: {
id: string;
filename: string;
downloadUrl: string | null;
} | null;
};
type Props = {
audits: Audit[];
organizationName: string;
isAuthenticated: boolean;
trustCenterId: string;
};
export function PublicTrustCenterAudits({
audits,
organizationName,
isAuthenticated,
trustCenterId
}: Props) {
const { __ } = useTranslate();
if (audits.length === 0) {
return (
<Card padded>
<div className="text-center py-8">
<h2 className="text-xl font-semibold text-txt-primary mb-2">
{__("Compliance")}
</h2>
<p className="text-txt-secondary">
{__("No compliance reports are currently available.")}
</p>
</div>
</Card>
);
}
return (
<Card padded className="space-y-4">
<div>
<h2 className="text-xl font-semibold text-txt-primary">
{__("Compliance")}
</h2>
<p className="text-sm text-txt-secondary mt-1">
{sprintf(__("%s is compliant with the following frameworks"), organizationName)}
</p>
</div>
<Table>
<Thead>
<Tr>
<Th>{__("Framework")}</Th>
<Th>{__("Report")}</Th>
</Tr>
</Thead>
<Tbody>
{audits.map((audit) => {
const hasReport = audit.report !== null;
const downloadUrl = audit.report?.downloadUrl;
const reportName = audit.report?.filename || __("Compliance Report");
return (
<Tr key={audit.id}>
<Td>
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<div className="w-8 h-8 [&>img]:w-8 [&>img]:h-8 [&>div]:w-8 [&>div]:h-8">
<FrameworkLogo name={audit.framework.name} />
</div>
</div>
<div className="font-medium">
{audit.framework.name}
</div>
</div>
</Td>
<Td>
{!hasReport ? (
<span className="text-txt-tertiary text-sm">
{__("No report")}
</span>
) : !isAuthenticated ? (
<TrustCenterAccessRequestDialog
trigger={
<Button
variant="secondary"
icon={IconLock}
>
{__("Request Access")}
</Button>
}
trustCenterId={trustCenterId}
organizationName={organizationName}
/>
) : downloadUrl ? (
<Button
variant="secondary"
icon={IconArrowDown}
onClick={() => {
const link = document.createElement('a');
link.href = downloadUrl;
link.download = reportName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
>
{__("Download")}
</Button>
) : (
<span className="text-txt-tertiary text-sm">
{__("Not available")}
</span>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -1,182 +0,0 @@
import {
Card,
Tr,
Td,
Table,
Thead,
Tbody,
Th,
DocumentTypeBadge,
Button,
IconArrowDown,
IconLock,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { buildEndpoint } from "/providers/RelayProviders";
import { TrustCenterAccessRequestDialog } from "./TrustCenterAccessRequestDialog";
const exportDocumentPDFMutation = {
params: {
name: "PublicTrustCenterDocumentsExportPDFMutation",
operationKind: "mutation",
text: `
mutation PublicTrustCenterDocumentsExportPDFMutation(
$input: ExportDocumentPDFInput!
) {
exportDocumentPDF(input: $input) {
data
}
}
`
}
};
type Document = {
id: string;
title: string;
documentType: string;
};
type Props = {
documents: Document[];
isAuthenticated: boolean;
trustCenterId: string;
organizationName: string;
};
type ExportDocumentPDFResponse = {
data?: {
exportDocumentPDF?: {
data: string;
};
};
errors?: Array<{ message: string }>;
};
export function PublicTrustCenterDocuments({
documents,
isAuthenticated,
trustCenterId,
organizationName
}: Props) {
const { __ } = useTranslate();
const { toast } = useToast();
const handleDownload = async (document: Document) => {
try {
const response = await fetch(buildEndpoint("/api/trust/v1/graphql"), {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
credentials: "include",
body: JSON.stringify({
operationName: exportDocumentPDFMutation.params.name,
query: exportDocumentPDFMutation.params.text,
variables: { input: { documentId: document.id } },
}),
});
const result: ExportDocumentPDFResponse = await response.json();
if (result.errors) {
throw new Error(result.errors[0].message);
}
if (result.data?.exportDocumentPDF?.data) {
const link = window.document.createElement("a");
link.href = result.data.exportDocumentPDF.data;
link.download = `${document.title}.pdf`;
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
}
} catch (error) {
toast({
title: __("Download Failed"),
description: __("Unable to download the document. Please try again."),
variant: "error",
});
}
};
if (documents.length === 0) {
return (
<Card padded>
<div className="text-center py-8">
<h2 className="text-xl font-semibold text-txt-primary mb-2">
{__("Documents")}
</h2>
<p className="text-txt-secondary">
{__("No documents are currently available.")}
</p>
</div>
</Card>
);
}
return (
<Card padded className="space-y-4">
<div>
<h2 className="text-xl font-semibold text-txt-primary">
{__("Documents")}
</h2>
<p className="text-sm text-txt-secondary mt-1">
{__("Security and compliance documentation")}
</p>
</div>
<Table>
<Thead>
<Tr>
<Th className="w-1/2">{__("Document")}</Th>
<Th className="w-1/4">{__("Type")}</Th>
<Th className="w-1/4">{__("Download")}</Th>
</Tr>
</Thead>
<Tbody>
{documents.map((document) => {
return (
<Tr key={document.id}>
<Td>
<div className="font-medium">
{document.title}
</div>
</Td>
<Td>
<DocumentTypeBadge type={document.documentType} />
</Td>
<Td>
{!isAuthenticated ? (
<TrustCenterAccessRequestDialog
trigger={
<Button
variant="secondary"
icon={IconLock}
>
{__("Request Access")}
</Button>
}
trustCenterId={trustCenterId}
organizationName={organizationName}
/>
) : (
<Button
variant="secondary"
icon={IconArrowDown}
onClick={() => handleDownload(document)}
>
{__("Download")}
</Button>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -1,117 +0,0 @@
import {
Card,
Tr,
Td,
Table,
Thead,
Tbody,
Th,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { faviconUrl, sprintf } from "@probo/helpers";
type Vendor = {
id: string;
name: string;
category: string;
description: string | null;
createdAt: string;
privacyPolicyUrl?: string | null;
websiteUrl?: string | null;
};
type Props = {
vendors: Vendor[];
organizationName: string;
};
export function PublicTrustCenterVendors({ vendors, organizationName }: Props) {
const { __ } = useTranslate();
if (vendors.length === 0) {
return (
<Card padded>
<div className="text-center py-8">
<h2 className="text-xl font-semibold text-txt-primary mb-2">
{__("Subcontractors")}
</h2>
<p className="text-txt-secondary">
{__("No subcontractor information is currently available.")}
</p>
</div>
</Card>
);
}
return (
<Card padded className="space-y-4">
<div>
<h2 className="text-xl font-semibold text-txt-primary">
{__("Subcontractors")}
</h2>
<p className="text-sm text-txt-secondary mt-1">
{sprintf(__("Third-party subcontractors %s work with"), organizationName)}
</p>
</div>
<Table>
<Thead>
<Tr>
<Th>{__("Company")}</Th>
<Th>{__("Website")}</Th>
</Tr>
</Thead>
<Tbody>
{vendors.map((vendor) => {
const url = vendor.privacyPolicyUrl || vendor.websiteUrl;
const logo = faviconUrl(vendor.websiteUrl);
const getCleanUrl = (url: string) => {
try {
const parsedUrl = new URL(url);
return parsedUrl.hostname + parsedUrl.pathname + parsedUrl.search;
} catch {
return url.replace(/^https?:\/\//, '');
}
};
return (
<Tr key={vendor.id}>
<Td>
<div className="flex items-center space-x-3">
{logo && (
<img
src={logo}
alt={`${vendor.name} logo`}
className="w-8 h-8 object-contain rounded-full"
/>
)}
<div className="font-medium">
{vendor.name}
</div>
</div>
</Td>
<Td>
{url ? (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-txt-info hover:opacity-80 underline transition-opacity"
>
{getCleanUrl(url)}
</a>
) : (
<span className="text-txt-secondary text-sm">
{__("No website available")}
</span>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -1,174 +0,0 @@
import { useState } from "react";
import {
Dialog,
DialogFooter,
DialogContent,
Button,
Field,
useToast,
useDialogRef,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { z } from "zod";
import { buildEndpoint } from "/providers/RelayProviders";
// Manual mutation for trust API (not processed by relay compiler)
const createTrustCenterAccessMutation = {
params: {
name: "CreateTrustCenterAccessMutation",
operationKind: "mutation",
text: `
mutation CreateTrustCenterAccessMutation(
$input: CreateTrustCenterAccessInput!
) {
createTrustCenterAccess(input: $input) {
trustCenterAccess {
id
email
name
}
}
}
`
}
};
type CreateTrustCenterAccessResponse = {
data?: {
createTrustCenterAccess?: {
trustCenterAccess: {
id: string;
email: string;
name: string;
};
};
};
errors?: Array<{ message: string }>;
};
type Props = {
trigger: React.ReactNode;
trustCenterId: string;
organizationName: string;
};
export function TrustCenterAccessRequestDialog({
trigger,
trustCenterId,
organizationName
}: Props) {
const { __ } = useTranslate();
const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
const dialogRef = useDialogRef();
const schema = z.object({
name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")),
email: z.string().min(1, __("Email is required")).email(__("Please enter a valid email address")),
});
const { register, handleSubmit, formState, reset } = useFormWithSchema(schema, {
defaultValues: { name: "", email: "" },
});
const onSubmit = handleSubmit(async (data) => {
setIsSubmitting(true);
try {
const response = await fetch(buildEndpoint("/api/trust/v1/graphql"), {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
credentials: "include",
body: JSON.stringify({
operationName: createTrustCenterAccessMutation.params.name,
query: createTrustCenterAccessMutation.params.text,
variables: {
input: {
trustCenterId,
email: data.email,
name: data.name
}
},
}),
});
const result: CreateTrustCenterAccessResponse = await response.json();
if (result.errors) {
throw new Error(result.errors[0].message);
}
if (result.data?.createTrustCenterAccess) {
toast({
title: __("Request Submitted"),
description: __("Your access request has been submitted. You will receive an email if your request is approved."),
variant: "success",
});
reset();
dialogRef.current?.close();
}
} catch (error) {
const errorMessage = error instanceof Error
? error.message
: __("An error occurred while submitting your request.");
toast({
title: __("Request Failed"),
description: errorMessage,
variant: "error",
});
} finally {
setIsSubmitting(false);
}
});
return (
<Dialog
ref={dialogRef}
trigger={trigger}
title={__("Request Access")}
>
<form onSubmit={onSubmit}>
<DialogContent padded className="space-y-4">
<div className="text-sm text-txt-secondary">
{sprintf(__("Request access to %s's Trust Center. Your request will be reviewed and you will receive an email notification with access instructions if approved."), organizationName)}
</div>
<Field
label={__("Your Name")}
required
error={formState.errors.name?.message}
{...register("name")}
placeholder={__("Enter your full name")}
disabled={isSubmitting}
/>
<Field
label={__("Email Address")}
required
type="email"
error={formState.errors.email?.message}
{...register("email")}
placeholder={__("Enter your email address")}
disabled={isSubmitting}
/>
</DialogContent>
<DialogFooter>
<Button
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? __("Submitting...") : __("Submit Request")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}