diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 40fbc35b1..c0e18a763 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -84,9 +84,10 @@ const getZodDefaultValue = (type: unknown, value: string | number | boolean | Ob } }; -const bigIntegerColumns = { +const bigIntegerColumns: Record = { "folder_commits": ["commitId"] -} +}; + const main = async () => { const tables = ( diff --git a/backend/src/ee/routes/v1/pit-router.ts b/backend/src/ee/routes/v1/pit-router.ts index ab88b5bd3..a2a6ae992 100644 --- a/backend/src/ee/routes/v1/pit-router.ts +++ b/backend/src/ee/routes/v1/pit-router.ts @@ -10,8 +10,8 @@ import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { booleanSchema } from "@app/server/routes/sanitizedSchemas"; -import { AuthMode } from "@app/services/auth/auth-type"; -import { ChangeType, ResourceChange } from "@app/services/folder-commit/folder-commit-service"; +import { ActorAuthMethod, ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { ChangeType } from "@app/services/folder-commit/folder-commit-service"; import { commitChangesResponseSchema } from "@app/services/folder-commit/folder-commit-types"; const commitHistoryItemSchema = z.object({ @@ -98,22 +98,34 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { querystring: z.object({ environment: z.string().trim(), path: z.string().trim().default("/").transform(removeTrailingSlash), - workspaceId: z.string().trim() + workspaceId: z.string().trim(), + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(100).default(20), + search: z.string().trim().optional(), + sort: z.enum(["asc", "desc"]).default("desc") }), response: { - 200: commitHistoryItemSchema.array() + 200: z.object({ + commits: commitHistoryItemSchema.array(), + total: z.number(), + hasMore: z.boolean() + }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const commits = await server.services.folderCommit.getCommitsForFolder({ + const result = await server.services.folderCommit.getCommitsForFolder({ actor: req.permission?.type, actorId: req.permission?.id, actorOrgId: req.permission?.orgId, actorAuthMethod: req.permission?.authMethod, projectId: req.query.workspaceId, environment: req.query.environment, - path: req.query.path + path: req.query.path, + offset: req.query.offset, + limit: req.query.limit, + search: req.query.search, + sort: req.query.sort }); await server.services.auditLog.createAuditLog({ @@ -124,30 +136,47 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { metadata: { environment: req.query.environment, path: req.query.path, - commitCount: commits.length.toString() + commitCount: result.commits.length.toString(), + offset: req.query.offset.toString(), + limit: req.query.limit.toString(), + search: req.query.search, + sort: req.query.sort } } }); - return commits.map((commit) => ({ - ...commit, - commitId: commit.commitId.toString() - })); + return { + commits: result.commits.map((commit) => ({ + ...commit, + commitId: commit.commitId.toString() + })), + total: result.total, + hasMore: result.hasMore + }; } }); const getChangeVersions = async ( - change: ResourceChange, + change: { + secretVersion?: string; + secretId?: string; + id?: string; + isUpdate?: boolean; + changeType?: string; + }, previousVersion: string, actorId: string, - actor: string, + actor: ActorType, actorOrgId: string, - actorAuthMethod: string, + actorAuthMethod: ActorAuthMethod, folderId: string ) => { if (change.secretVersion) { const currentVersion = change.secretVersion || "1"; const secretId = change.secretId ? change.secretId : change.id; + if (!secretId) { + return; + } // eslint-disable-next-line no-await-in-loop const versions = await server.services.secret.getSecretVersionsV2ByIds({ actorId, @@ -176,7 +205,15 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { } }; - const getFolderVersions = async (change: ResourceChange, fromVersion: string, folderId: string) => { + const getFolderVersions = async ( + change: { + folderVersion?: string; + isUpdate?: boolean; + changeType?: string; + }, + fromVersion: string, + folderId: string + ) => { const currentVersion = change.folderVersion || "1"; // eslint-disable-next-line no-await-in-loop const versions = await server.services.folder.getFolderVersionsByIds({ @@ -218,20 +255,18 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { commitId: req.params.commitId }); for (const change of changes.changes) { - if (change.secretVersionId) { - change.objectType = "secret"; + if (change.secretVersionId && change.secretVersion) { // eslint-disable-next-line no-await-in-loop change.versions = await getChangeVersions( change, (Number.parseInt(change.secretVersion, 10) - 1).toString(), - req.permission?.id, - req.permission?.type, - req.permission?.orgId, - req.permission?.authMethod, + req.permission.id, + req.permission.type, + req.permission.orgId, + req.permission.authMethod, change.folderId ); - } else if (change.folderVersionId && change.folderChangeId) { - change.objectType = "folder"; + } else if (change.folderVersionId && change.folderChangeId && change.folderVersion) { // eslint-disable-next-line no-await-in-loop change.versions = await getFolderVersions( change, @@ -340,10 +375,10 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { change.versions = await getChangeVersions( change, change.fromVersion || "1", - req.permission?.id, - req.permission?.type, - req.permission?.orgId, - req.permission?.authMethod, + req.permission.id, + req.permission.type, + req.permission.orgId, + req.permission.authMethod, diff.folderId ); } diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 1b8b725a6..dc0315a82 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -2927,6 +2927,10 @@ interface GetProjectPitCommitsEvent { commitCount: string; environment: string; path: string; + offset: string; + limit: string; + search?: string; + sort: string; }; } diff --git a/backend/src/services/folder-checkpoint/folder-checkpoint-dal.ts b/backend/src/services/folder-checkpoint/folder-checkpoint-dal.ts index 7dc150ab5..51bba9cc0 100644 --- a/backend/src/services/folder-checkpoint/folder-checkpoint-dal.ts +++ b/backend/src/services/folder-checkpoint/folder-checkpoint-dal.ts @@ -101,7 +101,7 @@ export const folderCheckpointDALFactory = (db: TDbClient) => { `${TableName.FolderCommit}.id` ) .where(`${TableName.FolderCommit}.folderId`, "=", folderId) - .where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId) + .where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId.toString()) .select(selectAllTableCols(TableName.FolderCheckpoint)) .select( db.ref("actorMetadata").withSchema(TableName.FolderCommit), diff --git a/backend/src/services/folder-commit-changes/folder-commit-changes-dal.ts b/backend/src/services/folder-commit-changes/folder-commit-changes-dal.ts index de3d9463b..e8807ddc4 100644 --- a/backend/src/services/folder-commit-changes/folder-commit-changes-dal.ts +++ b/backend/src/services/folder-commit-changes/folder-commit-changes-dal.ts @@ -14,7 +14,7 @@ import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex"; export type TFolderCommitChangesDALFactory = ReturnType; -type CommitChangeWithCommitInfo = TFolderCommitChanges & { +export type CommitChangeWithCommitInfo = TFolderCommitChanges & { actorMetadata: unknown; actorType: string; message?: string | null; @@ -25,6 +25,7 @@ type CommitChangeWithCommitInfo = TFolderCommitChanges & { secretVersion?: string; secretId?: string; folderChangeId?: string; + objectType?: string; versions?: { secretKey?: string; secretComment?: string; diff --git a/backend/src/services/folder-commit/folder-commit-dal.ts b/backend/src/services/folder-commit/folder-commit-dal.ts index 3e7e080ae..53fc5353b 100644 --- a/backend/src/services/folder-commit/folder-commit-dal.ts +++ b/backend/src/services/folder-commit/folder-commit-dal.ts @@ -9,7 +9,7 @@ import { TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; +import { DatabaseError, NotFoundError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex"; export type TFolderCommitDALFactory = ReturnType; @@ -209,8 +209,8 @@ export const folderCommitDALFactory = (db: TDbClient) => { const commits = await (tx || db.replicaNode())(TableName.FolderCommit) // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter({ folderId }, TableName.FolderCommit)) - .andWhere(`${TableName.FolderCommit}.commitId`, ">", checkpointCommitNumber) - .andWhere(`${TableName.FolderCommit}.commitId`, "<=", targetCommitNumber) + .andWhere(`${TableName.FolderCommit}.commitId`, ">", checkpointCommitNumber.toString()) + .andWhere(`${TableName.FolderCommit}.commitId`, "<=", targetCommitNumber.toString()) .select(selectAllTableCols(TableName.FolderCommit)) .orderBy(`${TableName.FolderCommit}.commitId`, "asc"); @@ -391,12 +391,89 @@ export const folderCommitDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.FolderCommit)) .orderBy("commitId", "desc") .first(); + if (!doc) { + throw new NotFoundError({ + message: `Folder commit not found for ID ${id}` + }); + } return doc; } catch (error) { throw new DatabaseError({ error, name: "FindById" }); } }; + const findByFolderIdPaginated = async ( + folderId: string, + options: { + offset?: number; + limit?: number; + search?: string; + sort?: "asc" | "desc"; + } = {}, + tx?: Knex + ): Promise<{ + commits: TFolderCommits[]; + total: number; + hasMore: boolean; + }> => { + try { + const { offset = 0, limit = 20, search, sort = "desc" } = options; + const trx = tx || db.replicaNode(); + + // Build base query + let baseQuery = trx(TableName.FolderCommit).where({ folderId }); + + // Add search functionality + if (search) { + baseQuery = baseQuery.where((qb) => { + void qb.whereILike("message", `%${search}%`); + }); + } + + // Get total count + const totalResult = await baseQuery.clone().count("*", { as: "count" }).first(); + const total = Number(totalResult?.count || 0); + + // Get paginated commits + const folderCommits = await baseQuery.select("*").orderBy("createdAt", sort).limit(limit).offset(offset); + + if (folderCommits.length === 0) { + return { commits: [], total, hasMore: false }; + } + + // Get all commit IDs for changes + const commitIds = folderCommits.map((commit) => commit.id); + + // Get all related changes + const changes = await trx(TableName.FolderCommitChanges).whereIn("folderCommitId", commitIds).select("*"); + + const changesMap = changes.reduce( + (acc, change) => { + const { folderCommitId } = change; + if (!acc[folderCommitId]) acc[folderCommitId] = []; + acc[folderCommitId].push(change); + return acc; + }, + {} as Record + ); + + const commitsWithChanges = folderCommits.map((commit) => ({ + ...commit, + changes: changesMap[commit.id] || [] + })); + + const hasMore = offset + limit < total; + + return { + commits: commitsWithChanges, + total, + hasMore + }; + } catch (error) { + throw new DatabaseError({ error, name: "FindByFolderIdPaginated" }); + } + }; + return { ...restOfOrm, findByFolderId, @@ -411,6 +488,7 @@ export const folderCommitDALFactory = (db: TDbClient) => { findLatestCommitByFolderIds, findAllFolderCommitsAfter, findPreviousCommitTo, - findById + findById, + findByFolderIdPaginated }; }; diff --git a/backend/src/services/folder-commit/folder-commit-service.test.ts b/backend/src/services/folder-commit/folder-commit-service.test.ts index 26c37f0ff..a0253bf96 100644 --- a/backend/src/services/folder-commit/folder-commit-service.test.ts +++ b/backend/src/services/folder-commit/folder-commit-service.test.ts @@ -107,7 +107,7 @@ describe("folderCommitServiceFactory", () => { deleteById: vi.fn().mockResolvedValue({}), create: vi.fn().mockResolvedValue({}), updateById: vi.fn().mockResolvedValue({}), - find: vi.fn().mockResolvedValue([]), + find: vi.fn().mockResolvedValue({}), // Changed from [] to {} to match Object.values() expectation findByIdsWithLatestVersion: vi.fn().mockResolvedValue({}) }; @@ -260,6 +260,17 @@ describe("folderCommitServiceFactory", () => { mockFolderDAL.findByParentId.mockResolvedValue([]); mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]); + // Mock folderVersionDAL.find to return an object with folder version data + mockFolderVersionDAL.find.mockResolvedValue({ + "folder-version-1": { + id: "folder-version-1", + folderId: "sub-folder-id", + envId: "env-id", + name: "Test Folder", + version: 1 + } + }); + const data = { actor: { type: ActorType.IDENTITY, @@ -449,6 +460,12 @@ describe("folderCommitServiceFactory", () => { const actorType = ActorType.USER; const projectId = "project-id"; + // Mock the transaction to properly handle the error + mockFolderCommitDAL.transaction.mockImplementation(async (callback) => { + return await callback({} as Knex); + }); + + // Mock findById to return null inside the transaction mockFolderCommitDAL.findById.mockResolvedValue(null); // Act & Assert @@ -501,8 +518,8 @@ describe("folderCommitServiceFactory", () => { const actorType = ActorType.USER; const differences = [ - { type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, commitId: 1 }, - { type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: 1 } + { type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, commitId: BigInt(1) }, + { type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: BigInt(1) } ]; const secretVersions = { diff --git a/backend/src/services/folder-commit/folder-commit-service.ts b/backend/src/services/folder-commit/folder-commit-service.ts index 21a50ed9f..fb78bf6f1 100644 --- a/backend/src/services/folder-commit/folder-commit-service.ts +++ b/backend/src/services/folder-commit/folder-commit-service.ts @@ -76,6 +76,7 @@ export type ResourceChange = { commitId: bigint; createdAt?: Date; parentId?: string; + isUpdate?: boolean; secretKey?: string; secretVersion?: string; secretId?: string; @@ -997,7 +998,11 @@ export const folderCommitServiceFactory = ({ actorOrgId, projectId, environment, - path + path, + offset = 0, + limit = 20, + search, + sort = "desc" }: { actor: ActorType; actorId: string; @@ -1006,6 +1011,10 @@ export const folderCommitServiceFactory = ({ projectId: string; environment: string; path: string; + offset: number; + limit: number; + search?: string; + sort: "asc" | "desc"; }) => { await checkProjectPermission({ actor, @@ -1020,7 +1029,12 @@ export const folderCommitServiceFactory = ({ message: `Folder not found for project ID ${projectId}, environment ${environment}, path ${path}` }); } - const folderCommits = await folderCommitDAL.findByFolderId(folder.id); + const folderCommits = await folderCommitDAL.findByFolderIdPaginated(folder.id, { + offset, + limit, + search, + sort + }); return folderCommits; }; @@ -1516,6 +1530,9 @@ export const folderCommitServiceFactory = ({ projectId: string; message?: string; }) => { + if (!permissionService) { + throw new Error("Permission service not initialized"); + } const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -1551,8 +1568,11 @@ export const folderCommitServiceFactory = ({ } // Sort commits by commitId (which appears to be numeric) - const sortedCommits = allCommits.sort((a, b) => a.commitId - b.commitId); - + const sortedCommits = allCommits.sort((a, b) => { + if (a.commitId < b.commitId) return -1; + if (a.commitId > b.commitId) return 1; + return 0; + }); // Find the index of the commit to revert const commitIndex = sortedCommits.findIndex((c) => c.id === commitId); if (commitIndex === -1) { diff --git a/backend/src/services/folder-commit/folder-commit-types.ts b/backend/src/services/folder-commit/folder-commit-types.ts index b848417f9..d99185de7 100644 --- a/backend/src/services/folder-commit/folder-commit-types.ts +++ b/backend/src/services/folder-commit/folder-commit-types.ts @@ -1,7 +1,7 @@ import { z } from "zod"; const secretVersionSchema = z.object({ - secretKey: z.string(), + secretKey: z.string().optional().nullable(), secretComment: z.string().optional().nullable(), skipMultilineEncoding: z.boolean().optional().nullable(), secretReminderRepeatDays: z.number().optional().nullable(), @@ -13,7 +13,7 @@ const secretVersionSchema = z.object({ }); const folderVersionSchema = z.object({ - name: z.string() + name: z.string().optional().nullable() }); const baseChangeSchema = z.object({ @@ -37,46 +37,37 @@ const baseChangeSchema = z.object({ folderId: z.string().optional() }); -const secretChangeSchema = baseChangeSchema.extend({ - objectType: z.literal("secret"), - secretVersionId: z.string(), - folderVersionId: z.null(), - folderName: z.null(), - folderChangeId: z.null(), - secretKey: z.string(), - secretVersion: z.union([z.string(), z.number()]), - secretId: z.string(), - versions: z.array(secretVersionSchema).optional() +const commitChangeSchema = baseChangeSchema.extend({ + secretVersionId: z.string().optional().nullable(), + folderVersionId: z.string().optional().nullable(), + folderName: z.string().optional().nullable(), + folderChangeId: z.string().optional().nullable(), + secretKey: z.string().optional().nullable(), + secretVersion: z.union([z.string(), z.number()]).optional().nullable(), + secretId: z.string().optional().nullable(), + folderVersion: z.union([z.string(), z.number()]).optional().nullable(), + versions: z.array(z.union([secretVersionSchema, folderVersionSchema])).optional() }); -const folderChangeSchema = baseChangeSchema.extend({ - objectType: z.literal("folder"), - secretVersionId: z.null(), - folderVersionId: z.string(), - folderName: z.string(), - folderChangeId: z.string(), - folderVersion: z.union([z.string(), z.number()]), - secretKey: z.null(), - secretId: z.null(), - versions: z.array(folderVersionSchema).optional() -}); - -const commitChangeSchema = z.discriminatedUnion("objectType", [secretChangeSchema, folderChangeSchema]); - const commitSchema = z.object({ id: z.string(), commitId: z.string(), - actorMetadata: z.object({ - id: z.string(), - name: z.string() - }), + actorMetadata: z + .union([ + z.object({ + id: z.string().optional(), + name: z.string().optional() + }), + z.unknown() + ]) + .optional(), actorType: z.string(), - message: z.string().nullable(), + message: z.string().nullable().optional(), folderId: z.string(), envId: z.string(), createdAt: z.union([z.string(), z.date()]), updatedAt: z.union([z.string(), z.date()]), - changes: z.array(commitChangeSchema) + changes: z.array(commitChangeSchema).optional() }); export const commitChangesResponseSchema = z.object({ diff --git a/backend/src/services/folder-tree-checkpoint/folder-tree-checkpoint-dal.ts b/backend/src/services/folder-tree-checkpoint/folder-tree-checkpoint-dal.ts index 5eced9da3..cc0634e74 100644 --- a/backend/src/services/folder-tree-checkpoint/folder-tree-checkpoint-dal.ts +++ b/backend/src/services/folder-tree-checkpoint/folder-tree-checkpoint-dal.ts @@ -41,7 +41,7 @@ export const folderTreeCheckpointDALFactory = (db: TDbClient) => { ) // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(`${TableName.FolderCommit}.envId`, "=", envId) - .where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId) + .andWhere(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId.toString()) .select(selectAllTableCols(TableName.FolderTreeCheckpoint)) .select(db.ref("commitId").withSchema(TableName.FolderCommit)) .orderBy(`${TableName.FolderCommit}.commitId`, "desc") diff --git a/backend/src/services/secret-folder/secret-folder-version-dal.ts b/backend/src/services/secret-folder/secret-folder-version-dal.ts index dc8d88bd5..b2c169a5a 100644 --- a/backend/src/services/secret-folder/secret-folder-version-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-version-dal.ts @@ -145,7 +145,9 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => { // Create lookup map for max versions const maxVersionMap = maxVersionsQuery.reduce>((acc, item) => { - acc[item.folderId] = item.maxVersion; + if (item.maxVersion) { + acc[item.folderId] = item.maxVersion; + } return acc; }, {}); @@ -160,6 +162,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => { try { if (!folderIds.length && (!versionIds || !versionIds.length)) return {}; + // Run both queries in parallel const [latestVersions, specificVersionsWithLatest] = await Promise.all([ folderIds.length ? getLatestFolderVersions(folderIds, tx) : [], versionIds?.length ? getSpecificFolderVersionsWithLatest(versionIds, tx) : [] diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index e9c9b1cd9..aaf74b735 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -324,7 +324,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { .whereIn("secretId", specificSecretIds) .groupBy("secretId") .select("secretId") - .max("version as maxVersion"); + .max("version", { as: "maxVersion" }); // Create a lookup map for max versions const maxVersionMap = maxVersionsQuery.reduce( diff --git a/frontend/src/hooks/api/folderCommits/queries.tsx b/frontend/src/hooks/api/folderCommits/queries.tsx index a7b4a7a84..503a0d3f1 100644 --- a/frontend/src/hooks/api/folderCommits/queries.tsx +++ b/frontend/src/hooks/api/folderCommits/queries.tsx @@ -68,13 +68,29 @@ const fetchFolderCommitsCount = async ({ const fetchFolderCommitHistory = async ( workspaceId: string, environment: string, - directory: string -): Promise => { - const res = await apiRequest.get("/api/v1/pit/commits", { + directory: string, + offset: number = 0, + limit: number = 20, + search?: string, + sort: "asc" | "desc" = "desc" +): Promise<{ + commits: CommitHistoryItem[]; + total: number; + hasMore: boolean; +}> => { + const res = await apiRequest.get<{ + commits: CommitHistoryItem[]; + total: number; + hasMore: boolean; + }>("/api/v1/pit/commits", { params: { environment, path: directory, - workspaceId + workspaceId, + offset, + limit, + search, + sort } }); return res.data; @@ -225,15 +241,30 @@ export const useGetFolderCommitsCount = ({ export const useGetFolderCommitHistory = ({ workspaceId, environment, - directory + directory, + offset = 0, + limit = 20, + search, + sort = "desc" }: { workspaceId: string; environment: string; directory: string; + offset?: number; + limit?: number; + search?: string; + sort?: "asc" | "desc"; }) => { return useQuery({ - queryKey: commitKeys.history({ workspaceId, environment, directory }), - queryFn: () => fetchFolderCommitHistory(workspaceId, environment, directory), + queryKey: [ + commitKeys.history({ workspaceId, environment, directory }), + offset, + limit, + search, + sort + ], + queryFn: () => + fetchFolderCommitHistory(workspaceId, environment, directory, offset, limit, search, sort), enabled: Boolean(workspaceId && environment) }); }; diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index f9684ee09..a4e5dd42c 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -11,7 +11,8 @@ export enum ApprovalStatus { export enum CommitType { DELETE = "delete", UPDATE = "update", - CREATE = "create" + CREATE = "create", + ADD = "add" } export type TSecretApprovalSecChangeData = { diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx index f1810bdf7..e563bf6f4 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx @@ -22,6 +22,7 @@ import { import { usePopUp } from "@app/hooks"; import { CommitWithChanges } from "@app/hooks/api/folderCommits"; import { useCommitRevert, useGetCommitDetails } from "@app/hooks/api/folderCommits/queries"; +import { CommitType } from "@app/hooks/api/types"; import { SecretVersionDiffView } from "../SecretVersionDiffView"; import { MergedItem } from "./types"; @@ -147,9 +148,9 @@ export const CommitDetailsTab = ({ const commitChanges = parsedCommitDetails.changes?.changes || []; // Separate changes by type - const addedChanges = commitChanges.filter((c) => c.changeType === "add" && !c.isUpdate); - const updatedChanges = commitChanges.filter((c) => c.changeType === "add" && c.isUpdate); - const deletedChanges = commitChanges.filter((c) => c.changeType === "delete"); + const addedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && !c.isUpdate); + const updatedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && c.isUpdate); + const deletedChanges = commitChanges.filter((c) => c.changeType === CommitType.DELETE); // Create merged item list from changes only const changedItems: MergedItem[] = []; diff --git a/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx b/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx index 19ff6df1b..357577d9e 100644 --- a/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx +++ b/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { faArrowDownWideShort, faArrowUpWideShort, @@ -144,41 +144,68 @@ export const CommitHistoryTab = ({ secretPath: string; }) => { const [searchTerm, setSearchTerm] = useState(""); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); - const [visibleCommits, setVisibleCommits] = useState(10); + const [offset, setOffset] = useState(0); + const [allCommits, setAllCommits] = useState([]); + const debounceTimeoutRef = useRef(); + const limit = 5; - const { data: commits, isLoading } = useGetFolderCommitHistory({ + // Debounce search term + useEffect(() => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + + debounceTimeoutRef.current = setTimeout(() => { + setDebouncedSearchTerm(searchTerm); + }, 500); + + return () => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + }; + }, [searchTerm]); + + const { + data: response, + isLoading, + isFetching + } = useGetFolderCommitHistory({ workspaceId: projectId, environment, - directory: secretPath + directory: secretPath, + offset, + limit, + search: debouncedSearchTerm, + sort: sortDirection }); - const filteredCommits = useMemo(() => { - if (!commits?.length) return []; + const commits = response?.commits || []; + const hasMore = response?.hasMore || false; - return commits.filter( - (commit) => - commit.id?.includes(searchTerm) || - commit.message?.toLowerCase().includes(searchTerm.toLowerCase()) || - commit.actorMetadata?.name?.toLowerCase().includes(searchTerm.toLowerCase()) - ); - }, [commits, searchTerm]); + // Reset accumulated commits when search or sort changes + useEffect(() => { + setAllCommits([]); + setOffset(0); + }, [debouncedSearchTerm, sortDirection]); - const sortedCommits = useMemo(() => { - if (!filteredCommits?.length) return []; + // Accumulate commits instead of replacing them + useEffect(() => { + if (commits.length > 0) { + if (offset === 0) { + // First load or after search/sort change - replace all commits + setAllCommits(commits); + } else { + // Subsequent loads - append new commits + setAllCommits((prev) => [...prev, ...commits]); + } + } + }, [commits, offset]); - return [...filteredCommits].sort((a, b) => { - const dateA = new Date(a.createdAt).getTime(); - const dateB = new Date(b.createdAt).getTime(); - return sortDirection === "desc" ? dateB - dateA : dateA - dateB; - }); - }, [filteredCommits, sortDirection]); - - const displayedCommits = useMemo(() => { - return sortedCommits.slice(0, visibleCommits); - }, [sortedCommits, visibleCommits]); const groupedCommits = useMemo(() => { - return displayedCommits.reduce( + return allCommits.reduce( (acc, commit) => { const date = format(new Date(commit.createdAt), "MMM d, yyyy"); if (!acc[date]) { @@ -189,25 +216,21 @@ export const CommitHistoryTab = ({ }, {} as Record ); - }, [displayedCommits]); + }, [allCommits]); const handleSort = useCallback(() => { setSortDirection((prev) => (prev === "desc" ? "asc" : "desc")); }, []); - const loadMoreCommits = useCallback(() => { - setVisibleCommits((prev) => prev + 10); + const handleSearch = useCallback((value: string) => { + setSearchTerm(value); }, []); - if (isLoading) { - return ( -
- -
- ); - } - - const hasMoreCommits = sortedCommits.length > visibleCommits; + const loadMoreCommits = useCallback(() => { + if (hasMore && !isFetching) { + setOffset((prev) => prev + limit); + } + }, [hasMore, isFetching, limit]); return (
@@ -217,7 +240,8 @@ export const CommitHistoryTab = ({ setSearchTerm(e.target.value)} + onChange={(e) => handleSearch(e.target.value)} + value={searchTerm} aria-label="Search commits" />
@@ -239,43 +263,57 @@ export const CommitHistoryTab = ({
-
- {Object.keys(groupedCommits).length > 0 ? ( - <> - {Object.entries(groupedCommits).map(([date, dateCommits]) => ( - + +
+ ) : ( +
+ {Object.keys(groupedCommits).length > 0 ? ( + <> + {Object.entries(groupedCommits).map(([date, dateCommits]) => ( + + ))} + + ) : ( +
+
+ )} - {hasMoreCommits && ( -
- -
- )} -
+ {hasMore && ( +
+ +
+ )} + + )} ); };