mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 22:27:22 +00:00
Type fixes and PIT history pagination
This commit is contained in:
@@ -84,9 +84,10 @@ const getZodDefaultValue = (type: unknown, value: string | number | boolean | Ob
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const bigIntegerColumns = {
|
const bigIntegerColumns: Record<string, string[]> = {
|
||||||
"folder_commits": ["commitId"]
|
"folder_commits": ["commitId"]
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const tables = (
|
const tables = (
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { removeTrailingSlash } from "@app/lib/fn";
|
|||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { booleanSchema } from "@app/server/routes/sanitizedSchemas";
|
import { booleanSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { ChangeType, ResourceChange } from "@app/services/folder-commit/folder-commit-service";
|
import { ChangeType } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { commitChangesResponseSchema } from "@app/services/folder-commit/folder-commit-types";
|
import { commitChangesResponseSchema } from "@app/services/folder-commit/folder-commit-types";
|
||||||
|
|
||||||
const commitHistoryItemSchema = z.object({
|
const commitHistoryItemSchema = z.object({
|
||||||
@@ -98,22 +98,34 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
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: {
|
response: {
|
||||||
200: commitHistoryItemSchema.array()
|
200: z.object({
|
||||||
|
commits: commitHistoryItemSchema.array(),
|
||||||
|
total: z.number(),
|
||||||
|
hasMore: z.boolean()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const commits = await server.services.folderCommit.getCommitsForFolder({
|
const result = await server.services.folderCommit.getCommitsForFolder({
|
||||||
actor: req.permission?.type,
|
actor: req.permission?.type,
|
||||||
actorId: req.permission?.id,
|
actorId: req.permission?.id,
|
||||||
actorOrgId: req.permission?.orgId,
|
actorOrgId: req.permission?.orgId,
|
||||||
actorAuthMethod: req.permission?.authMethod,
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.workspaceId,
|
||||||
environment: req.query.environment,
|
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({
|
await server.services.auditLog.createAuditLog({
|
||||||
@@ -124,30 +136,47 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
environment: req.query.environment,
|
environment: req.query.environment,
|
||||||
path: req.query.path,
|
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) => ({
|
return {
|
||||||
...commit,
|
commits: result.commits.map((commit) => ({
|
||||||
commitId: commit.commitId.toString()
|
...commit,
|
||||||
}));
|
commitId: commit.commitId.toString()
|
||||||
|
})),
|
||||||
|
total: result.total,
|
||||||
|
hasMore: result.hasMore
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const getChangeVersions = async (
|
const getChangeVersions = async (
|
||||||
change: ResourceChange,
|
change: {
|
||||||
|
secretVersion?: string;
|
||||||
|
secretId?: string;
|
||||||
|
id?: string;
|
||||||
|
isUpdate?: boolean;
|
||||||
|
changeType?: string;
|
||||||
|
},
|
||||||
previousVersion: string,
|
previousVersion: string,
|
||||||
actorId: string,
|
actorId: string,
|
||||||
actor: string,
|
actor: ActorType,
|
||||||
actorOrgId: string,
|
actorOrgId: string,
|
||||||
actorAuthMethod: string,
|
actorAuthMethod: ActorAuthMethod,
|
||||||
folderId: string
|
folderId: string
|
||||||
) => {
|
) => {
|
||||||
if (change.secretVersion) {
|
if (change.secretVersion) {
|
||||||
const currentVersion = change.secretVersion || "1";
|
const currentVersion = change.secretVersion || "1";
|
||||||
const secretId = change.secretId ? change.secretId : change.id;
|
const secretId = change.secretId ? change.secretId : change.id;
|
||||||
|
if (!secretId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const versions = await server.services.secret.getSecretVersionsV2ByIds({
|
const versions = await server.services.secret.getSecretVersionsV2ByIds({
|
||||||
actorId,
|
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";
|
const currentVersion = change.folderVersion || "1";
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const versions = await server.services.folder.getFolderVersionsByIds({
|
const versions = await server.services.folder.getFolderVersionsByIds({
|
||||||
@@ -218,20 +255,18 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
commitId: req.params.commitId
|
commitId: req.params.commitId
|
||||||
});
|
});
|
||||||
for (const change of changes.changes) {
|
for (const change of changes.changes) {
|
||||||
if (change.secretVersionId) {
|
if (change.secretVersionId && change.secretVersion) {
|
||||||
change.objectType = "secret";
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
change.versions = await getChangeVersions(
|
change.versions = await getChangeVersions(
|
||||||
change,
|
change,
|
||||||
(Number.parseInt(change.secretVersion, 10) - 1).toString(),
|
(Number.parseInt(change.secretVersion, 10) - 1).toString(),
|
||||||
req.permission?.id,
|
req.permission.id,
|
||||||
req.permission?.type,
|
req.permission.type,
|
||||||
req.permission?.orgId,
|
req.permission.orgId,
|
||||||
req.permission?.authMethod,
|
req.permission.authMethod,
|
||||||
change.folderId
|
change.folderId
|
||||||
);
|
);
|
||||||
} else if (change.folderVersionId && change.folderChangeId) {
|
} else if (change.folderVersionId && change.folderChangeId && change.folderVersion) {
|
||||||
change.objectType = "folder";
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
change.versions = await getFolderVersions(
|
change.versions = await getFolderVersions(
|
||||||
change,
|
change,
|
||||||
@@ -340,10 +375,10 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
change.versions = await getChangeVersions(
|
change.versions = await getChangeVersions(
|
||||||
change,
|
change,
|
||||||
change.fromVersion || "1",
|
change.fromVersion || "1",
|
||||||
req.permission?.id,
|
req.permission.id,
|
||||||
req.permission?.type,
|
req.permission.type,
|
||||||
req.permission?.orgId,
|
req.permission.orgId,
|
||||||
req.permission?.authMethod,
|
req.permission.authMethod,
|
||||||
diff.folderId
|
diff.folderId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2927,6 +2927,10 @@ interface GetProjectPitCommitsEvent {
|
|||||||
commitCount: string;
|
commitCount: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
offset: string;
|
||||||
|
limit: string;
|
||||||
|
search?: string;
|
||||||
|
sort: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const folderCheckpointDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.FolderCommit}.id`
|
`${TableName.FolderCommit}.id`
|
||||||
)
|
)
|
||||||
.where(`${TableName.FolderCommit}.folderId`, "=", folderId)
|
.where(`${TableName.FolderCommit}.folderId`, "=", folderId)
|
||||||
.where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId)
|
.where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId.toString())
|
||||||
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||||
.select(
|
.select(
|
||||||
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
|||||||
|
|
||||||
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
|
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
|
||||||
|
|
||||||
type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
export type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
||||||
actorMetadata: unknown;
|
actorMetadata: unknown;
|
||||||
actorType: string;
|
actorType: string;
|
||||||
message?: string | null;
|
message?: string | null;
|
||||||
@@ -25,6 +25,7 @@ type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
|||||||
secretVersion?: string;
|
secretVersion?: string;
|
||||||
secretId?: string;
|
secretId?: string;
|
||||||
folderChangeId?: string;
|
folderChangeId?: string;
|
||||||
|
objectType?: string;
|
||||||
versions?: {
|
versions?: {
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
secretComment?: string;
|
secretComment?: string;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
TSecretFolderVersions,
|
TSecretFolderVersions,
|
||||||
TSecretVersionsV2
|
TSecretVersionsV2
|
||||||
} from "@app/db/schemas";
|
} 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";
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TFolderCommitDALFactory = ReturnType<typeof folderCommitDALFactory>;
|
export type TFolderCommitDALFactory = ReturnType<typeof folderCommitDALFactory>;
|
||||||
@@ -209,8 +209,8 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
|||||||
const commits = await (tx || db.replicaNode())(TableName.FolderCommit)
|
const commits = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
|
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
|
||||||
.andWhere(`${TableName.FolderCommit}.commitId`, ">", checkpointCommitNumber)
|
.andWhere(`${TableName.FolderCommit}.commitId`, ">", checkpointCommitNumber.toString())
|
||||||
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", targetCommitNumber)
|
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", targetCommitNumber.toString())
|
||||||
.select(selectAllTableCols(TableName.FolderCommit))
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
.orderBy(`${TableName.FolderCommit}.commitId`, "asc");
|
.orderBy(`${TableName.FolderCommit}.commitId`, "asc");
|
||||||
|
|
||||||
@@ -391,12 +391,89 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
|||||||
.select(selectAllTableCols(TableName.FolderCommit))
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
.orderBy("commitId", "desc")
|
.orderBy("commitId", "desc")
|
||||||
.first();
|
.first();
|
||||||
|
if (!doc) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder commit not found for ID ${id}`
|
||||||
|
});
|
||||||
|
}
|
||||||
return doc;
|
return doc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindById" });
|
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<string, TFolderCommitChanges[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
...restOfOrm,
|
...restOfOrm,
|
||||||
findByFolderId,
|
findByFolderId,
|
||||||
@@ -411,6 +488,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
|||||||
findLatestCommitByFolderIds,
|
findLatestCommitByFolderIds,
|
||||||
findAllFolderCommitsAfter,
|
findAllFolderCommitsAfter,
|
||||||
findPreviousCommitTo,
|
findPreviousCommitTo,
|
||||||
findById
|
findById,
|
||||||
|
findByFolderIdPaginated
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ describe("folderCommitServiceFactory", () => {
|
|||||||
deleteById: vi.fn().mockResolvedValue({}),
|
deleteById: vi.fn().mockResolvedValue({}),
|
||||||
create: vi.fn().mockResolvedValue({}),
|
create: vi.fn().mockResolvedValue({}),
|
||||||
updateById: 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({})
|
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -260,6 +260,17 @@ describe("folderCommitServiceFactory", () => {
|
|||||||
mockFolderDAL.findByParentId.mockResolvedValue([]);
|
mockFolderDAL.findByParentId.mockResolvedValue([]);
|
||||||
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.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 = {
|
const data = {
|
||||||
actor: {
|
actor: {
|
||||||
type: ActorType.IDENTITY,
|
type: ActorType.IDENTITY,
|
||||||
@@ -449,6 +460,12 @@ describe("folderCommitServiceFactory", () => {
|
|||||||
const actorType = ActorType.USER;
|
const actorType = ActorType.USER;
|
||||||
const projectId = "project-id";
|
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);
|
mockFolderCommitDAL.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
@@ -501,8 +518,8 @@ describe("folderCommitServiceFactory", () => {
|
|||||||
const actorType = ActorType.USER;
|
const actorType = ActorType.USER;
|
||||||
|
|
||||||
const differences = [
|
const differences = [
|
||||||
{ type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, 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: 1 }
|
{ type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: BigInt(1) }
|
||||||
];
|
];
|
||||||
|
|
||||||
const secretVersions = {
|
const secretVersions = {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export type ResourceChange = {
|
|||||||
commitId: bigint;
|
commitId: bigint;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
|
isUpdate?: boolean;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
secretVersion?: string;
|
secretVersion?: string;
|
||||||
secretId?: string;
|
secretId?: string;
|
||||||
@@ -997,7 +998,11 @@ export const folderCommitServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
projectId,
|
projectId,
|
||||||
environment,
|
environment,
|
||||||
path
|
path,
|
||||||
|
offset = 0,
|
||||||
|
limit = 20,
|
||||||
|
search,
|
||||||
|
sort = "desc"
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
@@ -1006,6 +1011,10 @@ export const folderCommitServiceFactory = ({
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
search?: string;
|
||||||
|
sort: "asc" | "desc";
|
||||||
}) => {
|
}) => {
|
||||||
await checkProjectPermission({
|
await checkProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
@@ -1020,7 +1029,12 @@ export const folderCommitServiceFactory = ({
|
|||||||
message: `Folder not found for project ID ${projectId}, environment ${environment}, path ${path}`
|
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;
|
return folderCommits;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1516,6 +1530,9 @@ export const folderCommitServiceFactory = ({
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (!permissionService) {
|
||||||
|
throw new Error("Permission service not initialized");
|
||||||
|
}
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -1551,8 +1568,11 @@ export const folderCommitServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort commits by commitId (which appears to be numeric)
|
// 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
|
// Find the index of the commit to revert
|
||||||
const commitIndex = sortedCommits.findIndex((c) => c.id === commitId);
|
const commitIndex = sortedCommits.findIndex((c) => c.id === commitId);
|
||||||
if (commitIndex === -1) {
|
if (commitIndex === -1) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const secretVersionSchema = z.object({
|
const secretVersionSchema = z.object({
|
||||||
secretKey: z.string(),
|
secretKey: z.string().optional().nullable(),
|
||||||
secretComment: z.string().optional().nullable(),
|
secretComment: z.string().optional().nullable(),
|
||||||
skipMultilineEncoding: z.boolean().optional().nullable(),
|
skipMultilineEncoding: z.boolean().optional().nullable(),
|
||||||
secretReminderRepeatDays: z.number().optional().nullable(),
|
secretReminderRepeatDays: z.number().optional().nullable(),
|
||||||
@@ -13,7 +13,7 @@ const secretVersionSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const folderVersionSchema = z.object({
|
const folderVersionSchema = z.object({
|
||||||
name: z.string()
|
name: z.string().optional().nullable()
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseChangeSchema = z.object({
|
const baseChangeSchema = z.object({
|
||||||
@@ -37,46 +37,37 @@ const baseChangeSchema = z.object({
|
|||||||
folderId: z.string().optional()
|
folderId: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretChangeSchema = baseChangeSchema.extend({
|
const commitChangeSchema = baseChangeSchema.extend({
|
||||||
objectType: z.literal("secret"),
|
secretVersionId: z.string().optional().nullable(),
|
||||||
secretVersionId: z.string(),
|
folderVersionId: z.string().optional().nullable(),
|
||||||
folderVersionId: z.null(),
|
folderName: z.string().optional().nullable(),
|
||||||
folderName: z.null(),
|
folderChangeId: z.string().optional().nullable(),
|
||||||
folderChangeId: z.null(),
|
secretKey: z.string().optional().nullable(),
|
||||||
secretKey: z.string(),
|
secretVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||||
secretVersion: z.union([z.string(), z.number()]),
|
secretId: z.string().optional().nullable(),
|
||||||
secretId: z.string(),
|
folderVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||||
versions: z.array(secretVersionSchema).optional()
|
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({
|
const commitSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
commitId: z.string(),
|
commitId: z.string(),
|
||||||
actorMetadata: z.object({
|
actorMetadata: z
|
||||||
id: z.string(),
|
.union([
|
||||||
name: z.string()
|
z.object({
|
||||||
}),
|
id: z.string().optional(),
|
||||||
|
name: z.string().optional()
|
||||||
|
}),
|
||||||
|
z.unknown()
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
actorType: z.string(),
|
actorType: z.string(),
|
||||||
message: z.string().nullable(),
|
message: z.string().nullable().optional(),
|
||||||
folderId: z.string(),
|
folderId: z.string(),
|
||||||
envId: z.string(),
|
envId: z.string(),
|
||||||
createdAt: z.union([z.string(), z.date()]),
|
createdAt: z.union([z.string(), z.date()]),
|
||||||
updatedAt: 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({
|
export const commitChangesResponseSchema = z.object({
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
.where(`${TableName.FolderCommit}.envId`, "=", envId)
|
.where(`${TableName.FolderCommit}.envId`, "=", envId)
|
||||||
.where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId)
|
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId.toString())
|
||||||
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
||||||
.select(db.ref("commitId").withSchema(TableName.FolderCommit))
|
.select(db.ref("commitId").withSchema(TableName.FolderCommit))
|
||||||
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
|
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
|
||||||
|
|||||||
@@ -145,7 +145,9 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
// Create lookup map for max versions
|
// Create lookup map for max versions
|
||||||
const maxVersionMap = maxVersionsQuery.reduce<Record<string, number>>((acc, item) => {
|
const maxVersionMap = maxVersionsQuery.reduce<Record<string, number>>((acc, item) => {
|
||||||
acc[item.folderId] = item.maxVersion;
|
if (item.maxVersion) {
|
||||||
|
acc[item.folderId] = item.maxVersion;
|
||||||
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
@@ -160,6 +162,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
|||||||
try {
|
try {
|
||||||
if (!folderIds.length && (!versionIds || !versionIds.length)) return {};
|
if (!folderIds.length && (!versionIds || !versionIds.length)) return {};
|
||||||
|
|
||||||
|
// Run both queries in parallel
|
||||||
const [latestVersions, specificVersionsWithLatest] = await Promise.all([
|
const [latestVersions, specificVersionsWithLatest] = await Promise.all([
|
||||||
folderIds.length ? getLatestFolderVersions(folderIds, tx) : [],
|
folderIds.length ? getLatestFolderVersions(folderIds, tx) : [],
|
||||||
versionIds?.length ? getSpecificFolderVersionsWithLatest(versionIds, tx) : []
|
versionIds?.length ? getSpecificFolderVersionsWithLatest(versionIds, tx) : []
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
.whereIn("secretId", specificSecretIds)
|
.whereIn("secretId", specificSecretIds)
|
||||||
.groupBy("secretId")
|
.groupBy("secretId")
|
||||||
.select("secretId")
|
.select("secretId")
|
||||||
.max("version as maxVersion");
|
.max("version", { as: "maxVersion" });
|
||||||
|
|
||||||
// Create a lookup map for max versions
|
// Create a lookup map for max versions
|
||||||
const maxVersionMap = maxVersionsQuery.reduce(
|
const maxVersionMap = maxVersionsQuery.reduce(
|
||||||
|
|||||||
@@ -68,13 +68,29 @@ const fetchFolderCommitsCount = async ({
|
|||||||
const fetchFolderCommitHistory = async (
|
const fetchFolderCommitHistory = async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
environment: string,
|
environment: string,
|
||||||
directory: string
|
directory: string,
|
||||||
): Promise<CommitHistoryItem[]> => {
|
offset: number = 0,
|
||||||
const res = await apiRequest.get<CommitHistoryItem[]>("/api/v1/pit/commits", {
|
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: {
|
params: {
|
||||||
environment,
|
environment,
|
||||||
path: directory,
|
path: directory,
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
search,
|
||||||
|
sort
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -225,15 +241,30 @@ export const useGetFolderCommitsCount = ({
|
|||||||
export const useGetFolderCommitHistory = ({
|
export const useGetFolderCommitHistory = ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
directory
|
directory,
|
||||||
|
offset = 0,
|
||||||
|
limit = 20,
|
||||||
|
search,
|
||||||
|
sort = "desc"
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
directory: string;
|
directory: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
sort?: "asc" | "desc";
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: commitKeys.history({ workspaceId, environment, directory }),
|
queryKey: [
|
||||||
queryFn: () => fetchFolderCommitHistory(workspaceId, environment, directory),
|
commitKeys.history({ workspaceId, environment, directory }),
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
search,
|
||||||
|
sort
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
fetchFolderCommitHistory(workspaceId, environment, directory, offset, limit, search, sort),
|
||||||
enabled: Boolean(workspaceId && environment)
|
enabled: Boolean(workspaceId && environment)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export enum ApprovalStatus {
|
|||||||
export enum CommitType {
|
export enum CommitType {
|
||||||
DELETE = "delete",
|
DELETE = "delete",
|
||||||
UPDATE = "update",
|
UPDATE = "update",
|
||||||
CREATE = "create"
|
CREATE = "create",
|
||||||
|
ADD = "add"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSecretApprovalSecChangeData = {
|
export type TSecretApprovalSecChangeData = {
|
||||||
|
|||||||
+4
-3
@@ -22,6 +22,7 @@ import {
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { CommitWithChanges } from "@app/hooks/api/folderCommits";
|
import { CommitWithChanges } from "@app/hooks/api/folderCommits";
|
||||||
import { useCommitRevert, useGetCommitDetails } from "@app/hooks/api/folderCommits/queries";
|
import { useCommitRevert, useGetCommitDetails } from "@app/hooks/api/folderCommits/queries";
|
||||||
|
import { CommitType } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { SecretVersionDiffView } from "../SecretVersionDiffView";
|
import { SecretVersionDiffView } from "../SecretVersionDiffView";
|
||||||
import { MergedItem } from "./types";
|
import { MergedItem } from "./types";
|
||||||
@@ -147,9 +148,9 @@ export const CommitDetailsTab = ({
|
|||||||
const commitChanges = parsedCommitDetails.changes?.changes || [];
|
const commitChanges = parsedCommitDetails.changes?.changes || [];
|
||||||
|
|
||||||
// Separate changes by type
|
// Separate changes by type
|
||||||
const addedChanges = commitChanges.filter((c) => c.changeType === "add" && !c.isUpdate);
|
const addedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && !c.isUpdate);
|
||||||
const updatedChanges = commitChanges.filter((c) => c.changeType === "add" && c.isUpdate);
|
const updatedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && c.isUpdate);
|
||||||
const deletedChanges = commitChanges.filter((c) => c.changeType === "delete");
|
const deletedChanges = commitChanges.filter((c) => c.changeType === CommitType.DELETE);
|
||||||
|
|
||||||
// Create merged item list from changes only
|
// Create merged item list from changes only
|
||||||
const changedItems: MergedItem[] = [];
|
const changedItems: MergedItem[] = [];
|
||||||
|
|||||||
+112
-74
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
faArrowDownWideShort,
|
faArrowDownWideShort,
|
||||||
faArrowUpWideShort,
|
faArrowUpWideShort,
|
||||||
@@ -144,41 +144,68 @@ export const CommitHistoryTab = ({
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
||||||
const [visibleCommits, setVisibleCommits] = useState(10);
|
const [offset, setOffset] = useState(0);
|
||||||
|
const [allCommits, setAllCommits] = useState<Commit[]>([]);
|
||||||
|
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
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,
|
workspaceId: projectId,
|
||||||
environment,
|
environment,
|
||||||
directory: secretPath
|
directory: secretPath,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
search: debouncedSearchTerm,
|
||||||
|
sort: sortDirection
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredCommits = useMemo(() => {
|
const commits = response?.commits || [];
|
||||||
if (!commits?.length) return [];
|
const hasMore = response?.hasMore || false;
|
||||||
|
|
||||||
return commits.filter(
|
// Reset accumulated commits when search or sort changes
|
||||||
(commit) =>
|
useEffect(() => {
|
||||||
commit.id?.includes(searchTerm) ||
|
setAllCommits([]);
|
||||||
commit.message?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
setOffset(0);
|
||||||
commit.actorMetadata?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
}, [debouncedSearchTerm, sortDirection]);
|
||||||
);
|
|
||||||
}, [commits, searchTerm]);
|
|
||||||
|
|
||||||
const sortedCommits = useMemo(() => {
|
// Accumulate commits instead of replacing them
|
||||||
if (!filteredCommits?.length) return [];
|
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(() => {
|
const groupedCommits = useMemo(() => {
|
||||||
return displayedCommits.reduce(
|
return allCommits.reduce(
|
||||||
(acc, commit) => {
|
(acc, commit) => {
|
||||||
const date = format(new Date(commit.createdAt), "MMM d, yyyy");
|
const date = format(new Date(commit.createdAt), "MMM d, yyyy");
|
||||||
if (!acc[date]) {
|
if (!acc[date]) {
|
||||||
@@ -189,25 +216,21 @@ export const CommitHistoryTab = ({
|
|||||||
},
|
},
|
||||||
{} as Record<string, Commit[]>
|
{} as Record<string, Commit[]>
|
||||||
);
|
);
|
||||||
}, [displayedCommits]);
|
}, [allCommits]);
|
||||||
|
|
||||||
const handleSort = useCallback(() => {
|
const handleSort = useCallback(() => {
|
||||||
setSortDirection((prev) => (prev === "desc" ? "asc" : "desc"));
|
setSortDirection((prev) => (prev === "desc" ? "asc" : "desc"));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadMoreCommits = useCallback(() => {
|
const handleSearch = useCallback((value: string) => {
|
||||||
setVisibleCommits((prev) => prev + 10);
|
setSearchTerm(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
const loadMoreCommits = useCallback(() => {
|
||||||
return (
|
if (hasMore && !isFetching) {
|
||||||
<div className="flex h-64 items-center justify-center">
|
setOffset((prev) => prev + limit);
|
||||||
<Spinner size="lg" aria-label="Loading commits" />
|
}
|
||||||
</div>
|
}, [hasMore, isFetching, limit]);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasMoreCommits = sortedCommits.length > visibleCommits;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -217,7 +240,8 @@ export const CommitHistoryTab = ({
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search commits..."
|
placeholder="Search commits..."
|
||||||
className="h-10 w-full rounded-md border-transparent bg-zinc-800 pl-9 pr-3 text-sm text-white placeholder-gray-400 focus:border-gray-600 focus:ring-primary-500/20"
|
className="h-10 w-full rounded-md border-transparent bg-zinc-800 pl-9 pr-3 text-sm text-white placeholder-gray-400 focus:border-gray-600 focus:ring-primary-500/20"
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
value={searchTerm}
|
||||||
aria-label="Search commits"
|
aria-label="Search commits"
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400">
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400">
|
||||||
@@ -239,43 +263,57 @@ export const CommitHistoryTab = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-8">
|
{isLoading && offset === 0 ? (
|
||||||
{Object.keys(groupedCommits).length > 0 ? (
|
<div className="flex h-64 items-center justify-center">
|
||||||
<>
|
<Spinner size="lg" aria-label="Loading commits" />
|
||||||
{Object.entries(groupedCommits).map(([date, dateCommits]) => (
|
</div>
|
||||||
<DateGroup
|
) : (
|
||||||
key={date}
|
<div className="space-y-8">
|
||||||
date={date}
|
{Object.keys(groupedCommits).length > 0 ? (
|
||||||
commits={dateCommits}
|
<>
|
||||||
onSelectCommit={onSelectCommit}
|
{Object.entries(groupedCommits).map(([date, dateCommits]) => (
|
||||||
|
<DateGroup
|
||||||
|
key={date}
|
||||||
|
date={date}
|
||||||
|
commits={dateCommits}
|
||||||
|
onSelectCommit={onSelectCommit}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-white-400 flex min-h-40 flex-col items-center justify-center rounded-lg bg-zinc-900 py-8 text-center">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSearch}
|
||||||
|
className="text-white-500 mb-3 text-3xl"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
))}
|
<p>No matching commits found. Try a different search term.</p>
|
||||||
</>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<div className="text-white-400 flex min-h-40 flex-col items-center justify-center rounded-lg bg-zinc-900 py-8 text-center">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSearch}
|
|
||||||
className="text-white-500 mb-3 text-3xl"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<p>No matching commits found. Try a different search term.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasMoreCommits && (
|
{hasMore && (
|
||||||
<div className="flex justify-center pb-2">
|
<div className="flex justify-center pb-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
size="md"
|
size="md"
|
||||||
className="rounded-md bg-zinc-900 px-6 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
className="rounded-md bg-zinc-900 px-6 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
onClick={loadMoreCommits}
|
onClick={loadMoreCommits}
|
||||||
aria-label="Load more commits"
|
disabled={isFetching}
|
||||||
>
|
aria-label="Load more commits"
|
||||||
Load more commits
|
>
|
||||||
</Button>
|
{isFetching ? (
|
||||||
</div>
|
<>
|
||||||
)}
|
<Spinner size="sm" className="mr-2" />
|
||||||
</div>
|
Loading...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Load more commits"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user