PIT: general improvements and fixes

This commit is contained in:
carlosmonastyrski
2025-05-25 00:12:31 -03:00
parent 28a3bf0b94
commit b7b90aea33
21 changed files with 1588 additions and 896 deletions
@@ -58,7 +58,7 @@ const fetchFolderCommitsCount = async ({
params: {
environment,
path: directory,
workspaceId
projectId: workspaceId
}
}
);
@@ -86,7 +86,7 @@ const fetchFolderCommitHistory = async (
params: {
environment,
path: directory,
workspaceId,
projectId: workspaceId,
offset,
limit,
search,
@@ -101,7 +101,7 @@ export const fetchCommitDetails = async (workspaceId: string, commitId: string)
`/api/v1/pit/commits/${commitId}/changes`,
{
params: {
workspaceId
projectId: workspaceId
}
}
);
@@ -124,7 +124,7 @@ export const fetchRollbackPreview = async (
envId,
deepRollback,
secretPath,
workspaceId
projectId: workspaceId
}
}
);
@@ -146,7 +146,7 @@ const fetchRollback = async (
deepRollback,
message,
envId,
workspaceId
projectId: workspaceId
}
);
return data;
@@ -156,7 +156,7 @@ const fetchRevert = async (commitId: string, workspaceId: string) => {
const { data } = await apiRequest.post<{ success: boolean; message: string }>(
`/api/v1/pit/commits/${commitId}/revert`,
{
workspaceId
projectId: workspaceId
}
);
return data;
@@ -361,8 +361,8 @@ export const RollbackPreviewTab = (): JSX.Element => {
subTitle={`
${
deepRollback
? "This will revert all changes to how they appeared at the point in time of this commit."
: "This will revert this folder and all child folders to how they appeared at the point in time of this commit."
? "This will revert this folder and all child folders to how they appeared at the point in time of this commit."
: "This will revert all changes to how they appeared at the point in time of this commit."
}
Any changes made after this commit will be permanently removed.
`}
@@ -1,22 +1,18 @@
/* eslint-disable react/prop-types */
import { useRef, useState } from "react";
/* eslint-disable no-nested-ternary */
import { useCallback, useRef, useState } from "react";
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export interface Version {
id?: string;
version: number;
// Secret-specific fields
secretKey?: string;
secretValue?: string;
secretComment?: string;
skipMultilineEncoding?: boolean;
// Folder-specific fields
name?: string;
// Allow other properties
[key: string]: any;
}
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
type JsonObject = { [key: string]: JsonValue };
type JsonArray = JsonValue[];
export interface DiffViewItem {
type: "secret" | "folder";
isAdded?: boolean;
@@ -35,244 +31,423 @@ interface SecretVersionDiffViewProps {
onToggleCollapse?: (id: string) => void;
showHeader?: boolean;
customHeader?: JSX.Element;
excludedFieldsHighlight?: string[];
}
const highlightChangedFields = (
json: any,
changedFields: Set<string>,
isOldVersion: boolean
const isObject = (obj: JsonValue): obj is JsonObject => {
return obj !== null && typeof obj === "object" && !Array.isArray(obj);
};
const isArray = (obj: JsonValue): obj is JsonArray => {
return Array.isArray(obj);
};
const deepEqual = (a: JsonValue, b: JsonValue): boolean => {
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== typeof b) return false;
if (isArray(a) && isArray(b)) {
if (a.length !== b.length) return false;
return a.every((item: JsonValue, index: number) => deepEqual(item, b[index]));
}
if (isObject(a) && isObject(b)) {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => keysB.includes(key) && deepEqual(a[key], b[key]));
}
return false;
};
const getDiffPaths = (oldObj: JsonValue, newObj: JsonValue, path: string = ""): Set<string> => {
const diffPaths = new Set<string>();
if (oldObj === newObj) return diffPaths;
if (oldObj == null || newObj == null) {
diffPaths.add(path || "root");
return diffPaths;
}
if (typeof oldObj !== typeof newObj) {
diffPaths.add(path || "root");
return diffPaths;
}
if (isArray(oldObj) && isArray(newObj)) {
return diffPaths;
}
if (isObject(oldObj) && isObject(newObj)) {
const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
allKeys.forEach((key) => {
const currentPath = path ? `${path}.${key}` : key;
if (path.includes("[")) {
return;
}
if (!(key in oldObj) || !(key in newObj) || !deepEqual(oldObj[key], newObj[key])) {
diffPaths.add(currentPath);
if (
key in oldObj &&
key in newObj &&
(isObject(oldObj[key]) || isArray(oldObj[key])) &&
(isObject(newObj[key]) || isArray(newObj[key]))
) {
const nestedDiffs = getDiffPaths(oldObj[key], newObj[key], currentPath);
nestedDiffs.forEach((p) => diffPaths.add(p));
}
}
});
return diffPaths;
}
if (oldObj !== newObj) {
diffPaths.add(path || "root");
}
return diffPaths;
};
const getNestedValue = (obj: JsonValue, path: string): JsonValue => {
if (!path) return obj;
const parts = path.split(/[.[\]]+/).filter(Boolean);
let current: JsonValue = obj;
parts.forEach((part) => {
if (current == null) {
return;
}
if (isObject(current)) {
current = current[part];
} else if (isArray(current)) {
const index = parseInt(part, 10);
if (!Number.isNaN(index)) {
current = current[index];
}
}
});
return current;
};
const isPathDifferent = (jsonPath: string, diffPaths: Set<string>): boolean => {
if (diffPaths.has(jsonPath)) return true;
const diffPathsArray = Array.from(diffPaths);
return diffPathsArray.some((diffPath) => {
return (
jsonPath.startsWith(`${diffPath}.`) ||
jsonPath.startsWith(`${diffPath}[`) ||
diffPath.startsWith(`${jsonPath}.`) ||
diffPath.startsWith(`${jsonPath}[`)
);
});
};
const isContainerActuallyChanged = (
path: string,
diffPaths: Set<string>,
oldObj: JsonValue,
newObj: JsonValue
): boolean => {
if (diffPaths.has(path)) {
if (oldObj == null || newObj == null) return true;
if (typeof oldObj !== typeof newObj) return true;
if (isArray(oldObj) !== isArray(newObj)) return true;
if (isObject(oldObj) !== isObject(newObj)) return true;
if (!isObject(oldObj) && !isArray(oldObj)) return true;
return false;
}
if (isArray(oldObj) && isArray(newObj)) {
return false;
}
if (isObject(oldObj) && isObject(newObj)) {
return false;
}
return oldObj !== newObj;
};
const renderJsonWithDiffs = (
obj: JsonValue,
diffPaths: Set<string>,
isOldVersion: boolean,
path: string = "",
indentLevel: number = 0,
keyName?: string,
isLastItem: boolean = false,
excludedFieldsHighlight: string[] = [],
oldVersionObj?: JsonValue,
newVersionObj?: JsonValue
): JSX.Element => {
const lines = JSON.stringify(json, null, 2).split("\n");
return (
<div className="font-mono text-sm">
{lines.map((line, idx) => {
const fieldMatch = line.match(/"([^"]+)":/);
if (fieldMatch && changedFields.has(fieldMatch[1])) {
// Check for different value types and highlight them
const indent = " ".repeat(indentLevel);
// 1. String values: "field": "value"
const stringMatch = line.match(/: "([^"]*)"(,?)$/);
if (stringMatch) {
const beforeValue = line.substring(0, line.indexOf(': "'));
const value = `"${stringMatch[1]}"`;
const afterValue = stringMatch[2] || "";
let isDifferent = false;
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
if (path.includes("[") && oldVersionObj && newVersionObj) {
const arrayMatch = path.match(/^([^[]+)\[(\d+)\]/);
if (arrayMatch) {
const arrayPath = arrayMatch[1];
const itemIndex = parseInt(arrayMatch[2], 10);
// 2. Number values: "field": 123
const numberMatch = line.match(/: (-?\d+(?:\.\d+)?)(,?)$/);
if (numberMatch) {
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
const value = numberMatch[1];
const afterValue = numberMatch[2] || "";
const oldArray = getNestedValue(oldVersionObj, arrayPath);
const newArray = getNestedValue(newVersionObj, arrayPath);
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue.substring(0, beforeValue.length - 2)}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
if (isArray(oldArray) && isArray(newArray)) {
const currentItem = isOldVersion ? oldArray[itemIndex] : newArray[itemIndex];
// 3. Null values: "field": null
const nullMatch = line.match(/: (null)(,?)$/);
if (nullMatch) {
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
const value = nullMatch[1];
const afterValue = nullMatch[2] || "";
if (isOldVersion) {
isDifferent = !newArray.some((newItem: JsonValue) => deepEqual(currentItem, newItem));
} else {
isDifferent = !oldArray.some((oldItem: JsonValue) => deepEqual(currentItem, oldItem));
}
} else {
isDifferent = isPathDifferent(path, diffPaths);
}
} else {
isDifferent = isPathDifferent(path, diffPaths);
}
} else {
isDifferent = isPathDifferent(path, diffPaths);
}
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue.substring(0, beforeValue.length - 2)}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
const getLineClass = (different: boolean) => {
if (!different) return "flex";
return isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300";
};
// 4. Boolean values: "field": true|false
const booleanMatch = line.match(/: (true|false)(,?)$/);
if (booleanMatch) {
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
const value = booleanMatch[1];
const afterValue = booleanMatch[2] || "";
const getHighlightClass = (different: boolean) => {
if (!different) return "";
return isOldVersion ? "bg-red-900 rounded px-1" : "bg-green-900 rounded px-1";
};
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue.substring(0, beforeValue.length - 2)}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
const prefix = isDifferent ? (isOldVersion ? "-" : "+") : " ";
const keyDisplay = keyName ? `"${keyName}": ` : "";
const comma = !isLastItem ? "," : "";
// 5. Array values: Handle the first line of an array - "field": [
const arrayStartMatch = line.match(/: \[(,?)$/);
if (arrayStartMatch) {
// This is the start of an array, highlight the whole line
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>{line}</div>
</div>
);
}
const reactKey = `${path || "root"}-${keyName || "value"}-${indentLevel}-${typeof obj}`;
// 6. Empty Array values: "field": []
const emptyArrayMatch = line.match(/: \[\](,?)$/);
if (emptyArrayMatch) {
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
const value = "[]";
const afterValue = emptyArrayMatch[1] || "";
if (
obj === null ||
typeof obj === "string" ||
typeof obj === "number" ||
typeof obj === "boolean"
) {
let valueDisplay = "";
if (obj === null) valueDisplay = "null";
else if (typeof obj === "string") valueDisplay = `"${obj}"`;
else valueDisplay = String(obj);
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue.substring(0, beforeValue.length - 2)}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
return (
<div key={reactKey} className={getLineClass(isDifferent)}>
<div className="w-4 flex-shrink-0">{prefix}</div>
<div>
{indent}
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
<span className={getHighlightClass(isDifferent)}>{valueDisplay}</span>
{comma}
</div>
</div>
);
}
// Check if this is part of an array or object that belongs to a changed field
// This handles array items, closing brackets, and other complex structure contents
const belongsToChangedField = () => {
// If we're inside an array or object of a changed field, highlight it
let openBrackets = 0;
let openBraces = 0;
let currentFieldName = null;
if (isArray(obj) && obj.length === 0) {
return (
<div key={reactKey} className={getLineClass(isDifferent)}>
<div className="w-4 flex-shrink-0">{prefix}</div>
<div>
{indent}
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
<span className={getHighlightClass(isDifferent)}>[]</span>
{comma}
</div>
</div>
);
}
for (let i = idx - 1; i >= 0; i -= 1) {
const prevLine = lines[i];
if (isObject(obj) && Object.keys(obj).length === 0) {
return (
<div key={reactKey} className={getLineClass(isDifferent)}>
<div className="w-4 flex-shrink-0">{prefix}</div>
<div>
{indent}
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
<span className={getHighlightClass(isDifferent)}>{"{}"}</span>
{comma}
</div>
</div>
);
}
// Count brackets and braces to track nesting
openBrackets += (prevLine.match(/\[/g) || []).length;
openBrackets -= (prevLine.match(/\]/g) || []).length;
openBraces += (prevLine.match(/{/g) || []).length;
openBraces -= (prevLine.match(/}/g) || []).length;
let isContainerAddedOrRemoved = false;
// If we find a field and we're still inside its value, check if it's a changed field
const fieldNameMatch = prevLine.match(/"([^"]+)":/);
if (fieldNameMatch && (openBrackets > 0 || openBraces > 0)) {
[, currentFieldName] = fieldNameMatch;
return changedFields.has(currentFieldName);
}
if (oldVersionObj && newVersionObj) {
const oldValue = getNestedValue(oldVersionObj, path);
const newValue = getNestedValue(newVersionObj, path);
// If we've reached the root level, stop looking
if (openBrackets <= 0 && openBraces <= 0) {
return false;
}
}
if (oldValue == null || newValue == null) {
isContainerAddedOrRemoved = true;
} else if (typeof oldValue !== typeof newValue) {
isContainerAddedOrRemoved = true;
} else if (isArray(oldValue) !== isArray(newValue)) {
isContainerAddedOrRemoved = true;
} else if (isObject(oldValue) !== isObject(newValue)) {
isContainerAddedOrRemoved = true;
}
} else {
isContainerAddedOrRemoved = isContainerActuallyChanged(
path,
diffPaths,
isOldVersion ? obj : oldVersionObj || null,
isOldVersion ? newVersionObj || null : obj
);
}
return false;
};
if (isArray(obj)) {
return (
<div key={reactKey}>
<div className={getLineClass(isContainerAddedOrRemoved)}>
<div className="w-4 flex-shrink-0">
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
</div>
<div>
{indent}
{keyName && (
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
{keyDisplay}
</span>
)}
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>[</span>
</div>
</div>
// Regular line with changed field or part of a changed complex structure
if (belongsToChangedField()) {
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>{line}</div>
</div>
);
}
{obj.map((item: JsonValue, index: number) => {
const itemPath = path ? `${path}[${index}]` : `[${index}]`;
const isLast = index === obj.length - 1;
// Simple fallback for any other cases of changed fields
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>{line}</div>
<div key={`${reactKey}-item-${index + 1}`}>
{renderJsonWithDiffs(
item,
diffPaths,
isOldVersion,
itemPath,
indentLevel + 1,
undefined,
isLast,
excludedFieldsHighlight,
oldVersionObj,
newVersionObj
)}
</div>
);
}
})}
// Unchanged lines
<div className={getLineClass(isContainerAddedOrRemoved)}>
<div className="w-4 flex-shrink-0">
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
</div>
<div>
{indent}
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>]</span>
{comma}
</div>
</div>
</div>
);
}
if (isObject(obj)) {
const keys = Object.keys(obj);
return (
<div key={reactKey}>
<div className={getLineClass(isContainerAddedOrRemoved)}>
<div className="w-4 flex-shrink-0">
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
</div>
<div>
{indent}
{keyName && (
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
{keyDisplay}
</span>
)}
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"{"}</span>
</div>
</div>
{keys.map((key, index) => {
const keyPath = path ? `${path}.${key}` : key;
const isLast = index === keys.length - 1;
const propKey = `${reactKey}-prop-${key}`;
return (
<div key={propKey}>
{renderJsonWithDiffs(
obj[key],
diffPaths,
isOldVersion,
keyPath,
indentLevel + 1,
key,
isLast,
excludedFieldsHighlight,
oldVersionObj,
newVersionObj
)}
</div>
);
})}
<div className={getLineClass(isContainerAddedOrRemoved)}>
<div className="w-4 flex-shrink-0">
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
</div>
<div>
{indent}
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"}"}</span>
{comma}
</div>
</div>
</div>
);
}
return (
<div key={reactKey} className={getLineClass(isDifferent)}>
<div className="w-4 flex-shrink-0">{prefix}</div>
<div>
{indent}
{keyDisplay}
{String(obj)}
{comma}
</div>
</div>
);
};
const formatAddedJson = (json: JsonValue): JSX.Element => {
const lines = JSON.stringify(json, null, 2).split("\n");
return (
<div className="font-mono text-sm text-green-300">
{lines.map((line, lineIndex) => {
const lineKey = `added-${line.slice(0, 30)}-${lineIndex}`;
return (
<div key={`${idx + 1}`} className="flex">
<div className="w-4 flex-shrink-0" />
<div key={lineKey} className="flex">
<div className="w-4 flex-shrink-0">+</div>
<div>{line}</div>
</div>
);
@@ -281,224 +456,121 @@ const highlightChangedFields = (
);
};
// Helper: Format added JSON with + in a separate column
const formatAddedJson = (json: any): JSX.Element => {
const lines = JSON.stringify(json, null, 2).split("\n");
return (
<div className="font-mono text-sm text-green-300">
{lines.map((line, idx) => (
<div key={`${idx + 1}`} className="flex">
<div className="w-4 flex-shrink-0">+</div>
<div>{line}</div>
</div>
))}
</div>
);
};
// Helper: Format deleted JSON with - in a separate column
const formatDeletedJson = (json: any): JSX.Element => {
const formatDeletedJson = (json: JsonValue): JSX.Element => {
const lines = JSON.stringify(json, null, 2).split("\n");
return (
<div className="font-mono text-sm text-red-300">
{lines.map((line, idx) => (
<div key={`${idx + 1}`} className="flex">
<div className="w-4 flex-shrink-0">-</div>
<div>{line}</div>
</div>
))}
{lines.map((line, lineIndex) => {
const lineKey = `deleted-${line.slice(0, 30)}-${lineIndex}`;
return (
<div key={lineKey} className="flex">
<div className="w-4 flex-shrink-0">-</div>
<div>{line}</div>
</div>
);
})}
</div>
);
};
// Helper: Get differences between secret versions
export const getVersionDifferences = (versions: Version[]) => {
if (!versions || versions.length === 0) return [];
// Sort versions by version number (descending)
const sortedVersions = [...versions].sort((a, b) => b.version - a.version);
const newVersion = sortedVersions[0];
// Fields to process
const fieldsToProcess = [
"secretKey",
"secretValue",
"secretComment",
"skipMultilineEncoding",
"secretReminderRepeatDays",
"secretReminderNote",
"metadata",
"tags",
"secretReminderRecipients",
"name"
];
// If only one version exists
if (sortedVersions.length === 1) {
return fieldsToProcess.reduce(
(differences, field) => {
if (newVersion[field] !== undefined && newVersion[field] !== null) {
let newVal = newVersion[field];
if (field === "tags" && Array.isArray(newVersion[field])) {
if (newVersion[field].length > 0 && typeof newVersion[field][0] === "object") {
newVal = newVersion[field].map((tag) => tag.name).join(", ");
} else if (Array.isArray(newVersion[field])) {
newVal = newVersion[field].join(", ");
}
}
differences.push({
field,
oldValue: null,
newValue: newVal
});
}
return differences;
},
[] as { field: string; oldValue: any; newValue: any }[]
);
}
// Otherwise, compare the two versions
const oldVersion = sortedVersions[1];
return fieldsToProcess.reduce(
(differences, field) => {
if (JSON.stringify(oldVersion[field]) !== JSON.stringify(newVersion[field])) {
let oldVal = oldVersion[field];
let newVal = newVersion[field];
if (field === "tags" && Array.isArray(oldVersion[field])) {
if (oldVersion[field].length > 0 && typeof oldVersion[field][0] === "object") {
oldVal = oldVersion[field].map((tag) => tag.name).join(", ");
} else if (Array.isArray(oldVersion[field])) {
oldVal = oldVersion[field].join(", ");
}
}
if (field === "tags" && Array.isArray(newVersion[field])) {
if (newVersion[field].length > 0 && typeof newVersion[field][0] === "object") {
newVal = newVersion[field].map((tag) => tag.name).join(", ");
} else if (Array.isArray(newVersion[field])) {
newVal = newVersion[field].join(", ");
}
}
differences.push({
field,
oldValue: oldVal,
newValue: newVal
});
}
return differences;
},
[] as { field: string; oldValue: any; newValue: any }[]
);
const cleanVersionForComparison = (version: Version): JsonValue => {
const { id, version: versionNumber, ...cleanVersion } = version;
return cleanVersion;
};
// Helper: Get differences between folder versions
export const getFolderDifferences = (versions: Version[]) => {
if (!versions || versions.length === 0) return [];
const sortedVersions = [...versions].sort((a, b) => b.version - a.version);
const newVersion = sortedVersions[0];
if (sortedVersions.length === 1) {
return [
{
field: "folderName",
oldValue: null,
newValue: newVersion.name
}
];
}
const oldVersion = sortedVersions[1];
return [
{
field: "folderName",
oldValue: oldVersion.name,
newValue: newVersion.name
}
];
};
export const SecretVersionDiffView: React.FC<SecretVersionDiffViewProps> = ({
export const SecretVersionDiffView = ({
item,
isCollapsed = false,
onToggleCollapse,
showHeader = true,
customHeader
}) => {
customHeader,
excludedFieldsHighlight = ["metadata", "tags"]
}: SecretVersionDiffViewProps) => {
const oldContainerRef = useRef<HTMLDivElement>(null);
const newContainerRef = useRef<HTMLDivElement>(null);
const [isScrollingSynced, setIsScrollingSynced] = useState(false);
const [internalCollapsed, setInternalCollapsed] = useState(isCollapsed);
const collapsed = onToggleCollapse ? isCollapsed : internalCollapsed;
const handleToggle = () => {
const handleToggle = useCallback(() => {
if (onToggleCollapse && item.id) {
onToggleCollapse(item.id);
} else {
setInternalCollapsed(!internalCollapsed);
setInternalCollapsed((prev) => !prev);
}
};
}, [onToggleCollapse, item.id]);
if (!item.versions) {
const collapsed = onToggleCollapse ? isCollapsed : internalCollapsed;
if (!item.versions || item.versions.length === 0) {
return <div className="px-6 py-3 text-gray-400">No details available</div>;
}
const differences = getVersionDifferences(item.versions);
if (differences.length === 0) {
return null;
}
const changedFields = new Set<string>();
differences.forEach((diff) => {
if (JSON.stringify(diff.oldValue) !== JSON.stringify(diff.newValue)) {
changedFields.add(diff.field);
}
});
const handleScroll = (container: "old" | "new") => {
if (isScrollingSynced) return;
setIsScrollingSynced(true);
if (container === "old" && oldContainerRef.current && newContainerRef.current) {
newContainerRef.current.scrollTop = oldContainerRef.current.scrollTop;
} else if (container === "new" && oldContainerRef.current && newContainerRef.current) {
oldContainerRef.current.scrollTop = newContainerRef.current.scrollTop;
}
setTimeout(() => setIsScrollingSynced(false), 50);
};
const sortedVersions = [...item.versions].sort((a, b) => b.version - a.version);
let oldVersion = null;
let newVersion = null;
let oldVersionContent = null;
let newVersionContent = null;
let diffPaths = new Set<string>();
if (item.isUpdated) {
if (item.isUpdated && sortedVersions.length >= 2) {
if (item.isRollback) {
[oldVersion, newVersion] = sortedVersions;
} else {
[newVersion, oldVersion] = sortedVersions;
}
oldVersionContent = highlightChangedFields(oldVersion, changedFields, true);
newVersionContent = highlightChangedFields(newVersion, changedFields, false);
const cleanOldVersion = cleanVersionForComparison(oldVersion);
const cleanNewVersion = cleanVersionForComparison(newVersion);
diffPaths = getDiffPaths(cleanOldVersion, cleanNewVersion);
if (diffPaths.size === 0) {
return null;
}
oldVersionContent = (
<div className="font-mono text-sm">
{renderJsonWithDiffs(
cleanOldVersion,
diffPaths,
true,
"",
0,
undefined,
false,
excludedFieldsHighlight,
cleanOldVersion,
cleanNewVersion
)}
</div>
);
newVersionContent = (
<div className="font-mono text-sm">
{renderJsonWithDiffs(
cleanNewVersion,
diffPaths,
false,
"",
0,
undefined,
false,
excludedFieldsHighlight,
cleanOldVersion,
cleanNewVersion
)}
</div>
);
} else if (item.isAdded) {
[newVersion] = sortedVersions;
oldVersionContent = <div className="italic text-gray-400" />;
newVersionContent = formatAddedJson(newVersion);
const cleanNewVersion = cleanVersionForComparison(newVersion);
oldVersionContent = <div className="italic text-gray-400">No previous version</div>;
newVersionContent = formatAddedJson(cleanNewVersion);
} else if (item.isDeleted) {
[oldVersion] = sortedVersions;
oldVersionContent = formatDeletedJson(oldVersion);
newVersionContent = <div className="italic text-gray-400" />;
const cleanOldVersion = cleanVersionForComparison(oldVersion);
oldVersionContent = formatDeletedJson(cleanOldVersion);
newVersionContent = <div className="italic text-gray-400">Version deleted</div>;
} else {
return null;
}
const renderHeader = () => {
@@ -507,7 +579,7 @@ export const SecretVersionDiffView: React.FC<SecretVersionDiffViewProps> = ({
}
const isSecret = item.type === "secret";
const key = isSecret ? item.secretKey || "" : item.folderName || "";
const key = isSecret ? item.secretKey || "Unnamed Secret" : item.folderName || "Unnamed Folder";
let textStyle = "text-white";
let changeBadge = null;
@@ -565,7 +637,6 @@ export const SecretVersionDiffView: React.FC<SecretVersionDiffViewProps> = ({
<div
ref={oldContainerRef}
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
onScroll={() => handleScroll("old")}
>
{oldVersionContent}
</div>
@@ -573,7 +644,6 @@ export const SecretVersionDiffView: React.FC<SecretVersionDiffViewProps> = ({
<div
ref={newContainerRef}
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
onScroll={() => handleScroll("new")}
>
{newVersionContent}
</div>
@@ -43,11 +43,12 @@ const CommitItem = ({
<div className="border-b border-zinc-800 last:border-b-0">
<div className="px-4 py-4 transition-colors duration-200 hover:bg-zinc-800">
<div className="flex flex-col sm:flex-row sm:justify-between">
<div className="flex-1">
<div className="w-5/6 flex-1">
<div className="flex items-center">
<Button
variant="link"
className="text-white hover:underline"
className="truncate text-left text-white hover:underline"
isFullWidth
onClick={(e) => {
e.stopPropagation();
onSelectCommit(commit.id, "tab-commit-details");
@@ -64,7 +65,7 @@ const CommitItem = ({
</span>
</p>
</div>
<div className="mt-2 flex items-center sm:mt-0">
<div className="mt-2 flex w-1/6 items-center justify-end sm:mt-0">
<div className="flex items-center space-x-1">
<Button
variant="link"