move resource type filter to backend

This commit is contained in:
x032205
2025-11-14 21:37:47 -05:00
parent a99e60575c
commit 9ad69a487f
5 changed files with 63 additions and 49 deletions

View File

@@ -59,7 +59,16 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
limit: z.coerce.number().min(1).max(100).default(100),
orderBy: z.nativeEnum(PamResourceOrderBy).default(PamResourceOrderBy.Name),
orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC),
search: z.string().trim().optional()
search: z.string().trim().optional(),
filterResourceTypes: z
.string()
.transform((val) =>
val
.split(",")
.map((s) => s.trim())
.filter(Boolean)
)
.optional()
}),
response: {
200: z.object({
@@ -70,7 +79,7 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { projectId, limit, offset, search, orderBy, orderDirection } = req.query;
const { projectId, limit, offset, search, orderBy, orderDirection, filterResourceTypes } = req.query;
const { resources, totalCount } = await server.services.pamResource.list({
actorId: req.permission.id,
@@ -82,7 +91,8 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
offset,
search,
orderBy,
orderDirection
orderDirection,
filterResourceTypes
});
await server.services.auditLog.createAuditLog({

View File

@@ -31,7 +31,8 @@ export const pamResourceDALFactory = (db: TDbClient) => {
limit,
offset = 0,
orderBy = PamResourceOrderBy.Name,
orderDirection = OrderByDirection.ASC
orderDirection = OrderByDirection.ASC,
filterResourceTypes
}: {
projectId: string;
search?: string;
@@ -39,6 +40,7 @@ export const pamResourceDALFactory = (db: TDbClient) => {
offset?: number;
orderBy?: PamResourceOrderBy;
orderDirection?: OrderByDirection;
filterResourceTypes?: string[];
},
tx?: Knex
) => {
@@ -57,6 +59,10 @@ export const pamResourceDALFactory = (db: TDbClient) => {
});
}
if (filterResourceTypes && filterResourceTypes.length) {
void query.whereIn(`${TableName.PamResource}.resourceType`, filterResourceTypes);
}
const countQuery = query.clone().count("*", { as: "count" }).first();
void query.select(selectAllTableCols(TableName.PamResource));

View File

@@ -92,6 +92,7 @@ export const useListPamResources = (
return data;
},
placeholderData: (prev) => prev,
...options
});
};

View File

@@ -59,6 +59,7 @@ export type TListPamResourcesDTO = {
orderBy?: PamResourceOrderBy;
orderDirection?: OrderByDirection;
search?: string;
filterResourceTypes?: string;
};
export type TCreatePamResourceDTO = Pick<

View File

@@ -59,8 +59,8 @@ import { PamDeleteResourceModal } from "./PamDeleteResourceModal";
import { PamResourceRow } from "./PamResourceRow";
import { PamUpdateResourceModal } from "./PamUpdateResourceModal";
type Filters = {
resourceType: PamResourceType[];
type PamResourceFilter = {
resourceTypes: PamResourceType[];
};
type Props = {
@@ -80,8 +80,8 @@ export const PamResourcesTable = ({ projectId }: Props) => {
from: ROUTE_PATHS.Pam.ResourcesPage.id
});
const [filters, setFilters] = useState<Filters>({
resourceType: []
const [filter, setFilter] = useState<PamResourceFilter>({
resourceTypes: []
});
const {
@@ -114,7 +114,8 @@ export const PamResourcesTable = ({ projectId }: Props) => {
limit: perPage,
search: debouncedSearch,
orderBy,
orderDirection
orderDirection,
filterResourceTypes: filter.resourceTypes.length ? filter.resourceTypes.join(",") : undefined
});
const resources = data?.resources || [];
@@ -125,7 +126,7 @@ export const PamResourcesTable = ({ projectId }: Props) => {
resources.filter((resource) => {
const { name, resourceType } = resource;
if (filters.resourceType.length && !filters.resourceType.includes(resourceType)) {
if (filter.resourceTypes.length && !filter.resourceTypes.includes(resourceType)) {
return false;
}
@@ -136,7 +137,7 @@ export const PamResourcesTable = ({ projectId }: Props) => {
resourceType.toLowerCase().includes(searchValue)
);
}),
[resources, search, filters]
[resources, search, filter]
);
const handleSort = (column: PamResourceOrderBy) => {
@@ -155,7 +156,7 @@ export const PamResourcesTable = ({ projectId }: Props) => {
const getColSortIcon = (col: PamResourceOrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
const isTableFiltered = Boolean(filters.resourceType.length);
const isTableFiltered = Boolean(filter.resourceTypes.length);
const isContentEmpty = !filteredResources.length;
const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered);
@@ -192,43 +193,38 @@ export const PamResourcesTable = ({ projectId }: Props) => {
</DropdownMenuTrigger>
<DropdownMenuContent className="max-h-[70vh] thin-scrollbar overflow-y-auto" align="end">
<DropdownMenuLabel>Resource Type</DropdownMenuLabel>
{resources.length ? (
[...new Set(resources.map(({ resourceType }) => resourceType))].map((type) => {
const { name, image } = PAM_RESOURCE_TYPE_MAP[type];
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
resourceType: prev.resourceType.includes(type)
? prev.resourceType.filter((a) => a !== type)
: [...prev.resourceType, type]
}));
}}
key={type}
icon={
filters.resourceType.includes(type) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<img
alt={`${name} resource type`}
src={`/images/integrations/${image}`}
className="h-4 w-4"
/>
<span>{name}</span>
</div>
</DropdownMenuItem>
);
})
) : (
<DropdownMenuItem isDisabled>No Resources</DropdownMenuItem>
)}
{Object.entries(PAM_RESOURCE_TYPE_MAP).map(([type, { name, image }]) => {
const resourceType = type as PamResourceType;
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilter((prev) => ({
...prev,
resourceTypes: prev.resourceTypes.includes(resourceType)
? prev.resourceTypes.filter((a) => a !== resourceType)
: [...prev.resourceTypes, resourceType]
}));
}}
key={resourceType}
icon={
filter.resourceTypes.includes(resourceType) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<img
alt={`${name} resource type`}
src={`/images/integrations/${image}`}
className="h-4 w-4"
/>
<span>{name}</span>
</div>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
<OrgPermissionCan