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;

View File

@@ -650,6 +650,7 @@ input CreatePolicyInput {
content: String!
status: PolicyStatus!
reviewDate: Datetime
ownerId: ID!
}
input UpdatePolicyInput {
@@ -659,6 +660,7 @@ input UpdatePolicyInput {
content: String
status: PolicyStatus
reviewDate: Datetime
ownerId: ID
}
input DeletePolicyInput {
@@ -684,6 +686,7 @@ type Policy implements Node {
status: PolicyStatus!
content: String!
reviewDate: Datetime
owner: People! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}

View File

@@ -47,6 +47,7 @@ type ResolverRoot interface {
Framework() FrameworkResolver
Mutation() MutationResolver
Organization() OrganizationResolver
Policy() PolicyResolver
Query() QueryResolver
Task() TaskResolver
User() UserResolver
@@ -289,6 +290,7 @@ type ComplexityRoot struct {
CreatedAt func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
Owner func(childComplexity int) int
ReviewDate func(childComplexity int) int
Status func(childComplexity int) int
UpdatedAt func(childComplexity int) int
@@ -459,6 +461,9 @@ type OrganizationResolver interface {
Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PeopleConnection, error)
Policies(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PolicyConnection, error)
}
type PolicyResolver interface {
Owner(ctx context.Context, obj *types.Policy) (*types.People, error)
}
type QueryResolver interface {
Node(ctx context.Context, id gid.GID) (types.Node, error)
Viewer(ctx context.Context) (*types.User, error)
@@ -1498,6 +1503,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Policy.Name(childComplexity), true
case "Policy.owner":
if e.complexity.Policy.Owner == nil {
break
}
return e.complexity.Policy.Owner(childComplexity), true
case "Policy.reviewDate":
if e.complexity.Policy.ReviewDate == nil {
break
@@ -2743,6 +2755,7 @@ input CreatePolicyInput {
content: String!
status: PolicyStatus!
reviewDate: Datetime
ownerId: ID!
}
input UpdatePolicyInput {
@@ -2752,6 +2765,7 @@ input UpdatePolicyInput {
content: String
status: PolicyStatus
reviewDate: Datetime
ownerId: ID
}
input DeletePolicyInput {
@@ -2777,6 +2791,7 @@ type Policy implements Node {
status: PolicyStatus!
content: String!
reviewDate: Datetime
owner: People! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -9518,6 +9533,62 @@ func (ec *executionContext) fieldContext_Policy_reviewDate(_ context.Context, fi
return fc, nil
}
func (ec *executionContext) _Policy_owner(ctx context.Context, field graphql.CollectedField, obj *types.Policy) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Policy_owner(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Policy().Owner(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.People)
fc.Result = res
return ec.marshalNPeople2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeople(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Policy_owner(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Policy",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_People_id(ctx, field)
case "fullName":
return ec.fieldContext_People_fullName(ctx, field)
case "primaryEmailAddress":
return ec.fieldContext_People_primaryEmailAddress(ctx, field)
case "additionalEmailAddresses":
return ec.fieldContext_People_additionalEmailAddresses(ctx, field)
case "kind":
return ec.fieldContext_People_kind(ctx, field)
case "createdAt":
return ec.fieldContext_People_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_People_updatedAt(ctx, field)
case "version":
return ec.fieldContext_People_version(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type People", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _Policy_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Policy) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Policy_createdAt(ctx, field)
if err != nil {
@@ -9769,6 +9840,8 @@ func (ec *executionContext) fieldContext_PolicyEdge_node(_ context.Context, fiel
return ec.fieldContext_Policy_content(ctx, field)
case "reviewDate":
return ec.fieldContext_Policy_reviewDate(ctx, field)
case "owner":
return ec.fieldContext_Policy_owner(ctx, field)
case "createdAt":
return ec.fieldContext_Policy_createdAt(ctx, field)
case "updatedAt":
@@ -11193,6 +11266,8 @@ func (ec *executionContext) fieldContext_UpdatePolicyPayload_policy(_ context.Co
return ec.fieldContext_Policy_content(ctx, field)
case "reviewDate":
return ec.fieldContext_Policy_reviewDate(ctx, field)
case "owner":
return ec.fieldContext_Policy_owner(ctx, field)
case "createdAt":
return ec.fieldContext_Policy_createdAt(ctx, field)
case "updatedAt":
@@ -14016,7 +14091,7 @@ func (ec *executionContext) unmarshalInputCreatePolicyInput(ctx context.Context,
asMap[k] = v
}
fieldsInOrder := [...]string{"organizationId", "name", "content", "status", "reviewDate"}
fieldsInOrder := [...]string{"organizationId", "name", "content", "status", "reviewDate", "ownerId"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ -14058,6 +14133,13 @@ func (ec *executionContext) unmarshalInputCreatePolicyInput(ctx context.Context,
return it, err
}
it.ReviewDate = data
case "ownerId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ownerId"))
data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.OwnerID = data
}
}
@@ -14536,7 +14618,7 @@ func (ec *executionContext) unmarshalInputUpdatePolicyInput(ctx context.Context,
asMap[k] = v
}
fieldsInOrder := [...]string{"id", "expectedVersion", "name", "content", "status", "reviewDate"}
fieldsInOrder := [...]string{"id", "expectedVersion", "name", "content", "status", "reviewDate", "ownerId"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ -14585,6 +14667,13 @@ func (ec *executionContext) unmarshalInputUpdatePolicyInput(ctx context.Context,
return it, err
}
it.ReviewDate = data
case "ownerId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ownerId"))
data, err := ec.unmarshalOID2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.OwnerID = data
}
}
@@ -16945,39 +17034,70 @@ func (ec *executionContext) _Policy(ctx context.Context, sel ast.SelectionSet, o
case "id":
out.Values[i] = ec._Policy_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "version":
out.Values[i] = ec._Policy_version(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "name":
out.Values[i] = ec._Policy_name(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "status":
out.Values[i] = ec._Policy_status(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "content":
out.Values[i] = ec._Policy_content(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "reviewDate":
out.Values[i] = ec._Policy_reviewDate(ctx, field, obj)
case "owner":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
res = ec._Policy_owner(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
return res
}
if field.Deferrable != nil {
dfs, ok := deferred[field.Deferrable.Label]
di := 0
if ok {
dfs.AddField(field)
di = len(dfs.Values) - 1
} else {
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
deferred[field.Deferrable.Label] = dfs
}
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
return innerFunc(ctx, dfs)
})
// don't run the out.Concurrently() call below
out.Values[i] = graphql.Null
continue
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "createdAt":
out.Values[i] = ec._Policy_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "updatedAt":
out.Values[i] = ec._Policy_updatedAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
default:
panic("unknown field " + strconv.Quote(field.Name))
@@ -19276,6 +19396,10 @@ func (ec *executionContext) marshalNPageInfo2ᚖgithubᚗcomᚋgetproboᚋprobo
return ec._PageInfo(ctx, sel, v)
}
func (ec *executionContext) marshalNPeople2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeople(ctx context.Context, sel ast.SelectionSet, v types.People) graphql.Marshaler {
return ec._People(ctx, sel, &v)
}
func (ec *executionContext) marshalNPeople2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeople(ctx context.Context, sel ast.SelectionSet, v *types.People) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
@@ -20333,6 +20457,22 @@ var (
}
)
func (ec *executionContext) unmarshalOID2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx context.Context, v any) (*gid.GID, error) {
if v == nil {
return nil, nil
}
res, err := types.UnmarshalGIDScalar(v)
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOID2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx context.Context, sel ast.SelectionSet, v *gid.GID) graphql.Marshaler {
if v == nil {
return graphql.Null
}
res := types.MarshalGIDScalar(*v)
return res
}
func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v any) (*int, error) {
if v == nil {
return nil, nil

View File

@@ -108,6 +108,7 @@ type CreatePolicyInput struct {
Content string `json:"content"`
Status coredata.PolicyStatus `json:"status"`
ReviewDate *time.Time `json:"reviewDate,omitempty"`
OwnerID gid.GID `json:"ownerId"`
}
type CreatePolicyPayload struct {
@@ -322,6 +323,7 @@ type Policy struct {
Status coredata.PolicyStatus `json:"status"`
Content string `json:"content"`
ReviewDate *time.Time `json:"reviewDate,omitempty"`
Owner *People `json:"owner"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
@@ -434,6 +436,7 @@ type UpdatePolicyInput struct {
Content *string `json:"content,omitempty"`
Status *coredata.PolicyStatus `json:"status,omitempty"`
ReviewDate *time.Time `json:"reviewDate,omitempty"`
OwnerID *gid.GID `json:"ownerId,omitempty"`
}
type UpdatePolicyPayload struct {

View File

@@ -383,6 +383,7 @@ func (r *mutationResolver) CreatePolicy(ctx context.Context, input types.CreateP
Content: input.Content,
Status: input.Status,
ReviewDate: input.ReviewDate,
OwnerID: input.OwnerID,
})
if err != nil {
return nil, fmt.Errorf("cannot create policy: %w", err)
@@ -402,6 +403,7 @@ func (r *mutationResolver) UpdatePolicy(ctx context.Context, input types.UpdateP
Content: input.Content,
Status: input.Status,
ReviewDate: input.ReviewDate,
OwnerID: input.OwnerID,
})
if err != nil {
return nil, fmt.Errorf("cannot update policy: %w", err)
@@ -472,6 +474,22 @@ func (r *organizationResolver) Policies(ctx context.Context, obj *types.Organiza
return types.NewPolicyConnection(page), nil
}
// Owner is the resolver for the owner field.
func (r *policyResolver) Owner(ctx context.Context, obj *types.Policy) (*types.People, error) {
policy, err := r.proboSvc.Policies.Get(ctx, obj.ID)
if err != nil {
return nil, fmt.Errorf("cannot get policy: %w", err)
}
// Get the owner
owner, err := r.proboSvc.GetPeople(ctx, policy.OwnerID)
if err != nil {
return nil, fmt.Errorf("cannot get owner: %w", err)
}
return types.NewPeople(owner), nil
}
// Node is the resolver for the node field.
func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) {
switch id.EntityType() {
@@ -613,6 +631,9 @@ func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver
// Organization returns schema.OrganizationResolver implementation.
func (r *Resolver) Organization() schema.OrganizationResolver { return &organizationResolver{r} }
// Policy returns schema.PolicyResolver implementation.
func (r *Resolver) Policy() schema.PolicyResolver { return &policyResolver{r} }
// Query returns schema.QueryResolver implementation.
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
@@ -627,6 +648,7 @@ type evidenceResolver struct{ *Resolver }
type frameworkResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type policyResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type userResolver struct{ *Resolver }

View File

@@ -0,0 +1 @@
ALTER TABLE policies ADD COLUMN owner_id TEXT REFERENCES peoples(id) NOT NULL;

View File

@@ -16,6 +16,7 @@ type (
Policy struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
OwnerID gid.GID `db:"owner_id"`
Status PolicyStatus `db:"status"`
Name string `db:"name"`
Content string `db:"content"`
@@ -33,6 +34,7 @@ type (
Content *string
Status *PolicyStatus
ReviewDate **time.Time
OwnerID *gid.GID
}
)
@@ -50,6 +52,7 @@ func (p *Policy) LoadByID(
SELECT
id,
organization_id,
owner_id,
name,
status,
content,
@@ -96,6 +99,7 @@ func (p *Policies) LoadByOrganizationID(
SELECT
id,
organization_id,
owner_id,
name,
status,
content,
@@ -141,6 +145,7 @@ INSERT INTO
policies (
id,
organization_id,
owner_id,
name,
status,
content,
@@ -152,6 +157,7 @@ INSERT INTO
VALUES (
@policy_id,
@organization_id,
@owner_id,
@name,
@status,
@content,
@@ -165,6 +171,7 @@ VALUES (
args := pgx.StrictNamedArgs{
"policy_id": p.ID,
"organization_id": p.OrganizationID,
"owner_id": p.OwnerID,
"name": p.Name,
"status": p.Status,
"content": p.Content,
@@ -207,6 +214,7 @@ UPDATE policies SET
status = COALESCE(@status, status),
content = COALESCE(@content, content),
review_date = COALESCE(@review_date, review_date),
owner_id = COALESCE(@owner_id, owner_id),
updated_at = @updated_at,
version = version + 1
WHERE %s
@@ -215,6 +223,7 @@ WHERE %s
RETURNING
id,
organization_id,
owner_id,
name,
content,
review_date,
@@ -243,6 +252,9 @@ RETURNING
if params.ReviewDate != nil {
args["review_date"] = *params.ReviewDate
}
if params.OwnerID != nil {
args["owner_id"] = *params.OwnerID
}
maps.Copy(args, scope.SQLArguments())

View File

@@ -22,6 +22,7 @@ type (
Status coredata.PolicyStatus
Content string
ReviewDate *time.Time
OwnerID gid.GID
}
UpdatePolicyRequest struct {
@@ -31,6 +32,7 @@ type (
Content *string
Status *coredata.PolicyStatus
ReviewDate *time.Time
OwnerID *gid.GID
}
)
@@ -68,6 +70,7 @@ func (s *PolicyService) Create(
policy := &coredata.Policy{
ID: policyID,
OrganizationID: req.OrganizationID,
OwnerID: req.OwnerID,
Name: req.Name,
Content: req.Content,
Status: req.Status,
@@ -108,6 +111,7 @@ func (s *PolicyService) Update(
Content: req.Content,
Status: req.Status,
ReviewDate: &req.ReviewDate,
OwnerID: req.OwnerID,
}
policy := &coredata.Policy{ID: req.ID}