Continue project role page

This commit is contained in:
Tuan Dang
2024-07-26 06:43:09 -07:00
parent bf00d16c80
commit 5ad4061881
7 changed files with 429 additions and 66 deletions

View File

@@ -14,7 +14,8 @@ import {
faShield,
faTags,
faUser,
faUsers} from "@fortawesome/free-solid-svg-icons";
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";

View File

@@ -20,9 +20,7 @@ import { withProjectPermission } from "@app/hoc";
import { useGetProjectRoleBySlug } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { RoleDetailsSection, RolePermissionsSection } from "./components";
// import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components";
import { RoleDetailsSection, RoleModal,RolePermissionsSection } from "./components";
export const RolePage = withProjectPermission(
() => {
@@ -66,7 +64,7 @@ export const RolePage = withProjectPermission(
}
};
const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? "");
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? "");
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
@@ -148,7 +146,7 @@ export const RolePage = withProjectPermission(
</div>
</div>
)}
{/* <RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> */}
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete the project role ${data?.name ?? ""}?`}

View File

@@ -6,17 +6,14 @@ import { IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetProjectRoleBySlug } from "@app/hooks/api";
// import { UsePopUpState } from "@app/hooks/usePopUp";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
roleSlug: string;
// handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: {}) => void;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: {}) => void;
};
export const RoleDetailsSection = ({
roleSlug
}: // handlePopUpOpen
Props) => {
export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
@@ -41,10 +38,9 @@ Props) => {
variant="plain"
className="group relative"
onClick={() => {
// TODO
// handlePopUpOpen("role", {
// roleId
// })
handlePopUpOpen("role", {
roleSlug
});
}}
>
<FontAwesomeIcon icon={faPencil} />

View File

@@ -0,0 +1,231 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import {
// useOrganization,
useWorkspace
} from "@app/context";
import {
// useCreateProjectRole,
// useUpdateProjectRole,
useGetProjectRoleBySlug
// useCreateOrgRole,
// useGetOrgRole,
// useUpdateOrgRole
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
name: z.string(),
description: z.string(),
slug: z.string()
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["role"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["role"]>, state?: boolean) => void;
};
export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
// const router = useRouter();
const popupData = popUp?.role?.data as {
roleSlug: string;
};
const { currentWorkspace } = useWorkspace();
const projectSlug = currentWorkspace?.slug || "";
const { data: role } = useGetProjectRoleBySlug(projectSlug, popupData?.roleSlug ?? "");
console.log("Project RoleModal role: ", role);
// const { mutateAsync: createProjectRole } = useCreateProjectRole();
// const { mutateAsync: updateProjectRole } = useUpdateProjectRole();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: ""
}
});
useEffect(() => {
if (role) {
reset({
name: role.name,
description: role.description,
slug: role.slug
});
} else {
reset({
name: "",
description: "",
slug: ""
});
}
}, [role]);
const onFormSubmit = async ({ name, description, slug }: FormData) => {
try {
console.log("onFormSubmit args: ", {
name,
description,
slug
});
if (!projectSlug || !role?.id) return;
if (role) {
// update
console.log("update");
// const u1 = await updateProjectRole({
// id: role.id,
// projectSlug,
// name,
// description,
// slug
// });
// console.log("u1: ", u1);
} else {
// create
console.log("create");
}
// if (!orgId) return;
// if (role) {
// // update
// await updateOrgRole({
// orgId,
// id: role.id,
// name,
// description,
// slug
// });
// handlePopUpToggle("role", false);
// } else {
// // create
// const newRole = await createOrgRole({
// orgId,
// name,
// description,
// slug,
// permissions: []
// });
// handlePopUpToggle("role", false);
// router.push(`/org/${orgId}/roles/${newRole.id}`);
// }
createNotification({
text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`,
type: "success"
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ??
`Failed to ${popUp?.role?.data ? "update" : "create"} role`;
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.role?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("role", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.role?.data ? "Update" : "Create"} Role`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Billing Team" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Slug"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="billing" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="To manage billing" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.role?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("role", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

View File

@@ -1,16 +1,24 @@
import { useMemo } from "react";
import {
Control,
// Controller,
UseFormGetValues,
UseFormSetValue,
useWatch
} from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { Control, Controller, UseFormGetValues, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Select, SelectItem,Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples";
import {
Checkbox,
FormControl,
Input,
Select,
SelectItem,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { TFormSchema } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
type Props = {
@@ -37,8 +45,14 @@ export const RowPermissionSecretsRow = ({
getValue,
control
}: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
// const [isCustom, setIsCustom] = useToggle();
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
const customRule = useWatch({
control,
name: `permissions.${formName}.custom`
});
const isCustom = Boolean(customRule);
const allRule = useWatch({ control, name: `permissions.${formName}.all` });
@@ -83,17 +97,13 @@ export const RowPermissionSecretsRow = ({
};
return (
<Tr
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<>
<Tr>
<Td>{isCustom && <FontAwesomeIcon icon={faChevronDown} />}</Td>
<Td>{title}</Td>
<Td>
<Select
value={selectedPermissionCategory}
value={isCustom ? Permission.Custom : selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
@@ -106,5 +116,132 @@ export const RowPermissionSecretsRow = ({
</Select>
</Td>
</Tr>
{isCustom && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isCustom && " border-mineshaft-500 p-8"}`}
>
<div>
<TableContainer className="mt-6 border-mineshaft-500">
<Table>
<THead>
<Tr>
<Th />
<Th className="min-w-[8rem]">
<div className="flex items-center gap-2">
Secret Path
<span className="text-xs normal-case">
<GlobPatternExamples />
</span>
</div>
</Th>
<Th className="text-center">View</Th>
<Th className="text-center">Create</Th>
<Th className="text-center">Modify</Th>
<Th className="text-center">Delete</Th>
</Tr>
</THead>
<TBody>
{isCustom &&
environments.map(({ name, slug }) => (
<Tr key={`custom-role-project-secret-${slug}`}>
<Td>{name}</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.secretPath`}
control={control}
render={({ field }) => (
/* eslint-disable-next-line no-template-curly-in-string */
<FormControl helperText="Supports glob path pattern string">
<Input
{...field}
className="w-full overflow-ellipsis"
placeholder="Glob patterns are supported"
/>
</FormControl>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.read`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.${formName}.${slug}.read`}
isDisabled={!isEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.create`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
onBlur={field.onBlur}
id={`permissions.${formName}.${slug}.modify`}
isDisabled={!isEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.edit`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
onBlur={field.onBlur}
id={`permissions.${formName}.${slug}.modify`}
isDisabled={!isEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
defaultValue={false}
name={`permissions.${formName}.${slug}.delete`}
control={control}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.${formName}.${slug}.delete`}
isDisabled={!isEditable}
/>
</div>
)}
/>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</div>
</Td>
</Tr>
)}
</>
);
};

