Use framework logo on console & trust

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-16 19:05:34 +01:00
parent 427565c521
commit 8c772661bf
34 changed files with 375 additions and 337 deletions

View File

@@ -25,14 +25,24 @@ import {
IconArrowInbox,
useConfirm,
useToast,
FrameworkLogo,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { FrameworkLogo } from "/components/FrameworkLogo";
import { ControlledField } from "/components/form/ControlledField";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import z from "zod";
import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatError, formatDate, type GraphQLError } from "@probo/helpers";
import {
getAuditStateLabel,
getAuditStateVariant,
auditStates,
fileSize,
sprintf,
formatDatetime,
formatError,
formatDate,
type GraphQLError,
} from "@probo/helpers";
import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
@@ -42,7 +52,13 @@ const updateAuditSchema = z.object({
name: z.string().nullable().optional(),
validFrom: z.string().optional(),
validUntil: z.string().optional(),
state: z.enum(["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "REJECTED", "OUTDATED"]),
state: z.enum([
"NOT_STARTED",
"IN_PROGRESS",
"COMPLETED",
"REJECTED",
"OUTDATED",
]),
});
type Props = {
@@ -50,7 +66,10 @@ type Props = {
};
export default function AuditDetailsPage(props: Props) {
const audit = usePreloadedQuery<AuditGraphNodeQuery>(auditNodeQuery, props.queryRef);
const audit = usePreloadedQuery<AuditGraphNodeQuery>(
auditNodeQuery,
props.queryRef
);
const auditEntry = audit.node;
const { __ } = useTranslate();
const organizationId = useOrganizationId();
@@ -58,19 +77,20 @@ export default function AuditDetailsPage(props: Props) {
const navigate = useNavigate();
const deleteAudit = useDeleteAudit(
{ id: auditEntry.id!, framework: { name: auditEntry.framework!.name} },
{ id: auditEntry.id!, framework: { name: auditEntry.framework!.name } },
ConnectionHandler.getConnectionID(organizationId, "AuditsPage_audits"),
() => navigate(`/organizations/${organizationId}/audits`)
);
const { control, formState, handleSubmit, register, reset } = useFormWithSchema(updateAuditSchema, {
defaultValues: {
name: auditEntry.name || null,
validFrom: auditEntry.validFrom?.split('T')[0] || "",
validUntil: auditEntry.validUntil?.split('T')[0] || "",
state: auditEntry.state || "NOT_STARTED",
},
});
const { control, formState, handleSubmit, register, reset } =
useFormWithSchema(updateAuditSchema, {
defaultValues: {
name: auditEntry.name || null,
validFrom: auditEntry.validFrom?.split("T")[0] || "",
validUntil: auditEntry.validUntil?.split("T")[0] || "",
state: auditEntry.state || "NOT_STARTED",
},
});
const updateAudit = useUpdateAudit();
const [uploadAuditReport, isUploading] = useUploadAuditReport();
@@ -98,7 +118,10 @@ export default function AuditDetailsPage(props: Props) {
} catch (error) {
toast({
title: __("Error"),
description: formatError(__("Failed to update audit"), error as GraphQLError),
description: formatError(
__("Failed to update audit"),
error as GraphQLError
),
variant: "error",
});
}
@@ -131,7 +154,9 @@ export default function AuditDetailsPage(props: Props) {
to: `/organizations/${organizationId}/audits`,
},
{
label: (auditEntry.name || auditEntry.framework?.name) ?? __("Unknown Audit"),
label:
(auditEntry.name || auditEntry.framework?.name) ??
__("Unknown Audit"),
},
]}
/>
@@ -139,10 +164,16 @@ export default function AuditDetailsPage(props: Props) {
<div className="flex justify-between items-start">
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<FrameworkLogo name={auditEntry.framework?.name || ""} />
<FrameworkLogo
name={auditEntry.framework?.name || ""}
lightLogoURL={auditEntry.framework?.lightLogoURL}
darkLogoURL={auditEntry.framework?.darkLogoURL}
/>
<div className="text-2xl">{auditEntry.framework?.name}</div>
</div>
<Badge variant={getAuditStateVariant(auditEntry.state || "NOT_STARTED")}>
<Badge
variant={getAuditStateVariant(auditEntry.state || "NOT_STARTED")}
>
{getAuditStateLabel(__, auditEntry.state || "NOT_STARTED")}
</Badge>
</div>
@@ -187,12 +218,10 @@ export default function AuditDetailsPage(props: Props) {
</Field>
<div className="flex justify-end">
{formState.isDirty && (
isAuthorized("Audit", "updateAudit") && (
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")}
</Button>
)
{formState.isDirty && isAuthorized("Audit", "updateAudit") && (
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")}
</Button>
)}
</div>
</form>
@@ -201,7 +230,7 @@ export default function AuditDetailsPage(props: Props) {
<div className="space-y-4">
<h3 className="text-lg font-medium">{__("Audit Report")}</h3>
{auditEntry.report ? (
{auditEntry.report ? (
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-success-50 border border-success-200 rounded-lg">
<div className="flex items-center gap-3">
@@ -211,11 +240,10 @@ export default function AuditDetailsPage(props: Props) {
{auditEntry.report.filename}
</p>
<div className="flex items-center gap-4 text-sm text-success-700">
<span>{fileSize(__, auditEntry.report.size)}</span>
<span>
{fileSize(__, auditEntry.report.size)}
</span>
<span>
{__("Uploaded")} {formatDate(auditEntry.report.createdAt)}
{__("Uploaded")}{" "}
{formatDate(auditEntry.report.createdAt)}
</span>
</div>
</div>
@@ -224,7 +252,7 @@ export default function AuditDetailsPage(props: Props) {
<DropdownItem
onClick={() => {
if (auditEntry.report?.downloadUrl) {
window.open(auditEntry.report.downloadUrl, '_blank');
window.open(auditEntry.report.downloadUrl, "_blank");
}
}}
icon={IconArrowInbox}
@@ -244,10 +272,14 @@ export default function AuditDetailsPage(props: Props) {
) : (
<div className="space-y-4">
<p className="text-neutral-600">
{__("Upload the final audit report document (PDF recommended)")}
{__(
"Upload the final audit report document (PDF recommended)"
)}
</p>
<Dropzone
description={__("Only PDF, DOCX files up to 25MB are allowed")}
description={__(
"Only PDF, DOCX files up to 25MB are allowed"
)}
isUploading={isUploading}
onDrop={async (files) => {
if (files.length > 0 && auditEntry.id) {

View File

@@ -10,12 +10,12 @@ import {
Button,
ControlItem,
DropdownItem,
FrameworkLogo,
IconPencil,
IconPlusLarge,
IconTrashCan,
PageHeader,
} from "@probo/ui";
import { FrameworkLogo } from "/components/FrameworkLogo";
import {
connectionListKey,
frameworkNodeQuery,
@@ -39,6 +39,8 @@ const frameworkDetailFragment = graphql`
id
name
description
lightLogoURL
darkLogoURL
organization {
name
}
@@ -70,12 +72,8 @@ const generateFrameworkStateOfApplicabilityMutation = graphql`
`;
const exportFrameworkMutation = graphql`
mutation FrameworkDetailPageExportFrameworkMutation(
$frameworkId: ID!
) {
exportFramework(
input: { frameworkId: $frameworkId }
) {
mutation FrameworkDetailPageExportFrameworkMutation($frameworkId: ID!) {
exportFramework(input: { frameworkId: $frameworkId }) {
exportJobId
}
}
@@ -120,7 +118,8 @@ export default function FrameworkDetailPage(props: Props) {
exportFrameworkMutation,
{
errorMessage: "Failed to export framework",
successMessage: "Framework export started successfully. You will receive an email when the export is ready.",
successMessage:
"Framework export started successfully. You will receive an email when the export is ready.",
}
);
@@ -193,7 +192,11 @@ export default function FrameworkDetailPage(props: Props) {
{__("Export Framework")}
</DropdownItem>
{isAuthorized("Framework", "deleteFramework") && (
<DropdownItem icon={IconTrashCan} variant="danger" onClick={onDelete}>
<DropdownItem
icon={IconTrashCan}
variant="danger"
onClick={onDelete}
>
{__("Delete")}
</DropdownItem>
)}

View File

@@ -5,6 +5,7 @@ import {
Card,
DropdownItem,
FileButton,
FrameworkLogo,
FrameworkSelector,
IconFolderUpload,
IconPencil,
@@ -27,7 +28,6 @@ import { Link } from "react-router";
import type { FrameworksPageCardFragment$key } from "./__generated__/FrameworksPageCardFragment.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { useState, type ChangeEventHandler, use } from "react";
import { FrameworkLogo } from "/components/FrameworkLogo";
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
import { PermissionsContext } from "/providers/PermissionsContext";
@@ -120,7 +120,8 @@ export default function FrameworksPage(props: Props) {
const isLoading = isUploading || isImporting;
const hasAnyAction = isAuthorized("Framework", "updateFramework") ||
const hasAnyAction =
isAuthorized("Framework", "updateFramework") ||
isAuthorized("Framework", "deleteFramework");
return (
@@ -172,6 +173,8 @@ const frameworkCardFragment = graphql`
id
name
description
lightLogoURL
darkLogoURL
}
`;
@@ -200,7 +203,11 @@ function FrameworkCard(props: FrameworkCardProps) {
framework={framework}
/>
<div className="flex justify-between mb-3">
<FrameworkLogo {...framework} />
<FrameworkLogo
name={framework.name}
lightLogoURL={framework.lightLogoURL}
darkLogoURL={framework.darkLogoURL}
/>
{props.hasAnyAction && (
<ActionDropdown className="z-10 relative">
{isAuthorized("Framework", "updateFramework") && (

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<daf4012a65afffdedc10e4f5c7e2bc3c>>
* @generated SignedSource<<1da75e11f49e841800329ded9f67a1d4>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -24,8 +24,10 @@ export type FrameworkDetailPageFragment$data = {
};
}>;
};
readonly darkLogoURL: string | null | undefined;
readonly description: string | null | undefined;
readonly id: string;
readonly lightLogoURL: string | null | undefined;
readonly name: string;
readonly organization: {
readonly name: string;
@@ -67,6 +69,20 @@ return {
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -167,6 +183,6 @@ return {
};
})();
(node as any).hash = "c31fd5c0f675d2524f736d69c4ebebe3";
(node as any).hash = "34a82531b8812ee16e75eb035bc739e8";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9fdc712e118402371c9951169aac320a>>
* @generated SignedSource<<412cc494e90709fca575c68f6deef112>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,8 +11,10 @@
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type FrameworksPageCardFragment$data = {
readonly darkLogoURL: string | null | undefined;
readonly description: string | null | undefined;
readonly id: string;
readonly lightLogoURL: string | null | undefined;
readonly name: string;
readonly " $fragmentType": "FrameworksPageCardFragment";
};
@@ -47,12 +49,26 @@ const node: ReaderFragment = {
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
}
],
"type": "Framework",
"abstractKey": null
};
(node as any).hash = "4481f380673963e72f88071031e37d14";
(node as any).hash = "fe86a14971741a77f587ae015ebf0a22";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9436ef052ea63a9785d82ec810e810f1>>
* @generated SignedSource<<2256362d89b6b5c2f5c707acb44d283e>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -158,6 +158,20 @@ return {
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
}
],
"storageKey": null
@@ -187,12 +201,12 @@ return {
]
},
"params": {
"cacheID": "87a38788cb2e060657b552af69acb707",
"cacheID": "5584472f1e68fac3d766662c6d5dab90",
"id": null,
"metadata": {},
"name": "FrameworksPageImportMutation",
"operationKind": "mutation",
"text": "mutation FrameworksPageImportMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n"
"text": "mutation FrameworksPageImportMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n lightLogoURL\n darkLogoURL\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<b8d5c939f1b3a4337be9a4ecc2e8fd98>>
* @generated SignedSource<<af136d6db1a014d6241ad2c9c6dd5f55>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -159,6 +159,20 @@ return {
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
}
],
"storageKey": null
@@ -188,12 +202,12 @@ return {
]
},
"params": {
"cacheID": "931895b4bbb7caacd8ee0ee69de76cd6",
"cacheID": "c66177d87b59d87fdf7cbc9268af431a",
"id": null,
"metadata": {},
"name": "FrameworkFormDialogMutation",
"operationKind": "mutation",
"text": "mutation FrameworkFormDialogMutation(\n $input: CreateFrameworkInput!\n) {\n createFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n"
"text": "mutation FrameworkFormDialogMutation(\n $input: CreateFrameworkInput!\n) {\n createFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n lightLogoURL\n darkLogoURL\n}\n"
}
};
})();