mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 18:27:19 +00:00
improvement: add bulk delete org members endpoint and table support
This commit is contained in:
@@ -664,6 +664,10 @@ export const ORGANIZATIONS = {
|
|||||||
organizationId: "The ID of the organization to delete the membership from.",
|
organizationId: "The ID of the organization to delete the membership from.",
|
||||||
membershipId: "The ID of the membership to delete."
|
membershipId: "The ID of the membership to delete."
|
||||||
},
|
},
|
||||||
|
BULK_DELETE_USER_MEMBERSHIPS: {
|
||||||
|
organizationId: "The ID of the organization to delete the memberships from.",
|
||||||
|
membershipIds: "The IDs of the memberships 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.",
|
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
|
||||||
|
|||||||
@@ -264,6 +264,48 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:organizationId/memberships",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.Organizations],
|
||||||
|
description: "Bulk delete organization user memberships",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim().describe(ORGANIZATIONS.BULK_DELETE_USER_MEMBERSHIPS.organizationId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
membershipIds: z.string().trim().array().describe(ORGANIZATIONS.BULK_DELETE_USER_MEMBERSHIPS.membershipIds)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
memberships: OrgMembershipsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
|
const memberships = await server.services.org.bulkDeleteOrgMemberships({
|
||||||
|
userId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
membershipIds: req.body.membershipIds,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
return { memberships };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
// TODO: re-think endpoint structure in future so users only need to pass in membershipId bc organizationId is redundant
|
// TODO: re-think endpoint structure in future so users only need to pass in membershipId bc organizationId is redundant
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -513,6 +513,21 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteMembershipsById = async (ids: string[], orgId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const memberships = await (tx || db)(TableName.OrgMembership)
|
||||||
|
.where({
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
.whereIn("id", ids)
|
||||||
|
.delete()
|
||||||
|
.returning("*");
|
||||||
|
return memberships;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Delete org memberships" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findMembership = async (
|
const findMembership = async (
|
||||||
filter: TFindFilter<TOrgMemberships>,
|
filter: TFindFilter<TOrgMemberships>,
|
||||||
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
|
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
|
||||||
@@ -634,6 +649,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
createMembership,
|
createMembership,
|
||||||
updateMembershipById,
|
updateMembershipById,
|
||||||
deleteMembershipById,
|
deleteMembershipById,
|
||||||
|
deleteMembershipsById,
|
||||||
updateMembership
|
updateMembership
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
@@ -14,6 +15,19 @@ type TDeleteOrgMembership = {
|
|||||||
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TDeleteOrgMemberships = {
|
||||||
|
orgMembershipIds: string[];
|
||||||
|
orgId: string;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipsById" | "transaction">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "delete" | "findProjectMembershipsByUserIds">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
||||||
|
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||||
|
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteOrgMembershipFn = async ({
|
export const deleteOrgMembershipFn = async ({
|
||||||
@@ -24,11 +38,17 @@ export const deleteOrgMembershipFn = async ({
|
|||||||
projectUserAdditionalPrivilegeDAL,
|
projectUserAdditionalPrivilegeDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
userId
|
||||||
}: TDeleteOrgMembership) => {
|
}: TDeleteOrgMembership) => {
|
||||||
const deletedMembership = await orgDAL.transaction(async (tx) => {
|
const deletedMembership = await orgDAL.transaction(async (tx) => {
|
||||||
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
|
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
|
||||||
|
|
||||||
|
if (orgMembership.userId === userId) {
|
||||||
|
// scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken
|
||||||
|
throw new BadRequestError({ message: "You cannot remove yourself from an organization" });
|
||||||
|
}
|
||||||
|
|
||||||
if (!orgMembership.userId) {
|
if (!orgMembership.userId) {
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
return orgMembership;
|
return orgMembership;
|
||||||
@@ -86,3 +106,88 @@ export const deleteOrgMembershipFn = async ({
|
|||||||
|
|
||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteOrgMembershipsFn = async ({
|
||||||
|
orgMembershipIds,
|
||||||
|
orgId,
|
||||||
|
orgDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
userAliasDAL,
|
||||||
|
licenseService,
|
||||||
|
userId
|
||||||
|
}: TDeleteOrgMemberships) => {
|
||||||
|
const deletedMemberships = await orgDAL.transaction(async (tx) => {
|
||||||
|
const orgMemberships = await orgDAL.deleteMembershipsById(orgMembershipIds, orgId, tx);
|
||||||
|
|
||||||
|
const membershipUserIds = orgMemberships
|
||||||
|
.filter((member) => Boolean(member.userId))
|
||||||
|
.map((member) => member.userId) as string[];
|
||||||
|
|
||||||
|
if (membershipUserIds.includes(userId)) {
|
||||||
|
// scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken
|
||||||
|
throw new BadRequestError({ message: "You cannot remove yourself from an organization" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!membershipUserIds.length) {
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
|
return orgMemberships;
|
||||||
|
}
|
||||||
|
|
||||||
|
await userAliasDAL.delete(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
userId: membershipUserIds
|
||||||
|
},
|
||||||
|
orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await projectUserAdditionalPrivilegeDAL.delete(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
userId: membershipUserIds
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all the project memberships of the users in the organization
|
||||||
|
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserIds(orgId, membershipUserIds);
|
||||||
|
|
||||||
|
// Delete all the project memberships of the users in the organization
|
||||||
|
await projectMembershipDAL.delete(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
id: projectMemberships.map((membership) => membership.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all the project keys of the user in the organization
|
||||||
|
const projectKeys = await projectKeyDAL.find({
|
||||||
|
$in: {
|
||||||
|
projectId: projectMemberships.map((membership) => membership.projectId),
|
||||||
|
receiverId: membershipUserIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete all the project keys of the user in the organization
|
||||||
|
await projectKeyDAL.delete(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
id: projectKeys.map((key) => key.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
|
return orgMemberships;
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletedMemberships;
|
||||||
|
};
|
||||||
|
|||||||
@@ -75,10 +75,11 @@ import { TUserDALFactory } from "../user/user-dal";
|
|||||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||||
import { TOrgBotDALFactory } from "./org-bot-dal";
|
import { TOrgBotDALFactory } from "./org-bot-dal";
|
||||||
import { TOrgDALFactory } from "./org-dal";
|
import { TOrgDALFactory } from "./org-dal";
|
||||||
import { deleteOrgMembershipFn } from "./org-fns";
|
import { deleteOrgMembershipFn, deleteOrgMembershipsFn } from "./org-fns";
|
||||||
import { TOrgRoleDALFactory } from "./org-role-dal";
|
import { TOrgRoleDALFactory } from "./org-role-dal";
|
||||||
import {
|
import {
|
||||||
TDeleteOrgMembershipDTO,
|
TDeleteOrgMembershipDTO,
|
||||||
|
TDeleteOrgMembershipsDTO,
|
||||||
TFindAllWorkspacesDTO,
|
TFindAllWorkspacesDTO,
|
||||||
TFindOrgMembersByEmailDTO,
|
TFindOrgMembersByEmailDTO,
|
||||||
TGetOrgGroupsDTO,
|
TGetOrgGroupsDTO,
|
||||||
@@ -106,7 +107,13 @@ type TOrgServiceFactoryDep = {
|
|||||||
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
||||||
projectMembershipDAL: Pick<
|
projectMembershipDAL: Pick<
|
||||||
TProjectMembershipDALFactory,
|
TProjectMembershipDALFactory,
|
||||||
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
| "findProjectMembershipsByUserId"
|
||||||
|
| "delete"
|
||||||
|
| "create"
|
||||||
|
| "find"
|
||||||
|
| "insertMany"
|
||||||
|
| "transaction"
|
||||||
|
| "findProjectMembershipsByUserIds"
|
||||||
>;
|
>;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
|
||||||
orgMembershipDAL: Pick<
|
orgMembershipDAL: Pick<
|
||||||
@@ -1369,12 +1376,42 @@ export const orgServiceFactory = ({
|
|||||||
projectUserAdditionalPrivilegeDAL,
|
projectUserAdditionalPrivilegeDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bulkDeleteOrgMemberships = async ({
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
membershipIds,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TDeleteOrgMembershipsDTO) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
|
if (membershipIds.includes(userId)) {
|
||||||
|
throw new BadRequestError({ message: "You cannot delete your own organization membership" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedMemberships = await deleteOrgMembershipsFn({
|
||||||
|
orgMembershipIds: membershipIds,
|
||||||
|
orgId,
|
||||||
|
orgDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
userAliasDAL,
|
||||||
|
licenseService,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletedMemberships;
|
||||||
|
};
|
||||||
|
|
||||||
const listProjectMembershipsByOrgMembershipId = async ({
|
const listProjectMembershipsByOrgMembershipId = async ({
|
||||||
orgMembershipId,
|
orgMembershipId,
|
||||||
orgId,
|
orgId,
|
||||||
@@ -1528,6 +1565,7 @@ export const orgServiceFactory = ({
|
|||||||
findOrgBySlug,
|
findOrgBySlug,
|
||||||
resendOrgMemberInvitation,
|
resendOrgMemberInvitation,
|
||||||
upgradePrivilegeSystem,
|
upgradePrivilegeSystem,
|
||||||
notifyInvitedUsers
|
notifyInvitedUsers,
|
||||||
|
bulkDeleteOrgMemberships
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ export type TDeleteOrgMembershipDTO = {
|
|||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TDeleteOrgMembershipsDTO = {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
membershipIds: string[];
|
||||||
|
actorOrgId: string | undefined;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
};
|
||||||
|
|
||||||
export type TInviteUserToOrgDTO = {
|
export type TInviteUserToOrgDTO = {
|
||||||
inviteeEmails: string[];
|
inviteeEmails: string[];
|
||||||
organizationRoleSlug: string;
|
organizationRoleSlug: string;
|
||||||
|
|||||||
@@ -314,11 +314,123 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findProjectMembershipsByUserIds = async (orgId: string, userIds: string[]) => {
|
||||||
|
try {
|
||||||
|
const docs = await db
|
||||||
|
.replicaNode()(TableName.ProjectMembership)
|
||||||
|
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.whereIn(`${TableName.Users}.id`, userIds)
|
||||||
|
.where(`${TableName.Project}.orgId`, orgId)
|
||||||
|
.join<TUserEncryptionKeys>(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
|
`${TableName.Users}.id`
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
TableName.ProjectUserMembershipRole,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.ProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.ProjectMembership),
|
||||||
|
db.ref("userId").withSchema(TableName.ProjectMembership),
|
||||||
|
db.ref("isGhost").withSchema(TableName.Users),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
||||||
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("role").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"),
|
||||||
|
db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
|
db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||||
|
db.ref("id").as("projectId").withSchema(TableName.Project),
|
||||||
|
db.ref("type").as("projectType").withSchema(TableName.Project)
|
||||||
|
)
|
||||||
|
.where({ isGhost: false });
|
||||||
|
|
||||||
|
const members = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
parentMapper: ({
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
username,
|
||||||
|
lastName,
|
||||||
|
publicKey,
|
||||||
|
isGhost,
|
||||||
|
id,
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
projectType,
|
||||||
|
userId
|
||||||
|
}) => ({
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
||||||
|
project: {
|
||||||
|
id: projectId,
|
||||||
|
name: projectName,
|
||||||
|
type: projectType
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
key: "id",
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
label: "roles" as const,
|
||||||
|
key: "membershipRoleId",
|
||||||
|
mapper: ({
|
||||||
|
role,
|
||||||
|
customRoleId,
|
||||||
|
customRoleName,
|
||||||
|
customRoleSlug,
|
||||||
|
membershipRoleId,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
isTemporary
|
||||||
|
}) => ({
|
||||||
|
id: membershipRoleId,
|
||||||
|
role,
|
||||||
|
customRoleId,
|
||||||
|
customRoleName,
|
||||||
|
customRoleSlug,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
isTemporary
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return members;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find project memberships by user ids" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...projectMemberOrm,
|
...projectMemberOrm,
|
||||||
findAllProjectMembers,
|
findAllProjectMembers,
|
||||||
findProjectGhostUser,
|
findProjectGhostUser,
|
||||||
findMembershipsByUsername,
|
findMembershipsByUsername,
|
||||||
findProjectMembershipsByUserId
|
findProjectMembershipsByUserId,
|
||||||
|
findProjectMembershipsByUserIds
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Bulk Delete User Memberships"
|
||||||
|
openapi: "DELETE /api/v2/organizations/{organizationId}/memberships"
|
||||||
|
---
|
||||||
@@ -900,6 +900,7 @@
|
|||||||
"api-reference/endpoints/organizations/memberships",
|
"api-reference/endpoints/organizations/memberships",
|
||||||
"api-reference/endpoints/organizations/update-membership",
|
"api-reference/endpoints/organizations/update-membership",
|
||||||
"api-reference/endpoints/organizations/delete-membership",
|
"api-reference/endpoints/organizations/delete-membership",
|
||||||
|
"api-reference/endpoints/organizations/bulk-delete-memberships",
|
||||||
"api-reference/endpoints/organizations/list-identity-memberships",
|
"api-reference/endpoints/organizations/list-identity-memberships",
|
||||||
"api-reference/endpoints/organizations/workspaces"
|
"api-reference/endpoints/organizations/workspaces"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
APIKeyData,
|
APIKeyData,
|
||||||
AuthMethod,
|
AuthMethod,
|
||||||
CreateAPIKeyRes,
|
CreateAPIKeyRes,
|
||||||
DeletOrgMembershipDTO,
|
DeleteOrgMembershipBatchDTO,
|
||||||
|
DeleteOrgMembershipDTO,
|
||||||
OrgUser,
|
OrgUser,
|
||||||
RenameUserDTO,
|
RenameUserDTO,
|
||||||
TokenVersion,
|
TokenVersion,
|
||||||
@@ -243,7 +244,7 @@ export const useGetOrgMembershipProjectMemberships = (
|
|||||||
export const useDeleteOrgMembership = () => {
|
export const useDeleteOrgMembership = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<object, object, DeletOrgMembershipDTO>({
|
return useMutation<object, object, DeleteOrgMembershipDTO>({
|
||||||
mutationFn: ({ membershipId, orgId }) => {
|
mutationFn: ({ membershipId, orgId }) => {
|
||||||
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`);
|
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`);
|
||||||
},
|
},
|
||||||
@@ -253,10 +254,27 @@ export const useDeleteOrgMembership = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDeleteOrgMembershipBatch = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<object, object, DeleteOrgMembershipBatchDTO>({
|
||||||
|
mutationFn: ({ membershipIds, orgId }) => {
|
||||||
|
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships`, {
|
||||||
|
data: {
|
||||||
|
membershipIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: userKeys.getOrgUsers(orgId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useDeactivateOrgMembership = () => {
|
export const useDeactivateOrgMembership = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<object, object, DeletOrgMembershipDTO>({
|
return useMutation<object, object, DeleteOrgMembershipDTO>({
|
||||||
mutationFn: ({ membershipId, orgId }) => {
|
mutationFn: ({ membershipId, orgId }) => {
|
||||||
return apiRequest.post(
|
return apiRequest.post(
|
||||||
`/api/v2/organizations/${orgId}/memberships/${membershipId}/deactivate`
|
`/api/v2/organizations/${orgId}/memberships/${membershipId}/deactivate`
|
||||||
|
|||||||
@@ -143,11 +143,16 @@ export type UpdateOrgMembershipDTO = {
|
|||||||
metadata?: { key: string; value: string }[];
|
metadata?: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeletOrgMembershipDTO = {
|
export type DeleteOrgMembershipDTO = {
|
||||||
membershipId: string;
|
membershipId: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeleteOrgMembershipBatchDTO = {
|
||||||
|
membershipIds: string[];
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AddUserToOrgDTO = {
|
export type AddUserToOrgDTO = {
|
||||||
inviteeEmails: string[];
|
inviteeEmails: string[];
|
||||||
organizationRoleSlug: string;
|
organizationRoleSlug: string;
|
||||||
|
|||||||
+202
-69
@@ -1,18 +1,28 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faBan, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, DeleteActionModal, EmailServiceSetupModal } from "@app/components/v2";
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmailServiceSetupModal,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription
|
useSubscription,
|
||||||
|
useUser
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useDeleteOrgMembership, useUpdateOrgMembership } from "@app/hooks/api";
|
import { useDeleteOrgMembership, useGetOrgUsers, useUpdateOrgMembership } from "@app/hooks/api";
|
||||||
|
import { useDeleteOrgMembershipBatch } from "@app/hooks/api/users/queries";
|
||||||
|
import { OrgUser } from "@app/hooks/api/users/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
||||||
@@ -22,7 +32,8 @@ export const OrgMembersSection = () => {
|
|||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id ?? "";
|
const orgId = currentOrg?.id ?? "";
|
||||||
|
const { user } = useUser();
|
||||||
|
const userId = user?.id || "";
|
||||||
const [completeInviteLinks, setCompleteInviteLinks] = useState<Array<{
|
const [completeInviteLinks, setCompleteInviteLinks] = useState<Array<{
|
||||||
email: string;
|
email: string;
|
||||||
link: string;
|
link: string;
|
||||||
@@ -33,10 +44,14 @@ export const OrgMembersSection = () => {
|
|||||||
"removeMember",
|
"removeMember",
|
||||||
"deactivateMember",
|
"deactivateMember",
|
||||||
"upgradePlan",
|
"upgradePlan",
|
||||||
"setUpEmail"
|
"setUpEmail",
|
||||||
|
"removeMembers"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
||||||
|
const { mutateAsync: deleteBatchMutateAsync } = useDeleteOrgMembershipBatch();
|
||||||
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
|
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
|
||||||
|
|
||||||
const isMoreIdentitiesAllowed = subscription?.identityLimit
|
const isMoreIdentitiesAllowed = subscription?.identityLimit
|
||||||
@@ -109,70 +124,188 @@ export const OrgMembersSection = () => {
|
|||||||
handlePopUpClose("removeMember");
|
handlePopUpClose("removeMember");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { data: members = [] } = useGetOrgUsers(orgId);
|
||||||
|
|
||||||
|
const handleRemoveMembers = async (selectedMembers: OrgUser[]) => {
|
||||||
|
try {
|
||||||
|
await deleteBatchMutateAsync({
|
||||||
|
orgId,
|
||||||
|
membershipIds: selectedMembers
|
||||||
|
.filter((member) => member.user.id !== userId)
|
||||||
|
.map((member) => member.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully removed users from organization",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedMemberIds([]);
|
||||||
|
handlePopUpClose("removeMembers");
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove users from the organization",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<>
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
className={twMerge(
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
"h-0 flex-shrink-0 overflow-hidden transition-all",
|
||||||
{(isAllowed) => (
|
selectedMemberIds.length > 0 && "h-16"
|
||||||
<Button
|
)}
|
||||||
colorSchema="secondary"
|
>
|
||||||
type="submit"
|
<div className="flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
<div className="mr-2 text-sm">{selectedMemberIds.length > 0} Selected</div>
|
||||||
onClick={() => handleAddMemberModal()}
|
<button
|
||||||
isDisabled={!isAllowed}
|
type="button"
|
||||||
>
|
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
|
||||||
Add Member
|
onClick={() => setSelectedMemberIds([])}
|
||||||
</Button>
|
>
|
||||||
)}
|
Unselect All
|
||||||
</OrgPermissionCan>
|
</button>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Delete"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="danger"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
className="ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
const selectedOrgMemberships = members.filter((member) =>
|
||||||
|
selectedMemberIds.includes(member.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedOrgMemberships.length) return;
|
||||||
|
|
||||||
|
handlePopUpOpen("removeMembers", { selectedOrgMemberships });
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<OrgMembersTable
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
<div className="mb-4 flex items-center justify-between">
|
||||||
setCompleteInviteLinks={setCompleteInviteLinks}
|
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
||||||
/>
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||||
<AddOrgMemberModal
|
{(isAllowed) => (
|
||||||
popUp={popUp}
|
<Button
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
colorSchema="secondary"
|
||||||
completeInviteLinks={completeInviteLinks}
|
type="submit"
|
||||||
setCompleteInviteLinks={setCompleteInviteLinks}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
/>
|
onClick={() => handleAddMemberModal()}
|
||||||
<DeleteActionModal
|
isDisabled={!isAllowed}
|
||||||
isOpen={popUp.removeMember.isOpen}
|
>
|
||||||
title={`Are you sure you want to remove member with username ${
|
Add Member
|
||||||
(popUp?.removeMember?.data as { username: string })?.username || ""
|
</Button>
|
||||||
}?`}
|
)}
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
</OrgPermissionCan>
|
||||||
deleteKey="confirm"
|
</div>
|
||||||
onDeleteApproved={() =>
|
<OrgMembersTable
|
||||||
onRemoveMemberSubmit(
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
|
setCompleteInviteLinks={setCompleteInviteLinks}
|
||||||
)
|
selectedMemberIds={selectedMemberIds}
|
||||||
}
|
setSelectedMemberIds={setSelectedMemberIds}
|
||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<AddOrgMemberModal
|
||||||
isOpen={popUp.deactivateMember.isOpen}
|
popUp={popUp}
|
||||||
title={`Are you sure you want to deactivate member with username ${
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
(popUp?.deactivateMember?.data as { username: string })?.username || ""
|
completeInviteLinks={completeInviteLinks}
|
||||||
}?`}
|
setCompleteInviteLinks={setCompleteInviteLinks}
|
||||||
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
|
/>
|
||||||
deleteKey="confirm"
|
<DeleteActionModal
|
||||||
onDeleteApproved={() =>
|
isOpen={popUp.removeMember.isOpen}
|
||||||
onDeactivateMemberSubmit(
|
title={`Are you sure you want to remove member with username ${
|
||||||
(popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
|
(popUp?.removeMember?.data as { username: string })?.username || ""
|
||||||
)
|
}?`}
|
||||||
}
|
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||||
buttonText="Deactivate"
|
deleteKey="confirm"
|
||||||
/>
|
onDeleteApproved={() =>
|
||||||
<UpgradePlanModal
|
onRemoveMemberSubmit(
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
)
|
||||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
}
|
||||||
/>
|
/>
|
||||||
<EmailServiceSetupModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.setUpEmail?.isOpen}
|
isOpen={popUp.deactivateMember.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
title={`Are you sure you want to deactivate member with username ${
|
||||||
/>
|
(popUp?.deactivateMember?.data as { username: string })?.username || ""
|
||||||
</div>
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onDeactivateMemberSubmit(
|
||||||
|
(popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
buttonText="Deactivate"
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeMembers.isOpen}
|
||||||
|
title="Are you sure you want to remove the following members?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeMembers", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
handleRemoveMembers(popUp.removeMembers.data.selectedOrgMemberships as OrgUser[])
|
||||||
|
}
|
||||||
|
buttonText="Remove"
|
||||||
|
>
|
||||||
|
<div className="mt-4 text-sm text-mineshaft-400">
|
||||||
|
The following members will be removed:
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-red/40 bg-red/10 p-4 text-sm text-red-200">
|
||||||
|
<ul className="ml-4 list-disc">
|
||||||
|
{(popUp.removeMembers.data?.selectedOrgMemberships as OrgUser[])?.map((member) => {
|
||||||
|
const email = member.user.email ?? member.user.username ?? member.inviteEmail;
|
||||||
|
return (
|
||||||
|
<li className="flex items-center">
|
||||||
|
<span className={userId === member.user.id ? "line-through" : ""}>
|
||||||
|
{member.user.firstName || member.user.lastName
|
||||||
|
? `${`${member.user.firstName} ${member.user.lastName}`.trim()} (${email})`
|
||||||
|
: email}
|
||||||
|
</span>
|
||||||
|
{userId === member.user.id && (
|
||||||
|
<Tooltip content="You cannot remove yourself from this organization">
|
||||||
|
<div className="inline-block">
|
||||||
|
<Badge
|
||||||
|
variant="danger"
|
||||||
|
className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faBan} />
|
||||||
|
<span>Ignored</span>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</DeleteActionModal>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||||
|
/>
|
||||||
|
<EmailServiceSetupModal
|
||||||
|
isOpen={popUp.setUpEmail?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+252
-207
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { Dispatch, SetStateAction, useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
@@ -22,6 +22,7 @@ import { OrgPermissionCan } from "@app/components/permissions";
|
|||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -66,18 +67,24 @@ import {
|
|||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { useResendOrgMemberInvitation } from "@app/hooks/api/users/mutation";
|
import { useResendOrgMemberInvitation } from "@app/hooks/api/users/mutation";
|
||||||
|
import { OrgUser } from "@app/hooks/api/users/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["removeMember", "deactivateMember", "upgradePlan"]>,
|
popUpName: keyof UsePopUpState<
|
||||||
|
["removeMember", "deactivateMember", "upgradePlan", "removeMembers"]
|
||||||
|
>,
|
||||||
data?: {
|
data?: {
|
||||||
orgMembershipId?: string;
|
orgMembershipId?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
selectedOrgMemberships?: OrgUser[];
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void;
|
setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void;
|
||||||
|
selectedMemberIds: string[];
|
||||||
|
setSelectedMemberIds: Dispatch<SetStateAction<string[]>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum OrgMembersOrderBy {
|
enum OrgMembersOrderBy {
|
||||||
@@ -90,7 +97,12 @@ type Filter = {
|
|||||||
roles: string[];
|
roles: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Props) => {
|
export const OrgMembersTable = ({
|
||||||
|
handlePopUpOpen,
|
||||||
|
setCompleteInviteLinks,
|
||||||
|
selectedMemberIds,
|
||||||
|
setSelectedMemberIds
|
||||||
|
}: Props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
@@ -208,7 +220,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
roles: []
|
roles: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredUsers = useMemo(
|
const filteredMembers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
members
|
members
|
||||||
?.filter(({ user: u, inviteEmail, role, roleId }) => {
|
?.filter(({ user: u, inviteEmail, role, roleId }) => {
|
||||||
@@ -273,7 +285,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
};
|
};
|
||||||
|
|
||||||
useResetPageHelper({
|
useResetPageHelper({
|
||||||
totalCount: filteredUsers.length,
|
totalCount: filteredMembers.length,
|
||||||
offset,
|
offset,
|
||||||
setPage
|
setPage
|
||||||
});
|
});
|
||||||
@@ -293,6 +305,16 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
|
|
||||||
const isTableFiltered = Boolean(filter.roles.length);
|
const isTableFiltered = Boolean(filter.roles.length);
|
||||||
|
|
||||||
|
const filteredMembersPage = filteredMembers.slice(offset, perPage * page);
|
||||||
|
|
||||||
|
const isPageSelected = filteredMembersPage.every((member) =>
|
||||||
|
selectedMemberIds.includes(member.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPageIndeterminate = isPageSelected
|
||||||
|
? false
|
||||||
|
: filteredMembersPage.some((member) => selectedMemberIds.includes(member.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -357,6 +379,24 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
|
<Th className="w-5">
|
||||||
|
<Checkbox
|
||||||
|
id="member-page-select"
|
||||||
|
isChecked={isPageSelected || isPageIndeterminate}
|
||||||
|
isIndeterminate={isPageIndeterminate}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
if (isPageSelected) {
|
||||||
|
setSelectedMemberIds((prev) =>
|
||||||
|
prev.filter((member) => !filteredMembersPage.find((m) => m.id !== member))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedMemberIds((prev) => [
|
||||||
|
...new Set([...prev, ...filteredMembersPage.map((member) => member.id)])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Th>
|
||||||
<Th className="w-1/3">
|
<Th className="w-1/3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Name
|
Name
|
||||||
@@ -423,227 +463,232 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
|
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
filteredUsers
|
filteredMembersPage.map(
|
||||||
.slice(offset, perPage * page)
|
({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status, isActive }) => {
|
||||||
.map(
|
const name =
|
||||||
({
|
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null;
|
||||||
user: u,
|
const email = u?.email || inviteEmail;
|
||||||
inviteEmail,
|
const username = u?.username ?? inviteEmail ?? "-";
|
||||||
role,
|
const isSelected = selectedMemberIds.includes(orgMembershipId);
|
||||||
roleId,
|
return (
|
||||||
id: orgMembershipId,
|
<Tr
|
||||||
status,
|
key={`org-membership-${orgMembershipId}`}
|
||||||
isActive
|
className="h-10 w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
}) => {
|
onClick={() =>
|
||||||
const name =
|
navigate({
|
||||||
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null;
|
to: "/organization/members/$membershipId" as const,
|
||||||
const email = u?.email || inviteEmail;
|
params: {
|
||||||
const username = u?.username ?? inviteEmail ?? "-";
|
membershipId: orgMembershipId
|
||||||
return (
|
}
|
||||||
<Tr
|
})
|
||||||
key={`org-membership-${orgMembershipId}`}
|
}
|
||||||
className="h-10 w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
>
|
||||||
onClick={() =>
|
<Td>
|
||||||
navigate({
|
<Checkbox
|
||||||
to: "/organization/members/$membershipId" as const,
|
id={`select-member-${orgMembershipId}`}
|
||||||
params: {
|
isChecked={isSelected}
|
||||||
membershipId: orgMembershipId
|
onClick={(e) => {
|
||||||
}
|
e.stopPropagation();
|
||||||
})
|
setSelectedMemberIds((prev) =>
|
||||||
}
|
isSelected
|
||||||
>
|
? prev.filter((id) => id !== orgMembershipId)
|
||||||
<Td className={isActive ? "" : "text-mineshaft-400"}>
|
: [...prev, orgMembershipId]
|
||||||
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
);
|
||||||
{u.superAdmin && (
|
}}
|
||||||
<Badge variant="primary" className="ml-2">
|
/>
|
||||||
Server Admin
|
</Td>
|
||||||
</Badge>
|
<Td className={isActive ? "" : "text-mineshaft-400"}>
|
||||||
|
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
||||||
|
{u.superAdmin && (
|
||||||
|
<Badge variant="primary" className="ml-2">
|
||||||
|
Server Admin
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td className={isActive ? "" : "text-mineshaft-400"}>{username}</Td>
|
||||||
|
<Td>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Select
|
||||||
|
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
||||||
|
isDisabled={userId === u?.id || !isAllowed}
|
||||||
|
className="h-8 w-48 bg-mineshaft-700"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={(selectedRole) =>
|
||||||
|
onRoleChange(orgMembershipId, selectedRole)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(roles || [])
|
||||||
|
.filter(({ slug }) =>
|
||||||
|
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||||
|
)
|
||||||
|
.map(({ slug, name: roleName }) => (
|
||||||
|
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||||
|
{roleName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</OrgPermissionCan>
|
||||||
<Td className={isActive ? "" : "text-mineshaft-400"}>{username}</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<OrgPermissionCan
|
<div className="flex items-center justify-end gap-6">
|
||||||
I={OrgPermissionActions.Edit}
|
{isActive &&
|
||||||
a={OrgPermissionSubjects.Member}
|
(status === "invited" || status === "verified") &&
|
||||||
>
|
email &&
|
||||||
{(isAllowed) => (
|
serverDetails?.emailConfigured && (
|
||||||
<Select
|
<OrgPermissionCan
|
||||||
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
I={OrgPermissionActions.Edit}
|
||||||
isDisabled={userId === u?.id || !isAllowed}
|
a={OrgPermissionSubjects.Member}
|
||||||
className="h-8 w-48 bg-mineshaft-700"
|
|
||||||
position="popper"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={(selectedRole) =>
|
|
||||||
onRoleChange(orgMembershipId, selectedRole)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{(roles || [])
|
{(isAllowed) => (
|
||||||
.filter(({ slug }) =>
|
<Button
|
||||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
isDisabled={!isAllowed || isResendInvitePending}
|
||||||
)
|
className="h-8 border-mineshaft-600 bg-mineshaft-700 font-normal"
|
||||||
.map(({ slug, name: roleName }) => (
|
colorSchema="primary"
|
||||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
variant="outline_bg"
|
||||||
{roleName}
|
isLoading={
|
||||||
</SelectItem>
|
isResendInvitePending && resendInviteId === orgMembershipId
|
||||||
))}
|
}
|
||||||
</Select>
|
onClick={(e) => {
|
||||||
|
onResendInvite(orgMembershipId);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resend Invite
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
<DropdownMenu>
|
||||||
</Td>
|
<DropdownMenuTrigger disabled={userId === u?.id} asChild>
|
||||||
<Td>
|
<IconButton
|
||||||
<div className="flex items-center justify-end gap-6">
|
ariaLabel="Options"
|
||||||
{isActive &&
|
colorSchema="secondary"
|
||||||
(status === "invited" || status === "verified") &&
|
className={twMerge("w-6", userId === u?.id && "opacity-50")}
|
||||||
email &&
|
variant="plain"
|
||||||
serverDetails?.emailConfigured && (
|
isDisabled={userId === u?.id}
|
||||||
<OrgPermissionCan
|
>
|
||||||
I={OrgPermissionActions.Edit}
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
a={OrgPermissionSubjects.Member}
|
</IconButton>
|
||||||
>
|
</DropdownMenuTrigger>
|
||||||
{(isAllowed) => (
|
<DropdownMenuContent sideOffset={2} align="end">
|
||||||
<Button
|
<OrgPermissionCan
|
||||||
isDisabled={!isAllowed || isResendInvitePending}
|
I={OrgPermissionActions.Edit}
|
||||||
className="h-8 border-mineshaft-600 bg-mineshaft-700 font-normal"
|
a={OrgPermissionSubjects.Member}
|
||||||
colorSchema="primary"
|
>
|
||||||
variant="outline_bg"
|
{(isAllowed) => (
|
||||||
isLoading={
|
<DropdownMenuItem
|
||||||
isResendInvitePending && resendInviteId === orgMembershipId
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate({
|
||||||
|
to: "/organization/members/$membershipId" as const,
|
||||||
|
params: {
|
||||||
|
membershipId: orgMembershipId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
|
>
|
||||||
|
Edit User
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={<FontAwesomeIcon icon={faUserSlash} />}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (currentOrg?.scimEnabled) {
|
||||||
|
createNotification({
|
||||||
|
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
onClick={(e) => {
|
|
||||||
onResendInvite(orgMembershipId);
|
if (!isActive) {
|
||||||
e.stopPropagation();
|
// activate user
|
||||||
}}
|
await updateOrgMembership({
|
||||||
>
|
organizationId: orgId,
|
||||||
Resend Invite
|
membershipId: orgMembershipId,
|
||||||
</Button>
|
isActive: true
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
)}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="Options"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className={twMerge("w-6", userId === u?.id && "opacity-50")}
|
|
||||||
variant="plain"
|
|
||||||
isDisabled={userId === u?.id}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faEllipsisV} />
|
|
||||||
</IconButton>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent sideOffset={2} align="end">
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Edit}
|
|
||||||
a={OrgPermissionSubjects.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
navigate({
|
|
||||||
to: "/organization/members/$membershipId" as const,
|
|
||||||
params: {
|
|
||||||
membershipId: orgMembershipId
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
|
||||||
>
|
|
||||||
Edit User
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Delete}
|
|
||||||
a={OrgPermissionSubjects.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
icon={<FontAwesomeIcon icon={faUserSlash} />}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (currentOrg?.scimEnabled) {
|
return;
|
||||||
createNotification({
|
}
|
||||||
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isActive) {
|
// deactivate user
|
||||||
// activate user
|
handlePopUpOpen("deactivateMember", {
|
||||||
await updateOrgMembership({
|
orgMembershipId,
|
||||||
organizationId: orgId,
|
username
|
||||||
membershipId: orgMembershipId,
|
});
|
||||||
isActive: true
|
}}
|
||||||
});
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
{`${isActive ? "Deactivate" : "Activate"} User`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
return;
|
if (currentOrg?.scimEnabled && isActive) {
|
||||||
}
|
createNotification({
|
||||||
|
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
||||||
// deactivate user
|
type: "error"
|
||||||
handlePopUpOpen("deactivateMember", {
|
|
||||||
orgMembershipId,
|
|
||||||
username
|
|
||||||
});
|
});
|
||||||
}}
|
return;
|
||||||
isDisabled={!isAllowed}
|
}
|
||||||
>
|
|
||||||
{`${isActive ? "Deactivate" : "Activate"} User`}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Delete}
|
|
||||||
a={OrgPermissionSubjects.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (currentOrg?.scimEnabled && isActive) {
|
handlePopUpOpen("removeMember", {
|
||||||
createNotification({
|
orgMembershipId,
|
||||||
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
username
|
||||||
type: "error"
|
});
|
||||||
});
|
}}
|
||||||
return;
|
isDisabled={!isAllowed}
|
||||||
}
|
icon={<FontAwesomeIcon icon={faUserXmark} />}
|
||||||
|
>
|
||||||
handlePopUpOpen("removeMember", {
|
Remove User
|
||||||
orgMembershipId,
|
</DropdownMenuItem>
|
||||||
username
|
)}
|
||||||
});
|
</OrgPermissionCan>
|
||||||
}}
|
</DropdownMenuContent>
|
||||||
isDisabled={!isAllowed}
|
</DropdownMenu>
|
||||||
icon={<FontAwesomeIcon icon={faUserXmark} />}
|
</div>
|
||||||
>
|
</Td>
|
||||||
Remove User
|
</Tr>
|
||||||
</DropdownMenuItem>
|
);
|
||||||
)}
|
}
|
||||||
</OrgPermissionCan>
|
)}
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{Boolean(filteredUsers.length) && (
|
{Boolean(filteredMembers.length) && (
|
||||||
<Pagination
|
<Pagination
|
||||||
count={filteredUsers.length}
|
count={filteredMembers.length}
|
||||||
page={page}
|
page={page}
|
||||||
perPage={perPage}
|
perPage={perPage}
|
||||||
onChangePage={setPage}
|
onChangePage={setPage}
|
||||||
onChangePerPage={handlePerPageChange}
|
onChangePerPage={handlePerPageChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isMembersLoading && !filteredUsers?.length && (
|
{!isMembersLoading && !filteredMembers?.length && (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title={
|
title={
|
||||||
members.length
|
members.length
|
||||||
|
|||||||
+10
-8
@@ -1,11 +1,11 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faAnglesRight, faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faAnglesRight, faTrash } 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";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
|
import { Button, DeleteActionModal, Tooltip } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
@@ -221,12 +221,14 @@ export const SelectionPanel = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mt-3.5 flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
<div className="mt-3.5 flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
||||||
<Tooltip content="Clear">
|
<div className="mr-2 text-sm">{selectedCount} Selected</div>
|
||||||
<IconButton variant="plain" ariaLabel="clear-selection" onClick={resetSelectedEntries}>
|
<button
|
||||||
<FontAwesomeIcon icon={faMinusSquare} size="lg" />
|
type="button"
|
||||||
</IconButton>
|
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
|
||||||
</Tooltip>
|
onClick={resetSelectedEntries}
|
||||||
<div className="ml-1 flex-grow px-2 text-sm">{selectedCount} Selected</div>
|
>
|
||||||
|
Unselect All
|
||||||
|
</button>
|
||||||
{isRotatedSecretSelected && (
|
{isRotatedSecretSelected && (
|
||||||
<span className="text-sm text-mineshaft-400">
|
<span className="text-sm text-mineshaft-400">
|
||||||
Rotated Secrets will not be affected by action.
|
Rotated Secrets will not be affected by action.
|
||||||
|
|||||||
+8
-9
@@ -16,7 +16,6 @@ import {
|
|||||||
faFolderPlus,
|
faFolderPlus,
|
||||||
faKey,
|
faKey,
|
||||||
faLock,
|
faLock,
|
||||||
faMinusSquare,
|
|
||||||
faPaste,
|
faPaste,
|
||||||
faPlus,
|
faPlus,
|
||||||
faRotate,
|
faRotate,
|
||||||
@@ -1061,14 +1060,14 @@ export const ActionBar = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mt-3.5 flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
<div className="mt-3.5 flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
||||||
<Tooltip content="Clear">
|
<div className="mr-2 text-sm">{Object.keys(selectedSecrets).length} Selected</div>
|
||||||
<IconButton variant="plain" ariaLabel="clear-selection" onClick={resetSelectedSecret}>
|
<button
|
||||||
<FontAwesomeIcon icon={faMinusSquare} size="lg" />
|
type="button"
|
||||||
</IconButton>
|
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
|
||||||
</Tooltip>
|
onClick={resetSelectedSecret}
|
||||||
<div className="ml-2 flex-grow px-2 text-sm">
|
>
|
||||||
{Object.keys(selectedSecrets).length} Selected
|
Unselect All
|
||||||
</div>
|
</button>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
|||||||
Reference in New Issue
Block a user