mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 01:52:22 +00:00
chore(frontend): changed source code to src folder from root
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the data for a certain action performed by a user
|
||||
* @param {object} obj
|
||||
* @param {string} obj.actionId - id of an action for which we are trying to get data
|
||||
* @returns
|
||||
*/
|
||||
const getActionData = async ({ actionId }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/action/' + actionId, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).action;
|
||||
} else {
|
||||
console.log('Failed to get the info about an action');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getActionData;
|
||||
@@ -0,0 +1,72 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
userId: string;
|
||||
actionNames: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the activity logs for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log
|
||||
* @param {object} obj.offset - teh starting point of logs that we want to pull
|
||||
* @param {object} obj.limit - how many logs will we output
|
||||
* @param {object} obj.userId - optional userId filter - will only query logs for that user
|
||||
* @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions
|
||||
* @returns
|
||||
*/
|
||||
const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => {
|
||||
let payload;
|
||||
if (userId != "" && actionNames != '') {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId),
|
||||
actionNames: actionNames
|
||||
}
|
||||
} else if (userId != "") {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId)
|
||||
}
|
||||
} else if (actionNames != "") {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
actionNames: actionNames
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent'
|
||||
}
|
||||
}
|
||||
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/logs?' +
|
||||
new URLSearchParams(payload),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).logs;
|
||||
} else {
|
||||
console.log('Failed to get project logs');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectLogs;
|
||||
@@ -0,0 +1,39 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the secret snapshots for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||
* @param {object} obj.offset - teh starting point of snapshots that we want to pull
|
||||
* @param {object} obj.limit - how many snapshots will we output
|
||||
* @returns
|
||||
*/
|
||||
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/secret-snapshots?' +
|
||||
new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
}), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).secretSnapshots;
|
||||
} else {
|
||||
console.log('Failed to get project secret snapshots');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectSecretShanpshots;
|
||||
@@ -0,0 +1,31 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the count of secret snapshots for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||
* @returns
|
||||
*/
|
||||
const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).count;
|
||||
} else {
|
||||
console.log('Failed to get the count of project secret snapshots');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectSercetSnapshotsCount;
|
||||
@@ -0,0 +1,31 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface SnapshotProps {
|
||||
secretSnapshotId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the secrets for a certain secret snapshot
|
||||
* @param {object} obj
|
||||
* @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets
|
||||
* @returns
|
||||
*/
|
||||
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/secret-snapshot/' + secretSnapshotId, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).secretSnapshot;
|
||||
} else {
|
||||
console.log('Failed to get the secrets of a certain snapshot');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getSecretSnapshotData;
|
||||
@@ -0,0 +1,40 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface secretVersionProps {
|
||||
secretId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the versions of a specific secret
|
||||
* @param {object} obj
|
||||
* @param {string} obj.secretId - the id of a secret for which we are fetching the version history
|
||||
* @param {number} obj.offset - the start of our query
|
||||
* @param {number} obj.limit - how far our query goes
|
||||
* @returns
|
||||
*/
|
||||
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/secret/' + secretId + '/secret-versions?' +
|
||||
new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log('Failed to get secret version history');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getSecretVersions;
|
||||
@@ -0,0 +1,30 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function performs a rollback of secrets in a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - id of the project for which we are rolling back data
|
||||
* @param {number} obj.version - version to which we are rolling back
|
||||
* @returns
|
||||
*/
|
||||
const performSecretRollback = async ({ workspaceId, version }: { workspaceId: string; version: number; }) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + "/secret-snapshots/rollback", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version
|
||||
})
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to perform the secret rollback');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default performSecretRollback;
|
||||
@@ -0,0 +1,185 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import getActionData from "@app/ee/api/secrets/GetActionData";
|
||||
import patienceDiff from '@app/ee/utilities/findTextDifferences';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
||||
|
||||
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} = require('../../components/utilities/cryptography/crypto');
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
currentAction: string;
|
||||
}
|
||||
|
||||
interface SecretProps {
|
||||
secret: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyHash: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueHash: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
}
|
||||
|
||||
interface DecryptedSecretProps {
|
||||
newSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
oldSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed
|
||||
* @returns the sidebar with the payload of user activity logs
|
||||
*/
|
||||
const ActivitySideBar = ({
|
||||
toggleSidebar,
|
||||
currentAction
|
||||
}: SideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
|
||||
const [actionMetaData, setActionMetaData] = useState<ActionProps>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempActionData = await getActionData({ actionId: currentAction });
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
// #TODO: make this a separate function and reuse across the app
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: {
|
||||
newSecretVersion?: SecretProps;
|
||||
oldSecretVersion?: SecretProps;
|
||||
}) => {
|
||||
return {
|
||||
newSecretVersion: {
|
||||
key: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.newSecretVersion!.secretKeyIV,
|
||||
tag: encryptedSecretVersion.newSecretVersion!.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}),
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.newSecretVersion!.secretValueIV,
|
||||
tag: encryptedSecretVersion.newSecretVersion!.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
},
|
||||
oldSecretVersion: {
|
||||
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}): undefined,
|
||||
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
}): undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setActionData(decryptedSecretVersions);
|
||||
setActionMetaData({name: tempActionData.name});
|
||||
setIsLoading(false);
|
||||
}
|
||||
getLogData();
|
||||
}, [currentAction]);
|
||||
|
||||
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("activity:event." + actionMetaData?.name)}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col px-4'>
|
||||
{(actionMetaData?.name == 'readSecrets'
|
||||
|| actionMetaData?.name == 'addSecrets'
|
||||
|| actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) =>
|
||||
<div key={id}>
|
||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
||||
<DashboardInputField
|
||||
key={id}
|
||||
onChangeHandler={() => {}}
|
||||
type="value"
|
||||
position={1}
|
||||
value={item.newSecretVersion.value}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) =>
|
||||
<>
|
||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
||||
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
|
||||
<div className='bg-red/30 px-2'>- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
|
||||
<div className='bg-green-500/30 px-2'>+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
};
|
||||
|
||||
export default ActivitySideBar;
|
||||
@@ -0,0 +1,138 @@
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import timeSince from '@app/ee/utilities/timeSince';
|
||||
import {
|
||||
faAngleDown,
|
||||
faAngleRight,
|
||||
faUpRightFromSquare
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import guidGenerator from '../../components/utilities/randomId';
|
||||
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface logData {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: string;
|
||||
payload: PayloadProps[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a single row of the activity table
|
||||
* @param obj
|
||||
* @param {logData} obj.row - data for a certain event
|
||||
* @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event
|
||||
* @returns
|
||||
*/
|
||||
const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => {
|
||||
const [payloadOpened, setPayloadOpened] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
|
||||
<td
|
||||
onClick={() => setPayloadOpened(!payloadOpened)}
|
||||
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={payloadOpened ? faAngleDown : faAngleRight}
|
||||
className={`mt-2.5 ml-6 text-bunker-100 hover:bg-mineshaft-700 ${
|
||||
payloadOpened && 'bg-mineshaft-500'
|
||||
} p-1 duration-100 h-4 w-4 rounded-md`}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.user}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.channel}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{timeSince(new Date(row.createdAt))}
|
||||
</td>
|
||||
</tr>
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>{String(t("common:timestamp"))}</td>
|
||||
<td>{row.createdAt}</td>
|
||||
</tr>}
|
||||
{payloadOpened && row.payload?.map((action, index) => {
|
||||
return action.secretVersions.length > 0 && <tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
<td></td>
|
||||
<td className="">{t("activity:event." + action.name)}</td>
|
||||
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
|
||||
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>{String(t("activity:ip-address"))}</td>
|
||||
<td>{row.ipAddress}</td>
|
||||
</tr>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the table for activity logs (one of the tabs)
|
||||
* @param {object} obj
|
||||
* @param {logData} obj.data - data for user activity logs
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {boolean} obj.isLoading - whether the log data has been loaded yet or not
|
||||
* @returns
|
||||
*/
|
||||
const ActivityTable = ({ data, toggleSidebar, isLoading }: { data: logData[], toggleSidebar: (value: string) => void; isLoading: boolean; }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 mt-8">
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
|
||||
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5"></div>
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300">
|
||||
<tr className='text-sm'>
|
||||
<th className="text-left pl-6 pt-2.5 pb-3"></th>
|
||||
<th className="text-left font-semibold pt-2.5 pb-3">{String(t("common:event")).toUpperCase()}</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:user")).toUpperCase()}</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:source")).toUpperCase()}</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:time")).toUpperCase()}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((row, index) => {
|
||||
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{isLoading && <div className='w-full flex justify-center mb-8 mt-4'><Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="loading animation"
|
||||
></Image></div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityTable;
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import getProjectSecretShanpshots from "@app/ee/api/secrets/GetProjectSercetShanpshots";
|
||||
import getSecretSnapshotData from "@app/ee/api/secrets/GetSecretSnapshotData";
|
||||
import timeSince from "@app/ee/utilities/timeSince";
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from "~/components/basic/buttons/Button";
|
||||
import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cryptography/crypto";
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
|
||||
export interface SecretDataProps {
|
||||
pos: number;
|
||||
key: string;
|
||||
value: string;
|
||||
type: string;
|
||||
id: string;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: boolean) => void;
|
||||
setSnapshotData: (value: any) => void;
|
||||
chosenSnapshot: string;
|
||||
}
|
||||
|
||||
interface SnaphotProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface EncrypetedSecretVersionListProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
environment: string;
|
||||
type: "personal" | "shared";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {function} obj.setSnapshotData - state manager for snapshot data
|
||||
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
|
||||
* @returns the sidebar with the options for point-in-time recovery (commits)
|
||||
*/
|
||||
const PITRecoverySidebar = ({
|
||||
toggleSidebar,
|
||||
setSnapshotData,
|
||||
chosenSnapshot
|
||||
}: SideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [secretSnapshotsMetadata, setSecretSnapshotsMetadata] = useState<SnaphotProps[]>([]);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 15;
|
||||
|
||||
const loadMoreSnapshots = () => {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const results = await getProjectSecretShanpshots({ workspaceId: String(router.query.id), limit: currentLimit, offset: currentOffset })
|
||||
setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results));
|
||||
setIsLoading(false);
|
||||
}
|
||||
getLogData();
|
||||
}, [currentOffset]);
|
||||
|
||||
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string; }) => {
|
||||
const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
|
||||
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = secretSnapshotData.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => {
|
||||
return {
|
||||
id: encryptedSecretVersion._id,
|
||||
pos: pos,
|
||||
type: encryptedSecretVersion.type,
|
||||
environment: encryptedSecretVersion.environment,
|
||||
key: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.secretKeyIV,
|
||||
tag: encryptedSecretVersion.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}),
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.secretValueIV,
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const secretKeys = [...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))];
|
||||
|
||||
const result = secretKeys.map((key, index) => {
|
||||
return {
|
||||
id: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].id,
|
||||
pos: index,
|
||||
key: key,
|
||||
environment: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].environment,
|
||||
value: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0]?.value,
|
||||
valueOverride: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'personal')[0]?.value,
|
||||
}
|
||||
});
|
||||
|
||||
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: result, comment: '' })
|
||||
}
|
||||
|
||||
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-min'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("Point-in-time Recovery")}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar(false)}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col px-2 py-2 overflow-y-auto h-[92vh]'>
|
||||
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
|
||||
<div
|
||||
key={snapshot._id}
|
||||
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
|
||||
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black pointer-events-none" : "bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}
|
||||
>
|
||||
<div className="flex flex-row items-start">
|
||||
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
|
||||
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
|
||||
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
|
||||
</div>
|
||||
</div>)}
|
||||
<div className='flex justify-center w-full mb-14'>
|
||||
<div className='items-center w-40'>
|
||||
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
};
|
||||
|
||||
export default PITRecoverySidebar;
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import getSecretVersions from '@app/ee/api/secrets/GetSecretVersions';
|
||||
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { decryptAssymmetric, decryptSymmetric } from '~/components/utilities/cryptography/crypto';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
interface DecryptedSecretVersionListProps {
|
||||
createdAt: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface EncrypetedSecretVersionListProps {
|
||||
createdAt: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} secretId - the id of a secret for which are querying version history
|
||||
* @returns a list of versions for a specific secret
|
||||
*/
|
||||
const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getSecretVersionHistory = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10});
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
|
||||
return {
|
||||
createdAt: encryptedSecretVersion.createdAt,
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.secretValueIV,
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setSecretVersions(decryptedSecretVersions);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
};
|
||||
getSecretVersionHistory();
|
||||
}, [secretId]);
|
||||
|
||||
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
|
||||
<p className=''>{t("dashboard:sidebar.version-history")}</p>
|
||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full'>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-48 overflow-y-auto overflow-x-none'>
|
||||
{secretVersions
|
||||
? secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) =>
|
||||
<div key={index} className='flex flex-row'>
|
||||
<div className='pr-1 flex flex-col items-center'>
|
||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||
<div className='w-0 h-full border-l mt-1'></div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
|
||||
<div className='pr-2 pt-1'>
|
||||
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='w-full h-full flex items-center justify-center text-bunker-400'>No version history yet.</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default SecretVersionList;
|
||||
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
*
|
||||
* @param textOld - old secret
|
||||
* @param textNew - new (updated) secret
|
||||
* @param diffPlusFlag - a flag for whether we want to detect moving segments
|
||||
* - doesn't work in some examples (e.g., when we have a full reverse ordering of the text)
|
||||
* @returns
|
||||
*/
|
||||
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
|
||||
|
||||
/**
|
||||
* findUnique finds all unique values in arr[lo..hi], inclusive. This
|
||||
* function is used in preparation for determining the longest common
|
||||
* subsequence. Specifically, it first reduces the array range in question
|
||||
* to unique values.
|
||||
* @param chars - an array of characters
|
||||
* @param lo
|
||||
* @param hi
|
||||
* @returns - an ordered Map, with the arr[i] value as the Map key and the
|
||||
* array index i as the Map value.
|
||||
*/
|
||||
function findUnique(chars: string[], lo: number, hi: number) {
|
||||
const characterMap = new Map();
|
||||
|
||||
for (let i=lo; i<=hi; i++) {
|
||||
const character = chars[i];
|
||||
|
||||
if (characterMap.has(character)) {
|
||||
characterMap.get(character).count++;
|
||||
characterMap.get(character).index = i;
|
||||
} else {
|
||||
characterMap.set(character, { count: 1, index: i });
|
||||
}
|
||||
}
|
||||
|
||||
characterMap.forEach((val, key, map) => {
|
||||
if (val.count !== 1) {
|
||||
map.delete(key);
|
||||
} else {
|
||||
map.set(key, val.index);
|
||||
}
|
||||
});
|
||||
|
||||
return characterMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param aArray
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bArray
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @returns an ordered Map, with the Map key as the common line between aArray
|
||||
* and bArray, with the Map value as an object containing the array indexes of
|
||||
* the matching unique lines.
|
||||
*
|
||||
*/
|
||||
function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) {
|
||||
const ma = findUnique(aArray, aLo, aHi);
|
||||
const mb = findUnique(bArray, bLo, bHi);
|
||||
|
||||
ma.forEach((val, key, map) => {
|
||||
if (mb.has(key)) {
|
||||
map.set(key, {
|
||||
indexA: val,
|
||||
indexB: mb.get(key)
|
||||
});
|
||||
} else {
|
||||
map.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
return ma;
|
||||
}
|
||||
|
||||
/**
|
||||
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
|
||||
* and determines the Longest Common Subsequence (LCS).
|
||||
* @param abMap
|
||||
* @returns an ordered array of objects containing the array indexes of the
|
||||
* matching lines for a LCS.
|
||||
*/
|
||||
function longestCommonSubsequence(abMap: Map<number, { indexA: number, indexB: number, prev?: number }>) {
|
||||
const ja: any = [];
|
||||
|
||||
// First, walk the list creating the jagged array.
|
||||
abMap.forEach((val, key, map) => {
|
||||
let i = 0;
|
||||
|
||||
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!ja[i]) {
|
||||
ja[i] = [];
|
||||
}
|
||||
|
||||
if (0 < i) {
|
||||
val.prev = ja[i-1][ja[i - 1].length - 1];
|
||||
}
|
||||
ja[i].push(val);
|
||||
});
|
||||
|
||||
// Now, pull out the longest common subsequence.
|
||||
let lcs: any[] = [];
|
||||
|
||||
if (0 < ja.length) {
|
||||
const n = ja.length - 1;
|
||||
lcs = [ja[n][ja[n].length - 1]];
|
||||
|
||||
while (lcs[lcs.length - 1].prev) {
|
||||
lcs.push(lcs[lcs.length - 1].prev);
|
||||
}
|
||||
}
|
||||
|
||||
return lcs.reverse();
|
||||
}
|
||||
|
||||
// "result" is the array used to accumulate the textOld that are deleted, the
|
||||
// lines that are shared between textOld and textNew, and the textNew that were
|
||||
// inserted.
|
||||
|
||||
const result: any[] = [];
|
||||
let deleted = 0;
|
||||
let inserted = 0;
|
||||
|
||||
// aMove and bMove will contain the lines that don't match, and will be returned
|
||||
// for possible searching of lines that moved.
|
||||
|
||||
const aMove: any[] = [];
|
||||
const aMoveIndex: any[] = [];
|
||||
const bMove: any[] = [];
|
||||
const bMoveIndex: any[] = [];
|
||||
|
||||
/**
|
||||
* addToResult simply pushes the latest value onto the "result" array. This
|
||||
* array captures the diff of the line, aIndex, and bIndex from the textOld
|
||||
* and textNew array.
|
||||
* @param aIndex
|
||||
* @param bIndex
|
||||
*/
|
||||
function addToResult(aIndex: number, bIndex: number) {
|
||||
if (bIndex < 0) {
|
||||
aMove.push(textOld[aIndex]);
|
||||
aMoveIndex.push(result.length);
|
||||
deleted++;
|
||||
} else if (aIndex < 0) {
|
||||
bMove.push(textNew[bIndex]);
|
||||
bMoveIndex.push(result.length);
|
||||
inserted++;
|
||||
}
|
||||
|
||||
result.push({
|
||||
line: 0 <= aIndex ? textOld[aIndex] : textNew[bIndex],
|
||||
aIndex: aIndex,
|
||||
bIndex: bIndex,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* addSubMatch handles the lines between a pair of entries in the LCS. Thus,
|
||||
* this function might recursively call recurseLCS to further match the lines
|
||||
* between textOld and textNew.
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
*/
|
||||
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
|
||||
// Match any lines at the beginning of textOld and textNew.
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
|
||||
addToResult(aLo++, bLo++);
|
||||
}
|
||||
|
||||
// Match any lines at the end of textOld and textNew, but don't place them
|
||||
// in the "result" array just yet, as the lines between these matches at
|
||||
// the beginning and the end need to be analyzed first.
|
||||
|
||||
const aHiTemp = aHi;
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
|
||||
aHi--;
|
||||
bHi--;
|
||||
}
|
||||
|
||||
// Now, check to determine with the remaining lines in the subsequence
|
||||
// whether there are any unique common lines between textOld and textNew.
|
||||
//
|
||||
// If not, add the subsequence to the result (all textOld having been
|
||||
// deleted, and all textNew having been inserted).
|
||||
//
|
||||
// If there are unique common lines between textOld and textNew, then let's
|
||||
// recursively perform the patience diff on the subsequence.
|
||||
|
||||
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
|
||||
|
||||
if (uniqueCommonMap.size === 0) {
|
||||
while (aLo <= aHi) {
|
||||
addToResult(aLo++, -1);
|
||||
}
|
||||
|
||||
while (bLo <= bHi) {
|
||||
addToResult(-1, bLo++);
|
||||
}
|
||||
} else {
|
||||
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
|
||||
}
|
||||
|
||||
// Finally, let's add the matches at the end to the result.
|
||||
while (aHi < aHiTemp) {
|
||||
addToResult(++aHi, ++bHi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* recurseLCS finds the longest common subsequence (LCS) between the arrays
|
||||
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
|
||||
* recursively performs another LCS search (via addSubMatch), until there are
|
||||
* none found, at which point the subsequence is dumped to the result.
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @param uniqueCommonMap
|
||||
*/
|
||||
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
|
||||
const x = longestCommonSubsequence(uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi));
|
||||
|
||||
if (x.length === 0) {
|
||||
addSubMatch(aLo, aHi, bLo, bHi);
|
||||
} else {
|
||||
if (aLo < x[0].indexA || bLo < x[0].indexB) {
|
||||
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
|
||||
}
|
||||
|
||||
let i;
|
||||
for (i = 0; i < x.length - 1; i++) {
|
||||
addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1);
|
||||
}
|
||||
|
||||
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
|
||||
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
|
||||
|
||||
if (diffPlusFlag) {
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
aMove: aMove,
|
||||
aMoveIndex: aMoveIndex,
|
||||
bMove: bMove,
|
||||
bMoveIndex: bMoveIndex,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* use: patienceDiffPlus( textOld[], textNew[] )
|
||||
*
|
||||
* where:
|
||||
* textOld[] contains the original text lines.
|
||||
* textNew[] contains the new text lines.
|
||||
*
|
||||
* returns an object with the following properties:
|
||||
* lines[] with properties of:
|
||||
* line containing the line of text from textOld or textNew.
|
||||
* aIndex referencing the index in aLine[].
|
||||
* bIndex referencing the index in textNew[].
|
||||
* (Note: The line is text from either textOld or textNew, with aIndex and bIndex
|
||||
* referencing the original index. If aIndex === -1 then the line is new from textNew,
|
||||
* and if bIndex === -1 then the line is old from textOld.)
|
||||
* moved is true if the line was moved from elsewhere in textOld[] or textNew[].
|
||||
* lineCountDeleted is the number of lines from textOld[] not appearing in textNew[].
|
||||
* lineCountInserted is the number of lines from textNew[] not appearing in textOld[].
|
||||
* lineCountMoved is the number of lines that moved.
|
||||
*/
|
||||
|
||||
function patienceDiffPlus(textOld: string[], textNew: string[]) {
|
||||
|
||||
const difference = patienceDiff(textOld, textNew, true);
|
||||
|
||||
let aMoveNext = difference.aMove;
|
||||
let aMoveIndexNext = difference.aMoveIndex;
|
||||
let bMoveNext = difference.bMove;
|
||||
let bMoveIndexNext = difference.bMoveIndex;
|
||||
|
||||
delete difference.aMove;
|
||||
delete difference.aMoveIndex;
|
||||
delete difference.bMove;
|
||||
delete difference.bMoveIndex;
|
||||
|
||||
let lastLineCountMoved;
|
||||
|
||||
do {
|
||||
const aMove = aMoveNext;
|
||||
const aMoveIndex = aMoveIndexNext;
|
||||
const bMove = bMoveNext;
|
||||
const bMoveIndex = bMoveIndexNext;
|
||||
|
||||
aMoveNext = [];
|
||||
aMoveIndexNext = [];
|
||||
bMoveNext = [];
|
||||
bMoveIndexNext = [];
|
||||
|
||||
const subDiff = patienceDiff(aMove!, bMove!);
|
||||
|
||||
lastLineCountMoved = difference.lineCountMoved;
|
||||
|
||||
subDiff.lines.forEach((v, i) => {
|
||||
|
||||
if (0 <= v.aIndex && 0 <= v.bIndex) {
|
||||
|
||||
difference.lines[aMoveIndex![v.aIndex]].moved = true;
|
||||
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
|
||||
difference.lines[bMoveIndex![v.bIndex]].moved = true;
|
||||
difference.lineCountInserted--;
|
||||
difference.lineCountDeleted--;
|
||||
difference.lineCountMoved++;
|
||||
} else if (v.bIndex < 0) {
|
||||
aMoveNext!.push(aMove![v.aIndex]);
|
||||
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
|
||||
} else {
|
||||
bMoveNext!.push(bMove![v.bIndex]);
|
||||
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
|
||||
}
|
||||
});
|
||||
} while (0 < difference.lineCountMoved - lastLineCountMoved);
|
||||
|
||||
return difference;
|
||||
|
||||
}
|
||||
|
||||
export default patienceDiff;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Time since a certain date
|
||||
* @param {Date} date - the timestamp got which we want to understand how long ago it happened
|
||||
* @returns {String} text - how much time has passed since a certain timestamp
|
||||
*/
|
||||
function timeSince(date: Date) {
|
||||
const seconds = Math.floor(
|
||||
((new Date() as any) - (date as any)) / 1000
|
||||
) as number;
|
||||
|
||||
let interval = seconds / 31536000;
|
||||
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' years ago';
|
||||
}
|
||||
interval = seconds / 2592000;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' months ago';
|
||||
}
|
||||
interval = seconds / 86400;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' days ago';
|
||||
}
|
||||
interval = seconds / 3600;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' hours ago';
|
||||
}
|
||||
interval = seconds / 60;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' minutes ago';
|
||||
}
|
||||
return Math.floor(seconds) + ' seconds ago';
|
||||
}
|
||||
|
||||
export default timeSince;
|
||||
Reference in New Issue
Block a user