mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 19:44:52 +00:00
PIT: general improvements and fixes
This commit is contained in:
@@ -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;
|
||||
|
||||
+2
-2
@@ -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.
|
||||
`}
|
||||
|
||||
+471
-401
@@ -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>
|
||||
|
||||
+4
-3
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user