mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 04:27:29 +00:00
Merge pull request #2409 from scott-ray-wilson/identity-pagination
Feature: Project and Org Identities Table Additions: Pagination, Search and Sort
This commit is contained in:
@@ -63,6 +63,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Editor specific
|
# Editor specific
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
.idea/*
|
||||||
|
|
||||||
frontend-build
|
frontend-build
|
||||||
|
|
||||||
|
|||||||
@@ -363,7 +363,12 @@ export const ORGANIZATIONS = {
|
|||||||
membershipId: "The ID of the membership to delete."
|
membershipId: "The ID of the membership to delete."
|
||||||
},
|
},
|
||||||
LIST_IDENTITY_MEMBERSHIPS: {
|
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.",
|
||||||
|
orderDirection: "The direction identity memberships will be sorted in.",
|
||||||
|
search: "The text string that identity membership names will be filtered by."
|
||||||
},
|
},
|
||||||
GET_PROJECTS: {
|
GET_PROJECTS: {
|
||||||
organizationId: "The ID of the organization to get projects from."
|
organizationId: "The ID of the organization to get projects from."
|
||||||
@@ -472,7 +477,12 @@ export const PROJECT_USERS = {
|
|||||||
|
|
||||||
export const PROJECT_IDENTITIES = {
|
export const PROJECT_IDENTITIES = {
|
||||||
LIST_IDENTITY_MEMBERSHIPS: {
|
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.",
|
||||||
|
orderDirection: "The direction identity memberships will be sorted in.",
|
||||||
|
search: "The text string that identity membership names will be filtered by."
|
||||||
},
|
},
|
||||||
GET_IDENTITY_MEMBERSHIP_BY_ID: {
|
GET_IDENTITY_MEMBERSHIP_BY_ID: {
|
||||||
identityId: "The ID of the identity to get the membership for.",
|
identityId: "The ID of the identity to get the membership for.",
|
||||||
|
|||||||
@@ -52,3 +52,8 @@ export enum SecretSharingAccessType {
|
|||||||
Anyone = "anyone",
|
Anyone = "anyone",
|
||||||
Organization = "organization"
|
Organization = "organization"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrderByDirection {
|
||||||
|
ASC = "asc",
|
||||||
|
DESC = "desc"
|
||||||
|
}
|
||||||
|
|||||||
@@ -246,12 +246,13 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: true
|
description: true
|
||||||
}).optional(),
|
}).optional(),
|
||||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||||
}).array()
|
}).array(),
|
||||||
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const identities = await server.services.identity.listOrgIdentities({
|
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@@ -259,7 +260,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
orgId: req.query.orgId
|
orgId: req.query.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { identities };
|
return { identities: identityMemberships, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { OrgIdentityOrderBy } from "@app/services/identity/identity-types";
|
||||||
|
|
||||||
export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -24,6 +26,27 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
orgId: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orgId)
|
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(),
|
||||||
|
orderDirection: z
|
||||||
|
.nativeEnum(OrderByDirection)
|
||||||
|
.default(OrderByDirection.ASC)
|
||||||
|
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
|
||||||
|
.optional(),
|
||||||
|
search: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.search).optional()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityMemberships: IdentityOrgMembershipsSchema.merge(
|
identityMemberships: IdentityOrgMembershipsSchema.merge(
|
||||||
@@ -37,20 +60,26 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}).optional(),
|
}).optional(),
|
||||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||||
})
|
})
|
||||||
).array()
|
).array(),
|
||||||
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const identityMemberships = await server.services.identity.listOrgIdentities({
|
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
orgId: req.params.orgId
|
orgId: req.params.orgId,
|
||||||
|
limit: req.query.limit,
|
||||||
|
offset: req.query.offset,
|
||||||
|
orderBy: req.query.orderBy,
|
||||||
|
orderDirection: req.query.orderDirection,
|
||||||
|
search: req.query.search
|
||||||
});
|
});
|
||||||
|
|
||||||
return { identityMemberships };
|
return { identityMemberships, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import {
|
|||||||
ProjectMembershipRole,
|
ProjectMembershipRole,
|
||||||
ProjectUserMembershipRolesSchema
|
ProjectUserMembershipRolesSchema
|
||||||
} from "@app/db/schemas";
|
} 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 { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
||||||
|
|
||||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||||
@@ -214,6 +216,32 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId)
|
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(),
|
||||||
|
orderDirection: z
|
||||||
|
.nativeEnum(OrderByDirection)
|
||||||
|
.default(OrderByDirection.ASC)
|
||||||
|
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
|
||||||
|
.optional(),
|
||||||
|
search: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.search).optional()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityMemberships: z
|
identityMemberships: z
|
||||||
@@ -239,19 +267,25 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
|||||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
|
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
|
||||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||||
})
|
})
|
||||||
.array()
|
.array(),
|
||||||
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const identityMemberships = await server.services.identityProject.listProjectIdentities({
|
const { identityMemberships, totalCount } = await server.services.identityProject.listProjectIdentities({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.projectId
|
projectId: req.params.projectId,
|
||||||
|
limit: req.query.limit,
|
||||||
|
offset: req.query.offset,
|
||||||
|
orderBy: req.query.orderBy,
|
||||||
|
orderDirection: req.query.orderDirection,
|
||||||
|
search: req.query.search
|
||||||
});
|
});
|
||||||
return { identityMemberships };
|
return { identityMemberships, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
|
|||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
import { TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types";
|
||||||
|
|
||||||
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
|
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
|
||||||
|
|
||||||
@@ -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" | "search" | "orderBy" | "orderDirection"
|
||||||
|
> = {},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
|
const query = (tx || db.replicaNode())(TableName.IdentityProjectMembership)
|
||||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
|
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
|
||||||
@@ -117,6 +125,10 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
|||||||
if (filter.identityId) {
|
if (filter.identityId) {
|
||||||
void qb.where("identityId", filter.identityId);
|
void qb.where("identityId", filter.identityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filter.search) {
|
||||||
|
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.join(
|
.join(
|
||||||
TableName.IdentityProjectMembershipRole,
|
TableName.IdentityProjectMembershipRole,
|
||||||
@@ -154,6 +166,22 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
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.orderDirection);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
|
||||||
const members = sqlNestRelationships({
|
const members = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({
|
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({
|
||||||
@@ -208,9 +236,37 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCountByProjectId = async (
|
||||||
|
projectId: string,
|
||||||
|
filter: { identityId?: string } & Pick<TListProjectIdentityDTO, "search"> = {},
|
||||||
|
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.search) {
|
||||||
|
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
|
||||||
|
return Number(identities[0].count);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetCountByProjectId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...identityProjectOrm,
|
...identityProjectOrm,
|
||||||
findByIdentityId,
|
findByIdentityId,
|
||||||
findByProjectId
|
findByProjectId,
|
||||||
|
getCountByProjectId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -268,7 +268,12 @@ export const identityProjectServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
}: TListProjectIdentityDTO) => {
|
}: TListProjectIdentityDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -279,8 +284,17 @@ export const identityProjectServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||||
|
|
||||||
const identityMemberships = await identityProjectDAL.findByProjectId(projectId);
|
const identityMemberships = await identityProjectDAL.findByProjectId(projectId, {
|
||||||
return identityMemberships;
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCount = await identityProjectDAL.getCountByProjectId(projectId, { search });
|
||||||
|
|
||||||
|
return { identityMemberships, totalCount };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectIdentityByIdentityId = async ({
|
const getProjectIdentityByIdentityId = async ({
|
||||||
|
|||||||
@@ -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";
|
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||||
|
|
||||||
@@ -40,8 +40,18 @@ export type TDeleteProjectIdentityDTO = {
|
|||||||
identityId: string;
|
identityId: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TListProjectIdentityDTO = TProjectPermission;
|
export type TListProjectIdentityDTO = {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: ProjectIdentityOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetProjectIdentityByIdentityIdDTO = {
|
export type TGetProjectIdentityByIdentityIdDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export enum ProjectIdentityOrderBy {
|
||||||
|
Name = "name"
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
|
|||||||
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
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<typeof identityOrgDALFactory>;
|
export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>;
|
||||||
|
|
||||||
@@ -27,9 +29,20 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const find = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => {
|
const find = async (
|
||||||
|
{
|
||||||
|
limit,
|
||||||
|
offset = 0,
|
||||||
|
orderBy,
|
||||||
|
orderDirection = OrderByDirection.ASC,
|
||||||
|
search,
|
||||||
|
...filter
|
||||||
|
}: Partial<TIdentityOrgMemberships> &
|
||||||
|
Pick<TListOrgIdentitiesByOrgIdDTO, "offset" | "limit" | "orderBy" | "orderDirection" | "search">,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||||
.where(filter)
|
.where(filter)
|
||||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.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("id").as("identityId").withSchema(TableName.Identity))
|
||||||
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
|
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
|
||||||
.select(db.ref("authMethod").as("identityAuthMethod").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}`, orderDirection);
|
||||||
|
break;
|
||||||
|
case "role":
|
||||||
|
void query.orderBy(`${TableName.IdentityOrgMembership}.${orderBy}`, orderDirection);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search?.length) {
|
||||||
|
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
|
||||||
return docs.map(
|
return docs.map(
|
||||||
({
|
({
|
||||||
crId,
|
crId,
|
||||||
@@ -79,5 +116,27 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...identityOrgOrm, find, findOne };
|
const countAllOrgIdentities = async (
|
||||||
|
{ search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||||
|
.where(filter)
|
||||||
|
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if (search?.length) {
|
||||||
|
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const identities = await query;
|
||||||
|
|
||||||
|
return Number(identities[0].count);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "countAllOrgIdentities" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...identityOrgOrm, find, findOne, countAllOrgIdentities };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
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 { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
TCreateIdentityDTO,
|
TCreateIdentityDTO,
|
||||||
TDeleteIdentityDTO,
|
TDeleteIdentityDTO,
|
||||||
TGetIdentityByIdDTO,
|
TGetIdentityByIdDTO,
|
||||||
|
TListOrgIdentitiesByOrgIdDTO,
|
||||||
TListProjectIdentitiesByIdentityIdDTO,
|
TListProjectIdentitiesByIdentityIdDTO,
|
||||||
TUpdateIdentityDTO
|
TUpdateIdentityDTO
|
||||||
} from "./identity-types";
|
} from "./identity-types";
|
||||||
@@ -195,14 +195,36 @@ export const identityServiceFactory = ({
|
|||||||
return { ...deletedIdentity, orgId: identityOrgMembership.orgId };
|
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,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
|
}: TListOrgIdentitiesByOrgIdDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
const identityMemberships = await identityOrgMembershipDAL.find({
|
const identityMemberships = await identityOrgMembershipDAL.find({
|
||||||
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId
|
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
});
|
});
|
||||||
return identityMemberships;
|
|
||||||
|
const totalCount = await identityOrgMembershipDAL.countAllOrgIdentities({
|
||||||
|
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
|
||||||
|
search
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityMemberships, totalCount };
|
||||||
};
|
};
|
||||||
|
|
||||||
const listProjectIdentitiesByIdentityId = async ({
|
const listProjectIdentitiesByIdentityId = async ({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IPType } from "@app/lib/ip";
|
import { IPType } from "@app/lib/ip";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TCreateIdentityDTO = {
|
export type TCreateIdentityDTO = {
|
||||||
role: string;
|
role: string;
|
||||||
@@ -29,3 +29,16 @@ export interface TIdentityTrustedIp {
|
|||||||
export type TListProjectIdentitiesByIdentityIdDTO = {
|
export type TListProjectIdentitiesByIdentityIdDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TListOrgIdentitiesByOrgIdDTO = {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: OrgIdentityOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export enum OrgIdentityOrderBy {
|
||||||
|
Name = "name",
|
||||||
|
Role = "role"
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type Props = {
|
|||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
autoCapitalization?: boolean;
|
autoCapitalization?: boolean;
|
||||||
|
containerClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputVariants = cva(
|
const inputVariants = cva(
|
||||||
@@ -71,6 +72,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
(
|
(
|
||||||
{
|
{
|
||||||
className,
|
className,
|
||||||
|
containerClassName,
|
||||||
isRounded = true,
|
isRounded = true,
|
||||||
isFullWidth = true,
|
isFullWidth = true,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@@ -94,7 +96,15 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
|
<div
|
||||||
|
className={inputParentContainerVariants({
|
||||||
|
isRounded,
|
||||||
|
isError,
|
||||||
|
isFullWidth,
|
||||||
|
variant,
|
||||||
|
className: containerClassName
|
||||||
|
})}
|
||||||
|
>
|
||||||
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
|
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
|
||||||
<input
|
<input
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const Pagination = ({
|
|||||||
>
|
>
|
||||||
<div className="mr-6 flex items-center space-x-2">
|
<div className="mr-6 flex items-center space-x-2">
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{(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}
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum OrderByDirection {
|
||||||
|
ASC = "asc",
|
||||||
|
DESC = "desc"
|
||||||
|
}
|
||||||
@@ -469,3 +469,8 @@ export type RevokeTokenDTO = {
|
|||||||
export type RevokeTokenRes = {
|
export type RevokeTokenRes = {
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TProjectIdentitiesList = {
|
||||||
|
identityMemberships: IdentityMembership[];
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
import { TGroupOrgMembership } from "../groups/types";
|
import { TGroupOrgMembership } from "../groups/types";
|
||||||
import { IdentityMembershipOrg } from "../identities/types";
|
|
||||||
import {
|
import {
|
||||||
BillingDetails,
|
BillingDetails,
|
||||||
Invoice,
|
Invoice,
|
||||||
License,
|
License,
|
||||||
Organization,
|
Organization,
|
||||||
|
OrgIdentityOrderBy,
|
||||||
OrgPlanTable,
|
OrgPlanTable,
|
||||||
PlanBillingInfo,
|
PlanBillingInfo,
|
||||||
PmtMethod,
|
PmtMethod,
|
||||||
ProductsTable,
|
ProductsTable,
|
||||||
TaxID,
|
TaxID,
|
||||||
|
TListOrgIdentitiesDTO,
|
||||||
|
TOrgIdentitiesList,
|
||||||
UpdateOrgDTO
|
UpdateOrgDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -30,6 +33,12 @@ export const organizationKeys = {
|
|||||||
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const,
|
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const,
|
||||||
getOrgIdentityMemberships: (orgId: string) =>
|
getOrgIdentityMemberships: (orgId: string) =>
|
||||||
[{ orgId }, "organization-identity-memberships"] as const,
|
[{ 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
|
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -360,19 +369,51 @@ export const useGetOrgLicenses = (organizationId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetIdentityMembershipOrgs = (organizationId: string) => {
|
export const useGetIdentityMembershipOrgs = (
|
||||||
|
{
|
||||||
|
organizationId,
|
||||||
|
offset = 0,
|
||||||
|
limit = 100,
|
||||||
|
orderBy = OrgIdentityOrderBy.Name,
|
||||||
|
orderDirection = OrderByDirection.ASC,
|
||||||
|
search = ""
|
||||||
|
}: TListOrgIdentitiesDTO,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TOrgIdentitiesList,
|
||||||
|
unknown,
|
||||||
|
TOrgIdentitiesList,
|
||||||
|
ReturnType<typeof organizationKeys.getOrgIdentityMembershipsWithParams>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
offset: String(offset),
|
||||||
|
limit: String(limit),
|
||||||
|
orderBy: String(orderBy),
|
||||||
|
orderDirection: String(orderDirection),
|
||||||
|
search: String(search)
|
||||||
|
});
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId),
|
queryKey: organizationKeys.getOrgIdentityMembershipsWithParams({
|
||||||
|
organizationId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
|
}),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const {
|
const { data } = await apiRequest.get<TOrgIdentitiesList>(
|
||||||
data: { identityMemberships }
|
`/api/v2/organizations/${organizationId}/identity-memberships`,
|
||||||
} = await apiRequest.get<{ identityMemberships: IdentityMembershipOrg[] }>(
|
{ params }
|
||||||
`/api/v2/organizations/${organizationId}/identity-memberships`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return identityMemberships;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
import { IdentityMembershipOrg } from "@app/hooks/api/identities/types";
|
||||||
|
|
||||||
export type Organization = {
|
export type Organization = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -102,3 +105,22 @@ export type ProductsTable = {
|
|||||||
head: ProductsTableHead[];
|
head: ProductsTableHead[];
|
||||||
rows: ProductsTableRow[];
|
rows: ProductsTableRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TListOrgIdentitiesDTO = {
|
||||||
|
organizationId: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: OrgIdentityOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrgIdentitiesList = {
|
||||||
|
identityMemberships: IdentityMembershipOrg[];
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum OrgIdentityOrderBy {
|
||||||
|
Name = "name",
|
||||||
|
Role = "role"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
import { CaStatus } from "../ca/enums";
|
import { CaStatus } from "../ca/enums";
|
||||||
import { TCertificateAuthority } from "../ca/types";
|
import { TCertificateAuthority } from "../ca/types";
|
||||||
@@ -8,7 +9,7 @@ import { TCertificate } from "../certificates/types";
|
|||||||
import { TCertificateTemplate } from "../certificateTemplates/types";
|
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||||
import { TGroupMembership } from "../groups/types";
|
import { TGroupMembership } from "../groups/types";
|
||||||
import { identitiesKeys } from "../identities/queries";
|
import { identitiesKeys } from "../identities/queries";
|
||||||
import { IdentityMembership } from "../identities/types";
|
import { TProjectIdentitiesList } from "../identities/types";
|
||||||
import { IntegrationAuth } from "../integrationAuth/types";
|
import { IntegrationAuth } from "../integrationAuth/types";
|
||||||
import { TIntegration } from "../integrations/types";
|
import { TIntegration } from "../integrations/types";
|
||||||
import { TPkiAlert } from "../pkiAlerts/types";
|
import { TPkiAlert } from "../pkiAlerts/types";
|
||||||
@@ -24,8 +25,10 @@ import {
|
|||||||
DeleteEnvironmentDTO,
|
DeleteEnvironmentDTO,
|
||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
NameWorkspaceSecretsDTO,
|
NameWorkspaceSecretsDTO,
|
||||||
|
ProjectIdentityOrderBy,
|
||||||
RenameWorkspaceDTO,
|
RenameWorkspaceDTO,
|
||||||
TGetUpgradeProjectStatusDTO,
|
TGetUpgradeProjectStatusDTO,
|
||||||
|
TListProjectIdentitiesDTO,
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
TUpdateWorkspaceIdentityRoleDTO,
|
TUpdateWorkspaceIdentityRoleDTO,
|
||||||
TUpdateWorkspaceUserRoleDTO,
|
TUpdateWorkspaceUserRoleDTO,
|
||||||
@@ -484,18 +487,51 @@ export const useDeleteIdentityFromWorkspace = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetWorkspaceIdentityMemberships = (workspaceId: string) => {
|
export const useGetWorkspaceIdentityMemberships = (
|
||||||
|
{
|
||||||
|
workspaceId,
|
||||||
|
offset = 0,
|
||||||
|
limit = 100,
|
||||||
|
orderBy = ProjectIdentityOrderBy.Name,
|
||||||
|
orderDirection = OrderByDirection.ASC,
|
||||||
|
search = ""
|
||||||
|
}: TListProjectIdentitiesDTO,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TProjectIdentitiesList,
|
||||||
|
unknown,
|
||||||
|
TProjectIdentitiesList,
|
||||||
|
ReturnType<typeof workspaceKeys.getWorkspaceIdentityMembershipsWithParams>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId),
|
queryKey: workspaceKeys.getWorkspaceIdentityMembershipsWithParams({
|
||||||
|
workspaceId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
|
}),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const {
|
const params = new URLSearchParams({
|
||||||
data: { identityMemberships }
|
offset: String(offset),
|
||||||
} = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>(
|
limit: String(limit),
|
||||||
`/api/v2/workspace/${workspaceId}/identity-memberships`
|
orderBy: String(orderBy),
|
||||||
|
orderDirection: String(orderDirection),
|
||||||
|
search: String(search)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await apiRequest.get<TProjectIdentitiesList>(
|
||||||
|
`/api/v2/workspace/${workspaceId}/identity-memberships`,
|
||||||
|
{ params }
|
||||||
);
|
);
|
||||||
return identityMemberships;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TListProjectIdentitiesDTO } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import type { CaStatus } from "../ca";
|
import type { CaStatus } from "../ca";
|
||||||
|
|
||||||
export const workspaceKeys = {
|
export const workspaceKeys = {
|
||||||
@@ -15,6 +17,12 @@ export const workspaceKeys = {
|
|||||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
|
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
|
||||||
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
[{ 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) =>
|
getWorkspaceGroupMemberships: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-groups"] as const,
|
[{ workspaceId }, "workspace-groups"] as const,
|
||||||
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
|
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
import { TProjectRole } from "../roles/types";
|
import { TProjectRole } from "../roles/types";
|
||||||
|
|
||||||
export enum ProjectVersion {
|
export enum ProjectVersion {
|
||||||
@@ -141,3 +143,16 @@ export type TUpdateWorkspaceGroupRoleDTO = {
|
|||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TListProjectIdentitiesDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: ProjectIdentityOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ProjectIdentityOrderBy {
|
||||||
|
Name = "name"
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const useNavigateToSelectOrganization = () => {
|
|||||||
await navigateUserToOrg(router, config.defaultAuthOrgId);
|
await navigateUserToOrg(router, config.defaultAuthOrgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let localOrgId = localStorage.getItem("orgData.id")
|
const localOrgId = localStorage.getItem("orgData.id")
|
||||||
if(!cliCallbackPort && localOrgId != null){
|
if(!cliCallbackPort && localOrgId != null){
|
||||||
await navigateUserToOrg(router, localOrgId);
|
await navigateUserToOrg(router, localOrgId);
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-24
@@ -1,5 +1,12 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@@ -11,8 +18,12 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
Spinner,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -23,7 +34,10 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
|
import { useDebounce } from "@app/hooks";
|
||||||
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -36,22 +50,60 @@ type Props = {
|
|||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INIT_PER_PAGE = 10;
|
||||||
|
|
||||||
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||||
|
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||||
|
const [orderBy, setOrderBy] = useState(OrgIdentityOrderBy.Name);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const debouncedSearch = useDebounce(search);
|
||||||
|
|
||||||
|
const organizationId = currentOrg?.id || "";
|
||||||
|
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
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,
|
||||||
|
orderDirection,
|
||||||
|
orderBy,
|
||||||
|
search: debouncedSearch
|
||||||
|
},
|
||||||
|
{ 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: OrgIdentityOrderBy) => {
|
||||||
|
if (column === orderBy) {
|
||||||
|
setOrderDirection((prev) =>
|
||||||
|
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrderBy(column);
|
||||||
|
setOrderDirection(OrderByDirection.ASC);
|
||||||
|
};
|
||||||
|
|
||||||
const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => {
|
const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => {
|
||||||
try {
|
try {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId,
|
identityId,
|
||||||
role,
|
role,
|
||||||
organizationId: orgId
|
organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -71,24 +123,70 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
containerClassName="mb-4"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search identities by name..."
|
||||||
|
/>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr className="h-14">
|
||||||
<Th>Name</Th>
|
<Th className="w-1/2">
|
||||||
<Th>Role</Th>
|
<div className="flex items-center">
|
||||||
<Th className="w-5" />
|
Name
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Name ? "" : "opacity-30"}`}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(OrgIdentityOrderBy.Name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
orderDirection === OrderByDirection.DESC &&
|
||||||
|
orderBy === OrgIdentityOrderBy.Name
|
||||||
|
? faArrowUp
|
||||||
|
: faArrowDown
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Role
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(OrgIdentityOrderBy.Role)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
orderDirection === OrderByDirection.DESC &&
|
||||||
|
orderBy === OrgIdentityOrderBy.Role
|
||||||
|
? faArrowUp
|
||||||
|
: faArrowDown
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={4} innerKey="org-identities" />}
|
{isLoading && <TableSkeleton columns={3} innerKey="org-identities" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data?.map(({ identity: { id, name }, role, customRole }) => {
|
data?.identityMemberships.map(({ identity: { id, name }, role, customRole }) => {
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
key={`identity-${id}`}
|
key={`identity-${id}`}
|
||||||
onClick={() => router.push(`/org/${orgId}/identities/${id}`)}
|
onClick={() => router.push(`/org/${organizationId}/identities/${id}`)}
|
||||||
>
|
>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -123,7 +221,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
<div className="flex justify-center hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -139,7 +237,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
router.push(`/org/${orgId}/identities/${id}`);
|
router.push(`/org/${organizationId}/identities/${id}`);
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
disabled={!isAllowed}
|
||||||
>
|
>
|
||||||
@@ -177,18 +275,28 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!isLoading && data && data?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={4}>
|
|
||||||
<EmptyState
|
|
||||||
title="No identities have been created in this organization"
|
|
||||||
icon={faServer}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{!isLoading && data && data.totalCount > INIT_PER_PAGE && (
|
||||||
|
<Pagination
|
||||||
|
count={data.totalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading && data && data?.identityMemberships.length === 0 && (
|
||||||
|
<EmptyState
|
||||||
|
title={
|
||||||
|
debouncedSearch.trim().length > 0
|
||||||
|
? "No identities match search filter"
|
||||||
|
: "No identities have been created in this organization"
|
||||||
|
}
|
||||||
|
icon={faServer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
faArrowUpRightFromSquare,
|
faArrowUpRightFromSquare,
|
||||||
faClock,
|
faClock,
|
||||||
faEdit,
|
faEdit,
|
||||||
|
faMagnifyingGlass,
|
||||||
faPlus,
|
faPlus,
|
||||||
faServer,
|
faServer,
|
||||||
faXmark
|
faXmark
|
||||||
@@ -22,8 +26,11 @@ import {
|
|||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
Pagination,
|
||||||
|
Spinner,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -37,15 +44,19 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
import { withProjectPermission } from "@app/hoc";
|
||||||
|
import { useDebounce } from "@app/hooks";
|
||||||
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
|
import { ProjectIdentityOrderBy } from "@app/hooks/api/workspace/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityModal } from "./components/IdentityModal";
|
import { IdentityModal } from "./components/IdentityModal";
|
||||||
import { IdentityRoleForm } from "./components/IdentityRoleForm";
|
import { IdentityRoleForm } from "./components/IdentityRoleForm";
|
||||||
|
|
||||||
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||||
|
const INIT_PER_PAGE = 10;
|
||||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||||
@@ -56,9 +67,27 @@ export const IdentityTab = withProjectPermission(
|
|||||||
() => {
|
() => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||||
|
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||||
|
const [orderBy, setOrderBy] = useState(ProjectIdentityOrderBy.Name);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const debouncedSearch = useDebounce(search);
|
||||||
|
|
||||||
const workspaceId = currentWorkspace?.id ?? "";
|
const workspaceId = currentWorkspace?.id ?? "";
|
||||||
|
|
||||||
const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?.id || "");
|
const offset = (page - 1) * perPage;
|
||||||
|
const { data, isLoading, isFetching } = useGetWorkspaceIdentityMemberships(
|
||||||
|
{
|
||||||
|
workspaceId: currentWorkspace?.id || "",
|
||||||
|
offset,
|
||||||
|
limit: perPage,
|
||||||
|
orderDirection,
|
||||||
|
orderBy,
|
||||||
|
search: debouncedSearch
|
||||||
|
},
|
||||||
|
{ keepPreviousData: true }
|
||||||
|
);
|
||||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
|
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
@@ -93,6 +122,23 @@ export const IdentityTab = withProjectPermission(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// reset page if no longer valid
|
||||||
|
if (data && data.totalCount < offset) setPage(1);
|
||||||
|
}, [data?.totalCount]);
|
||||||
|
|
||||||
|
const handleSort = (column: ProjectIdentityOrderBy) => {
|
||||||
|
if (column === orderBy) {
|
||||||
|
setOrderDirection((prev) =>
|
||||||
|
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrderBy(column);
|
||||||
|
setOrderDirection(OrderByDirection.ASC);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="identity-role-panel"
|
key="identity-role-panel"
|
||||||
@@ -132,22 +178,50 @@ export const IdentityTab = withProjectPermission(
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
|
<Input
|
||||||
|
containerClassName="mb-4"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search identities by name..."
|
||||||
|
/>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr className="h-14">
|
||||||
<Th>Name</Th>
|
<Th className="w-1/3">
|
||||||
<Th>Role</Th>
|
<div className="flex items-center">
|
||||||
|
Name
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={`ml-2 ${
|
||||||
|
orderBy === ProjectIdentityOrderBy.Name ? "" : "opacity-30"
|
||||||
|
}`}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(ProjectIdentityOrderBy.Name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
orderDirection === OrderByDirection.DESC &&
|
||||||
|
orderBy === ProjectIdentityOrderBy.Name
|
||||||
|
? faArrowUp
|
||||||
|
: faArrowDown
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th className="w-1/3">Role</Th>
|
||||||
<Th>Added on</Th>
|
<Th>Added on</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={7} innerKey="project-identities" />}
|
{isLoading && <TableSkeleton columns={4} innerKey="project-identities" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data &&
|
data &&
|
||||||
data.length > 0 &&
|
data.identityMemberships.length > 0 &&
|
||||||
data.map((identityMember, index) => {
|
data.identityMemberships.map((identityMember, index) => {
|
||||||
const {
|
const {
|
||||||
identity: { id, name },
|
identity: { id, name },
|
||||||
roles,
|
roles,
|
||||||
@@ -293,18 +367,27 @@ export const IdentityTab = withProjectPermission(
|
|||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!isLoading && data && data?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={7}>
|
|
||||||
<EmptyState
|
|
||||||
title="No identities have been added to this project"
|
|
||||||
icon={faServer}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{!isLoading && data && data.totalCount > INIT_PER_PAGE && (
|
||||||
|
<Pagination
|
||||||
|
count={data.totalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading && data && data?.identityMemberships.length === 0 && (
|
||||||
|
<EmptyState
|
||||||
|
title={
|
||||||
|
debouncedSearch.trim().length > 0
|
||||||
|
? "No identities match search filter"
|
||||||
|
: "No identities have been added to this project"
|
||||||
|
}
|
||||||
|
icon={faServer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp.updateRole.isOpen}
|
isOpen={popUp.updateRole.isOpen}
|
||||||
@@ -312,7 +395,8 @@ export const IdentityTab = withProjectPermission(
|
|||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
className="max-w-3xl"
|
className="max-w-3xl"
|
||||||
title={`Manage Access for ${(popUp.updateRole.data as IdentityMembership)?.identity?.name
|
title={`Manage Access for ${
|
||||||
|
(popUp.updateRole.data as IdentityMembership)?.identity?.name
|
||||||
}`}
|
}`}
|
||||||
subTitle={`
|
subTitle={`
|
||||||
Configure role-based access control by assigning machine identities a mix of roles and specific privileges. An identity will gain access to all actions within the roles assigned to it, not just the actions those roles share in common. You must choose at least one permanent role.
|
Configure role-based access control by assigning machine identities a mix of roles and specific privileges. An identity will gain access to all actions within the roles assigned to it, not just the actions those roles share in common. You must choose at least one permanent role.
|
||||||
@@ -323,7 +407,7 @@ export const IdentityTab = withProjectPermission(
|
|||||||
handlePopUpOpen("upgradePlan", { description })
|
handlePopUpOpen("upgradePlan", { description })
|
||||||
}
|
}
|
||||||
identityProjectMember={
|
identityProjectMember={
|
||||||
data?.[
|
data?.identityMemberships[
|
||||||
(popUp.updateRole?.data as IdentityMembership & { index: number })?.index
|
(popUp.updateRole?.data as IdentityMembership & { index: number })?.index
|
||||||
] as IdentityMembership
|
] as IdentityMembership
|
||||||
}
|
}
|
||||||
@@ -333,7 +417,8 @@ export const IdentityTab = withProjectPermission(
|
|||||||
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteIdentity.isOpen}
|
isOpen={popUp.deleteIdentity.isOpen}
|
||||||
title={`Are you sure want to remove ${(popUp?.deleteIdentity?.data as { name: string })?.name || ""
|
title={`Are you sure want to remove ${
|
||||||
|
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
|
||||||
} from the project?`}
|
} from the project?`}
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
||||||
deleteKey="confirm"
|
deleteKey="confirm"
|
||||||
|
|||||||
+22
-4
@@ -5,7 +5,15 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityToWorkspace,
|
useAddIdentityToWorkspace,
|
||||||
@@ -33,12 +41,20 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const orgId = currentOrg?.id || "";
|
const organizationId = currentOrg?.id || "";
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
|
||||||
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
|
const { data: identityMembershipOrgsData } = useGetIdentityMembershipOrgs({
|
||||||
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
|
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);
|
const { data: roles } = useGetProjectRoles(projectSlug);
|
||||||
|
|
||||||
@@ -158,9 +174,11 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
>
|
>
|
||||||
{popUp?.identity?.data ? "Update" : "Create"}
|
{popUp?.identity?.data ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
<Button colorSchema="secondary" variant="plain">
|
<Button colorSchema="secondary" variant="plain">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user