diff --git a/frontend/components/basic/buttons/Button.tsx b/frontend/components/basic/buttons/Button.tsx
index 5fe2db803f..9f8d481b4f 100644
--- a/frontend/components/basic/buttons/Button.tsx
+++ b/frontend/components/basic/buttons/Button.tsx
@@ -12,7 +12,7 @@ type ButtonProps = {
text: string;
onButtonPressed: () => void;
loading?: boolean;
- color: string;
+ color?: string;
size: string;
icon?: IconProp;
active?: boolean;
diff --git a/frontend/components/context/Notifications/Notification.tsx b/frontend/components/context/Notifications/Notification.tsx
new file mode 100644
index 0000000000..974843cf6f
--- /dev/null
+++ b/frontend/components/context/Notifications/Notification.tsx
@@ -0,0 +1,38 @@
+import { faXmarkCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import classnames from "classnames";
+
+import { Notification as NotificationType } from "./NotificationProvider";
+
+interface NotificationProps {
+ notification: NotificationType;
+ clearNotification: (text?: string) => void;
+}
+
+const Notification = ({
+ notification,
+ clearNotification,
+}: NotificationProps) => {
+ return (
+
+
{notification.text}
+
+
+ );
+};
+
+export default Notification;
diff --git a/frontend/components/context/Notifications/NotificationProvider.tsx b/frontend/components/context/Notifications/NotificationProvider.tsx
new file mode 100644
index 0000000000..723319615c
--- /dev/null
+++ b/frontend/components/context/Notifications/NotificationProvider.tsx
@@ -0,0 +1,64 @@
+import { createContext, ReactNode, useContext, useState } from "react";
+
+import Notifications from "./Notifications";
+
+type NotificationType = "success" | "error";
+
+export type Notification = {
+ text: string;
+ type: NotificationType;
+};
+
+type NotificationContextState = {
+ createNotification: ({ text, type }: Notification) => void;
+};
+
+const NotificationContext = createContext
({
+ createNotification: () => console.log("createNotification not set!"),
+});
+
+export const useNotificationContext = () => useContext(NotificationContext);
+
+interface NotificationProviderProps {
+ children: ReactNode;
+}
+
+const NotificationProvider = ({ children }: NotificationProviderProps) => {
+ const [notifications, setNotifications] = useState([]);
+
+ const clearNotification = (text?: string) => {
+ if (text) {
+ return setNotifications((state) =>
+ state.filter((notif) => notif.text !== text)
+ );
+ }
+
+ return setNotifications([]);
+ };
+
+ const createNotification = ({ text, type = "success" }: Notification) => {
+ const doesNotifExist = notifications.some((notif) => notif.text === text);
+
+ if (doesNotifExist) {
+ return;
+ }
+
+ return setNotifications((state) => [...state, { text, type }]);
+ };
+
+ return (
+
+
+ {children}
+
+ );
+};
+
+export default NotificationProvider;
diff --git a/frontend/components/context/Notifications/Notifications.tsx b/frontend/components/context/Notifications/Notifications.tsx
new file mode 100644
index 0000000000..856331e24b
--- /dev/null
+++ b/frontend/components/context/Notifications/Notifications.tsx
@@ -0,0 +1,28 @@
+import Notification from "./Notification";
+import { Notification as NotificationType } from "./NotificationProvider";
+
+interface NoticationsProps {
+ notifications: NotificationType[];
+ clearNotification: (text?: string) => void;
+}
+
+const Notifications = ({
+ notifications,
+ clearNotification,
+}: NoticationsProps) => {
+ return (
+
+
+ {notifications.map((notif) => (
+
+ ))}
+
+
+ );
+};
+
+export default Notifications;
diff --git a/frontend/components/utilities/SecurityClient.js b/frontend/components/utilities/SecurityClient.js
index f8be6a0e19..78eebdebef 100644
--- a/frontend/components/utilities/SecurityClient.js
+++ b/frontend/components/utilities/SecurityClient.js
@@ -3,7 +3,7 @@ import token from "~/pages/api/auth/Token";
export default class SecurityClient {
static #token = "";
- contructor() {}
+ constructor() {}
static setToken(token) {
this.#token = token;
diff --git a/frontend/components/utilities/attemptLogin.js b/frontend/components/utilities/attemptLogin.js
index 4c6852625c..7a8cbf96fb 100644
--- a/frontend/components/utilities/attemptLogin.js
+++ b/frontend/components/utilities/attemptLogin.js
@@ -7,6 +7,7 @@ import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProj
import { initPostHog } from "../analytics/posthog";
import pushKeys from "./secrets/pushKeys";
import { ENV } from "./config";
+import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import SecurityClient from "./SecurityClient";
const nacl = require("tweetnacl");
@@ -32,7 +33,6 @@ const attemptLogin = async (
isLogin
) => {
try {
- let userWorkspace, userOrg;
client.init(
{
username: email,
@@ -41,66 +41,38 @@ const attemptLogin = async (
async () => {
const clientPublicKey = client.getPublicKey();
- let serverPublicKey, salt;
- try {
- let res = await login1(email, clientPublicKey);
- res = await res.json();
- serverPublicKey = res.serverPublicKey;
- salt = res.salt;
- } catch (err) {
- setErrorLogin(true);
- console.log("Wrong password", err);
- }
+ const { serverPublicKey, salt } = await login1(email, clientPublicKey);
- let response;
try {
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1
- response = await login2(email, clientProof);
- } catch (err) {
- setErrorLogin(true);
- console.log("Password verification failed");
- }
- // if everything works, go the main dashboard page.
- try {
- if (response.status == "200") {
- response = await response.json();
- SecurityClient.setToken(response["token"]);
- const publicKey = response["publicKey"];
- const encryptedPrivateKey = response["encryptedPrivateKey"];
- const iv = response["iv"];
- const tag = response["tag"];
+ // if everything works, go the main dashboard page.
+ const { token, publicKey, encryptedPrivateKey, iv, tag } =
+ await login2(email, clientProof);
+ SecurityClient.setToken(token);
- const PRIVATE_KEY = Aes256Gcm.decrypt(
- encryptedPrivateKey,
- iv,
- tag,
- password
- .slice(0, 32)
- .padStart(
- 32 +
- (password.slice(0, 32).length - new Blob([password]).size),
- "0"
- )
- );
+ const privateKey = Aes256Gcm.decrypt(
+ encryptedPrivateKey,
+ iv,
+ tag,
+ password
+ .slice(0, 32)
+ .padStart(
+ 32 + (password.slice(0, 32).length - new Blob([password]).size),
+ "0"
+ )
+ );
- try {
- localStorage.setItem("publicKey", publicKey);
- localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
- localStorage.setItem("iv", iv);
- localStorage.setItem("tag", tag);
- localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY);
- } catch (err) {
- setErrorLogin(true);
- console.error(
- "Unable to send the tokens in local storage:" + err.message
- );
- }
- } else {
- setErrorLogin(true);
- }
+ saveTokenToLocalStorage({
+ token,
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ privateKey,
+ });
const userOrgs = await getOrganizations();
const userOrgsData = userOrgs.map((org) => org._id);
@@ -140,14 +112,8 @@ const attemptLogin = async (
"mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
"personal",
],
- DB_USERNAME: [
- "user1234",
- "personal",
- ],
- DB_PASSWORD: [
- "ah8jak3hk8dhiu4dw7whxwe1l",
- "personal",
- ],
+ DB_USERNAME: ["user1234", "personal"],
+ DB_PASSWORD: ["ah8jak3hk8dhiu4dw7whxwe1l", "personal"],
TWILIO_AUTH_TOKEN: [
"hgSIwDAKvz8PJfkj6xkzYqzGmAP3HLuG",
"shared",
@@ -156,7 +122,7 @@ const attemptLogin = async (
STRIPE_SECRET_KEY: ["sk_test_7348oyho4hfq398HIUOH78", "shared"],
},
workspaceId: projectToLogin,
- env: "Development"
+ env: "Development",
});
}
try {
diff --git a/frontend/components/utilities/checks/PasswordCheck.js b/frontend/components/utilities/checks/PasswordCheck.ts
similarity index 83%
rename from frontend/components/utilities/checks/PasswordCheck.js
rename to frontend/components/utilities/checks/PasswordCheck.ts
index 448c2942e4..10240b7476 100644
--- a/frontend/components/utilities/checks/PasswordCheck.js
+++ b/frontend/components/utilities/checks/PasswordCheck.ts
@@ -1,18 +1,21 @@
+interface PasswordCheckProps {
+ password: string;
+ currentErrorCheck: boolean;
+ setPasswordErrorLength: (value: boolean) => void;
+ setPasswordErrorNumber: (value: boolean) => void;
+ setPasswordErrorLowerCase: (value: boolean) => void;
+}
+
/**
* This function checks a user password with respect to some criteria.
- * @param {*} password
- * @param {*} setPasswordError
- * @param {*} setPasswordErrorMessage
- * @param {*} currentErrorCheck
- * @returns
*/
-const passwordCheck = (
+const passwordCheck = ({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
- currentErrorCheck
-) => {
+ currentErrorCheck,
+}: PasswordCheckProps) => {
let errorCheck = currentErrorCheck;
if (!password || password.length < 14) {
setPasswordErrorLength(true);
diff --git a/frontend/components/utilities/checks/tempLocalStorage.ts b/frontend/components/utilities/checks/tempLocalStorage.ts
new file mode 100644
index 0000000000..f24d53aeb3
--- /dev/null
+++ b/frontend/components/utilities/checks/tempLocalStorage.ts
@@ -0,0 +1,11 @@
+// this is temporary util function. create error handling logic for localStorage and delete this.
+export const tempLocalStorage = (key: string) => {
+ const value = localStorage.getItem(key);
+
+ if (value === null || value === "") {
+ console.warn("No value found in localStorage for key");
+ return "";
+ }
+
+ return value;
+};
diff --git a/frontend/components/utilities/saveTokenToLocalStorage.ts b/frontend/components/utilities/saveTokenToLocalStorage.ts
new file mode 100644
index 0000000000..13e50b2c47
--- /dev/null
+++ b/frontend/components/utilities/saveTokenToLocalStorage.ts
@@ -0,0 +1,29 @@
+interface Props {
+ publicKey: string;
+ encryptedPrivateKey: string;
+ iv: string;
+ tag: string;
+ privateTag: string;
+}
+
+export const saveTokenToLocalStorage = ({
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ privateTag,
+}: Props) => {
+ try {
+ localStorage.setItem("publicKey", publicKey);
+ localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
+ localStorage.setItem("iv", iv);
+ localStorage.setItem("tag", tag);
+ localStorage.setItem("PRIVATE_KEY", privateTag);
+ } catch (err) {
+ if (err instanceof Error) {
+ throw new Error(
+ "Unable to send the tokens in local storage:" + err.message
+ );
+ }
+ }
+};
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 38dd5afd02..cc83e73951 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -51,6 +51,7 @@
"devDependencies": {
"@tailwindcss/typography": "^0.5.4",
"@types/node": "18.11.9",
+ "@types/react": "^18.0.26",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.7",
@@ -1143,9 +1144,9 @@
"optional": true
},
"node_modules/@types/react": {
- "version": "18.0.15",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz",
- "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==",
+ "version": "18.0.26",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
+ "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -7978,9 +7979,9 @@
"optional": true
},
"@types/react": {
- "version": "18.0.15",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz",
- "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==",
+ "version": "18.0.26",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
+ "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
diff --git a/frontend/package.json b/frontend/package.json
index 257245d4e4..ec1157e673 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -54,6 +54,7 @@
"devDependencies": {
"@tailwindcss/typography": "^0.5.4",
"@types/node": "18.11.9",
+ "@types/react": "^18.0.26",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.7",
diff --git a/frontend/pages/_app.js b/frontend/pages/_app.js
index 6ed9af0131..907bbc9413 100644
--- a/frontend/pages/_app.js
+++ b/frontend/pages/_app.js
@@ -4,6 +4,7 @@ import { config } from "@fortawesome/fontawesome-svg-core";
import { initPostHog } from "~/components/analytics/posthog";
import Layout from "~/components/basic/layout";
+import NotificationProvider from "~/components/context/Notifications/NotificationProvider";
import RouteGuard from "~/components/RouteGuard";
import { publicPaths } from "~/const";
import { ENV } from "~/utilities/config";
@@ -46,9 +47,11 @@ const App = ({ Component, pageProps, ...appProps }) => {
return (
-
-
-
+
+
+
+
+
);
};
diff --git a/frontend/pages/api/auth/Login1.js b/frontend/pages/api/auth/Login1.js
deleted file mode 100644
index 3f3493f77c..0000000000
--- a/frontend/pages/api/auth/Login1.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * This is the first step of the login process (pake)
- * @param {*} email
- * @param {*} clientPublicKey
- * @returns
- */
-const login1 = (email, clientPublicKey) => {
- return fetch("/api/v1/auth/login1", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- email: email,
- clientPublicKey,
- }),
- });
-};
-
-export default login1;
diff --git a/frontend/pages/api/auth/Login1.ts b/frontend/pages/api/auth/Login1.ts
new file mode 100644
index 0000000000..818a2d1572
--- /dev/null
+++ b/frontend/pages/api/auth/Login1.ts
@@ -0,0 +1,32 @@
+interface Login1 {
+ serverPublicKey: string;
+ salt: string;
+}
+
+/**
+ * This is the first step of the login process (pake)
+ * @param {*} email
+ * @param {*} clientPublicKey
+ * @returns
+ */
+const login1 = async (email: string, clientPublicKey: string) => {
+ const response = await fetch("/api/v1/auth/login1", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ email: email,
+ clientPublicKey,
+ }),
+ });
+ // need precise error handling about the status code
+ if (response?.status === 200) {
+ const data = (await response.json()) as unknown as Login1;
+ return data;
+ }
+
+ throw new Error("Wrong password");
+};
+
+export default login1;
diff --git a/frontend/pages/api/auth/Login2.js b/frontend/pages/api/auth/Login2.js
deleted file mode 100644
index 923e261dfd..0000000000
--- a/frontend/pages/api/auth/Login2.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * This is the second step of the login process
- * @param {*} email
- * @param {*} clientPublicKey
- * @returns
- */
-const login2 = (email, clientProof) => {
- return fetch("/api/v1/auth/login2", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- email: email,
- clientProof,
- }),
- credentials: "include",
- }).then((res) => {
- if (res.status == 200) {
- console.log("User logged in", res);
- return res;
- } else {
- console.log("Failed to log in");
- }
- });
-};
-
-export default login2;
diff --git a/frontend/pages/api/auth/Login2.ts b/frontend/pages/api/auth/Login2.ts
new file mode 100644
index 0000000000..3adcb695f8
--- /dev/null
+++ b/frontend/pages/api/auth/Login2.ts
@@ -0,0 +1,36 @@
+interface Login2Response {
+ encryptedPrivateKey: string;
+ iv: string;
+ publicKey: string;
+ tag: string;
+ token: string;
+}
+
+/**
+ * This is the second step of the login process
+ * @param {*} email
+ * @param {*} clientPublicKey
+ * @returns
+ */
+const login2 = async (email: string, clientProof: string) => {
+ const response = await fetch("/api/v1/auth/login2", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ email: email,
+ clientProof,
+ }),
+ credentials: "include",
+ });
+ // need precise error handling about the status code
+ if (response.status == 200) {
+ const data = (await response.json()) as unknown as Login2Response;
+ return data;
+ }
+
+ throw new Error("Password verification failed");
+};
+
+export default login2;
diff --git a/frontend/pages/api/organization/GetOrgUserProjects.js b/frontend/pages/api/organization/GetOrgUserProjects.js
index c091934562..c2d751f64d 100644
--- a/frontend/pages/api/organization/GetOrgUserProjects.js
+++ b/frontend/pages/api/organization/GetOrgUserProjects.js
@@ -6,7 +6,7 @@ import SecurityClient from "~/utilities/SecurityClient";
* @param {*} res
* @returns
*/
-const getOrganizationUserProjects = (req, res) => {
+const getOrganizationUserProjects = (req) => {
return SecurityClient.fetchCall(
"/api/v1/organization/" + req.orgId + "/my-workspaces",
{
diff --git a/frontend/pages/api/workspace/getWorkspaces.ts b/frontend/pages/api/workspace/getWorkspaces.ts
index 9427c03d00..33292b6eec 100644
--- a/frontend/pages/api/workspace/getWorkspaces.ts
+++ b/frontend/pages/api/workspace/getWorkspaces.ts
@@ -1,5 +1,13 @@
import SecurityClient from "~/utilities/SecurityClient";
+interface Workspaces {
+ __v: number;
+ _id: string;
+ name: string;
+ organization: string;
+}
+[];
+
/**
* This route lets us get the workspaces of a certain user
* @returns
@@ -12,10 +20,11 @@ const getWorkspaces = () => {
},
}).then(async (res) => {
if (res?.status == 200) {
- return (await res.json()).workspaces;
- } else {
- console.log("Failed to get projects");
+ const data = (await res.json()) as unknown as { workspaces: Workspaces };
+ return data.workspaces;
}
+
+ throw new Error("Failed to get projects");
});
};
diff --git a/frontend/pages/dashboard/[id].js b/frontend/pages/dashboard/[id].js
index a4dc9c7820..c4aa8f5e26 100644
--- a/frontend/pages/dashboard/[id].js
+++ b/frontend/pages/dashboard/[id].js
@@ -27,6 +27,7 @@ import { Menu, Transition } from "@headlessui/react";
import Button from "~/components/basic/buttons/Button";
import ListBox from "~/components/basic/Listbox";
import BottonRightPopup from "~/components/basic/popups/BottomRightPopup";
+import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider";
import DashboardInputField from "~/components/dashboard/DashboardInputField";
import DropZone from "~/components/dashboard/DropZone";
import NavHeader from "~/components/navigation/NavHeader";
@@ -61,7 +62,7 @@ const KeyPair = ({
modifyValue,
modifyVisibility,
isBlurred,
- duplicates
+ duplicates,
}) => {
const [randomStringLength, setRandomStringLength] = useState(32);
const { t } = useTranslation();
@@ -233,6 +234,8 @@ export default function Dashboard() {
const { t } = useTranslation();
+ const { createNotification } = useNotificationContext();
+
// #TODO: fix save message for changing reroutes
// const beforeRouteHandler = (url) => {
// const warningText =
@@ -376,46 +379,61 @@ export default function Dashboard() {
);
// Checking if any of the secret keys start with a number - if so, don't do anything
- const nameErrors = !Object.keys(obj).map(key => !isNaN(key.charAt(0))).every(v => v === false);
- const duplicatesExist = data?.map(item => item[2]).filter((item, index) => index !== data?.map(item => item[2]).indexOf(item)).length > 0;
+ const nameErrors = !Object.keys(obj)
+ .map((key) => !isNaN(key.charAt(0)))
+ .every((v) => v === false);
+ const duplicatesExist =
+ data
+ ?.map((item) => item[2])
+ .filter(
+ (item, index) => index !== data?.map((item) => item[2]).indexOf(item)
+ ).length > 0;
if (nameErrors) {
- console.log("Solve all name errors first!");
- } else if (duplicatesExist) {
- console.log("Remove the duplicated entries first!");
- } else {
- // Once "Save changes is clicked", disable that button
- setButtonReady(false);
- pushKeys({obj, workspaceId: router.query.id, env});
-
- /**
- * Check which integrations are active for this project and environment
- * If there are any, update environment variables for those integrations
- */
- let integrations = await getWorkspaceIntegrations({
- workspaceId: router.query.id,
+ return createNotification({
+ text: "Solve all name errors first!",
+ type: "error",
});
- integrations.map(async (integration) => {
- if (
- envMapping[env] == integration.environment &&
- integration.isActive == true
- ) {
- let objIntegration = Object.assign(
- {},
- ...data.map((row) => ({ [row[2]]: row[3] }))
- );
- await pushKeysIntegration({
- obj: objIntegration,
- integrationId: integration._id,
- });
- }
+ }
+
+ if (duplicatesExist) {
+ return createNotification({
+ text: "Your secrets weren't saved; please fix the conflicts first.",
+ type: "error",
});
+ }
- // If this user has never saved environment variables before, show them a prompt to read docs
- if (!hasUserEverPushed) {
- setCheckDocsPopUpVisible(true);
- await registerUserAction({ action: "first_time_secrets_pushed" });
+ // Once "Save changed is clicked", disable that button
+ setButtonReady(false);
+ pushKeys({ obj, workspaceId: router.query.id, env });
+
+ /**
+ * Check which integrations are active for this project and environment
+ * If there are any, update environment variables for those integrations
+ */
+ let integrations = await getWorkspaceIntegrations({
+ workspaceId: router.query.id,
+ });
+ integrations.map(async (integration) => {
+ if (
+ envMapping[env] == integration.environment &&
+ integration.isActive == true
+ ) {
+ let objIntegration = Object.assign(
+ {},
+ ...data.map((row) => ({ [row[2]]: row[3] }))
+ );
+ await pushKeysIntegration({
+ obj: objIntegration,
+ integrationId: integration._id,
+ });
}
+ });
+
+ // If this user has never saved environment variables before, show them a prompt to read docs
+ if (!hasUserEverPushed) {
+ setCheckDocsPopUpVisible(true);
+ await registerUserAction({ action: "first_time_secrets_pushed" });
}
};
@@ -654,7 +672,13 @@ export default function Dashboard() {
modifyKey={listenChangeKey}
modifyVisibility={listenChangeVisibility}
isBlurred={blurred}
- duplicates={data?.map(item => item[2]).filter((item, index) => index !== data?.map(item => item[2]).indexOf(item))}
+ duplicates={data
+ ?.map((item) => item[2])
+ .filter(
+ (item, index) =>
+ index !==
+ data?.map((item) => item[2]).indexOf(item)
+ )}
/>
))}
@@ -702,7 +726,13 @@ export default function Dashboard() {
modifyKey={listenChangeKey}
modifyVisibility={listenChangeVisibility}
isBlurred={blurred}
- duplicates={data?.map(item => item[2]).filter((item, index) => index !== data?.map(item => item[2]).indexOf(item))}
+ duplicates={data
+ ?.map((item) => item[2])
+ .filter(
+ (item, index) =>
+ index !==
+ data?.map((item) => item[2]).indexOf(item)
+ )}
/>
))}