View File

@@ -2,27 +2,17 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
import {
Button,
Table,
TableContainer,
TBody,
Th,
THead,
Tr
// Td
} from "@app/components/v2";
import { ProjectPermissionSub,useWorkspace } from "@app/context";
import {
// useUpdateOrgRole,
useGetProjectRoleBySlug
} from "@app/hooks/api";
import {
formSchema,
formRolePermission2API, formSchema,
rolePermission2Form,
TFormSchema} from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
TFormSchema
} from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
import { RolePermissionRow } from "./RolePermissionRow";
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
const SINGLE_PERMISSION_LIST = [
{
@@ -99,16 +89,15 @@ type Props = {
roleSlug: string;
};
// TODO: fill table
// note: isEditable should also depend on if user has edit role permission
export const RolePermissionsSection = ({ roleSlug }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectSlug = currentWorkspace?.slug || "";
const { data: role } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string);
console.log("RolePermissionsSection role data: ", role);
const {
setValue,
getValues,
control,
handleSubmit,
formState: { isDirty, isSubmitting },
@@ -118,16 +107,19 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => {
resolver: zodResolver(formSchema)
});
// const { mutateAsync: updateRole } = useUpdateOrgRole();
const { mutateAsync: updateRole } = useUpdateProjectRole();
const onSubmit = async () => {
const onSubmit = async (el: TFormSchema) => {
try {
// await updateRole({
// orgId,
// id: roleId,
// ...el,
// permissions: formRolePermission2API(el.permissions)
// });
if (!projectSlug || !role?.id) return;
await updateRole({
id: role?.id as string,
projectSlug,
...el,
permissions: formRolePermission2API(el.permissions)
});
createNotification({ type: "success", text: "Successfully updated role" });
} catch (err) {
console.log(err);
@@ -177,8 +169,15 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => {
</Tr>
</THead>
<TBody>
<RowPermissionSecretsRow
title="Secrets"
formName={ProjectPermissionSub.Secrets}
isEditable={isCustomRole}
setValue={setValue}
getValue={getValues}
control={control}
/>
{SINGLE_PERMISSION_LIST.map((permission) => {
console.log("permission: ", permission);
return (
<RolePermissionRow
title={permission.title}

View File

@@ -1,2 +1,3 @@
export { RoleDetailsSection } from "./RoleDetailsSection";
export { RoleModal } from "./RoleModal";
export { RolePermissionsSection } from "./RolePermissionsSection";