Add granular permissions on document and report

Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
Jonathan
2025-10-03 15:56:15 +02:00
committed by Sacha Al Himdani
parent 3543de0bf8
commit 2ee30efed0
11 changed files with 603 additions and 156 deletions

View File

@@ -6,7 +6,6 @@ import {
Button,
Dialog,
DialogContent,
FrameworkLogo,
IconArrowInbox,
IconLock,
IconMedal,
@@ -14,11 +13,10 @@ import {
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 { type PropsWithChildren, useState } from "react";
import { useLocation } from "react-router";
import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx";
@@ -35,6 +33,8 @@ const auditRowFragment = graphql`
report {
id
filename
isUserAuthorized
hasUserRequestedAccess
}
framework {
id
@@ -46,7 +46,6 @@ const auditRowFragment = graphql`
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 = () => {
@@ -64,35 +63,42 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) {
},
});
};
const [hasRequested, setHasRequested] = useState(
audit.report?.hasUserRequestedAccess,
);
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" />
<IconMedal size={16} className="flex-none text-txt-tertiary" />
{audit.framework.name}
</div>
{audit.report ? (
isAuthenticated ? (
{audit.report && audit.report.isUserAuthorized && (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
)}
{audit.report && !audit.report.isUserAuthorized && (
<RequestAccessDialog
reportId={audit.report.id}
onSuccess={() => setHasRequested(true)}
>
<Button
disabled={hasRequested}
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
icon={IconLock}
>
{__("Download")}
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
) : (
<RequestAccessDialog>
<Button
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
>
{__("Request access")}
</Button>
</RequestAccessDialog>
)
) : null}
</RequestAccessDialog>
)}
</div>
);
}
@@ -108,41 +114,28 @@ 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"
title={`Logo ${audit.framework.name}`}
>
<div
className="bg-[#F0F7E2] aspect-square w-full rounded-full text-xs text-[#000] font-bold flex items-center justify-center pb-6 px-2"
style={{ background: "url(/trust/logos/blank.svg) no-repeat" }}
>
<span className="line-clamp-2 overflow-hidden">
{" "}
{audit.framework.name}
</span>
</div>
</button>
</AuditDialog>
<AuditDialog audit={props.audit}>
<AuditDialog
audit={props.audit}
logo={logos[audit.framework.name as keyof typeof logos]}
>
<button
className="block cursor-pointer aspect-square"
title={`Logo ${audit.framework.name}`}
>
{audit.framework.name in logos ? (
<img
className="block aspect-square w-full"
width={75}
height={75}
src={logos[audit.framework.name as keyof typeof logos]}
alt={`Logo ${audit.framework.name}`}
alt={`${audit.framework.name} Logo`}
/>
) : (
<div
className="bg-[#F0F7E2] aspect-square w-full rounded-full text-xs text-[#000] font-bold flex items-center justify-center pb-4 px-2"
style={{ background: "url(/trust/logos/blank.png)" }}
className="bg-[#F0F7E2] aspect-square w-full rounded-full text-xs text-[#000] font-bold flex items-center justify-center pb-6 px-2"
style={{ background: "url(/trust/logos/blank.svg) no-repeat" }}
>
{audit.framework.name}
<span className="line-clamp-2 overflow-hidden">
{" "}
{audit.framework.name}
</span>
</div>
)}
</button>
@@ -152,7 +145,7 @@ export function AuditRowAvatar(props: { audit: AuditRowFragment$key }) {
}
function AuditDialog(
props: PropsWithChildren<{ audit: AuditRowFragment$key }>,
props: PropsWithChildren<{ audit: AuditRowFragment$key; logo?: string }>,
) {
const audit = useFragment(auditRowFragment, props.audit);
const location = useLocation();
@@ -174,11 +167,13 @@ function AuditDialog(
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"
/>
{props.logo && (
<img
alt={audit.framework.name}
src={props.logo}
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>

View File

@@ -9,11 +9,11 @@ import {
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";
import { useState } from "react";
const downloadMutation = graphql`
mutation DocumentRowDownloadMutation($input: ExportDocumentPDFInput!) {
@@ -27,13 +27,14 @@ const documentRowFragment = graphql`
fragment DocumentRowFragment on Document {
id
title
isUserAuthorized
hasUserRequestedAccess
}
`;
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 = () => {
@@ -48,13 +49,16 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
},
});
};
const [hasRequested, setHasRequested] = useState(
document.hasUserRequestedAccess,
);
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" />
<IconPageTextLine size={16} className=" flex-none text-txt-tertiary" />
{document.title}
</div>
{isAuthenticated ? (
{document.isUserAuthorized ? (
<Button
className="w-full md:w-max"
variant="secondary"
@@ -65,13 +69,17 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
{__("Download")}
</Button>
) : (
<RequestAccessDialog>
<RequestAccessDialog
documentId={document.id}
onSuccess={() => setHasRequested(true)}
>
<Button
disabled={hasRequested}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
>
{__("Request access")}
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
</RequestAccessDialog>
)}

View File

@@ -71,24 +71,28 @@ export function OrganizationSidebar({
<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} />
))}
</div>
</div>
{trustCenter.audits.edges.length > 0 && (
<>
<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} />
))}
</div>
</div>
<hr className="my-6 -mx-6 h-[1px] bg-border-low border-none" />
<hr className="my-6 -mx-6 h-[1px] bg-border-low border-none" />
</>
)}
{/* Actions */}
{!isAuthenticated && (

View File

@@ -15,27 +15,26 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { useTrustCenter } from "/hooks/useTrustCenter";
import { type FormEventHandler, type PropsWithChildren } from "react";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated.ts";
type Props = {
children: React.ReactNode;
};
type Props = PropsWithChildren<{
documentId?: string;
reportId?: string;
onSuccess?: () => void;
}>;
const schema = z.object({
name: z.string(),
email: z.string().email(),
});
const requestAccessMutation = graphql`
mutation RequestAccessDialogMutation($input: RequestAllAccessesInput!) {
requestAllAccesses(input: $input) {
trustCenterAccess {
id
}
}
}
`;
export function RequestAccessDialog({ children }: Props) {
export function RequestAccessDialog({
children,
documentId,
reportId,
onSuccess,
}: Props) {
const trustCenter = useTrustCenter();
const { toast } = useToast();
const { __ } = useTranslate();
@@ -45,32 +44,37 @@ export function RequestAccessDialog({ children }: Props) {
email: "",
},
});
const isAuthenticated = useIsAuthenticated();
const dialogRef = useDialogRef();
const [commitRequestAccess, isRequestingAccess] = useMutationWithToasts(
requestAccessMutation,
{
onSuccess: () => {
const [commitMutation, isMutating] = useMutation({ documentId, reportId });
const submitCallback = (data: z.infer<typeof schema> | null) => {
commitMutation(data)
.then(() => {
onSuccess?.();
toast({
title: __("Success"),
description: __("Access request submitted successfully"),
variant: "success",
});
dialogRef.current?.close();
},
},
);
})
.catch((error) => {
console.error(error);
toast({
title: __("Error"),
description: __("Cannot request access"),
variant: "error",
});
});
};
const onSubmit = handleSubmit((data) => {
commitRequestAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
name: data.name,
email: data.email,
},
},
});
});
const onSubmit: FormEventHandler<HTMLFormElement> = isAuthenticated
? (e) => {
e.preventDefault();
submitCallback(null);
}
: handleSubmit(submitCallback);
return (
<Dialog
ref={dialogRef}
@@ -90,23 +94,25 @@ export function RequestAccessDialog({ children }: Props) {
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>
{!isAuthenticated && (
<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">
<Button disabled={isMutating} type="submit">
{__("Continue")}
</Button>
</DialogFooter>
@@ -114,3 +120,103 @@ export function RequestAccessDialog({ children }: Props) {
</Dialog>
);
}
const requestAccessMutation = graphql`
mutation RequestAccessDialogMutation($input: RequestAllAccessesInput!) {
requestAllAccesses(input: $input) {
trustCenterAccess {
id
}
}
}
`;
const requestDocumentAccessMutation = graphql`
mutation RequestAccessDialogDocumentMutation(
$input: RequestDocumentAccessInput!
) {
requestDocumentAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
const requestReportAccessMutation = graphql`
mutation RequestAccessDialogReportMutation(
$input: RequestReportAccessInput!
) {
requestReportAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
/**
* Use the correct mutation using the shape
*/
function useMutation({
documentId,
reportId,
}: Pick<Props, "documentId" | "reportId">): [
(data: z.infer<typeof schema> | null) => Promise<unknown>,
boolean,
] {
const trustCenter = useTrustCenter();
const [commitRequestAccess, isRequestingAccess] = useMutationWithToasts(
requestAccessMutation,
);
const [commitRequestDocumentAccess, isRequestingDocumentAccess] =
useMutationWithToasts(requestDocumentAccessMutation);
const [commitRequestReportAccess, isRequestingReportAccess] =
useMutationWithToasts(requestReportAccessMutation);
if (reportId) {
return [
(data) => {
return commitRequestReportAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
reportId: reportId,
...data,
},
},
});
},
isRequestingReportAccess,
];
} else if (documentId) {
return [
(data) => {
return commitRequestDocumentAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
documentId: documentId,
...data,
},
},
});
},
isRequestingDocumentAccess,
];
} else {
return [
(data) => {
return commitRequestAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
...data,
},
},
});
},
isRequestingAccess,
];
}
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5ad8bcd5ca3b7635248d13cef0a24edf>>
* @generated SignedSource<<2f595b4efd5e7b13207d433830a64c98>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -17,7 +17,9 @@ export type AuditRowFragment$data = {
};
readonly report: {
readonly filename: string;
readonly hasUserRequestedAccess: boolean;
readonly id: string;
readonly isUserAuthorized: boolean;
} | null | undefined;
readonly " $fragmentType": "AuditRowFragment";
};
@@ -55,6 +57,20 @@ return {
"kind": "ScalarField",
"name": "filename",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "isUserAuthorized",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasUserRequestedAccess",
"storageKey": null
}
],
"storageKey": null
@@ -84,6 +100,6 @@ return {
};
})();
(node as any).hash = "255e447eb5ac9b4cb889e7a8f0463902";
(node as any).hash = "3aabed006b65dc913c81a63f0f6f4523";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<266bdec238fe7397f3cff10dc7da8fc6>>
* @generated SignedSource<<396f9d87342c909ca373f60a1e6f7ff3>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,7 +11,9 @@
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DocumentRowFragment$data = {
readonly hasUserRequestedAccess: boolean;
readonly id: string;
readonly isUserAuthorized: boolean;
readonly title: string;
readonly " $fragmentType": "DocumentRowFragment";
};
@@ -39,12 +41,26 @@ const node: ReaderFragment = {
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "isUserAuthorized",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasUserRequestedAccess",
"storageKey": null
}
],
"type": "Document",
"abstractKey": null
};
(node as any).hash = "437d68812bcc68724d2e347fea22b5e3";
(node as any).hash = "1b0f0b9cae621268d72b0fd0646aa672";
export default node;

View File

@@ -0,0 +1,108 @@
/**
* @generated SignedSource<<446953c5eafa3b9a465d245da3bea1af>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RequestDocumentAccessInput = {
documentId: string;
email?: string | null | undefined;
name?: string | null | undefined;
trustCenterId: string;
};
export type RequestAccessDialogDocumentMutation$variables = {
input: RequestDocumentAccessInput;
};
export type RequestAccessDialogDocumentMutation$data = {
readonly requestDocumentAccess: {
readonly trustCenterAccess: {
readonly id: string;
};
};
};
export type RequestAccessDialogDocumentMutation = {
response: RequestAccessDialogDocumentMutation$data;
variables: RequestAccessDialogDocumentMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "RequestAccessesPayload",
"kind": "LinkedField",
"name": "requestDocumentAccess",
"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": "RequestAccessDialogDocumentMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "RequestAccessDialogDocumentMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "2ed1f0d19711a9f22acdc6251dbfca8f",
"id": null,
"metadata": {},
"name": "RequestAccessDialogDocumentMutation",
"operationKind": "mutation",
"text": "mutation RequestAccessDialogDocumentMutation(\n $input: RequestDocumentAccessInput!\n) {\n requestDocumentAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "c3ebabe2f3fac32e5035cc7acfd54799";
export default node;

View File

@@ -0,0 +1,108 @@
/**
* @generated SignedSource<<74bc40a8998c962930b3f0b63dc6acd9>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RequestReportAccessInput = {
email?: string | null | undefined;
name?: string | null | undefined;
reportId: string;
trustCenterId: string;
};
export type RequestAccessDialogReportMutation$variables = {
input: RequestReportAccessInput;
};
export type RequestAccessDialogReportMutation$data = {
readonly requestReportAccess: {
readonly trustCenterAccess: {
readonly id: string;
};
};
};
export type RequestAccessDialogReportMutation = {
response: RequestAccessDialogReportMutation$data;
variables: RequestAccessDialogReportMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "RequestAccessesPayload",
"kind": "LinkedField",
"name": "requestReportAccess",
"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": "RequestAccessDialogReportMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "RequestAccessDialogReportMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "da69c3e370244e4a9a7793b06b2abdd5",
"id": null,
"metadata": {},
"name": "RequestAccessDialogReportMutation",
"operationKind": "mutation",
"text": "mutation RequestAccessDialogReportMutation(\n $input: RequestReportAccessInput!\n) {\n requestReportAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "3ef2b6424ba751f15f91ad7f4bd28fc5";
export default node;

View File

@@ -1,6 +1,9 @@
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { OverviewPageFragment$key } from "./__generated__/OverviewPageFragment.graphql";
import type {
OverviewPageFragment$data,
OverviewPageFragment$key,
} from "./__generated__/OverviewPageFragment.graphql";
import { Link, useOutletContext } from "react-router";
import { groupBy, objectEntries, sprintf } from "@probo/helpers";
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
@@ -51,26 +54,64 @@ export function OverviewPage() {
trustCenter: OverviewPageFragment$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>
<References
references={fragment.references.edges.map((edge) => edge.node)}
/>
<Documents
audits={trustCenter.audits.edges}
documents={fragment.documents.edges}
url={`/trust/${trustCenter.slug}/documents`}
/>
<Subprocessors
organizationName={trustCenter.organization.name}
vendors={fragment.vendors.edges}
url={`/trust/${trustCenter.slug}/subprocessors`}
/>
</div>
);
}
function Documents({
documents,
audits,
url,
}: {
documents: OverviewPageFragment$data["documents"]["edges"];
audits: NonNullable<
TrustGraphQuery$data["trustCenterBySlug"]
>["audits"]["edges"];
url: string;
}) {
const { __ } = useTranslate();
const documentsPerType = groupBy(
documents.map((edge) => edge.node),
(node) => documentTypeLabel(node.documentType, __),
);
const hasAudits = audits.length > 0;
const hasDocuments = hasAudits || documents.length > 0;
if (!hasDocuments) {
return null;
}
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} />
))}
{audits.length > 0 && (
<>
<RowHeader>{__("Certifications")}</RowHeader>
{audits.map((audit) => (
<AuditRow key={audit.node.id} audit={audit.node} />
))}
</>
)}
{objectEntries(documentsPerType).map(([label, documents]) => (
<Fragment key={label}>
<RowHeader>{label}</RowHeader>
@@ -79,30 +120,43 @@ export function OverviewPage() {
))}
</Fragment>
))}
<Link
to={`/trust/${trustCenter.slug}/documents`}
className="text-sm font-medium flex gap-2 items-center"
>
<Link to={url} className="text-sm font-medium flex gap-2 items-center">
{__("See all documents")}
<IconChevronRight size={16} />
</Link>
</Rows>
</div>
);
}
function Subprocessors({
vendors,
url,
organizationName,
}: {
vendors: OverviewPageFragment$data["vendors"]["edges"];
url: string;
organizationName: string;
}) {
const { __ } = useTranslate();
if (vendors.length === 0) {
return null;
}
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:"),
trustCenter.organization.name,
organizationName,
)}
</p>
<Rows className="mb-8 *:py-5">
{fragment.vendors.edges.map((edge) => (
<VendorRow key={edge.node.id} vendor={edge.node} />
{vendors.map((vendor) => (
<VendorRow key={vendor.node.id} vendor={vendor.node} />
))}
<Link
to={`/trust/${trustCenter.slug}/subprocessors`}
className="text-sm font-medium flex gap-2 items-center"
>
<Link to={url} className="text-sm font-medium flex gap-2 items-center">
{__("See all subprocessors")}
<IconChevronRight size={16} />
</Link>

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<55d256287ee091b57754d2d76080ad25>>
* @generated SignedSource<<f592abfc32b9882d40bdfd0d91908ea2>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -214,6 +214,20 @@ return {
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "isUserAuthorized",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasUserRequestedAccess",
"storageKey": null
}
],
"storageKey": null
@@ -230,12 +244,12 @@ return {
]
},
"params": {
"cacheID": "71e1dc4102d19cdd2fb8d0ff60a391ca",
"cacheID": "8a333723782b24714393e89240704cf2",
"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"
"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 isUserAuthorized\n hasUserRequestedAccess\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<25d09fc2d93d9fbbcde08bd8a8d48214>>
* @generated SignedSource<<a763db35fb72256cc3905206fc18e60b>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -150,7 +150,21 @@ v14 = [
"name": "first",
"value": 50
}
];
],
v15 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "isUserAuthorized",
"storageKey": null
},
v16 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasUserRequestedAccess",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
@@ -422,6 +436,8 @@ return {
"name": "title",
"storageKey": null
},
(v15/*: any*/),
(v16/*: any*/),
{
"alias": null,
"args": null,
@@ -478,7 +494,9 @@ return {
"kind": "ScalarField",
"name": "filename",
"storageKey": null
}
},
(v15/*: any*/),
(v16/*: any*/)
],
"storageKey": null
},
@@ -510,12 +528,12 @@ return {
]
},
"params": {
"cacheID": "3303b20452c558a107492263e7fc0c61",
"cacheID": "512205bc2cccff46b097cabaab4ec338",
"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 ...OverviewPageFragment\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 OverviewPageFragment 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"
"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 ...OverviewPageFragment\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 isUserAuthorized\n hasUserRequestedAccess\n }\n framework {\n id\n name\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment OverviewPageFragment 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"
}
};
})();