Add all missing page skeletons and use them as fallback

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-03-21 15:13:55 +04:00
parent eca4f03ec1
commit f176bf13cd
20 changed files with 2540 additions and 2659 deletions

View File

@@ -13,6 +13,23 @@ import AuthLayout from "./layouts/AuthLayout";
import { RelayEnvironment } from "./RelayEnvironment"; import { RelayEnvironment } from "./RelayEnvironment";
import VisitorErrorBoundary from "./components/VisitorErrorBoundary"; import VisitorErrorBoundary from "./components/VisitorErrorBoundary";
import { FrameworkListPageSkeleton } from "./pages/FrameworkListPage"; import { FrameworkListPageSkeleton } from "./pages/FrameworkListPage";
import { ControlOverviewPageSkeleton } from "./pages/ControlOverviewPage";
import { CreateControlPageSkeleton } from "./pages/CreateControlPage";
import { UpdateFrameworkPageSkeleton } from "./pages/UpdateFrameworkPage";
import { UpdateControlPageSkeleton } from "./pages/UpdateControlPage";
import { CreateFrameworkPageSkeleton } from "./pages/CreateFrameworkPage";
import { CreateOrganizationPageSkeleton } from "./pages/CreateOrganizationPage";
import { CreatePeoplePageSkeleton } from "./pages/CreatePeoplePage";
import { CreatePolicyPageSkeleton } from "./pages/CreatePolicyPage";
import { FrameworkOverviewPageSkeleton } from "./pages/FrameworkOverviewPage";
import { PeopleListPageSkeleton } from "./pages/PeopleListPage";
import { PeopleOverviewPageSkeleton } from "./pages/PeopleOverviewPage";
import { PolicyListPageSkeleton } from "./pages/PolicyListPage";
import { PolicyOverviewPageSkeleton } from "./pages/PolicyOverviewPage";
import { UpdatePolicyPageSkeleton } from "./pages/UpdatePolicyPage";
import { VendorListPageSkeleton } from "./pages/VendorListPage";
import { VendorOverviewPageSkeleton } from "./pages/VendorOverviewPage";
import { SettingsPageSkeleton } from "./pages/SettingsPage";
posthog.init(process.env.POSTHOG_KEY!, { posthog.init(process.env.POSTHOG_KEY!, {
api_host: process.env.POSTHOG_HOST, api_host: process.env.POSTHOG_HOST,
@@ -85,7 +102,7 @@ function App() {
<Route <Route
path="create" path="create"
element={ element={
<Suspense> <Suspense fallback={<CreateOrganizationPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<CreateOrganizationPage /> <CreateOrganizationPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -106,7 +123,7 @@ function App() {
<Route <Route
path="people" path="people"
element={ element={
<Suspense> <Suspense fallback={<PeopleListPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<PeopleListPage /> <PeopleListPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -116,7 +133,7 @@ function App() {
<Route <Route
path="people/create" path="people/create"
element={ element={
<Suspense> <Suspense fallback={<CreatePeoplePageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<CreatePeoplePage /> <CreatePeoplePage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -126,7 +143,7 @@ function App() {
<Route <Route
path="people/:peopleId" path="people/:peopleId"
element={ element={
<Suspense> <Suspense fallback={<PeopleOverviewPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<PeopleOverviewPage /> <PeopleOverviewPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -136,7 +153,7 @@ function App() {
<Route <Route
path="vendors" path="vendors"
element={ element={
<Suspense> <Suspense fallback={<VendorListPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<VendorListPage /> <VendorListPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -156,7 +173,7 @@ function App() {
<Route <Route
path="frameworks/create" path="frameworks/create"
element={ element={
<Suspense> <Suspense fallback={<CreateFrameworkPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<CreateFrameworkPage /> <CreateFrameworkPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -166,7 +183,9 @@ function App() {
<Route <Route
path="frameworks/:frameworkId" path="frameworks/:frameworkId"
element={ element={
<Suspense> <Suspense
fallback={<FrameworkOverviewPageSkeleton />}
>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<FrameworkOverviewPage /> <FrameworkOverviewPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -176,7 +195,7 @@ function App() {
<Route <Route
path="frameworks/:frameworkId/update" path="frameworks/:frameworkId/update"
element={ element={
<Suspense> <Suspense fallback={<UpdateFrameworkPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<UpdateFrameworkPage /> <UpdateFrameworkPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -186,7 +205,7 @@ function App() {
<Route <Route
path="frameworks/:frameworkId/controls/create" path="frameworks/:frameworkId/controls/create"
element={ element={
<Suspense> <Suspense fallback={<CreateControlPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<CreateControlPage /> <CreateControlPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -196,7 +215,7 @@ function App() {
<Route <Route
path="frameworks/:frameworkId/controls/:controlId" path="frameworks/:frameworkId/controls/:controlId"
element={ element={
<Suspense> <Suspense fallback={<ControlOverviewPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<ControlOverviewPage /> <ControlOverviewPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -206,7 +225,7 @@ function App() {
<Route <Route
path="frameworks/:frameworkId/controls/:controlId/update" path="frameworks/:frameworkId/controls/:controlId/update"
element={ element={
<Suspense> <Suspense fallback={<UpdateControlPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<UpdateControlPage /> <UpdateControlPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -216,7 +235,7 @@ function App() {
<Route <Route
path="vendors/:vendorId" path="vendors/:vendorId"
element={ element={
<Suspense> <Suspense fallback={<VendorOverviewPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<VendorOverviewPage /> <VendorOverviewPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -227,7 +246,7 @@ function App() {
<Route <Route
path="policies" path="policies"
element={ element={
<Suspense> <Suspense fallback={<PolicyListPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<PolicyListPage /> <PolicyListPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -237,7 +256,7 @@ function App() {
<Route <Route
path="policies/create" path="policies/create"
element={ element={
<Suspense> <Suspense fallback={<CreatePolicyPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<CreatePolicyPage /> <CreatePolicyPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -247,7 +266,7 @@ function App() {
<Route <Route
path="policies/:policyId" path="policies/:policyId"
element={ element={
<Suspense> <Suspense fallback={<PolicyOverviewPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<PolicyOverviewPage /> <PolicyOverviewPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -257,7 +276,7 @@ function App() {
<Route <Route
path="policies/:policyId/update" path="policies/:policyId/update"
element={ element={
<Suspense> <Suspense fallback={<UpdatePolicyPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<UpdatePolicyPage /> <UpdatePolicyPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
@@ -267,7 +286,7 @@ function App() {
<Route <Route
path="settings" path="settings"
element={ element={
<Suspense> <Suspense fallback={<SettingsPageSkeleton />}>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<SettingsPage /> <SettingsPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>

View File

@@ -56,8 +56,8 @@ export function PageHeading({
const PageHeadingSkeleton: FC = () => { const PageHeadingSkeleton: FC = () => {
return ( return (
<div className="py-[3px]"> <div className="py-[3px] w-2/5">
<div className="bg-muted animate rounded-lg w-60 h-7.5" /> <div className="bg-muted animate rounded-lg h-7.5" />
</div> </div>
); );
}; };

View File

@@ -64,7 +64,6 @@ import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Helmet } from "react-helmet-async";
import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql"; import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql";
import type { ControlOverviewPageUpdateTaskStateMutation as ControlOverviewPageUpdateTaskStateMutationType } from "./__generated__/ControlOverviewPageUpdateTaskStateMutation.graphql"; import type { ControlOverviewPageUpdateTaskStateMutation as ControlOverviewPageUpdateTaskStateMutationType } from "./__generated__/ControlOverviewPageUpdateTaskStateMutation.graphql";
import type { ControlOverviewPageCreateTaskMutation as ControlOverviewPageCreateTaskMutationType } from "./__generated__/ControlOverviewPageCreateTaskMutation.graphql"; import type { ControlOverviewPageCreateTaskMutation as ControlOverviewPageCreateTaskMutationType } from "./__generated__/ControlOverviewPageCreateTaskMutation.graphql";
@@ -75,7 +74,7 @@ import type { ControlOverviewPageAssignTaskMutation as ControlOverviewPageAssign
import type { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql"; import type { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql";
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql"; import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.graphql"; import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
// Function to format ISO8601 duration to human-readable format // Function to format ISO8601 duration to human-readable format
const formatDuration = (isoDuration: string): string => { const formatDuration = (isoDuration: string): string => {
@@ -1128,13 +1127,7 @@ function ControlOverviewPageContent({
}; };
return ( return (
<> <PageTemplate
<Helmet>
<title>{data.control.name || "Control"} - Probo</title>
</Helmet>
<div className="container">
<PageHeader
className="mb-17"
title={data.control.name ?? ""} title={data.control.name ?? ""}
actions={ actions={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -1170,7 +1163,7 @@ function ControlOverviewPageContent({
</div> </div>
</div> </div>
} }
/> >
<div className="space-y-4 mb-8"> <div className="space-y-4 mb-8">
<Card className="mt-4"> <Card className="mt-4">
<CardContent className="pt-6"> <CardContent className="pt-6">
@@ -1189,10 +1182,7 @@ function ControlOverviewPageContent({
<FileIcon className="w-4 h-4 mr-2 text-blue-500" /> <FileIcon className="w-4 h-4 mr-2 text-blue-500" />
<span>Drag & drop files onto tasks to add evidence</span> <span>Drag & drop files onto tasks to add evidence</span>
</div> </div>
<Dialog <Dialog open={isCreateTaskOpen} onOpenChange={setIsCreateTaskOpen}>
open={isCreateTaskOpen}
onOpenChange={setIsCreateTaskOpen}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" className="flex items-center gap-1"> <Button size="sm" className="flex items-center gap-1">
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
@@ -1203,8 +1193,8 @@ function ControlOverviewPageContent({
<DialogHeader> <DialogHeader>
<DialogTitle>Create New Task</DialogTitle> <DialogTitle>Create New Task</DialogTitle>
<DialogDescription> <DialogDescription>
Add a new task to this control. Click save when Add a new task to this control. Click save when you&apos;re
you&apos;re done. done.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
@@ -1253,9 +1243,7 @@ function ControlOverviewPageContent({
type="number" type="number"
min="0" min="0"
value={timeEstimateDays} value={timeEstimateDays}
onChange={(e) => onChange={(e) => setTimeEstimateDays(e.target.value)}
setTimeEstimateDays(e.target.value)
}
placeholder="0" placeholder="0"
/> />
</div> </div>
@@ -1272,9 +1260,7 @@ function ControlOverviewPageContent({
min="0" min="0"
max="23" max="23"
value={timeEstimateHours} value={timeEstimateHours}
onChange={(e) => onChange={(e) => setTimeEstimateHours(e.target.value)}
setTimeEstimateHours(e.target.value)
}
placeholder="0" placeholder="0"
/> />
</div> </div>
@@ -1647,10 +1633,7 @@ function ControlOverviewPageContent({
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="bg-white p-2 rounded-md border border-gray-200"> <div className="bg-white p-2 rounded-md border border-gray-200">
{getFileIcon( {getFileIcon(evidence.mimeType, evidence.type)}
evidence.mimeType,
evidence.type
)}
</div> </div>
<div> <div>
<div className="text-sm font-medium text-gray-800"> <div className="text-sm font-medium text-gray-800">
@@ -1672,9 +1655,7 @@ function ControlOverviewPageContent({
<span>•</span> <span>•</span>
</> </>
) : null} ) : null}
<span> <span>{formatDate(evidence.createdAt)}</span>
{formatDate(evidence.createdAt)}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -1821,9 +1802,7 @@ function ControlOverviewPageContent({
<Textarea <Textarea
id="evidence-description" id="evidence-description"
value={linkEvidenceDescription} value={linkEvidenceDescription}
onChange={(e) => onChange={(e) => setLinkEvidenceDescription(e.target.value)}
setLinkEvidenceDescription(e.target.value)
}
placeholder="Describe this evidence (optional)" placeholder="Describe this evidence (optional)"
className="min-h-[100px]" className="min-h-[100px]"
/> />
@@ -1972,8 +1951,7 @@ function ControlOverviewPageContent({
<DialogTitle>Delete Evidence</DialogTitle> <DialogTitle>Delete Evidence</DialogTitle>
<DialogDescription> <DialogDescription>
Are you sure you want to delete the evidence &quot; Are you sure you want to delete the evidence &quot;
{evidenceToDelete?.filename}&quot;? This action cannot be {evidenceToDelete?.filename}&quot;? This action cannot be undone.
undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
@@ -1989,29 +1967,32 @@ function ControlOverviewPageContent({
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </PageTemplate>
</>
); );
} }
function ControlOverviewPageFallback() { export function ControlOverviewPageSkeleton() {
return ( return (
<div className="min-h-screen bg-white p-6"> <PageTemplateSkeleton
<div className="mb-8"> actions={
<div className="h-8 w-48 bg-gray-100 animate-pulse rounded" /> <div className="flex items-center gap-2 w-1/3">
<div className="h-4 w-96 bg-gray-100 animate-pulse rounded mt-2" /> <div className="bg-muted animate-pulse h-8 w-1/3 rounded-lg" />
<div className="bg-muted animate-pulse h-8 w-1/3 rounded-lg" />
<div className="bg-muted animate-pulse h-8 w-1/3 rounded-full" />
</div> </div>
}
>
<div className="space-y-4"> <div className="space-y-4">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<Card key={i}> <Card key={i}>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="h-6 w-48 bg-gray-100 animate-pulse rounded mb-2" /> <div className="h-6 w-48 bg-muted animate-pulse rounded mb-2" />
<div className="h-4 w-full bg-gray-100 animate-pulse rounded" /> <div className="h-4 w-full bg-muted animate-pulse rounded" />
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
</div> </PageTemplateSkeleton>
); );
} }
@@ -2028,11 +2009,11 @@ export default function ControlOverviewPage() {
}, [controlId, loadQuery]); }, [controlId, loadQuery]);
if (!queryRef) { if (!queryRef) {
return <ControlOverviewPageFallback />; return <ControlOverviewPageSkeleton />;
} }
return ( return (
<Suspense fallback={<ControlOverviewPageFallback />}> <Suspense fallback={<ControlOverviewPageSkeleton />}>
<ControlOverviewPageContent queryRef={queryRef} /> <ControlOverviewPageContent queryRef={queryRef} />
</Suspense> </Suspense>
); );

View File

@@ -1,7 +1,6 @@
import { Suspense, useState } from "react"; import { Suspense, useState } from "react";
import { useNavigate, useParams } from "react-router"; import { useNavigate, useParams } from "react-router";
import { graphql, useMutation, ConnectionHandler } from "react-relay"; import { graphql, useMutation, ConnectionHandler } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -21,7 +20,7 @@ import {
CreateControlPageCreateControlMutation, CreateControlPageCreateControlMutation,
ControlImportance, ControlImportance,
} from "./__generated__/CreateControlPageCreateControlMutation.graphql"; } from "./__generated__/CreateControlPageCreateControlMutation.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const createControlMutation = graphql` const createControlMutation = graphql`
mutation CreateControlPageCreateControlMutation( mutation CreateControlPageCreateControlMutation(
@@ -182,17 +181,10 @@ function CreateControlPageContent() {
}; };
return ( return (
<> <PageTemplate
<Helmet>
<title>Create Control - Probo</title>
</Helmet>
<div className="container">
<PageHeader
className="mb-17"
title="Create Control" title="Create Control"
description="Create a new control for your framework" description="Create a new control for your framework"
/> >
<Card className="max-w-2xl"> <Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6"> <form onSubmit={handleSubmit} className="p-6 space-y-6">
<EditableField <EditableField
@@ -258,14 +250,24 @@ function CreateControlPageContent() {
</div> </div>
</form> </form>
</Card> </Card>
</div> </PageTemplate>
</> );
}
export function CreateControlPageSkeleton() {
return (
<PageTemplateSkeleton
title="Create Control"
description="Create a new control for your framework"
>
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
</PageTemplateSkeleton>
); );
} }
export default function CreateControlPage() { export default function CreateControlPage() {
return ( return (
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<CreateControlPageSkeleton />}>
<CreateControlPageContent /> <CreateControlPageContent />
</Suspense> </Suspense>
); );

View File

@@ -1,7 +1,6 @@
import { Suspense, useState } from "react"; import { Suspense, useState } from "react";
import { useNavigate, useParams } from "react-router"; import { useNavigate, useParams } from "react-router";
import { graphql, useMutation, ConnectionHandler } from "react-relay"; import { graphql, useMutation, ConnectionHandler } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -11,6 +10,7 @@ import { useToast } from "@/hooks/use-toast";
import { HelpCircle } from "lucide-react"; import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CreateFrameworkPageCreateFrameworkMutation } from "./__generated__/CreateFrameworkPageCreateFrameworkMutation.graphql"; import { CreateFrameworkPageCreateFrameworkMutation } from "./__generated__/CreateFrameworkPageCreateFrameworkMutation.graphql";
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const createFrameworkMutation = graphql` const createFrameworkMutation = graphql`
mutation CreateFrameworkPageCreateFrameworkMutation( mutation CreateFrameworkPageCreateFrameworkMutation(
@@ -69,7 +69,7 @@ function EditableField({
} }
className={cn( className={cn(
"w-full resize-none", "w-full resize-none",
required && !value && "border-red-500", required && !value && "border-red-500"
)} )}
placeholder={`Enter ${label.toLowerCase()}`} placeholder={`Enter ${label.toLowerCase()}`}
rows={4} rows={4}
@@ -101,7 +101,7 @@ function CreateFrameworkPageContent() {
const [commit, isInFlight] = const [commit, isInFlight] =
useMutation<CreateFrameworkPageCreateFrameworkMutation>( useMutation<CreateFrameworkPageCreateFrameworkMutation>(
createFrameworkMutation, createFrameworkMutation
); );
const handleFieldChange = (field: keyof typeof formData, value: unknown) => { const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
@@ -125,7 +125,7 @@ function CreateFrameworkPageContent() {
const connectionId = ConnectionHandler.getConnectionID( const connectionId = ConnectionHandler.getConnectionID(
organizationId!, organizationId!,
"FrameworkListPage_frameworks", "FrameworkListPage_frameworks"
); );
commit({ commit({
@@ -153,7 +153,7 @@ function CreateFrameworkPageContent() {
}); });
navigate( navigate(
`/organizations/${organizationId}/frameworks/${data.createFramework.frameworkEdge.node.id}`, `/organizations/${organizationId}/frameworks/${data.createFramework.frameworkEdge.node.id}`
); );
}, },
onError(error) { onError(error) {
@@ -167,18 +167,10 @@ function CreateFrameworkPageContent() {
}; };
return ( return (
<> <PageTemplate
<Helmet> title="Create Framework"
<title>Create Framework - Probo</title> description="Create a new framework to organize your controls"
</Helmet> >
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-2xl font-bold">Create Framework</h1>
<p className="text-muted-foreground">
Create a new framework to organize your controls
</p>
</div>
<Card className="max-w-2xl"> <Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6"> <form onSubmit={handleSubmit} className="p-6 space-y-6">
<EditableField <EditableField
@@ -213,14 +205,24 @@ function CreateFrameworkPageContent() {
</div> </div>
</form> </form>
</Card> </Card>
</div> </PageTemplate>
</> );
}
export function CreateFrameworkPageSkeleton() {
return (
<PageTemplateSkeleton
title="Create Framework"
description="Create a new framework to organize your controls"
>
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
</PageTemplateSkeleton>
); );
} }
export default function CreateFrameworkPage() { export default function CreateFrameworkPage() {
return ( return (
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<CreateFrameworkPageSkeleton />}>
<CreateFrameworkPageContent /> <CreateFrameworkPageContent />
</Suspense> </Suspense>
); );

View File

@@ -6,7 +6,6 @@ import {
useMutation, useMutation,
useLazyLoadQuery, useLazyLoadQuery,
} from "react-relay"; } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -21,6 +20,7 @@ import { useToast } from "@/hooks/use-toast";
import { HelpCircle } from "lucide-react"; import { HelpCircle } from "lucide-react";
import { CreateOrganizationPageCreateOrganizationMutation } from "./__generated__/CreateOrganizationPageCreateOrganizationMutation.graphql"; import { CreateOrganizationPageCreateOrganizationMutation } from "./__generated__/CreateOrganizationPageCreateOrganizationMutation.graphql";
import { CreateOrganizationPageViewerQuery } from "./__generated__/CreateOrganizationPageViewerQuery.graphql"; import { CreateOrganizationPageViewerQuery } from "./__generated__/CreateOrganizationPageViewerQuery.graphql";
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const createOrganizationMutation = graphql` const createOrganizationMutation = graphql`
mutation CreateOrganizationPageCreateOrganizationMutation( mutation CreateOrganizationPageCreateOrganizationMutation(
@@ -81,16 +81,27 @@ function EditableField({
); );
} }
export function CreateOrganizationPageSkeleton() {
return (
<PageTemplateSkeleton
title="Create Organization"
description="Create a new organization to manage your compliance and security needs."
>
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
</PageTemplateSkeleton>
);
}
export default function CreateOrganizationPage() { export default function CreateOrganizationPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const data = useLazyLoadQuery<CreateOrganizationPageViewerQuery>( const data = useLazyLoadQuery<CreateOrganizationPageViewerQuery>(
viewerQuery, viewerQuery,
{}, {}
); );
const [createOrganization] = const [createOrganization] =
useMutation<CreateOrganizationPageCreateOrganizationMutation>( useMutation<CreateOrganizationPageCreateOrganizationMutation>(
createOrganizationMutation, createOrganizationMutation
); );
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
@@ -114,7 +125,7 @@ export default function CreateOrganizationPage() {
connections: [ connections: [
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
data.viewer.id, data.viewer.id,
"OrganizationSwitcher_organizations", "OrganizationSwitcher_organizations"
), ),
], ],
}, },
@@ -138,24 +149,12 @@ export default function CreateOrganizationPage() {
}; };
return ( return (
<> <PageTemplate
<Helmet> title="Create Organization"
<title>Create Organization - Probo</title> description="Create a new organization to manage your compliance and security needs."
</Helmet> >
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
Create Organization
</h1>
<p className="text-muted-foreground">
Create a new organization to manage your compliance and security
needs.
</p>
</div>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<Card> <Card className="max-w-2xl">
<CardHeader> <CardHeader>
<CardTitle>Organization Details</CardTitle> <CardTitle>Organization Details</CardTitle>
<CardDescription> <CardDescription>
@@ -177,7 +176,6 @@ export default function CreateOrganizationPage() {
</CardContent> </CardContent>
</Card> </Card>
</form> </form>
</div> </PageTemplate>
</>
); );
} }

View File

@@ -1,7 +1,6 @@
import { Suspense, useState } from "react"; import { Suspense, useState } from "react";
import { useNavigate, useParams } from "react-router"; import { useNavigate, useParams } from "react-router";
import { graphql, useMutation, ConnectionHandler } from "react-relay"; import { graphql, useMutation, ConnectionHandler } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -10,7 +9,7 @@ import { useToast } from "@/hooks/use-toast";
import { HelpCircle } from "lucide-react"; import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CreatePeoplePageCreatePeopleMutation } from "./__generated__/CreatePeoplePageCreatePeopleMutation.graphql"; import { CreatePeoplePageCreatePeopleMutation } from "./__generated__/CreatePeoplePageCreatePeopleMutation.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const createPeopleMutation = graphql` const createPeopleMutation = graphql`
mutation CreatePeoplePageCreatePeopleMutation( mutation CreatePeoplePageCreatePeopleMutation(
@@ -150,19 +149,12 @@ function CreatePeoplePageContent() {
}; };
return ( return (
<> <PageTemplate
<Helmet> title="Create Person"
<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" description="Add a new person interacting with organization"
/> >
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="max-w-4xl space-y-6"> <div className="max-w-2xl space-y-6">
<EditableField <EditableField
label="Full Name" label="Full Name"
value={formData.fullName} value={formData.fullName}
@@ -192,14 +184,9 @@ function CreatePeoplePageContent() {
type="email" type="email"
value={email} value={email}
onChange={(e) => { onChange={(e) => {
const newEmails = [ const newEmails = [...formData.additionalEmailAddresses];
...formData.additionalEmailAddresses,
];
newEmails[index] = e.target.value; newEmails[index] = e.target.value;
handleFieldChange( handleFieldChange("additionalEmailAddresses", newEmails);
"additionalEmailAddresses",
newEmails
);
}} }}
/> />
<Button <Button
@@ -210,10 +197,7 @@ function CreatePeoplePageContent() {
formData.additionalEmailAddresses.filter( formData.additionalEmailAddresses.filter(
(_, i) => i !== index (_, i) => i !== index
); );
handleFieldChange( handleFieldChange("additionalEmailAddresses", newEmails);
"additionalEmailAddresses",
newEmails
);
}} }}
> >
Remove Remove
@@ -238,9 +222,7 @@ function CreatePeoplePageContent() {
<Card className="p-6"> <Card className="p-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<h2 className="text-lg font-medium"> <h2 className="text-lg font-medium">Additional Information</h2>
Additional Information
</h2>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Additional details about the person Additional details about the person
</p> </p>
@@ -298,11 +280,7 @@ function CreatePeoplePageContent() {
</Card> </Card>
</div> </div>
<div className="fixed bottom-6 right-6 flex gap-2"> <div className="fixed bottom-6 right-6 flex gap-2">
<Button <Button type="button" variant="outline" onClick={() => navigate(-1)}>
type="button"
variant="outline"
onClick={() => navigate(-1)}
>
Cancel Cancel
</Button> </Button>
<Button <Button
@@ -313,14 +291,24 @@ function CreatePeoplePageContent() {
</Button> </Button>
</div> </div>
</form> </form>
</div> </PageTemplate>
</> );
}
export function CreatePeoplePageSkeleton() {
return (
<PageTemplateSkeleton
title="Create Person"
description="Add a new person interacting with organization"
>
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
</PageTemplateSkeleton>
); );
} }
export default function CreatePeoplePage() { export default function CreatePeoplePage() {
return ( return (
<Suspense fallback={null}> <Suspense fallback={<CreatePeoplePageSkeleton />}>
<CreatePeoplePageContent /> <CreatePeoplePageContent />
</Suspense> </Suspense>
); );

View File

@@ -8,7 +8,6 @@ import {
usePreloadedQuery, usePreloadedQuery,
PreloadedQuery, PreloadedQuery,
} from "react-relay"; } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -21,7 +20,7 @@ import PeopleSelector from "@/components/PeopleSelector";
import { Suspense } from "react"; import { Suspense } from "react";
import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql"; import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql";
import type { CreatePolicyPageQuery as CreatePolicyPageQueryType } from "./__generated__/CreatePolicyPageQuery.graphql"; import type { CreatePolicyPageQuery as CreatePolicyPageQueryType } from "./__generated__/CreatePolicyPageQuery.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const CreatePolicyQuery = graphql` const CreatePolicyQuery = graphql`
query CreatePolicyPageQuery($organizationId: ID!) { query CreatePolicyPageQuery($organizationId: ID!) {
@@ -156,17 +155,10 @@ function CreatePolicyForm({
}; };
return ( return (
<> <PageTemplate
<Helmet>
<title>Create Policy - Probo Console</title>
</Helmet>
<div className="container">
<PageHeader
className="mb-17"
title="Create Policy" title="Create Policy"
description="Create a new policy for your organization" description="Create a new policy for your organization"
/> >
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="grid gap-6"> <div className="grid gap-6">
<Card> <Card>
@@ -234,10 +226,7 @@ function CreatePolicyForm({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label <Label htmlFor="reviewDate" className="flex items-center gap-2">
htmlFor="reviewDate"
className="flex items-center gap-2"
>
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
Review Date Review Date
</Label> </Label>
@@ -267,25 +256,18 @@ function CreatePolicyForm({
</div> </div>
</div> </div>
</form> </form>
</div> </PageTemplate>
</>
); );
} }
function CreatePolicyPageFallback() { export function CreatePolicyPageSkeleton() {
return ( return (
<div className="container mx-auto py-6"> <PageTemplateSkeleton
<div className="flex items-center mb-6"> title="Create Policy"
<div className="mr-4"> description="Create a new policy for your organization"
<div className="h-12 w-12 bg-muted animate-pulse rounded-lg" /> >
</div>
<div>
<div className="h-8 w-48 bg-muted animate-pulse rounded mb-2" />
<div className="h-4 w-64 bg-muted animate-pulse rounded" />
</div>
</div>
<div className="bg-muted animate-pulse rounded-lg h-[600px]" /> <div className="bg-muted animate-pulse rounded-lg h-[600px]" />
</div> </PageTemplateSkeleton>
); );
} }
@@ -300,9 +282,13 @@ export default function CreatePolicyPage() {
} }
}, [organizationId, loadQuery]); }, [organizationId, loadQuery]);
if (!queryRef) {
return <CreatePolicyPageSkeleton />;
}
return ( return (
<Suspense fallback={<CreatePolicyPageFallback />}> <Suspense fallback={<CreatePolicyPageSkeleton />}>
{queryRef && <CreatePolicyForm queryRef={queryRef} />} {<CreatePolicyForm queryRef={queryRef} />}
</Suspense> </Suspense>
); );
} }

View File

@@ -320,6 +320,10 @@ export default function FrameworkListPage() {
loadQuery({ organizationId: organizationId! }); loadQuery({ organizationId: organizationId! });
}, [loadQuery, organizationId]); }, [loadQuery, organizationId]);
if (!queryRef) {
return <FrameworkListPageSkeleton />;
}
return ( return (
<Suspense fallback={<FrameworkListPageSkeleton />}> <Suspense fallback={<FrameworkListPageSkeleton />}>
{queryRef && <FrameworkListPageContent queryRef={queryRef} />} {queryRef && <FrameworkListPageContent queryRef={queryRef} />}

View File

@@ -20,8 +20,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql"; import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql";
import { Helmet } from "react-helmet-async"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
import { PageHeader } from "@/components/PageHeader";
const FrameworkOverviewPageQuery = graphql` const FrameworkOverviewPageQuery = graphql`
query FrameworkOverviewPageQuery($frameworkId: ID!) { query FrameworkOverviewPageQuery($frameworkId: ID!) {
@@ -169,9 +168,7 @@ function FrameworkOverviewPageContent({
}; };
return ( return (
<div className="container space-y-6"> <PageTemplate
<PageHeader
className="mb-10"
title={framework.name ?? ""} title={framework.name ?? ""}
description={framework.description ?? ""} description={framework.description ?? ""}
actions={ actions={
@@ -193,8 +190,7 @@ function FrameworkOverviewPageContent({
</Button> </Button>
</div> </div>
} }
/> >
<div className="grid gap-6"> <div className="grid gap-6">
{categories.map((category) => { {categories.map((category) => {
const isExpanded = expandedCategories.includes(category.id); const isExpanded = expandedCategories.includes(category.id);
@@ -326,17 +322,13 @@ function FrameworkOverviewPageContent({
); );
})} })}
</div> </div>
</div> </PageTemplate>
); );
} }
function FrameworkOverviewPageFallback() { export function FrameworkOverviewPageSkeleton() {
return ( return (
<div className="min-h-screen bg-background p-6"> <PageTemplateSkeleton>
<div className="mb-8">
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-2" />
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<Card key={i}> <Card key={i}>
@@ -351,7 +343,7 @@ function FrameworkOverviewPageFallback() {
</Card> </Card>
))} ))}
</div> </div>
</div> </PageTemplateSkeleton>
); );
} }
@@ -365,14 +357,13 @@ export default function FrameworkOverviewPage() {
loadQuery({ frameworkId: frameworkId! }); loadQuery({ frameworkId: frameworkId! });
}, [loadQuery, frameworkId]); }, [loadQuery, frameworkId]);
if (!queryRef) {
return <FrameworkOverviewPageSkeleton />;
}
return ( return (
<> <Suspense fallback={<FrameworkOverviewPageSkeleton />}>
<Helmet>
<title>Framework Overview - Probo Console</title>
</Helmet>
<Suspense fallback={<FrameworkOverviewPageFallback />}>
{queryRef && <FrameworkOverviewPageContent queryRef={queryRef} />} {queryRef && <FrameworkOverviewPageContent queryRef={queryRef} />}
</Suspense> </Suspense>
</>
); );
} }

View File

@@ -13,12 +13,11 @@ import { Badge } from "@/components/ui/badge";
import { UserPlus, Trash2, ChevronRight } from "lucide-react"; import { UserPlus, Trash2, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Link } from "react-router"; import { Link } from "react-router";
import { Helmet } from "react-helmet-async";
import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generated__/PeopleListPageQuery.graphql"; import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generated__/PeopleListPageQuery.graphql";
import type { PeopleListPageDeletePeopleMutation } from "./__generated__/PeopleListPageDeletePeopleMutation.graphql"; import type { PeopleListPageDeletePeopleMutation } from "./__generated__/PeopleListPageDeletePeopleMutation.graphql";
import { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql"; import { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql";
import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql"; import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const ITEMS_PER_PAGE = 25; const ITEMS_PER_PAGE = 25;
@@ -174,14 +173,8 @@ function PeopleListContent({
const pageInfo = peoplesConnection.peoples.pageInfo; const pageInfo = peoplesConnection.peoples.pageInfo;
return ( return (
<> <PageTemplate
<Helmet> title="People"
<title>People - Probo</title>
</Helmet>
<div className="container space-y-6">
<PageHeader
className="mb-17"
title="Employees"
description="Keep track of your company's workforce and their progress description="Keep track of your company's workforce and their progress
towards completing tasks assigned to them." towards completing tasks assigned to them."
actions={ actions={
@@ -197,8 +190,8 @@ function PeopleListContent({
</Link> </Link>
</Button> </Button>
} }
/> >
<div className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
{peoples.map((person) => ( {peoples.map((person) => (
<Link <Link
@@ -296,27 +289,32 @@ function PeopleListContent({
}} }}
/> />
</div> </div>
</> </PageTemplate>
); );
} }
function PeopleListPageFallback() { export function PeopleListPageSkeleton() {
return ( return (
<PageTemplateSkeleton
title="People"
description="Keep track of your company's workforce and their progress towards completing tasks assigned to them."
actions={<div className="bg-muted animate-pulse h-9 w-1/6 rounded-lg" />}
>
<div className="space-y-6"> <div className="space-y-6">
<div> <div className="rounded-xl border bg-card p-4 space-y-2">
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-1" />
</div>
<div className="rounded-xl border bg-card p-4 space-y-4">
<div className="h-5 w-32 bg-muted animate-pulse rounded" /> <div className="h-5 w-32 bg-muted animate-pulse rounded" />
<div className="h-10 w-full bg-muted animate-pulse rounded" /> <div className="h-10 w-full bg-muted animate-pulse rounded" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<div key={i} className="h-[72px] bg-muted animate-pulse rounded-xl" /> <div
key={i}
className="h-[72px] bg-muted animate-pulse rounded-xl"
/>
))} ))}
</div> </div>
</div> </div>
</PageTemplateSkeleton>
); );
} }
@@ -341,17 +339,12 @@ export default function PeopleListPage() {
}, [loadQuery, organizationId, searchParams]); }, [loadQuery, organizationId, searchParams]);
if (!queryRef) { if (!queryRef) {
return <PeopleListPageFallback />; return <PeopleListPageSkeleton />;
} }
return ( return (
<> <Suspense fallback={<PeopleListPageSkeleton />}>
<Helmet>
<title>People - Probo Console</title>
</Helmet>
<Suspense fallback={<PeopleListPageFallback />}>
<PeopleListContent queryRef={queryRef} /> <PeopleListContent queryRef={queryRef} />
</Suspense> </Suspense>
</>
); );
} }

View File

@@ -16,9 +16,8 @@ import {
import { Suspense, useEffect, useState, useCallback } from "react"; import { Suspense, useEffect, useState, useCallback } from "react";
import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./__generated__/PeopleOverviewPageQuery.graphql"; import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./__generated__/PeopleOverviewPageQuery.graphql";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { Helmet } from "react-helmet-async";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const peopleOverviewPageQuery = graphql` const peopleOverviewPageQuery = graphql`
query PeopleOverviewPageQuery($peopleId: ID!) { query PeopleOverviewPageQuery($peopleId: ID!) {
@@ -161,12 +160,7 @@ function PeopleOverviewPageContent({
}; };
return ( return (
<> <PageTemplate title={formData.fullName}>
<Helmet>
<title>Person - Probo</title>
</Helmet>
<div className="container">
<PageHeader className="mb-17" title={formData.fullName} />
<div className="space-y-6"> <div className="space-y-6">
<div className="mx-auto max-w-4xl space-y-6"> <div className="mx-auto max-w-4xl space-y-6">
<EditableField <EditableField
@@ -196,14 +190,9 @@ function PeopleOverviewPageContent({
type="email" type="email"
value={email} value={email}
onChange={(e) => { onChange={(e) => {
const newEmails = [ const newEmails = [...formData.additionalEmailAddresses];
...formData.additionalEmailAddresses,
];
newEmails[index] = e.target.value; newEmails[index] = e.target.value;
handleFieldChange( handleFieldChange("additionalEmailAddresses", newEmails);
"additionalEmailAddresses",
newEmails
);
}} }}
/> />
<Button <Button
@@ -213,10 +202,7 @@ function PeopleOverviewPageContent({
formData.additionalEmailAddresses.filter( formData.additionalEmailAddresses.filter(
(_, i) => i !== index (_, i) => i !== index
); );
handleFieldChange( handleFieldChange("additionalEmailAddresses", newEmails);
"additionalEmailAddresses",
newEmails
);
}} }}
> >
Remove Remove
@@ -240,9 +226,7 @@ function PeopleOverviewPageContent({
<Card className="p-6"> <Card className="p-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<h2 className="text-lg font-medium"> <h2 className="text-lg font-medium">Additional Information</h2>
Additional Information
</h2>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Additional details about the person Additional details about the person
</p> </p>
@@ -311,14 +295,14 @@ function PeopleOverviewPageContent({
</Button> </Button>
</div> </div>
)} )}
</div> </PageTemplate>
</>
); );
} }
function PeopleOverviewPageFallback() { export function PeopleOverviewPageSkeleton() {
return ( return (
<div className="p-6 space-y-6"> <PageTemplateSkeleton>
<div className="space-y-6">
<div className="space-y-1"> <div className="space-y-1">
<div className="h-8 w-48 bg-muted animate-pulse rounded" /> <div className="h-8 w-48 bg-muted animate-pulse rounded" />
<div className="h-4 w-96 bg-muted animate-pulse rounded" /> <div className="h-4 w-96 bg-muted animate-pulse rounded" />
@@ -329,6 +313,7 @@ function PeopleOverviewPageFallback() {
))} ))}
</div> </div>
</div> </div>
</PageTemplateSkeleton>
); );
} }
@@ -343,17 +328,12 @@ export default function PeopleOverviewPage() {
}, [loadQuery, peopleId]); }, [loadQuery, peopleId]);
if (!queryRef) { if (!queryRef) {
return <PeopleOverviewPageFallback />; return <PeopleOverviewPageSkeleton />;
} }
return ( return (
<> <Suspense fallback={<PeopleOverviewPageSkeleton />}>
<Helmet>
<title>People Overview - Probo Console</title>
</Helmet>
<Suspense fallback={<PeopleOverviewPageFallback />}>
<PeopleOverviewPageContent queryRef={queryRef} /> <PeopleOverviewPageContent queryRef={queryRef} />
</Suspense> </Suspense>
</>
); );
} }

View File

@@ -7,7 +7,6 @@ import {
} from "react-relay"; } from "react-relay";
import { Card, CardContent, CardFooter } from "@/components/ui/card"; import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { Link, useParams } from "react-router"; import { Link, useParams } from "react-router";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
@@ -34,7 +33,7 @@ import {
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { format } from "date-fns"; import { format } from "date-fns";
import type { PolicyListPageQuery as PolicyListPageQueryType } from "./__generated__/PolicyListPageQuery.graphql"; import type { PolicyListPageQuery as PolicyListPageQueryType } from "./__generated__/PolicyListPageQuery.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const PolicyListPageQuery = graphql` const PolicyListPageQuery = graphql`
query PolicyListPageQuery($organizationId: ID!) { query PolicyListPageQuery($organizationId: ID!) {
@@ -220,13 +219,7 @@ function PolicyListPageContent({
}); });
return ( return (
<> <PageTemplate
<Helmet>
<title>Policies - Probo</title>
</Helmet>
<div className="container space-y-6">
<PageHeader
className="mb-17"
title="Policies" title="Policies"
description="Manage your organization's policies" description="Manage your organization's policies"
actions={ actions={
@@ -237,8 +230,7 @@ function PolicyListPageContent({
</Link> </Link>
</Button> </Button>
} }
/> >
{/* Search and filter controls */} {/* Search and filter controls */}
<div className="flex flex-col md:flex-row gap-4 mb-6"> <div className="flex flex-col md:flex-row gap-4 mb-6">
<div className="relative flex-1"> <div className="relative flex-1">
@@ -351,22 +343,17 @@ function PolicyListPageContent({
</div> </div>
)} )}
</div> </div>
</div> </PageTemplate>
</>
); );
} }
function PolicyListPageFallback() { export function PolicyListPageSkeleton() {
return ( return (
<div className="container mx-auto py-6"> <PageTemplateSkeleton
<div className="flex justify-between items-center mb-6"> title="Policies"
<div> description="Manage your organization's policies"
<div className="h-8 w-48 bg-muted animate-pulse rounded" /> actions={<div className="bg-muted animate-pulse h-9 w-1/6 rounded-lg" />}
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-1" /> >
</div>
<div className="h-10 w-36 bg-muted animate-pulse rounded" />
</div>
{/* Search and filter controls skeleton */} {/* Search and filter controls skeleton */}
<div className="flex flex-col md:flex-row gap-4 mb-6"> <div className="flex flex-col md:flex-row gap-4 mb-6">
<div className="flex-1 h-10 bg-muted animate-pulse rounded" /> <div className="flex-1 h-10 bg-muted animate-pulse rounded" />
@@ -407,7 +394,7 @@ function PolicyListPageFallback() {
</Card> </Card>
))} ))}
</div> </div>
</div> </PageTemplateSkeleton>
); );
} }
@@ -421,14 +408,13 @@ export default function PolicyListPage() {
loadQuery({ organizationId: organizationId! }); loadQuery({ organizationId: organizationId! });
}, [loadQuery, organizationId]); }, [loadQuery, organizationId]);
if (!queryRef) {
return <PolicyListPageSkeleton />;
}
return ( return (
<> <Suspense fallback={<PolicyListPageSkeleton />}>
<Helmet> {<PolicyListPageContent queryRef={queryRef} />}
<title>Policies - Probo Console</title>
</Helmet>
<Suspense fallback={<PolicyListPageFallback />}>
{queryRef && <PolicyListPageContent queryRef={queryRef} />}
</Suspense> </Suspense>
</>
); );
} }

View File

@@ -15,10 +15,9 @@ import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { PolicyOverviewPageQuery as PolicyOverviewPageQueryType } from "./__generated__/PolicyOverviewPageQuery.graphql"; import type { PolicyOverviewPageQuery as PolicyOverviewPageQueryType } from "./__generated__/PolicyOverviewPageQuery.graphql";
import type { PolicyOverviewPageDeleteMutation } from "./__generated__/PolicyOverviewPageDeleteMutation.graphql"; import type { PolicyOverviewPageDeleteMutation } from "./__generated__/PolicyOverviewPageDeleteMutation.graphql";
import { Helmet } from "react-helmet-async";
import "../styles/policy-content.css"; import "../styles/policy-content.css";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const PolicyOverviewPageQuery = graphql` const PolicyOverviewPageQuery = graphql`
query PolicyOverviewPageQuery($policyId: ID!) { query PolicyOverviewPageQuery($policyId: ID!) {
@@ -149,19 +148,11 @@ function PolicyOverviewPageContent({
}; };
return ( return (
<> <PageTemplate
<Helmet>
<title>{policy.name} - Probo Console</title>
</Helmet>
<div className="container">
<PageHeader
className="mb-17"
title={policy.name ?? ""} title={policy.name ?? ""}
description={ description={
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center gap-2 text-muted-foreground">
<Badge <Badge variant={policy.status === "ACTIVE" ? "default" : "outline"}>
variant={policy.status === "ACTIVE" ? "default" : "outline"}
>
{policy.status === "ACTIVE" ? "Active" : "Draft"} {policy.status === "ACTIVE" ? "Active" : "Draft"}
</Badge> </Badge>
{policy.owner && ( {policy.owner && (
@@ -214,8 +205,7 @@ function PolicyOverviewPageContent({
</Button> </Button>
</div> </div>
} }
/> >
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<Card className="border shadow-sm"> <Card className="border shadow-sm">
@@ -332,9 +322,7 @@ function PolicyOverviewPageContent({
<FileText className="h-4 w-4" /> <FileText className="h-4 w-4" />
<span className="text-sm">Last Updated</span> <span className="text-sm">Last Updated</span>
</div> </div>
<p className="font-medium"> <p className="font-medium">{formatDate(policy.updatedAt)}</p>
{formatDate(policy.updatedAt)}
</p>
</div> </div>
<div> <div>
@@ -380,8 +368,8 @@ function PolicyOverviewPageContent({
<CardContent className="p-6"> <CardContent className="p-6">
<h3 className="text-red-500 font-semibold mb-3">Danger Zone</h3> <h3 className="text-red-500 font-semibold mb-3">Danger Zone</h3>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
Permanently delete this policy and all of its data. This Permanently delete this policy and all of its data. This action
action cannot be undone. cannot be undone.
</p> </p>
<Button <Button
variant="destructive" variant="destructive"
@@ -411,14 +399,21 @@ function PolicyOverviewPageContent({
</Card> </Card>
</div> </div>
</div> </div>
</div> </PageTemplate>
</>
); );
} }
function PolicyOverviewPageFallback() { export function PolicyOverviewPageSkeleton() {
return ( return (
<div className="container mx-auto py-6"> <PageTemplateSkeleton
withDescription
actions={
<div className="flex gap-2 w-1/3">
<div className="bg-muted animate-pulse h-8 w-1/2 rounded-lg" />
<div className="bg-muted animate-pulse h-8 w-1/2 rounded-lg" />
</div>
}
>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<Card className="border shadow-sm"> <Card className="border shadow-sm">
@@ -505,7 +500,7 @@ function PolicyOverviewPageFallback() {
</Card> </Card>
</div> </div>
</div> </div>
</div> </PageTemplateSkeleton>
); );
} }
@@ -520,8 +515,12 @@ export default function PolicyOverviewPage() {
loadQuery({ policyId: policyId! }); loadQuery({ policyId: policyId! });
}, [loadQuery, policyId]); }, [loadQuery, policyId]);
if (!queryRef) {
return <PolicyOverviewPageSkeleton />;
}
return ( return (
<Suspense fallback={<PolicyOverviewPageFallback />}> <Suspense fallback={<PolicyOverviewPageSkeleton />}>
{queryRef && <PolicyOverviewPageContent queryRef={queryRef} />} {queryRef && <PolicyOverviewPageContent queryRef={queryRef} />}
</Suspense> </Suspense>
); );

View File

@@ -14,7 +14,6 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Helmet } from "react-helmet-async";
import { Suspense, useEffect, useState, useRef } from "react"; import { Suspense, useEffect, useState, useRef } from "react";
import { import {
graphql, graphql,
@@ -39,7 +38,7 @@ import type { SettingsPageQuery as SettingsPageQueryType } from "./__generated__
import type { SettingsPageUpdateOrganizationMutation as SettingsPageUpdateOrganizationMutationType } from "./__generated__/SettingsPageUpdateOrganizationMutation.graphql"; import type { SettingsPageUpdateOrganizationMutation as SettingsPageUpdateOrganizationMutationType } from "./__generated__/SettingsPageUpdateOrganizationMutation.graphql";
import type { SettingsPageInviteUserMutation as SettingsPageInviteUserMutationType } from "./__generated__/SettingsPageInviteUserMutation.graphql"; import type { SettingsPageInviteUserMutation as SettingsPageInviteUserMutationType } from "./__generated__/SettingsPageInviteUserMutation.graphql";
import type { SettingsPageRemoveUserMutation as SettingsPageRemoveUserMutationType } from "./__generated__/SettingsPageRemoveUserMutation.graphql"; import type { SettingsPageRemoveUserMutation as SettingsPageRemoveUserMutationType } from "./__generated__/SettingsPageRemoveUserMutation.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const settingsPageQuery = graphql` const settingsPageQuery = graphql`
query SettingsPageQuery($organizationID: ID!) { query SettingsPageQuery($organizationID: ID!) {
@@ -290,14 +289,11 @@ function SettingsPageContent({
}; };
return ( return (
<> <PageTemplate
<div className="container space-y-6">
<PageHeader
className="mb-17"
title="Settings" title="Settings"
description="Manage your details and personal preferences here" description="Manage your details and personal preferences here"
/> >
<div className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Organization information</CardTitle> <CardTitle>Organization information</CardTitle>
@@ -519,13 +515,17 @@ function SettingsPageContent({
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</> </PageTemplate>
); );
} }
function SettingsPageFallback() { export function SettingsPageSkeleton() {
return ( return (
<div className="p-6 space-y-6"> <PageTemplateSkeleton
title="Settings"
description="Manage your details and personal preferences here"
>
<div className="space-y-6">
<div className="space-y-1"> <div className="space-y-1">
<div className="h-8 w-48 bg-muted animate-pulse rounded" /> <div className="h-8 w-48 bg-muted animate-pulse rounded" />
<div className="h-4 w-96 bg-muted animate-pulse rounded" /> <div className="h-4 w-96 bg-muted animate-pulse rounded" />
@@ -536,6 +536,7 @@ function SettingsPageFallback() {
))} ))}
</div> </div>
</div> </div>
</PageTemplateSkeleton>
); );
} }
@@ -550,17 +551,12 @@ export default function SettingsPage() {
}, [loadQuery, organizationId]); }, [loadQuery, organizationId]);
if (!queryRef) { if (!queryRef) {
return <SettingsPageFallback />; return <SettingsPageSkeleton />;
} }
return ( return (
<> <Suspense fallback={<SettingsPageSkeleton />}>
<Helmet>
<title>Settings - Probo Console</title>
</Helmet>
<Suspense fallback={<SettingsPageFallback />}>
<SettingsPageContent queryRef={queryRef} /> <SettingsPageContent queryRef={queryRef} />
</Suspense> </Suspense>
</>
); );
} }

View File

@@ -7,7 +7,6 @@ import {
PreloadedQuery, PreloadedQuery,
useQueryLoader, useQueryLoader,
} from "react-relay"; } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -28,7 +27,7 @@ import type {
ControlState, ControlState,
ControlImportance, ControlImportance,
} from "./__generated__/UpdateControlPageUpdateControlMutation.graphql"; } from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const updateControlMutation = graphql` const updateControlMutation = graphql`
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) { mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
@@ -250,17 +249,10 @@ function UpdateControlPageContent({
}; };
return ( return (
<> <PageTemplate
<Helmet>
<title>Update Control - Probo</title>
</Helmet>
<div className="container">
<PageHeader
className="mb-17"
title="Update Control" title="Update Control"
description="Update the control details" description="Update the control details"
/> >
<Card className="max-w-2xl"> <Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6"> <form onSubmit={handleSubmit} className="p-6 space-y-6">
<EditableField <EditableField
@@ -292,9 +284,7 @@ function UpdateControlPageContent({
</Label> </Label>
<Select <Select
value={formData.importance} value={formData.importance}
onValueChange={(value) => onValueChange={(value) => handleFieldChange("importance", value)}
handleFieldChange("importance", value)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select importance" /> <SelectValue placeholder="Select importance" />
@@ -337,13 +327,19 @@ function UpdateControlPageContent({
</div> </div>
</form> </form>
</Card> </Card>
</div> </PageTemplate>
</>
); );
} }
function UpdateControlPageFallback() { export function UpdateControlPageSkeleton() {
return <div>Loading...</div>; return (
<PageTemplateSkeleton
title="Update Control"
description="Update the control details"
>
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
</PageTemplateSkeleton>
);
} }
export default function UpdateControlPage() { export default function UpdateControlPage() {
@@ -358,11 +354,11 @@ export default function UpdateControlPage() {
}, [controlId, loadQuery]); }, [controlId, loadQuery]);
if (!queryRef) { if (!queryRef) {
return <UpdateControlPageFallback />; return <UpdateControlPageSkeleton />;
} }
return ( return (
<Suspense fallback={<UpdateControlPageFallback />}> <Suspense fallback={<UpdateControlPageSkeleton />}>
<UpdateControlPageContent queryRef={queryRef} /> <UpdateControlPageContent queryRef={queryRef} />
</Suspense> </Suspense>
); );

View File

@@ -7,7 +7,6 @@ import {
PreloadedQuery, PreloadedQuery,
useQueryLoader, useQueryLoader,
} from "react-relay"; } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -18,7 +17,7 @@ import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { UpdateFrameworkPageUpdateFrameworkMutation } from "./__generated__/UpdateFrameworkPageUpdateFrameworkMutation.graphql"; import { UpdateFrameworkPageUpdateFrameworkMutation } from "./__generated__/UpdateFrameworkPageUpdateFrameworkMutation.graphql";
import { UpdateFrameworkPageQuery as UpdateFrameworkPageQueryType } from "./__generated__/UpdateFrameworkPageQuery.graphql"; import { UpdateFrameworkPageQuery as UpdateFrameworkPageQueryType } from "./__generated__/UpdateFrameworkPageQuery.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const updateFrameworkMutation = graphql` const updateFrameworkMutation = graphql`
mutation UpdateFrameworkPageUpdateFrameworkMutation( mutation UpdateFrameworkPageUpdateFrameworkMutation(
@@ -199,17 +198,10 @@ function UpdateFrameworkPageContent({
}; };
return ( return (
<> <PageTemplate
<Helmet>
<title>Update Framework - Probo</title>
</Helmet>
<div className="container">
<PageHeader
className="mb-17"
title="Update Framework" title="Update Framework"
description="Update the framework details" description="Update the framework details"
/> >
<Card className="max-w-2xl"> <Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6"> <form onSubmit={handleSubmit} className="p-6 space-y-6">
<EditableField <EditableField
@@ -238,13 +230,19 @@ function UpdateFrameworkPageContent({
</div> </div>
</form> </form>
</Card> </Card>
</div> </PageTemplate>
</>
); );
} }
function UpdateFrameworkPageFallback() { export function UpdateFrameworkPageSkeleton() {
return <div>Loading...</div>; return (
<PageTemplateSkeleton
title="Update Framework"
description="Update the framework details"
>
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
</PageTemplateSkeleton>
);
} }
export default function UpdateFrameworkPage() { export default function UpdateFrameworkPage() {
@@ -259,11 +257,11 @@ export default function UpdateFrameworkPage() {
}, [frameworkId, loadQuery]); }, [frameworkId, loadQuery]);
if (!queryRef) { if (!queryRef) {
return <UpdateFrameworkPageFallback />; return <UpdateFrameworkPageSkeleton />;
} }
return ( return (
<Suspense fallback={<UpdateFrameworkPageFallback />}> <Suspense fallback={<UpdateFrameworkPageSkeleton />}>
<UpdateFrameworkPageContent queryRef={queryRef} /> <UpdateFrameworkPageContent queryRef={queryRef} />
</Suspense> </Suspense>
); );

View File

@@ -7,7 +7,6 @@ import {
useQueryLoader, useQueryLoader,
PreloadedQuery, PreloadedQuery,
} from "react-relay"; } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -20,7 +19,7 @@ import PolicyEditor from "@/components/PolicyEditor";
import PeopleSelector from "@/components/PeopleSelector"; import PeopleSelector from "@/components/PeopleSelector";
import type { UpdatePolicyPageQuery as UpdatePolicyPageQueryType } from "./__generated__/UpdatePolicyPageQuery.graphql"; import type { UpdatePolicyPageQuery as UpdatePolicyPageQueryType } from "./__generated__/UpdatePolicyPageQuery.graphql";
import type { UpdatePolicyPageMutation as UpdatePolicyPageMutationType } from "./__generated__/UpdatePolicyPageMutation.graphql"; import type { UpdatePolicyPageMutation as UpdatePolicyPageMutationType } from "./__generated__/UpdatePolicyPageMutation.graphql";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const UpdatePolicyPageQuery = graphql` const UpdatePolicyPageQuery = graphql`
query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) { query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) {
@@ -148,17 +147,7 @@ function UpdatePolicyPageContent({
}; };
return ( return (
<> <PageTemplate title="Update Policy" description="Update an existing policy">
<Helmet>
<title>Update Policy - Probo Console</title>
</Helmet>
<div className="container">
<PageHeader
className="mb-17"
title="Update Policy"
description="Update an existing policy"
/>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="grid gap-6"> <div className="grid gap-6">
<Card> <Card>
@@ -226,10 +215,7 @@ function UpdatePolicyPageContent({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label <Label htmlFor="reviewDate" className="flex items-center gap-2">
htmlFor="reviewDate"
className="flex items-center gap-2"
>
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
Review Date Review Date
</Label> </Label>
@@ -261,14 +247,16 @@ function UpdatePolicyPageContent({
</div> </div>
</div> </div>
</form> </form>
</div> </PageTemplate>
</>
); );
} }
function UpdatePolicyPageFallback() { export function UpdatePolicyPageSkeleton() {
return ( return (
<div className="container mx-auto py-6"> <PageTemplateSkeleton
title="Update Policy"
description="Update an existing policy"
>
<div className="flex items-center mb-6"> <div className="flex items-center mb-6">
<div className="mr-4"> <div className="mr-4">
<div className="h-12 w-12 bg-muted animate-pulse rounded-lg" /> <div className="h-12 w-12 bg-muted animate-pulse rounded-lg" />
@@ -279,7 +267,7 @@ function UpdatePolicyPageFallback() {
</div> </div>
</div> </div>
<div className="bg-muted animate-pulse rounded-lg h-[600px]" /> <div className="bg-muted animate-pulse rounded-lg h-[600px]" />
</div> </PageTemplateSkeleton>
); );
} }
@@ -295,8 +283,12 @@ export default function UpdatePolicyPage() {
} }
}, [organizationId, policyId, loadQuery]); }, [organizationId, policyId, loadQuery]);
if (!queryRef) {
return <UpdatePolicyPageSkeleton />;
}
return ( return (
<Suspense fallback={<UpdatePolicyPageFallback />}> <Suspense fallback={<UpdatePolicyPageSkeleton />}>
{queryRef && <UpdatePolicyPageContent queryRef={queryRef} />} {queryRef && <UpdatePolicyPageContent queryRef={queryRef} />}
</Suspense> </Suspense>
); );

View File

@@ -16,13 +16,12 @@ import { Button } from "@/components/ui/button";
import { Link } from "react-router"; import { Link } from "react-router";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import type { VendorListPageQuery as VendorListPageQueryType } from "./__generated__/VendorListPageQuery.graphql"; import type { VendorListPageQuery as VendorListPageQueryType } from "./__generated__/VendorListPageQuery.graphql";
import { Helmet } from "react-helmet-async";
import { VendorListPageCreateVendorMutation } from "./__generated__/VendorListPageCreateVendorMutation.graphql"; import { VendorListPageCreateVendorMutation } from "./__generated__/VendorListPageCreateVendorMutation.graphql";
import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPageDeleteVendorMutation.graphql"; import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPageDeleteVendorMutation.graphql";
import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql"; import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql";
import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql"; import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const ITEMS_PER_PAGE = 25; const ITEMS_PER_PAGE = 25;
@@ -240,18 +239,11 @@ function VendorListContent({
}); });
return ( return (
<> <PageTemplate
<Helmet>
<title>Vendors - Probo</title>
</Helmet>
<div className="space-y-6">
<PageHeader
className="mb-17"
title="Vendors" title="Vendors"
description="Vendors are third-party services that your company uses. Add them to description="Vendors are third-party services that your company uses. Add them to
keep track of their risk and compliance status." keep track of their risk and compliance status."
/> >
<div className="rounded-xl border bg-card p-4"> <div className="rounded-xl border bg-card p-4">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Store className="h-5 w-5" /> <Store className="h-5 w-5" />
@@ -269,9 +261,7 @@ function VendorListContent({
if (value.trim() === "") { if (value.trim() === "") {
setFilteredVendors([]); setFilteredVendors([]);
} else { } else {
const results = fuse const results = fuse.search(value).map((result) => result.item);
.search(value)
.map((result) => result.item);
setFilteredVendors(results); setFilteredVendors(results);
} }
}} }}
@@ -462,14 +452,17 @@ function VendorListContent({
}); });
}} }}
/> />
</div> </PageTemplate>
</>
); );
} }
function VendorListFallback() { export function VendorListPageSkeleton() {
return ( return (
<div className="space-y-6"> <PageTemplateSkeleton
title="Vendors"
description="Vendors are third-party services that your company uses. Add them to
keep track of their risk and compliance status."
>
<div> <div>
<div className="h-8 w-48 bg-muted animate-pulse rounded" /> <div className="h-8 w-48 bg-muted animate-pulse rounded" />
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-1" /> <div className="h-4 w-96 bg-muted animate-pulse rounded mt-1" />
@@ -486,7 +479,7 @@ function VendorListFallback() {
<div key={i} className="h-[72px] bg-muted animate-pulse rounded-xl" /> <div key={i} className="h-[72px] bg-muted animate-pulse rounded-xl" />
))} ))}
</div> </div>
</div> </PageTemplateSkeleton>
); );
} }
@@ -511,17 +504,12 @@ export default function VendorListPage() {
}, [loadQuery, organizationId]); }, [loadQuery, organizationId]);
if (!queryRef) { if (!queryRef) {
return <VendorListFallback />; return <VendorListPageSkeleton />;
} }
return ( return (
<> <Suspense fallback={<VendorListPageSkeleton />}>
<Helmet>
<title>Vendors - Probo Console</title>
</Helmet>
<Suspense fallback={<VendorListFallback />}>
<VendorListContent queryRef={queryRef} /> <VendorListContent queryRef={queryRef} />
</Suspense> </Suspense>
</>
); );
} }

View File

@@ -16,9 +16,8 @@ import {
import { Suspense, useEffect, useState, useCallback } from "react"; import { Suspense, useEffect, useState, useCallback } from "react";
import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql"; import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { Helmet } from "react-helmet-async";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PageHeader } from "@/components/PageHeader"; import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
const vendorOverviewPageQuery = graphql` const vendorOverviewPageQuery = graphql`
query VendorOverviewPageQuery($vendorId: ID!) { query VendorOverviewPageQuery($vendorId: ID!) {
@@ -205,13 +204,8 @@ function VendorOverviewPageContent({
}; };
return ( return (
<> <PageTemplate title={formData.name}>
<Helmet> <div className="max-w-2xl space-y-6">
<title>Vendor - Probo</title>
</Helmet>
<div className="container">
<PageHeader className="mb-17" title={formData.name} />
<div className="max-w-4xl space-y-6">
<EditableField <EditableField
label="Name" label="Name"
value={formData.name} value={formData.name}
@@ -238,9 +232,7 @@ function VendorOverviewPageContent({
label="Service Start At" label="Service Start At"
value={formData.serviceStartAt} value={formData.serviceStartAt}
type="datetime-local" type="datetime-local"
onChange={(value) => onChange={(value) => handleFieldChange("serviceStartAt", value)}
handleFieldChange("serviceStartAt", value)
}
/> />
<EditableField <EditableField
@@ -326,9 +318,7 @@ function VendorOverviewPageContent({
Critical Critical
</button> </button>
<button <button
onClick={() => onClick={() => handleFieldChange("riskTier", "SIGNIFICANT")}
handleFieldChange("riskTier", "SIGNIFICANT")
}
className={cn( className={cn(
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
formData.riskTier === "SIGNIFICANT" formData.riskTier === "SIGNIFICANT"
@@ -363,9 +353,7 @@ function VendorOverviewPageContent({
<EditableField <EditableField
label="Status Page URL" label="Status Page URL"
value={formData.statusPageUrl || ""} value={formData.statusPageUrl || ""}
onChange={(value) => onChange={(value) => handleFieldChange("statusPageUrl", value)}
handleFieldChange("statusPageUrl", value)
}
/> />
<EditableField <EditableField
@@ -387,7 +375,6 @@ function VendorOverviewPageContent({
</div> </div>
</Card> </Card>
</div> </div>
</div>
{hasChanges && ( {hasChanges && (
<div className="fixed bottom-6 right-6 flex gap-2"> <div className="fixed bottom-6 right-6 flex gap-2">
@@ -402,13 +389,13 @@ function VendorOverviewPageContent({
</Button> </Button>
</div> </div>
)} )}
</> </PageTemplate>
); );
} }
function VendorOverviewPageFallback() { export function VendorOverviewPageSkeleton() {
return ( return (
<div className="p-6 space-y-6"> <PageTemplateSkeleton>
<div className="space-y-1"> <div className="space-y-1">
<div className="h-8 w-48 bg-muted animate-pulse rounded" /> <div className="h-8 w-48 bg-muted animate-pulse rounded" />
<div className="h-4 w-96 bg-muted animate-pulse rounded" /> <div className="h-4 w-96 bg-muted animate-pulse rounded" />
@@ -418,7 +405,7 @@ function VendorOverviewPageFallback() {
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" /> <div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
))} ))}
</div> </div>
</div> </PageTemplateSkeleton>
); );
} }
@@ -433,17 +420,12 @@ export default function VendorOverviewPage() {
}, [loadQuery, vendorId]); }, [loadQuery, vendorId]);
if (!queryRef) { if (!queryRef) {
return <VendorOverviewPageFallback />; return <VendorOverviewPageSkeleton />;
} }
return ( return (
<> <Suspense fallback={<VendorOverviewPageSkeleton />}>
<Helmet>
<title>Vendor Overview - Probo Console</title>
</Helmet>
<Suspense fallback={<VendorOverviewPageFallback />}>
<VendorOverviewPageContent queryRef={queryRef} /> <VendorOverviewPageContent queryRef={queryRef} />
</Suspense> </Suspense>
</>
); );
} }