(4) Drop modals from V2: change permissions Modal (#5233)

Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com>
This commit is contained in:
Tuval Simha 2024-07-25 12:16:24 +03:00 committed by GitHub
parent 00926e750c
commit dc4da0d32f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 234 additions and 116 deletions

View file

@ -3,6 +3,7 @@ import { MoreHorizontalIcon, MoveDownIcon, MoveUpIcon, SettingsIcon } from 'luci
import type { IconType } from 'react-icons';
import { FaGithub, FaGoogle, FaOpenid, FaUserLock } from 'react-icons/fa';
import { useMutation } from 'urql';
import { PermissionsSpace, usePermissionsManager } from '@/components/organization/Permissions';
import {
AlertDialog,
AlertDialogAction,
@ -14,6 +15,13 @@ import {
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
DropdownMenu,
DropdownMenuContent,
@ -21,11 +29,17 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useToast } from '@/components/ui/use-toast';
import { ChangePermissionsModal } from '@/components/v2/modals';
import { FragmentType, graphql, useFragment } from '@/gql';
import { AuthProvider } from '@/gql/graphql';
import {
AuthProvider,
OrganizationAccessScope,
ProjectAccessScope,
TargetAccessScope,
} from '@/gql/graphql';
import { scopes } from '@/lib/access/common';
import { useToggle } from '@/lib/hooks';
import { RoleSelector } from './common';
import { MemberInvitationButton } from './invitations';
@ -267,8 +281,8 @@ function OrganizationMemberRoleSwitcher(props: {
<ChangePermissionsModal
isOpen={isPermissionsModalOpen}
toggleModalOpen={togglePermissionsModalOpen}
organization={organization}
member={member}
organizationFragment={organization}
memberFragment={member}
/>
</>
) : null}
@ -587,3 +601,118 @@ export function OrganizationMembers(props: {
</SubPageLayout>
);
}
const ChangePermissionsModal_OrganizationFragment = graphql(`
fragment ChangePermissionsModal_OrganizationFragment on Organization {
...UsePermissionManager_OrganizationFragment
}
`);
export const ChangePermissionsModal_MemberFragment = graphql(`
fragment ChangePermissionsModal_MemberFragment on Member {
id
...UsePermissionManager_MemberFragment
}
`);
export function ChangePermissionsModal(props: {
isOpen: boolean;
toggleModalOpen: () => void;
organizationFragment: FragmentType<typeof ChangePermissionsModal_OrganizationFragment>;
memberFragment: FragmentType<typeof ChangePermissionsModal_MemberFragment>;
}) {
const organization = useFragment(
ChangePermissionsModal_OrganizationFragment,
props.organizationFragment,
);
const member = useFragment(ChangePermissionsModal_MemberFragment, props.memberFragment);
const manager = usePermissionsManager({
onSuccess: props.toggleModalOpen,
organization,
member,
passMemberScopes: true,
});
const initialScopes = {
organization: [...manager.organizationScopes],
project: [...manager.projectScopes],
target: [...manager.targetScopes],
};
return (
<ChangePermissionsModalContent
isOpen={props.isOpen}
toggleModalOpen={props.toggleModalOpen}
manager={manager}
initialScopes={initialScopes}
onSubmit={() => manager.submit}
/>
);
}
export function ChangePermissionsModalContent(props: {
isOpen: boolean;
toggleModalOpen: () => void;
manager: ReturnType<typeof usePermissionsManager>;
initialScopes: {
organization: OrganizationAccessScope[];
project: ProjectAccessScope[];
target: TargetAccessScope[];
};
onSubmit: () => void;
}) {
return (
<Dialog open={props.isOpen} onOpenChange={props.toggleModalOpen}>
<DialogContent className="w-4/5 max-w-[750px] md:w-3/5">
<form className="flex w-full flex-col gap-5" onSubmit={props.onSubmit}>
<DialogHeader>
<DialogTitle>Permissions (legacy)</DialogTitle>
</DialogHeader>
<Tabs defaultValue="Organization" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="Organization">Organization</TabsTrigger>
<TabsTrigger value="Projects">Projects</TabsTrigger>
<TabsTrigger value="Targets">Targets</TabsTrigger>
</TabsList>
<PermissionsSpace
title="Organization"
scopes={scopes.organization}
initialScopes={props.initialScopes.organization}
selectedScopes={props.manager.organizationScopes}
onChange={props.manager.setOrganizationScopes}
checkAccess={props.manager.canAccessOrganization}
/>
<PermissionsSpace
title="Projects"
scopes={scopes.project}
initialScopes={props.initialScopes.project}
selectedScopes={props.manager.projectScopes}
onChange={props.manager.setProjectScopes}
checkAccess={props.manager.canAccessProject}
/>
<PermissionsSpace
title="Targets"
scopes={scopes.target}
initialScopes={props.initialScopes.target}
selectedScopes={props.manager.targetScopes}
onChange={props.manager.setTargetScopes}
checkAccess={props.manager.canAccessTarget}
/>
</Tabs>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={ev => {
ev.preventDefault();
props.toggleModalOpen();
}}
>
Cancel
</Button>
<Button type="submit">Save permissions</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View file

@ -104,7 +104,14 @@ function CreateCDNAccessTokenModal(props: {
</div>
<div className="mt-auto flex w-full gap-2 self-end">
<Button variant="secondary" className="ml-auto" onClick={props.onClose}>
<Button
variant="secondary"
className="ml-auto"
onClick={ev => {
ev.preventDefault();
props.onClose();
}}
>
Cancel
</Button>

View file

@ -302,7 +302,14 @@ export function GenerateTokenContent(props: {
</Accordion>
</div>
<DialogFooter>
<Button variant="outline" type="button" onClick={props.toggleModalOpen}>
<Button
variant="outline"
type="button"
onClick={ev => {
ev.preventDefault();
props.toggleModalOpen();
}}
>
Cancel
</Button>
<Button

View file

@ -1,99 +0,0 @@
import { ReactElement } from 'react';
import { PermissionsSpace, usePermissionsManager } from '@/components/organization/Permissions';
import { Button } from '@/components/ui/button';
import { Heading } from '@/components/ui/heading';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Modal } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { scopes } from '@/lib/access/common';
const ChangePermissionsModal_OrganizationFragment = graphql(`
fragment ChangePermissionsModal_OrganizationFragment on Organization {
...UsePermissionManager_OrganizationFragment
}
`);
const ChangePermissionsModal_MemberFragment = graphql(`
fragment ChangePermissionsModal_MemberFragment on Member {
id
...UsePermissionManager_MemberFragment
}
`);
export function ChangePermissionsModal({
isOpen,
toggleModalOpen,
...props
}: {
isOpen: boolean;
toggleModalOpen: () => void;
organization: FragmentType<typeof ChangePermissionsModal_OrganizationFragment>;
member: FragmentType<typeof ChangePermissionsModal_MemberFragment>;
}): ReactElement {
const organization = useFragment(ChangePermissionsModal_OrganizationFragment, props.organization);
const member = useFragment(ChangePermissionsModal_MemberFragment, props.member);
const manager = usePermissionsManager({
onSuccess: toggleModalOpen,
organization,
member,
passMemberScopes: true,
});
const initialScopes = {
organization: [...manager.organizationScopes],
project: [...manager.projectScopes],
target: [...manager.targetScopes],
};
return (
<Modal open={isOpen} onOpenChange={toggleModalOpen} className="w-[600px]">
<form className="flex w-full flex-col items-center gap-5" onSubmit={manager.submit}>
<Heading>Permissions (legacy)</Heading>
<Tabs defaultValue="Organization" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="Organization">Organization</TabsTrigger>
<TabsTrigger value="Projects">Projects</TabsTrigger>
<TabsTrigger value="Targets">Targets</TabsTrigger>
</TabsList>
<PermissionsSpace
title="Organization"
scopes={scopes.organization}
initialScopes={initialScopes.organization}
selectedScopes={manager.organizationScopes}
onChange={manager.setOrganizationScopes}
checkAccess={manager.canAccessOrganization}
/>
<PermissionsSpace
title="Projects"
scopes={scopes.project}
initialScopes={initialScopes.project}
selectedScopes={manager.projectScopes}
onChange={manager.setProjectScopes}
checkAccess={manager.canAccessProject}
/>
<PermissionsSpace
title="Targets"
scopes={scopes.target}
initialScopes={initialScopes.target}
selectedScopes={manager.targetScopes}
onChange={manager.setTargetScopes}
checkAccess={manager.canAccessTarget}
/>
</Tabs>
<div className="flex w-full gap-2">
<Button
type="button"
size="lg"
className="w-full justify-center"
onClick={toggleModalOpen}
>
Cancel
</Button>
<Button type="submit" size="lg" className="w-full justify-center" variant="primary">
Save permissions
</Button>
</div>
</form>
</Modal>
);
}

View file

@ -1,3 +1,2 @@
export { ChangePermissionsModal } from './change-permissions';
export { ConnectSchemaModal } from './connect-schema';
export { TransferOrganizationOwnershipModal } from './transfer-organization-ownership';

View file

@ -692,7 +692,13 @@ export function DeleteOrganizationModalContent(props: {
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button variant="outline" onClick={props.toggleModalOpen}>
<Button
variant="outline"
onClick={ev => {
ev.preventDefault();
props.toggleModalOpen();
}}
>
Cancel
</Button>
<Button variant="destructive" onClick={props.handleDelete}>

View file

@ -520,7 +520,13 @@ export function DeleteProjectModalContent(props: {
<DialogDescription className="font-bold">This action is irreversible!</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button variant="outline" onClick={props.toggleModalOpen}>
<Button
variant="outline"
onClick={ev => {
ev.preventDefault();
props.toggleModalOpen();
}}
>
Cancel
</Button>
<Button variant="destructive" onClick={props.handleDelete}>

View file

@ -1,4 +1,4 @@
import React, { ReactElement, useCallback, useState } from 'react';
import { ComponentProps, PropsWithoutRef, useCallback, useState } from 'react';
import clsx from 'clsx';
import { formatISO } from 'date-fns';
import { useFormik } from 'formik';
@ -111,7 +111,7 @@ function RegistryAccessTokens(props: {
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
}) {
const me = useFragment(RegistryAccessTokens_MeFragment, props.me);
const [{ fetching: deleting }, mutate] = useMutation(DeleteTokensDocument);
const [checked, setChecked] = useState<string[]>([]);
@ -242,7 +242,7 @@ const ExtendBaseSchema = (props: {
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement => {
}) => {
const [mutation, mutate] = useMutation(Settings_UpdateBaseSchemaMutation);
const [baseSchema, setBaseSchema] = useState(props.baseSchema);
const { toast } = useToast();
@ -342,14 +342,14 @@ const ClientExclusion_AvailableClientNamesQuery = graphql(`
`);
function ClientExclusion(
props: React.PropsWithoutRef<
props: PropsWithoutRef<
{
organizationId: string;
projectId: string;
selectedTargets: string[];
clientsFromSettings: string[];
value: string[];
} & Pick<React.ComponentProps<typeof Combobox>, 'name' | 'disabled' | 'onBlur' | 'onChange'>
} & Pick<ComponentProps<typeof Combobox>, 'name' | 'disabled' | 'onBlur' | 'onChange'>
>,
) {
const now = floorDate(new Date());
@ -461,7 +461,7 @@ const ConditionalBreakingChanges = (props: {
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement => {
}) => {
const [targetValidation, setValidation] = useMutation(SetTargetValidationMutation);
const [mutation, updateValidation] = useMutation(
TargetSettingsPage_UpdateTargetValidationSettingsMutation,
@ -875,7 +875,7 @@ function GraphQLEndpointUrl(props: {
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
}) {
const { toast } = useToast();
const [mutation, mutate] = useMutation(TargetSettingsPage_UpdateTargetGraphQLEndpointUrl);
const { handleSubmit, values, handleChange, handleBlur, isSubmitting, errors, touched } =
@ -1364,7 +1364,13 @@ export function DeleteTargetModalContent(props: {
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button variant="outline" onClick={props.toggleModalOpen}>
<Button
variant="outline"
onClick={ev => {
ev.preventDefault();
props.toggleModalOpen();
}}
>
Cancel
</Button>
<Button variant="destructive" onClick={props.handleDelete}>

View file

@ -0,0 +1,57 @@
import { useState } from 'react';
import { ChangePermissionsModalContent } from '@/components/organization/members/list';
import { usePermissionsManager } from '@/components/organization/Permissions';
import { Button } from '@/components/ui/button';
import { Meta, StoryObj } from '@storybook/react';
const meta: Meta<typeof ChangePermissionsModalContent> = {
title: 'Modals/Change Permissions Modal',
component: ChangePermissionsModalContent,
};
export default meta;
type Story = StoryObj<typeof ChangePermissionsModalContent>;
export const Default: Story = {
render: () => {
const initialScopes = {
organization: [],
project: [],
target: [],
};
const manager = {
organizationScopes: [],
projectScopes: [],
targetScopes: [],
canAccessOrganization: () => true,
canAccessProject: () => true,
canAccessTarget: () => true,
noneSelected: true,
setOrganizationScopes: () => console.log('Set Organization Scopes'),
setProjectScopes: () => console.log('Set Project Scopes'),
setTargetScopes: () => console.log('Set Target Scopes'),
state: 'IDLE',
submit: () => console.log('Submit'),
} as ReturnType<typeof usePermissionsManager>;
const [openModal, setOpenModal] = useState(false);
const toggleModalOpen = () => setOpenModal(!openModal);
return (
<>
<Button onClick={toggleModalOpen}>Open Modal</Button>
{openModal && (
<ChangePermissionsModalContent
manager={manager}
isOpen={openModal}
toggleModalOpen={toggleModalOpen}
initialScopes={initialScopes}
onSubmit={() => console.log('Submit')}
key="change-permissions-modal"
/>
)}
</>
);
},
};