Update remaining page headers

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-03-21 11:37:40 +04:00
parent ffa0072b35
commit cee114089a
12 changed files with 300 additions and 258 deletions

View File

@@ -291,6 +291,7 @@ function BreadcrumbControlOverview() {
{data.control?.name}
</BreadCrumbNavLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
@@ -306,6 +307,17 @@ function BreadcrumbCreateControl() {
);
}
function BreadcrumbUpdateControl() {
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Update Control</BreadcrumbPage>
</BreadcrumbItem>
</>
);
}
function BreadcrumbPolicyList() {
const { organizationId } = useParams();
return (
@@ -413,7 +425,12 @@ export default function ConsoleLayout() {
<BreadcrumbControlOverview />
</Suspense>
}
/>
>
<Route
path="update"
element={<BreadcrumbUpdateControl />}
/>
</Route>
<Route
path="update"
element={<BreadcrumbUpdateFramework />}

View File

@@ -75,6 +75,7 @@ import type { ControlOverviewPageAssignTaskMutation as ControlOverviewPageAssign
import type { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql";
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.graphql";
import { PageHeader } from "./PageHeader";
// Function to format ISO8601 duration to human-readable format
const formatDuration = (isoDuration: string): string => {
@@ -1131,10 +1132,11 @@ function ControlOverviewPageContent({
<Helmet>
<title>{data.control.name || "Control"} - Probo</title>
</Helmet>
<div className="min-h-screen bg-white p-6 space-y-6">
<div className="space-y-4 mb-8">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold">{data.control.name}</h1>
<div className="container">
<PageHeader
className="mb-17"
title={data.control.name ?? ""}
actions={
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleEditControl}>
Edit Control
@@ -1167,7 +1169,9 @@ function ControlOverviewPageContent({
{formatImportance(data.control.importance)}
</div>
</div>
</div>
}
/>
<div className="space-y-4 mb-8">
<Card className="mt-4">
<CardContent className="pt-6">
<div className="prose prose-gray prose-sm md:prose-base text-gray-600 max-w-3xl">

View File

@@ -21,6 +21,7 @@ import {
CreateControlPageCreateControlMutation,
ControlImportance,
} from "./__generated__/CreateControlPageCreateControlMutation.graphql";
import { PageHeader } from "./PageHeader";
const createControlMutation = graphql`
mutation CreateControlPageCreateControlMutation(
@@ -185,13 +186,12 @@ function CreateControlPageContent() {
<Helmet>
<title>Create Control - Probo</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-2xl font-bold">Create Control</h1>
<p className="text-muted-foreground">
Create a new control for your framework
</p>
</div>
<div className="container">
<PageHeader
className="mb-17"
title="Create Control"
description="Create a new control for your framework"
/>
<Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6">

View File

@@ -10,6 +10,7 @@ import { useToast } from "@/hooks/use-toast";
import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import { CreatePeoplePageCreatePeopleMutation } from "./__generated__/CreatePeoplePageCreatePeopleMutation.graphql";
import { PageHeader } from "./PageHeader";
const createPeopleMutation = graphql`
mutation CreatePeoplePageCreatePeopleMutation(
@@ -153,10 +154,15 @@ function CreatePeoplePageContent() {
<Helmet>
<title>Create Person - Probo Console</title>
</Helmet>
<div className="container">
<PageHeader
className="mb-17"
title="Add a Person"
description="Add a new person interacting with organization"
/>
<form onSubmit={handleSubmit}>
<div className="space-y-6 p-4 md:p-6 lg:p-8">
<div className="mx-auto max-w-4xl space-y-6">
<form onSubmit={handleSubmit}>
<div className="max-w-4xl space-y-6">
<EditableField
label="Full Name"
value={formData.fullName}
@@ -291,19 +297,23 @@ function CreatePeoplePageContent() {
</div>
</Card>
</div>
</div>
<div className="fixed bottom-6 right-6 flex gap-2">
<Button type="button" variant="outline" onClick={() => navigate(-1)}>
Cancel
</Button>
<Button
type="submit"
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
Create Person
</Button>
</div>
</form>
<div className="fixed bottom-6 right-6 flex gap-2">
<Button
type="button"
variant="outline"
onClick={() => navigate(-1)}
>
Cancel
</Button>
<Button
type="submit"
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
Create Person
</Button>
</div>
</form>
</div>
</>
);
}

View File

@@ -14,13 +14,14 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import { FileText, Calendar, User } from "lucide-react";
import { Calendar, User } from "lucide-react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import PolicyEditor from "@/components/PolicyEditor";
import PeopleSelector from "@/components/PeopleSelector";
import { Suspense } from "react";
import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql";
import type { CreatePolicyPageQuery as CreatePolicyPageQueryType } from "./__generated__/CreatePolicyPageQuery.graphql";
import { PageHeader } from "./PageHeader";
const CreatePolicyQuery = graphql`
query CreatePolicyPageQuery($organizationId: ID!) {
@@ -159,20 +160,12 @@ function CreatePolicyForm({
<Helmet>
<title>Create Policy - Probo Console</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="flex items-center mb-6">
<div className="mr-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<FileText className="h-6 w-6 text-primary" />
</div>
</div>
<div>
<h1 className="text-2xl font-bold">Create Policy</h1>
<p className="text-muted-foreground">
Create a new policy for your organization
</p>
</div>
</div>
<div className="container">
<PageHeader
className="mb-17"
title="Create Policy"
description="Create a new policy for your organization"
/>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">

View File

@@ -3,14 +3,16 @@ import { ReactNode } from "react";
export function PageHeader({
className,
children,
title,
actions,
description,
}: {
className?: string;
children?: ReactNode;
title: string;
actions?: ReactNode;
description: string;
description?: string;
}) {
return (
<div className={cn("space-y-6", className)}>
@@ -22,7 +24,8 @@ export function PageHeader({
) : (
<PageHeading title={title} />
)}
<PageDescription>{description}</PageDescription>
{description && <PageDescription>{description}</PageDescription>}
{children}
</div>
);
}

View File

@@ -18,6 +18,7 @@ import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./_
import { useParams } from "react-router";
import { Helmet } from "react-helmet-async";
import { cn } from "@/lib/utils";
import { PageHeader } from "./PageHeader";
const peopleOverviewPageQuery = graphql`
query PeopleOverviewPageQuery($peopleId: ID!) {
@@ -161,140 +162,156 @@ function PeopleOverviewPageContent({
return (
<>
<div className="space-y-6 p-4 md:p-6 lg:p-8">
<div className="mx-auto max-w-4xl space-y-6">
<EditableField
label="Full Name"
value={formData.fullName}
onChange={(value) => handleFieldChange("fullName", value)}
/>
<Helmet>
<title>Person - Probo</title>
</Helmet>
<div className="container">
<PageHeader className="mb-17" title={formData.fullName} />
<div className="space-y-6">
<div className="mx-auto max-w-4xl space-y-6">
<EditableField
label="Full Name"
value={formData.fullName}
onChange={(value) => handleFieldChange("fullName", value)}
/>
<EditableField
label="Primary Email"
value={formData.primaryEmailAddress}
type="email"
onChange={(value) =>
handleFieldChange("primaryEmailAddress", value)
}
/>
<EditableField
label="Primary Email"
value={formData.primaryEmailAddress}
type="email"
onChange={(value) =>
handleFieldChange("primaryEmailAddress", value)
}
/>
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">Additional Email Addresses</Label>
</div>
<div className="space-y-2">
{formData.additionalEmailAddresses.map((email, index) => (
<div key={index} className="flex gap-2">
<Input
type="email"
value={email}
onChange={(e) => {
const newEmails = [...formData.additionalEmailAddresses];
newEmails[index] = e.target.value;
handleFieldChange("additionalEmailAddresses", newEmails);
}}
/>
<Button
variant="outline"
onClick={() => {
const newEmails =
formData.additionalEmailAddresses.filter(
(_, i) => i !== index
);
handleFieldChange("additionalEmailAddresses", newEmails);
}}
>
Remove
</Button>
</div>
))}
<Button
variant="outline"
onClick={() => {
handleFieldChange("additionalEmailAddresses", [
...formData.additionalEmailAddresses,
"",
]);
}}
>
Add Email
</Button>
</div>
</div>
<Card className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-lg font-medium">Additional Information</h2>
<p className="text-sm text-gray-500">
Additional details about the person
</p>
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">Additional Email Addresses</Label>
</div>
<div className="space-y-2">
{formData.additionalEmailAddresses.map((email, index) => (
<div key={index} className="flex gap-2">
<Input
type="email"
value={email}
onChange={(e) => {
const newEmails = [
...formData.additionalEmailAddresses,
];
newEmails[index] = e.target.value;
handleFieldChange(
"additionalEmailAddresses",
newEmails
);
}}
/>
<Button
variant="outline"
onClick={() => {
const newEmails =
formData.additionalEmailAddresses.filter(
(_, i) => i !== index
);
handleFieldChange(
"additionalEmailAddresses",
newEmails
);
}}
>
Remove
</Button>
</div>
))}
<Button
variant="outline"
onClick={() => {
handleFieldChange("additionalEmailAddresses", [
...formData.additionalEmailAddresses,
"",
]);
}}
>
Add Email
</Button>
</div>
</div>
<Card className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">Kind</Label>
</div>
<div className="flex gap-2">
<button
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === "EMPLOYEE"
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Employee
</button>
<button
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === "CONTRACTOR"
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Contractor
</button>
<button
onClick={() =>
handleFieldChange("kind", "SERVICE_ACCOUNT")
}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === "SERVICE_ACCOUNT"
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Service Account
</button>
<h2 className="text-lg font-medium">
Additional Information
</h2>
<p className="text-sm text-gray-500">
Additional details about the person
</p>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">Kind</Label>
</div>
<div className="flex gap-2">
<button
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === "EMPLOYEE"
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Employee
</button>
<button
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === "CONTRACTOR"
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Contractor
</button>
<button
onClick={() =>
handleFieldChange("kind", "SERVICE_ACCOUNT")
}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === "SERVICE_ACCOUNT"
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Service Account
</button>
</div>
</div>
</div>
</div>
</div>
</Card>
</Card>
</div>
</div>
</div>
{hasChanges && (
<div className="fixed bottom-6 right-6 flex gap-2">
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button
onClick={handleSave}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
Save Changes
</Button>
</div>
)}
{hasChanges && (
<div className="fixed bottom-6 right-6 flex gap-2">
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button
onClick={handleSave}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
Save Changes
</Button>
</div>
)}
</div>
</>
);
}

View File

@@ -18,6 +18,7 @@ import type { PolicyOverviewPageDeleteMutation } from "./__generated__/PolicyOve
import { Helmet } from "react-helmet-async";
import "../styles/policy-content.css";
import { useToast } from "@/hooks/use-toast";
import { PageHeader } from "./PageHeader";
const PolicyOverviewPageQuery = graphql`
query PolicyOverviewPageQuery($policyId: ID!) {
@@ -152,72 +153,65 @@ function PolicyOverviewPageContent({
<Helmet>
<title>{policy.name} - Probo Console</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="flex justify-between items-start mb-6">
<div className="flex items-center">
<div className="mr-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<FileText className="h-6 w-6 text-primary" />
</div>
</div>
<div>
<h1 className="text-2xl font-bold">{policy.name}</h1>
<div className="flex items-center gap-2 text-muted-foreground">
<Badge
variant={policy.status === "ACTIVE" ? "default" : "outline"}
<div className="container">
<PageHeader
className="mb-17"
title={policy.name ?? ""}
actions={
<div className="flex gap-2">
<Button variant="outline" size="sm" asChild>
<Link
to={`/organizations/${organizationId}/policies/${policy.id}/update`}
>
{policy.status === "ACTIVE" ? "Active" : "Draft"}
</Badge>
{policy.owner && (
<div className="flex items-center gap-1">
<User className="h-3 w-3" />
<Link
to={`/organizations/${organizationId}/people/${policy.owner.id}`}
className="hover:underline"
>
{policy.owner.fullName}
</Link>
</div>
)}
{policy.reviewDate && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
<span>Review: {formatDate(policy.reviewDate)}</span>
</div>
)}
</div>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" asChild>
<Link
to={`/organizations/${organizationId}/policies/${policy.id}/update`}
<Edit className="h-4 w-4 mr-2" />
Edit
</Link>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
// Logic to download policy content as PDF or text
const element = document.createElement("a");
const file = new Blob([policy.content || ""], {
type: "text/plain",
});
element.href = URL.createObjectURL(file);
element.download = `${policy.name}.txt`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}}
>
<Edit className="h-4 w-4 mr-2" />
Edit
</Link>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
// Logic to download policy content as PDF or text
const element = document.createElement("a");
const file = new Blob([policy.content || ""], {
type: "text/plain",
});
element.href = URL.createObjectURL(file);
element.download = `${policy.name}.txt`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}}
>
<Download className="h-4 w-4 mr-2" />
Download
</Button>
<Download className="h-4 w-4 mr-2" />
Download
</Button>
</div>
}
>
<div className="flex items-center gap-2 text-muted-foreground">
<Badge variant={policy.status === "ACTIVE" ? "default" : "outline"}>
{policy.status === "ACTIVE" ? "Active" : "Draft"}
</Badge>
{policy.owner && (
<div className="flex items-center gap-1">
<User className="h-3 w-3" />
<Link
to={`/organizations/${organizationId}/people/${policy.owner.id}`}
className="hover:underline"
>
{policy.owner.fullName}
</Link>
</div>
)}
{policy.reviewDate && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
<span>Review: {formatDate(policy.reviewDate)}</span>
</div>
)}
</div>
</div>
</PageHeader>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">

View File

@@ -28,6 +28,7 @@ import type {
ControlState,
ControlImportance,
} from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
import { PageHeader } from "./PageHeader";
const updateControlMutation = graphql`
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
@@ -253,11 +254,12 @@ function UpdateControlPageContent({
<Helmet>
<title>Update Control - Probo</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-2xl font-bold">Update Control</h1>
<p className="text-muted-foreground">Update the control details</p>
</div>
<div className="container">
<PageHeader
className="mb-17"
title="Update Control"
description="Update the control details"
/>
<Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6">

View File

@@ -18,6 +18,7 @@ import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import { UpdateFrameworkPageUpdateFrameworkMutation } from "./__generated__/UpdateFrameworkPageUpdateFrameworkMutation.graphql";
import { UpdateFrameworkPageQuery as UpdateFrameworkPageQueryType } from "./__generated__/UpdateFrameworkPageQuery.graphql";
import { PageHeader } from "./PageHeader";
const updateFrameworkMutation = graphql`
mutation UpdateFrameworkPageUpdateFrameworkMutation(
@@ -87,7 +88,7 @@ function EditableField({
}
className={cn(
"w-full resize-none",
required && !value && "border-red-500",
required && !value && "border-red-500"
)}
placeholder={`Enter ${label.toLowerCase()}`}
rows={4}
@@ -134,7 +135,7 @@ function UpdateFrameworkPageContent({
const [commit, isInFlight] =
useMutation<UpdateFrameworkPageUpdateFrameworkMutation>(
updateFrameworkMutation,
updateFrameworkMutation
);
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
@@ -202,11 +203,12 @@ function UpdateFrameworkPageContent({
<Helmet>
<title>Update Framework - Probo</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-2xl font-bold">Update Framework</h1>
<p className="text-muted-foreground">Update the framework details</p>
</div>
<div className="container">
<PageHeader
className="mb-17"
title="Update Framework"
description="Update the framework details"
/>
<Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6">

View File

@@ -13,13 +13,14 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "@/hooks/use-toast";
import { FileText, Calendar, User } from "lucide-react";
import { Calendar, User } from "lucide-react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Suspense } from "react";
import PolicyEditor from "@/components/PolicyEditor";
import PeopleSelector from "@/components/PeopleSelector";
import type { UpdatePolicyPageQuery as UpdatePolicyPageQueryType } from "./__generated__/UpdatePolicyPageQuery.graphql";
import type { UpdatePolicyPageMutation as UpdatePolicyPageMutationType } from "./__generated__/UpdatePolicyPageMutation.graphql";
import { PageHeader } from "./PageHeader";
const UpdatePolicyPageQuery = graphql`
query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) {
@@ -151,18 +152,12 @@ function UpdatePolicyPageContent({
<Helmet>
<title>Update Policy - Probo Console</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="flex items-center mb-6">
<div className="mr-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<FileText className="h-6 w-6 text-primary" />
</div>
</div>
<div>
<h1 className="text-2xl font-bold">Update Policy</h1>
<p className="text-muted-foreground">Update an existing policy</p>
</div>
</div>
<div className="container">
<PageHeader
className="mb-17"
title="Update Policy"
description="Update an existing policy"
/>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">

View File

@@ -18,6 +18,7 @@ import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./_
import { useParams } from "react-router";
import { Helmet } from "react-helmet-async";
import { cn } from "@/lib/utils";
import { PageHeader } from "./PageHeader";
const vendorOverviewPageQuery = graphql`
query VendorOverviewPageQuery($vendorId: ID!) {
@@ -205,8 +206,12 @@ function VendorOverviewPageContent({
return (
<>
<div className="space-y-6 p-4 md:p-6 lg:p-8">
<div className="mx-auto max-w-4xl space-y-6">
<Helmet>
<title>Vendor - Probo</title>
</Helmet>
<div className="container">
<PageHeader className="mb-17" title={formData.name} />
<div className="max-w-4xl space-y-6">
<EditableField
label="Name"
value={formData.name}