From 5495ffd78e9a0152717f43726d9e132a08886c56 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 09:44:20 -0800 Subject: [PATCH 01/10] improvement: update add group to project modal to use filterable selects --- .../src/ee/services/license/license-fns.ts | 2 +- .../components/GroupsSection/GroupModal.tsx | 175 +++++++++--------- 2 files changed, 84 insertions(+), 93 deletions(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 70c299564..accab79b5 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ oidcSSO: false, scim: false, ldap: false, - groups: false, + groups: true, status: null, trial_end: null, has_used_trial: true, diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx index 3ece05497..ef1c89e58 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx @@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; +import { Button, FilterableSelect, FormControl, Modal, ModalContent } from "@app/components/v2"; import { useOrganization, useWorkspace } from "@app/context"; import { useAddGroupToWorkspace, @@ -16,8 +16,8 @@ import { import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z.object({ - id: z.string(), - role: z.string() + group: z.object({ id: z.string(), name: z.string() }), + role: z.object({ slug: z.string(), name: z.string() }) }); export type FormData = z.infer; @@ -27,7 +27,9 @@ type Props = { handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void; }; -export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => { +// TODO: update backend to support adding multiple roles at once + +const Content = ({ popUp, handlePopUpToggle }: Props) => { const { currentOrg } = useOrganization(); const { currentWorkspace } = useWorkspace(); @@ -59,12 +61,12 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => { resolver: zodResolver(schema) }); - const onFormSubmit = async ({ id, role }: FormData) => { + const onFormSubmit = async ({ group, role }: FormData) => { try { await addGroupToWorkspaceMutateAsync({ projectId: currentWorkspace?.id || "", - groupId: id, - role: role || undefined + groupId: group.id, + role: role.slug || undefined }); reset(); @@ -82,95 +84,84 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => { } }; + return filteredGroupMembershipOrgs.length ? ( +
+ ( + + option.id} + getOptionLabel={(option) => option.name} + options={filteredGroupMembershipOrgs} + placeholder="Select group..." + /> + + )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + options={roles} + placeholder="Select role..." + /> + + )} + /> +
+ + +
+ + ) : ( +
+
+ All groups in your organization have already been added to this project. +
+ + + +
+ ); +}; + +export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => { return ( { - handlePopUpToggle("group", isOpen); - reset(); - }} + onOpenChange={(isOpen) => handlePopUpToggle("group", isOpen)} > - - {filteredGroupMembershipOrgs.length ? ( -
- ( - - - - )} - /> - ( - - - - )} - /> -
- - -
- - ) : ( -
-
- All groups in your organization have already been added to this project. -
- - - -
- )} + +
); From 9c03144f19d066e6252fd05d860f7c1832dd27bf Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 10:03:45 -0800 Subject: [PATCH 02/10] improvement: use filterable multi-select for add users to project role select --- .../MembersTab/components/AddMemberModal.tsx | 100 +++--------------- 1 file changed, 16 insertions(+), 84 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx index fd0b13172..ed8271973 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx @@ -2,24 +2,11 @@ import { useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import Link from "next/link"; -import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; -import { twMerge } from "tailwind-merge"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - FilterableSelect, - FormControl, - Modal, - ModalContent -} from "@app/components/v2"; +import { Button, FilterableSelect, FormControl, Modal, ModalContent } from "@app/components/v2"; import { useOrganization, useWorkspace } from "@app/context"; import { useAddUsersToOrg, @@ -33,7 +20,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp"; const addMemberFormSchema = z.object({ orgMemberships: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).min(1), - projectRoleSlugs: z.array(z.string().trim().min(1)).min(1) + projectRoleSlugs: z.array(z.object({ slug: z.string().trim(), name: z.string().trim() })).min(1) }); type TAddMemberForm = z.infer; @@ -64,7 +51,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { formState: { isSubmitting, errors } } = useForm({ resolver: zodResolver(addMemberFormSchema), - defaultValues: { orgMemberships: [], projectRoleSlugs: [ProjectMembershipRole.Member] } + defaultValues: { orgMemberships: [], projectRoleSlugs: [] } }); const { mutateAsync: addMembersToProject } = useAddUsersToOrg(); @@ -94,7 +81,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { { slug: currentWorkspace.slug, id: currentWorkspace.id, - projectRoleSlug: projectRoleSlugs + projectRoleSlug: projectRoleSlugs.map((role) => role.slug) } ] }); @@ -172,78 +159,23 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { ( + render={({ field: { onChange, value }, fieldState: { error } }) => ( - - - {roles && roles.length > 0 ? ( -
- {/* eslint-disable-next-line no-nested-ternary */} - {selectedRoleSlugs.length === 1 - ? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name - : selectedRoleSlugs.length === 0 - ? "Select at least one role" - : `${selectedRoleSlugs.length} roles selected`} - -
- ) : ( -
- No roles found -
- )} -
- - {roles && roles.length > 0 ? ( - roles.map((role) => { - const isSelected = selectedRoleSlugs.includes(role.slug); - - return ( - roles.length > 1 && event.preventDefault()} - onClick={() => { - if (selectedRoleSlugs.includes(String(role.slug))) { - field.onChange( - selectedRoleSlugs.filter( - (roleSlug: string) => roleSlug !== String(role.slug) - ) - ); - } else { - field.onChange([...selectedRoleSlugs, role.slug]); - } - }} - key={`role-slug-${role.slug}`} - icon={ - isSelected ? ( - - ) : ( -
- ) - } - iconPos="left" - className="w-[28.4rem] text-sm" - > - {role.name} - - ); - }) - ) : ( -
- )} - - + option.slug} + getOptionLabel={(option) => option.name} + /> )} /> From d131314de00c72f9c8ebd3ec2b1d31c20fd06a87 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 10:21:52 -0800 Subject: [PATCH 03/10] improvement: filter select for invite users to org --- .../src/ee/services/license/license-fns.ts | 2 +- .../OrgMembersSection/AddOrgMemberModal.tsx | 37 ++++++++----------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index accab79b5..70c299564 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ oidcSSO: false, scim: false, ldap: false, - groups: true, + groups: false, status: null, trial_end: null, has_used_trial: true, diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index 74aa5d7c2..2b90276de 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -45,7 +45,7 @@ const addMemberFormSchema = z.object({ ) .default([]), projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG), - organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG) + organizationRole: z.object({ name: z.string(), slug: z.string() }) }); type TAddMemberForm = z.infer; @@ -87,16 +87,16 @@ export const AddOrgMemberModal = ({ useEffect(() => { if (organizationRoles) { reset({ - organizationRoleSlug: isCustomOrgRole(currentOrg?.defaultMembershipRole!) - ? organizationRoles?.find((role) => role.id === currentOrg?.defaultMembershipRole)?.slug! - : currentOrg?.defaultMembershipRole + organizationRole: isCustomOrgRole(currentOrg?.defaultMembershipRole!) + ? organizationRoles?.find((role) => role.id === currentOrg?.defaultMembershipRole) + : organizationRoles?.find((role) => role.slug === currentOrg?.defaultMembershipRole) }); } }, [organizationRoles]); const onAddMembers = async ({ emails, - organizationRoleSlug, + organizationRole, projects: selectedProjects, projectRoleSlug }: TAddMemberForm) => { @@ -138,7 +138,7 @@ export const AddOrgMemberModal = ({ const { data } = await addUsersMutateAsync({ organizationId: currentOrg?.id, inviteeEmails: emails.split(",").map((email) => email.trim()), - organizationRoleSlug, + organizationRoleSlug: organizationRole.slug, projects: selectedProjects.map(({ id }) => ({ id, projectRoleSlug: [projectRoleSlug] })) }); @@ -207,27 +207,22 @@ export const AddOrgMemberModal = ({ ( + name="organizationRole" + render={({ field: { value, onChange }, fieldState: { error } }) => ( -
- -
+ option.slug} + getOptionLabel={(option) => option.name} + value={value} + onChange={onChange} + />
)} /> From 9ca58894f0805291aa1d5d504d6bb7fb5a4f8a7e Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 11:07:14 -0800 Subject: [PATCH 04/10] improvement: filter select for create identity role --- .../IdentitySection/IdentityModal.tsx | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index 4b71aaea3..badab9a3c 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -9,27 +9,24 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, + FilterableSelect, FormControl, FormLabel, IconButton, Input, Modal, - ModalContent, - Select, - SelectItem + ModalContent } from "@app/components/v2"; import { useOrganization } from "@app/context"; +import { isCustomOrgRole } from "@app/helpers/roles"; import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; -import { - // IdentityAuthMethod, - useAddIdentityUniversalAuth -} from "@app/hooks/api/identities"; +import { useAddIdentityUniversalAuth } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z .object({ - name: z.string(), - role: z.string(), + name: z.string().min(1, "Required"), + role: z.object({ slug: z.string(), name: z.string() }), metadata: z .object({ key: z.string().trim().min(1), @@ -101,13 +98,15 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { if (identity) { reset({ name: identity.name, - role: identity?.customRole?.slug ?? identity.role, + role: identity?.customRole ?? roles.find((role) => role.slug === identity.role), metadata: identity.metadata }); } else { reset({ name: "", - role: roles[0].slug + role: isCustomOrgRole(currentOrg?.defaultMembershipRole!) + ? roles?.find((role) => role.id === currentOrg?.defaultMembershipRole) + : roles?.find((role) => role.slug === currentOrg?.defaultMembershipRole) }); } }, [popUp?.identity?.data, roles]); @@ -126,7 +125,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { await updateMutateAsync({ identityId: identity.identityId, name, - role: role || undefined, + role: role.slug || undefined, organizationId: orgId, metadata }); @@ -137,7 +136,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const { id: createdId } = await createMutateAsync({ name, - role: role || undefined, + role: role.slug || undefined, organizationId: orgId, metadata }); @@ -184,7 +183,10 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { reset(); }} > - +
{ ( + render={({ field: { onChange, value }, fieldState: { error } }) => ( - + option.slug} + getOptionLabel={(option) => option.name} + /> )} /> From 8b3af92d23619cb84f4606f6e8da7235bfe5d1be Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 11:55:48 -0800 Subject: [PATCH 05/10] improvement: edit user role filterable select --- frontend/src/views/Org/UserPage/UserPage.tsx | 3 +- .../components/UserDetailsSection.tsx | 1 + .../components/UserOrgMembershipModal.tsx | 56 ++++++++++--------- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/frontend/src/views/Org/UserPage/UserPage.tsx b/frontend/src/views/Org/UserPage/UserPage.tsx index ad0f66d6e..2b3817bcd 100644 --- a/frontend/src/views/Org/UserPage/UserPage.tsx +++ b/frontend/src/views/Org/UserPage/UserPage.tsx @@ -148,7 +148,8 @@ export const UserPage = withPermission( onClick={() => handlePopUpOpen("orgMembership", { membershipId: membership.id, - role: membership.role + role: membership.role, + roleId: membership.roleId }) } disabled={!isAllowed} diff --git a/frontend/src/views/Org/UserPage/components/UserDetailsSection.tsx b/frontend/src/views/Org/UserPage/components/UserDetailsSection.tsx index d439c7ecd..6939eca17 100644 --- a/frontend/src/views/Org/UserPage/components/UserDetailsSection.tsx +++ b/frontend/src/views/Org/UserPage/components/UserDetailsSection.tsx @@ -100,6 +100,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => handlePopUpOpen("orgMembership", { membershipId: membership.id, role: membership.role, + roleId: membership.roleId, metadata: membership.metadata }); }} diff --git a/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx b/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx index 57c8cebb2..9362881a4 100644 --- a/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx +++ b/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { SingleValue } from "react-select"; import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -8,21 +9,21 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, + FilterableSelect, FormControl, FormLabel, IconButton, Input, Modal, - ModalContent, - Select, - SelectItem + ModalContent } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; +import { isCustomOrgRole } from "@app/helpers/roles"; import { useGetOrgRoles, useUpdateOrgMembership } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z.object({ - role: z.string(), + role: z.object({ name: z.string(), slug: z.string() }), metadata: z .object({ key: z.string().trim().min(1), @@ -45,7 +46,7 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; - const { data: roles } = useGetOrgRoles(orgId); + const { data: roles = [] } = useGetOrgRoles(orgId); const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership(); @@ -66,6 +67,7 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg const popUpData = popUp?.orgMembership?.data as { membershipId: string; role: string; + roleId?: string; metadata: { key: string; value: string }[]; }; @@ -73,13 +75,18 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg if (!roles?.length) return; if (popUpData) { + console.log("roles", roles, popUpData.roleId); reset({ - role: popUpData.role, + role: popUpData.roleId + ? roles?.find((role) => role.id === popUpData.roleId) + : roles?.find((role) => role.slug === popUpData.role), metadata: popUpData.metadata }); } else { reset({ - role: roles[0].slug + role: isCustomOrgRole(currentOrg?.defaultMembershipRole!) + ? roles?.find((role) => role.id === currentOrg?.defaultMembershipRole) + : roles?.find((role) => role.slug === currentOrg?.defaultMembershipRole) }); } }, [popUp?.orgMembership?.data, roles]); @@ -91,7 +98,7 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg await updateOrgMembership({ organizationId: orgId, membershipId: popUpData.membershipId, - role, + role: role.slug, metadata }); @@ -123,23 +130,26 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg reset(); }} > - + ( + render={({ field: { onChange, value }, fieldState: { error } }) => ( - + value={value} + getOptionValue={(option) => option.slug} + getOptionLabel={(option) => option.name} + /> )} /> From bcc2840020c5335d022b6fd5a6b3fad13cfa3aea Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 13:13:23 -0800 Subject: [PATCH 06/10] improvement: filterable role selection on create/edit group --- .../src/ee/services/license/license-fns.ts | 2 +- frontend/src/helpers/roles.ts | 5 ++- .../OrgGroupsSection/OrgGroupModal.tsx | 44 +++++++++---------- .../IdentitySection/IdentityModal.tsx | 8 ++-- .../components/UserOrgMembershipModal.tsx | 11 ++--- 5 files changed, 32 insertions(+), 38 deletions(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 70c299564..accab79b5 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ oidcSSO: false, scim: false, ldap: false, - groups: false, + groups: true, status: null, trial_end: null, has_used_trial: true, diff --git a/frontend/src/helpers/roles.ts b/frontend/src/helpers/roles.ts index de6291a13..580b635b4 100644 --- a/frontend/src/helpers/roles.ts +++ b/frontend/src/helpers/roles.ts @@ -1,4 +1,4 @@ -import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { ProjectMembershipRole, TOrgRole } from "@app/hooks/api/roles/types"; enum OrgMembershipRole { Admin = "admin", @@ -23,3 +23,6 @@ export const formatProjectRoleName = (name: string) => { export const isCustomProjectRole = (slug: string) => !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); + +export const findOrgMembershipRole = (roles: TOrgRole[], role: string) => + isCustomOrgRole(role) ? roles.find((r) => r.id === role) : roles.find((r) => r.slug === role); diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx index 4ea4516de..b09c88763 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx @@ -6,14 +6,14 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, + FilterableSelect, FormControl, Input, Modal, - ModalContent, - Select, - SelectItem + ModalContent } from "@app/components/v2"; import { useOrganization } from "@app/context"; +import { findOrgMembershipRole } from "@app/helpers/roles"; import { useCreateGroup, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -23,7 +23,7 @@ const GroupFormSchema = z.object({ .string() .min(5, "Slug must be at least 5 characters long") .max(36, "Slug must be 36 characters or fewer"), - role: z.string() + role: z.object({ name: z.string(), slug: z.string() }) }); export type TGroupFormData = z.infer; @@ -62,13 +62,13 @@ export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Pr reset({ name: group.name, slug: group.slug, - role: group?.customRole?.slug ?? group.role + role: group?.customRole ?? findOrgMembershipRole(roles, group.role) }); } else { reset({ name: "", slug: "", - role: roles[0].slug + role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole) }); } }, [popUp?.group?.data, roles]); @@ -88,14 +88,14 @@ export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Pr id: group.groupId, name, slug, - role: role || undefined + role: role.slug || undefined }); } else { await createMutateAsync({ name, slug, organizationId: currentOrg.id, - role: role || undefined + role: role.slug || undefined }); } handlePopUpToggle("group", false); @@ -121,7 +121,10 @@ export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Pr reset(); }} > - + ( + render={({ field: { onChange, value }, fieldState: { error } }) => ( - + option.slug} + getOptionLabel={(option) => option.name} + /> )} /> diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index badab9a3c..d483d0ea7 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -18,7 +18,7 @@ import { ModalContent } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { isCustomOrgRole } from "@app/helpers/roles"; +import { findOrgMembershipRole } from "@app/helpers/roles"; import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; import { useAddIdentityUniversalAuth } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -98,15 +98,13 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { if (identity) { reset({ name: identity.name, - role: identity?.customRole ?? roles.find((role) => role.slug === identity.role), + role: identity.customRole ?? findOrgMembershipRole(roles, identity.role), metadata: identity.metadata }); } else { reset({ name: "", - role: isCustomOrgRole(currentOrg?.defaultMembershipRole!) - ? roles?.find((role) => role.id === currentOrg?.defaultMembershipRole) - : roles?.find((role) => role.slug === currentOrg?.defaultMembershipRole) + role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole) }); } }, [popUp?.identity?.data, roles]); diff --git a/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx b/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx index 9362881a4..289553ba8 100644 --- a/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx +++ b/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx @@ -18,7 +18,7 @@ import { ModalContent } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; -import { isCustomOrgRole } from "@app/helpers/roles"; +import { findOrgMembershipRole, isCustomOrgRole } from "@app/helpers/roles"; import { useGetOrgRoles, useUpdateOrgMembership } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -75,18 +75,13 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg if (!roles?.length) return; if (popUpData) { - console.log("roles", roles, popUpData.roleId); reset({ - role: popUpData.roleId - ? roles?.find((role) => role.id === popUpData.roleId) - : roles?.find((role) => role.slug === popUpData.role), + role: findOrgMembershipRole(roles, popUpData.roleId ?? popUpData.role), metadata: popUpData.metadata }); } else { reset({ - role: isCustomOrgRole(currentOrg?.defaultMembershipRole!) - ? roles?.find((role) => role.id === currentOrg?.defaultMembershipRole) - : roles?.find((role) => role.slug === currentOrg?.defaultMembershipRole) + role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole!) }); } }, [popUp?.orgMembership?.data, roles]); From 4c739fd57fd2ef65507d44632d5ab05e85607e28 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 13:36:42 -0800 Subject: [PATCH 07/10] chore: revert license --- backend/src/ee/services/license/license-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index accab79b5..70c299564 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ oidcSSO: false, scim: false, ldap: false, - groups: true, + groups: false, status: null, trial_end: null, has_used_trial: true, From 8afa65c272528dfda8f7c3ccb1a7f331afc88df2 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 13:47:09 -0800 Subject: [PATCH 08/10] improvements: minor refactoring --- frontend/src/helpers/roles.ts | 6 ++++-- .../components/OrgMembersSection/AddOrgMemberModal.tsx | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/src/helpers/roles.ts b/frontend/src/helpers/roles.ts index 580b635b4..4e26e1b15 100644 --- a/frontend/src/helpers/roles.ts +++ b/frontend/src/helpers/roles.ts @@ -24,5 +24,7 @@ export const formatProjectRoleName = (name: string) => { export const isCustomProjectRole = (slug: string) => !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); -export const findOrgMembershipRole = (roles: TOrgRole[], role: string) => - isCustomOrgRole(role) ? roles.find((r) => r.id === role) : roles.find((r) => r.slug === role); +export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) => + isCustomOrgRole(roleIdOrSlug) + ? roles.find((r) => r.id === roleIdOrSlug) + : roles.find((r) => r.slug === roleIdOrSlug); diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index 2b90276de..38faf53f1 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -15,7 +15,7 @@ import { TextArea } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { isCustomOrgRole } from "@app/helpers/roles"; +import { findOrgMembershipRole } from "@app/helpers/roles"; import { useAddUsersToOrg, useFetchServerStatus, @@ -87,9 +87,10 @@ export const AddOrgMemberModal = ({ useEffect(() => { if (organizationRoles) { reset({ - organizationRole: isCustomOrgRole(currentOrg?.defaultMembershipRole!) - ? organizationRoles?.find((role) => role.id === currentOrg?.defaultMembershipRole) - : organizationRoles?.find((role) => role.slug === currentOrg?.defaultMembershipRole) + organizationRole: findOrgMembershipRole( + organizationRoles, + currentOrg?.defaultMembershipRole! + ) }); } }, [organizationRoles]); From 429366513022da3ac6244c153598663d4f5fc361 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 14:10:45 -0800 Subject: [PATCH 09/10] improvement: user groups table pagination --- .../UserProjectsSection/UserGroupsTable.tsx | 147 ++++++++++++++---- 1 file changed, 121 insertions(+), 26 deletions(-) diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx index 15299da26..999ffe794 100644 --- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx @@ -1,6 +1,27 @@ -import { faFolder } from "@fortawesome/free-solid-svg-icons"; +import { useMemo } from "react"; +import { + faArrowDown, + faArrowUp, + faMagnifyingGlass, + faSearch, + faUser +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { EmptyState, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2"; +import { + EmptyState, + IconButton, + Input, + Pagination, + Table, + TableContainer, + TBody, + Th, + THead, + Tr +} from "@app/components/v2"; +import { usePagination, useResetPageHelper } from "@app/hooks"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrgUser } from "@app/hooks/api/types"; import { useListUserGroupMemberships } from "@app/hooks/api/users/queries"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -12,31 +33,105 @@ type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void; }; -export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => { - const { data: groups, isLoading } = useListUserGroupMemberships(orgMembership.user.username); +enum UserGroupsOrderBy { + Name = "name" +} +export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => { + const { data: groupMemberships = [], isLoading } = useListUserGroupMemberships( + orgMembership.user.username + ); + + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + toggleOrderDirection + } = usePagination(UserGroupsOrderBy.Name, { initPerPage: 10 }); + + const filteredGroupMemberships = useMemo( + () => + groupMemberships + ?.filter((group) => group.name.toLowerCase().includes(search.trim().toLowerCase())) + .sort((a, b) => { + const [membershipOne, membershipTwo] = + orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + return membershipOne.name.toLowerCase().localeCompare(membershipTwo.name.toLowerCase()); + }), + [groupMemberships, orderDirection, search] + ); + + useResetPageHelper({ + totalCount: filteredGroupMemberships.length, + offset, + setPage + }); return ( - - - - - - - - - {groups?.map((group) => ( - - ))} - -
Name -
- {!isLoading && !groups?.length && ( - - )} -
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search projects..." + /> + + + + + + + + + {filteredGroupMemberships.slice(offset, perPage * page).map((group) => ( + + ))} + +
+
+ Name + + + +
+
+
+ {Boolean(filteredGroupMemberships.length) && ( + + )} + {!isLoading && !filteredGroupMemberships?.length && ( + + )} +
+
); }; From dab8f0b2610f4d4cacf4691d62c40a7bd3eac676 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 28 Nov 2024 14:29:41 -0800 Subject: [PATCH 10/10] improvement: secret tags table pagination --- .../UserProjectsSection/UserGroupsTable.tsx | 5 +- .../SecretTagsSection/SecretTagsSection.tsx | 3 +- .../SecretTagsSection/SecretTagsTable.tsx | 175 +++++++++++++----- 3 files changed, 130 insertions(+), 53 deletions(-) diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx index 999ffe794..af136d7ff 100644 --- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx @@ -57,7 +57,7 @@ export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => { const filteredGroupMemberships = useMemo( () => groupMemberships - ?.filter((group) => group.name.toLowerCase().includes(search.trim().toLowerCase())) + .filter((group) => group.name.toLowerCase().includes(search.trim().toLowerCase())) .sort((a, b) => { const [membershipOne, membershipTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; @@ -72,13 +72,14 @@ export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => { offset, setPage }); + return (
setSearch(e.target.value)} leftIcon={} - placeholder="Search projects..." + placeholder="Search groups..." /> diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx index d1ba06835..26be8eb17 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx @@ -19,7 +19,6 @@ import { SecretTagsTable } from "./SecretTagsTable"; type DeleteModalData = { name: string; id: string }; export const SecretTagsSection = (): JSX.Element => { - const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([ "CreateSecretTag", "deleteTagConfirmation" @@ -65,7 +64,7 @@ export const SecretTagsSection = (): JSX.Element => { }} isDisabled={!isAllowed} > - Create tag + Create Tag )} diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx index cc68b0700..b6793ea1d 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx @@ -1,10 +1,20 @@ -import { faTags, faTrashCan } from "@fortawesome/free-solid-svg-icons"; +import { useMemo } from "react"; +import { + faArrowDown, + faArrowUp, + faMagnifyingGlass, + faSearch, + faTag, + faTrashCan +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ProjectPermissionCan } from "@app/components/permissions"; import { EmptyState, IconButton, + Input, + Pagination, Table, TableContainer, TableSkeleton, @@ -15,7 +25,9 @@ import { Tr } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetWsTags } from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -31,59 +43,124 @@ type Props = { ) => void; }; +enum TagsOrderBy { + Slug = "slug" +} + export const SecretTagsTable = ({ handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); - const { data, isLoading } = useGetWsTags(currentWorkspace?.id ?? ""); + const { data: tags = [], isLoading } = useGetWsTags(currentWorkspace?.id ?? ""); + + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + toggleOrderDirection + } = usePagination(TagsOrderBy.Slug, { initPerPage: 10 }); + + const filteredTags = useMemo( + () => + tags + .filter((tag) => tag.slug.toLowerCase().includes(search.trim().toLowerCase())) + .sort((a, b) => { + const [tagOne, tagTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + return tagOne.slug.toLowerCase().localeCompare(tagTwo.slug.toLowerCase()); + }), + [tags, orderDirection, search] + ); + + useResetPageHelper({ + totalCount: filteredTags.length, + offset, + setPage + }); return ( - -
- - - - - - - {isLoading && } - {!isLoading && - data && - data.map(({ id, slug }) => ( - - - - - ))} - {!isLoading && data && data?.length === 0 && ( +
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search tags..." + /> + +
Slug -
{slug} - - {(isAllowed) => ( - - handlePopUpOpen("deleteTagConfirmation", { - name: slug, - id - }) - } - colorSchema="danger" - ariaLabel="update" - isDisabled={!isAllowed} - > - - - )} - -
+ - + + - )} - -
- - +
+ Slug + + + +
+
-
+ + + {isLoading && } + {!isLoading && + filteredTags.slice(offset, perPage * page).map(({ id, slug }) => ( + + {slug} + + + {(isAllowed) => ( + + handlePopUpOpen("deleteTagConfirmation", { + name: slug, + id + }) + } + size="xs" + colorSchema="danger" + ariaLabel="update" + variant="plain" + isDisabled={!isAllowed} + > + + + )} + + + + ))} + + + {Boolean(filteredTags.length) && ( + + )} + {!isLoading && !filteredTags?.length && ( + + )} + +
); };