diff --git a/.gitignore b/.gitignore index fd008a493a..e76fd0c11b 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ yarn-error.log* # Editor specific .vscode/* +.idea/* frontend-build diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b08c70d939..52ae765c64 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -363,7 +363,12 @@ export const ORGANIZATIONS = { membershipId: "The ID of the membership to delete." }, LIST_IDENTITY_MEMBERSHIPS: { - orgId: "The ID of the organization to get identity memberships from." + orgId: "The ID of the organization to get identity memberships from.", + offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.", + limit: "The number of identity memberships to return.", + orderBy: "The column to order identity memberships by.", + direction: "The direction identity memberships will be sorted in.", + textFilter: "The text string that identity membership names will be filtered by." }, GET_PROJECTS: { organizationId: "The ID of the organization to get projects from." @@ -470,7 +475,12 @@ export const PROJECT_USERS = { export const PROJECT_IDENTITIES = { LIST_IDENTITY_MEMBERSHIPS: { - projectId: "The ID of the project to get identity memberships from." + projectId: "The ID of the project to get identity memberships from.", + offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.", + limit: "The number of identity memberships to return.", + orderBy: "The column to order identity memberships by.", + direction: "The direction identity memberships will be sorted in.", + textFilter: "The text string that identity membership names will be filtered by." }, GET_IDENTITY_MEMBERSHIP_BY_ID: { identityId: "The ID of the identity to get the membership for.", diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index 4d892b02c5..604ec3dd5c 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -52,3 +52,8 @@ export enum SecretSharingAccessType { Anyone = "anyone", Organization = "organization" } + +export enum OrderByDirection { + ASC = "asc", + DESC = "desc" +} diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 23aca96258..cf72ba2fff 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -246,12 +246,13 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { description: true }).optional(), identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) - }).array() + }).array(), + totalCount: z.number() }) } }, handler: async (req) => { - const identities = await server.services.identity.listOrgIdentities({ + const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -259,7 +260,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { orgId: req.query.orgId }); - return { identities }; + return { identities: identityMemberships, totalCount }; } }); diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index aab84ef8df..8779757cd6 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -2,9 +2,11 @@ import { z } from "zod"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; import { ORGANIZATIONS } from "@app/lib/api-docs"; +import { OrderByDirection } from "@app/lib/types"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { OrgIdentityOrderBy } from "@app/services/identity/identity-types"; export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { server.route({ @@ -24,6 +26,32 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { params: z.object({ orgId: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orgId) }), + querystring: z.object({ + offset: z.coerce.number().min(0).default(0).describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.offset).optional(), + limit: z.coerce + .number() + .min(1) + .max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added + .default(100) + .describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.limit) + .optional(), + orderBy: z + .nativeEnum(OrgIdentityOrderBy) + .default(OrgIdentityOrderBy.Name) + .describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy) + .optional(), + direction: z + .nativeEnum(OrderByDirection) + .default(OrderByDirection.ASC) + .describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.direction) + .optional(), + textFilter: z + .string() + .trim() + .default("") + .describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.textFilter) + .optional() + }), response: { 200: z.object({ identityMemberships: IdentityOrgMembershipsSchema.merge( @@ -37,20 +65,26 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { }).optional(), identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) }) - ).array() + ).array(), + totalCount: z.number() }) } }, handler: async (req) => { - const identityMemberships = await server.services.identity.listOrgIdentities({ + const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - orgId: req.params.orgId + orgId: req.params.orgId, + limit: req.query.limit, + offset: req.query.offset, + orderBy: req.query.orderBy, + direction: req.query.direction, + textFilter: req.query.textFilter }); - return { identityMemberships }; + return { identityMemberships, totalCount }; } }); }; diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index 01b51b3154..58b1653393 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -7,11 +7,13 @@ import { ProjectMembershipRole, ProjectUserMembershipRolesSchema } from "@app/db/schemas"; -import { PROJECT_IDENTITIES } from "@app/lib/api-docs"; +import { ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; +import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types"; import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; import { SanitizedProjectSchema } from "../sanitizedSchemas"; @@ -214,6 +216,37 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) params: z.object({ projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId) }), + querystring: z.object({ + offset: z.coerce + .number() + .min(0) + .default(0) + .describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.offset) + .optional(), + limit: z.coerce + .number() + .min(1) + .max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added + .default(100) + .describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.limit) + .optional(), + orderBy: z + .nativeEnum(ProjectIdentityOrderBy) + .default(ProjectIdentityOrderBy.Name) + .describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy) + .optional(), + direction: z + .nativeEnum(OrderByDirection) + .default(OrderByDirection.ASC) + .describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.direction) + .optional(), + textFilter: z + .string() + .trim() + .default("") + .describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.textFilter) + .optional() + }), response: { 200: z.object({ identityMemberships: z @@ -239,19 +272,25 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), project: SanitizedProjectSchema.pick({ name: true, id: true }) }) - .array() + .array(), + totalCount: z.number() }) } }, handler: async (req) => { - const identityMemberships = await server.services.identityProject.listProjectIdentities({ + const { identityMemberships, totalCount } = await server.services.identityProject.listProjectIdentities({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.projectId + projectId: req.params.projectId, + limit: req.query.limit, + offset: req.query.offset, + orderBy: req.query.orderBy, + direction: req.query.direction, + textFilter: req.query.textFilter }); - return { identityMemberships }; + return { identityMemberships, totalCount }; } }); diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index 76618889b9..c5327b7eb8 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, sqlNestRelationships } from "@app/lib/knex"; +import { TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types"; export type TIdentityProjectDALFactory = ReturnType; @@ -107,9 +108,16 @@ export const identityProjectDALFactory = (db: TDbClient) => { } }; - const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => { + const findByProjectId = async ( + projectId: string, + filter: { identityId?: string } & Pick< + TListProjectIdentityDTO, + "limit" | "offset" | "textFilter" | "orderBy" | "direction" + > = {}, + tx?: Knex + ) => { try { - const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) + const query = (tx || db.replicaNode())(TableName.IdentityProjectMembership) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) @@ -117,6 +125,10 @@ export const identityProjectDALFactory = (db: TDbClient) => { if (filter.identityId) { void qb.where("identityId", filter.identityId); } + + if (filter.textFilter) { + void qb.whereILike(`${TableName.Identity}.name`, `%${filter.textFilter}%`); + } }) .join( TableName.IdentityProjectMembershipRole, @@ -154,6 +166,22 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("name").as("projectName").withSchema(TableName.Project) ); + if (filter.limit) { + void query.offset(filter.offset ?? 0).limit(filter.limit); + } + + if (filter.orderBy) { + switch (filter.orderBy) { + case "name": + void query.orderBy(`${TableName.Identity}.${filter.orderBy}`, filter.direction); + break; + default: + // do nothing + } + } + + const docs = await query; + const members = sqlNestRelationships({ data: docs, parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({ @@ -208,9 +236,36 @@ export const identityProjectDALFactory = (db: TDbClient) => { } }; + const getCountByProjectId = async ( + projectId: string, + filter: { identityId?: string } & Pick = {}, + tx?: Knex + ) => { + try { + const identities = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) + .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) + .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) + .where((qb) => { + if (filter.identityId) { + void qb.where("identityId", filter.identityId); + } + + if (filter.textFilter) { + void qb.whereILike(`${TableName.Identity}.name`, `%${filter.textFilter}%`); + } + }); + + return identities.length; + } catch (error) { + throw new DatabaseError({ error, name: "GetCountByProjectId" }); + } + }; + return { ...identityProjectOrm, findByIdentityId, - findByProjectId + findByProjectId, + getCountByProjectId }; }; diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 10f2b34601..927790d12c 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -268,7 +268,12 @@ export const identityProjectServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId + actorOrgId, + limit, + offset, + orderBy, + direction, + textFilter }: TListProjectIdentityDTO) => { const { permission } = await permissionService.getProjectPermission( actor, @@ -279,8 +284,17 @@ export const identityProjectServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); - const identityMemberships = await identityProjectDAL.findByProjectId(projectId); - return identityMemberships; + const identityMemberships = await identityProjectDAL.findByProjectId(projectId, { + limit, + offset, + orderBy, + direction, + textFilter + }); + + const totalCount = await identityProjectDAL.getCountByProjectId(projectId, { textFilter }); + + return { identityMemberships, totalCount }; }; const getProjectIdentityByIdentityId = async ({ diff --git a/backend/src/services/identity-project/identity-project-types.ts b/backend/src/services/identity-project/identity-project-types.ts index 43c671e50e..0c834fc0e4 100644 --- a/backend/src/services/identity-project/identity-project-types.ts +++ b/backend/src/services/identity-project/identity-project-types.ts @@ -1,4 +1,4 @@ -import { TProjectPermission } from "@app/lib/types"; +import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; @@ -40,8 +40,18 @@ export type TDeleteProjectIdentityDTO = { identityId: string; } & TProjectPermission; -export type TListProjectIdentityDTO = TProjectPermission; +export type TListProjectIdentityDTO = { + limit?: number; + offset?: number; + orderBy?: ProjectIdentityOrderBy; + direction?: OrderByDirection; + textFilter?: string; +} & TProjectPermission; export type TGetProjectIdentityByIdentityIdDTO = { identityId: string; } & TProjectPermission; + +export enum ProjectIdentityOrderBy { + Name = "name" +} diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 104b917e70..c3a54736ca 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -4,6 +4,8 @@ import { TDbClient } from "@app/db"; import { TableName, TIdentityOrgMemberships } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; +import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types"; export type TIdentityOrgDALFactory = ReturnType; @@ -27,9 +29,20 @@ export const identityOrgDALFactory = (db: TDbClient) => { } }; - const find = async (filter: Partial, tx?: Knex) => { + const find = async ( + { + limit, + offset = 0, + orderBy, + direction = OrderByDirection.ASC, + textFilter, + ...filter + }: Partial & + Pick, + tx?: Knex + ) => { try { - const docs = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) + const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) .where(filter) .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) @@ -44,6 +57,30 @@ export const identityOrgDALFactory = (db: TDbClient) => { .select(db.ref("id").as("identityId").withSchema(TableName.Identity)) .select(db.ref("name").as("identityName").withSchema(TableName.Identity)) .select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity)); + + if (limit) { + void query.offset(offset).limit(limit); + } + + if (orderBy) { + switch (orderBy) { + case "name": + void query.orderBy(`${TableName.Identity}.${orderBy}`, direction); + break; + case "role": + void query.orderBy(`${TableName.IdentityOrgMembership}.${orderBy}`, direction); + break; + default: + // do nothing + } + } + + if (textFilter?.length) { + void query.whereILike(`${TableName.Identity}.name`, `%${textFilter}%`); + } + + const docs = await query; + return docs.map( ({ crId, @@ -79,5 +116,26 @@ export const identityOrgDALFactory = (db: TDbClient) => { } }; - return { ...identityOrgOrm, find, findOne }; + const countAllOrgIdentities = async ( + { textFilter, ...filter }: Partial & Pick, + tx?: Knex + ) => { + try { + const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) + .where(filter) + .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`); + + if (textFilter?.length) { + void query.whereILike(`${TableName.Identity}.name`, `%${textFilter}%`); + } + + const identities = await query; + + return identities.length; + } catch (error) { + throw new DatabaseError({ error, name: "countAllOrgIdentities" }); + } + }; + + return { ...identityOrgOrm, find, findOne, countAllOrgIdentities }; }; diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 78dcdda4e7..f31c5cebea 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -6,7 +6,6 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { TOrgPermission } from "@app/lib/types"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { ActorType } from "../auth/auth-type"; @@ -16,6 +15,7 @@ import { TCreateIdentityDTO, TDeleteIdentityDTO, TGetIdentityByIdDTO, + TListOrgIdentitiesByOrgIdDTO, TListProjectIdentitiesByIdentityIdDTO, TUpdateIdentityDTO } from "./identity-types"; @@ -195,14 +195,36 @@ export const identityServiceFactory = ({ return { ...deletedIdentity, orgId: identityOrgMembership.orgId }; }; - const listOrgIdentities = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPermission) => { + const listOrgIdentities = async ({ + orgId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + limit, + offset, + orderBy, + direction, + textFilter + }: TListOrgIdentitiesByOrgIdDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityOrgMembershipDAL.find({ - [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, + limit, + offset, + orderBy, + direction, + textFilter }); - return identityMemberships; + + const totalCount = await identityOrgMembershipDAL.countAllOrgIdentities({ + [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, + textFilter + }); + + return { identityMemberships, totalCount }; }; const listProjectIdentitiesByIdentityId = async ({ diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index 52df905cea..9de5d8f35c 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -1,5 +1,5 @@ import { IPType } from "@app/lib/ip"; -import { TOrgPermission } from "@app/lib/types"; +import { OrderByDirection, TOrgPermission } from "@app/lib/types"; export type TCreateIdentityDTO = { role: string; @@ -29,3 +29,16 @@ export interface TIdentityTrustedIp { export type TListProjectIdentitiesByIdentityIdDTO = { identityId: string; } & Omit; + +export type TListOrgIdentitiesByOrgIdDTO = { + limit?: number; + offset?: number; + orderBy?: OrgIdentityOrderBy; + direction?: OrderByDirection; + textFilter?: string; +} & TOrgPermission; + +export enum OrgIdentityOrderBy { + Name = "name", + Role = "role" +} diff --git a/frontend/src/components/v2/Input/Input.tsx b/frontend/src/components/v2/Input/Input.tsx index fb5b36a9e8..d325719e6c 100644 --- a/frontend/src/components/v2/Input/Input.tsx +++ b/frontend/src/components/v2/Input/Input.tsx @@ -11,6 +11,7 @@ type Props = { isDisabled?: boolean; isReadOnly?: boolean; autoCapitalization?: boolean; + containerClassName?: string; }; const inputVariants = cva( @@ -71,6 +72,7 @@ export const Input = forwardRef( ( { className, + containerClassName, isRounded = true, isFullWidth = true, isDisabled, @@ -94,7 +96,15 @@ export const Input = forwardRef( }; return ( -
+
{leftIcon && {leftIcon}}
- {(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count} + {(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index a944493c93..697f0ed82a 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -469,3 +469,8 @@ export type RevokeTokenDTO = { export type RevokeTokenRes = { message: string; }; + +export type TProjectIdentitiesList = { + identityMemberships: IdentityMembership[]; + totalCount: number; +}; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 5c57428dfa..53e44092be 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -1,9 +1,8 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { TGroupOrgMembership } from "../groups/types"; -import { IdentityMembershipOrg } from "../identities/types"; import { BillingDetails, Invoice, @@ -14,6 +13,8 @@ import { PmtMethod, ProductsTable, TaxID, + TListOrgIdentitiesDTO, + TOrgIdentitiesList, UpdateOrgDTO } from "./types"; @@ -30,6 +31,12 @@ export const organizationKeys = { getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const, getOrgIdentityMemberships: (orgId: string) => [{ orgId }, "organization-identity-memberships"] as const, + // allows invalidation using above key without knowing params + getOrgIdentityMembershipsWithParams: ({ + organizationId: orgId, + ...params + }: TListOrgIdentitiesDTO) => + [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const }; @@ -360,19 +367,51 @@ export const useGetOrgLicenses = (organizationId: string) => { }); }; -export const useGetIdentityMembershipOrgs = (organizationId: string) => { +export const useGetIdentityMembershipOrgs = ( + { + organizationId, + offset = 0, + limit = 100, + orderBy = "name", + direction = "asc", + textFilter = "" + }: TListOrgIdentitiesDTO, + options?: Omit< + UseQueryOptions< + TOrgIdentitiesList, + unknown, + TOrgIdentitiesList, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit), + orderBy: String(orderBy), + direction: String(direction), + textFilter: String(textFilter) + }); return useQuery({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId), + queryKey: organizationKeys.getOrgIdentityMembershipsWithParams({ + organizationId, + offset, + limit, + orderBy, + direction, + textFilter + }), queryFn: async () => { - const { - data: { identityMemberships } - } = await apiRequest.get<{ identityMemberships: IdentityMembershipOrg[] }>( - `/api/v2/organizations/${organizationId}/identity-memberships` + const { data } = await apiRequest.get( + `/api/v2/organizations/${organizationId}/identity-memberships`, + { params } ); - return identityMemberships; + return data; }, - enabled: true + enabled: true, + ...options }); }; diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 9c26ce02ba..56dcb22e70 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -1,3 +1,5 @@ +import { IdentityMembershipOrg } from "@app/hooks/api/identities/types"; + export type Organization = { id: string; name: string; @@ -102,3 +104,17 @@ export type ProductsTable = { head: ProductsTableHead[]; rows: ProductsTableRow[]; }; + +export type TListOrgIdentitiesDTO = { + organizationId: string; + offset?: number; + limit?: number; + orderBy?: string; + direction?: string; + textFilter?: string; +}; + +export type TOrgIdentitiesList = { + identityMemberships: IdentityMembershipOrg[]; + totalCount: number; +}; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 0557e5f6f6..bccb4b23d7 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -1,4 +1,4 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; @@ -8,7 +8,7 @@ import { TCertificate } from "../certificates/types"; import { TCertificateTemplate } from "../certificateTemplates/types"; import { TGroupMembership } from "../groups/types"; import { identitiesKeys } from "../identities/queries"; -import { IdentityMembership } from "../identities/types"; +import { TProjectIdentitiesList } from "../identities/types"; import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; import { TPkiAlert } from "../pkiAlerts/types"; @@ -25,6 +25,7 @@ import { NameWorkspaceSecretsDTO, RenameWorkspaceDTO, TGetUpgradeProjectStatusDTO, + TListProjectIdentitiesDTO, ToggleAutoCapitalizationDTO, TUpdateWorkspaceIdentityRoleDTO, TUpdateWorkspaceUserRoleDTO, @@ -49,6 +50,12 @@ export const workspaceKeys = { getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const, getWorkspaceIdentityMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-identity-memberships"] as const, + // allows invalidation using above key without knowing params + getWorkspaceIdentityMembershipsWithParams: ({ + workspaceId, + ...params + }: TListProjectIdentitiesDTO) => + [...workspaceKeys.getWorkspaceIdentityMemberships(workspaceId), params] as const, getWorkspaceGroupMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-groups"] as const, getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) => @@ -526,18 +533,51 @@ export const useDeleteIdentityFromWorkspace = () => { }); }; -export const useGetWorkspaceIdentityMemberships = (workspaceId: string) => { +export const useGetWorkspaceIdentityMemberships = ( + { + workspaceId, + offset = 0, + limit = 100, + orderBy = "name", + direction = "asc", + textFilter = "" + }: TListProjectIdentitiesDTO, + options?: Omit< + UseQueryOptions< + TProjectIdentitiesList, + unknown, + TProjectIdentitiesList, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId), + queryKey: workspaceKeys.getWorkspaceIdentityMembershipsWithParams({ + workspaceId, + offset, + limit, + orderBy, + direction, + textFilter + }), queryFn: async () => { - const { - data: { identityMemberships } - } = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>( - `/api/v2/workspace/${workspaceId}/identity-memberships` + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit), + orderBy: String(orderBy), + direction: String(direction), + textFilter: String(textFilter) + }); + + const { data } = await apiRequest.get( + `/api/v2/workspace/${workspaceId}/identity-memberships`, + { params } ); - return identityMemberships; + return data; }, - enabled: true + enabled: true, + ...options }); }; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index efb31b330b..42f5322212 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -141,3 +141,12 @@ export type TUpdateWorkspaceGroupRoleDTO = { } )[]; }; + +export type TListProjectIdentitiesDTO = { + workspaceId: string; + offset?: number; + limit?: number; + orderBy?: string; + direction?: string; + textFilter?: string; +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index 9d23a6d717..0bdbefdcd8 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -1,5 +1,12 @@ +import { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import { faEllipsis, faServer } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowDown, + faArrowUp, + faEllipsis, + faMagnifyingGlass, + faServer +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; @@ -11,8 +18,12 @@ import { DropdownMenuItem, DropdownMenuTrigger, EmptyState, + IconButton, + Input, + Pagination, Select, SelectItem, + Spinner, Table, TableContainer, TableSkeleton, @@ -23,6 +34,7 @@ import { Tr } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { useDebounce } from "@app/hooks"; import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -36,22 +48,58 @@ type Props = { ) => void; }; +const INIT_PER_PAGE = 10; + export const IdentityTable = ({ handlePopUpOpen }: Props) => { const router = useRouter(); const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(INIT_PER_PAGE); + const [direction, setDirection] = useState("asc"); + const [orderBy, setOrderBy] = useState("name"); + const [textFilter, setTextFilter] = useState(""); + const debouncedTextFilter = useDebounce(textFilter); + + const organizationId = currentOrg?.id || ""; const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); - const { data, isLoading } = useGetIdentityMembershipOrgs(orgId); - const { data: roles } = useGetOrgRoles(orgId); + const offset = (page - 1) * perPage; + const { data, isLoading, isFetching } = useGetIdentityMembershipOrgs( + { + organizationId, + offset, + limit: perPage, + direction, + orderBy, + textFilter: debouncedTextFilter + }, + { keepPreviousData: true } + ); + + useEffect(() => { + // reset page if no longer valid + if (data && data.totalCount < offset) setPage(1); + }, [data?.totalCount]); + + const { data: roles } = useGetOrgRoles(organizationId); + + const handleSort = (column: string) => { + if (column === orderBy) { + setDirection((prev) => (prev === "asc" ? "desc" : "asc")); + return; + } + + setOrderBy(column); + setDirection("asc"); + }; const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => { try { await updateMutateAsync({ identityId, role, - organizationId: orgId + organizationId }); createNotification({ @@ -71,124 +119,170 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { }; return ( - - - - - - - - - - {isLoading && } - {!isLoading && - data?.map(({ identity: { id, name }, role, customRole }) => { - return ( - router.push(`/org/${orgId}/identities/${id}`)} - > - - - - - ); - })} - {!isLoading && data && data?.length === 0 && ( - - +
+ setTextFilter(e.target.value)} + leftIcon={} + placeholder="Search identities by name..." + /> + +
NameRole -
{name} - - {(isAllowed) => { - return ( - - ); - }} - - - - -
- -
-
- - - {(isAllowed) => ( - { - e.stopPropagation(); - router.push(`/org/${orgId}/identities/${id}`); - }} - disabled={!isAllowed} - > - Edit Identity - - )} - - - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("deleteIdentity", { - identityId: id, - name - }); - }} - disabled={!isAllowed} - > - Delete Identity - - )} - - -
-
- -
+ + + + + - )} - -
+
+ Name + handleSort("name")} + > + + +
+
+
+ Role + handleSort("role")} + > + + +
+
{isFetching ? : null}
-
+ + + {isLoading && } + {!isLoading && + data?.identityMemberships.map(({ identity: { id, name }, role, customRole }) => { + return ( + router.push(`/org/${organizationId}/identities/${id}`)} + > + {name} + + + {(isAllowed) => { + return ( + + ); + }} + + + + + +
+ +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + router.push(`/org/${organizationId}/identities/${id}`); + }} + disabled={!isAllowed} + > + Edit Identity + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteIdentity", { + identityId: id, + name + }); + }} + disabled={!isAllowed} + > + Delete Identity + + )} + + +
+ + + ); + })} + + + {!isLoading && data && data.totalCount > INIT_PER_PAGE && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isLoading && data && data?.identityMemberships.length === 0 && ( + 0 + ? "No identities match search filter" + : "No identities have been created in this organization" + } + icon={faServer} + /> + )} + +
); }; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx index 2ab1ebeecd..17c3849fb6 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx @@ -1,11 +1,15 @@ +import { useEffect, useState } from "react"; import Link from "next/link"; import { - faArrowUpRightFromSquare, - faClock, - faEdit, - faPlus, - faServer, - faXmark + faArrowDown, + faArrowUp, + faArrowUpRightFromSquare, + faClock, + faEdit, + faMagnifyingGlass, + faPlus, + faServer, + faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; @@ -15,28 +19,32 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { - Button, - DeleteActionModal, - EmptyState, - HoverCard, - HoverCardContent, - HoverCardTrigger, - IconButton, - Modal, - ModalContent, - Table, - TableContainer, - TableSkeleton, - Tag, - TBody, - Td, - Th, - THead, - Tooltip, - Tr + Button, + DeleteActionModal, + EmptyState, + HoverCard, + HoverCardContent, + HoverCardTrigger, + IconButton, + Input, + Modal, + ModalContent, + Pagination, + Spinner, + Table, + TableContainer, + TableSkeleton, + Tag, + TBody, + Td, + Th, + THead, + Tooltip, + Tr } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { withProjectPermission } from "@app/hoc"; +import { useDebounce } from "@app/hooks"; import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api"; import { IdentityMembership } from "@app/hooks/api/identities/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; @@ -46,306 +54,374 @@ import { IdentityModal } from "./components/IdentityModal"; import { IdentityRoleForm } from "./components/IdentityRoleForm"; const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; +const INIT_PER_PAGE = 10; const formatRoleName = (role: string, customRoleName?: string) => { - if (role === ProjectMembershipRole.Custom) return customRoleName; - if (role === ProjectMembershipRole.Member) return "Developer"; - if (role === ProjectMembershipRole.NoAccess) return "No access"; - return role; + if (role === ProjectMembershipRole.Custom) return customRoleName; + if (role === ProjectMembershipRole.Member) return "Developer"; + if (role === ProjectMembershipRole.NoAccess) return "No access"; + return role; }; export const IdentityTab = withProjectPermission( - () => { - const { currentWorkspace } = useWorkspace(); + () => { + const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id ?? ""; + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(INIT_PER_PAGE); + const [direction, setDirection] = useState("asc"); + const [orderBy, setOrderBy] = useState("name"); + const [textFilter, setTextFilter] = useState(""); + const debouncedTextFilter = useDebounce(textFilter); - const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?.id || ""); - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace(); + const workspaceId = currentWorkspace?.id ?? ""; - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "identity", - "deleteIdentity", - "upgradePlan", - "updateRole" - ] as const); + const offset = (page - 1) * perPage; + const { data, isLoading, isFetching } = useGetWorkspaceIdentityMemberships( + { + workspaceId: currentWorkspace?.id || "", + offset, + limit: perPage, + direction, + orderBy, + textFilter: debouncedTextFilter + }, + { keepPreviousData: true } + ); + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace(); - const onRemoveIdentitySubmit = async (identityId: string) => { - try { - await deleteMutateAsync({ - identityId, - workspaceId - }); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "identity", + "deleteIdentity", + "upgradePlan", + "updateRole" + ] as const); - createNotification({ - text: "Successfully removed identity from project", - type: "success" - }); + const onRemoveIdentitySubmit = async (identityId: string) => { + try { + await deleteMutateAsync({ + identityId, + workspaceId + }); - handlePopUpClose("deleteIdentity"); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to remove identity from project"; + createNotification({ + text: "Successfully removed identity from project", + type: "success" + }); - createNotification({ - text, - type: "error" - }); - } - }; + handlePopUpClose("deleteIdentity"); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to remove identity from project"; - return ( - { + // reset page if no longer valid + if (data && data.totalCount < offset) setPage(1); + }, [data?.totalCount]); + + const handleSort = (column: string) => { + if (column === orderBy) { + setDirection((prev) => (prev === "asc" ? "desc" : "asc")); + return; + } + + setOrderBy(column); + setDirection("asc"); + }; + + return ( + +
+
+

Identities

+
+ + + Documentation{" "} + + + +
+ -
-
-

Identities

-
- - - Documentation{" "} - - - -
- - {(isAllowed) => ( - - )} - + {(isAllowed) => ( + + )} + +
+ setTextFilter(e.target.value)} + leftIcon={} + placeholder="Search identities by name..." + /> + + + + + + + + ); + })} + +
+
+ Name + handleSort("name")} + > + +
- - - - - - - - - - - {isLoading && } - {!isLoading && - data && - data.length > 0 && - data.map((identityMember, index) => { - const { - identity: { id, name }, - roles, - createdAt - } = identityMember; - return ( - - + + + + + + + + {isLoading && } + {!isLoading && + data && + data.identityMemberships.length > 0 && + data.identityMemberships.map((identityMember, index) => { + const { + identity: { id, name }, + roles, + createdAt + } = identityMember; + return ( + + - - - - + - - - )} - -
NameRoleAdded on -
{name}RoleAdded on{isFetching ? : null}
{name} -
- {roles - .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) - .map( - ({ - role, - customRoleName, - id: roleId, - isTemporary, - temporaryAccessEndTime - }) => { - const isExpired = - new Date() > new Date(temporaryAccessEndTime || ("" as string)); - return ( - -
-
- {formatRoleName(role, customRoleName)} -
- {isTemporary && ( -
- - - -
- )} -
-
- ); - } - )} - {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && ( - - - +{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE} - - - {roles - .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE) - .map( - ({ - role, - customRoleName, - id: roleId, - isTemporary, - temporaryAccessEndTime - }) => { - const isExpired = - new Date() > - new Date(temporaryAccessEndTime || ("" as string)); - return ( - -
-
{formatRoleName(role, customRoleName)}
- {isTemporary && ( -
- - - new Date( - temporaryAccessEndTime as string - ) && "text-red-600" - )} - /> - -
- )} -
-
- ); - } - )} -
-
- )} - - - handlePopUpOpen("updateRole", { ...identityMember, index }) - } - > - - - -
-
{format(new Date(createdAt), "yyyy-MM-dd")} - - {(isAllowed) => ( - { - handlePopUpOpen("deleteIdentity", { - identityId: id, - name - }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - )} - -
+
+ {roles + .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) + .map( + ({ + role, + customRoleName, + id: roleId, + isTemporary, + temporaryAccessEndTime + }) => { + const isExpired = + new Date() > new Date(temporaryAccessEndTime || ("" as string)); + return ( + +
+
+ {formatRoleName(role, customRoleName)} +
+ {isTemporary && ( +
+ + + +
+ )} +
+
+ ); + } + )} + {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && ( + + + +{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE} + + + {roles + .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE) + .map( + ({ + role, + customRoleName, + id: roleId, + isTemporary, + temporaryAccessEndTime + }) => { + const isExpired = + new Date() > + new Date(temporaryAccessEndTime || ("" as string)); + return ( + +
+
{formatRoleName(role, customRoleName)}
+ {isTemporary && ( +
+ + + new Date( + temporaryAccessEndTime as string + ) && "text-red-600" + )} + /> + +
+ )} +
+
); - })} - {!isLoading && data && data?.length === 0 && ( -
- -
-
- handlePopUpToggle("updateRole", state)} - > - + + )} + + + handlePopUpOpen("updateRole", { ...identityMember, index }) + } + > + + + + + +
{format(new Date(createdAt), "yyyy-MM-dd")} + + {(isAllowed) => ( + { + handlePopUpOpen("deleteIdentity", { + identityId: id, + name + }); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + )} + +
+ {!isLoading && data && data.totalCount > INIT_PER_PAGE && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isLoading && data && data?.identityMemberships.length === 0 && ( + 0 + ? "No identities match search filter" + : "No identities have been added to this project" + } + icon={faServer} + /> + )} +
+ handlePopUpToggle("updateRole", state)} + > + - - handlePopUpOpen("upgradePlan", { description }) - } - identityProjectMember={ - data?.[ - (popUp.updateRole?.data as IdentityMembership & { index: number })?.index - ] as IdentityMembership - } - /> - - - - handlePopUpToggle("deleteIdentity", isOpen)} - deleteKey="confirm" - onDeleteApproved={() => - onRemoveIdentitySubmit( - (popUp?.deleteIdentity?.data as { identityId: string })?.identityId - ) - } - /> -
- - ); - }, - { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity } + > + + handlePopUpOpen("upgradePlan", { description }) + } + identityProjectMember={ + data?.identityMemberships[ + (popUp.updateRole?.data as IdentityMembership & { index: number })?.index + ] as IdentityMembership + } + /> + + + + handlePopUpToggle("deleteIdentity", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveIdentitySubmit( + (popUp?.deleteIdentity?.data as { identityId: string })?.identityId + ) + } + /> +
+ + ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity } ); diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx index 5637b2f1a2..d8c5545241 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx @@ -5,7 +5,15 @@ import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; +import { + Button, + FormControl, + Modal, + ModalClose, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; import { useOrganization, useWorkspace } from "@app/context"; import { useAddIdentityToWorkspace, @@ -33,12 +41,20 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentOrg } = useOrganization(); const { currentWorkspace } = useWorkspace(); - const orgId = currentOrg?.id || ""; + const organizationId = currentOrg?.id || ""; const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || ""; - const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId); - const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId); + const { data: identityMembershipOrgsData } = useGetIdentityMembershipOrgs({ + organizationId, + limit: 20000 // TODO: this is temp to preserve functionality for bitcoindepot, will replace with combobox in separate PR + }); + const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships; + const { data: identityMembershipsData } = useGetWorkspaceIdentityMemberships({ + workspaceId, + limit: 20000 // TODO: this is temp to preserve functionality for bitcoindepot, will optimize in PR referenced above + }); + const identityMemberships = identityMembershipsData?.identityMemberships; const { data: roles } = useGetProjectRoles(projectSlug); @@ -158,9 +174,11 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { > {popUp?.identity?.data ? "Update" : "Create"} - + + +
) : (