diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index f2d0f98ef..11f2cf774 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -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({ diff --git a/backend/src/ee/services/pam-resource/pam-resource-dal.ts b/backend/src/ee/services/pam-resource/pam-resource-dal.ts index 7205542ed..9e5cbc985 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-dal.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-dal.ts @@ -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)); diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index a9f67897f..8f8b681e6 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -92,6 +92,7 @@ export const useListPamResources = ( return data; }, + placeholderData: (prev) => prev, ...options }); }; diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index ef6309aa8..d8ada6cb7 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -59,6 +59,7 @@ export type TListPamResourcesDTO = { orderBy?: PamResourceOrderBy; orderDirection?: OrderByDirection; search?: string; + filterResourceTypes?: string; }; export type TCreatePamResourceDTO = Pick< diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx index cb2e35a04..4e550a81f 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx @@ -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({ - resourceType: [] + const [filter, setFilter] = useState({ + 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) => { Resource Type - {resources.length ? ( - [...new Set(resources.map(({ resourceType }) => resourceType))].map((type) => { - const { name, image } = PAM_RESOURCE_TYPE_MAP[type]; - - return ( - { - 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) && ( - - ) - } - iconPos="right" - > -
- {`${name} - {name} -
-
- ); - }) - ) : ( - No Resources - )} + {Object.entries(PAM_RESOURCE_TYPE_MAP).map(([type, { name, image }]) => { + const resourceType = type as PamResourceType; + return ( + { + 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) && ( + + ) + } + iconPos="right" + > +
+ {`${name} + {name} +
+
+ ); + })}