mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 15:27:27 +00:00
Merge pull request #4629 from Infisical/feat/pagination-in-resource-overview-tables
feat: adds pagination to resource overview tables
This commit is contained in:
@@ -172,7 +172,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
email: true,
|
email: true,
|
||||||
id: true,
|
id: true,
|
||||||
superAdmin: true
|
superAdmin: true
|
||||||
}).array()
|
}).array(),
|
||||||
|
total: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -182,13 +183,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const users = await server.services.superAdmin.getUsers({
|
const result = await server.services.superAdmin.getUsers({
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
users
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -230,7 +229,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
createdAt: z.date()
|
createdAt: z.date()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
}).array()
|
}).array(),
|
||||||
|
total: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -240,13 +240,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const organizations = await server.services.superAdmin.getOrganizations({
|
const result = await server.services.superAdmin.getOrganizations({
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
organizations
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -337,7 +335,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
.extend({
|
.extend({
|
||||||
isInstanceAdmin: z.boolean()
|
isInstanceAdmin: z.boolean()
|
||||||
})
|
})
|
||||||
.array()
|
.array(),
|
||||||
|
total: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -347,13 +346,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const identities = await server.services.superAdmin.getIdentities({
|
const result = await server.services.superAdmin.getIdentities({
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
identities
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,11 +50,23 @@ export const identityDALFactory = (db: TDbClient) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const countQuery = query.clone();
|
||||||
|
|
||||||
if (sortBy) {
|
if (sortBy) {
|
||||||
query = query.orderBy(sortBy);
|
query = query.orderBy(sortBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity));
|
const [identities, totalResult] = await Promise.all([
|
||||||
|
query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity)),
|
||||||
|
countQuery.countDistinct(`${TableName.Identity}.id`, { as: "count" }).first()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const total = Number(totalResult?.count || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
identities,
|
||||||
|
total
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Get identities by filter" });
|
throw new DatabaseError({ error, name: "Get identities by filter" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
sortBy?: keyof TOrganizations;
|
sortBy?: keyof TOrganizations;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const query = db.replicaNode()(TableName.Organization);
|
|
||||||
|
|
||||||
// Build the subquery for limited organization IDs
|
// Build the subquery for limited organization IDs
|
||||||
const orgSubquery = db.replicaNode().select("id").from(TableName.Organization);
|
const orgSubquery = db.replicaNode().select("id").from(TableName.Organization);
|
||||||
|
|
||||||
@@ -54,37 +52,45 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const countQuery = orgSubquery.clone();
|
||||||
|
|
||||||
if (sortBy) {
|
if (sortBy) {
|
||||||
void orgSubquery.orderBy(sortBy);
|
void orgSubquery.orderBy(sortBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
void orgSubquery.limit(limit).offset(offset);
|
void orgSubquery.limit(limit).offset(offset);
|
||||||
|
|
||||||
// Main query with joins, limited to the subquery results
|
const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => {
|
||||||
const docs = await query
|
return db
|
||||||
.whereIn(`${TableName.Organization}.id`, orgSubquery)
|
.replicaNode()(TableName.Organization)
|
||||||
.leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
|
.whereIn(`${TableName.Organization}.id`, orgIdSubquery)
|
||||||
.leftJoin(TableName.OrgMembership, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
.leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
|
||||||
.leftJoin(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
.leftJoin(TableName.OrgMembership, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||||
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
.leftJoin(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
.where((qb) => {
|
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
void qb.where(`${TableName.Users}.isGhost`, false).orWhereNull(`${TableName.Users}.id`);
|
.where((qb) => {
|
||||||
})
|
void qb.where(`${TableName.Users}.isGhost`, false).orWhereNull(`${TableName.Users}.id`);
|
||||||
.select(selectAllTableCols(TableName.Organization))
|
});
|
||||||
.select(db.ref("name").withSchema(TableName.Project).as("projectName"))
|
};
|
||||||
.select(db.ref("id").withSchema(TableName.Project).as("projectId"))
|
|
||||||
.select(db.ref("slug").withSchema(TableName.Project).as("projectSlug"))
|
const [docs, totalResult] = await Promise.all([
|
||||||
.select(db.ref("createdAt").withSchema(TableName.Project).as("projectCreatedAt"))
|
buildBaseQuery(orgSubquery)
|
||||||
.select(db.ref("email").withSchema(TableName.Users).as("userEmail"))
|
.select(selectAllTableCols(TableName.Organization))
|
||||||
.select(db.ref("username").withSchema(TableName.Users).as("username"))
|
.select(db.ref("name").withSchema(TableName.Project).as("projectName"))
|
||||||
.select(db.ref("firstName").withSchema(TableName.Users).as("firstName"))
|
.select(db.ref("id").withSchema(TableName.Project).as("projectId"))
|
||||||
.select(db.ref("lastName").withSchema(TableName.Users).as("lastName"))
|
.select(db.ref("slug").withSchema(TableName.Project).as("projectSlug"))
|
||||||
.select(db.ref("id").withSchema(TableName.Users).as("userId"))
|
.select(db.ref("createdAt").withSchema(TableName.Project).as("projectCreatedAt"))
|
||||||
.select(db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"))
|
.select(db.ref("email").withSchema(TableName.Users).as("userEmail"))
|
||||||
.select(db.ref("role").withSchema(TableName.OrgMembership).as("orgMembershipRole"))
|
.select(db.ref("username").withSchema(TableName.Users).as("username"))
|
||||||
.select(db.ref("roleId").withSchema(TableName.OrgMembership).as("orgMembershipRoleId"))
|
.select(db.ref("firstName").withSchema(TableName.Users).as("firstName"))
|
||||||
.select(db.ref("status").withSchema(TableName.OrgMembership).as("orgMembershipStatus"))
|
.select(db.ref("lastName").withSchema(TableName.Users).as("lastName"))
|
||||||
.select(db.ref("name").withSchema(TableName.OrgRoles).as("orgMembershipRoleName"));
|
.select(db.ref("id").withSchema(TableName.Users).as("userId"))
|
||||||
|
.select(db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"))
|
||||||
|
.select(db.ref("role").withSchema(TableName.OrgMembership).as("orgMembershipRole"))
|
||||||
|
.select(db.ref("roleId").withSchema(TableName.OrgMembership).as("orgMembershipRoleId"))
|
||||||
|
.select(db.ref("status").withSchema(TableName.OrgMembership).as("orgMembershipStatus"))
|
||||||
|
.select(db.ref("name").withSchema(TableName.OrgRoles).as("orgMembershipRoleName")),
|
||||||
|
buildBaseQuery(countQuery).countDistinct(`${TableName.Organization}.id`, { as: "count" }).first()
|
||||||
|
]);
|
||||||
|
|
||||||
const formattedDocs = sqlNestRelationships({
|
const formattedDocs = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
@@ -132,7 +138,12 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
return formattedDocs;
|
const total = Number(totalResult?.count || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizations: formattedDocs,
|
||||||
|
total
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find organizations by filter" });
|
throw new DatabaseError({ error, name: "Find organizations by filter" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -741,13 +741,12 @@ export const superAdminServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getOrganizations = async ({ offset, limit, searchTerm }: TGetOrganizationsDTO) => {
|
const getOrganizations = async ({ offset, limit, searchTerm }: TGetOrganizationsDTO) => {
|
||||||
const organizations = await orgDAL.findOrganizationsByFilter({
|
return orgDAL.findOrganizationsByFilter({
|
||||||
offset,
|
offset,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
sortBy: "name",
|
sortBy: "name",
|
||||||
limit
|
limit
|
||||||
});
|
});
|
||||||
return organizations;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createOrganization = async (
|
const createOrganization = async (
|
||||||
@@ -1015,7 +1014,7 @@ export const superAdminServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
|
const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
|
||||||
const identities = await identityDAL.getIdentitiesByFilter({
|
const result = await identityDAL.getIdentitiesByFilter({
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
@@ -1023,10 +1022,13 @@ export const superAdminServiceFactory = ({
|
|||||||
});
|
});
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
return identities.map((identity) => ({
|
return {
|
||||||
...identity,
|
identities: result.identities.map((identity) => ({
|
||||||
isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id))
|
...identity,
|
||||||
}));
|
isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id))
|
||||||
|
})),
|
||||||
|
total: result.total
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const grantServerAdminAccessToUser = async (userId: string) => {
|
const grantServerAdminAccessToUser = async (userId: string) => {
|
||||||
|
|||||||
@@ -60,11 +60,20 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
query = query.where("superAdmin", true);
|
query = query.where("superAdmin", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const countQuery = query.clone();
|
||||||
|
|
||||||
if (sortBy) {
|
if (sortBy) {
|
||||||
query = query.orderBy(sortBy);
|
query = query.orderBy(sortBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users));
|
const [users, totalResult] = await Promise.all([
|
||||||
|
query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users)),
|
||||||
|
countQuery.count("*", { as: "count" }).first()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const total = Number(totalResult?.count || 0);
|
||||||
|
|
||||||
|
return { users, total };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Get users by filter" });
|
throw new DatabaseError({ error, name: "Get users by filter" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
import {
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
DefaultError,
|
|
||||||
InfiniteData,
|
|
||||||
UndefinedInitialDataInfiniteOptions,
|
|
||||||
useInfiniteQuery,
|
|
||||||
useQuery,
|
|
||||||
UseQueryOptions
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { Identity } from "@app/hooks/api/identities/types";
|
|
||||||
|
|
||||||
import { User } from "../types";
|
|
||||||
import {
|
import {
|
||||||
AdminGetIdentitiesFilters,
|
AdminGetIdentitiesFilters,
|
||||||
AdminGetOrganizationsFilters,
|
AdminGetOrganizationsFilters,
|
||||||
AdminGetUsersFilters,
|
AdminGetUsersFilters,
|
||||||
AdminIntegrationsConfig,
|
AdminIntegrationsConfig,
|
||||||
OrganizationWithProjects,
|
|
||||||
TGetEnvOverrides,
|
TGetEnvOverrides,
|
||||||
|
TGetIdentitiesResponse,
|
||||||
TGetInvalidatingCacheStatus,
|
TGetInvalidatingCacheStatus,
|
||||||
|
TGetOrganizationsResponse,
|
||||||
TGetServerRootKmsEncryptionDetails,
|
TGetServerRootKmsEncryptionDetails,
|
||||||
|
TGetUsersResponse,
|
||||||
TServerConfig
|
TServerConfig
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -49,24 +42,21 @@ export const fetchServerConfig = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useAdminGetOrganizations = (filters: AdminGetOrganizationsFilters) => {
|
export const useAdminGetOrganizations = (filters: AdminGetOrganizationsFilters) => {
|
||||||
return useInfiniteQuery({
|
return useQuery({
|
||||||
initialPageParam: 0,
|
|
||||||
queryKey: adminQueryKeys.getOrganizations(filters),
|
queryKey: adminQueryKeys.getOrganizations(filters),
|
||||||
queryFn: async ({ pageParam }) => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ organizations: OrganizationWithProjects[] }>(
|
const { data } = await apiRequest.get<TGetOrganizationsResponse>(
|
||||||
"/api/v1/admin/organization-management/organizations",
|
"/api/v1/admin/organization-management/organizations",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
...filters,
|
...filters
|
||||||
offset: pageParam
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.organizations;
|
return { organizations: data.organizations, totalCount: data.total };
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage, pages) =>
|
placeholderData: (previousData) => previousData
|
||||||
lastPage.length !== 0 ? pages.length * filters.limit : undefined
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,59 +80,41 @@ export const useGetServerConfig = ({
|
|||||||
enabled: options?.enabled ?? true
|
enabled: options?.enabled ?? true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAdminGetUsers = (
|
export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
|
||||||
filters: AdminGetUsersFilters,
|
return useQuery({
|
||||||
options?: Partial<
|
|
||||||
UndefinedInitialDataInfiniteOptions<
|
|
||||||
User[],
|
|
||||||
DefaultError,
|
|
||||||
InfiniteData<User[]>,
|
|
||||||
ReturnType<typeof adminQueryKeys.getUsers>,
|
|
||||||
number
|
|
||||||
>
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
return useInfiniteQuery({
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryKey: adminQueryKeys.getUsers(filters),
|
queryKey: adminQueryKeys.getUsers(filters),
|
||||||
queryFn: async ({ pageParam }) => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ users: User[] }>(
|
const { data } = await apiRequest.get<TGetUsersResponse>(
|
||||||
"/api/v1/admin/user-management/users",
|
"/api/v1/admin/user-management/users",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
...filters,
|
...filters
|
||||||
offset: pageParam
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.users;
|
return { users: data.users, totalCount: data.total };
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage, pages) =>
|
placeholderData: (previousData) => previousData
|
||||||
lastPage.length !== 0 ? pages.length * filters.limit : undefined,
|
|
||||||
...options
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => {
|
export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => {
|
||||||
return useInfiniteQuery({
|
return useQuery({
|
||||||
initialPageParam: 0,
|
|
||||||
queryKey: adminQueryKeys.getIdentities(filters),
|
queryKey: adminQueryKeys.getIdentities(filters),
|
||||||
queryFn: async ({ pageParam }) => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ identities: Identity[] }>(
|
const { data } = await apiRequest.get<TGetIdentitiesResponse>(
|
||||||
"/api/v1/admin/identity-management/identities",
|
"/api/v1/admin/identity-management/identities",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
...filters,
|
...filters
|
||||||
offset: pageParam
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.identities;
|
return { identities: data.identities, totalCount: data.total };
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage, pages) =>
|
placeholderData: (previousData) => previousData
|
||||||
lastPage.length !== 0 ? pages.length * filters.limit : undefined
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { Identity } from "@app/hooks/api/identities/types";
|
||||||
import { OrgMembershipStatus } from "@app/hooks/api/organization/types";
|
import { OrgMembershipStatus } from "@app/hooks/api/organization/types";
|
||||||
|
|
||||||
import { Organization } from "../types";
|
import { Organization, User } from "../types";
|
||||||
|
|
||||||
export enum LoginMethod {
|
export enum LoginMethod {
|
||||||
EMAIL = "email",
|
EMAIL = "email",
|
||||||
@@ -34,6 +35,21 @@ export type OrganizationWithProjects = Organization & {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetOrganizationsResponse = {
|
||||||
|
organizations: OrganizationWithProjects[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetIdentitiesResponse = {
|
||||||
|
identities: Identity[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetUsersResponse = {
|
||||||
|
users: User[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type TServerConfig = {
|
export type TServerConfig = {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
allowSignUp: boolean;
|
allowSignUp: boolean;
|
||||||
@@ -81,19 +97,22 @@ export type TCreateAdminUserDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AdminGetOrganizationsFilters = {
|
export type AdminGetOrganizationsFilters = {
|
||||||
limit: number;
|
searchTerm?: string;
|
||||||
searchTerm: string;
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminGetUsersFilters = {
|
export type AdminGetUsersFilters = {
|
||||||
limit: number;
|
limit?: number;
|
||||||
searchTerm: string;
|
offset?: number;
|
||||||
adminsOnly: boolean;
|
searchTerm?: string;
|
||||||
|
adminsOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminGetIdentitiesFilters = {
|
export type AdminGetIdentitiesFilters = {
|
||||||
limit: number;
|
limit?: number;
|
||||||
searchTerm: string;
|
offset?: number;
|
||||||
|
searchTerm?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminIntegrationsConfig = {
|
export type AdminIntegrationsConfig = {
|
||||||
|
|||||||
@@ -103,29 +103,29 @@ export type TWorkspaceUser = {
|
|||||||
organization: string;
|
organization: string;
|
||||||
roles: (
|
roles: (
|
||||||
| {
|
| {
|
||||||
id: string;
|
id: string;
|
||||||
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
||||||
customRoleId: string;
|
customRoleId: string;
|
||||||
customRoleName: string;
|
customRoleName: string;
|
||||||
customRoleSlug: string;
|
customRoleSlug: string;
|
||||||
isTemporary: false;
|
isTemporary: false;
|
||||||
temporaryRange: null;
|
temporaryRange: null;
|
||||||
temporaryMode: null;
|
temporaryMode: null;
|
||||||
temporaryAccessEndTime: null;
|
temporaryAccessEndTime: null;
|
||||||
temporaryAccessStartTime: null;
|
temporaryAccessStartTime: null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
id: string;
|
id: string;
|
||||||
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
||||||
customRoleId: string;
|
customRoleId: string;
|
||||||
customRoleName: string;
|
customRoleName: string;
|
||||||
customRoleSlug: string;
|
customRoleSlug: string;
|
||||||
isTemporary: true;
|
isTemporary: true;
|
||||||
temporaryRange: string;
|
temporaryRange: string;
|
||||||
temporaryMode: ProjectUserMembershipTemporaryMode;
|
temporaryMode: ProjectUserMembershipTemporaryMode;
|
||||||
temporaryAccessEndTime: string;
|
temporaryAccessEndTime: string;
|
||||||
temporaryAccessStartTime: string;
|
temporaryAccessStartTime: string;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
status: "invited" | "accepted" | "verified" | "completed";
|
status: "invited" | "accepted" | "verified" | "completed";
|
||||||
deniedPermissions: any[];
|
deniedPermissions: any[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -55,18 +55,14 @@ const Content = ({ onClose }: ContentProps) => {
|
|||||||
const [searchUserFilter, setSearchUserFilter] = useState("");
|
const [searchUserFilter, setSearchUserFilter] = useState("");
|
||||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||||
|
|
||||||
const { data, isFetching } = useAdminGetUsers(
|
const { data, isPending } = useAdminGetUsers({
|
||||||
{
|
limit: 20,
|
||||||
limit: 20,
|
searchTerm: debouncedSearchTerm,
|
||||||
searchTerm: debouncedSearchTerm,
|
adminsOnly: false
|
||||||
adminsOnly: false
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
placeholderData: (prev) => prev
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const users = useMemo(() => data?.pages.flat().filter((user) => !user.superAdmin) ?? [], [data]);
|
const { users: usersData = [] } = data ?? {};
|
||||||
|
const users = usersData.filter((user) => !user.superAdmin);
|
||||||
|
|
||||||
const onSubmit = async ({ user }: FormData) => {
|
const onSubmit = async ({ user }: FormData) => {
|
||||||
try {
|
try {
|
||||||
@@ -91,7 +87,7 @@ const Content = ({ onClose }: ContentProps) => {
|
|||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="User">
|
<FormControl isError={Boolean(error)} errorText={error?.message} label="User">
|
||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
isLoading={searchUserFilter !== debouncedSearchTerm || isFetching}
|
isLoading={searchUserFilter !== debouncedSearchTerm || isPending}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder="Search users..."
|
placeholder="Search users..."
|
||||||
options={users}
|
options={users}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
faXmark
|
faXmark
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { InfiniteData } from "@tanstack/react-query";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -39,7 +39,12 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useSubscription, useUser } from "@app/context";
|
import { useSubscription, useUser } from "@app/context";
|
||||||
import { useDebounce, usePopUp } from "@app/hooks";
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useAdminBulkDeleteUsers,
|
useAdminBulkDeleteUsers,
|
||||||
useAdminDeleteUser,
|
useAdminDeleteUser,
|
||||||
@@ -54,15 +59,17 @@ const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions f
|
|||||||
|
|
||||||
const ServerAdminsPanelTable = ({
|
const ServerAdminsPanelTable = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
users: usersPages,
|
users,
|
||||||
isPending,
|
isPending,
|
||||||
searchUserFilter,
|
searchUserFilter,
|
||||||
setSearchUserFilter,
|
setSearchUserFilter,
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
selectedUsers,
|
selectedUsers,
|
||||||
setSelectedUsers
|
setSelectedUsers,
|
||||||
|
totalCount,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPage,
|
||||||
|
handlePerPageChange
|
||||||
}: {
|
}: {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<
|
popUpName: keyof UsePopUpState<
|
||||||
@@ -75,20 +82,20 @@ const ServerAdminsPanelTable = ({
|
|||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
users: InfiniteData<User[], unknown> | undefined;
|
users?: User[];
|
||||||
searchUserFilter: string;
|
searchUserFilter: string;
|
||||||
setSearchUserFilter: (filter: string) => void;
|
setSearchUserFilter: (filter: string) => void;
|
||||||
selectedUsers: User[];
|
selectedUsers: User[];
|
||||||
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
|
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
|
||||||
isFetchingNextPage: boolean;
|
totalCount: number;
|
||||||
fetchNextPage: () => void;
|
page: number;
|
||||||
hasNextPage: boolean;
|
perPage: number;
|
||||||
|
setPage: Dispatch<SetStateAction<number>>;
|
||||||
|
handlePerPageChange: (newPerPage: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const users = usersPages?.pages.flat();
|
const isEmpty = !isPending && totalCount === 0;
|
||||||
|
|
||||||
const isEmpty = !isPending && !users?.length;
|
|
||||||
|
|
||||||
const selectedUserIds = selectedUsers.map((user) => user.id);
|
const selectedUserIds = selectedUsers.map((user) => user.id);
|
||||||
|
|
||||||
@@ -241,16 +248,13 @@ const ServerAdminsPanelTable = ({
|
|||||||
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
|
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
{!isEmpty && (
|
{!isEmpty && (
|
||||||
<Button
|
<Pagination
|
||||||
className="mt-4 py-3 text-sm"
|
count={totalCount}
|
||||||
isFullWidth
|
page={page}
|
||||||
variant="outline_bg"
|
perPage={perPage}
|
||||||
isLoading={isFetchingNextPage}
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
onChangePerPage={handlePerPageChange}
|
||||||
onClick={() => fetchNextPage()}
|
/>
|
||||||
>
|
|
||||||
{hasNextPage ? "Load More" : "End of List"}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -278,18 +282,24 @@ export const ServerAdminsTable = () => {
|
|||||||
const [searchUserFilter, setSearchUserFilter] = useState("");
|
const [searchUserFilter, setSearchUserFilter] = useState("");
|
||||||
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||||
|
|
||||||
const {
|
const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", {
|
||||||
data: users,
|
initPerPage: getUserTablePreference("ServerAdminUsersTable", PreferenceKey.PerPage, 20)
|
||||||
isPending,
|
});
|
||||||
isFetchingNextPage,
|
|
||||||
hasNextPage,
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
fetchNextPage
|
setPerPage(newPerPage);
|
||||||
} = useAdminGetUsers({
|
setUserTablePreference("ServerAdminUsersTable", PreferenceKey.PerPage, newPerPage);
|
||||||
limit: 20,
|
};
|
||||||
|
|
||||||
|
const { data, isPending } = useAdminGetUsers({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
searchTerm: debouncedSearchTerm,
|
searchTerm: debouncedSearchTerm,
|
||||||
adminsOnly: true
|
adminsOnly: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { users = [], totalCount = 0 } = data ?? {};
|
||||||
|
|
||||||
const handleRemoveUser = async () => {
|
const handleRemoveUser = async () => {
|
||||||
const { id } = popUp?.removeUser?.data as { id: string; username: string };
|
const { id } = popUp?.removeUser?.data as { id: string; username: string };
|
||||||
|
|
||||||
@@ -389,9 +399,11 @@ export const ServerAdminsTable = () => {
|
|||||||
searchUserFilter={searchUserFilter}
|
searchUserFilter={searchUserFilter}
|
||||||
setSearchUserFilter={setSearchUserFilter}
|
setSearchUserFilter={setSearchUserFilter}
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
page={page}
|
||||||
hasNextPage={hasNextPage}
|
perPage={perPage}
|
||||||
fetchNextPage={fetchNextPage}
|
setPage={setPage}
|
||||||
|
handlePerPageChange={handlePerPageChange}
|
||||||
|
totalCount={totalCount}
|
||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.removeUser.isOpen}
|
isOpen={popUp.removeUser.isOpen}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -72,18 +72,13 @@ const Content = ({ onClose }: ContentProps) => {
|
|||||||
const [searchUserFilter, setSearchUserFilter] = useState("");
|
const [searchUserFilter, setSearchUserFilter] = useState("");
|
||||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||||
|
|
||||||
const { data, isFetching } = useAdminGetUsers(
|
const { data, isPending } = useAdminGetUsers({
|
||||||
{
|
limit: 20,
|
||||||
limit: 20,
|
searchTerm: debouncedSearchTerm,
|
||||||
searchTerm: debouncedSearchTerm,
|
adminsOnly: false
|
||||||
adminsOnly: false
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
placeholderData: (prev) => prev
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const users = useMemo(() => data?.pages.flat() ?? [], [data]);
|
const { users = [] } = data ?? {};
|
||||||
|
|
||||||
const onSubmit = async ({ name, invitees }: FormData) => {
|
const onSubmit = async ({ name, invitees }: FormData) => {
|
||||||
try {
|
try {
|
||||||
@@ -155,7 +150,7 @@ const Content = ({ onClose }: ContentProps) => {
|
|||||||
})
|
})
|
||||||
.includes(input)
|
.includes(input)
|
||||||
}
|
}
|
||||||
isLoading={searchUserFilter !== debouncedSearchTerm || isFetching}
|
isLoading={searchUserFilter !== debouncedSearchTerm || isPending}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder="Search users or invite new ones..."
|
placeholder="Search users or invite new ones..."
|
||||||
isMulti
|
isMulti
|
||||||
|
|||||||
+92
-74
@@ -11,7 +11,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -29,7 +29,12 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useDebounce, usePopUp } from "@app/hooks";
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import { useAdminRemoveIdentitySuperAdminAccess } from "@app/hooks/api/admin";
|
import { useAdminRemoveIdentitySuperAdminAccess } from "@app/hooks/api/admin";
|
||||||
import { useAdminGetIdentities } from "@app/hooks/api/admin/queries";
|
import { useAdminGetIdentities } from "@app/hooks/api/admin/queries";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -48,14 +53,34 @@ const IdentityPanelTable = ({
|
|||||||
const [searchIdentityFilter, setSearchIdentityFilter] = useState("");
|
const [searchIdentityFilter, setSearchIdentityFilter] = useState("");
|
||||||
const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500);
|
const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500);
|
||||||
|
|
||||||
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetIdentities(
|
const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", {
|
||||||
{
|
initPerPage: getUserTablePreference(
|
||||||
limit: 20,
|
"ResourceOverviewIdentitiesTable",
|
||||||
searchTerm: debouncedSearchTerm
|
PreferenceKey.PerPage,
|
||||||
}
|
10
|
||||||
);
|
)
|
||||||
|
});
|
||||||
|
|
||||||
const isEmpty = !isPending && !data?.pages?.[0].length;
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("ResourceOverviewIdentitiesTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isPending } = useAdminGetIdentities({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
searchTerm: debouncedSearchTerm
|
||||||
|
});
|
||||||
|
|
||||||
|
const { identities, totalCount = 0 } = data ?? {};
|
||||||
|
|
||||||
|
const isEmpty = !isPending && !totalCount;
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -80,75 +105,68 @@ const IdentityPanelTable = ({
|
|||||||
<TBody>
|
<TBody>
|
||||||
{isPending && <TableSkeleton columns={2} innerKey="identities" />}
|
{isPending && <TableSkeleton columns={2} innerKey="identities" />}
|
||||||
{!isPending &&
|
{!isPending &&
|
||||||
data?.pages?.map((identities) =>
|
identities?.map(({ name, id, isInstanceAdmin }) => (
|
||||||
identities.map(({ name, id, isInstanceAdmin }) => (
|
<Tr key={`identity-${id}`} className="w-full">
|
||||||
<Tr key={`identity-${id}`} className="w-full">
|
<Td>
|
||||||
<Td>
|
{name}
|
||||||
{name}
|
{isInstanceAdmin && (
|
||||||
{isInstanceAdmin && (
|
<Badge variant="primary" className="ml-2">
|
||||||
<Badge variant="primary" className="ml-2">
|
Server Admin
|
||||||
Server Admin
|
</Badge>
|
||||||
</Badge>
|
)}
|
||||||
)}
|
</Td>
|
||||||
</Td>
|
<Td>
|
||||||
<Td>
|
{isInstanceAdmin && (
|
||||||
{isInstanceAdmin && (
|
<div className="flex justify-end">
|
||||||
<div className="flex justify-end">
|
<DropdownMenu>
|
||||||
<DropdownMenu>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<IconButton
|
||||||
<IconButton
|
ariaLabel="Options"
|
||||||
ariaLabel="Options"
|
colorSchema="secondary"
|
||||||
colorSchema="secondary"
|
className="w-6"
|
||||||
className="w-6"
|
variant="plain"
|
||||||
variant="plain"
|
>
|
||||||
>
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
<FontAwesomeIcon icon={faEllipsisV} />
|
</IconButton>
|
||||||
</IconButton>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent sideOffset={2} align="end">
|
||||||
<DropdownMenuContent sideOffset={2} align="end">
|
<DropdownMenuItem
|
||||||
{isInstanceAdmin && (
|
onClick={(e) => {
|
||||||
<DropdownMenuItem
|
e.stopPropagation();
|
||||||
onClick={(e) => {
|
handlePopUpOpen("removeServerAdmin", { name, id });
|
||||||
e.stopPropagation();
|
}}
|
||||||
handlePopUpOpen("removeServerAdmin", { name, id });
|
icon={
|
||||||
}}
|
<div className="relative">
|
||||||
icon={
|
<FontAwesomeIcon icon={faShieldHalved} />
|
||||||
<div className="relative">
|
<FontAwesomeIcon
|
||||||
<FontAwesomeIcon icon={faShieldHalved} />
|
className="absolute -bottom-[0.01rem] -right-1"
|
||||||
<FontAwesomeIcon
|
size="2xs"
|
||||||
className="absolute -bottom-[0.01rem] -right-1"
|
icon={faXmark}
|
||||||
size="2xs"
|
/>
|
||||||
icon={faXmark}
|
</div>
|
||||||
/>
|
}
|
||||||
</div>
|
>
|
||||||
}
|
Remove Server Admin
|
||||||
>
|
</DropdownMenuItem>
|
||||||
Remove Server Admin
|
</DropdownMenuContent>
|
||||||
</DropdownMenuItem>
|
</DropdownMenu>
|
||||||
)}
|
</div>
|
||||||
</DropdownMenuContent>
|
)}
|
||||||
</DropdownMenu>
|
</Td>
|
||||||
</div>
|
</Tr>
|
||||||
)}
|
))}
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{!isPending && isEmpty && <EmptyState title="No identities found" icon={faWrench} />}
|
{!isPending && isEmpty && <EmptyState title="No identities found" icon={faWrench} />}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
{!isEmpty && (
|
{!isPending && totalCount > 0 && (
|
||||||
<Button
|
<Pagination
|
||||||
className="mt-4 py-3 text-sm"
|
count={totalCount}
|
||||||
isFullWidth
|
page={page}
|
||||||
variant="outline_bg"
|
perPage={perPage}
|
||||||
isLoading={isFetchingNextPage}
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
onChangePerPage={handlePerPageChange}
|
||||||
onClick={() => fetchNextPage()}
|
/>
|
||||||
>
|
|
||||||
{hasNextPage ? "Load More" : "End of List"}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useUser } from "@app/context";
|
import { useUser } from "@app/context";
|
||||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||||
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useAdminDeleteOrganization,
|
useAdminDeleteOrganization,
|
||||||
@@ -443,13 +448,31 @@ const OrganizationsPanelTable = ({
|
|||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
|
||||||
useAdminGetOrganizations({
|
|
||||||
limit: 20,
|
|
||||||
searchTerm: debouncedSearchTerm
|
|
||||||
});
|
|
||||||
|
|
||||||
const isEmpty = !isPending && !data?.pages?.[0].length;
|
const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", {
|
||||||
|
initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isPending } = useAdminGetOrganizations({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
searchTerm: debouncedSearchTerm
|
||||||
|
});
|
||||||
|
|
||||||
|
const { organizations, totalCount = 0 } = data ?? {};
|
||||||
|
|
||||||
|
const isEmpty = !isPending && !totalCount;
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: accessOrganization } = useServerAdminAccessOrg();
|
const { mutateAsync: accessOrganization } = useServerAdminAccessOrg();
|
||||||
|
|
||||||
@@ -499,125 +522,120 @@ const OrganizationsPanelTable = ({
|
|||||||
<TBody>
|
<TBody>
|
||||||
{isPending && <TableSkeleton columns={4} innerKey="organizations" />}
|
{isPending && <TableSkeleton columns={4} innerKey="organizations" />}
|
||||||
{!isPending &&
|
{!isPending &&
|
||||||
data?.pages?.map((orgs) =>
|
organizations?.map((org) => {
|
||||||
orgs.map((org) => {
|
const isMember = org.members.find((member) => member.user.id === user.id);
|
||||||
const isMember = org.members.find((member) => member.user.id === user.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr key={`org-${org.id}`} className="w-full">
|
<Tr key={`org-${org.id}`} className="w-full">
|
||||||
<Td className="w-1/2 max-w-0">
|
<Td className="w-1/2 max-w-0">
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5">
|
||||||
{org.name ? (
|
{org.name ? (
|
||||||
<p className="truncate">{org.name}</p>
|
<p className="truncate">{org.name}</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-mineshaft-400">Not Set</span>
|
<span className="text-mineshaft-400">Not Set</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="w-1/3">
|
<Td className="w-1/3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handlePopUpOpen("viewMembers", { organization: org })}
|
onClick={() => handlePopUpOpen("viewMembers", { organization: org })}
|
||||||
className="flex items-center hover:underline"
|
className="flex items-center hover:underline"
|
||||||
>
|
>
|
||||||
<Tooltip className="text-center" content="View Members">
|
<Tooltip className="text-center" content="View Members">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faEye}
|
icon={faEye}
|
||||||
className="mr-1.5 text-mineshaft-300"
|
className="mr-1.5 text-mineshaft-300"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{org.members.length} {org.members.length === 1 ? "Member" : "Members"}
|
||||||
|
{!org.members.some(
|
||||||
|
(member) =>
|
||||||
|
member.role === OrgMembershipRole.Admin &&
|
||||||
|
member.status === OrgMembershipStatus.Accepted
|
||||||
|
) && (
|
||||||
|
<Tooltip content="No admins have accepted their invitations.">
|
||||||
|
<div className="ml-1.5">
|
||||||
|
<FontAwesomeIcon className="text-yellow" icon={faWarning} />
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{org.members.length} {org.members.length === 1 ? "Member" : "Members"}
|
)}
|
||||||
{!org.members.some(
|
</button>
|
||||||
(member) =>
|
</Td>
|
||||||
member.role === OrgMembershipRole.Admin &&
|
<Td className="w-1/3">
|
||||||
member.status === OrgMembershipStatus.Accepted
|
{org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"}
|
||||||
) && (
|
</Td>
|
||||||
<Tooltip content="No admins have accepted their invitations.">
|
<Td>
|
||||||
<div className="ml-1.5">
|
<div className="flex justify-end gap-x-1">
|
||||||
<FontAwesomeIcon className="text-yellow" icon={faWarning} />
|
{isMember && (
|
||||||
</div>
|
<Tooltip
|
||||||
</Tooltip>
|
className="text-center"
|
||||||
)}
|
content="You are a member of this organization"
|
||||||
</button>
|
>
|
||||||
</Td>
|
<div>
|
||||||
<Td className="w-1/3">
|
<FontAwesomeIcon
|
||||||
{org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"}
|
className="text-mineshaft-400"
|
||||||
</Td>
|
icon={faUserCheck}
|
||||||
<Td>
|
size="sm"
|
||||||
<div className="flex justify-end gap-x-1">
|
/>
|
||||||
{isMember && (
|
</div>
|
||||||
<Tooltip
|
</Tooltip>
|
||||||
className="text-center"
|
)}
|
||||||
content="You are a member of this organization"
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Options"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="w-6"
|
||||||
|
variant="plain"
|
||||||
>
|
>
|
||||||
<div>
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
<FontAwesomeIcon
|
</IconButton>
|
||||||
className="text-mineshaft-400"
|
</DropdownMenuTrigger>
|
||||||
icon={faUserCheck}
|
<DropdownMenuContent sideOffset={2} align="end">
|
||||||
size="sm"
|
{!isMember && (
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="Options"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="w-6"
|
|
||||||
variant="plain"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faEllipsisV} />
|
|
||||||
</IconButton>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent sideOffset={2} align="end">
|
|
||||||
{!isMember && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleAccessOrg(org.id);
|
|
||||||
}}
|
|
||||||
icon={<FontAwesomeIcon icon={faUserPlus} />}
|
|
||||||
>
|
|
||||||
Join Organization
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handlePopUpOpen("deleteOrganization", {
|
handleAccessOrg(org.id);
|
||||||
orgId: org.id,
|
|
||||||
orgName: org.name
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
icon={<FontAwesomeIcon icon={faUserPlus} />}
|
||||||
>
|
>
|
||||||
Delete Organization
|
Join Organization
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
)}
|
||||||
</DropdownMenu>
|
<DropdownMenuItem
|
||||||
</div>
|
onClick={(e) => {
|
||||||
</Td>
|
e.stopPropagation();
|
||||||
</Tr>
|
handlePopUpOpen("deleteOrganization", {
|
||||||
);
|
orgId: org.id,
|
||||||
})
|
orgName: org.name
|
||||||
)}
|
});
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete Organization
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{!isPending && isEmpty && <EmptyState title="No organizations found" icon={faBuilding} />}
|
{!isPending && isEmpty && <EmptyState title="No organizations found" icon={faBuilding} />}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
{!isEmpty && (
|
{!isPending && totalCount > 0 && (
|
||||||
<Button
|
<Pagination
|
||||||
className="mt-4 py-3 text-sm"
|
count={totalCount}
|
||||||
isFullWidth
|
page={page}
|
||||||
variant="outline_bg"
|
perPage={perPage}
|
||||||
isLoading={isFetchingNextPage}
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
onChangePerPage={handlePerPageChange}
|
||||||
onClick={() => fetchNextPage()}
|
/>
|
||||||
>
|
|
||||||
{hasNextPage ? "Load More" : "End of list"}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ViewMembersModal
|
<ViewMembersModal
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
faXmark
|
faXmark
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { InfiniteData } from "@tanstack/react-query";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
@@ -31,6 +30,7 @@ import {
|
|||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -42,7 +42,12 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useSubscription, useUser } from "@app/context";
|
import { useSubscription, useUser } from "@app/context";
|
||||||
import { useDebounce, usePopUp } from "@app/hooks";
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useAdminBulkDeleteUsers,
|
useAdminBulkDeleteUsers,
|
||||||
useAdminDeleteUser,
|
useAdminDeleteUser,
|
||||||
@@ -58,17 +63,19 @@ const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions f
|
|||||||
|
|
||||||
const UserPanelTable = ({
|
const UserPanelTable = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
users: usersPages,
|
users,
|
||||||
isPending,
|
isPending,
|
||||||
adminsOnly,
|
adminsOnly,
|
||||||
searchUserFilter,
|
searchUserFilter,
|
||||||
setSearchUserFilter,
|
setSearchUserFilter,
|
||||||
setAdminsOnly,
|
setAdminsOnly,
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
selectedUsers,
|
selectedUsers,
|
||||||
setSelectedUsers
|
setSelectedUsers,
|
||||||
|
totalCount,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPage,
|
||||||
|
handlePerPageChange
|
||||||
}: {
|
}: {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<
|
popUpName: keyof UsePopUpState<
|
||||||
@@ -81,22 +88,22 @@ const UserPanelTable = ({
|
|||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
users: InfiniteData<User[], unknown> | undefined;
|
users: User[] | undefined;
|
||||||
adminsOnly: boolean;
|
adminsOnly: boolean;
|
||||||
setAdminsOnly: (adminsOnly: boolean) => void;
|
setAdminsOnly: (adminsOnly: boolean) => void;
|
||||||
searchUserFilter: string;
|
searchUserFilter: string;
|
||||||
setSearchUserFilter: (filter: string) => void;
|
setSearchUserFilter: (filter: string) => void;
|
||||||
selectedUsers: User[];
|
selectedUsers: User[];
|
||||||
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
|
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
|
||||||
isFetchingNextPage: boolean;
|
totalCount: number;
|
||||||
fetchNextPage: () => void;
|
page: number;
|
||||||
hasNextPage: boolean;
|
perPage: number;
|
||||||
|
setPage: Dispatch<SetStateAction<number>>;
|
||||||
|
handlePerPageChange: (newPerPage: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const users = usersPages?.pages.flat();
|
const isEmpty = !isPending && totalCount === 0;
|
||||||
|
|
||||||
const isEmpty = !isPending && !users?.length;
|
|
||||||
const isTableFiltered = Boolean(adminsOnly);
|
const isTableFiltered = Boolean(adminsOnly);
|
||||||
|
|
||||||
const selectedUserIds = selectedUsers.map((user) => user.id);
|
const selectedUserIds = selectedUsers.map((user) => user.id);
|
||||||
@@ -302,16 +309,13 @@ const UserPanelTable = ({
|
|||||||
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
|
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
{!isEmpty && (
|
{!isEmpty && (
|
||||||
<Button
|
<Pagination
|
||||||
className="mt-4 py-3 text-sm"
|
count={totalCount}
|
||||||
isFullWidth
|
page={page}
|
||||||
variant="outline_bg"
|
perPage={perPage}
|
||||||
isLoading={isFetchingNextPage}
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
onChangePerPage={handlePerPageChange}
|
||||||
onClick={() => fetchNextPage()}
|
/>
|
||||||
>
|
|
||||||
{hasNextPage ? "Load More" : "End of List"}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -341,18 +345,24 @@ export const UserIdentitiesTable = () => {
|
|||||||
const [adminsOnly, setAdminsOnly] = useState(false);
|
const [adminsOnly, setAdminsOnly] = useState(false);
|
||||||
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||||
|
|
||||||
const {
|
const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", {
|
||||||
data: users,
|
initPerPage: getUserTablePreference("ResourceOverviewUsersTable", PreferenceKey.PerPage, 20)
|
||||||
isPending,
|
});
|
||||||
isFetchingNextPage,
|
|
||||||
hasNextPage,
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
fetchNextPage
|
setPerPage(newPerPage);
|
||||||
} = useAdminGetUsers({
|
setUserTablePreference("ResourceOverviewUsersTable", PreferenceKey.PerPage, newPerPage);
|
||||||
limit: 20,
|
};
|
||||||
|
|
||||||
|
const { data, isPending } = useAdminGetUsers({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
searchTerm: debouncedSearchTerm,
|
searchTerm: debouncedSearchTerm,
|
||||||
adminsOnly
|
adminsOnly
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { users, totalCount = 0 } = data ?? {};
|
||||||
|
|
||||||
const handleRemoveUser = async () => {
|
const handleRemoveUser = async () => {
|
||||||
const { id } = popUp?.removeUser?.data as { id: string; username: string };
|
const { id } = popUp?.removeUser?.data as { id: string; username: string };
|
||||||
|
|
||||||
@@ -479,9 +489,11 @@ export const UserIdentitiesTable = () => {
|
|||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
adminsOnly={adminsOnly}
|
adminsOnly={adminsOnly}
|
||||||
setAdminsOnly={setAdminsOnly}
|
setAdminsOnly={setAdminsOnly}
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
page={page}
|
||||||
hasNextPage={hasNextPage}
|
perPage={perPage}
|
||||||
fetchNextPage={fetchNextPage}
|
setPage={setPage}
|
||||||
|
handlePerPageChange={handlePerPageChange}
|
||||||
|
totalCount={totalCount}
|
||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.removeUser.isOpen}
|
isOpen={popUp.removeUser.isOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user