Improved frontend for activity logs

This commit is contained in:
Vladyslav Matsiiako
2023-01-01 18:27:31 -08:00
parent 4576e8f6a7
commit 0167342722
12 changed files with 757 additions and 300 deletions

View File

@ -49,6 +49,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
filters.workspace = workspaceId;
logs = await Log.find(filters)
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit)
.populate('actions')

View File

@ -1,60 +1,50 @@
import React from 'react';
import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import {
faAngleDown,
faCheck,
faDownload,
faEye,
faPlus,
faUpload,
faShuffle,
faX
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Listbox, Transition } from '@headlessui/react';
import guidGenerator from '../utilities/randomId';
import Button from './buttons/Button';
interface ListBoxProps {
selected: string;
select: (event: string) => void;
data: string[];
text?: string;
buttonAction?: () => void;
isFull?: boolean;
}
const eventOptions = [
{
name: 'Secrets Pushed',
icon: faUpload
name: 'addSecrets',
icon: faPlus
},
{
name: 'Secrets Pulled',
icon: faDownload
name: 'readSecrets',
icon: faEye
},
{
name: 'updateSecrets',
icon: faShuffle
}
];
/**
* This is the component that we use for drop down lists.
* This is the component that we use for the event picker in the activity logs tab.
* @param {object} obj
* @param {string} obj.selected - the item that is currently selected
* @param {function} obj.select - what happends if you select the item inside a list
* @param {string[]} obj.data - all the options available
* @param {string} obj.text - the text that shows us in front of the select option
* @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button
* @param {string} obj.width - button width
* @returns
* @param {string} obj.selected - the event that is currently selected
* @param {function} obj.select - an action that happens when an item is selected
*/
export default function EventFilter({
selected,
select,
data,
text,
buttonAction,
isFull
select
}: ListBoxProps): JSX.Element {
const { t } = useTranslation();
return (
<Listbox value={selected} onChange={select}>
<Listbox value={t("activity:event." + selected)} onChange={select}>
<div className="relative">
<Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm">
{selected != '' ? (
@ -84,9 +74,9 @@ export default function EventFilter({
<Listbox.Option
key={id}
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
selected == event.name && 'bg-mineshaft-700'
selected == t("activity:event." + event.name) && 'bg-mineshaft-700'
}`}
value={event.name}
value={t("activity:event." + event.name)}
>
{({ selected }) => (
<>
@ -96,7 +86,7 @@ export default function EventFilter({
}`}
>
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
{event.name}
{t("activity:event." + event.name)}
</span>
</>
)}

View File

@ -1,141 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import {
faAngleDown,
faAngleRight,
faX
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../../utilities/randomId';
interface ActivityTableProps {
eventName: string;
user: string;
source: string;
time: Date;
}
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';
}
const ActivityLogsRow = ({ row }: { row: ActivityTableProps }): JSX.Element => {
const [payloadOpened, setPayloadOpened] = useState(false);
return (
<>
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full">
<div
onClick={() => setPayloadOpened(!payloadOpened)}
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
>
<FontAwesomeIcon
icon={payloadOpened ? faAngleDown : faAngleRight}
className={`mt-3 ml-6 text-bunker-100 hover:bg-primary-100/[0.15] ${
payloadOpened && 'bg-primary-100/10'
} p-1 duration-100 h-4 w-4 rounded-md`}
/>
</div>
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
{row.eventName}
</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.source}
</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
{timeSince(row.time)}
</td>
{/* <td className="py-2 border-mineshaft-700 border-t">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button
onButtonPressed={() => {}}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</td> */}
</tr>
{payloadOpened && (
<tr className="w-full h-10 bg-bunker-700 text-bunker-200 col-span-2">
<td colSpan={5} className="">
<div className="flex flex-row ml-12 py-2 border-mineshaft-700 border-t">
<div className="w-96">Timestamp</div>
<div className="w-96">2022-12-16T04:02:44.517Z</div>
</div>
<div className="flex flex-row ml-12 py-2 border-mineshaft-700 border-t">
<div className="w-96">Number of Secrets</div>
<div className="w-96">32</div>
</div>
<div className="flex flex-row ml-12 py-2 border-mineshaft-700 border-t">
<div className="w-96">IP Address</div>
<div className="w-96">159.223.164.24</div>
</div>
</td>
</tr>
)}
</>
);
};
/**
* This is the table for activity logs (one of the tabs)
* @param {*} props
* @returns
*/
const ActivityTable = ({ data }: { data: ActivityTableProps[] }) => {
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-[3.15rem] bg-white/5"></div>
<table className="w-full my-1">
<thead className="text-bunker-300">
<tr>
<th className="text-left pl-6 pt-2.5 pb-3"></th>
<th className="text-left pt-2.5 pb-3">Event</th>
<th className="text-left pl-6 pt-2.5 pb-3">User</th>
<th className="text-left pl-6 pt-2.5 pb-3">Source</th>
<th className="text-left pl-6 pt-2.5 pb-3">Time</th>
<th></th>
</tr>
</thead>
<tbody>
{data.map((row, index) => {
return <ActivityLogsRow key={index} row={row} />;
})}
</tbody>
</table>
</div>
</div>
);
};
export default ActivityTable;

