committed by
Sacha Al Himdani
parent
44898be9d3
commit
55e85e5f67
12
apps/trust/src/App.tsx
Normal file
12
apps/trust/src/App.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { RouterProvider } from "react-router";
|
||||
import { router } from "./routes";
|
||||
import { Toasts } from "@probo/ui";
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<>
|
||||
<RouterProvider router={router} />
|
||||
<Toasts />
|
||||
</>
|
||||
);
|
||||
}
|
||||
153
apps/trust/src/components/AuditRow.tsx
Normal file
153
apps/trust/src/components/AuditRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
80
apps/trust/src/components/DocumentRow.tsx
Normal file
80
apps/trust/src/components/DocumentRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
115
apps/trust/src/components/NDADialog.tsx
Normal file
115
apps/trust/src/components/NDADialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
125
apps/trust/src/components/OrganizationSidebar.tsx
Normal file
125
apps/trust/src/components/OrganizationSidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
130
apps/trust/src/components/PDFPreview.tsx
Normal file
130
apps/trust/src/components/PDFPreview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
88
apps/trust/src/components/PageError.tsx
Normal file
88
apps/trust/src/components/PageError.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
116
apps/trust/src/components/RequestAccessDialog.tsx
Normal file
116
apps/trust/src/components/RequestAccessDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
apps/trust/src/components/RowHeader.tsx
Normal file
9
apps/trust/src/components/RowHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
18
apps/trust/src/components/Rows.tsx
Normal file
18
apps/trust/src/components/Rows.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
apps/trust/src/components/Skeletons/MainSkeleton.tsx
Normal file
25
apps/trust/src/components/Skeletons/MainSkeleton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
apps/trust/src/components/Skeletons/TabSkeleton.tsx
Normal file
11
apps/trust/src/components/Skeletons/TabSkeleton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
53
apps/trust/src/components/VendorRow.tsx
Normal file
53
apps/trust/src/components/VendorRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
92
apps/trust/src/components/__generated__/AuditRowDownloadMutation.graphql.ts
generated
Normal file
92
apps/trust/src/components/__generated__/AuditRowDownloadMutation.graphql.ts
generated
Normal 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;
|
||||
89
apps/trust/src/components/__generated__/AuditRowFragment.graphql.ts
generated
Normal file
89
apps/trust/src/components/__generated__/AuditRowFragment.graphql.ts
generated
Normal 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;
|
||||
92
apps/trust/src/components/__generated__/DocumentRowDownloadMutation.graphql.ts
generated
Normal file
92
apps/trust/src/components/__generated__/DocumentRowDownloadMutation.graphql.ts
generated
Normal 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;
|
||||
50
apps/trust/src/components/__generated__/DocumentRowFragment.graphql.ts
generated
Normal file
50
apps/trust/src/components/__generated__/DocumentRowFragment.graphql.ts
generated
Normal 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;
|
||||
92
apps/trust/src/components/__generated__/NDADialogSignMutation.graphql.ts
generated
Normal file
92
apps/trust/src/components/__generated__/NDADialogSignMutation.graphql.ts
generated
Normal 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;
|
||||
107
apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts
generated
Normal file
107
apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts
generated
Normal 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;
|
||||
84
apps/trust/src/components/__generated__/VendorRowFragment.graphql.ts
generated
Normal file
84
apps/trust/src/components/__generated__/VendorRowFragment.graphql.ts
generated
Normal 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;
|
||||
10
apps/trust/src/helpers/documents.ts
Normal file
10
apps/trust/src/helpers/documents.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export function documentTypeLabel(type: string, __: (s: string) => string) {
|
||||
switch (type) {
|
||||
case "POLICY":
|
||||
return __("Policy");
|
||||
case "ISMS":
|
||||
return __("Security");
|
||||
default:
|
||||
return __("Other");
|
||||
}
|
||||
}
|
||||
19
apps/trust/src/hooks/useDelayedEffect.ts
Normal file
19
apps/trust/src/hooks/useDelayedEffect.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* Hook to handle cleanup after a delay
|
||||
*
|
||||
* Used for disposing the graphQL query when the component unmounts
|
||||
*/
|
||||
export function useCleanup(callback: () => void, delay: number) {
|
||||
const timer = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
return () => {
|
||||
timer.current = setTimeout(callback, delay);
|
||||
};
|
||||
}, [callback, delay]);
|
||||
}
|
||||
13
apps/trust/src/hooks/useFormWithSchema.ts
Normal file
13
apps/trust/src/hooks/useFormWithSchema.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { z, ZodTypeAny } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
export function useFormWithSchema<T extends ZodTypeAny>(
|
||||
schema: T,
|
||||
options: Parameters<typeof useForm<z.infer<T>>>[0],
|
||||
) {
|
||||
return useForm<z.infer<T>>({
|
||||
...options,
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
}
|
||||
6
apps/trust/src/hooks/useIsAuthenticated.ts
Normal file
6
apps/trust/src/hooks/useIsAuthenticated.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useContext } from "react";
|
||||
import { AuthContext } from "/providers/AuthProvider";
|
||||
|
||||
export function useIsAuthenticated(): boolean {
|
||||
return useContext(AuthContext).isAuthenticated;
|
||||
}
|
||||
63
apps/trust/src/hooks/useMutationWithToast.ts
Normal file
63
apps/trust/src/hooks/useMutationWithToast.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useCallback } from "react";
|
||||
import { useMutation, type UseMutationConfig } from "react-relay";
|
||||
import { useToast } from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import type { MutationParameters, GraphQLTaggedNode } from "relay-runtime";
|
||||
|
||||
/**
|
||||
* A decorated useMutation hook that emits toast notifications on success or error.
|
||||
*/
|
||||
export function useMutationWithToasts<T extends MutationParameters>(
|
||||
query: GraphQLTaggedNode,
|
||||
baseOptions?: {
|
||||
onSuccess?: (response: T["response"]) => void;
|
||||
errorMessage?: string;
|
||||
}
|
||||
) {
|
||||
const [mutate, isLoading] = useMutation<T>(query);
|
||||
const { toast } = useToast();
|
||||
const { __ } = useTranslate();
|
||||
const mutateWithToast = useCallback(
|
||||
(
|
||||
queryOptions: UseMutationConfig<T> & {
|
||||
onSuccess?: (response: T["response"]) => void;
|
||||
errorMessage?: string;
|
||||
}
|
||||
) => {
|
||||
const options = { ...baseOptions, ...queryOptions };
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
mutate({
|
||||
...queryOptions,
|
||||
onCompleted: (response, error) => {
|
||||
options.onCompleted?.(response, error);
|
||||
if (error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description:
|
||||
options.errorMessage ??
|
||||
__("Failed to commit this operation."),
|
||||
variant: "error",
|
||||
});
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
options.onSuccess?.(response);
|
||||
resolve();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description:
|
||||
options.errorMessage ?? __("Failed to commit this operation."),
|
||||
variant: "error",
|
||||
});
|
||||
reject(error);
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return [mutateWithToast, isLoading] as const;
|
||||
}
|
||||
13
apps/trust/src/hooks/useTrustCenter.ts
Normal file
13
apps/trust/src/hooks/useTrustCenter.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { TrustCenterContext } from "/providers/TrustCenterProvider";
|
||||
import { useContext } from "react";
|
||||
|
||||
export function useTrustCenter(): {
|
||||
id: string;
|
||||
organization: { name: string };
|
||||
} {
|
||||
const context = useContext(TrustCenterContext);
|
||||
if (!context) {
|
||||
throw new Error("useTrustCenter must be used within a TrustCenterProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
10
apps/trust/src/index.css
Normal file
10
apps/trust/src/index.css
Normal file
@@ -0,0 +1,10 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap");
|
||||
@import "tailwindcss";
|
||||
@import "@probo/ui/src/theme.css";
|
||||
@import "tw-animate-css";
|
||||
@source "../../../packages/ui/src";
|
||||
@source "../../../packages/helpers/src";
|
||||
|
||||
.react-pdf__Page__annotations.annotationLayer {
|
||||
display: none;
|
||||
}
|
||||
67
apps/trust/src/layouts/MainLayout.tsx
Normal file
67
apps/trust/src/layouts/MainLayout.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
import type { TrustGraphQuery } from "/queries/__generated__/TrustGraphQuery.graphql.ts";
|
||||
import { trustGraphQuery } from "/queries/TrustGraph.ts";
|
||||
import { Logo, TabLink, Tabs } from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { OrganizationSidebar } from "/components/OrganizationSidebar";
|
||||
import { Outlet } from "react-router";
|
||||
import { NDADialog } from "/components/NDADialog";
|
||||
import { AuthProvider } from "/providers/AuthProvider";
|
||||
import { TrustCenterProvider } from "/providers/TrustCenterProvider";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<TrustGraphQuery>;
|
||||
};
|
||||
|
||||
export function MainLayout(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const trustCenter = usePreloadedQuery(
|
||||
trustGraphQuery,
|
||||
props.queryRef,
|
||||
).trustCenterBySlug;
|
||||
|
||||
if (!trustCenter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseTabUrl = `/trust/${trustCenter.slug}`;
|
||||
const showNDADialog =
|
||||
trustCenter.isUserAuthenticated &&
|
||||
!trustCenter.hasAcceptedNonDisclosureAgreement;
|
||||
return (
|
||||
<AuthProvider isAuthenticated={trustCenter.isUserAuthenticated}>
|
||||
<TrustCenterProvider trustCenter={trustCenter}>
|
||||
{showNDADialog && (
|
||||
<NDADialog
|
||||
name={trustCenter.organization.name}
|
||||
trustCenterId={trustCenter.id}
|
||||
url={trustCenter.ndaFileUrl}
|
||||
fileName={trustCenter.ndaFileName}
|
||||
/>
|
||||
)}
|
||||
<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] lg:items-start ">
|
||||
<OrganizationSidebar trustCenter={trustCenter} />
|
||||
<main>
|
||||
<Tabs className="mb-8">
|
||||
<TabLink to={`${baseTabUrl}/overview`}>{__("Overview")}</TabLink>
|
||||
<TabLink to={`${baseTabUrl}/documents`}>
|
||||
{__("Documents")}
|
||||
</TabLink>
|
||||
<TabLink to={`${baseTabUrl}/subprocessors`}>
|
||||
{__("Subprocessors")}
|
||||
</TabLink>
|
||||
</Tabs>
|
||||
<Outlet context={{ trustCenter }} />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://www.getprobo.com/"
|
||||
className="flex gap-2 text-sm font-medium text-txt-tertiary items-center w-max mx-auto my-10"
|
||||
>
|
||||
{__("Powered by")} <Logo withPicto className="h-6" />
|
||||
</a>
|
||||
</TrustCenterProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
27
apps/trust/src/main.tsx
Normal file
27
apps/trust/src/main.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import { App } from "./App";
|
||||
import { RelayProvider } from "./providers/RelayProviders";
|
||||
import { TranslatorProvider } from "./providers/TranslatorProvider";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RelayProvider>
|
||||
<TranslatorProvider>
|
||||
<App />
|
||||
</TranslatorProvider>
|
||||
</RelayProvider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
3
apps/trust/src/module.d.ts
vendored
Normal file
3
apps/trust/src/module.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module "https://cdn.jsdelivr.net/npm/pdfjs-dist@5.4.149/+esm" {
|
||||
export * from "pdfjs-dist";
|
||||
}
|
||||
80
apps/trust/src/pages/AccessPage.tsx
Normal file
80
apps/trust/src/pages/AccessPage.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router";
|
||||
import { buildEndpoint } from "/providers/RelayProviders";
|
||||
import { PageError } from "/components/PageError";
|
||||
import { Spinner } from "@probo/ui";
|
||||
|
||||
/**
|
||||
* Page requested with an access token to authenticate the user for the Trust center
|
||||
*/
|
||||
export function AccessPage() {
|
||||
const { __ } = useTranslate();
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get("token");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isValidRequest = !!(slug && token);
|
||||
const [error, setError] = useState<string | null>(() => {
|
||||
if (!slug) {
|
||||
return __("Invalid trust center");
|
||||
}
|
||||
if (!token) {
|
||||
return __("Invalid access token");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Initiate an authentication attempt
|
||||
useEffect(() => {
|
||||
if (!isValidRequest) {
|
||||
return;
|
||||
}
|
||||
fetch(buildEndpoint("/api/trust/v1/auth/authenticate"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
// For invalid response throw an error
|
||||
if (!response.ok) {
|
||||
const defaultMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
return response
|
||||
.json()
|
||||
.then((json) => {
|
||||
throw new Error(json.message ?? defaultMessage);
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error(defaultMessage);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
navigate(`/trust/${slug}`);
|
||||
return;
|
||||
}
|
||||
throw new Error(data.message ?? __("Authentication failed"));
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error.message);
|
||||
});
|
||||
}, [isValidRequest, slug, token, __, navigate]);
|
||||
|
||||
if (error) {
|
||||
return <PageError error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 text-center flex items-center justify-center gap-2">
|
||||
<Spinner size={16} />
|
||||
{__("Redirecting to trust center")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
apps/trust/src/pages/DocumentsPage.tsx
Normal file
42
apps/trust/src/pages/DocumentsPage.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
import { trustDocumentsQuery } from "/queries/TrustGraph";
|
||||
import type { TrustGraphDocumentsQuery } from "/queries/__generated__/TrustGraphDocumentsQuery.graphql.ts";
|
||||
import { groupBy, objectEntries } from "@probo/helpers";
|
||||
import { documentTypeLabel } from "/helpers/documents";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Fragment } from "react";
|
||||
import { DocumentRow } from "/components/DocumentRow";
|
||||
import { Rows } from "/components/Rows.tsx";
|
||||
import { RowHeader } from "/components/RowHeader.tsx";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<TrustGraphDocumentsQuery>;
|
||||
};
|
||||
|
||||
export function DocumentsPage({ queryRef }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const data = usePreloadedQuery(trustDocumentsQuery, queryRef);
|
||||
const documents =
|
||||
data.trustCenterBySlug?.documents.edges.map((edge) => edge.node) ?? [];
|
||||
const documentsPerType = groupBy(documents, (document) =>
|
||||
documentTypeLabel(document.documentType, __),
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-medium mb-1">{__("Documents")}</h2>
|
||||
<p className="text-sm text-txt-secondary mb-4">
|
||||
{__("Security and compliance documentation:")}
|
||||
</p>
|
||||
<Rows className="mb-8">
|
||||
{objectEntries(documentsPerType).map(([label, documents]) => (
|
||||
<Fragment key={label}>
|
||||
<RowHeader>{label}</RowHeader>
|
||||
{documents.map((document) => (
|
||||
<DocumentRow key={document.id} document={document} />
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</Rows>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
147
apps/trust/src/pages/OverviewPage.tsx
Normal file
147
apps/trust/src/pages/OverviewPage.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import type { OverviewFragment$key } from "./__generated__/OverviewFragment.graphql";
|
||||
import { Link, useOutletContext } from "react-router";
|
||||
import { groupBy, objectEntries, sprintf } from "@probo/helpers";
|
||||
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Card, IconChevronRight } from "@probo/ui";
|
||||
import { AuditRow } from "/components/AuditRow";
|
||||
import { documentTypeLabel } from "/helpers/documents";
|
||||
import { Fragment } from "react";
|
||||
import { DocumentRow } from "/components/DocumentRow";
|
||||
import { VendorRow } from "/components/VendorRow";
|
||||
import { RowHeader } from "/components/RowHeader.tsx";
|
||||
import { Rows } from "/components/Rows.tsx";
|
||||
|
||||
const overviewFragment = graphql`
|
||||
fragment OverviewFragment on TrustCenter {
|
||||
references(first: 14) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
websiteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
vendors(first: 3) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...VendorRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
documents(first: 5) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...DocumentRowFragment
|
||||
documentType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function OverviewPage() {
|
||||
const { trustCenter } = useOutletContext<{
|
||||
trustCenter: OverviewFragment$key &
|
||||
TrustGraphQuery$data["trustCenterBySlug"];
|
||||
}>();
|
||||
const { __ } = useTranslate();
|
||||
const fragment = useFragment(overviewFragment, trustCenter);
|
||||
const documentsPerType = groupBy(
|
||||
fragment.documents.edges.map((edge) => edge.node),
|
||||
(node) => documentTypeLabel(node.documentType, __),
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-medium mb-1">{__("Documents")}</h2>
|
||||
<p className="text-sm text-txt-secondary mb-4">
|
||||
{__("Security and compliance documentation:")}
|
||||
</p>
|
||||
<Rows className="mb-8">
|
||||
<RowHeader>{__("Certifications")}</RowHeader>
|
||||
{trustCenter.audits.edges.map((edge) => (
|
||||
<AuditRow key={edge.node.id} audit={edge.node} />
|
||||
))}
|
||||
{objectEntries(documentsPerType).map(([label, documents]) => (
|
||||
<Fragment key={label}>
|
||||
<RowHeader>{label}</RowHeader>
|
||||
{documents.map((document) => (
|
||||
<DocumentRow key={document.id} document={document} />
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
<Link
|
||||
to={`/trust/${trustCenter.slug}/documents`}
|
||||
className="text-sm font-medium flex gap-2 items-center"
|
||||
>
|
||||
{__("See all documents")}
|
||||
<IconChevronRight size={16} />
|
||||
</Link>
|
||||
</Rows>
|
||||
|
||||
<h2 className="font-medium mb-1">{__("Subprocessors")}</h2>
|
||||
<p className="text-sm text-txt-secondary mb-4">
|
||||
{sprintf(
|
||||
__("Third-party subprocessors %s work with:"),
|
||||
trustCenter.organization.name,
|
||||
)}
|
||||
</p>
|
||||
<Rows className="mb-8 *:py-5">
|
||||
{fragment.vendors.edges.map((edge) => (
|
||||
<VendorRow key={edge.node.id} vendor={edge.node} />
|
||||
))}
|
||||
<Link
|
||||
to={`/trust/${trustCenter.slug}/subprocessors`}
|
||||
className="text-sm font-medium flex gap-2 items-center"
|
||||
>
|
||||
{__("See all subprocessors")}
|
||||
<IconChevronRight size={16} />
|
||||
</Link>
|
||||
</Rows>
|
||||
|
||||
<References
|
||||
references={fragment.references.edges.map((edge) => edge.node)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Reference = {
|
||||
name: string;
|
||||
logoUrl: string;
|
||||
websiteUrl: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
function References({ references }: { references: Reference[] }) {
|
||||
const { __ } = useTranslate();
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-medium mb-4">{__("Trusted by")}</h2>
|
||||
<Card className="grid grid-cols-2 flex-wrap p-6 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7">
|
||||
{references.map((reference) => (
|
||||
<a
|
||||
key={reference.id}
|
||||
href={reference.websiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex flex-col justify-center items-center gap-2"
|
||||
>
|
||||
<img
|
||||
src={reference.logoUrl}
|
||||
alt={reference.name}
|
||||
className="rounded-2xl size-12 block"
|
||||
/>
|
||||
<span className="text-xs text-txt-secondary">{reference.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
apps/trust/src/pages/SubprocessorsPage.tsx
Normal file
34
apps/trust/src/pages/SubprocessorsPage.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
import { trustVendorsQuery } from "/queries/TrustGraph";
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import type { TrustGraphVendorsQuery } from "/queries/__generated__/TrustGraphVendorsQuery.graphql";
|
||||
import { VendorRow } from "/components/VendorRow";
|
||||
import { Rows } from "/components/Rows.tsx";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<TrustGraphVendorsQuery>;
|
||||
};
|
||||
|
||||
export function SubprocessorsPage({ queryRef }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const data = usePreloadedQuery(trustVendorsQuery, queryRef);
|
||||
const vendors =
|
||||
data.trustCenterBySlug?.vendors.edges.map((edge) => edge.node) ?? [];
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-medium mb-1">{__("Subprocessors")}</h2>
|
||||
<p className="text-sm text-txt-secondary mb-4">
|
||||
{sprintf(
|
||||
__("Third-party subprocessors %s work with:"),
|
||||
data.trustCenterBySlug?.organization.name ?? "",
|
||||
)}
|
||||
</p>
|
||||
<Rows>
|
||||
{vendors.map((vendor) => (
|
||||
<VendorRow key={vendor.id} vendor={vendor} />
|
||||
))}
|
||||
</Rows>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
229
apps/trust/src/pages/__generated__/OverviewFragment.graphql.ts
generated
Normal file
229
apps/trust/src/pages/__generated__/OverviewFragment.graphql.ts
generated
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* @generated SignedSource<<0767a7e9f6fb3f0464027b92a5239521>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
export type DocumentType = "ISMS" | "OTHER" | "POLICY";
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type OverviewFragment$data = {
|
||||
readonly documents: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly documentType: DocumentType;
|
||||
readonly id: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"DocumentRowFragment">;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly references: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly id: string;
|
||||
readonly logoUrl: string;
|
||||
readonly name: string;
|
||||
readonly websiteUrl: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly vendors: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly id: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"VendorRowFragment">;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly " $fragmentType": "OverviewFragment";
|
||||
};
|
||||
export type OverviewFragment$key = {
|
||||
readonly " $data"?: OverviewFragment$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"OverviewFragment">;
|
||||
};
|
||||
|
||||
const node: ReaderFragment = (function(){
|
||||
var v0 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "OverviewFragment",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 14
|
||||
}
|
||||
],
|
||||
"concreteType": "TrustCenterReferenceConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "references",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "TrustCenterReferenceEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "TrustCenterReference",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "websiteUrl",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "references(first:14)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 3
|
||||
}
|
||||
],
|
||||
"concreteType": "VendorConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "vendors",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "VendorEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Vendor",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "VendorRowFragment"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "vendors(first:3)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 5
|
||||
}
|
||||
],
|
||||
"concreteType": "DocumentConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "documents",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "DocumentEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Document",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "DocumentRowFragment"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "documentType",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "documents(first:5)"
|
||||
}
|
||||
],
|
||||
"type": "TrustCenter",
|
||||
"abstractKey": null
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "2f42031d2017248051c8dfea72b4e502";
|
||||
|
||||
export default node;
|
||||
13
apps/trust/src/providers/AuthProvider.tsx
Normal file
13
apps/trust/src/providers/AuthProvider.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createContext, useMemo } from "react";
|
||||
|
||||
export const AuthContext = createContext({ isAuthenticated: false });
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
isAuthenticated: boolean;
|
||||
};
|
||||
|
||||
export function AuthProvider({ children, isAuthenticated }: Props) {
|
||||
const value = useMemo(() => ({ isAuthenticated }), [isAuthenticated]);
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
146
apps/trust/src/providers/RelayProviders.tsx
Normal file
146
apps/trust/src/providers/RelayProviders.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
Environment,
|
||||
type FetchFunction,
|
||||
Network,
|
||||
RecordSource,
|
||||
Store,
|
||||
} from "relay-runtime";
|
||||
import { GraphQLError } from "graphql";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { RelayEnvironmentProvider } from "react-relay";
|
||||
|
||||
export class UnAuthenticatedError extends Error {
|
||||
constructor() {
|
||||
super("UNAUTHENTICATED");
|
||||
this.name = "UnAuthenticatedError";
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalServerError extends Error {
|
||||
constructor() {
|
||||
super("INTERNAL_SERVER_ERROR");
|
||||
this.name = "InternalServerError";
|
||||
}
|
||||
}
|
||||
|
||||
export function buildEndpoint(path: string): string {
|
||||
const host = import.meta.env.VITE_API_URL;
|
||||
|
||||
if (!host) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const formattedHost =
|
||||
host.startsWith("http://") || host.startsWith("https://")
|
||||
? host
|
||||
: `https://${host}`;
|
||||
|
||||
const url = new URL(formattedHost);
|
||||
|
||||
if (path) {
|
||||
url.pathname = path.startsWith("/") ? path : `/${path}`;
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const hasUnauthenticatedError = (error: GraphQLError) =>
|
||||
error.extensions?.code == "UNAUTHENTICATED";
|
||||
|
||||
const fetchRelay: FetchFunction = async (
|
||||
request,
|
||||
variables,
|
||||
_,
|
||||
uploadables,
|
||||
) => {
|
||||
const requestInit: RequestInit = {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {},
|
||||
};
|
||||
|
||||
if (uploadables) {
|
||||
const formData = new FormData();
|
||||
formData.append(
|
||||
"operations",
|
||||
JSON.stringify({
|
||||
operationName: request.name,
|
||||
query: request.text,
|
||||
variables: variables,
|
||||
}),
|
||||
);
|
||||
|
||||
const uploadableMap: {
|
||||
[key: string]: string[];
|
||||
} = {};
|
||||
|
||||
Object.keys(uploadables).forEach((key, index) => {
|
||||
uploadableMap[index] = [`variables.${key}`];
|
||||
});
|
||||
|
||||
formData.append("map", JSON.stringify(uploadableMap));
|
||||
|
||||
Object.keys(uploadables).forEach((key, index) => {
|
||||
formData.append(index.toString(), uploadables[key]);
|
||||
});
|
||||
|
||||
requestInit.body = formData;
|
||||
} else {
|
||||
requestInit.headers = {
|
||||
Accept:
|
||||
"application/graphql-response+json; charset=utf-8, application/json; charset=utf-8",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
requestInit.body = JSON.stringify({
|
||||
operationName: request.name,
|
||||
query: request.text,
|
||||
variables,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
buildEndpoint("/api/trust/v1/graphql"),
|
||||
requestInit,
|
||||
);
|
||||
|
||||
if (response.status === 500) {
|
||||
throw new InternalServerError();
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
if (json.errors) {
|
||||
const errors = json.errors as GraphQLError[];
|
||||
|
||||
if (errors.find(hasUnauthenticatedError)) {
|
||||
throw new UnAuthenticatedError();
|
||||
}
|
||||
|
||||
throw new Error(`Error fetching GraphQL query '${request.name}'`);
|
||||
}
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
const source = new RecordSource();
|
||||
const store = new Store(source, {
|
||||
queryCacheExpirationTime: 1 * 60 * 1000,
|
||||
gcReleaseBufferSize: 20,
|
||||
});
|
||||
|
||||
export const relayEnvironment = new Environment({
|
||||
network: Network.create(fetchRelay),
|
||||
store,
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider for relay with the probo environment
|
||||
*/
|
||||
export function RelayProvider({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<RelayEnvironmentProvider environment={relayEnvironment}>
|
||||
{children}
|
||||
</RelayEnvironmentProvider>
|
||||
);
|
||||
}
|
||||
18
apps/trust/src/providers/TranslatorProvider.tsx
Normal file
18
apps/trust/src/providers/TranslatorProvider.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { TranslatorProvider as ProboTranslatorProvider } from "../../../../packages/i18n/TranslatorProvider";
|
||||
|
||||
// TODO : implement a way to retrieve translations strings
|
||||
const loader = () => {
|
||||
return Promise.resolve({} as Record<string, string>);
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider for the translator
|
||||
*/
|
||||
export function TranslatorProvider({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<ProboTranslatorProvider lang="en" loader={loader}>
|
||||
{children}
|
||||
</ProboTranslatorProvider>
|
||||
);
|
||||
}
|
||||
20
apps/trust/src/providers/TrustCenterProvider.tsx
Normal file
20
apps/trust/src/providers/TrustCenterProvider.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createContext, type ReactNode } from "react";
|
||||
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
|
||||
|
||||
export const TrustCenterContext = createContext<
|
||||
TrustGraphQuery$data["trustCenterBySlug"] | null
|
||||
>(null);
|
||||
|
||||
export const TrustCenterProvider = ({
|
||||
children,
|
||||
trustCenter,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
trustCenter: TrustGraphQuery$data["trustCenterBySlug"];
|
||||
}) => {
|
||||
return (
|
||||
<TrustCenterContext.Provider value={trustCenter}>
|
||||
{children}
|
||||
</TrustCenterContext.Provider>
|
||||
);
|
||||
};
|
||||
70
apps/trust/src/queries/TrustGraph.ts
Normal file
70
apps/trust/src/queries/TrustGraph.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
export const trustGraphQuery = graphql`
|
||||
query TrustGraphQuery($slug: String!) {
|
||||
trustCenterBySlug(slug: $slug) {
|
||||
id
|
||||
slug
|
||||
isUserAuthenticated
|
||||
hasAcceptedNonDisclosureAgreement
|
||||
ndaFileName
|
||||
ndaFileUrl
|
||||
organization {
|
||||
name
|
||||
description
|
||||
websiteUrl
|
||||
logoUrl
|
||||
email
|
||||
headquarterAddress
|
||||
}
|
||||
...OverviewFragment
|
||||
audits(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...AuditRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const trustDocumentsQuery = graphql`
|
||||
query TrustGraphDocumentsQuery($slug: String!) {
|
||||
trustCenterBySlug(slug: $slug) {
|
||||
id
|
||||
organization {
|
||||
name
|
||||
}
|
||||
documents(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
documentType
|
||||
...DocumentRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const trustVendorsQuery = graphql`
|
||||
query TrustGraphVendorsQuery($slug: String!) {
|
||||
trustCenterBySlug(slug: $slug) {
|
||||
id
|
||||
organization {
|
||||
name
|
||||
}
|
||||
vendors(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...VendorRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
245
apps/trust/src/queries/__generated__/TrustGraphDocumentsQuery.graphql.ts
generated
Normal file
245
apps/trust/src/queries/__generated__/TrustGraphDocumentsQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* @generated SignedSource<<55d256287ee091b57754d2d76080ad25>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type DocumentType = "ISMS" | "OTHER" | "POLICY";
|
||||
export type TrustGraphDocumentsQuery$variables = {
|
||||
slug: string;
|
||||
};
|
||||
export type TrustGraphDocumentsQuery$data = {
|
||||
readonly trustCenterBySlug: {
|
||||
readonly documents: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly documentType: DocumentType;
|
||||
readonly id: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"DocumentRowFragment">;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly id: string;
|
||||
readonly organization: {
|
||||
readonly name: string;
|
||||
};
|
||||
} | null | undefined;
|
||||
};
|
||||
export type TrustGraphDocumentsQuery = {
|
||||
response: TrustGraphDocumentsQuery$data;
|
||||
variables: TrustGraphDocumentsQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "slug"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "slug",
|
||||
"variableName": "slug"
|
||||
}
|
||||
],
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v4 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
v5 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "documentType",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "TrustGraphDocumentsQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": "TrustCenter",
|
||||
"kind": "LinkedField",
|
||||
"name": "trustCenterBySlug",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v4/*: any*/),
|
||||
"concreteType": "DocumentConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "documents",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "DocumentEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Document",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v5/*: any*/),
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "DocumentRowFragment"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "documents(first:50)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "TrustGraphDocumentsQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": "TrustCenter",
|
||||
"kind": "LinkedField",
|
||||
"name": "trustCenterBySlug",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
(v2/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v4/*: any*/),
|
||||
"concreteType": "DocumentConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "documents",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "DocumentEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Document",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v5/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "title",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "documents(first:50)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "71e1dc4102d19cdd2fb8d0ff60a391ca",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "TrustGraphDocumentsQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query TrustGraphDocumentsQuery(\n $slug: String!\n) {\n trustCenterBySlug(slug: $slug) {\n id\n organization {\n name\n id\n }\n documents(first: 50) {\n edges {\n node {\n id\n documentType\n ...DocumentRowFragment\n }\n }\n }\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "9c56f70fca2afb316f4fd38d646ec545";
|
||||
|
||||
export default node;
|
||||
525
apps/trust/src/queries/__generated__/TrustGraphQuery.graphql.ts
generated
Normal file
525
apps/trust/src/queries/__generated__/TrustGraphQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,525 @@
|
||||
/**
|
||||
* @generated SignedSource<<669a97d59bae0528905d34c18586ce11>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type TrustGraphQuery$variables = {
|
||||
slug: string;
|
||||
};
|
||||
export type TrustGraphQuery$data = {
|
||||
readonly trustCenterBySlug: {
|
||||
readonly audits: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly id: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"AuditRowFragment">;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly hasAcceptedNonDisclosureAgreement: boolean;
|
||||
readonly id: string;
|
||||
readonly isUserAuthenticated: boolean;
|
||||
readonly ndaFileName: string | null | undefined;
|
||||
readonly ndaFileUrl: string | null | undefined;
|
||||
readonly organization: {
|
||||
readonly description: string | null | undefined;
|
||||
readonly email: string | null | undefined;
|
||||
readonly headquarterAddress: string | null | undefined;
|
||||
readonly logoUrl: string | null | undefined;
|
||||
readonly name: string;
|
||||
readonly websiteUrl: string | null | undefined;
|
||||
};
|
||||
readonly slug: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"OverviewFragment">;
|
||||
} | null | undefined;
|
||||
};
|
||||
export type TrustGraphQuery = {
|
||||
response: TrustGraphQuery$data;
|
||||
variables: TrustGraphQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "slug"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "slug",
|
||||
"variableName": "slug"
|
||||
}
|
||||
],
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "slug",
|
||||
"storageKey": null
|
||||
},
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "isUserAuthenticated",
|
||||
"storageKey": null
|
||||
},
|
||||
v5 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasAcceptedNonDisclosureAgreement",
|
||||
"storageKey": null
|
||||
},
|
||||
v6 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "ndaFileName",
|
||||
"storageKey": null
|
||||
},
|
||||
v7 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "ndaFileUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
v8 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v9 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
v10 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "websiteUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
v11 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
v12 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
v13 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "headquarterAddress",
|
||||
"storageKey": null
|
||||
},
|
||||
v14 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 50
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "TrustGraphQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": "TrustCenter",
|
||||
"kind": "LinkedField",
|
||||
"name": "trustCenterBySlug",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/),
|
||||
(v10/*: any*/),
|
||||
(v11/*: any*/),
|
||||
(v12/*: any*/),
|
||||
(v13/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "OverviewFragment"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v14/*: any*/),
|
||||
"concreteType": "AuditConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "audits",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "AuditEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Audit",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "AuditRowFragment"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "audits(first:50)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "TrustGraphQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": "TrustCenter",
|
||||
"kind": "LinkedField",
|
||||
"name": "trustCenterBySlug",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/),
|
||||
(v10/*: any*/),
|
||||
(v11/*: any*/),
|
||||
(v12/*: any*/),
|
||||
(v13/*: any*/),
|
||||
(v2/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 14
|
||||
}
|
||||
],
|
||||
"concreteType": "TrustCenterReferenceConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "references",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "TrustCenterReferenceEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "TrustCenterReference",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v8/*: any*/),
|
||||
(v11/*: any*/),
|
||||
(v10/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "references(first:14)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 3
|
||||
}
|
||||
],
|
||||
"concreteType": "VendorConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "vendors",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "VendorEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Vendor",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v8/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "category",
|
||||
"storageKey": null
|
||||
},
|
||||
(v10/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "privacyPolicyUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "countries",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "vendors(first:3)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 5
|
||||
}
|
||||
],
|
||||
"concreteType": "DocumentConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "documents",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "DocumentEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Document",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "title",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "documentType",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "documents(first:5)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v14/*: any*/),
|
||||
"concreteType": "AuditConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "audits",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "AuditEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Audit",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Report",
|
||||
"kind": "LinkedField",
|
||||
"name": "report",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: 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": [
|
||||
(v2/*: any*/),
|
||||
(v8/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "audits(first:50)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "00f2723e0dac553f037af3570903fb51",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "TrustGraphQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query TrustGraphQuery(\n $slug: String!\n) {\n trustCenterBySlug(slug: $slug) {\n id\n slug\n isUserAuthenticated\n hasAcceptedNonDisclosureAgreement\n ndaFileName\n ndaFileUrl\n organization {\n name\n description\n websiteUrl\n logoUrl\n email\n headquarterAddress\n id\n }\n ...OverviewFragment\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n report {\n id\n filename\n }\n framework {\n id\n name\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n}\n\nfragment OverviewFragment on TrustCenter {\n references(first: 14) {\n edges {\n node {\n id\n name\n logoUrl\n websiteUrl\n }\n }\n }\n vendors(first: 3) {\n edges {\n node {\n id\n ...VendorRowFragment\n }\n }\n }\n documents(first: 5) {\n edges {\n node {\n id\n ...DocumentRowFragment\n documentType\n }\n }\n }\n}\n\nfragment VendorRowFragment on Vendor {\n id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "192a8d21a41871de201009a2dae65870";
|
||||
|
||||
export default node;
|
||||
256
apps/trust/src/queries/__generated__/TrustGraphVendorsQuery.graphql.ts
generated
Normal file
256
apps/trust/src/queries/__generated__/TrustGraphVendorsQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* @generated SignedSource<<43ebc34eede5683873c41a65f93e58b1>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type TrustGraphVendorsQuery$variables = {
|
||||
slug: string;
|
||||
};
|
||||
export type TrustGraphVendorsQuery$data = {
|
||||
readonly trustCenterBySlug: {
|
||||
readonly id: string;
|
||||
readonly organization: {
|
||||
readonly name: string;
|
||||
};
|
||||
readonly vendors: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly id: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"VendorRowFragment">;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
} | null | undefined;
|
||||
};
|
||||
export type TrustGraphVendorsQuery = {
|
||||
response: TrustGraphVendorsQuery$data;
|
||||
variables: TrustGraphVendorsQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "slug"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "slug",
|
||||
"variableName": "slug"
|
||||
}
|
||||
],
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v4 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 50
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "TrustGraphVendorsQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": "TrustCenter",
|
||||
"kind": "LinkedField",
|
||||
"name": "trustCenterBySlug",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v4/*: any*/),
|
||||
"concreteType": "VendorConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "vendors",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "VendorEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Vendor",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "VendorRowFragment"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "vendors(first:50)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "TrustGraphVendorsQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": "TrustCenter",
|
||||
"kind": "LinkedField",
|
||||
"name": "trustCenterBySlug",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
(v2/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v4/*: any*/),
|
||||
"concreteType": "VendorConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "vendors",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "VendorEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Vendor",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"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
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "vendors(first:50)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "1975e4b062f66fef036fc08bd7ff0b53",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "TrustGraphVendorsQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query TrustGraphVendorsQuery(\n $slug: String!\n) {\n trustCenterBySlug(slug: $slug) {\n id\n organization {\n name\n id\n }\n vendors(first: 50) {\n edges {\n node {\n id\n ...VendorRowFragment\n }\n }\n }\n }\n}\n\nfragment VendorRowFragment on Vendor {\n id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "b21c7b35a682aa1fc4210e1c717e55b4";
|
||||
|
||||
export default node;
|
||||
153
apps/trust/src/routes.tsx
Normal file
153
apps/trust/src/routes.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
createBrowserRouter,
|
||||
Navigate,
|
||||
redirect,
|
||||
type RouteObject,
|
||||
useLoaderData,
|
||||
useRouteError,
|
||||
} from "react-router";
|
||||
import { type ComponentType, Fragment, Suspense } from "react";
|
||||
import {
|
||||
relayEnvironment,
|
||||
UnAuthenticatedError,
|
||||
} from "./providers/RelayProviders";
|
||||
import { loadQuery, type PreloadedQuery } from "react-relay";
|
||||
import { useCleanup } from "./hooks/useDelayedEffect";
|
||||
import { PageError } from "./components/PageError";
|
||||
import { MainLayout } from "/layouts/MainLayout";
|
||||
import {
|
||||
trustGraphQuery,
|
||||
trustDocumentsQuery,
|
||||
trustVendorsQuery,
|
||||
} from "/queries/TrustGraph";
|
||||
import { OverviewPage } from "/pages/OverviewPage";
|
||||
import { DocumentsPage } from "/pages/DocumentsPage";
|
||||
import { SubprocessorsPage } from "/pages/SubprocessorsPage";
|
||||
import { AccessPage } from "./pages/AccessPage.tsx";
|
||||
import { TabSkeleton } from "./components/Skeletons/TabSkeleton";
|
||||
import { MainSkeleton } from "./components/Skeletons/MainSkeleton";
|
||||
|
||||
export type AppRoute = Omit<RouteObject, "Component" | "children"> & {
|
||||
Component?: ComponentType<any>;
|
||||
children?: AppRoute[];
|
||||
fallback?: ComponentType;
|
||||
queryLoader?: (params: any) => PreloadedQuery<any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Top level error boundary
|
||||
*/
|
||||
function ErrorBoundary({ error: propsError }: { error?: string }) {
|
||||
const error = useRouteError() ?? propsError;
|
||||
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
return <Navigate to="/auth/login" />;
|
||||
}
|
||||
|
||||
return <PageError error={error?.toString()} />;
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
Component: Fragment,
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
},
|
||||
{
|
||||
path: "/trust/:slug/access",
|
||||
Component: AccessPage,
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
},
|
||||
{
|
||||
path: "/trust/:slug",
|
||||
queryLoader: ({ slug }) =>
|
||||
loadQuery(relayEnvironment, trustGraphQuery, { slug: slug }),
|
||||
Component: MainLayout,
|
||||
fallback: MainSkeleton,
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
loader: ({ params }) => {
|
||||
throw redirect(`/trust/${params.slug}/overview`);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "overview",
|
||||
fallback: TabSkeleton,
|
||||
Component: OverviewPage,
|
||||
},
|
||||
{
|
||||
path: "documents",
|
||||
fallback: TabSkeleton,
|
||||
Component: DocumentsPage,
|
||||
queryLoader: ({ slug }) =>
|
||||
loadQuery(relayEnvironment, trustDocumentsQuery, { slug: slug }),
|
||||
},
|
||||
{
|
||||
path: "subprocessors",
|
||||
fallback: TabSkeleton,
|
||||
Component: SubprocessorsPage,
|
||||
queryLoader: ({ slug }) =>
|
||||
loadQuery(relayEnvironment, trustVendorsQuery, { slug: slug }),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Fallback URL to the NotFound Page
|
||||
{
|
||||
path: "*",
|
||||
Component: PageError,
|
||||
},
|
||||
] satisfies AppRoute[];
|
||||
|
||||
/**
|
||||
* Wrap components with suspense to handle lazy loading & relay loading states
|
||||
*/
|
||||
function routeTransformer({
|
||||
fallback: FallbackComponent,
|
||||
queryLoader,
|
||||
...route
|
||||
}: AppRoute): RouteObject {
|
||||
let result = { ...route };
|
||||
if (FallbackComponent && route.Component) {
|
||||
const OriginalComponent = route.Component;
|
||||
result = {
|
||||
...result,
|
||||
Component: (props) => (
|
||||
<Suspense fallback={<FallbackComponent />}>
|
||||
<OriginalComponent {...props} />
|
||||
</Suspense>
|
||||
),
|
||||
};
|
||||
}
|
||||
if (queryLoader && route.Component) {
|
||||
const OriginalComponent = route.Component;
|
||||
result = {
|
||||
...result,
|
||||
loader: ({ params }) => {
|
||||
const query = queryLoader(params as Record<string, string>);
|
||||
return {
|
||||
queryRef: query,
|
||||
dispose: query.dispose,
|
||||
};
|
||||
},
|
||||
Component: () => {
|
||||
const { queryRef, dispose } = useLoaderData();
|
||||
|
||||
useCleanup(dispose, 1000);
|
||||
|
||||
return (
|
||||
<Suspense fallback={FallbackComponent ? <FallbackComponent /> : null}>
|
||||
<OriginalComponent queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
children: route.children?.map(routeTransformer),
|
||||
} as RouteObject;
|
||||
}
|
||||
|
||||
export const router = createBrowserRouter(routes.map(routeTransformer));
|
||||
8
apps/trust/src/types.ts
Normal file
8
apps/trust/src/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type NodeOf<T> =
|
||||
NonNullable<T> extends
|
||||
| { readonly edges: ReadonlyArray<{ readonly node: infer U }> }
|
||||
| undefined
|
||||
? U
|
||||
: never;
|
||||
|
||||
export type ItemOf<T> = T extends (infer U)[] ? U : never;
|
||||
9
apps/trust/src/vite-env.d.ts
vendored
Normal file
9
apps/trust/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Reference in New Issue
Block a user