Resolve PR review issues

This commit is contained in:
Tuan Dang
2024-04-11 20:44:38 -07:00
parent fa1b236f26
commit 747acfe070
14 changed files with 272 additions and 297 deletions

View File

@ -13,7 +13,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
name: z.string().trim().min(1).describe(GROUPS.CREATE.name),
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
slug: z
.string()
.min(5)

View File

@ -118,9 +118,8 @@ export const groupDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId")
// db.raw(`CASE WHEN "${TableName.UserGroupMembership}"."groupId" IS NOT NULL THEN TRUE ELSE FALSE END as isPartOfGroup`)
)
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
.where({ isGhost: false })
.offset(offset);
if (limit) {

View File

@ -12,15 +12,15 @@ export const getDefaultOnPremFeatures = () => {
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: true,
rbac: true,
rbac: false,
customRateLimits: false,
customAlerts: false,
auditLogs: false,
auditLogsRetentionDays: 0,
samlSSO: true,
scim: true,
samlSSO: false,
scim: false,
ldap: false,
groups: true,
groups: false,
status: null,
trial_end: null,
has_used_trial: true,

View File

@ -19,15 +19,15 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: true,
rbac: false,
customRateLimits: false,
customAlerts: false,
auditLogs: false,
auditLogsRetentionDays: 0,
samlSSO: true,
scim: true,
samlSSO: false,
scim: false,
ldap: false,
groups: true,
groups: false,
status: null,
trial_end: null,
has_used_trial: true,

View File

@ -35,15 +35,15 @@ export type TFeatureSet = {
secretVersioning: true;
pitRecovery: false;
ipAllowlisting: false;
rbac: true;
rbac: false;
customRateLimits: false;
customAlerts: false;
auditLogs: false;
auditLogsRetentionDays: 0;
samlSSO: true;
scim: true;
samlSSO: false;
scim: false;
ldap: false;
groups: true;
groups: false;
status: null;
trial_end: null;
has_used_trial: true;

View File

@ -92,8 +92,8 @@ export const groupProjectServiceFactory = ({
role,
project.id
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPrivilege)
throw new ForbiddenRequestError({
message: "Failed to add group to project with more privileged role"
});
@ -226,7 +226,7 @@ export const groupProjectServiceFactory = ({
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const santiziedProjectMembershipRoles = roles.map((inputRole) => {
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
@ -252,7 +252,7 @@ export const groupProjectServiceFactory = ({
const updatedRoles = await groupProjectMembershipRoleDAL.transaction(async (tx) => {
await groupProjectMembershipRoleDAL.delete({ projectMembershipId: projectGroup.id }, tx);
return groupProjectMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
});
return updatedRoles;
@ -325,8 +325,8 @@ export const groupProjectServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
const groupMemberhips = await groupProjectDAL.findByProjectId(project.id);
return groupMemberhips;
const groupMemberships = await groupProjectDAL.findByProjectId(project.id);
return groupMemberships;
};
return {

View File

@ -163,7 +163,7 @@ export const identityProjectServiceFactory = ({
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const santiziedProjectMembershipRoles = roles.map((inputRole) => {
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
@ -189,7 +189,7 @@ export const identityProjectServiceFactory = ({
const updatedRoles = await identityProjectMembershipRoleDAL.transaction(async (tx) => {
await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx);
return identityProjectMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
});
return updatedRoles;
@ -226,7 +226,7 @@ export const identityProjectServiceFactory = ({
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
const [deletedIdentity] = await identityProjectDAL.delete({ identityId }); // TODO: fix
const [deletedIdentity] = await identityProjectDAL.delete({ identityId });
return deletedIdentity;
};
@ -246,8 +246,8 @@ export const identityProjectServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
const identityMemberhips = await identityProjectDAL.findByProjectId(projectId);
return identityMemberhips;
const identityMemberships = await identityProjectDAL.findByProjectId(projectId);
return identityMemberships;
};
return {

View File

@ -157,8 +157,8 @@ export const identityServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const identityMemberhips = await identityOrgMembershipDAL.findByOrgId(orgId);
return identityMemberhips;
const identityMemberships = await identityOrgMembershipDAL.findByOrgId(orgId);
return identityMemberships;
};
return {

View File

@ -362,7 +362,7 @@ export const projectMembershipServiceFactory = ({
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const santiziedProjectMembershipRoles = roles.map((inputRole) => {
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
@ -388,7 +388,7 @@ export const projectMembershipServiceFactory = ({
const updatedRoles = await projectMembershipDAL.transaction(async (tx) => {
await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx);
return projectUserMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
});
return updatedRoles;

View File

@ -1,9 +1,12 @@
export {
useAddGroupToWorkspace,
useDeleteGroupFromWorkspace,
useUpdateGroupWorkspaceRole
} from "./mutations";
export {
useAddIdentityToWorkspace,
useCreateWorkspace,
useCreateWsEnvironment,
useDeleteGroupFromWorkspace,
useDeleteIdentityFromWorkspace,
useDeleteUserFromWorkspace,
useDeleteWorkspace,
@ -22,8 +25,8 @@ export {
useNameWorkspaceSecrets,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateGroupWorkspaceRole,
useUpdateIdentityWorkspaceRole,
useUpdateUserWorkspaceRole,
useUpdateWsEnvironment,
useUpgradeProject} from "./queries";
useUpgradeProject
} from "./queries";

View File

@ -0,0 +1,64 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "./queries";
import { TUpdateWorkspaceGroupRoleDTO } from "./types";
export const useAddGroupToWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
groupSlug,
projectSlug,
role
}: {
groupSlug: string;
projectSlug: string;
role?: string;
}) => {
const {
data: { groupMembership }
} = await apiRequest.post(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`, {
role
});
return groupMembership;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
}
});
};
export const useUpdateGroupWorkspaceRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ groupSlug, projectSlug, roles }: TUpdateWorkspaceGroupRoleDTO) => {
const {
data: { groupMembership }
} = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`, {
roles
});
return groupMembership;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
}
});
};
export const useDeleteGroupFromWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ groupSlug, projectSlug }: { groupSlug: string; projectSlug: string }) => {
const {
data: { groupMembership }
} = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`);
return groupMembership;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
}
});
};

View File

@ -17,7 +17,6 @@ import {
RenameWorkspaceDTO,
TGetUpgradeProjectStatusDTO,
ToggleAutoCapitalizationDTO,
TUpdateWorkspaceGroupRoleDTO,
TUpdateWorkspaceIdentityRoleDTO,
TUpdateWorkspaceUserRoleDTO,
UpdateEnvironmentDTO,
@ -455,83 +454,12 @@ export const useGetWorkspaceIdentityMemberships = (workspaceId: string) => {
});
};
export const useAddGroupToWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
groupSlug,
projectSlug,
role
}: {
groupSlug: string;
projectSlug: string;
role?: string;
}) => {
const {
data: { groupMembership }
} = await apiRequest.post(
`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`,
{
role
}
);
return groupMembership;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
}
});
};
export const useUpdateGroupWorkspaceRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ groupSlug, projectSlug, roles }: TUpdateWorkspaceGroupRoleDTO) => {
const {
data: { groupMembership }
} = await apiRequest.patch(
`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`, {
roles
}
);
return groupMembership;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
}
});
};
export const useDeleteGroupFromWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
groupSlug,
projectSlug
}: {
groupSlug: string;
projectSlug: string;
}) => {
const {
data: { groupMembership }
} = await apiRequest.delete(
`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`
);
return groupMembership;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
}
});
};
export const useListWorkspaceGroups = (projectSlug: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectSlug),
queryFn: async () => {
const {
data: { groupMemberships }
data: { groupMemberships }
} = await apiRequest.get<{ groupMemberships: TGroupMembership[] }>(
`/api/v2/workspace/${projectSlug}/groups`
);

View File

@ -4,19 +4,23 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useCreateGroup,
useGetOrgRoles,
useUpdateGroup
} from "@app/hooks/api";
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useCreateGroup, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const GroupFormSchema = z.object({
name: z.string().min(1, "Name cannot be empty").max(50, "Name must be 50 characters or fewer"),
slug: z.string().min(1, "Slug cannot be empty").max(36, "Slug must be 36 characters or fewer"),
role: z.string()
name: z.string().min(1, "Name cannot be empty").max(50, "Name must be 50 characters or fewer"),
slug: z.string().min(5, "Slug cannot be empty").max(36, "Slug must be 36 characters or fewer"),
role: z.string()
});
export type TGroupFormData = z.infer<typeof GroupFormSchema>;
@ -27,185 +31,158 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
};
export const OrgGroupModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { currentOrg } = useOrganization();
const { data: roles } = useGetOrgRoles(currentOrg?.id || "");
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateGroup();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateGroup();
const {
control,
handleSubmit,
reset,
} = useForm<TGroupFormData>({
resolver: zodResolver(GroupFormSchema)
});
useEffect(() => {
const group = popUp?.group?.data as {
groupId: string;
name: string;
slug: string;
role: string;
customRole: {
name: string;
slug: string;
};
};
export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const { data: roles } = useGetOrgRoles(currentOrg?.id || "");
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateGroup();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateGroup();
if (!roles?.length) return;
const { control, handleSubmit, reset } = useForm<TGroupFormData>({
resolver: zodResolver(GroupFormSchema)
});
if (group) {
reset({
name: group.name,
slug: group.slug,
role: group?.customRole?.slug ?? group.role
});
} else {
reset({
name: "",
slug: "",
role: roles[0].slug
});
}
}, [popUp?.group?.data, roles]);
const onGroupModalSubmit = async ({
name,
slug,
role
}: TGroupFormData) => {
try {
if (!currentOrg?.id) return;
useEffect(() => {
const group = popUp?.group?.data as {
groupId: string;
name: string;
slug: string;
role: string;
customRole: {
name: string;
slug: string;
};
};
const group = popUp?.group?.data as {
groupId: string;
name: string;
slug: string;
};
if (group) {
await updateMutateAsync({
currentSlug: group.slug,
name,
slug,
role: role || undefined
});
} else {
await createMutateAsync({
name,
slug,
organizationId: currentOrg.id,
role: role || undefined
});
}
handlePopUpToggle("group", false);
reset();
createNotification({
text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`,
type: "success"
});
} catch (err) {
createNotification({
text: `Failed to ${popUp?.group?.data ? "updated" : "created"} group`,
type: "error"
});
}
if (!roles?.length) return;
if (group) {
reset({
name: group.name,
slug: group.slug,
role: group?.customRole?.slug ?? group.role
});
} else {
reset({
name: "",
slug: "",
role: roles[0].slug
});
}
return (
<Modal
isOpen={popUp?.group?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("group", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.group?.data ? "Update" : "Create"} Group`}>
<form onSubmit={handleSubmit(onGroupModalSubmit)}>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="Engineering"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Slug"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="engineering"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.group?.data ? "Update" : ""} Role`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`org-group-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!popUp?.group?.data ? "Create" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("group")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
}, [popUp?.group?.data, roles]);
const onGroupModalSubmit = async ({ name, slug, role }: TGroupFormData) => {
try {
if (!currentOrg?.id) return;
const group = popUp?.group?.data as {
groupId: string;
name: string;
slug: string;
};
if (group) {
await updateMutateAsync({
currentSlug: group.slug,
name,
slug,
role: role || undefined
});
} else {
await createMutateAsync({
name,
slug,
organizationId: currentOrg.id,
role: role || undefined
});
}
handlePopUpToggle("group", false);
reset();
createNotification({
text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`,
type: "success"
});
} catch (err) {
createNotification({
text: `Failed to ${popUp?.group?.data ? "updated" : "created"} group`,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.group?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("group", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.group?.data ? "Update" : "Create"} Group`}>
<form onSubmit={handleSubmit(onGroupModalSubmit)}>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" errorText={error?.message} isError={Boolean(error)}>
<Input {...field} placeholder="Engineering" />
</FormControl>
)}
/>
<Controller
control={control}
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl label="Slug" errorText={error?.message} isError={Boolean(error)}>
<Input {...field} placeholder="engineering" />
</FormControl>
)}
/>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.group?.data ? "Update" : ""} Role`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`org-group-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!popUp?.group?.data ? "Create" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("group")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

View File

@ -5,7 +5,13 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { GroupsTab, IdentityTab, MemberListTab, ProjectRoleListTab, ServiceTokenTab } from "./components";
import {
GroupsTab,
IdentityTab,
MemberListTab,
ProjectRoleListTab,
ServiceTokenTab
} from "./components";
enum TabSections {
Member = "members",
@ -18,8 +24,6 @@ enum TabSections {
export const MembersPage = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
console.log("currentWorkspace: ", currentWorkspace);
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
@ -44,7 +48,7 @@ export const MembersPage = withProjectPermission(
{currentWorkspace?.version && currentWorkspace.version > 1 && (
<TabPanel value={TabSections.Groups}>
<motion.div
key="panel-1"
key="panel-groups"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}