Add trust center v2

Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
Jonathan
2025-09-26 16:42:52 +02:00
committed by Sacha Al Himdani
parent 44898be9d3
commit 55e85e5f67
182 changed files with 4609 additions and 425 deletions

View File

@@ -0,0 +1,153 @@
import { graphql } from "relay-runtime";
import type { AuditRowFragment$key } from "./__generated__/AuditRowFragment.graphql";
import { useFragment } from "react-relay";
import {
Breadcrumb,
Button,
Dialog,
DialogContent,
FrameworkLogo,
IconArrowInbox,
IconLock,
IconMedal,
Spinner,
Table,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated";
import type { AuditRowDownloadMutation } from "./__generated__/AuditRowDownloadMutation.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { downloadFile } from "@probo/helpers";
import type { PropsWithChildren } from "react";
import { useLocation } from "react-router";
import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx";
const downloadMutation = graphql`
mutation AuditRowDownloadMutation($input: ExportReportPDFInput!) {
exportReportPDF(input: $input) {
data
}
}
`;
const auditRowFragment = graphql`
fragment AuditRowFragment on Audit {
report {
id
filename
}
framework {
id
name
}
}
`;
export function AuditRow(props: { audit: AuditRowFragment$key }) {
const audit = useFragment(auditRowFragment, props.audit);
const { __ } = useTranslate();
const isAuthenticated = useIsAuthenticated();
const [commitDownload, downloading] =
useMutationWithToasts<AuditRowDownloadMutation>(downloadMutation);
const handleDownload = () => {
if (!audit.report?.id) {
return;
}
commitDownload({
variables: {
input: {
reportId: audit.report.id,
},
},
onSuccess(response) {
downloadFile(response.exportReportPDF.data, audit.report!.filename);
},
});
};
return (
<div className="text-sm border-1 border-border-solid -mt-[1px] flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconMedal size={16} className="flex-none" />
{audit.framework.name}
</div>
{audit.report ? (
isAuthenticated ? (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
) : (
<RequestAccessDialog>
<Button
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
>
{__("Request access")}
</Button>
</RequestAccessDialog>
)
) : (
<span className=" text-txt-secondary">{__("No report")}</span>
)}
</div>
);
}
export function AuditRowAvatar(props: { audit: AuditRowFragment$key }) {
const audit = useFragment(auditRowFragment, props.audit);
return (
<AuditDialog audit={props.audit}>
<button className="block cursor-pointer aspect-square">
<FrameworkLogo
alt={audit.framework.name}
name={audit.framework.name}
className="size-full"
/>
</button>
</AuditDialog>
);
}
function AuditDialog(
props: PropsWithChildren<{ audit: AuditRowFragment$key }>,
) {
const audit = useFragment(auditRowFragment, props.audit);
const location = useLocation();
const { __ } = useTranslate();
const items = [
{
label: __("Certifications"),
to: location.pathname,
},
{
label: audit.framework.name,
to: location.pathname,
},
];
return (
<Dialog
trigger={props.children}
className="max-w-[500px]"
title={<Breadcrumb items={items} />}
>
<DialogContent className="p-4 lg:p-8 space-y-6">
<FrameworkLogo
alt={audit.framework.name}
name={audit.framework.name}
className="size-24 block mx-auto"
/>
<h2 className="text-xl font-semibold mb-1">{audit.framework.name}</h2>
<p className="text-txt-secondary">Framework description</p>
<Table>
<AuditRow audit={props.audit} />
</Table>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,80 @@
import { graphql } from "relay-runtime";
import type { DocumentRowFragment$key } from "./__generated__/DocumentRowFragment.graphql";
import { useFragment } from "react-relay";
import {
Button,
IconArrowInbox,
IconLock,
IconPageTextLine,
Spinner,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated";
import type { DocumentRowDownloadMutation } from "./__generated__/DocumentRowDownloadMutation.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { downloadFile } from "@probo/helpers";
import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx";
const downloadMutation = graphql`
mutation DocumentRowDownloadMutation($input: ExportDocumentPDFInput!) {
exportDocumentPDF(input: $input) {
data
}
}
`;
const documentRowFragment = graphql`
fragment DocumentRowFragment on Document {
id
title
}
`;
export function DocumentRow(props: { document: DocumentRowFragment$key }) {
const document = useFragment(documentRowFragment, props.document);
const { __ } = useTranslate();
const isAuthenticated = useIsAuthenticated();
const [commitDownload, downloading] =
useMutationWithToasts<DocumentRowDownloadMutation>(downloadMutation);
const handleDownload = () => {
commitDownload({
variables: {
input: {
documentId: document.id,
},
},
onSuccess(response) {
downloadFile(response.exportDocumentPDF.data, document.title);
},
});
};
return (
<div className="text-sm border-1 border-border-solid -mt-[1px] flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconPageTextLine size={16} className=" flex-none" />
{document.title}
</div>
{isAuthenticated ? (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
) : (
<RequestAccessDialog>
<Button
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
>
{__("Request access")}
</Button>
</RequestAccessDialog>
)}
</div>
);
}

View File

@@ -0,0 +1,115 @@
import { Button, Card, Logo, Spinner } from "@probo/ui";
import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useEffect } from "react";
import { PDFPreview } from "./PDFPreview";
import { useWindowSize } from "usehooks-ts";
import clsx from "clsx";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
const signMutation = graphql`
mutation NDADialogSignMutation($input: AcceptNonDisclosureAgreementInput!) {
acceptNonDisclosureAgreement(input: $input) {
success
}
}
`;
export function NDADialog({
name,
url,
fileName,
trustCenterId,
}: {
name: string;
url?: string | null;
fileName?: string | null;
trustCenterId: string;
}) {
const { __ } = useTranslate();
useEffect(() => {
document.body.style.setProperty("overflow", "hidden");
return () => {
document.body.style.removeProperty("overflow");
};
}, []);
const { width } = useWindowSize();
const isMobile = width < 1100;
const isDesktop = !isMobile;
const [commitSigning, isSigning] = useMutationWithToasts(signMutation, {
onSuccess: () => {
window.location.reload();
},
});
const handleSign = () => {
commitSigning({
variables: {
input: {
trustCenterId,
},
},
});
};
return (
<div className="fixed inset-0 bg-level-2 z-100 flex flex-col lg:h-screen">
<header className="flex items-center h-12 justify-between border-b border-border-solid px-4 flex-none">
<Logo />
</header>
<div className="grid lg:grid-cols-2 min-h-0 h-full">
<div className="max-w-[440px] mx-auto py-20">
<h1 className="text-2xl font-semibold mb-4">
{__("Review & Sign NDA")}
</h1>
<p className="text-txt-secondary">
{sprintf(
__(
"Access to %s Trust Center documents requires signing a Non-Disclosure Agreement (NDA). Please review the agreement below. Once signed, you’ll receive immediate access to the requested documents.",
),
name,
)}
</p>
{isMobile && url && (
<Card className="flex justify-between py-3 px-4 text-sm items-center my-6">
{fileName}
<Button variant="secondary" asChild>
<a target="_blank" rel="noopener noreferrer" href={url}>
{__("View document")}
</a>
</Button>
</Card>
)}
<Button
onClick={handleSign}
className="h-10 w-full my-8"
disabled={isSigning}
icon={isSigning ? Spinner : undefined}
>
{__("Review & Sign")}
</Button>
<p className="text-xs text-txt-secondary">
{__(
"By clicking Review & Sign, you agree to the terms of this NDA. If you have questions about the NDA, please contact security@probo.com.",
)}
</p>
<a
href="https://www.getprobo.com/"
className={clsx(
"flex gap-1 text-sm font-medium text-txt-tertiary items-center w-max mx-auto",
isMobile ? "mt-15" : "mt-30",
)}
>
Powered by <Logo withPicto className="h-6" />
</a>
</div>
{isDesktop && (
<div className="bg-subtle h-full border-l border-border-solid min-h-0">
{url && <PDFPreview src={url} name={fileName ?? ""} />}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,125 @@
import { useTranslate } from "@probo/i18n";
import { Button, Card, IconBlock, IconLock, IconMedal } from "@probo/ui";
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
import type { PropsWithChildren } from "react";
import { domain } from "@probo/helpers";
import { AuditRowAvatar } from "./AuditRow";
import { RequestAccessDialog } from "./RequestAccessDialog";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated";
export function OrganizationSidebar({
trustCenter,
}: {
trustCenter: TrustGraphQuery$data["trustCenterBySlug"];
}) {
const { __ } = useTranslate();
const isAuthenticated = useIsAuthenticated();
if (!trustCenter) {
return null;
}
return (
<Card className="p-6 relative overflow-hidden border-b-1 border-border-low isolate">
<div className="h-21 bg-[#044E4114] absolute top-0 left-0 right-0 -z-1"></div>
{trustCenter.organization.logoUrl ? (
<img
alt=""
src={trustCenter.organization.logoUrl}
className="size-24 rounded-2xl border border-border-mid shadow-mid"
/>
) : (
<div className="size-24 rounded-2xl border border-border-mid bg-level-1 shadow-mid" />
)}
<h1 className="text-2xl mt-6">{trustCenter.organization.name}</h1>
<p className="text-sm text-txt-secondary mt-1">
{trustCenter.organization.description}
</p>
<hr className="my-6 -mx-6 h-[1px] bg-border-low border-none" />
{/* Business information */}
<div className="space-y-4">
<h2 className="text-xs text-txt-secondary flex gap-1 items-center">
<IconBlock size={16} />
{__("Business information")}
</h2>
{trustCenter.organization.websiteUrl && (
<BusinessInfo label={__("Website")}>
<a
href={trustCenter.organization.websiteUrl}
target="_blank"
rel="noopener noreferrer"
>
<span className="text-txt-info hover:underline ">
{domain(trustCenter.organization.websiteUrl)}
</span>
</a>
</BusinessInfo>
)}
{trustCenter.organization.email && (
<BusinessInfo label={__("Contact")}>
{trustCenter.organization.email}
</BusinessInfo>
)}
{trustCenter.organization.headquarterAddress && (
<BusinessInfo label={__("HQ address")}>
{trustCenter.organization.headquarterAddress}
</BusinessInfo>
)}
<hr className="my-6 -mx-6 h-[1px] bg-border-low border-none" />
{/* Certifications */}
<div className="space-y-4">
<h2 className="text-xs text-txt-secondary flex gap-1 items-center">
<IconMedal size={16} />
{__("Certifications")}
</h2>
<div
className="grid grid-cols-4 gap-4"
style={{
gridTemplateColumns: "repeat(auto-fit, 75px",
}}
>
{trustCenter.audits.edges.map((audit) => (
<AuditRowAvatar key={audit.node.id} audit={audit.node} />
))}
{trustCenter.audits.edges.map((audit) => (
<AuditRowAvatar key={audit.node.id} audit={audit.node} />
))}
{trustCenter.audits.edges.map((audit) => (
<AuditRowAvatar key={audit.node.id} audit={audit.node} />
))}
</div>
</div>
<hr className="my-6 -mx-6 h-[1px] bg-border-low border-none" />
{/* Actions */}
{!isAuthenticated && (
<RequestAccessDialog>
<Button variant="primary" icon={IconLock} className="w-full h-10">
{__("Request access")}
</Button>
</RequestAccessDialog>
)}
{/* <Button variant="secondary" icon={IconMail} className="w-full h-10">
{__("Subscribe to updates")}
</Button>*/}
</div>
</Card>
);
}
function BusinessInfo({
children,
label,
}: PropsWithChildren<{ label: string }>) {
return (
<div>
<div className="text-xs text-txt-secondary">{label}</div>
<div className="text-sm">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,130 @@
import { Document, Page, pdfjs } from "react-pdf";
import "react-pdf/dist/Page/TextLayer.css";
import "react-pdf/dist/Page/AnnotationLayer.css";
import { type ComponentProps, useRef, useState } from "react";
import {
IconArrowInbox,
IconChevronLeft,
IconChevronRight,
IconPlusLarge,
Spinner,
} from "@probo/ui";
import { times } from "@probo/helpers";
import { IconMinusLarge } from "@probo/ui/src/Atoms/Icons/IconMinusLarge.tsx";
// Worker for PDF.js
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
const btnClass =
"size-8 grid place-items-center hover:bg-secondary-hover cursor-pointer rounded-sm disabled:opacity-30 transition-all";
export function PDFPreview({ src, name }: { src: string; name?: string }) {
const [numPages, setNumPages] = useState(0);
const [scale, setScale] = useState(1.0);
const [currentPage, setCurrentPage] = useState(1);
const documentRef: ComponentProps<typeof Document>["ref"] = useRef(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const onDocumentLoadSuccess: ComponentProps<
typeof Document
>["onLoadSuccess"] = (document) => {
setNumPages(document.numPages);
setCurrentPage(1);
};
const zoomFactor = (factor: number) => () => {
setScale(scale * factor);
};
const movePage = (direction: 1 | -1) => () => {
if (currentPage === 1 && direction === -1) {
return;
}
const newPage = currentPage + direction;
const page = documentRef.current?.pages.current[newPage - 1];
if (!page) {
return;
}
page.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "center",
});
setCurrentPage(newPage);
};
const resolveCurrentPage = () => {
if (!wrapperRef.current) {
return;
}
const pages = documentRef.current?.pages.current;
if (!pages?.length) {
return;
}
const parentRect = wrapperRef.current.getBoundingClientRect();
const parentMiddleY = parentRect.top + parentRect.height / 2;
for (let i = 0; i < pages.length; i++) {
const childRect = pages[i].getBoundingClientRect();
if (childRect.top <= parentMiddleY && childRect.bottom >= parentMiddleY) {
return setCurrentPage(i + 1);
}
}
};
return (
<div className="grid grid-rows-[max-content_1fr] h-full bg-subtle">
{/* Custom Zoom Controls */}
<nav className="flex-none flex items-center gap-2 bg-level-1 py-3 text-sm pl-4 pr-3 text-txt-primary">
<div>{name}</div>
<div className="mx-auto flex gap-1 items-center">
<button
onClick={movePage(-1)}
className={btnClass}
disabled={currentPage === 1}
>
<IconChevronLeft size={16} />
</button>
<div>
{currentPage} / {numPages}
</div>
<button onClick={movePage(1)} className={btnClass}>
<IconChevronRight size={16} />
</button>
</div>
<button onClick={zoomFactor(0.8)} className={btnClass}>
<IconMinusLarge size={16} />
</button>
<button onClick={zoomFactor(1.2)} className={btnClass}>
<IconPlusLarge size={16} />
</button>
<button onClick={zoomFactor(1.2)} className={btnClass}>
<IconArrowInbox size={16} />
</button>
</nav>
{/* PDF Document */}
<div
className="overflow-auto scroll-p-6"
onScrollEnd={resolveCurrentPage}
ref={wrapperRef}
>
<Document
file={src}
onLoadSuccess={onDocumentLoadSuccess}
className="flex flex-col gap-4 py-10"
ref={documentRef}
>
{numPages === 0 && <Spinner className="mx-auto" />}
{times(numPages, (index) => (
<Page
className="w-max h-max mx-auto shadow-mid"
key={index.toString()}
pageNumber={index + 1}
scale={scale} // Apply zoom via scale prop
/>
))}
</Document>
</div>
</div>
);
}

View File

@@ -0,0 +1,88 @@
import { useLocation, useRouteError } from "react-router";
import { IconPageCross } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useEffect, useRef } from "react";
const classNames = {
wrapper: "py-10 text-center space-y-2 ",
title: "text-2xl flex gap-2 font-semibold items-center justify-center",
description: "text-base text-txt-tertiary",
detail:
"text-sm text-txt-tertiary font-mono text-start border border-border-low p-2 rounded bg-level-1 mt-2",
};
type Props = {
resetErrorBoundary?: () => void;
error?: string;
};
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
const error = useRouteError() ?? propsError;
const { __ } = useTranslate();
const location = useLocation();
const baseLocation = useRef(location);
// Reset error boundary on page change
useEffect(() => {
if (
location.pathname !== baseLocation.current.pathname &&
resetErrorBoundary
) {
resetErrorBoundary();
}
}, [location, resetErrorBoundary]);
if (!error || (error && error.toString().includes("PAGE_NOT_FOUND"))) {
return (
<div className={classNames.wrapper}>
<h1 className={classNames.title}>
<IconPageCross size={26} />
{__("Page not found")}
</h1>
<p className={classNames.description}>
{__("The page you are looking for does not exist")}
</p>
</div>
);
}
if (
error
.toString()
.toLowerCase()
.match(/(token|expired|invalid|401|unauthorized)/)
) {
const isExpiredToken = error.toString().toLowerCase().includes("expired");
const title = isExpiredToken
? __("Expired token")
: __("Invalid Access Link");
const description = isExpiredToken
? __(
"This access link has expired. Trust center access links are valid for 7 days for security reasons."
)
: __(
"This access link is not valid. It may have been revoked or the link might be incorrect."
);
return (
<div className={classNames.wrapper}>
<h1 className={classNames.title}>
<IconPageCross size={26} />
{title}
</h1>
<p className={classNames.description}>{description}</p>
</div>
);
}
return (
<div className={classNames.wrapper}>
<h1 className={classNames.title}>{__("Unexpected error :(")}</h1>
<details>
<summary className={classNames.description}>
{__("Something went wrong")}
</summary>
<p className={classNames.detail}>{error.toString()}</p>
</details>
</div>
);
}

View File

@@ -0,0 +1,116 @@
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
Field,
useDialogRef,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { useTrustCenter } from "/hooks/useTrustCenter";
type Props = {
children: React.ReactNode;
};
const schema = z.object({
name: z.string(),
email: z.string().email(),
});
const requestAccessMutation = graphql`
mutation RequestAccessDialogMutation($input: CreateTrustCenterAccessInput!) {
createTrustCenterAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
export function RequestAccessDialog({ children }: Props) {
const trustCenter = useTrustCenter();
const { toast } = useToast();
const { __ } = useTranslate();
const { handleSubmit, register } = useFormWithSchema(schema, {
defaultValues: {
name: "",
email: "",
},
});
const dialogRef = useDialogRef();
const [commitRequestAccess, isRequestingAccess] = useMutationWithToasts(
requestAccessMutation,
{
onSuccess: () => {
toast({
title: __("Success"),
description: __("Access request submitted successfully"),
variant: "success",
});
dialogRef.current?.close();
},
},
);
const onSubmit = handleSubmit((data) => {
commitRequestAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
name: data.name,
email: data.email,
},
},
});
});
return (
<Dialog
ref={dialogRef}
trigger={children}
className="max-w-[500px] text-txt-primary"
>
<form onSubmit={onSubmit}>
<DialogTitle className="text-2xl font-semibold mb-4 pt-4 md:pt-8 px-4 md:px-8">
{__("Request access to documentation")}
</DialogTitle>
<DialogContent className="px-4 md:px-8 pb-4 md:pb-8 text-txt-primary">
<p className="text-txt-secondary mb-4">
{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.",
),
trustCenter.organization.name,
)}
</p>
<div className="space-y-4">
<Field
label={__("Full name")}
placeholder="John Doe"
{...register("name")}
type="text"
/>
<Field
label={__("Email")}
placeholder="john.doe@acme.com"
{...register("email")}
type="email"
/>
</div>
</DialogContent>
<DialogFooter>
<Button disabled={isRequestingAccess} type="submit">
{__("Continue")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,9 @@
import type { PropsWithChildren } from "react";
export function RowHeader({ children }: PropsWithChildren) {
return (
<div className="bg-subtle text-xs font-medium text-txt-tertiary uppercase ">
{children}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import type { PropsWithChildren } from "react";
import clsx from "clsx";
export function Rows({
children,
className,
}: PropsWithChildren<{ className?: string }>) {
return (
<div
className={clsx(
"*:first:rounded-t-lg *:last:rounded-b-lg *:px-6 *:py-3 *:-mt-[1px] *:border *:border-border-solid",
className,
)}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { Skeleton, TabLink, Tabs } from "@probo/ui";
import { TabSkeleton } from "./TabSkeleton";
import { useParams } from "react-router";
import { useTranslate } from "@probo/i18n";
export function MainSkeleton() {
const { slug } = useParams<{ slug: string }>();
const baseTabUrl = `/trust/${slug}`;
const { __ } = useTranslate();
return (
<div className="grid grid-cols-1 max-w-[1280px] mx-4 pt-6 gap-4 lg:mx-auto lg:gap-10 lg:pt-20 lg:grid-cols-[400px_1fr] ">
<Skeleton className="w-full h-300" />
<main>
<Tabs className="mb-8">
<TabLink to={`${baseTabUrl}/overview`}>{__("Overview")}</TabLink>
<TabLink to={`${baseTabUrl}/documents`}>{__("Documents")}</TabLink>
<TabLink to={`${baseTabUrl}/subprocessors`}>
{__("Subprocessors")}
</TabLink>
</Tabs>
<TabSkeleton />
</main>
</div>
);
}

View File

@@ -0,0 +1,11 @@
import { Skeleton } from "@probo/ui";
export function TabSkeleton() {
return (
<div className="h-64">
<Skeleton className="h-6 w-[110px] mb-1" />
<Skeleton className="h-6 w-[250px] mb-4" />
<Skeleton className="w-full h-[180px]" />
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { graphql } from "relay-runtime";
import type { VendorRowFragment$key } from "./__generated__/VendorRowFragment.graphql";
import { useFragment } from "react-relay";
import { IconPin } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { faviconUrl, getCountryName } from "@probo/helpers";
const vendorRowFragment = graphql`
fragment VendorRowFragment on Vendor {
id
name
category
websiteUrl
privacyPolicyUrl
countries
}
`;
export function VendorRow(props: { vendor: VendorRowFragment$key }) {
const vendor = useFragment(vendorRowFragment, props.vendor);
const logo = faviconUrl(vendor.websiteUrl);
const { __ } = useTranslate();
return (
<div className="flex text-sm leading-tight gap-3 md:items-center">
{logo ? (
<img src={logo} className="size-8 md:size-6 flex-none" alt="" />
) : (
<div className="size-8 md:size-6 flex-none" />
)}
<div className="flex flex-col md:flex-row flex-1 gap-0.5">
<div>{vendor.name}</div>
{vendor.privacyPolicyUrl && (
<a
href={vendor.privacyPolicyUrl}
target="_blank"
className="text-txt-info md:mx-auto"
>
{vendor.privacyPolicyUrl.split("//").at(-1)}
</a>
)}
<div className="flex gap-1 text-txt-secondary items-center">
<IconPin size={16} className="flex-none" />
<span>
{vendor.countries
.map((country) => getCountryName(__, country))
.join(", ")}
</span>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,92 @@
/**
* @generated SignedSource<<ac19789e6716697e6723a0a7b108cb81>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type ExportReportPDFInput = {
reportId: string;
};
export type AuditRowDownloadMutation$variables = {
input: ExportReportPDFInput;
};
export type AuditRowDownloadMutation$data = {
readonly exportReportPDF: {
readonly data: string;
};
};
export type AuditRowDownloadMutation = {
response: AuditRowDownloadMutation$data;
variables: AuditRowDownloadMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "ExportReportPDFPayload",
"kind": "LinkedField",
"name": "exportReportPDF",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "data",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "AuditRowDownloadMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "AuditRowDownloadMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "79de6a9f2755587818dac20ec392b8e7",
"id": null,
"metadata": {},
"name": "AuditRowDownloadMutation",
"operationKind": "mutation",
"text": "mutation AuditRowDownloadMutation(\n $input: ExportReportPDFInput!\n) {\n exportReportPDF(input: $input) {\n data\n }\n}\n"
}
};
})();
(node as any).hash = "e3d7ceb7a0da979dad83b1b8289e192e";
export default node;

View File

@@ -0,0 +1,89 @@
/**
* @generated SignedSource<<5ad8bcd5ca3b7635248d13cef0a24edf>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type AuditRowFragment$data = {
readonly framework: {
readonly id: string;
readonly name: string;
};
readonly report: {
readonly filename: string;
readonly id: string;
} | null | undefined;
readonly " $fragmentType": "AuditRowFragment";
};
export type AuditRowFragment$key = {
readonly " $data"?: AuditRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"AuditRowFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "AuditRowFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Report",
"kind": "LinkedField",
"name": "report",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "filename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "Framework",
"kind": "LinkedField",
"name": "framework",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Audit",
"abstractKey": null
};
})();
(node as any).hash = "255e447eb5ac9b4cb889e7a8f0463902";
export default node;

View File

@@ -0,0 +1,92 @@
/**
* @generated SignedSource<<cae4044b1357cd4510e12f83f19001ba>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type ExportDocumentPDFInput = {
documentId: string;
};
export type DocumentRowDownloadMutation$variables = {
input: ExportDocumentPDFInput;
};
export type DocumentRowDownloadMutation$data = {
readonly exportDocumentPDF: {
readonly data: string;
};
};
export type DocumentRowDownloadMutation = {
response: DocumentRowDownloadMutation$data;
variables: DocumentRowDownloadMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "ExportDocumentPDFPayload",
"kind": "LinkedField",
"name": "exportDocumentPDF",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "data",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "DocumentRowDownloadMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "DocumentRowDownloadMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "126681ef86a7a6aa5c831bdf53b7cf3e",
"id": null,
"metadata": {},
"name": "DocumentRowDownloadMutation",
"operationKind": "mutation",
"text": "mutation DocumentRowDownloadMutation(\n $input: ExportDocumentPDFInput!\n) {\n exportDocumentPDF(input: $input) {\n data\n }\n}\n"
}
};
})();
(node as any).hash = "8e48a364a7f9cf3d3a51a9db60ff792f";
export default node;

View File

@@ -0,0 +1,50 @@
/**
* @generated SignedSource<<266bdec238fe7397f3cff10dc7da8fc6>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DocumentRowFragment$data = {
readonly id: string;
readonly title: string;
readonly " $fragmentType": "DocumentRowFragment";
};
export type DocumentRowFragment$key = {
readonly " $data"?: DocumentRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"DocumentRowFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DocumentRowFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
}
],
"type": "Document",
"abstractKey": null
};
(node as any).hash = "437d68812bcc68724d2e347fea22b5e3";
export default node;

View File

@@ -0,0 +1,92 @@
/**
* @generated SignedSource<<febdedca828f8b6dc384f0d4f08e02f8>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type AcceptNonDisclosureAgreementInput = {
trustCenterId: string;
};
export type NDADialogSignMutation$variables = {
input: AcceptNonDisclosureAgreementInput;
};
export type NDADialogSignMutation$data = {
readonly acceptNonDisclosureAgreement: {
readonly success: boolean;
};
};
export type NDADialogSignMutation = {
response: NDADialogSignMutation$data;
variables: NDADialogSignMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "AcceptNonDisclosureAgreementPayload",
"kind": "LinkedField",
"name": "acceptNonDisclosureAgreement",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "success",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "NDADialogSignMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "NDADialogSignMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "130cfc307dca0525194e0103a0548bd0",
"id": null,
"metadata": {},
"name": "NDADialogSignMutation",
"operationKind": "mutation",
"text": "mutation NDADialogSignMutation(\n $input: AcceptNonDisclosureAgreementInput!\n) {\n acceptNonDisclosureAgreement(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "1b9447e5cbb2ec7dce4f3f9c68493555";
export default node;

View File

@@ -0,0 +1,107 @@
/**
* @generated SignedSource<<843902dd4fb6e2a5ee4d2d175ddcab03>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type CreateTrustCenterAccessInput = {
email: string;
name: string;
trustCenterId: string;
};
export type RequestAccessDialogMutation$variables = {
input: CreateTrustCenterAccessInput;
};
export type RequestAccessDialogMutation$data = {
readonly createTrustCenterAccess: {
readonly trustCenterAccess: {
readonly id: string;
};
};
};
export type RequestAccessDialogMutation = {
response: RequestAccessDialogMutation$data;
variables: RequestAccessDialogMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "CreateTrustCenterAccessPayload",
"kind": "LinkedField",
"name": "createTrustCenterAccess",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TrustCenterAccess",
"kind": "LinkedField",
"name": "trustCenterAccess",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "RequestAccessDialogMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "RequestAccessDialogMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "13fedcfe7c72292417b76b3624c5434f",
"id": null,
"metadata": {},
"name": "RequestAccessDialogMutation",
"operationKind": "mutation",
"text": "mutation RequestAccessDialogMutation(\n $input: CreateTrustCenterAccessInput!\n) {\n createTrustCenterAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "18075ac4298dd3ca05bbcf8fb1717a9d";
export default node;

View File

@@ -0,0 +1,84 @@
/**
* @generated SignedSource<<4f486a2ab88b07376ece5f29705c2553>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type CountryCode = "AD" | "AE" | "AF" | "AG" | "AI" | "AL" | "AM" | "AO" | "AQ" | "AR" | "AS" | "AT" | "AU" | "AW" | "AX" | "AZ" | "BA" | "BB" | "BD" | "BE" | "BF" | "BG" | "BH" | "BI" | "BJ" | "BL" | "BM" | "BN" | "BO" | "BQ" | "BR" | "BS" | "BT" | "BV" | "BW" | "BY" | "BZ" | "CA" | "CC" | "CD" | "CF" | "CG" | "CH" | "CI" | "CK" | "CL" | "CM" | "CN" | "CO" | "CR" | "CU" | "CV" | "CW" | "CX" | "CY" | "CZ" | "DE" | "DJ" | "DK" | "DM" | "DO" | "DZ" | "EC" | "EE" | "EG" | "EH" | "ER" | "ES" | "ET" | "FI" | "FJ" | "FK" | "FM" | "FO" | "FR" | "GA" | "GB" | "GD" | "GE" | "GF" | "GG" | "GH" | "GI" | "GL" | "GM" | "GN" | "GP" | "GQ" | "GR" | "GT" | "GU" | "GW" | "GY" | "HK" | "HM" | "HN" | "HR" | "HT" | "HU" | "ID" | "IE" | "IL" | "IM" | "IN" | "IO" | "IQ" | "IR" | "IS" | "IT" | "JE" | "JM" | "JO" | "JP" | "KE" | "KG" | "KH" | "KI" | "KM" | "KN" | "KP" | "KR" | "KW" | "KY" | "KZ" | "LA" | "LB" | "LC" | "LI" | "LK" | "LR" | "LS" | "LT" | "LU" | "LV" | "LY" | "MA" | "MC" | "MD" | "ME" | "MF" | "MG" | "MH" | "MK" | "ML" | "MM" | "MN" | "MO" | "MP" | "MQ" | "MR" | "MS" | "MT" | "MU" | "MV" | "MW" | "MX" | "MY" | "MZ" | "NA" | "NC" | "NE" | "NF" | "NG" | "NI" | "NL" | "NO" | "NP" | "NR" | "NU" | "NZ" | "OM" | "PA" | "PE" | "PF" | "PG" | "PH" | "PK" | "PL" | "PM" | "PN" | "PR" | "PS" | "PT" | "PW" | "PY" | "QA" | "RE" | "RO" | "RS" | "RU" | "RW" | "SA" | "SB" | "SC" | "SD" | "SE" | "SG" | "SH" | "SI" | "SJ" | "SK" | "SL" | "SM" | "SN" | "SO" | "SR" | "SS" | "ST" | "SV" | "SX" | "SY" | "SZ" | "TC" | "TD" | "TF" | "TG" | "TH" | "TJ" | "TK" | "TL" | "TM" | "TN" | "TO" | "TR" | "TT" | "TV" | "TW" | "TZ" | "UA" | "UG" | "UM" | "US" | "UY" | "UZ" | "VA" | "VC" | "VE" | "VG" | "VI" | "VN" | "VU" | "WF" | "WS" | "YE" | "YT" | "ZA" | "ZM" | "ZW";
export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL";
import { FragmentRefs } from "relay-runtime";
export type VendorRowFragment$data = {
readonly category: VendorCategory;
readonly countries: ReadonlyArray<CountryCode>;
readonly id: string;
readonly name: string;
readonly privacyPolicyUrl: string | null | undefined;
readonly websiteUrl: string | null | undefined;
readonly " $fragmentType": "VendorRowFragment";
};
export type VendorRowFragment$key = {
readonly " $data"?: VendorRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"VendorRowFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "VendorRowFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "privacyPolicyUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "countries",
"storageKey": null
}
],
"type": "Vendor",
"abstractKey": null
};
(node as any).hash = "eba669a2b65ef7b9990799516b7d9932";
export default node;