Add policy owner

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-05 11:25:08 +01:00
parent de4e91cab2
commit b214b0930c
18 changed files with 1232 additions and 317 deletions

View File

@@ -0,0 +1,73 @@
import { useState, useEffect } from "react";
import { graphql, useFragment } from "react-relay";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PeopleSelector_organization$key } from "./__generated__/PeopleSelector_organization.graphql";
const peopleSelectorFragment = graphql`
fragment PeopleSelector_organization on Organization {
id
peoples(first: 100) {
edges {
node {
id
fullName
primaryEmailAddress
}
}
}
}
`;
interface PeopleSelectorProps {
organizationRef: PeopleSelector_organization$key;
selectedPersonId: string | null;
onSelect: (personId: string) => void;
placeholder?: string;
required?: boolean;
}
export default function PeopleSelector({
organizationRef,
selectedPersonId,
onSelect,
placeholder = "Select a person",
required = false,
}: PeopleSelectorProps) {
const organization = useFragment(peopleSelectorFragment, organizationRef);
const [value, setValue] = useState<string>(selectedPersonId || "");
useEffect(() => {
if (selectedPersonId) {
setValue(selectedPersonId);
}
}, [selectedPersonId]);
const handleValueChange = (newValue: string) => {
setValue(newValue);
onSelect(newValue);
};
return (
<Select value={value} onValueChange={handleValueChange} required={required}>
<SelectTrigger>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{organization.peoples?.edges?.map(
(edge) =>
edge?.node && (
<SelectItem key={edge.node.id} value={edge.node.id}>
{edge.node.fullName} ({edge.node.primaryEmailAddress})
</SelectItem>
)
)}
</SelectContent>
</Select>
);
}

View File

