PIT: general improvements and fixes

This commit is contained in:
carlosmonastyrski
2025-05-25 11:00:06 -03:00
parent b7b90aea33
commit 14362dbe6a
18 changed files with 390 additions and 139 deletions
@@ -3,13 +3,15 @@ import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory";
import { ProjectType, TableName } from "../schemas";
import { getMigrationEnvConfig } from "./utils/env-config";
import { getMigrationPITServices } from "./utils/services";
export async function up(knex: Knex): Promise<void> {
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
if (hasFolderCommitTable) {
const keyStore = inMemoryKeyStore();
const { folderCommitService } = await getMigrationPITServices({ db: knex, keyStore });
const envConfig = getMigrationEnvConfig();
const { folderCommitService } = await getMigrationPITServices({ db: knex, keyStore, envConfig });
const projects = await knex(TableName.Project).where({ version: 3, type: ProjectType.SecretManager }).select("id");
for (const project of projects) {
// eslint-disable-next-line no-await-in-loop
+38 -2
View File
@@ -64,7 +64,15 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }
return { kmsService };
};
export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyStore: TKeyStoreFactory }) => {
export const getMigrationPITServices = async ({
db,
keyStore,
envConfig
}: {
db: Knex;
keyStore: TKeyStoreFactory;
envConfig: TMigrationEnvConfig;
}) => {
const projectDAL = projectDALFactory(db);
const folderCommitDAL = folderCommitDALFactory(db);
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
@@ -79,6 +87,33 @@ export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyS
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
const orgDAL = orgDALFactory(db);
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
const kmsDAL = kmskeyDALFactory(db);
const internalKmsDAL = internalKmsDALFactory(db);
const hsmModule = initializeHsmModule(envConfig);
hsmModule.initialize();
const hsmService = hsmServiceFactory({
hsmModule: hsmModule.getModule(),
envConfig
});
const kmsService = kmsServiceFactory({
kmsRootConfigDAL,
keyStore,
kmsDAL,
internalKmsDAL,
orgDAL,
projectDAL,
hsmService,
envConfig
});
await hsmService.startService();
await kmsService.startService();
const folderCommitService = folderCommitServiceFactory({
folderCommitDAL,
folderCommitChangesDAL,
@@ -92,7 +127,8 @@ export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyS
projectDAL,
folderCheckpointResourcesDAL,
secretV2BridgeDAL,
folderTreeCheckpointResourcesDAL
folderTreeCheckpointResourcesDAL,
kmsService
});
return { folderCommitService };
+1 -1
View File
@@ -267,7 +267,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
body: z.object({
folderId: z.string().trim(),
deepRollback: z.boolean().default(false),
message: z.string().trim().optional(),
message: z.string().max(256).trim().optional(),
envId: z.string().trim(),
projectId: z.string().trim()
}),
+12 -3
View File
@@ -12,6 +12,7 @@ import {
isSecretCommitChange
} from "@app/services/folder-commit-changes/folder-commit-changes-dal";
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -21,6 +22,7 @@ type TPitServiceFactoryDep = {
secretService: Pick<TSecretServiceFactory, "getSecretVersionsV2ByIds" | "getChangeVersions">;
folderService: Pick<TSecretFolderServiceFactory, "getFolderById" | "getFolderVersions">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">;
};
export type TPitServiceFactory = ReturnType<typeof pitServiceFactory>;
@@ -29,7 +31,8 @@ export const pitServiceFactory = ({
folderCommitService,
secretService,
folderService,
permissionService
permissionService,
folderDAL
}: TPitServiceFactoryDep) => {
const getCommitsCount = async ({
actor,
@@ -134,6 +137,8 @@ export const pitServiceFactory = ({
commitId
});
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(projectId, [changes.folderId]);
for (const change of changes.changes) {
if (isSecretCommitChange(change)) {
change.versions = await secretService.getChangeVersions(
@@ -149,7 +154,9 @@ export const pitServiceFactory = ({
actor,
actorOrgId,
actorAuthMethod,
change.folderId
changes.envId,
projectId,
folderWithPath?.path || ""
);
} else if (isFolderCommitChange(change)) {
change.versions = await folderService.getFolderVersions(
@@ -261,7 +268,9 @@ export const pitServiceFactory = ({
actor,
actorOrgId,
actorAuthMethod,
diff.folderId
envId,
projectId,
diff.folderPath || ""
);
}
} else if (change.type === ResourceType.FOLDER) {
+4 -2
View File
@@ -598,7 +598,8 @@ export const registerRoutes = async (
secretV2BridgeDAL,
folderTreeCheckpointResourcesDAL,
folderCommitQueueService,
permissionService
permissionService,
kmsService
});
const scimService = scimServiceFactory({
licenseService,
@@ -1525,7 +1526,8 @@ export const registerRoutes = async (
folderCommitService,
secretService,
folderService,
permissionService
permissionService,
folderDAL
});
const identityOidcAuthService = identityOidcAuthServiceFactory({
@@ -52,6 +52,7 @@ export type FolderCommitChange = BaseCommitChangeInfo & {
folderVersion: string;
folderChangeId: string;
versions?: {
version: string;
name?: string;
}[];
};
@@ -36,6 +36,7 @@ const secretVersionSchema = z.object({
// Folder-specific versions schema
const folderVersionSchema = z.object({
version: z.string().optional(),
name: z.string().optional()
});
@@ -4,7 +4,7 @@
import { Knex } from "knex";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
import { ProjectType, TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ActorType } from "../auth/auth-type";
@@ -53,6 +53,10 @@ describe("folderCommitServiceFactory", () => {
findLatestCommitByFolderIds: vi.fn().mockResolvedValue({})
};
const mockKmsService = {
createCipherPairWithDataKey: vi.fn().mockResolvedValue({})
};
const mockFolderCommitChangesDAL = {
create: vi.fn().mockResolvedValue({}),
findByCommitId: vi.fn().mockResolvedValue([]),
@@ -136,7 +140,8 @@ describe("folderCommitServiceFactory", () => {
};
const mockProjectDAL = {
findById: vi.fn().mockResolvedValue({})
findById: vi.fn().mockResolvedValue({}),
findProjectByEnvId: vi.fn().mockResolvedValue({})
};
const mockFolderCommitQueueService = {
@@ -181,7 +186,8 @@ describe("folderCommitServiceFactory", () => {
secretV2BridgeDAL: mockSecretV2BridgeDAL,
folderCommitQueueService: mockFolderCommitQueueService,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
permissionService: mockPermissionService
permissionService: mockPermissionService,
kmsService: mockKmsService
});
});
@@ -408,6 +414,11 @@ describe("folderCommitServiceFactory", () => {
{ folderVersionId: "folder-version-1", referencedFolderId: "folder-1" }
]);
mockFolderCommitDAL.findCommitsToRecreate.mockResolvedValue([]);
mockProjectDAL.findProjectByEnvId.mockResolvedValue({
id: "project-id",
name: "test-project",
type: ProjectType.SecretManager
});
// Act
const result = await folderCommitService.compareFolderStates({
@@ -17,6 +17,8 @@ import { TFolderCommitChangesDALFactory } from "../folder-commit-changes/folder-
import { TFolderTreeCheckpointDALFactory } from "../folder-tree-checkpoint/folder-tree-checkpoint-dal";
import { TFolderTreeCheckpointResourcesDALFactory } from "../folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
@@ -80,7 +82,7 @@ type BaseChange = {
fromVersion?: string;
};
type SecretChange = {
type SecretChange = BaseChange & {
type: ResourceType.SECRET;
secretKey?: string;
secretVersion?: string;
@@ -98,7 +100,7 @@ type SecretChange = {
}[];
};
type FolderChange = {
type FolderChange = BaseChange & {
type: ResourceType.FOLDER;
folderName?: string;
folderVersion?: string;
@@ -107,7 +109,7 @@ type FolderChange = {
}[];
};
export type ResourceChange = BaseChange & (SecretChange | FolderChange);
export type ResourceChange = SecretChange | FolderChange;
type ActorInfo = {
actorType: string;
@@ -134,12 +136,13 @@ type TFolderCommitServiceFactoryDep = {
folderVersionDAL: TSecretFolderVersionDALFactory;
secretVersionV2BridgeDAL: TSecretVersionV2DALFactory;
secretV2BridgeDAL: secretV2BridgeDal.TSecretV2BridgeDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectByEnvId">;
folderCommitQueueService?: Pick<
TFolderCommitQueueServiceFactory,
"scheduleTreeCheckpoint" | "createFolderTreeCheckpoint"
>;
permissionService?: TPermissionServiceFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export const folderCommitServiceFactory = ({
@@ -157,7 +160,8 @@ export const folderCommitServiceFactory = ({
secretV2BridgeDAL,
folderTreeCheckpointResourcesDAL,
folderCommitQueueService,
permissionService
permissionService,
kmsService
}: TFolderCommitServiceFactoryDep) => {
const appCfg = getConfig();
@@ -394,6 +398,12 @@ export const folderCommitServiceFactory = ({
throw new NotFoundError({ message: `Commit with ID ${targetCommitId} not found` });
}
const project = await projectDAL.findProjectByEnvId(targetCommit.envId, tx);
if (!project) {
throw new NotFoundError({ message: `No project found for envId ${targetCommit.envId}` });
}
// If currentCommitId is not provided, mark all resources in target as creates
if (!currentCommitId) {
const targetState = await reconstructFolderState(targetCommitId, tx);
@@ -565,7 +575,63 @@ export const folderCommitServiceFactory = ({
}
});
return differences;
const removeNoChangeUpdate: string[] = [];
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: project.id
});
await Promise.all(
differences.map(async (change) => {
if (change.changeType === ChangeType.UPDATE) {
if (change.type === ResourceType.FOLDER && change.folderVersion && change.fromVersion) {
const versions = await folderVersionDAL.find({
folderId: change.id,
$in: {
version: [Number(change.folderVersion), Number(change.fromVersion)]
}
});
const versionsShaped = [...new Set(versions.map((version) => version.name))];
if (versionsShaped.length === 1) {
removeNoChangeUpdate.push(change.id);
}
} else if (change.type === ResourceType.SECRET && change.secretVersion && change.fromVersion) {
const versions = await secretVersionV2BridgeDAL.findVersionsBySecretIdWithActors({
secretId: change.id,
projectId: project.id,
secretVersions: [change.secretVersion, change.fromVersion]
});
const versionsShaped = versions.map((el) => ({
secretKey: el.key,
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: "",
skipMultilineEncoding: el.skipMultilineEncoding,
secretReminderRepeatDays: el.reminderRepeatDays,
tags: el.tags,
metadata: el.metadata,
secretReminderNote: el.reminderNote,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: ""
}));
const uniqueVersions = versionsShaped.filter(
(item, index, arr) =>
arr.findIndex((other) =>
Object.entries(item).every(
([key, value]) => JSON.stringify(value) === JSON.stringify(other[key as keyof typeof other])
)
) === index
);
if (uniqueVersions.length === 1) {
removeNoChangeUpdate.push(change.id);
}
}
}
})
);
return differences.filter((change) => !removeNoChangeUpdate.includes(change.id));
};
/**
@@ -1375,6 +1441,63 @@ export const folderCommitServiceFactory = ({
);
};
const addNestedFolderChanges = async ({
changes,
beforeCommit,
folderId,
folderName,
folderPath,
step = 1,
tx
}: {
changes: {
folderId: string;
folderName: string;
changes: ResourceChange[];
folderPath?: string;
}[];
beforeCommit: bigint;
folderId: string;
folderName?: string;
folderPath?: string;
step?: number;
tx?: Knex;
}) => {
if (step > 20) {
return;
}
const latestFolderCommit = await folderCommitDAL.findCommitBefore(folderId, beforeCommit, tx);
if (!latestFolderCommit) {
return;
}
const diff = await compareFolderStates({
targetCommitId: latestFolderCommit.id,
tx
});
changes.push({
folderId,
folderName: folderName || "",
changes: diff,
folderPath: folderPath || ""
});
await Promise.all(
diff.map(async (change) => {
if (change.type === ResourceType.FOLDER && change.changeType === ChangeType.CREATE) {
await addNestedFolderChanges({
changes,
beforeCommit,
folderId: change.id,
folderName: change.folderName,
folderPath: `${folderPath}/${change.folderName}`,
step: step + 1,
tx
});
}
})
);
};
const deepCompareFolder = async ({
targetCommitId,
envId,
@@ -1463,7 +1586,12 @@ export const folderCommitServiceFactory = ({
const folders = await folderDAL.findFoldersByRootAndIds({ rootId: targetCommit.folderId, folderIds }, tx);
const sortedFolders = sortFoldersByHierarchy(folders);
const response = [];
const response: {
folderId: string;
folderName: string;
changes: ResourceChange[];
folderPath?: string;
}[] = [];
for (const folder of sortedFolders) {
const diff = folderDiffs.get(folder.id);
if (diff) {
@@ -1474,6 +1602,29 @@ export const folderCommitServiceFactory = ({
changes: diff,
folderPath: folderPath?.[0]?.path
});
const recreatedFolders = diff
.filter(
(change): change is FolderChange =>
change.type === ResourceType.FOLDER && change.changeType === ChangeType.CREATE
)
.map((change) => ({
id: change.id,
folderName: change.folderName,
folderPath: folderPath?.[0]?.path
}));
await Promise.all(
recreatedFolders.map(async (change) => {
const nestedFolderPath = folderPath?.[0]?.path;
await addNestedFolderChanges({
changes: response,
beforeCommit: targetCommit.commitId,
folderId: change.id,
folderName: change.folderName,
folderPath: `${nestedFolderPath !== "/" ? nestedFolderPath : ""}/${change.folderName}`,
tx
});
})
);
}
}
return response;
+13 -2
View File
@@ -12,7 +12,7 @@ import {
TProjectsUpdate
} from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { ActorType } from "../auth/auth-type";
import { Filter, ProjectFilterType, SearchProjectSortBy } from "./project-types";
@@ -425,6 +425,16 @@ export const projectDALFactory = (db: TDbClient) => {
return { docs, totalCount: Number(docs?.[0]?.count ?? 0) };
};
const findProjectByEnvId = async (envId: string, tx?: Knex) => {
const project = await (tx || db.replicaNode())(TableName.Project)
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ id: envId }, TableName.Environment))
.select(selectAllTableCols(TableName.Project))
.first();
return project;
};
return {
...projectOrm,
findUserProjects,
@@ -437,6 +447,7 @@ export const projectDALFactory = (db: TDbClient) => {
findProjectWithOrg,
checkProjectUpgradeStatus,
getProjectFromSplitId,
searchProjects
searchProjects,
findProjectByEnvId
};
};
@@ -800,6 +800,7 @@ export const secretFolderServiceFactory = ({
change.isUpdate || change.changeType === ChangeType.UPDATE ? [currentVersion, fromVersion] : [currentVersion]
});
return versions.map((v) => ({
version: v.version?.toString() || "1",
name: v.name
}));
};
@@ -2875,24 +2875,21 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
secretId,
secretVersionNumbers,
folderId
secretPath,
envId,
projectId
}: TGetSecretVersionsDTO & {
secretVersionNumbers: string[];
folderId: string;
secretPath: string;
envId: string;
projectId: string;
}) => {
const folder = await folderDAL.findById(folderId);
if (!folder) throw new NotFoundError({ message: `Folder with ID '${folderId}' not found` });
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
if (!folderWithPath) {
throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` });
}
const environment = await projectEnvDAL.findOne({ id: envId });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: folder.projectId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
@@ -2906,11 +2903,11 @@ export const secretV2BridgeServiceFactory = ({
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: folder.projectId
projectId
});
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
secretId,
projectId: folder.projectId,
projectId,
secretVersions: secretVersionNumbers
});
return secretVersions.map((el) => {
@@ -2918,8 +2915,8 @@ export const secretV2BridgeServiceFactory = ({
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment: folder.environment.envSlug,
secretPath: folderWithPath.path,
environment: environment.slug,
secretPath,
secretName: el.key,
...(el.tags?.length && {
secretTags: el.tags.map((tag) => tag.slug)
@@ -2928,9 +2925,9 @@ export const secretV2BridgeServiceFactory = ({
);
return reshapeBridgeSecret(
folder.projectId,
folder.environment.envSlug,
folderWithPath.path,
projectId,
environment.slug,
secretPath,
{
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
+16 -6
View File
@@ -2529,10 +2529,14 @@ export const secretServiceFactory = ({
actorAuthMethod,
secretId,
secretVersions,
folderId
secretPath,
envId,
projectId
}: TGetSecretVersionsDTO & {
secretVersions: string[];
folderId: string;
secretPath: string;
envId: string;
projectId: string;
}) => {
const secretVersionV2 = await secretV2BridgeService.getSecretVersionsByIds({
actorId,
@@ -2540,8 +2544,10 @@ export const secretServiceFactory = ({
actorOrgId,
actorAuthMethod,
secretId,
folderId,
secretVersionNumbers: secretVersions
secretVersionNumbers: secretVersions,
secretPath,
envId,
projectId
});
return secretVersionV2;
};
@@ -3317,7 +3323,9 @@ export const secretServiceFactory = ({
actor: ActorType,
actorOrgId: string,
actorAuthMethod: ActorAuthMethod,
folderId: string
envId: string,
projectId: string,
secretPath: string
) => {
const currentVersion = change.secretVersion;
const secretId = change.secretId ? change.secretId : change.id;
@@ -3335,7 +3343,9 @@ export const secretServiceFactory = ({
change.isUpdate || change.changeType === ChangeType.UPDATE
? [currentVersion, previousVersion]
: [currentVersion],
folderId
secretPath,
envId,
projectId
});
return versions?.map((v) => ({
secretKey: v.secretKey,
@@ -1,15 +1,17 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { ROUTE_PATHS } from "@app/const/routes";
import { useWorkspace } from "@app/context";
import {
ProjectPermissionCommitsActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { CommitDetailsTab } from "./components/CommitDetailsTab";
export const CommitDetailsPage = () => {
const { t } = useTranslation();
const envSlug = useParams({
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id,
select: (el) => el.environment
@@ -64,10 +66,12 @@ export const CommitDetailsPage = () => {
return (
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pb-4 pt-2 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Commit History" })}</title>
</Helmet>
<div className="w-full max-w-[75vw]">
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionCommitsActions.Read}
a={ProjectPermissionSub.Commits}
>
<CommitDetailsTab
selectedCommitId={selectedCommitId}
workspaceId={currentWorkspace.id}
@@ -75,6 +79,7 @@ export const CommitDetailsPage = () => {
envSlug={envSlug}
goToRollbackPreview={handleGoToRollbackPreview}
/>
</ProjectPermissionCan>
</div>
</div>
);
@@ -243,10 +243,10 @@ export const CommitDetailsTab = ({
<div className="w-full">
<div>
<div className="flex justify-between pb-2">
<div>
<div className="w-5/6">
<div>
<div className="flex items-center">
<h1 className="mr-4 text-3xl font-semibold text-white">
<h1 className="mr-4 truncate text-3xl font-semibold text-white">
{parsedCommitDetails.changes?.message || "No message"}
</h1>
</div>
@@ -266,7 +266,7 @@ export const CommitDetailsTab = ({
</div>
<div className="flex items-center justify-start">
<ProjectPermissionCan
I={ProjectPermissionCommitsActions.Read}
I={ProjectPermissionCommitsActions.PerformRollback}
a={ProjectPermissionSub.Commits}
>
{(isAllowed) => (
@@ -6,6 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
@@ -17,6 +18,10 @@ import {
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useWorkspace } from "@app/context";
import {
ProjectPermissionCommitsActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks";
import { useCommitRollback, useGetRollbackPreview } from "@app/hooks/api/folderCommits/queries";
import { ProjectType } from "@app/hooks/api/workspace/types";
@@ -294,6 +299,11 @@ export const RollbackPreviewTab = (): JSX.Element => {
return (
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pb-4 pt-2 text-white">
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionCommitsActions.PerformRollback}
a={ProjectPermissionSub.Commits}
>
<div className="w-full max-w-[75vw]">
<div className="h-full w-full">
<div>
@@ -333,6 +343,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full border-mineshaft-500 bg-mineshaft-700 py-2 text-sm"
maxLength={256}
/>
<Button
onClick={() => {
@@ -372,6 +383,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
/>
</div>{" "}
</div>{" "}
</ProjectPermissionCan>
</div>
);
};
@@ -505,7 +505,6 @@ export const SecretVersionDiffView = ({
}
const sortedVersions = [...item.versions].sort((a, b) => b.version - a.version);
let oldVersion = null;
let newVersion = null;
let oldVersionContent = null;
@@ -562,13 +561,11 @@ export const SecretVersionDiffView = ({
} else if (item.isAdded) {
[newVersion] = sortedVersions;
const cleanNewVersion = cleanVersionForComparison(newVersion);
oldVersionContent = <div className="italic text-gray-400">No previous version</div>;
newVersionContent = formatAddedJson(cleanNewVersion);
} else if (item.isDeleted) {
[oldVersion] = sortedVersions;
const cleanOldVersion = cleanVersionForComparison(oldVersion);
oldVersionContent = formatDeletedJson(cleanOldVersion);
newVersionContent = <div className="italic text-gray-400">Version deleted</div>;
} else {
return null;
}
@@ -1,16 +1,18 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useWorkspace } from "@app/context";
import {
ProjectPermissionCommitsActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { CommitHistoryTab } from "./components/CommitHistoryTab";
export const CommitsPage = () => {
const { t } = useTranslation();
const envSlug = useParams({
from: ROUTE_PATHS.SecretManager.CommitsPage.id,
select: (el) => el.environment
@@ -45,20 +47,23 @@ export const CommitsPage = () => {
return (
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Commit History" })}</title>
</Helmet>
<div className="w-full max-w-[75vw]">
<PageHeader
title="Commits"
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
/>
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionCommitsActions.Read}
a={ProjectPermissionSub.Commits}
>
<CommitHistoryTab
onSelectCommit={handleSelectCommit}
projectId={currentWorkspace.id}
environment={envSlug}
secretPath={secretPath}
/>
</ProjectPermissionCan>
</div>
</div>
);