From 7644dfa047f40d846e1ebb26254253c58674c514 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 14 Nov 2024 16:50:36 +0100 Subject: [PATCH 01/13] wip: new permissions picker prototype --- .../new-permissions-picker.stories.tsx | 690 ++++++++++++++++++ 1 file changed, 690 insertions(+) create mode 100644 packages/web/app/src/stories/new-permissions-picker.stories.tsx diff --git a/packages/web/app/src/stories/new-permissions-picker.stories.tsx b/packages/web/app/src/stories/new-permissions-picker.stories.tsx new file mode 100644 index 0000000000..ebfc1fa55a --- /dev/null +++ b/packages/web/app/src/stories/new-permissions-picker.stories.tsx @@ -0,0 +1,690 @@ +import { ReactElement, useState } from 'react'; +import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; +import { zodResolver } from '@hookform/resolvers/zod'; +import type { Meta, StoryObj } from '@storybook/react'; + +const enum ResourceLevel { + organization = 'organization', + project = 'project', + target = 'target', + service = 'service', +} + +type PermissionGroup = { + title: string; + permissions: Array; +}; + +type PermissionRecord = { + id: string; + title: string; + description: string; + dependsOn?: string; + readOnly?: true; + level: ResourceLevel | Array; +}; + +const permissionGroups: Array = [ + { + title: 'Organization', + permissions: [ + { + id: 'organization:describe', + title: 'View', + description: 'Member can see the organization. Permission can not be modified.', + readOnly: true, + level: ResourceLevel.organization, + }, + { + id: 'organization:support', + title: 'Support', + description: 'Member can access, create and reply to support tickets.', + level: ResourceLevel.organization, + }, + { + id: 'organization:updateSlug', + title: 'Update Slug', + description: 'Member can modify the organization slug.', + level: ResourceLevel.organization, + }, + { + id: 'organization:delete', + title: 'Delete', + description: 'Member can delete the Organization.', + level: ResourceLevel.organization, + }, + ], + }, + { + title: 'Members', + permissions: [ + { + id: 'members:describe', + title: 'View', + description: 'Member can view the organization members.', + level: ResourceLevel.organization, + }, + { + id: 'members:assignRole', + title: 'Assign Role', + description: 'Member can assign roles to users.', + dependsOn: 'members:describe', + level: ResourceLevel.organization, + }, + { + id: 'members:modifyRole', + title: 'Modify Role', + description: 'Member can modify, create and delete roles.', + dependsOn: 'members:describe', + level: ResourceLevel.organization, + }, + { + id: 'members:remove', + title: 'Remove Member', + description: 'Member can remove users from the Organization.', + dependsOn: 'members:describe', + level: ResourceLevel.organization, + }, + { + id: 'members:manageInvites', + title: 'Manage Invites', + description: 'Member can invite users via email and modify or delete pending invites.', + dependsOn: 'members:describe', + level: ResourceLevel.organization, + }, + ], + }, + { + title: 'Billing', + permissions: [ + { + id: 'billing:describe', + title: 'Describe', + description: 'Member can see the billing information.', + level: ResourceLevel.organization, + }, + { + id: 'billing:update', + title: 'Manage project level schema linting', + description: 'Member can see the billing information.', + dependsOn: 'billing:describe', + level: ResourceLevel.organization, + }, + ], + }, + { + title: 'Open ID Connect', + permissions: [ + { + id: 'oidc:manage', + title: 'Manage Integration', + description: 'Member can connect, modify, and remove an OIDC provider to the connection.', + level: ResourceLevel.organization, + }, + ], + }, + { + title: 'GitHub Integration', + permissions: [ + { + id: 'github:manage', + title: 'Manage Integration', + description: + 'Member can connect, modify, and remove access for the GitHub integration and repository access.', + level: ResourceLevel.organization, + }, + ], + }, + { + title: 'Slack Integration', + permissions: [ + { + id: 'slack:manage', + title: 'Manage Slack Integration', + description: + 'Member can connect, modify, and remove access for the Slack integration and repository access.', + level: ResourceLevel.organization, + }, + ], + }, + { + title: 'Project', + permissions: [ + { + id: 'project:create', + title: 'Create Project', + description: 'Member can create new projects.', + level: ResourceLevel.organization, + }, + { + id: 'project:describe', + title: 'View Project', + description: 'Member can access the specified projects.', + level: [ResourceLevel.project, ResourceLevel.organization], + }, + { + id: 'project:delete', + title: 'View Project', + description: 'Member can access the specified projects.', + dependsOn: 'project:describe', + level: [ResourceLevel.project, ResourceLevel.organization], + }, + { + id: 'project:modifySettings', + title: 'Modify Settings', + description: 'Member can access the specified projects.', + dependsOn: 'project:describe', + level: [ResourceLevel.project, ResourceLevel.organization], + }, + ], + }, + { + title: 'Schema Linting', + permissions: [ + { + id: 'schemaLinting:modifyOrganizationSettings', + title: 'Manage organization level schema linting', + description: 'Member can view and modify the organization schema linting rules.', + level: ResourceLevel.organization, + }, + { + id: 'schemaLinting:modifyProjectSettings', + title: 'Manage project level schema linting', + description: 'Member can view and modify the organization schema linting rules.', + level: [ResourceLevel.project, ResourceLevel.organization], + dependsOn: 'project:view', + }, + ], + }, + { + title: 'Target', + permissions: [ + { + id: 'target:create', + title: 'Create Target', + description: 'Member can create new projects.', + level: [ResourceLevel.project, ResourceLevel.organization], + }, + { + id: 'target:delete', + title: 'Delete Target', + description: 'Member can access the specified projects.', + dependsOn: 'project:describe', + level: [ResourceLevel.project, ResourceLevel.organization, ResourceLevel.target], + }, + { + id: 'target:modifySettings', + title: 'Modify Settings', + description: 'Member can access the specified projects.', + dependsOn: 'project:describe', + level: [ResourceLevel.project, ResourceLevel.organization, ResourceLevel.target], + }, + ], + }, + { + title: 'Laboratory', + permissions: [ + { + id: 'laboratory:describe', + title: 'View Laboratory', + description: 'Member can access the laboratory, view and execute GraphQL documents.', + level: [ResourceLevel.project, ResourceLevel.organization, ResourceLevel.target], + }, + { + id: 'laboratory:modify', + title: 'Modify Laboratory', + description: + 'Member can create, delete and update collections and documents in the laboratory.', + dependsOn: 'laboratory:describe', + level: [ResourceLevel.project, ResourceLevel.organization, ResourceLevel.target], + }, + ], + }, + { + title: 'App Deployments', + permissions: [ + { + id: 'appDeployments:describe', + title: 'View App Deployments', + description: 'Member can view app deployments.', + level: [ResourceLevel.project, ResourceLevel.organization, ResourceLevel.target], + }, + ], + }, + { + title: 'Schema Checks', + permissions: [ + { + id: 'appDeployments:describe', + title: 'Approve Schema Check', + description: 'Member can approve failed schema checks.', + level: [ + ResourceLevel.project, + ResourceLevel.organization, + ResourceLevel.target, + ResourceLevel.service, + ], + }, + ], + }, +]; + +const roleFormSchema = z.object({ + name: z + .string({ + required_error: 'Required', + }) + .trim() + .min(2, 'Too short') + .max(64, 'Max 64 characters long') + .refine( + val => typeof val === 'string' && val.length > 0 && val[0] === val[0].toUpperCase(), + 'Must start with a capital letter', + ) + .refine(val => val !== 'Viewer' && val !== 'Admin', 'Viewer and Admin are reserved'), + description: z + .string({ + required_error: 'Please enter role description', + }) + .trim() + .min(2, 'Too short') + .max(256, 'Description is too long'), +}); + +function PermissionSelector(props: { + resourceLevel: ResourceLevel; + grantedPermissions: Record; + updateGrantedPermissions: (group: Record) => void; +}) { + return ( + + {permissionGroups.map(group => { + let selectedPermissions = 0; + for (const permission of group.permissions) { + if (props.grantedPermissions[permission.id] !== undefined) { + selectedPermissions++; + } + } + + const dependencyGraph = new Map>(); + for (const permission of group.permissions) { + if (!permission.dependsOn) { + continue; + } + let arr = dependencyGraph.get(permission.dependsOn); + if (!arr) { + arr = []; + dependencyGraph.set(permission.dependsOn, arr); + } + arr.push(permission.id); + } + + const filteredGroupPermissions = group.permissions.filter(permission => + Array.isArray(permission.level) + ? permission.level.includes(props.resourceLevel) + : props.resourceLevel === permission.level, + ); + + if (filteredGroupPermissions.length === 0) { + return null; + } + + return ( + + + {group.title}{' '} + {selectedPermissions > 0 && ( + + {selectedPermissions} selected + + )} + + + {filteredGroupPermissions.map(permission => { + const needsDependency = + !!permission.dependsOn && + props.grantedPermissions[permission.dependsOn] !== 'allow'; + return ( +
+
+
{permission.title}
+
{permission.description}
+
+ +
+ ); + })} +
+
+ ); + })} +
+ ); +} + +type GrantedPermissions = { [key: string]: 'allow' | 'deny' | undefined }; + +function GroupTeaser(props: { + title: string; + grantedPermissions: GrantedPermissions; + onClick: () => void; +}) { + const assignedPermissionsCount = Array.from(Object.values(props.grantedPermissions)).reduce( + (current, next) => { + if (!next) { + return current; + } + return current + 1; + }, + 0, + ); + + return ( + + ); +} + +function GG(props: { mode?: 'read-only'; role?: { name: string; description: string } }) { + const form = useForm({ + resolver: zodResolver(roleFormSchema), + mode: 'onChange', + defaultValues: { + name: props.role?.name ?? 'New Role', + description: props.role?.description, + }, + disabled: props.mode === 'read-only', + }); + + const [selectedGroupId, setSelectedGroupId] = useState(null); + + const [dynamicGroups, setDynamicGroups] = useState< + Array<{ + id: string; + level: ResourceLevel; + title: string; + permissions: { [key: string]: 'allow' | 'deny' | undefined }; + canDelete?: true; + }> + >([ + { + id: 'default', + level: ResourceLevel.organization, + title: 'Global Organization Wide Permissions', + permissions: {}, + }, + ]); + + const selectedGroup = dynamicGroups.find(group => group.id === selectedGroupId) ?? null; + + return ( + +
+ + {selectedGroup === null && ( + <> + + Member Role Editor + + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod + tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. + + +
+
+ ( + + Name + + + + + + )} + /> + ( + + Description + +