move account filter to backend & optimize totalCount logic

This commit is contained in:
x032205
2025-11-14 21:26:10 -05:00
parent a37d27ece3
commit 19610f4fd6
6 changed files with 71 additions and 43 deletions
@@ -36,7 +36,16 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
limit: z.coerce.number().min(1).max(100).default(100), limit: z.coerce.number().min(1).max(100).default(100),
orderBy: z.nativeEnum(PamAccountOrderBy).default(PamAccountOrderBy.Name), orderBy: z.nativeEnum(PamAccountOrderBy).default(PamAccountOrderBy.Name),
orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC), orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC),
search: z.string().trim().optional() search: z.string().trim().optional(),
filterResourceIds: z
.string()
.transform((val) =>
val
.split(",")
.map((s) => s.trim())
.filter(Boolean)
)
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -50,7 +59,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const { projectId, accountPath, accountView, limit, offset, search, orderBy, orderDirection } = req.query; const { projectId, accountPath, accountView, limit, offset, search, orderBy, orderDirection, filterResourceIds } =
req.query;
const { accounts, folders, totalCount, folderId, folderPaths } = await server.services.pamAccount.list({ const { accounts, folders, totalCount, folderId, folderPaths } = await server.services.pamAccount.list({
actorId: req.permission.id, actorId: req.permission.id,
@@ -64,7 +74,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
offset, offset,
search, search,
orderBy, orderBy,
orderDirection orderDirection,
filterResourceIds
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -22,7 +22,8 @@ export const pamAccountDALFactory = (db: TDbClient) => {
limit, limit,
offset = 0, offset = 0,
orderBy = PamAccountOrderBy.Name, orderBy = PamAccountOrderBy.Name,
orderDirection = OrderByDirection.ASC orderDirection = OrderByDirection.ASC,
filterResourceIds
}: { }: {
projectId: string; projectId: string;
folderId?: string | null; folderId?: string | null;
@@ -32,6 +33,7 @@ export const pamAccountDALFactory = (db: TDbClient) => {
offset?: number; offset?: number;
orderBy?: PamAccountOrderBy; orderBy?: PamAccountOrderBy;
orderDirection?: OrderByDirection; orderDirection?: OrderByDirection;
filterResourceIds?: string[];
}, },
tx?: Knex tx?: Knex
) => { ) => {
@@ -61,6 +63,10 @@ export const pamAccountDALFactory = (db: TDbClient) => {
}); });
} }
if (filterResourceIds && filterResourceIds.length) {
void query.whereIn(`${TableName.PamAccount}.resourceId`, filterResourceIds);
}
const countQuery = query.clone().count("*", { as: "count" }).first(); const countQuery = query.clone().count("*", { as: "count" }).first();
void query.select(selectAllTableCols(TableName.PamAccount)).select( void query.select(selectAllTableCols(TableName.PamAccount)).select(
@@ -374,14 +374,6 @@ export const pamAccountServiceFactory = ({
}); });
totalFolderCount = totalCount; totalFolderCount = totalCount;
} }
const { totalCount: totalAccountCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({
projectId,
folderId,
accountView,
search: params.search
});
const totalCount = totalFolderCount + totalAccountCount;
let folders: TPamFolders[] = []; let folders: TPamFolders[] = [];
if (canReadFolders && accountView === PamAccountView.Nested && offset < totalFolderCount) { if (canReadFolders && accountView === PamAccountView.Nested && offset < totalFolderCount) {
@@ -402,10 +394,12 @@ export const pamAccountServiceFactory = ({
let accountsWithResourceDetails: Awaited< let accountsWithResourceDetails: Awaited<
ReturnType<typeof pamAccountDAL.findByProjectIdWithResourceDetails> ReturnType<typeof pamAccountDAL.findByProjectIdWithResourceDetails>
>["accounts"] = []; >["accounts"] = [];
let totalAccountCount = 0;
const accountsToFetch = limit - folders.length; const accountsToFetch = limit - folders.length;
if (accountsToFetch > 0) { if (accountsToFetch > 0) {
const accountOffset = Math.max(0, offset - totalFolderCount); const accountOffset = Math.max(0, offset - totalFolderCount);
const { accounts: accountsResp } = await pamAccountDAL.findByProjectIdWithResourceDetails({ const { accounts, totalCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({
projectId, projectId,
folderId, folderId,
accountView, accountView,
@@ -413,11 +407,25 @@ export const pamAccountServiceFactory = ({
limit: accountsToFetch, limit: accountsToFetch,
search: params.search, search: params.search,
orderBy: params.orderBy, orderBy: params.orderBy,
orderDirection: params.orderDirection orderDirection: params.orderDirection,
filterResourceIds: params.filterResourceIds
}); });
accountsWithResourceDetails = accountsResp; accountsWithResourceDetails = accounts;
totalAccountCount = totalCount;
} else {
// if no accounts are to be fetched for the current page, we still need the total count for pagination
const { totalCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({
projectId,
folderId,
accountView,
search: params.search,
filterResourceIds: params.filterResourceIds
});
totalAccountCount = totalCount;
} }
const totalCount = totalFolderCount + totalAccountCount;
const decryptedAndPermittedAccounts: Array< const decryptedAndPermittedAccounts: Array<
TPamAccounts & { TPamAccounts & {
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean }; resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
@@ -30,4 +30,5 @@ export type TListAccountsDTO = {
orderDirection?: OrderByDirection; orderDirection?: OrderByDirection;
limit?: number; limit?: number;
offset?: number; offset?: number;
filterResourceIds?: string[];
} & TProjectPermission; } & TProjectPermission;
@@ -79,6 +79,10 @@ export type TDeletePamResourceDTO = {
}; };
// Account DTOs // Account DTOs
export type PamAccountFilter = {
resourceIds: string[];
};
export type TListPamAccountsDTO = { export type TListPamAccountsDTO = {
projectId: string; projectId: string;
accountPath?: string | null; accountPath?: string | null;
@@ -88,6 +92,7 @@ export type TListPamAccountsDTO = {
orderBy?: PamAccountOrderBy; orderBy?: PamAccountOrderBy;
orderDirection?: OrderByDirection; orderDirection?: OrderByDirection;
search?: string; search?: string;
filterResourceIds?: string;
}; };
export type TCreatePamAccountDTO = Pick< export type TCreatePamAccountDTO = Pick<
@@ -50,12 +50,12 @@ import { usePagination, usePopUp } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { import {
PAM_RESOURCE_TYPE_MAP, PAM_RESOURCE_TYPE_MAP,
PamAccountFilter,
PamAccountOrderBy, PamAccountOrderBy,
PamAccountView, PamAccountView,
TPamAccount,
TPamFolder TPamFolder
} from "@app/hooks/api/pam"; } from "@app/hooks/api/pam";
import { useListPamAccounts } from "@app/hooks/api/pam/queries"; import { useListPamAccounts, useListPamResources } from "@app/hooks/api/pam/queries";
import { AccountViewToggle } from "./AccountViewToggle"; import { AccountViewToggle } from "./AccountViewToggle";
import { FolderBreadCrumbs } from "./FolderBreadCrumbs"; import { FolderBreadCrumbs } from "./FolderBreadCrumbs";
@@ -69,10 +69,6 @@ import { PamFolderRow } from "./PamFolderRow";
import { PamUpdateAccountModal } from "./PamUpdateAccountModal"; import { PamUpdateAccountModal } from "./PamUpdateAccountModal";
import { PamUpdateFolderModal } from "./PamUpdateFolderModal"; import { PamUpdateFolderModal } from "./PamUpdateFolderModal";
type Filters = {
resource: string[];
};
type Props = { type Props = {
projectId: string; projectId: string;
}; };
@@ -103,8 +99,8 @@ export const PamAccountsTable = ({ projectId }: Props) => {
initAccountView ?? PamAccountView.Flat initAccountView ?? PamAccountView.Flat
); );
const [filters, setFilters] = useState<Filters>({ const [filter, setFilter] = useState<PamAccountFilter>({
resource: [] resourceIds: []
}); });
const { const {
@@ -139,7 +135,8 @@ export const PamAccountsTable = ({ projectId }: Props) => {
limit: perPage, limit: perPage,
search: debouncedSearch, search: debouncedSearch,
orderBy, orderBy,
orderDirection orderDirection,
filterResourceIds: filter.resourceIds.length ? filter.resourceIds.join(",") : undefined
}); });
const accounts = data?.accounts || []; const accounts = data?.accounts || [];
@@ -166,7 +163,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
resource: { name: resourceName, id: resourceId } resource: { name: resourceName, id: resourceId }
} = account; } = account;
if (filters.resource.length && !filters.resource.includes(resourceId)) { if (filter.resourceIds.length && !filter.resourceIds.includes(resourceId)) {
return false; return false;
} }
@@ -178,7 +175,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
(description || "").toLowerCase().includes(searchValue) (description || "").toLowerCase().includes(searchValue)
); );
}), }),
[accounts, search, filters] [accounts, search, filter]
); );
const handleSort = (column: PamAccountOrderBy) => { const handleSort = (column: PamAccountOrderBy) => {
@@ -197,7 +194,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
const getColSortIcon = (col: PamAccountOrderBy) => const getColSortIcon = (col: PamAccountOrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
const isTableFiltered = Boolean(filters.resource.length); const isTableFiltered = Boolean(filter.resourceIds.length);
const handleFolderClick = (folder: TPamFolder) => { const handleFolderClick = (folder: TPamFolder) => {
if (accountView === PamAccountView.Flat) { if (accountView === PamAccountView.Flat) {
@@ -210,13 +207,13 @@ export const PamAccountsTable = ({ projectId }: Props) => {
const isContentEmpty = !filteredAccounts.length && !foldersToRender.length; const isContentEmpty = !filteredAccounts.length && !foldersToRender.length;
const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered); const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered);
const uniqueResources = useMemo(() => { const { data: resourcesData } = useListPamResources({
const resourceMap = new Map<string, TPamAccount["resource"]>(); projectId,
accounts.forEach((account) => { // temporarily returning a large number until we rework table filtering
resourceMap.set(account.resource.id, account.resource); limit: 100
}); });
return Array.from(resourceMap.values());
}, [accounts]); const resources = resourcesData?.resources || [];
return ( return (
<div> <div>
@@ -229,7 +226,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
value={accountView} value={accountView}
onChange={(e) => { onChange={(e) => {
setPage(1); setPage(1);
setFilters({ resource: [] }); setFilter({ resourceIds: [] });
setAccountView(e); setAccountView(e);
navigate({ navigate({
search: (prev) => ({ search: (prev) => ({
@@ -273,25 +270,25 @@ export const PamAccountsTable = ({ projectId }: Props) => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="max-h-[70vh] thin-scrollbar overflow-y-auto" align="end"> <DropdownMenuContent className="max-h-[70vh] thin-scrollbar overflow-y-auto" align="end">
<DropdownMenuLabel>Resource</DropdownMenuLabel> <DropdownMenuLabel>Resource</DropdownMenuLabel>
{uniqueResources.length ? ( {resources.length ? (
uniqueResources.map((resource) => { resources.map((resource) => {
const { name, image } = PAM_RESOURCE_TYPE_MAP[resource.resourceType]; const { name, image } = PAM_RESOURCE_TYPE_MAP[resource.resourceType];
return ( return (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
const newResources = filters.resource.includes(resource.id) const newResources = filter.resourceIds.includes(resource.id)
? filters.resource.filter((a) => a !== resource.id) ? filter.resourceIds.filter((a) => a !== resource.id)
: [...filters.resource, resource.id]; : [...filter.resourceIds, resource.id];
setFilters((prev) => ({ setFilter((prev) => ({
...prev, ...prev,
resource: newResources resourceIds: newResources
})); }));
}} }}
key={resource.id} key={resource.id}
icon={ icon={
filters.resource.includes(resource.id) && ( filter.resourceIds.includes(resource.id) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} /> <FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
) )
} }