diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index cff7f4a98..2f8020ef0 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -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') diff --git a/frontend/components/basic/EventFilter.tsx b/frontend/components/basic/EventFilter.tsx index 5a9506f0e..d4fe36193 100644 --- a/frontend/components/basic/EventFilter.tsx +++ b/frontend/components/basic/EventFilter.tsx @@ -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 ( - +
{selected != '' ? ( @@ -84,9 +74,9 @@ export default function EventFilter({ {({ selected }) => ( <> @@ -96,7 +86,7 @@ export default function EventFilter({ }`} > {' '} - {event.name} + {t("activity:event." + event.name)} )} diff --git a/frontend/components/basic/table/ActivityTable.tsx b/frontend/components/basic/table/ActivityTable.tsx deleted file mode 100644 index 0f5f3eb6b..000000000 --- a/frontend/components/basic/table/ActivityTable.tsx +++ /dev/null @@ -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 ( - <> - -
setPayloadOpened(!payloadOpened)} - className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer" - > - -
- - {row.eventName} - - - {row.user} - - - {row.source} - - - {timeSince(row.time)} - - {/* -
-
- */} - - {payloadOpened && ( - - -
-
Timestamp
-
2022-12-16T04:02:44.517Z
-
-
-
Number of Secrets
-
32
-
-
-
IP Address
-
159.223.164.24
-
- - - )} - - ); -}; - -/** - * This is the table for activity logs (one of the tabs) - * @param {*} props - * @returns - */ -const ActivityTable = ({ data }: { data: ActivityTableProps[] }) => { - return ( -
-
-
- - - - - - - - - - - - - {data.map((row, index) => { - return ; - })} - -
EventUserSourceTime
-
-
- ); -}; - -export default ActivityTable; diff --git a/frontend/ee/api/secrets/GetProjectLogs.ts b/frontend/ee/api/secrets/GetProjectLogs.ts new file mode 100644 index 000000000..d5324355c --- /dev/null +++ b/frontend/ee/api/secrets/GetProjectLogs.ts @@ -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; diff --git a/frontend/ee/api/secrets/GetSecretVersions.ts b/frontend/ee/api/secrets/GetSecretVersions.ts index 8185234e6..7b3840399 100644 --- a/frontend/ee/api/secrets/GetSecretVersions.ts +++ b/frontend/ee/api/secrets/GetSecretVersions.ts @@ -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'); } }); }; diff --git a/frontend/ee/components/ActivitySideBar.tsx b/frontend/ee/components/ActivitySideBar.tsx new file mode 100644 index 000000000..d96a3f6ab --- /dev/null +++ b/frontend/ee/components/ActivitySideBar.tsx @@ -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
+
+
+

{t("activity:event." + currentEvent)}

+
toggleSidebar([])}> + +
+
+
+ {currentEvent == 'readSecrets' && sidebarData.map((item, id) => + <> +
Key {id}
+ {}} + type="varName" + position={1} + value={"a" + item} + isDuplicate={false} + blurred={false} + /> + + )} + {currentEvent == 'updateSecrets' && sidebarData.map((item, id) => + secretChanges.map(secretChange => + <> +
Secret Name {id}
+
+
- {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split(''), false).lines.map((character, id) => character.aIndex != -1 && {character.line})}
+
+ {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split('')).lines.map((character, id) => character.bIndex != -1 && {character.line})}
+
+ + ))} +
+
+ +
+}; + +export default ActivitySideBar; diff --git a/frontend/ee/components/ActivityTable.tsx b/frontend/ee/components/ActivityTable.tsx new file mode 100644 index 000000000..a25920202 --- /dev/null +++ b/frontend/ee/components/ActivityTable.tsx @@ -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 ( + <> + + setPayloadOpened(!payloadOpened)} + className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer" + > + + + + {row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")} + + + {row.user} + + + {row.channel} + + + {timeSince(new Date(row.createdAt))} + + + {payloadOpened && + + + Timestamp + {row.createdAt} + } + {payloadOpened && + row.payload?.map((action, index) => + + + {t("activity:event." + action.name)} + { + toggleSidebar(action.secretVersions); + setCurrentEvent(action.name); + }}> + {action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")} + + + )} + {payloadOpened && + + + IP Address + {row.ipAddress} + } + + ); +}; + +/** + * 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 ( +
+
+
+ + + + + + + + + + + + + {data?.map((row, index) => { + return ; + })} + +
EventUserSourceTime
+
+
+ ); +}; + +export default ActivityTable; diff --git a/frontend/ee/utilities/findTextDifferences.ts b/frontend/ee/utilities/findTextDifferences.ts new file mode 100644 index 000000000..ffd847a77 --- /dev/null +++ b/frontend/ee/utilities/findTextDifferences.ts @@ -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) { + 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; diff --git a/frontend/ee/utilities/timeSince.ts b/frontend/ee/utilities/timeSince.ts new file mode 100644 index 000000000..b79249967 --- /dev/null +++ b/frontend/ee/utilities/timeSince.ts @@ -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; diff --git a/frontend/pages/activity/[id].tsx b/frontend/pages/activity/[id].tsx index 9b51d7f74..f384ae69f 100644 --- a/frontend/pages/activity/[id].tsx +++ b/frontend/pages/activity/[id].tsx @@ -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([]); + const [currentOffset, setCurrentOffset] = useState(0); + const currentLimit = 10; + const [sidebarData, toggleSidebar] = useState([]) + 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 (
+ {sidebarData.length > 0 && }

Activity Logs

@@ -140,22 +93,32 @@ export default function Activity() { Event history limited to the last 12 months.

- {/* Licence Required
-
*/} +
- 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} /> +
+
+
+
); } Activity.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(["activity"]); \ No newline at end of file diff --git a/frontend/public/locales/en/activity.json b/frontend/public/locales/en/activity.json new file mode 100644 index 000000000..b84d570ae --- /dev/null +++ b/frontend/public/locales/en/activity.json @@ -0,0 +1,7 @@ +{ + "event": { + "readSecrets": "Secrets Viewed", + "updateSecrets": "Secrets Updated", + "addSecrets": "Secrets Added" + } +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 6f3d038cf..81e97e6ed 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -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%)", }, }, },