mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 00:27:35 +00:00
pam: accounts table server-side filter, search, pagination
This commit is contained in:
@@ -2,11 +2,14 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { PamFoldersSchema } from "@app/db/schemas";
|
import { PamFoldersSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
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 { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -26,33 +29,58 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
description: "List PAM accounts",
|
description: "List PAM accounts",
|
||||||
querystring: z.object({
|
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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
accounts: SanitizedAccountSchema.array(),
|
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]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
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({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
projectId: req.query.projectId,
|
projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.PAM_ACCOUNT_LIST,
|
type: EventType.PAM_ACCOUNT_LIST,
|
||||||
metadata: {
|
metadata: {
|
||||||
accountCount: response.accounts.length,
|
accountCount: accounts.length,
|
||||||
folderCount: response.folders.length
|
folderCount: folders.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return { accounts, folders, totalCount, folderId, folderPaths };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,100 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TPamAccounts } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } 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, PamAccountView } from "./pam-account-types";
|
||||||
|
|
||||||
export type TPamAccountDALFactory = ReturnType<typeof pamAccountDALFactory>;
|
export type TPamAccountDALFactory = ReturnType<typeof pamAccountDALFactory>;
|
||||||
|
|
||||||
type PamAccountFindFilter = Parameters<typeof buildFindFilter<TPamAccounts>>[0];
|
|
||||||
|
|
||||||
export const pamAccountDALFactory = (db: TDbClient) => {
|
export const pamAccountDALFactory = (db: TDbClient) => {
|
||||||
const orm = ormify(db, TableName.PamAccount);
|
const orm = ormify(db, TableName.PamAccount);
|
||||||
|
|
||||||
const findWithResourceDetails = async (filter: PamAccountFindFilter, tx?: Knex) => {
|
const findByProjectIdWithResourceDetails = async (
|
||||||
const query = (tx || db.replicaNode())(TableName.PamAccount)
|
{
|
||||||
.leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`)
|
projectId,
|
||||||
.select(selectAllTableCols(TableName.PamAccount))
|
folderId,
|
||||||
.select(
|
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
|
// resource
|
||||||
db.ref("name").withSchema(TableName.PamResource).as("resourceName"),
|
db.ref("name").withSchema(TableName.PamResource).as("resourceName"),
|
||||||
db.ref("resourceType").withSchema(TableName.PamResource),
|
db.ref("resourceType").withSchema(TableName.PamResource),
|
||||||
db.ref("encryptedRotationAccountCredentials").withSchema(TableName.PamResource)
|
db.ref("encryptedRotationAccountCredentials").withSchema(TableName.PamResource)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (filter) {
|
const direction = orderDirection === OrderByDirection.ASC ? "ASC" : "DESC";
|
||||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
|
||||||
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PamAccount, filter)));
|
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) => {
|
const findAccountsDueForRotation = async (tx?: Knex) => {
|
||||||
@@ -59,5 +113,9 @@ export const pamAccountDALFactory = (db: TDbClient) => {
|
|||||||
return accounts;
|
return accounts;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...orm, findWithResourceDetails, findAccountsDueForRotation };
|
return {
|
||||||
|
...orm,
|
||||||
|
findByProjectIdWithResourceDetails,
|
||||||
|
findAccountsDueForRotation
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
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 { 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 { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
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 { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPamAccountDALFactory } from "./pam-account-dal";
|
import { TPamAccountDALFactory } from "./pam-account-dal";
|
||||||
import { decryptAccount, decryptAccountCredentials, encryptAccountCredentials } from "./pam-account-fns";
|
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 = {
|
type TPamAccountServiceFactoryDep = {
|
||||||
pamResourceDAL: TPamResourceDALFactory;
|
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({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor: actor.type,
|
actor,
|
||||||
actorAuthMethod: actor.authMethod,
|
actorId,
|
||||||
actorId: actor.id,
|
|
||||||
actorOrgId: actor.orgId,
|
|
||||||
projectId,
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.PAM
|
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 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<typeof pamAccountDAL.findByProjectIdWithResourceDetails>
|
||||||
|
>["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<
|
const decryptedAndPermittedAccounts: Array<
|
||||||
TPamAccounts & {
|
TPamAccounts & {
|
||||||
@@ -359,12 +430,6 @@ export const pamAccountServiceFactory = ({
|
|||||||
> = [];
|
> = [];
|
||||||
|
|
||||||
for await (const account of accountsWithResourceDetails) {
|
for await (const account of accountsWithResourceDetails) {
|
||||||
const accountPath = await getFullPamFolderPath({
|
|
||||||
pamFolderDAL,
|
|
||||||
folderId: account.folderId,
|
|
||||||
projectId: account.projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check permission for each individual account
|
// Check permission for each individual account
|
||||||
if (
|
if (
|
||||||
permission.can(
|
permission.can(
|
||||||
@@ -391,9 +456,27 @@ export const pamAccountServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const folderPaths: Record<string, string> = {};
|
||||||
|
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 {
|
return {
|
||||||
accounts: decryptedAndPermittedAccounts,
|
accounts: decryptedAndPermittedAccounts,
|
||||||
folders
|
folders,
|
||||||
|
totalCount,
|
||||||
|
folderId,
|
||||||
|
folderPaths
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { TPamAccount } from "../pam-resource/pam-resource-types";
|
import { TPamAccount } from "../pam-resource/pam-resource-types";
|
||||||
|
|
||||||
// DTOs
|
// DTOs
|
||||||
@@ -18,3 +20,22 @@ export type TAccessAccountDTO = {
|
|||||||
actorUserAgent: string;
|
actorUserAgent: string;
|
||||||
duration: number;
|
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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,104 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
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<typeof pamFolderDALFactory>;
|
export type TPamFolderDALFactory = ReturnType<typeof pamFolderDALFactory>;
|
||||||
export const pamFolderDALFactory = (db: TDbClient) => {
|
export const pamFolderDALFactory = (db: TDbClient) => {
|
||||||
const orm = ormify(db, TableName.PamFolder);
|
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<ReturnType<typeof orm.findOne>> | 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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,3 +12,12 @@ export enum PamSessionStatus {
|
|||||||
Ended = "ended",
|
Ended = "ended",
|
||||||
Terminated = "terminated"
|
Terminated = "terminated"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PamAccountOrderBy {
|
||||||
|
Name = "name"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PamAccountView {
|
||||||
|
Flat = "flat",
|
||||||
|
Nested = "nested"
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const useCreatePamAccount = () => {
|
|||||||
return data.account;
|
return data.account;
|
||||||
},
|
},
|
||||||
onSuccess: ({ projectId }) => {
|
onSuccess: ({ projectId }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -99,7 +99,7 @@ export const useUpdatePamAccount = () => {
|
|||||||
return data.account;
|
return data.account;
|
||||||
},
|
},
|
||||||
onSuccess: ({ projectId }) => {
|
onSuccess: ({ projectId }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -115,7 +115,7 @@ export const useDeletePamAccount = () => {
|
|||||||
return data.account;
|
return data.account;
|
||||||
},
|
},
|
||||||
onSuccess: ({ projectId }) => {
|
onSuccess: ({ projectId }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -130,7 +130,7 @@ export const useCreatePamFolder = () => {
|
|||||||
return data.folder;
|
return data.folder;
|
||||||
},
|
},
|
||||||
onSuccess: ({ projectId }) => {
|
onSuccess: ({ projectId }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -147,7 +147,7 @@ export const useUpdatePamFolder = () => {
|
|||||||
return data.folder;
|
return data.folder;
|
||||||
},
|
},
|
||||||
onSuccess: ({ projectId }) => {
|
onSuccess: ({ projectId }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -163,7 +163,7 @@ export const useDeletePamFolder = () => {
|
|||||||
return data.folder;
|
return data.folder;
|
||||||
},
|
},
|
||||||
onSuccess: ({ projectId }) => {
|
onSuccess: ({ projectId }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
|
|
||||||
import { TPamResourceOption } from "./types/resource-options";
|
import { TPamResourceOption } from "./types/resource-options";
|
||||||
import { PamResourceType } from "./enums";
|
import { PamResourceType } from "./enums";
|
||||||
import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types";
|
import { TListPamAccountsDTO, TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types";
|
||||||
|
|
||||||
export const pamKeys = {
|
export const pamKeys = {
|
||||||
all: ["pam"] as const,
|
all: ["pam"] as const,
|
||||||
@@ -19,7 +19,12 @@ export const pamKeys = {
|
|||||||
resourceType,
|
resourceType,
|
||||||
resourceId
|
resourceId
|
||||||
],
|
],
|
||||||
listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId],
|
listAccounts: ({ projectId, ...params }: TListPamAccountsDTO) => [
|
||||||
|
...pamKeys.account(),
|
||||||
|
"list",
|
||||||
|
projectId,
|
||||||
|
params
|
||||||
|
],
|
||||||
getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId],
|
getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId],
|
||||||
listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId]
|
listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId]
|
||||||
};
|
};
|
||||||
@@ -98,25 +103,32 @@ export const useGetPamResourceById = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Accounts
|
// Accounts
|
||||||
|
type TListPamAccountsResponse = {
|
||||||
|
accounts: TPamAccount[];
|
||||||
|
folders: TPamFolder[];
|
||||||
|
totalCount: number;
|
||||||
|
folderId?: string;
|
||||||
|
folderPaths: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
export const useListPamAccounts = (
|
export const useListPamAccounts = (
|
||||||
projectId: string,
|
params: TListPamAccountsDTO,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<
|
UseQueryOptions<
|
||||||
{ accounts: TPamAccount[]; folders: TPamFolder[] },
|
TListPamAccountsResponse,
|
||||||
unknown,
|
unknown,
|
||||||
{ accounts: TPamAccount[]; folders: TPamFolder[] },
|
TListPamAccountsResponse,
|
||||||
ReturnType<typeof pamKeys.listAccounts>
|
ReturnType<typeof pamKeys.listAccounts>
|
||||||
>,
|
>,
|
||||||
"queryKey" | "queryFn"
|
"queryKey" | "queryFn"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: pamKeys.listAccounts(projectId),
|
queryKey: pamKeys.listAccounts(params),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ accounts: TPamAccount[]; folders: TPamFolder[] }>(
|
const { data } = await apiRequest.get<TListPamAccountsResponse>("/api/v1/pam/accounts", {
|
||||||
"/api/v1/pam/accounts",
|
params
|
||||||
{ params: { projectId } }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 { TMySQLAccount, TMySQLResource } from "./mysql-resource";
|
||||||
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
||||||
|
|
||||||
@@ -63,6 +64,17 @@ export type TDeletePamResourceDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Account DTOs
|
// 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<
|
export type TCreatePamAccountDTO = Pick<
|
||||||
TPamAccount,
|
TPamAccount,
|
||||||
"name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId"
|
"name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId"
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
|
import { PamAccountView } from "@app/hooks/api/pam";
|
||||||
export enum AccountView {
|
|
||||||
Flat = "flat",
|
|
||||||
Nested = "nested"
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: AccountView;
|
value: PamAccountView;
|
||||||
onChange: (value: AccountView) => void;
|
onChange: (value: PamAccountView) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AccountViewToggle = ({ value, onChange }: Props) => {
|
export const AccountViewToggle = ({ value, onChange }: Props) => {
|
||||||
@@ -16,11 +12,11 @@ export const AccountViewToggle = ({ value, onChange }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(AccountView.Flat);
|
onChange(PamAccountView.Flat);
|
||||||
}}
|
}}
|
||||||
size="xs"
|
size="xs"
|
||||||
className={`${
|
className={`${
|
||||||
value === AccountView.Flat ? "bg-mineshaft-500" : "bg-transparent"
|
value === PamAccountView.Flat ? "bg-mineshaft-500" : "bg-transparent"
|
||||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||||
>
|
>
|
||||||
Hide Folders
|
Hide Folders
|
||||||
@@ -28,11 +24,11 @@ export const AccountViewToggle = ({ value, onChange }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(AccountView.Nested);
|
onChange(PamAccountView.Nested);
|
||||||
}}
|
}}
|
||||||
size="xs"
|
size="xs"
|
||||||
className={`${
|
className={`${
|
||||||
value === AccountView.Nested ? "bg-mineshaft-500" : "bg-transparent"
|
value === PamAccountView.Nested ? "bg-mineshaft-500" : "bg-transparent"
|
||||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||||
>
|
>
|
||||||
Show Folders
|
Show Folders
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
import { ContentLoader } from "@app/components/v2";
|
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import { useListPamAccounts } from "@app/hooks/api/pam";
|
|
||||||
|
|
||||||
import { PamAccountsTable } from "./PamAccountsTable";
|
import { PamAccountsTable } from "./PamAccountsTable";
|
||||||
|
|
||||||
export const PamAccountsSection = () => {
|
export const PamAccountsSection = () => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
const { data, isPending } = useListPamAccounts(currentProject.id, {
|
return <PamAccountsTable projectId={currentProject.id} />;
|
||||||
refetchInterval: 30000
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPending) return <ContentLoader />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PamAccountsTable
|
|
||||||
projectId={currentProject.id}
|
|
||||||
accounts={data?.accounts || []}
|
|
||||||
folders={data?.folders || []}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
@@ -40,11 +41,23 @@ import {
|
|||||||
ProjectPermissionPamAccountActions,
|
ProjectPermissionPamAccountActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { usePagination, usePopUp } from "@app/hooks";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { PAM_RESOURCE_TYPE_MAP, TPamAccount, TPamFolder } from "@app/hooks/api/pam";
|
import {
|
||||||
|
PAM_RESOURCE_TYPE_MAP,
|
||||||
|
PamAccountOrderBy,
|
||||||
|
PamAccountView,
|
||||||
|
TPamAccount,
|
||||||
|
TPamFolder
|
||||||
|
} from "@app/hooks/api/pam";
|
||||||
|
import { useListPamAccounts } from "@app/hooks/api/pam/queries";
|
||||||
|
|
||||||
import { AccountView, AccountViewToggle } from "./AccountViewToggle";
|
import { AccountViewToggle } from "./AccountViewToggle";
|
||||||
import { FolderBreadCrumbs } from "./FolderBreadCrumbs";
|
import { FolderBreadCrumbs } from "./FolderBreadCrumbs";
|
||||||
import { PamAccessAccountModal } from "./PamAccessAccountModal";
|
import { PamAccessAccountModal } from "./PamAccessAccountModal";
|
||||||
import { PamAccountRow } from "./PamAccountRow";
|
import { PamAccountRow } from "./PamAccountRow";
|
||||||
@@ -56,21 +69,15 @@ import { PamFolderRow } from "./PamFolderRow";
|
|||||||
import { PamUpdateAccountModal } from "./PamUpdateAccountModal";
|
import { PamUpdateAccountModal } from "./PamUpdateAccountModal";
|
||||||
import { PamUpdateFolderModal } from "./PamUpdateFolderModal";
|
import { PamUpdateFolderModal } from "./PamUpdateFolderModal";
|
||||||
|
|
||||||
enum OrderBy {
|
|
||||||
Name = "name"
|
|
||||||
}
|
|
||||||
|
|
||||||
type Filters = {
|
type Filters = {
|
||||||
resource: string[];
|
resource: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
accounts: TPamAccount[];
|
|
||||||
folders: TPamFolder[];
|
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
export const PamAccountsTable = ({ projectId }: Props) => {
|
||||||
const navigate = useNavigate({ from: ROUTE_PATHS.Pam.AccountsPage.path });
|
const navigate = useNavigate({ from: ROUTE_PATHS.Pam.AccountsPage.path });
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
@@ -92,7 +99,9 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
from: ROUTE_PATHS.Pam.AccountsPage.id
|
from: ROUTE_PATHS.Pam.AccountsPage.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const [accountView, setAccountView] = useState<AccountView>(initAccountView ?? AccountView.Flat);
|
const [accountView, setAccountView] = useState<PamAccountView>(
|
||||||
|
initAccountView ?? PamAccountView.Flat
|
||||||
|
);
|
||||||
|
|
||||||
const [filters, setFilters] = useState<Filters>({
|
const [filters, setFilters] = useState<Filters>({
|
||||||
resource: []
|
resource: []
|
||||||
@@ -100,10 +109,11 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
search,
|
search,
|
||||||
|
debouncedSearch,
|
||||||
setSearch,
|
setSearch,
|
||||||
setPage,
|
|
||||||
page,
|
page,
|
||||||
perPage,
|
perPage,
|
||||||
|
setPage,
|
||||||
setPerPage,
|
setPerPage,
|
||||||
offset,
|
offset,
|
||||||
orderDirection,
|
orderDirection,
|
||||||
@@ -111,114 +121,67 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
orderBy,
|
orderBy,
|
||||||
setOrderDirection,
|
setOrderDirection,
|
||||||
setOrderBy
|
setOrderBy
|
||||||
} = usePagination<OrderBy>(OrderBy.Name, { initPerPage: 20, initSearch });
|
} = usePagination<PamAccountOrderBy>(PamAccountOrderBy.Name, {
|
||||||
|
initPerPage: getUserTablePreference("pamAccountsTable", PreferenceKey.PerPage, 20),
|
||||||
|
initSearch
|
||||||
|
});
|
||||||
|
|
||||||
const { foldersByParentId, pathMap, folderPaths } = useMemo(() => {
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
const foldersById: Record<string, TPamFolder> = {};
|
setPerPage(newPerPage);
|
||||||
const tempFoldersByParentId: Record<string, TPamFolder[]> = { null: [] };
|
setUserTablePreference("pamAccountsTable", PreferenceKey.PerPage, newPerPage);
|
||||||
const tempPathMap: Record<string, string> = { "/": "null" };
|
};
|
||||||
const tempFolderPaths: Record<string, string> = {};
|
|
||||||
|
|
||||||
folders.forEach((folder) => {
|
const { data, isLoading } = useListPamAccounts({
|
||||||
foldersById[folder.id] = folder;
|
projectId,
|
||||||
if (!tempFoldersByParentId[folder.parentId || "null"]) {
|
accountPath,
|
||||||
tempFoldersByParentId[folder.parentId || "null"] = [];
|
accountView,
|
||||||
}
|
offset,
|
||||||
tempFoldersByParentId[folder.parentId || "null"].push(folder);
|
limit: perPage,
|
||||||
});
|
search: debouncedSearch,
|
||||||
|
orderBy,
|
||||||
|
orderDirection
|
||||||
|
});
|
||||||
|
|
||||||
const buildPaths = (parentId: string | null, currentPath: string) => {
|
const accounts = data?.accounts || [];
|
||||||
(tempFoldersByParentId[parentId || "null"] || []).forEach((folder) => {
|
const folders = data?.folders || [];
|
||||||
const newPath = `${currentPath}${folder.name}/`;
|
const totalCount = data?.totalCount || 0;
|
||||||
tempPathMap[newPath] = folder.id;
|
const folderPaths = data?.folderPaths || {};
|
||||||
tempFolderPaths[folder.id] = newPath;
|
const currentFolderId = data?.folderId ?? null;
|
||||||
buildPaths(folder.id, newPath);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
buildPaths(null, "/");
|
|
||||||
|
|
||||||
return {
|
|
||||||
foldersByParentId: tempFoldersByParentId,
|
|
||||||
pathMap: tempPathMap,
|
|
||||||
folderPaths: tempFolderPaths
|
|
||||||
};
|
|
||||||
}, [folders]);
|
|
||||||
|
|
||||||
const effectiveFolderIdForFiltering = useMemo(() => {
|
|
||||||
if (accountView === AccountView.Flat) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const folderId = pathMap[accountPath];
|
|
||||||
return folderId === "null" ? null : folderId || null;
|
|
||||||
}, [accountView, accountPath, pathMap]);
|
|
||||||
|
|
||||||
const foldersToRender = useMemo(() => {
|
const foldersToRender = useMemo(() => {
|
||||||
if (accountView === AccountView.Flat) {
|
if (accountView === PamAccountView.Flat) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return (foldersByParentId[effectiveFolderIdForFiltering || "null"] || []).filter((folder) =>
|
return folders.filter((folder) =>
|
||||||
folder.name.toLowerCase().includes(search.trim().toLowerCase())
|
folder.name.toLowerCase().includes(search.trim().toLowerCase())
|
||||||
);
|
);
|
||||||
}, [accountView, effectiveFolderIdForFiltering, foldersByParentId, search]);
|
}, [accountView, folders, search]);
|
||||||
|
|
||||||
const accountsToProcess = useMemo(() => {
|
|
||||||
if (accountView === AccountView.Flat) {
|
|
||||||
return accounts;
|
|
||||||
}
|
|
||||||
return accounts.filter(
|
|
||||||
(acc) => (acc.folderId || "null") === (effectiveFolderIdForFiltering || "null")
|
|
||||||
);
|
|
||||||
}, [accountView, accounts, effectiveFolderIdForFiltering]);
|
|
||||||
|
|
||||||
const filteredAccounts = useMemo(
|
const filteredAccounts = useMemo(
|
||||||
() =>
|
() =>
|
||||||
accountsToProcess
|
accounts.filter((account) => {
|
||||||
.filter((account) => {
|
const {
|
||||||
const {
|
name,
|
||||||
name,
|
description,
|
||||||
description,
|
resource: { name: resourceName, id: resourceId }
|
||||||
resource: { name: resourceName, id: resourceId }
|
} = account;
|
||||||
} = account;
|
|
||||||
|
|
||||||
if (filters.resource.length && !filters.resource.includes(resourceId)) {
|
if (filters.resource.length && !filters.resource.includes(resourceId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchValue = search.trim().toLowerCase();
|
const searchValue = search.trim().toLowerCase();
|
||||||
const path = (account.folderId && folderPaths[account.folderId]) || "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
name.toLowerCase().includes(searchValue) ||
|
name.toLowerCase().includes(searchValue) ||
|
||||||
resourceName.toLowerCase().includes(searchValue) ||
|
resourceName.toLowerCase().includes(searchValue) ||
|
||||||
(description || "").toLowerCase().includes(searchValue) ||
|
(description || "").toLowerCase().includes(searchValue)
|
||||||
path.toLowerCase().includes(searchValue)
|
);
|
||||||
);
|
}),
|
||||||
})
|
[accounts, search, filters]
|
||||||
.sort((a, b) => {
|
|
||||||
const [accOne, accTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
|
||||||
|
|
||||||
switch (orderBy) {
|
|
||||||
case OrderBy.Name:
|
|
||||||
default:
|
|
||||||
return accOne.name.toLowerCase().localeCompare(accTwo.name.toLowerCase());
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[accountsToProcess, orderDirection, search, orderBy, filters, folderPaths]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useResetPageHelper({
|
const handleSort = (column: PamAccountOrderBy) => {
|
||||||
totalCount: filteredAccounts.length,
|
|
||||||
offset,
|
|
||||||
setPage
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentPageData = useMemo(
|
|
||||||
() => filteredAccounts.slice(offset, perPage * page),
|
|
||||||
[filteredAccounts, offset, perPage, page]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSort = (column: OrderBy) => {
|
|
||||||
if (column === orderBy) {
|
if (column === orderBy) {
|
||||||
toggleOrderDirection();
|
toggleOrderDirection();
|
||||||
return;
|
return;
|
||||||
@@ -228,15 +191,16 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
setOrderDirection(OrderByDirection.ASC);
|
setOrderDirection(OrderByDirection.ASC);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClassName = (col: OrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
const getClassName = (col: PamAccountOrderBy) =>
|
||||||
|
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||||
|
|
||||||
const getColSortIcon = (col: OrderBy) =>
|
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(filters.resource.length);
|
||||||
|
|
||||||
const handleFolderClick = (folder: TPamFolder) => {
|
const handleFolderClick = (folder: TPamFolder) => {
|
||||||
if (accountView === AccountView.Flat) {
|
if (accountView === PamAccountView.Flat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newPath = `${accountPath}${folder.name}/`;
|
const newPath = `${accountPath}${folder.name}/`;
|
||||||
@@ -256,7 +220,7 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{accountView === AccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
|
{accountView === PamAccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
|
||||||
<div className="mt-4 flex gap-2">
|
<div className="mt-4 flex gap-2">
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
|
||||||
{(isAllowed) =>
|
{(isAllowed) =>
|
||||||
@@ -268,8 +232,8 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
navigate({
|
navigate({
|
||||||
search: (prev) => ({
|
search: (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
accountView: e === AccountView.Flat ? undefined : e,
|
accountView: e === PamAccountView.Flat ? undefined : e,
|
||||||
accountPath: e === AccountView.Flat ? "/" : prev.accountPath
|
accountPath: e === PamAccountView.Flat ? "/" : prev.accountPath
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -358,12 +322,12 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handlePopUpOpen("addAccount")}
|
onClick={() => handlePopUpOpen("addAccount")}
|
||||||
isDisabled={!isAllowedToCreateAccounts}
|
isDisabled={!isAllowedToCreateAccounts}
|
||||||
className={`h-10 transition-colors ${accountView === AccountView.Flat ? "" : "rounded-r-none"}`}
|
className={`h-10 transition-colors ${accountView === PamAccountView.Flat ? "" : "rounded-r-none"}`}
|
||||||
>
|
>
|
||||||
Add Account
|
Add Account
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{accountView !== AccountView.Flat && (
|
{accountView !== PamAccountView.Flat && (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={popUp.misc.isOpen}
|
open={popUp.misc.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
|
||||||
@@ -414,11 +378,11 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
Accounts
|
Accounts
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
className={getClassName(OrderBy.Name)}
|
className={getClassName(PamAccountOrderBy.Name)}
|
||||||
ariaLabel="sort"
|
ariaLabel="sort"
|
||||||
onClick={() => handleSort(OrderBy.Name)}
|
onClick={() => handleSort(PamAccountOrderBy.Name)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={getColSortIcon(OrderBy.Name)} />
|
<FontAwesomeIcon icon={getColSortIcon(PamAccountOrderBy.Name)} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
@@ -426,45 +390,48 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{accountView !== AccountView.Flat &&
|
{isLoading && <TableSkeleton columns={2} innerKey="pam-accounts" />}
|
||||||
foldersToRender.map((folder) => (
|
{!isLoading && (
|
||||||
<PamFolderRow
|
<>
|
||||||
key={folder.id}
|
{accountView !== PamAccountView.Flat &&
|
||||||
folder={folder}
|
foldersToRender.map((folder) => (
|
||||||
search={search}
|
<PamFolderRow
|
||||||
onClick={() => handleFolderClick(folder)}
|
key={folder.id}
|
||||||
onUpdate={(e) => handlePopUpOpen("updateFolder", e)}
|
folder={folder}
|
||||||
onDelete={(e) => handlePopUpOpen("deleteFolder", e)}
|
search={search}
|
||||||
/>
|
onClick={() => handleFolderClick(folder)}
|
||||||
))}
|
onUpdate={(e) => handlePopUpOpen("updateFolder", e)}
|
||||||
{currentPageData.map((account) => (
|
onDelete={(e) => handlePopUpOpen("deleteFolder", e)}
|
||||||
<PamAccountRow
|
/>
|
||||||
key={account.id}
|
))}
|
||||||
account={account}
|
{filteredAccounts.map((account) => (
|
||||||
search={search}
|
<PamAccountRow
|
||||||
isFlatView={accountView === AccountView.Flat}
|
key={account.id}
|
||||||
accountPath={
|
account={account}
|
||||||
account.folderId ? folderPaths[account.folderId]?.slice(0, -1) : undefined
|
search={search}
|
||||||
}
|
isFlatView={accountView === PamAccountView.Flat}
|
||||||
onAccess={(e) => {
|
accountPath={account.folderId ? folderPaths[account.folderId] : undefined}
|
||||||
handlePopUpOpen("accessAccount", e);
|
onAccess={(e) => {
|
||||||
}}
|
handlePopUpOpen("accessAccount", e);
|
||||||
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
|
}}
|
||||||
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
|
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
|
||||||
/>
|
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{Boolean(filteredAccounts.length) && (
|
{Boolean(totalCount) && !isLoading && (
|
||||||
<Pagination
|
<Pagination
|
||||||
count={filteredAccounts.length}
|
count={totalCount}
|
||||||
page={page}
|
page={page}
|
||||||
perPage={perPage}
|
perPage={perPage}
|
||||||
onChangePage={setPage}
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
onChangePerPage={setPerPage}
|
onChangePerPage={handlePerPageChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isContentEmpty && (
|
{!isLoading && isContentEmpty && (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
|
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
|
||||||
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
||||||
@@ -485,7 +452,7 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
isOpen={popUp.addFolder.isOpen}
|
isOpen={popUp.addFolder.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
currentFolderId={effectiveFolderIdForFiltering}
|
currentFolderId={currentFolderId}
|
||||||
/>
|
/>
|
||||||
<PamAccessAccountModal
|
<PamAccessAccountModal
|
||||||
isOpen={popUp.accessAccount.isOpen}
|
isOpen={popUp.accessAccount.isOpen}
|
||||||
@@ -506,7 +473,7 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
|||||||
isOpen={popUp.addAccount.isOpen}
|
isOpen={popUp.addAccount.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("addAccount", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("addAccount", isOpen)}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
currentFolderId={effectiveFolderIdForFiltering}
|
currentFolderId={currentFolderId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react
|
|||||||
import { zodValidator } from "@tanstack/zod-adapter";
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AccountView } from "./components/AccountViewToggle";
|
import { PamAccountView } from "@app/hooks/api/pam";
|
||||||
|
|
||||||
import { PamAccountsPage } from "./PamAccountsPage";
|
import { PamAccountsPage } from "./PamAccountsPage";
|
||||||
|
|
||||||
const PamAccountsPageQueryParamsSchema = z.object({
|
const PamAccountsPageQueryParamsSchema = z.object({
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
accountView: z.nativeEnum(AccountView).optional(),
|
accountView: z.nativeEnum(PamAccountView).optional(),
|
||||||
accountPath: z.string().catch("/")
|
accountPath: z.string().catch("/")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user