From 433c16c732bde8e8022d72fdcd9937c2e9816e57 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 7 Nov 2025 05:44:38 -0500 Subject: [PATCH] pam: accounts table server-side filter, search, pagination --- .../pam-account-routers/pam-account-router.ts | 42 ++- .../services/pam-account/pam-account-dal.ts | 114 ++++++-- .../pam-account/pam-account-service.ts | 115 ++++++-- .../services/pam-account/pam-account-types.ts | 21 ++ .../ee/services/pam-folder/pam-folder-dal.ts | 99 ++++++- frontend/src/hooks/api/pam/enums.ts | 9 + frontend/src/hooks/api/pam/mutations.tsx | 12 +- frontend/src/hooks/api/pam/queries.tsx | 32 ++- frontend/src/hooks/api/pam/types/index.ts | 14 +- .../components/AccountViewToggle.tsx | 18 +- .../components/PamAccountsSection.tsx | 16 +- .../components/PamAccountsTable.tsx | 263 ++++++++---------- .../src/pages/pam/PamAccountsPage/route.tsx | 5 +- 13 files changed, 514 insertions(+), 246 deletions(-) diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index 286e0896f..626daa898 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -2,11 +2,14 @@ import { z } from "zod"; import { PamFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PamAccountOrderBy, PamAccountView } from "@app/ee/services/pam-account/pam-account-types"; import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; import { ms } from "@app/lib/ms"; +import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -26,33 +29,58 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { schema: { description: "List PAM accounts", querystring: z.object({ - projectId: z.string().uuid() + projectId: z.string().uuid(), + accountPath: z.string().trim().default("/").transform(removeTrailingSlash), + accountView: z.nativeEnum(PamAccountView).default(PamAccountView.Flat), + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(100).default(100), + orderBy: z.nativeEnum(PamAccountOrderBy).default(PamAccountOrderBy.Name), + orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC), + search: z.string().trim().optional() }), response: { 200: z.object({ accounts: SanitizedAccountSchema.array(), - folders: PamFoldersSchema.array() + folders: PamFoldersSchema.array(), + totalCount: z.number().default(0), + folderId: z.string().optional(), + folderPaths: z.record(z.string(), z.string()) }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const response = await server.services.pamAccount.list(req.query.projectId, req.permission); + const { projectId, accountPath, accountView, limit, offset, search, orderBy, orderDirection } = req.query; + + const { accounts, folders, totalCount, folderId, folderPaths } = await server.services.pamAccount.list({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + accountPath, + accountView, + limit, + offset, + search, + orderBy, + orderDirection + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: req.query.projectId, + projectId, event: { type: EventType.PAM_ACCOUNT_LIST, metadata: { - accountCount: response.accounts.length, - folderCount: response.folders.length + accountCount: accounts.length, + folderCount: folders.length } } }); - return response; + return { accounts, folders, totalCount, folderId, folderPaths }; } }); diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts index 6ef7df76e..562567172 100644 --- a/backend/src/ee/services/pam-account/pam-account-dal.ts +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -1,46 +1,100 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TPamAccounts } from "@app/db/schemas"; -import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { PamAccountOrderBy, PamAccountView } from "./pam-account-types"; export type TPamAccountDALFactory = ReturnType; -type PamAccountFindFilter = Parameters>[0]; - export const pamAccountDALFactory = (db: TDbClient) => { const orm = ormify(db, TableName.PamAccount); - const findWithResourceDetails = async (filter: PamAccountFindFilter, tx?: Knex) => { - const query = (tx || db.replicaNode())(TableName.PamAccount) - .leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) - .select(selectAllTableCols(TableName.PamAccount)) - .select( + const findByProjectIdWithResourceDetails = async ( + { + projectId, + folderId, + accountView = PamAccountView.Nested, + search, + limit, + offset = 0, + orderBy = PamAccountOrderBy.Name, + orderDirection = OrderByDirection.ASC + }: { + projectId: string; + folderId?: string | null; + accountView?: PamAccountView; + search?: string; + limit?: number; + offset?: number; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + }, + tx?: Knex + ) => { + try { + const dbInstance = tx || db.replicaNode(); + const query = dbInstance(TableName.PamAccount) + .leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) + .where(`${TableName.PamAccount}.projectId`, projectId); + + if (accountView === PamAccountView.Nested) { + if (folderId) { + void query.where(`${TableName.PamAccount}.folderId`, folderId); + } else { + void query.whereNull(`${TableName.PamAccount}.folderId`); + } + } + + if (search) { + void query.where((q) => { + void q + .whereILike(`${TableName.PamAccount}.name`, `%${search}%`) + .orWhereILike(`${TableName.PamResource}.name`, `%${search}%`) + .orWhereILike(`${TableName.PamAccount}.description`, `%${search}%`); + }); + } + + const countQuery = query.clone().count("*", { as: "count" }).first(); + + void query.select(selectAllTableCols(TableName.PamAccount)).select( // resource db.ref("name").withSchema(TableName.PamResource).as("resourceName"), db.ref("resourceType").withSchema(TableName.PamResource), db.ref("encryptedRotationAccountCredentials").withSchema(TableName.PamResource) ); - if (filter) { - /* eslint-disable @typescript-eslint/no-misused-promises */ - void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PamAccount, filter))); + const direction = orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"; + + void query.orderByRaw(`${TableName.PamAccount}.?? COLLATE "en-x-icu" ${direction}`, [orderBy]); + + if (typeof limit === "number") { + void query.limit(limit).offset(offset); + } + + const [results, countResult] = await Promise.all([query, countQuery]); + const totalCount = Number(countResult?.count || 0); + + const accounts = results.map( + // @ts-expect-error resourceName, resourceType, encryptedRotationAccountCredentials are from joined table + ({ resourceId, resourceName, resourceType, encryptedRotationAccountCredentials, ...account }) => ({ + ...account, + resourceId, + resource: { + id: resourceId, + name: resourceName as string, + resourceType, + encryptedRotationAccountCredentials + } + }) + ); + return { accounts, totalCount }; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM accounts with resource details" }); } - - const accounts = await query; - - return accounts.map( - ({ resourceId, resourceName, resourceType, encryptedRotationAccountCredentials, ...account }) => ({ - ...account, - resourceId, - resource: { - id: resourceId, - name: resourceName, - resourceType, - encryptedRotationAccountCredentials - } - }) - ); }; const findAccountsDueForRotation = async (tx?: Knex) => { @@ -59,5 +113,9 @@ export const pamAccountDALFactory = (db: TDbClient) => { return accounts; }; - return { ...orm, findWithResourceDetails, findAccountsDueForRotation }; + return { + ...orm, + findByProjectIdWithResourceDetails, + findAccountsDueForRotation + }; }; diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 2f66d28d7..bfc6c7e7f 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamResources } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamFolders, TPamResources } from "@app/db/schemas"; import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory"; import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -32,7 +32,13 @@ import { PamSessionStatus } from "../pam-session/pam-session-enums"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPamAccountDALFactory } from "./pam-account-dal"; import { decryptAccount, decryptAccountCredentials, encryptAccountCredentials } from "./pam-account-fns"; -import { TAccessAccountDTO, TCreateAccountDTO, TUpdateAccountDTO } from "./pam-account-types"; +import { + PamAccountView, + TAccessAccountDTO, + TCreateAccountDTO, + TListAccountsDTO, + TUpdateAccountDTO +} from "./pam-account-types"; type TPamAccountServiceFactoryDep = { pamResourceDAL: TPamResourceDALFactory; @@ -334,21 +340,86 @@ export const pamAccountServiceFactory = ({ }; }; - const list = async (projectId: string, actor: OrgServiceActor) => { + const list = async ({ + projectId, + accountPath, + accountView, + actor, + actorId, + actorAuthMethod, + actorOrgId, + ...params + }: TListAccountsDTO) => { const { permission } = await permissionService.getProjectPermission({ - actor: actor.type, - actorAuthMethod: actor.authMethod, - actorId: actor.id, - actorOrgId: actor.orgId, + actor, + actorId, projectId, + actorAuthMethod, + actorOrgId, actionProjectType: ActionProjectType.PAM }); - const accountsWithResourceDetails = await pamAccountDAL.findWithResourceDetails({ projectId }); + const limit = params.limit || 20; + const offset = params.offset || 0; const canReadFolders = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.PamFolders); - const folders = canReadFolders ? await pamFolderDAL.find({ projectId }) : []; + const folder = accountPath === "/" ? null : await pamFolderDAL.findByPath(projectId, accountPath); + if (accountPath !== "/" && !folder) { + return { accounts: [], folders: [], totalCount: 0, folderPaths: {} }; + } + const folderId = folder?.id; + + let totalFolderCount = 0; + if (canReadFolders && accountView === PamAccountView.Nested) { + const { totalCount } = await pamFolderDAL.findByProjectId({ + projectId, + parentId: folderId + }); + totalFolderCount = totalCount; + } + const { totalCount: totalAccountCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({ + projectId, + folderId, + accountView + }); + + const totalCount = totalFolderCount + totalAccountCount; + + let folders: TPamFolders[] = []; + if (canReadFolders && accountView === PamAccountView.Nested && offset < totalFolderCount) { + const folderLimit = Math.min(limit, totalFolderCount - offset); + const { folders: foldersResp } = await pamFolderDAL.findByProjectId({ + projectId, + parentId: folderId, + limit: folderLimit, + offset, + search: params.search, + orderBy: params.orderBy, + orderDirection: params.orderDirection + }); + + folders = foldersResp; + } + + let accountsWithResourceDetails: Awaited< + ReturnType + >["accounts"] = []; + const accountsToFetch = limit - folders.length; + if (accountsToFetch > 0) { + const accountOffset = Math.max(0, offset - totalFolderCount); + const { accounts: accountsResp } = await pamAccountDAL.findByProjectIdWithResourceDetails({ + projectId, + folderId, + accountView, + offset: accountOffset, + limit: accountsToFetch, + search: params.search, + orderBy: params.orderBy, + orderDirection: params.orderDirection + }); + accountsWithResourceDetails = accountsResp; + } const decryptedAndPermittedAccounts: Array< TPamAccounts & { @@ -359,12 +430,6 @@ export const pamAccountServiceFactory = ({ > = []; for await (const account of accountsWithResourceDetails) { - const accountPath = await getFullPamFolderPath({ - pamFolderDAL, - folderId: account.folderId, - projectId: account.projectId - }); - // Check permission for each individual account if ( permission.can( @@ -391,9 +456,27 @@ export const pamAccountServiceFactory = ({ } } + const folderPaths: Record = {}; + const accountFolderIds = [ + ...new Set(decryptedAndPermittedAccounts.flatMap((a) => (a.folderId ? [a.folderId] : []))) + ]; + + await Promise.all( + accountFolderIds.map(async (fId) => { + folderPaths[fId] = await getFullPamFolderPath({ + pamFolderDAL, + folderId: fId, + projectId + }); + }) + ); + return { accounts: decryptedAndPermittedAccounts, - folders + folders, + totalCount, + folderId, + folderPaths }; }; diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index 4bbccc6fa..775ef1f78 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -1,3 +1,5 @@ +import { OrderByDirection, TProjectPermission } from "@app/lib/types"; + import { TPamAccount } from "../pam-resource/pam-resource-types"; // DTOs @@ -18,3 +20,22 @@ export type TAccessAccountDTO = { actorUserAgent: string; duration: number; }; + +export type TListAccountsDTO = { + accountPath: string; + accountView: PamAccountView; + search?: string; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + limit?: number; + offset?: number; +} & TProjectPermission; + +export enum PamAccountOrderBy { + Name = "name" +} + +export enum PamAccountView { + Flat = "flat", + Nested = "nested" +} diff --git a/backend/src/ee/services/pam-folder/pam-folder-dal.ts b/backend/src/ee/services/pam-folder/pam-folder-dal.ts index aa334618d..9d0ca6cd2 100644 --- a/backend/src/ee/services/pam-folder/pam-folder-dal.ts +++ b/backend/src/ee/services/pam-folder/pam-folder-dal.ts @@ -1,9 +1,104 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { PamAccountOrderBy } from "../pam-account/pam-account-types"; export type TPamFolderDALFactory = ReturnType; export const pamFolderDALFactory = (db: TDbClient) => { const orm = ormify(db, TableName.PamFolder); - return { ...orm }; + + const findByProjectId = async ( + { + projectId, + parentId, + search, + limit, + offset = 0, + orderBy = PamAccountOrderBy.Name, + orderDirection = OrderByDirection.ASC + }: { + projectId: string; + parentId?: string | null; + search?: string; + limit?: number; + offset?: number; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + }, + tx?: Knex + ) => { + try { + const dbInstance = tx || db.replicaNode(); + const query = dbInstance(TableName.PamFolder).where(`${TableName.PamFolder}.projectId`, projectId); + + if (parentId) { + void query.where(`${TableName.PamFolder}.parentId`, parentId); + } else { + void query.whereNull(`${TableName.PamFolder}.parentId`); + } + + if (search) { + void query.whereILike(`${TableName.PamFolder}.name`, `%${search}%`); + } + + const countQuery = query.clone().count("*", { as: "count" }).first(); + + void query.select(selectAllTableCols(TableName.PamFolder)); + const direction = orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"; + + void query.orderByRaw(`${TableName.PamFolder}.?? COLLATE "en-x-icu" ${direction}`, [orderBy]); + + if (typeof limit === "number") { + void query.limit(limit).offset(offset); + } + + const [folders, countResult] = await Promise.all([query, countQuery]); + const totalCount = Number(countResult?.count || 0); + + return { folders, totalCount }; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM folders" }); + } + }; + + const findByPath = async (projectId: string, path: string, tx?: Knex) => { + try { + const dbInstance = tx || db.replicaNode(); + const pathSegments = path.split("/").filter(Boolean); + + let parentId: string | null = null; + let currentFolder: Awaited> | undefined; + + for await (const segment of pathSegments) { + const query = dbInstance(TableName.PamFolder) + .where(`${TableName.PamFolder}.projectId`, projectId) + .where(`${TableName.PamFolder}.name`, segment); + + if (parentId) { + void query.where(`${TableName.PamFolder}.parentId`, parentId); + } else { + void query.whereNull(`${TableName.PamFolder}.parentId`); + } + + currentFolder = await query.first(); + + if (!currentFolder) { + return undefined; + } + + parentId = currentFolder.id; + } + + return currentFolder; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM folder by path" }); + } + }; + + return { ...orm, findByProjectId, findByPath }; }; diff --git a/frontend/src/hooks/api/pam/enums.ts b/frontend/src/hooks/api/pam/enums.ts index 0684f6073..bd5779487 100644 --- a/frontend/src/hooks/api/pam/enums.ts +++ b/frontend/src/hooks/api/pam/enums.ts @@ -12,3 +12,12 @@ export enum PamSessionStatus { Ended = "ended", Terminated = "terminated" } + +export enum PamAccountOrderBy { + Name = "name" +} + +export enum PamAccountView { + Flat = "flat", + Nested = "nested" +} diff --git a/frontend/src/hooks/api/pam/mutations.tsx b/frontend/src/hooks/api/pam/mutations.tsx index 99a89b425..e6ee62e02 100644 --- a/frontend/src/hooks/api/pam/mutations.tsx +++ b/frontend/src/hooks/api/pam/mutations.tsx @@ -82,7 +82,7 @@ export const useCreatePamAccount = () => { return data.account; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -99,7 +99,7 @@ export const useUpdatePamAccount = () => { return data.account; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -115,7 +115,7 @@ export const useDeletePamAccount = () => { return data.account; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -130,7 +130,7 @@ export const useCreatePamFolder = () => { return data.folder; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -147,7 +147,7 @@ export const useUpdatePamFolder = () => { return data.folder; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -163,7 +163,7 @@ export const useDeletePamFolder = () => { return data.folder; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index 6339b4761..08861d1e2 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request"; import { TPamResourceOption } from "./types/resource-options"; import { PamResourceType } from "./enums"; -import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; +import { TListPamAccountsDTO, TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; export const pamKeys = { all: ["pam"] as const, @@ -19,7 +19,12 @@ export const pamKeys = { resourceType, resourceId ], - listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId], + listAccounts: ({ projectId, ...params }: TListPamAccountsDTO) => [ + ...pamKeys.account(), + "list", + projectId, + params + ], getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId], listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId] }; @@ -98,25 +103,32 @@ export const useGetPamResourceById = ( }; // Accounts +type TListPamAccountsResponse = { + accounts: TPamAccount[]; + folders: TPamFolder[]; + totalCount: number; + folderId?: string; + folderPaths: Record; +}; + export const useListPamAccounts = ( - projectId: string, + params: TListPamAccountsDTO, options?: Omit< UseQueryOptions< - { accounts: TPamAccount[]; folders: TPamFolder[] }, + TListPamAccountsResponse, unknown, - { accounts: TPamAccount[]; folders: TPamFolder[] }, + TListPamAccountsResponse, ReturnType >, "queryKey" | "queryFn" > ) => { return useQuery({ - queryKey: pamKeys.listAccounts(projectId), + queryKey: pamKeys.listAccounts(params), queryFn: async () => { - const { data } = await apiRequest.get<{ accounts: TPamAccount[]; folders: TPamFolder[] }>( - "/api/v1/pam/accounts", - { params: { projectId } } - ); + const { data } = await apiRequest.get("/api/v1/pam/accounts", { + params + }); return data; }, diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index 1b1890cbd..0b91cb9b0 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -1,4 +1,5 @@ -import { PamResourceType, PamSessionStatus } from "../enums"; +import { OrderByDirection } from "../../generic/types"; +import { PamAccountOrderBy, PamAccountView, PamResourceType, PamSessionStatus } from "../enums"; import { TMySQLAccount, TMySQLResource } from "./mysql-resource"; import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; @@ -63,6 +64,17 @@ export type TDeletePamResourceDTO = { }; // Account DTOs +export type TListPamAccountsDTO = { + projectId: string; + accountPath?: string | null; + accountView?: PamAccountView; + offset?: number; + limit?: number; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + search?: string; +}; + export type TCreatePamAccountDTO = Pick< TPamAccount, "name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId" diff --git a/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx b/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx index 92b2f7859..51e568551 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx @@ -1,13 +1,9 @@ import { Button } from "@app/components/v2"; - -export enum AccountView { - Flat = "flat", - Nested = "nested" -} +import { PamAccountView } from "@app/hooks/api/pam"; type Props = { - value: AccountView; - onChange: (value: AccountView) => void; + value: PamAccountView; + onChange: (value: PamAccountView) => void; }; export const AccountViewToggle = ({ value, onChange }: Props) => { @@ -16,11 +12,11 @@ export const AccountViewToggle = ({ value, onChange }: Props) => { - {accountView !== AccountView.Flat && ( + {accountView !== PamAccountView.Flat && ( handlePopUpToggle("misc", isOpen)} @@ -414,11 +378,11 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => { Accounts handleSort(OrderBy.Name)} + onClick={() => handleSort(PamAccountOrderBy.Name)} > - + @@ -426,45 +390,48 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => { - {accountView !== AccountView.Flat && - foldersToRender.map((folder) => ( - handleFolderClick(folder)} - onUpdate={(e) => handlePopUpOpen("updateFolder", e)} - onDelete={(e) => handlePopUpOpen("deleteFolder", e)} - /> - ))} - {currentPageData.map((account) => ( - { - handlePopUpOpen("accessAccount", e); - }} - onUpdate={(e) => handlePopUpOpen("updateAccount", e)} - onDelete={(e) => handlePopUpOpen("deleteAccount", e)} - /> - ))} + {isLoading && } + {!isLoading && ( + <> + {accountView !== PamAccountView.Flat && + foldersToRender.map((folder) => ( + handleFolderClick(folder)} + onUpdate={(e) => handlePopUpOpen("updateFolder", e)} + onDelete={(e) => handlePopUpOpen("deleteFolder", e)} + /> + ))} + {filteredAccounts.map((account) => ( + { + handlePopUpOpen("accessAccount", e); + }} + onUpdate={(e) => handlePopUpOpen("updateAccount", e)} + onDelete={(e) => handlePopUpOpen("deleteAccount", e)} + /> + ))} + + )} - {Boolean(filteredAccounts.length) && ( + {Boolean(totalCount) && !isLoading && ( setPage(newPage)} + onChangePerPage={handlePerPageChange} /> )} - {isContentEmpty && ( + {!isLoading && isContentEmpty && ( { isOpen={popUp.addFolder.isOpen} onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)} projectId={projectId} - currentFolderId={effectiveFolderIdForFiltering} + currentFolderId={currentFolderId} /> { isOpen={popUp.addAccount.isOpen} onOpenChange={(isOpen) => handlePopUpToggle("addAccount", isOpen)} projectId={projectId} - currentFolderId={effectiveFolderIdForFiltering} + currentFolderId={currentFolderId} /> ); diff --git a/frontend/src/pages/pam/PamAccountsPage/route.tsx b/frontend/src/pages/pam/PamAccountsPage/route.tsx index 7fbf81b45..65a8c567e 100644 --- a/frontend/src/pages/pam/PamAccountsPage/route.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/route.tsx @@ -2,12 +2,13 @@ import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; -import { AccountView } from "./components/AccountViewToggle"; +import { PamAccountView } from "@app/hooks/api/pam"; + import { PamAccountsPage } from "./PamAccountsPage"; const PamAccountsPageQueryParamsSchema = z.object({ search: z.string().optional(), - accountView: z.nativeEnum(AccountView).optional(), + accountView: z.nativeEnum(PamAccountView).optional(), accountPath: z.string().catch("/") });