From de618dd0072d52f061884fd613e0f68249e0025e Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Thu, 24 Apr 2025 21:09:57 -0700 Subject: [PATCH] Cleanup policies list Signed-off-by: Bryan Frimin --- .../organizations/policies/NewPolicyView.tsx | 6 +- .../organizations/policies/PolicyListView.tsx | 457 ++++++++---------- .../PolicyListViewDeleteMutation.graphql.ts | 132 +++++ .../PolicyListViewQuery.graphql.ts | 12 +- 4 files changed, 331 insertions(+), 276 deletions(-) create mode 100644 apps/console/src/pages/organizations/policies/__generated__/PolicyListViewDeleteMutation.graphql.ts diff --git a/apps/console/src/pages/organizations/policies/NewPolicyView.tsx b/apps/console/src/pages/organizations/policies/NewPolicyView.tsx index ecf569dc9..b7a56bc34 100644 --- a/apps/console/src/pages/organizations/policies/NewPolicyView.tsx +++ b/apps/console/src/pages/organizations/policies/NewPolicyView.tsx @@ -77,7 +77,7 @@ function CreatePolicyForm({ : "empty" ); - const [commitMutation] = + const [createPolicy] = useMutation(CreatePolicyMutation); const handleSubmit = (e: React.FormEvent) => { @@ -109,13 +109,13 @@ function CreatePolicyForm({ ownerId, }; - commitMutation({ + createPolicy({ variables: { input, connections: [ ConnectionHandler.getConnectionID( organizationId!, - "PolicyListPage_policies" + "PolicyListView_policies" ), ], }, diff --git a/apps/console/src/pages/organizations/policies/PolicyListView.tsx b/apps/console/src/pages/organizations/policies/PolicyListView.tsx index 1e7ee1006..5003ddcde 100644 --- a/apps/console/src/pages/organizations/policies/PolicyListView.tsx +++ b/apps/console/src/pages/organizations/policies/PolicyListView.tsx @@ -4,35 +4,28 @@ import { PreloadedQuery, usePreloadedQuery, useQueryLoader, + useMutation, + ConnectionHandler, } from "react-relay"; -import { Card, CardContent, CardFooter } from "@/components/ui/card"; -import { Link, useParams } from "react-router"; +import { Link, useParams, useNavigate } from "react-router"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Plus, - FileText, - Search, - Clock, - Filter, - ArrowUpDown, + ChevronDown, + MoreHorizontal, + Trash2, + Eye, } from "lucide-react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Badge } from "@/components/ui/badge"; -import { format } from "date-fns"; +import { useToast } from "@/hooks/use-toast"; import type { PolicyListViewQuery as PolicyListViewQueryType } from "./__generated__/PolicyListViewQuery.graphql"; +import type { PolicyListViewDeleteMutation } from "./__generated__/PolicyListViewDeleteMutation.graphql"; import { PageTemplate } from "@/components/PageTemplate"; import { PolicyListViewSkeleton } from "./PolicyListPage"; @@ -40,7 +33,7 @@ const PolicyListViewQuery = graphql` query PolicyListViewQuery($organizationId: ID!) { organization: node(id: $organizationId) { ... on Organization { - policies(first: 25) @connection(key: "PolicyListView_policies") { + policies(first: 100) @connection(key: "PolicyListView_policies") { edges { node { id @@ -57,105 +50,139 @@ const PolicyListViewQuery = graphql` } `; -function PolicyCard({ - title, - content, - status, - updatedAt, -}: { - title: string; - content?: string; - status?: string; - updatedAt: string; -}) { - const formattedUpdatedAt = new Date(updatedAt); +const DeletePolicyMutation = graphql` + mutation PolicyListViewDeleteMutation( + $input: DeletePolicyInput! + $connections: [ID!]! + ) { + deletePolicy(input: $input) { + deletedPolicyId @deleteEdge(connections: $connections) + } + } +`; - // Extract a short description from the content and strip HTML tags - const stripHtmlTags = (html: string) => { - // First remove HTML tags - const withoutTags = html.replace(/<[^>]*>/g, ""); - // Then decode HTML entities - const decoded = withoutTags - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/ /g, " "); - // Remove markdown headers - return decoded.replace(/#.*?\n/, "").trim(); +function PolicyTableRow({ + policy, + organizationId, +}: { + policy: { + id: string; + name: string; + content?: string; + status?: string; + updatedAt: string; + }; + organizationId: string; +}) { + const navigate = useNavigate(); + const { toast } = useToast(); + const [isDeleting, setIsDeleting] = useState(false); + const [commitDeleteMutation] = useMutation(DeletePolicyMutation); + + const handleDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + if (window.confirm("Are you sure you want to delete this policy? This action cannot be undone.")) { + setIsDeleting(true); + commitDeleteMutation({ + variables: { + input: { + policyId: policy.id, + }, + connections: [ + ConnectionHandler.getConnectionID( + organizationId, + "PolicyListView_policies" + ), + ], + }, + onCompleted: (_, errors) => { + setIsDeleting(false); + if (errors) { + console.error("Error deleting policy:", errors); + toast({ + title: "Error", + description: "Failed to delete policy. Please try again.", + variant: "destructive", + }); + return; + } + toast({ + title: "Success", + description: "Policy deleted successfully.", + }); + }, + onError: (error) => { + setIsDeleting(false); + console.error("Error deleting policy:", error); + toast({ + title: "Error", + description: "Failed to delete policy. Please try again.", + variant: "destructive", + }); + }, + }); + } }; - const description = content - ? stripHtmlTags(content).substring(0, 120) + - (content.length > 120 ? "..." : "") - : "No description available"; + const handleView = (e: React.MouseEvent) => { + e.stopPropagation(); + navigate(`/organizations/${organizationId}/policies/${policy.id}`); + }; return ( - - -
-
-

{title}

- {status && ( - - {status === "ACTIVE" - ? "Security" - : status === "DRAFT" - ? "Draft" - : status} - - )} -
- -

- {description} -

- -
- - - Last updated: {format(formattedUpdatedAt, "yyyy-MM-dd")} - -
+ { + navigate(`/organizations/${organizationId}/policies/${policy.id}`); + }} + > + +
+ {policy.name} + + Description +
- - -
- - -
-
- + + + + + + + View + + + + {isDeleting ? "Deleting..." : "Delete"} + + + + + ); } @@ -172,177 +199,73 @@ function PolicyListViewContent({ const policies = data.organization.policies?.edges.map((edge) => edge?.node) ?? []; - // State for search, filtering and sorting - const [searchQuery, setSearchQuery] = useState(""); - const [statusFilter, setStatusFilter] = useState("ALL"); - const [sortBy, setSortBy] = useState("name-asc"); - - // Filter and sort policies - const filteredPolicies = policies - .filter((policy) => { - // Filter by search query - const matchesSearch = policy.name - .toLowerCase() - .includes(searchQuery.toLowerCase()); - - // Filter by status - const matchesStatus = - statusFilter === "ALL" || policy.status === statusFilter; - - return matchesSearch && matchesStatus; - }) - .sort((a, b) => { - // Sort policies - switch (sortBy) { - case "name-asc": - return a.name.localeCompare(b.name); - case "name-desc": - return b.name.localeCompare(a.name); - case "updated-desc": - return ( - new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() - ); - case "updated-asc": - return ( - new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() - ); - case "created-desc": - return ( - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); - case "created-asc": - return ( - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() - ); - default: - return 0; - } - }); - return ( - Create Policy + Create policy } > - {/* Search and filter mesures */} -
-
- - setSearchQuery(e.target.value)} - /> -
- -
- - - - - - - - setSortBy("name-asc")}> - Name (A-Z) - - setSortBy("name-desc")}> - Name (Z-A) - - setSortBy("updated-desc")}> - Recently Updated - - setSortBy("updated-asc")}> - Oldest Updated - - setSortBy("created-desc")}> - Recently Created - - setSortBy("created-asc")}> - Oldest Created - - - -
-
- - {/* Results summary */} -
- Showing {filteredPolicies.length} of {policies.length} policies -
- - {/* Policy grid */} -
- {filteredPolicies.length > 0 ? ( -
- {filteredPolicies.map((policy) => ( - - + + + + + + + + + + + {policies.length > 0 ? ( + policies.map((policy) => ( + - - ))} - - ) : ( -
- -

No policies found

-

- {searchQuery || statusFilter !== "ALL" - ? "Try adjusting your search or filters" - : "Create your first policy to get started"} -

- {searchQuery || statusFilter !== "ALL" ? ( - + )) ) : ( - +
+ + )} - - )} + +
+
+ Vendor + +
+
+
+ Last update + +
+
+
+ Status + +
+
+
+

No policies found

+

+ Create your first policy to get started +

+ +
+
); diff --git a/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewDeleteMutation.graphql.ts b/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewDeleteMutation.graphql.ts new file mode 100644 index 000000000..02635ec22 --- /dev/null +++ b/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewDeleteMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeletePolicyInput = { + policyId: string; +}; +export type PolicyListViewDeleteMutation$variables = { + connections: ReadonlyArray; + input: DeletePolicyInput; +}; +export type PolicyListViewDeleteMutation$data = { + readonly deletePolicy: { + readonly deletedPolicyId: string; + }; +}; +export type PolicyListViewDeleteMutation = { + response: PolicyListViewDeleteMutation$data; + variables: PolicyListViewDeleteMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedPolicyId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "PolicyListViewDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeletePolicyPayload", + "kind": "LinkedField", + "name": "deletePolicy", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "PolicyListViewDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeletePolicyPayload", + "kind": "LinkedField", + "name": "deletePolicy", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedPolicyId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "3a182fce4a0616599bea274ddf7b95a1", + "id": null, + "metadata": {}, + "name": "PolicyListViewDeleteMutation", + "operationKind": "mutation", + "text": "mutation PolicyListViewDeleteMutation(\n $input: DeletePolicyInput!\n) {\n deletePolicy(input: $input) {\n deletedPolicyId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "d2a47c3563b6dcbbb88e78b954345768"; + +export default node; diff --git a/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewQuery.graphql.ts b/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewQuery.graphql.ts index f7493f921..5412d443c 100644 --- a/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewQuery.graphql.ts +++ b/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<605e2b8b9bc6fdd85a121a1f1904cc00>> + * @generated SignedSource<<391e3817107bb96f50aad61d20cad0a8>> * @lightSyntaxTransform * @nogrep */ @@ -160,7 +160,7 @@ v5 = [ { "kind": "Literal", "name": "first", - "value": 25 + "value": 100 } ]; return { @@ -228,7 +228,7 @@ return { "name": "policies", "plural": false, "selections": (v4/*: any*/), - "storageKey": "policies(first:25)" + "storageKey": "policies(first:100)" }, { "alias": null, @@ -250,7 +250,7 @@ return { ] }, "params": { - "cacheID": "361b81169a803b656d673b7b8d1c5583", + "cacheID": "6434c8135f68ac64eb2f06983ddc3595", "id": null, "metadata": { "connection": [ @@ -267,11 +267,11 @@ return { }, "name": "PolicyListViewQuery", "operationKind": "query", - "text": "query PolicyListViewQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n policies(first: 25) {\n edges {\n node {\n id\n name\n content\n createdAt\n updatedAt\n status\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n" + "text": "query PolicyListViewQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n policies(first: 100) {\n edges {\n node {\n id\n name\n content\n createdAt\n updatedAt\n status\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "c0ed3d1334a535f752062c6479ed3a7e"; +(node as any).hash = "5567339ad9b2be90b94edc8b1a16fe1e"; export default node;