@@ -0,0 +1,108 @@
/**
* @generated SignedSource<<40624c6362024ecac90d44c5dd174a1c>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PeopleSelector_organization$data = {
readonly id: string;
readonly peoples: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly fullName: string;
readonly id: string;
readonly primaryEmailAddress: string;
};
}>;
};
readonly " $fragmentType": "PeopleSelector_organization";
};
export type PeopleSelector_organization$key = {
readonly " $data"?: PeopleSelector_organization$data;
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "PeopleSelector_organization",
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PeopleEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "peoples(first:100)"
}
],
"type": "Organization",
"abstractKey": null
};
})();
(node as any).hash = "33e28b2889fa1180f6bf491e428fbafd";
export default node;

View File

@@ -1,16 +1,34 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router";
import { ConnectionHandler, graphql, useMutation } from "react-relay";
import {
ConnectionHandler,
graphql,
useMutation,
useQueryLoader,
usePreloadedQuery,
PreloadedQuery,
} from "react-relay";
import { Helmet } from "react-helmet-async";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import { FileText, Calendar } from "lucide-react";
import { FileText, Calendar, User } from "lucide-react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import PolicyEditor from "@/components/PolicyEditor";
import PeopleSelector from "@/components/PeopleSelector";
import { Suspense } from "react";
import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql";
import type { CreatePolicyPageQuery as CreatePolicyPageQueryType } from "./__generated__/CreatePolicyPageQuery.graphql";
const CreatePolicyQuery = graphql`
query CreatePolicyPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
...PeopleSelector_organization
}
}
`;
const CreatePolicyMutation = graphql`
mutation CreatePolicyPageMutation(
@@ -25,19 +43,32 @@ const CreatePolicyMutation = graphql`
content
status
reviewDate
owner {
id
fullName
}
}
}
}
}
`;
export default function CreatePolicyPage() {
function CreatePolicyForm({
queryRef,
}: {
queryRef: PreloadedQuery<CreatePolicyPageQueryType>;
}) {
const navigate = useNavigate();
const { organizationId } = useParams();
const data = usePreloadedQuery<CreatePolicyPageQueryType>(
CreatePolicyQuery,
queryRef
);
const [name, setName] = useState("");
const [content, setContent] = useState("");
const [status, setStatus] = useState<"DRAFT" | "ACTIVE">("DRAFT");
const [reviewDate, setReviewDate] = useState("");
const [ownerId, setOwnerId] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast();
@@ -53,6 +84,16 @@ export default function CreatePolicyPage() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!ownerId) {
toast({
title: "Error",
description: "Please select an owner for the policy.",
variant: "destructive",
});
return;
}
setIsSubmitting(true);
// Convert reviewDate string to ISO format for the API
@@ -67,6 +108,7 @@ export default function CreatePolicyPage() {
content,
status,
reviewDate: reviewDateValue,
ownerId,
};
commitMutation({
@@ -184,6 +226,20 @@ export default function CreatePolicyPage() {
</RadioGroup>
</div>
<div className="space-y-2">
<Label htmlFor="owner" className="flex items-center gap-2">
<User className="h-4 w-4" />
Policy Owner
</Label>
<PeopleSelector
organizationRef={data.organization}
selectedPersonId={ownerId}
onSelect={setOwnerId}
placeholder="Select policy owner"
required
/>
</div>
<div className="space-y-2">
<Label
htmlFor="reviewDate"
@@ -222,3 +278,38 @@ export default function CreatePolicyPage() {
</>
);
}
function CreatePolicyPageFallback() {
return (
<div className="container mx-auto py-6">
<div className="flex items-center mb-6">
<div className="mr-4">
<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>
);
}
export default function CreatePolicyPage() {
const { organizationId } = useParams();
const [queryRef, loadQuery] =
useQueryLoader<CreatePolicyPageQueryType>(CreatePolicyQuery);
useEffect(() => {
if (organizationId) {
loadQuery({ organizationId });
}
}, [organizationId, loadQuery]);
return (
<Suspense fallback={<CreatePolicyPageFallback />}>
{queryRef && <CreatePolicyForm queryRef={queryRef} />}
</Suspense>
);
}

View File

@@ -6,7 +6,7 @@ import {
usePreloadedQuery,
useQueryLoader,
} from "react-relay";
import { Edit, Download, Shield, User, FileText } from "lucide-react";
import { Edit, Download, Shield, User, FileText, Calendar } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -26,6 +26,11 @@ const PolicyOverviewPageQuery = graphql`
updatedAt
reviewDate
status
owner {
id
fullName
primaryEmailAddress
}
}
}
}
@@ -70,205 +75,268 @@ function PolicyOverviewPageContent({
};
return (
<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 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 && (
<>
<Helmet>
<title>{policy.name} - Probo Console</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="flex justify-between items-start mb-6">
<div className="flex items-center">
<div className="mr-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<FileText className="h-6 w-6 text-primary" />
</div>
</div>
<div>
<h1 className="text-2xl font-bold">{policy.name}</h1>
<div className="flex items-center gap-2 text-muted-foreground">
<Badge
variant={policy.status === "ACTIVE" ? "default" : "outline"}
>
{policy.status === "ACTIVE" ? "Active" : "Draft"}
</Badge>
{policy.owner && (
<div className="flex items-center gap-1">
<User className="h-3 w-3" />
<span>{policy.owner.fullName}</span>
</div>
)}
{policy.reviewDate && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
<span>Review: {formatDate(policy.reviewDate)}</span>
</div>
)}
</div>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" asChild>
<Link
to={`/organizations/${organizationId}/policies/${policy.id}/edit`}
>
<Edit className="h-4 w-4 mr-2" />
Edit
</Link>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
// Logic to download policy content as PDF or text
const element = document.createElement("a");
const file = new Blob([policy.content || ""], {
type: "text/plain",
});
element.href = URL.createObjectURL(file);
element.download = `${policy.name}.txt`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}}
>
<Download className="h-4 w-4 mr-2" />
Download
</Button>
</div>
</div>
<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 className="flex gap-2">
<Badge
className={`px-3 py-1 rounded-md font-medium ${
policy.status === "ACTIVE"
? "bg-green-100 text-green-700 hover:bg-green-200"
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"
? "bg-yellow-100 text-yellow-700 hover:bg-yellow-200"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
? "Draft"
: policy.status}
</Badge>
)}
</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.status === "ACTIVE"
? "Active"
: policy.status === "DRAFT"
? "Draft"
: policy.status}
</Badge>
)}
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 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">
{policy.reviewDate
? formatDate(policy.reviewDate)
: "Not set"}
</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">
{policy.owner ? policy.owner.fullName : "Not assigned"}
</p>
</div>
</div>
<div className="ml-auto">
<Button
variant="outline"
size="icon"
className="rounded-full"
>
<Download className="h-5 w-5" />
</Button>
</div>
</div>
</CardContent>
</Card>
<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"
<Button asChild className="w-full">
<Link
to={`/organizations/${organizationId}/policies/${policy.id}/update`}
>
<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"
}`}
<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"
>
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 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">
{policy.reviewDate
? formatDate(policy.reviewDate)
: "Not set"}
</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>
<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>
</div>
</>
);
}
@@ -377,13 +445,8 @@ export default function PolicyOverviewPage() {
}, [loadQuery, policyId]);
return (
<>
<Helmet>
<title>Policy Details - Probo Console</title>
</Helmet>
<Suspense fallback={<PolicyOverviewPageFallback />}>
{queryRef && <PolicyOverviewPageContent queryRef={queryRef} />}
</Suspense>
</>
<Suspense fallback={<PolicyOverviewPageFallback />}>
{queryRef && <PolicyOverviewPageContent queryRef={queryRef} />}
</Suspense>
);
}

View File

@@ -13,16 +13,17 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "@/hooks/use-toast";
import { FileText, Calendar } from "lucide-react";
import { FileText, Calendar, User } from "lucide-react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Suspense } from "react";
import PolicyEditor from "@/components/PolicyEditor";
import PeopleSelector from "@/components/PeopleSelector";
import type { UpdatePolicyPageQuery as UpdatePolicyPageQueryType } from "./__generated__/UpdatePolicyPageQuery.graphql";
import type { UpdatePolicyPageMutation as UpdatePolicyPageMutationType } from "./__generated__/UpdatePolicyPageMutation.graphql";
const UpdatePolicyPageQuery = graphql`
query UpdatePolicyPageQuery($policyId: ID!) {
node(id: $policyId) {
query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) {
policy: node(id: $policyId) {
id
... on Policy {
name
@@ -30,8 +31,15 @@ const UpdatePolicyPageQuery = graphql`
status
version
reviewDate
owner {
id
fullName
}
}
}
organization: node(id: $organizationId) {
...PeopleSelector_organization
}
}
`;
@@ -45,6 +53,10 @@ const UpdatePolicyMutation = graphql`
status
version
reviewDate
owner {
id
fullName
}
}
}
}
@@ -62,12 +74,15 @@ function UpdatePolicyPageContent({
queryRef
);
console.log("UpdatePolicyPage data:", data.node);
console.log("UpdatePolicyPage data:", data.policy);
const [name, setName] = useState(data.node.name);
const [content, setContent] = useState(data.node.content || "");
const [status, setStatus] = useState(data.node.status);
const [reviewDate, setReviewDate] = useState(data.node.reviewDate || "");
const [name, setName] = useState(data.policy.name);
const [content, setContent] = useState(data.policy.content || "");
const [status, setStatus] = useState(data.policy.status);
const [reviewDate, setReviewDate] = useState(data.policy.reviewDate || "");
const [ownerId, setOwnerId] = useState<string | null>(
data.policy.owner?.id || null
);
const [isSubmitting, setIsSubmitting] = useState(false);
console.log(
@@ -93,12 +108,13 @@ function UpdatePolicyPageContent({
commitMutation({
variables: {
input: {
id: data.node.id,
id: data.policy.id,
name,
content,
status,
reviewDate: reviewDateValue,
expectedVersion: data.node.version!,
ownerId,
expectedVersion: data.policy.version!,
},
},
onCompleted: (response, errors) => {
@@ -200,6 +216,20 @@ function UpdatePolicyPageContent({
</RadioGroup>
</div>
<div className="space-y-2">
<Label htmlFor="owner" className="flex items-center gap-2">
<User className="h-4 w-4" />
Policy Owner
</Label>
<PeopleSelector
organizationRef={data.organization}
selectedPersonId={ownerId}
onSelect={setOwnerId}
placeholder="Select policy owner"
required
/>
</div>
<div className="space-y-2">
<Label
htmlFor="reviewDate"
@@ -253,48 +283,26 @@ function UpdatePolicyPageFallback() {
<div className="h-4 w-64 bg-muted animate-pulse rounded" />
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader>
<div className="h-6 w-32 bg-muted animate-pulse rounded" />
</CardHeader>
<CardContent className="space-y-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="space-y-2">
<div className="h-4 w-24 bg-muted animate-pulse rounded" />
<div className="h-10 w-full bg-muted animate-pulse rounded" />
</div>
))}
</CardContent>
</Card>
</div>
<div className="bg-muted animate-pulse rounded-lg h-[600px]" />
</div>
);
}
export default function UpdatePolicyPage() {
const { organizationId, policyId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<UpdatePolicyPageQueryType>(
UpdatePolicyPageQuery
);
const { policyId } = useParams();
useEffect(() => {
loadQuery({ policyId: policyId! });
}, [loadQuery, policyId]);
if (!queryRef) {
return <UpdatePolicyPageFallback />;
}
if (organizationId && policyId) {
loadQuery({ organizationId, policyId });
}
}, [organizationId, policyId, loadQuery]);
return (
<>
<Helmet>
<title>Update Policy - Probo Console</title>
</Helmet>
<Suspense fallback={<UpdatePolicyPageFallback />}>
<UpdatePolicyPageContent queryRef={queryRef} />
</Suspense>
</>
<Suspense fallback={<UpdatePolicyPageFallback />}>
{queryRef && <UpdatePolicyPageContent queryRef={queryRef} />}
</Suspense>
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<d242fdb748915009dc461be5596b6fc1>>
* @generated SignedSource<<29caed5a50be912ff48f9c131ce4045b>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -14,6 +14,7 @@ export type CreatePolicyInput = {
content: string;
name: string;
organizationId: string;
ownerId: string;
reviewDate?: string | null | undefined;
status: PolicyStatus;
};
@@ -28,6 +29,10 @@ export type CreatePolicyPageMutation$data = {
readonly content: string;
readonly id: string;
readonly name: string;
readonly owner: {
readonly fullName: string;
readonly id: string;
};
readonly reviewDate: string | null | undefined;
readonly status: PolicyStatus;
};
@@ -58,6 +63,13 @@ v2 = [
}
],
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"concreteType": "PolicyEdge",
@@ -73,13 +85,7 @@ v3 = {
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
(v3/*: any*/),
{
"alias": null,
"args": null,
@@ -107,6 +113,25 @@ v3 = {
"kind": "ScalarField",
"name": "reviewDate",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
@@ -132,7 +157,7 @@ return {
"name": "createPolicy",
"plural": false,
"selections": [
(v3/*: any*/)
(v4/*: any*/)
],
"storageKey": null
}
@@ -157,7 +182,7 @@ return {
"name": "createPolicy",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
{
"alias": null,
"args": null,
@@ -180,16 +205,16 @@ return {
]
},
"params": {
"cacheID": "64796b84575bf926f45da4c98d31e645",
"cacheID": "cf17928879d0f2b2e68367ed2c132d0a",
"id": null,
"metadata": {},
"name": "CreatePolicyPageMutation",
"operationKind": "mutation",
"text": "mutation CreatePolicyPageMutation(\n $input: CreatePolicyInput!\n) {\n createPolicy(input: $input) {\n policyEdge {\n node {\n id\n name\n content\n status\n reviewDate\n }\n }\n }\n}\n"
"text": "mutation CreatePolicyPageMutation(\n $input: CreatePolicyInput!\n) {\n createPolicy(input: $input) {\n policyEdge {\n node {\n id\n name\n content\n status\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "b7ecb61ced1e31c0c0093946135eca3b";
(node as any).hash = "1ddb9b7010ecf1fe55e2b56aaa034295";
export default node;

View File

@@ -0,0 +1,176 @@
/**
* @generated SignedSource<<0229a3e8e814cf3971479aa5abc20322>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type CreatePolicyPageQuery$variables = {
organizationId: string;
};
export type CreatePolicyPageQuery$data = {
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
};
};
export type CreatePolicyPageQuery = {
response: CreatePolicyPageQuery$data;
variables: CreatePolicyPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "CreatePolicyPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "PeopleSelector_organization"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "CreatePolicyPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PeopleEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "peoples(first:100)"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "edb18dbe0439fa7ca7bc791e607cd447",
"id": null,
"metadata": {},
"name": "CreatePolicyPageQuery",
"operationKind": "query",
"text": "query CreatePolicyPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "9ab69c4bfcba5c896760f297ab38ff51";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<ebffaaa37d8815b22f9fa75aca6e75d2>>
* @generated SignedSource<<a49c4b1ca92e109b1091a1431fe0c871>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -19,11 +19,11 @@ export type PolicyListPageQuery$data = {
readonly edges: ReadonlyArray<{
readonly node: {
readonly content: string;
readonly createdAt: any;
readonly createdAt: string;
readonly id: string;
readonly name: string;
readonly status: PolicyStatus;
readonly updatedAt: any;
readonly updatedAt: string;
};
}>;
};

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<0eb945d3cbf8f223621fe7aa2b93eddc>>
* @generated SignedSource<<67ee63bc3751efdd9ecdf2ad9ab5e197>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -19,6 +19,11 @@ export type PolicyOverviewPageQuery$data = {
readonly createdAt?: string;
readonly id: string;
readonly name?: string;
readonly owner?: {
readonly fullName: string;
readonly id: string;
readonly primaryEmailAddress: string;
};
readonly reviewDate?: string | null | undefined;
readonly status?: PolicyStatus;
readonly updatedAt?: string;
@@ -95,6 +100,32 @@ v3 = {
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Policy",
@@ -153,16 +184,16 @@ return {
]
},
"params": {
"cacheID": "d3b31142132a379f9c3a8812ddcfb7da",
"cacheID": "c57fd1a2ff5ffa776bebb9f15bd534bc",
"id": null,
"metadata": {},
"name": "PolicyOverviewPageQuery",
"operationKind": "query",
"text": "query PolicyOverviewPageQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n createdAt\n updatedAt\n reviewDate\n status\n }\n }\n}\n"
"text": "query PolicyOverviewPageQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n createdAt\n updatedAt\n reviewDate\n status\n owner {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "1519b589fe7da5a258621ad445970ca7";
(node as any).hash = "e6c4f286358df61d18ea6deb367d7182";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9a54ece29b9ff9a2efa624f67f40f803>>
* @generated SignedSource<<f37225ac5a4b6774dadbd4f23c3bbfe9>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,6 +15,7 @@ export type UpdatePolicyInput = {
expectedVersion: number;
id: string;
name?: string | null | undefined;
ownerId?: string | null | undefined;
reviewDate?: string | null | undefined;
status?: PolicyStatus | null | undefined;
};
@@ -27,6 +28,10 @@ export type UpdatePolicyPageMutation$data = {
readonly content: string;
readonly id: string;
readonly name: string;
readonly owner: {
readonly fullName: string;
readonly id: string;
};
readonly reviewDate: string | null | undefined;
readonly status: PolicyStatus;
readonly version: number;
@@ -46,7 +51,14 @@ var v0 = [
"name": "input"
}
],
v1 = [
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v2 = [
{
"alias": null,
"args": [
@@ -69,13 +81,7 @@ v1 = [
"name": "policy",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
(v1/*: any*/),
{
"alias": null,
"args": null,
@@ -110,6 +116,25 @@ v1 = [
"kind": "ScalarField",
"name": "reviewDate",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
@@ -124,7 +149,7 @@ return {
"kind": "Fragment",
"metadata": null,
"name": "UpdatePolicyPageMutation",
"selections": (v1/*: any*/),
"selections": (v2/*: any*/),
"type": "Mutation",
"abstractKey": null
},
@@ -133,19 +158,19 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "UpdatePolicyPageMutation",
"selections": (v1/*: any*/)
"selections": (v2/*: any*/)
},
"params": {
"cacheID": "ce2ccf9449b52e3df3b2b9bb82c55166",
"cacheID": "cab3ba1fb88944fade60b5d6a0e0b7b9",
"id": null,
"metadata": {},
"name": "UpdatePolicyPageMutation",
"operationKind": "mutation",
"text": "mutation UpdatePolicyPageMutation(\n $input: UpdatePolicyInput!\n) {\n updatePolicy(input: $input) {\n policy {\n id\n name\n content\n status\n version\n reviewDate\n }\n }\n}\n"
"text": "mutation UpdatePolicyPageMutation(\n $input: UpdatePolicyInput!\n) {\n updatePolicy(input: $input) {\n policy {\n id\n name\n content\n status\n version\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "d5329966d0e13b8931d6f01d9afb74e2";
(node as any).hash = "51e4a0e297540f4e83dcb6d809430ac7";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5ee387b238e8fdc7c41851c7e6740c95>>
* @generated SignedSource<<04e2a37d31d4cd204101efdd85471dc5>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,15 +9,24 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type UpdatePolicyPageQuery$variables = {
organizationId: string;
policyId: string;
};
export type UpdatePolicyPageQuery$data = {
readonly node: {
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
};
readonly policy: {
readonly content?: string;
readonly id: string;
readonly name?: string;
readonly owner?: {
readonly fullName: string;
readonly id: string;
};
readonly reviewDate?: string | null | undefined;
readonly status?: PolicyStatus;
readonly version?: number;
@@ -29,28 +38,38 @@ export type UpdatePolicyPageQuery = {
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "policyId"
}
],
v1 = [
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "policyId"
},
v2 = [
{
"kind": "Variable",
"name": "id",
"variableName": "policyId"
}
],
v2 = {
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
v5 = {
"kind": "InlineFragment",
"selections": [
{
@@ -87,28 +106,74 @@ v3 = {
"kind": "ScalarField",
"name": "reviewDate",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/)
],
"storageKey": null
}
],
"type": "Policy",
"abstractKey": null
},
v6 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "UpdatePolicyPageQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"alias": "policy",
"args": (v2/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/)
(v3/*: any*/),
(v5/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "PeopleSelector_organization"
}
],
"storageKey": null
}
@@ -118,43 +183,108 @@ return {
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "UpdatePolicyPageQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"alias": "policy",
"args": (v2/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v7/*: any*/),
(v3/*: any*/),
(v5/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v7/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
(v3/*: any*/)
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PeopleEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "peoples(first:100)"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "fc062f2d2fcce04954feb69436433093",
"cacheID": "abbd1265e5fbacc34d5442772cbd645b",
"id": null,
"metadata": {},
"name": "UpdatePolicyPageQuery",
"operationKind": "query",
"text": "query UpdatePolicyPageQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n status\n version\n reviewDate\n }\n }\n}\n"
"text": "query UpdatePolicyPageQuery(\n $policyId: ID!\n $organizationId: ID!\n) {\n policy: node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n status\n version\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "4ae49cad4f03a09d87475483c271bd94";
(node as any).hash = "abba76ee5208701e443130d650b5c8d1";
export default node;