View File

@ -0,0 +1,43 @@
import SecurityClient from '~/utilities/SecurityClient';
interface workspaceProps {
workspaceId: string;
offset: number;
limit: number;
filters: object;
}
/**
* 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.filters
* @returns
*/
const getProjectLogs = async ({ workspaceId, offset, limit, filters }: workspaceProps) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/logs?' +
new URLSearchParams({
offset: String(offset),
limit: String(limit),
filters: JSON.stringify(filters)
}),
{
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;

View File

@ -17,7 +17,7 @@ interface secretVersionProps {
*/
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => {
return SecurityClient.fetchCall(
'/api/v1/secret/' + secretId + '/secret-versions?'+
'/api/v1/secret/' + secretId + '/secret-versions?' +
new URLSearchParams({
offset: String(offset),
limit: String(limit)
@ -32,7 +32,7 @@ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps
if (res && res.status == 200) {
return await res.json();
} else {
console.log('Failed to get project secrets');
console.log('Failed to get secret version history');
}
});
};

View File

@ -0,0 +1,80 @@
import { useTranslation } from "next-i18next";
import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import patienceDiff from 'ee/utilities/findTextDifferences';
import DashboardInputField from '../../components/dashboard/DashboardInputField';
const secretChanges = [{
"oldSecret": "secret1",
"newSecret": "ecret2"
}, {
"oldSecret": "secret1",
"newSecret": "sercet2"
}, {
"oldSecret": "localhosta:8080",
"newSecret": "aaaalocalhoats:3000"
}]
interface SideBarProps {
toggleSidebar: (value: string[]) => void;
sidebarData: string[];
currentEvent: string;
}
/**
* @param {object} obj
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {string[]} obj.secretIds - data of payload
* @param {string} obj.currentEvent - the event name for which a sidebar is being displayed
* @returns the sidebar with the payload of user activity logs
*/
const ActivitySideBar = ({
toggleSidebar,
sidebarData,
currentEvent
}: SideBarProps) => {
const { t } = useTranslation();
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
<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." + currentEvent)}</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'>
{currentEvent == 'readSecrets' && sidebarData.map((item, id) =>
<>
<div className='text-sm text-bunker-200 mt-4 pl-1'>Key {id}</div>
<DashboardInputField
key={id}
onChangeHandler={() => {}}
type="varName"
position={1}
value={"a" + item}
isDuplicate={false}
blurred={false}
/>
</>
)}
{currentEvent == 'updateSecrets' && sidebarData.map((item, id) =>
secretChanges.map(secretChange =>
<>
<div className='text-sm text-bunker-200 mt-4 pl-1'>Secret Name {id}</div>
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
<div className='bg-red/30 px-2'>- {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
<div className='bg-green-500/30 px-2'>+ {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split('')).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
</div>
</>
))}
</div>
</div>
</div>
};
export default ActivitySideBar;

View File

@ -0,0 +1,132 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next";
import {
faAngleDown,
faAngleRight,
faUpRightFromSquare,
faX
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import timeSince from 'ee/utilities/timeSince';
import guidGenerator from '../../components/utilities/randomId';
interface PayloadProps {
name: string;
secretVersions: string[];
}
interface logData {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: string;
payload: PayloadProps[];
}
/**
*
* @param obj
* @param {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened
* @returns
*/
const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData, toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => {
const [payloadOpened, setPayloadOpened] = useState(false);
const { t } = useTranslation();
return (
<>
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full">
<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-3 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'>
<td></td>
<td>Timestamp</td>
<td>{row.createdAt}</td>
</tr>}
{payloadOpened &&
row.payload?.map((action, index) =>
<tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t">
<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.secretVersions);
setCurrentEvent(action.name);
}}>
{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'>
<td></td>
<td>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 {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened
* @returns
*/
const ActivityTable = ({ data, toggleSidebar, setCurrentEvent }: { data: logData[], toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => {
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-[3.15rem] bg-white/5"></div>
<table className="w-full my-1">
<thead className="text-bunker-300">
<tr>
<th className="text-left pl-6 pt-2.5 pb-3"></th>
<th className="text-left pt-2.5 pb-3">Event</th>
<th className="text-left pl-6 pt-2.5 pb-3">User</th>
<th className="text-left pl-6 pt-2.5 pb-3">Source</th>
<th className="text-left pl-6 pt-2.5 pb-3">Time</th>
<th></th>
</tr>
</thead>
<tbody>
{data?.map((row, index) => {
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} setCurrentEvent={setCurrentEvent} />;
})}
</tbody>
</table>
</div>
</div>
);
};
export default ActivityTable;

View File

@ -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;

View File

@ -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;

View File

@ -1,137 +1,90 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next";
import ActivitySideBar from 'ee/components/ActivitySideBar';
import Button from '~/components/basic/buttons/Button';
import EventFilter from '~/components/basic/EventFilter';
import ActivityTable from '~/components/basic/table/ActivityTable';
import NavHeader from '~/components/navigation/NavHeader';
import onboardingCheck from '~/components/utilities/checks/OnboardingCheck';
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
const data = [
{
eventName: 'Secrets Pulled',
user: 'matsiiako@gmail.com',
source: 'CLI',
time: new Date()
},
{
eventName: 'Secrets Pushed',
user: 'matsiiako@gmail.com',
source: 'Web',
time: new Date()
},
{
eventName: 'Secrets Pulled',
user: 'matsiiako@gmail.com',
source: 'CLI',
time: new Date()
},
{
eventName: 'Secrets Pushed',
user: 'matsiiako@gmail.com',
source: 'Web',
time: new Date()
},
{
eventName: 'Secrets Pulled',
user: 'matsiiako@gmail.com',
source: 'CLI',
time: new Date()
},
{
eventName: 'Secrets Pushed',
user: 'matsiiako@gmail.com',
source: 'Web',
time: new Date()
},
{
eventName: 'Secrets Pulled',
user: 'matsiiako@gmail.com',
source: 'CLI',
time: new Date()
},
{
eventName: 'Secrets Pushed',
user: 'matsiiako@gmail.com',
source: 'Web',
time: new Date()
},
{
eventName: 'Secrets Pulled',
user: 'matsiiako@gmail.com',
source: 'CLI',
time: new Date()
},
{
eventName: 'Secrets Pushed',
user: 'matsiiako@gmail.com',
source: 'Web',
time: new Date()
},
{
eventName: 'Secrets Pulled',
user: 'matsiiako@gmail.com',
source: 'CLI',
time: new Date()
},
{
eventName: 'Secrets Pushed',
user: 'matsiiako@gmail.com',
source: 'Web',
time: new Date()
},
{
eventName: 'Secrets Pulled',
user: 'matsiiako@gmail.com',
source: 'CLI',
time: new Date()
},
{
eventName: 'Secrets Pushed',
user: 'matsiiako@gmail.com',
source: 'Web',
time: new Date()
},
{
eventName: 'Secrets Pulled',
user: 'matsiiako@gmail.com',
source: 'CLI',
time: new Date()
},
{
eventName: 'Secrets Pushed',
user: 'matsiiako@gmail.com',
source: 'Web',
time: new Date()
}
];
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
import ActivityTable from '../../ee/components/ActivityTable';
interface logData {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: {
email: string;
};
actions: {
name: string;
payload: {
secretVersions: string[];
}
}[]
}
interface PayloadProps {
name: string;
secretVersions: string[];
}
interface logDataPoint {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: string;
payload: PayloadProps[];
}
/**
* This tab is called Home because in the future it will include some company news,
* updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction
* for the new users
* This is the tab that includes all of the user activity logs
*/
export default function Activity() {
const router = useRouter();
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
const [hasUserStarred, setHasUserStarred] = useState(false);
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
const [usersInOrg, setUsersInOrg] = useState(false);
const [eventChosen, setEventChosen] = useState('');
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
const [currentOffset, setCurrentOffset] = useState(0);
const currentLimit = 10;
const [sidebarData, toggleSidebar] = useState<string[]>([])
const [currentEvent, setCurrentEvent] = useState("");
const { t } = useTranslation();
useEffect(() => {
onboardingCheck({
setHasUserClickedIntro,
setHasUserClickedSlack,
setHasUserPushedSecrets,
setHasUserStarred,
setUsersInOrg
});
}, []);
const getLogData = async () => {
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, filters: {} })
setLogsData(logsData.concat(tempLogsData.map((log: logData) => {
return {
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log.user.email,
payload: log.actions.map(action => {
return {
name: action.name,
secretVersions: action.payload.secretVersions
}
})
}
})))
}
getLogData();
}, [currentLimit, currentOffset]);
const loadMoreLogs = () => {
setCurrentOffset(currentOffset + currentLimit);
}
return (
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
<NavHeader pageName="Project Activity" isProjectRelated={true} />
{sidebarData.length > 0 && <ActivitySideBar sidebarData={sidebarData} toggleSidebar={toggleSidebar} currentEvent={currentEvent} />}
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
<div className="flex flex-row justify-start items-center text-3xl">
<p className="font-semibold mr-4 text-bunker-100">Activity Logs</p>
@ -140,22 +93,32 @@ export default function Activity() {
Event history limited to the last 12 months.
</p>
</div>
{/* Licence Required
<div className="px-6 h-8 mt-2">
<EventFilter
selected={eventChosen}
select={setEventChosen}
data={["Secrets Pulled", "Secrets Pushed"]}
data={["readSecrets", "updateSecrets", "addSecrets"]}
isFull={false}
/>
</div> */}
</div>
<ActivityTable
data={data.filter((event) =>
eventChosen != '' ? event.eventName == eventChosen : event
)}
data={logsData!.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.filter((log) =>
eventChosen != '' ? log.payload?.map(action => t("activity:event." + action.name)).includes(eventChosen) : true
)
}
toggleSidebar={toggleSidebar}
setCurrentEvent={setCurrentEvent}
/>
<div className='flex justify-center w-full mb-6'>
<div className='items-center w-60'>
<Button text="View More" textDisabled="End of History" active={logsData.length % 10 == 0 ? true : false} onButtonPressed={loadMoreLogs} size="md" color="mineshaft"/>
</div>
</div>
</div>
);
}
Activity.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(["activity"]);

View File

@ -0,0 +1,7 @@
{
"event": {
"readSecrets": "Secrets Viewed",
"updateSecrets": "Secrets Updated",
"addSecrets": "Secrets Added"
}
}

View File

@ -1238,6 +1238,7 @@ module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./ee/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
@ -1382,12 +1383,12 @@ module.exports = {
"0%": {
transform: "scale(0.2)",
opacity: 0,
transform: "translateY(120%)",
// transform: "translateY(120%)",
},
"100%": {
transform: "scale(1)",
opacity: 1,
transform: "translateY(100%)",
// transform: "translateY(100%)",
},
},
popright: {
@ -1410,12 +1411,12 @@ module.exports = {
"0%": {
transform: "scale(0.2)",
opacity: 0,
transform: "translateY(80%)",
// transform: "translateY(80%)",
},
"100%": {
transform: "scale(1)",
opacity: 1,
transform: "translateY(100%)",
// transform: "translateY(100%)",
},
},
},