Improve policy overview style

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-05 10:58:15 +01:00
parent f253a15af8
commit 79c69b681e
4 changed files with 370 additions and 134 deletions

View File

@@ -23,6 +23,7 @@
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
"class-variance-authority": "^0.7.1",
@@ -38,7 +39,7 @@
"react-relay": "^18.2.0",
"react-router": "^7.1.5",
"relay-runtime": "^18.2.0",
"tailwind-merge": "^3.0.1",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {

View File

@@ -0,0 +1,52 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -1,4 +1,4 @@
import { Suspense, useEffect } from "react";
import { Suspense, useEffect, useState } from "react";
import { useParams, Link } from "react-router";
import {
graphql,
@@ -6,10 +6,11 @@ import {
usePreloadedQuery,
useQueryLoader,
} from "react-relay";
import { Edit } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Edit, Download, Shield, User, FileText } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { PolicyOverviewPageQuery as PolicyOverviewPageQueryType } from "./__generated__/PolicyOverviewPageQuery.graphql";
import { Helmet } from "react-helmet-async";
import "../styles/policy-content.css";
@@ -37,110 +38,229 @@ function PolicyOverviewPageContent({
const data = usePreloadedQuery(PolicyOverviewPageQuery, queryRef);
const policy = data.node;
const { organizationId } = useParams();
const [activeTab, setActiveTab] = useState("content");
const getStatusBadge = (status: string | undefined) => {
if (!status) return null;
// Extract a short description from the content
const getDescription = (content: string | undefined) => {
if (!content) return "No description available";
switch (status) {
case "ACTIVE":
return (
<Badge className="bg-green-100 text-green-700 hover:bg-green-200">
Active
</Badge>
);
case "DRAFT":
return (
<Badge className="bg-yellow-100 text-yellow-700 hover:bg-yellow-200">
Draft
</Badge>
);
default:
return (
<Badge className="bg-gray-100 text-gray-700 hover:bg-gray-200">
{status}
</Badge>
);
}
// Remove HTML tags
const withoutTags = content.replace(/<[^>]*>/g, "");
// Decode HTML entities
const decoded = withoutTags
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'")
.replace(/&nbsp;/g, " ");
// Get first paragraph or first 150 characters
const firstParagraph = decoded.split("\n\n")[0].trim();
return firstParagraph.length > 150
? firstParagraph.substring(0, 150) + "..."
: firstParagraph;
};
const formatDate = (dateString: string | undefined) => {
if (!dateString) return "N/A";
const date = new Date(dateString);
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
return date.toISOString().split("T")[0]; // YYYY-MM-DD format
};
return (
<div className="min-h-screen bg-background p-6 space-y-6">
<div className="space-y-4 mb-8">
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-semibold mb-1">{policy.name}</h2>
</div>
<div className="flex gap-2">
<Button variant="outline" asChild>
<Link
to={`/organizations/${organizationId}/policies/${policy.id}/update`}
>
<Edit className="mr-2 h-4 w-4" />
Edit Policy
</Link>
</Button>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-6">
<div>
<Card>
<CardHeader>
<CardTitle>Policy Details</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-1">
Status
</h4>
<div>{getStatusBadge(policy.status)}</div>
<div className="container mx-auto py-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card className="border shadow-sm">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-md bg-slate-100">
<Shield className="h-6 w-6" />
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-1">
Created
</h4>
<p>{formatDate(policy.createdAt)}</p>
<div className="flex gap-2">
<Badge
variant="outline"
className="bg-black text-white hover:bg-black/90 px-3 py-1 rounded-md font-medium"
>
SOC2
</Badge>
<Badge className="px-3 py-1 rounded-md font-medium">
Security
</Badge>
{policy.status && (
<Badge
className={`px-3 py-1 rounded-md font-medium ${
policy.status === "ACTIVE"
? "bg-green-100 text-green-700 hover:bg-green-200"
: policy.status === "DRAFT"
? "bg-yellow-100 text-yellow-700 hover:bg-yellow-200"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{policy.status === "ACTIVE"
? "Active"
: policy.status === "DRAFT"
? "Draft"
: policy.status}
</Badge>
)}
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-1">
Last Updated
</h4>
<p>{formatDate(policy.updatedAt)}</p>
<div className="ml-auto">
<Button
variant="outline"
size="icon"
className="rounded-full"
>
<Download className="h-5 w-5" />
</Button>
</div>
</div>
<h1 className="text-3xl font-bold mb-3">{policy.name}</h1>
<p className="text-muted-foreground mb-6">
{getDescription(policy.content)}
</p>
<Tabs
defaultValue="content"
value={activeTab}
onValueChange={setActiveTab}
className="mb-6"
>
<TabsList className="border-b w-full rounded-none bg-transparent p-0 h-auto">
<TabsTrigger
value="content"
className={`rounded-none border-b-2 border-transparent px-4 py-2 font-medium ${
activeTab === "content"
? "border-primary text-primary"
: "text-muted-foreground"
}`}
>
Policy Content
</TabsTrigger>
<TabsTrigger
value="history"
className={`rounded-none border-b-2 border-transparent px-4 py-2 font-medium ${
activeTab === "history"
? "border-primary text-primary"
: "text-muted-foreground"
}`}
>
Version History
</TabsTrigger>
<TabsTrigger
value="approvals"
className={`rounded-none border-b-2 border-transparent px-4 py-2 font-medium ${
activeTab === "approvals"
? "border-primary text-primary"
: "text-muted-foreground"
}`}
>
Approvals
</TabsTrigger>
</TabsList>
<TabsContent value="content" className="pt-6">
<div className="prose prose-sm md:prose-base lg:prose-lg max-w-none">
<div
className="policy-content"
dangerouslySetInnerHTML={{ __html: policy.content || "" }}
/>
</div>
</TabsContent>
<TabsContent value="history" className="pt-6">
<div className="text-center py-12">
<p className="text-muted-foreground">
Version history will be available soon.
</p>
</div>
</TabsContent>
<TabsContent value="approvals" className="pt-6">
<div className="text-center py-12">
<p className="text-muted-foreground">
Approval workflow will be available soon.
</p>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
<div>
<Card>
<CardHeader>
<CardTitle>Policy Content</CardTitle>
</CardHeader>
<CardContent>
<div className="prose prose-sm md:prose-base lg:prose-lg max-w-none">
<div
className="policy-content"
dangerouslySetInnerHTML={{ __html: policy.content || "" }}
/>
<div className="lg:col-span-1">
<Card className="border shadow-sm mb-6">
<CardContent className="p-6">
<h2 className="text-xl font-semibold mb-6">Policy Details</h2>
<div className="space-y-6">
<div>
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<FileText className="h-4 w-4" />
<span className="text-sm">Last Updated</span>
</div>
<p className="font-medium">{formatDate(policy.updatedAt)}</p>
</div>
<div>
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<FileText className="h-4 w-4" />
<span className="text-sm">Review Due</span>
</div>
<p className="font-medium">2025-02-15</p>
</div>
<div>
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<User className="h-4 w-4" />
<span className="text-sm">Owner</span>
</div>
<p className="font-medium">Jane Smith, CISO</p>
</div>
</div>
</CardContent>
</Card>
<Button asChild className="w-full">
<Link
to={`/organizations/${organizationId}/policies/${policy.id}/update`}
>
<Edit className="mr-2 h-4 w-4" />
Edit Policy
</Link>
</Button>
<Card className="border shadow-sm mt-6 bg-red-50">
<CardContent className="p-6">
<h3 className="text-red-500 font-semibold mb-3">Danger Zone</h3>
<p className="text-sm text-muted-foreground mb-4">
Permanently delete this policy and all of its data. This action
cannot be undone.
</p>
<Button variant="destructive" className="w-full">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2"
>
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
</svg>
Delete Policy
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
@@ -149,59 +269,91 @@ function PolicyOverviewPageContent({
function PolicyOverviewPageFallback() {
return (
<div className="min-h-screen bg-background p-6 space-y-6">
<div className="space-y-4 mb-8">
<div className="flex justify-between items-center">
<div>
<div className="h-8 w-48 bg-muted animate-pulse rounded mb-2" />
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
</div>
<div>
<div className="h-10 w-32 bg-muted animate-pulse rounded" />
</div>
</div>
</div>
<div className="container mx-auto py-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card className="border shadow-sm">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-md bg-slate-100/50 h-10 w-10 animate-pulse" />
<div className="flex gap-2">
<div className="h-6 w-16 bg-muted animate-pulse rounded-md" />
<div className="h-6 w-20 bg-muted animate-pulse rounded-md" />
<div className="h-6 w-16 bg-muted animate-pulse rounded-md" />
</div>
<div className="ml-auto">
<div className="h-9 w-9 bg-muted animate-pulse rounded-full" />
</div>
</div>
<div className="h-8 w-3/4 bg-muted animate-pulse rounded mb-3" />
<div className="space-y-2 mb-6">
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<div className="h-4 w-2/3 bg-muted animate-pulse rounded" />
</div>
<div className="flex border-b mb-6">
<div className="px-4 py-2 h-8 w-28 bg-muted animate-pulse rounded" />
<div className="px-4 py-2 h-8 w-28 bg-muted animate-pulse rounded opacity-50" />
<div className="px-4 py-2 h-8 w-28 bg-muted animate-pulse rounded opacity-50" />
</div>
<div className="grid grid-cols-1 gap-6">
<div>
<Card>
<CardHeader>
<div className="h-6 w-32 bg-muted animate-pulse rounded" />
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<div className="h-4 w-16 bg-muted animate-pulse rounded mb-2" />
<div className="h-6 w-24 bg-muted animate-pulse rounded" />
</div>
<div>
<div className="h-4 w-16 bg-muted animate-pulse rounded mb-2" />
<div className="h-6 w-48 bg-muted animate-pulse rounded" />
</div>
<div>
<div className="h-4 w-24 bg-muted animate-pulse rounded mb-2" />
<div className="h-6 w-48 bg-muted animate-pulse rounded" />
</div>
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<div className="h-4 w-3/4 bg-muted animate-pulse rounded" />
</div>
</CardContent>
</Card>
</div>
<div>
<Card>
<CardHeader>
<div className="h-6 w-32 bg-muted animate-pulse rounded" />
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<div className="h-4 w-3/4 bg-muted animate-pulse rounded" />
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<div className="h-4 w-5/6 bg-muted animate-pulse rounded" />
<div className="lg:col-span-1">
<Card className="border shadow-sm mb-6">
<CardContent className="p-6">
<div className="h-6 w-32 bg-muted animate-pulse rounded mb-6" />
<div className="space-y-6">
<div>
<div className="h-4 w-28 bg-muted animate-pulse rounded mb-1" />
<div className="h-5 w-24 bg-muted animate-pulse rounded" />
</div>
<div>
<div className="h-4 w-28 bg-muted animate-pulse rounded mb-1" />
<div className="h-5 w-24 bg-muted animate-pulse rounded" />
</div>
<div>
<div className="h-4 w-28 bg-muted animate-pulse rounded mb-1" />
<div className="h-5 w-36 bg-muted animate-pulse rounded" />
</div>
<hr className="my-4" />
<div>
<div className="h-5 w-32 bg-muted animate-pulse rounded mb-3" />
<div className="space-y-2">
<div className="h-4 w-48 bg-muted animate-pulse rounded" />
<div className="h-4 w-48 bg-muted animate-pulse rounded" />
</div>
</div>
</div>
</CardContent>
</Card>
<div className="h-10 w-full bg-muted animate-pulse rounded mb-6" />
<Card className="border shadow-sm bg-red-50/30">
<CardContent className="p-6">
<div className="h-5 w-28 bg-muted animate-pulse rounded mb-3" />
<div className="h-16 w-full bg-muted animate-pulse rounded mb-4" />
<div className="h-10 w-full bg-muted animate-pulse rounded" />
</CardContent>
</Card>
</div>
</div>
</div>

39
package-lock.json generated
View File

@@ -30,6 +30,7 @@
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
"class-variance-authority": "^0.7.1",
@@ -45,7 +46,7 @@
"react-relay": "^18.2.0",
"react-router": "^7.1.5",
"relay-runtime": "^18.2.0",
"tailwind-merge": "^3.0.1",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
@@ -3485,6 +3486,36 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
"integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-roving-focus": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
@@ -8478,9 +8509,9 @@
}
},
"node_modules/tailwind-merge": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.1.tgz",
"integrity": "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
"license": "MIT",
"funding": {
"type": "github",