From 7c24e0181a0147992df74ea1f5f94ab1fb766345 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Mon, 17 Jul 2023 15:09:15 +0530 Subject: [PATCH 01/31] add windmill variables to integration --- backend/src/variables/integration.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 18eaeb049..bd077a019 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -26,6 +26,7 @@ export const INTEGRATION_SUPABASE = "supabase"; export const INTEGRATION_CHECKLY = "checkly"; export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; +export const INTEGRATION_WINDMILL = "windmill"; export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -41,7 +42,8 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_WINDMILL ]); // integration types @@ -71,6 +73,7 @@ export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com"; export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com"; export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com"; export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; +export const INTEGRATION_WINDMILL_API_URL = "https://app.windmill.dev/api"; export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ @@ -245,7 +248,17 @@ export const getIntegrationOptions = async () => { type: "pat", clientId: "", docsLink: "" - } + }, + { + name: "Windmill", + slug: "windmill", + image: "Cloudflare.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + ] return INTEGRATION_OPTIONS; From 06bd98bf56ad4772a707c6b4c262f11a8faa1e69 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Mon, 17 Jul 2023 15:12:12 +0530 Subject: [PATCH 02/31] add windmill variables to model schema --- backend/src/models/integration.ts | 5 ++++- backend/src/models/integrationAuth.ts | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index c4021977d..7c487cbfe 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -18,6 +18,7 @@ import { INTEGRATION_SUPABASE, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, + INTEGRATION_WINDMILL, } from "../variables"; export interface IIntegration { @@ -54,7 +55,8 @@ export interface IIntegration { | "supabase" | "checkly" | "hashicorp-vault" - | "cloudflare-pages"; + | "cloudflare-pages" + | "windmill"; integrationAuth: Types.ObjectId; } @@ -144,6 +146,7 @@ const integrationSchema = new Schema( INTEGRATION_CHECKLY, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_WINDMILL, ], required: true, }, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index ed3dabadf..3d18166f8 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -19,13 +19,14 @@ import { INTEGRATION_RENDER, INTEGRATION_SUPABASE, INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL + INTEGRATION_VERCEL, + INTEGRATION_WINDMILL } from "../variables"; export interface IIntegrationAuth extends Document { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "laravel-forge" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "checkly" | "cloudflare-pages"; + integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "laravel-forge" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "checkly" | "cloudflare-pages" | "windmill"; teamId: string; accountId: string; url: string; @@ -71,6 +72,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_SUPABASE, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_WINDMILL, ], required: true, }, From 52e26fc6fac948c05032e25a7f116f655f498f4f Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Mon, 17 Jul 2023 16:34:39 +0530 Subject: [PATCH 03/31] create integration pages for windmill --- .../pages/integrations/windmill/authorize.tsx | 65 ++++++++ .../pages/integrations/windmill/create.tsx | 156 ++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 frontend/src/pages/integrations/windmill/authorize.tsx create mode 100644 frontend/src/pages/integrations/windmill/create.tsx diff --git a/frontend/src/pages/integrations/windmill/authorize.tsx b/frontend/src/pages/integrations/windmill/authorize.tsx new file mode 100644 index 000000000..fcf7048d1 --- /dev/null +++ b/frontend/src/pages/integrations/windmill/authorize.tsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; + +import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; +import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; + + +export default function WindmillCreateIntegrationPage() { + const router = useRouter(); + const [apiKey, setApiKey] = useState(""); + const [apiKeyErrorText, setApiKeyErrorText] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleButtonClick = async () => { + try { + setApiKeyErrorText(""); + if (apiKey.length === 0) { + setApiKeyErrorText("API Key cannot be blank"); + return; + } + + setIsLoading(true); + + const integrationAuth = await saveIntegrationAccessToken({ + workspaceId: localStorage.getItem("projectData.id"), + integration: "windmill", + accessToken: apiKey, + accessId: null, + url: null, + namespace: null + }); + + setIsLoading(false); + + router.push(`/integrations/windmill/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + console.error(err); + } + }; + + return ( +
+ + Windmill Integration + + setApiKey(e.target.value)} /> + + + +
+ ); +} + +WindmillCreateIntegrationPage.requireAuth = true; \ No newline at end of file diff --git a/frontend/src/pages/integrations/windmill/create.tsx b/frontend/src/pages/integrations/windmill/create.tsx new file mode 100644 index 000000000..519355f65 --- /dev/null +++ b/frontend/src/pages/integrations/windmill/create.tsx @@ -0,0 +1,156 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from "../../../components/v2"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById +} from "../../../hooks/api/integrationAuth"; +import { useGetWorkspaceById } from "../../../hooks/api/workspace"; +import createIntegration from "../../api/integrations/createIntegration"; + +export default function WindmillCreateIntegrationPage() { + const router = useRouter(); + + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); + const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); + const { data: integrationAuthApps } = useGetIntegrationAuthApps({ + integrationAuthId: (integrationAuthId as string) ?? "" + }); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [secretPath, setSecretPath] = useState("/"); + const [targetApp, setTargetApp] = useState(""); + + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (workspace) { + setSelectedSourceEnvironment(workspace.environments[0].slug); + } + }, [workspace]); + + useEffect(() => { + if (integrationAuthApps) { + if (integrationAuthApps.length > 0) { + setTargetApp(integrationAuthApps[0].name); + } else { + setTargetApp("none"); + } + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + if (!integrationAuth?._id) return; + + setIsLoading(true); + + await createIntegration({ + integrationAuthId: integrationAuth?._id, + isActive: true, + app: targetApp, + appId: + integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp) + ?.appId ?? null, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: null, + targetEnvironmentId: null, + targetService: null, + targetServiceId: null, + owner: null, + path: null, + region: null, + secretPath + }); + + setIsLoading(false); + + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && + workspace && + selectedSourceEnvironment && + integrationAuthApps && + targetApp ? ( +
+ + Windmill Integration + + + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + + + + +
+ ) : ( +
+ ); +} + +WindmillCreateIntegrationPage.requireAuth = true; From 6125246794a6b279c435bc5054fdf773230c9912 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Mon, 17 Jul 2023 16:35:11 +0530 Subject: [PATCH 04/31] add integration authorize redirect url --- frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index fd82d1fa7..fa6633df0 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -92,6 +92,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "cloudflare-pages": link = `${window.location.origin}/integrations/cloudflare-pages/authorize`; break; + case "windmill": + link = `${window.location.origin}/integrations/windmill/authorize`; + break; default: break; } From 04611d980b55fd58ad355481a19973c0415c112e Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Mon, 17 Jul 2023 16:50:27 +0530 Subject: [PATCH 05/31] create windmill get all workspaces list function --- backend/src/integrations/apps.ts | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index b2fec9292..0d5a8239f 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -32,6 +32,8 @@ import { INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, INTEGRATION_VERCEL_API_URL, + INTEGRATION_WINDMILL, + INTEGRATION_WINDMILL_API_URL, } from "../variables"; interface App { @@ -145,6 +147,11 @@ const getApps = async ({ accountId: accessId }) break; + case INTEGRATION_WINDMILL: + apps = await getAppsWindmill({ + accessToken, + }); + break; } return apps; @@ -721,4 +728,33 @@ const getAppsCloudflarePages = async ({ return apps; } +/** + * Return list of projects for Windmill integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Windmill API + * @returns {Object[]} apps - names of Windmill workspaces + * @returns {String} apps.name - name of Windmill workspace + */ +const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { + const { data } = await standardRequest.get( + `${INTEGRATION_WINDMILL_API_URL}/workspaces/list`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + const apps = data.map((a: any) => { + return { + name: a.name, + appId: a.id, + }; + }); + + return apps; +}; + + export { getApps }; From 4db7b0c05e3b65b9368c9f32209d536d028a203d Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Wed, 19 Jul 2023 13:13:14 +0530 Subject: [PATCH 06/31] add function for windmill secret sync --- backend/src/integrations/sync.ts | 115 +++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 5d8efa101..a085dd110 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -42,6 +42,8 @@ import { INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, INTEGRATION_VERCEL_API_URL, + INTEGRATION_WINDMILL, + INTEGRATION_WINDMILL_API_URL, } from "../variables"; import { standardRequest} from "../config/request"; @@ -202,6 +204,13 @@ const syncSecrets = async ({ accessToken }); break; + case INTEGRATION_WINDMILL: + await syncSecretsWindmill({ + integration, + secrets, + accessToken, + }); + break; } }; @@ -1937,4 +1946,110 @@ const syncSecretsCloudflarePages = async ({ ); } +/** + * Sync/push [secrets] to Windmil with name [integration.app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for windmill integration + */ +const syncSecretsWindmill = async ({ + integration, + secrets, + accessToken, +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + const { data: getSecretsRes } = await standardRequest.get( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.app}/variables/list`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + // convert secret results to [key] format + const secretsResList = getSecretsRes.map((secretObj: any) => (secretObj.path)); + + // convert the secrets to [{}] format + const modifiedFormatForSecretInjection: any[] = []; + const modifiedFormatForCreateSecretInjection: any[] = []; + const modifiedFormatForUpdateSecretInjection: any[] = []; + Object.keys(secrets).forEach( + (key) => { + if(key.startsWith("u/") || key.startsWith("f/")) { + if(secretsResList.includes(key)) { + modifiedFormatForUpdateSecretInjection.push({ + path: key, + value: secrets[key], + is_secret: true + }); + } else { + modifiedFormatForCreateSecretInjection.push({ + path: key, + value: secrets[key], + is_secret: true, + description: "" + }); + } + }; + } + ); + + // create new secrets in windmill workspace + modifiedFormatForCreateSecretInjection.forEach(async (secretObj: any) => { + await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.app}/variables/create`, + secretObj, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + }) + + // update old secrets already present in windmill workspace + modifiedFormatForUpdateSecretInjection.forEach(async (secretObj: any) => { + await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.app}/variables/update/${secretObj.path}`, + secretObj, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ) + }) + + // create list of secrets to delete + const secretsToDelete: any = []; + secretsResList.forEach((secret: string) => { + if(!(secret in secrets)) { + secretsToDelete.push(secret); + } + }) + + // delete all secrets from secretsToDelete List + secretsToDelete.forEach(async (secret: string) => { + await standardRequest.delete( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.app}/variables/delete/${secret}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", + } + } + ); + }); +}; + export { syncSecrets }; From a52c2f03bfe79534b5d35b000e19522397be6bc4 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Wed, 19 Jul 2023 14:12:05 +0530 Subject: [PATCH 07/31] add integration slug name mapping for windmill --- frontend/public/data/frequentConstants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index c3c74091d..a6a21c966 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -20,7 +20,8 @@ const integrationSlugNameMapping: Mapping = { 'supabase': 'Supabase', 'checkly': 'Checkly', 'hashicorp-vault': 'Vault', - 'cloudflare-pages': 'Cloudflare Pages' + 'cloudflare-pages': 'Cloudflare Pages', + 'windmill': 'windmill' } const envMapping: Mapping = { From d2d23a7abaafc97d7d411a64dc4c178ba561647d Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Wed, 19 Jul 2023 14:47:15 +0530 Subject: [PATCH 08/31] add windmill logo --- .../public/images/integrations/Windmill.png | Bin 0 -> 10188 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 frontend/public/images/integrations/Windmill.png diff --git a/frontend/public/images/integrations/Windmill.png b/frontend/public/images/integrations/Windmill.png new file mode 100644 index 0000000000000000000000000000000000000000..c4297077fc1c6dc9524ae71f0c033a672c239430 GIT binary patch literal 10188 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 Date: Wed, 19 Jul 2023 14:47:37 +0530 Subject: [PATCH 09/31] add windmill logo to integration variable --- backend/src/variables/integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index bd077a019..12ff525bc 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -252,7 +252,7 @@ export const getIntegrationOptions = async () => { { name: "Windmill", slug: "windmill", - image: "Cloudflare.png", + image: "Windmill.png", isAvailable: true, type: "pat", clientId: "", From 83d52919988518de53b7541b4b54bba05e228d16 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Wed, 19 Jul 2023 15:00:42 +0530 Subject: [PATCH 10/31] add interface for windmill request body --- backend/src/integrations/sync.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index a085dd110..f633cb88e 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1963,6 +1963,17 @@ const syncSecretsWindmill = async ({ secrets: any; accessToken: string; }) => { + interface WindmilSecretUpdate { + path: string; + value: string; + is_secret: boolean; + } + + interface WindmillSecretCreate extends WindmilSecretUpdate { + description: string; + } + + // get secrets stored in windmill workspace const { data: getSecretsRes } = await standardRequest.get( `${INTEGRATION_WINDMILL_API_URL}/w/${integration.app}/variables/list`, { @@ -1977,9 +1988,9 @@ const syncSecretsWindmill = async ({ const secretsResList = getSecretsRes.map((secretObj: any) => (secretObj.path)); // convert the secrets to [{}] format - const modifiedFormatForSecretInjection: any[] = []; - const modifiedFormatForCreateSecretInjection: any[] = []; - const modifiedFormatForUpdateSecretInjection: any[] = []; + const modifiedFormatForCreateSecretInjection: WindmillSecretCreate[] = []; + const modifiedFormatForUpdateSecretInjection: WindmilSecretUpdate[] = []; + Object.keys(secrets).forEach( (key) => { if(key.startsWith("u/") || key.startsWith("f/")) { @@ -2030,7 +2041,7 @@ const syncSecretsWindmill = async ({ }) // create list of secrets to delete - const secretsToDelete: any = []; + const secretsToDelete: string[] = []; secretsResList.forEach((secret: string) => { if(!(secret in secrets)) { secretsToDelete.push(secret); From ec1e8422026511780b63268875f2ff5ead359b8c Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Wed, 19 Jul 2023 19:04:59 +0530 Subject: [PATCH 11/31] change windmill workspace label --- frontend/src/pages/integrations/windmill/create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/integrations/windmill/create.tsx b/frontend/src/pages/integrations/windmill/create.tsx index 519355f65..a6cb6f8eb 100644 --- a/frontend/src/pages/integrations/windmill/create.tsx +++ b/frontend/src/pages/integrations/windmill/create.tsx @@ -114,7 +114,7 @@ export default function WindmillCreateIntegrationPage() { placeholder="Provide a path, default is /" /> - + setTargetApp(val)} From aa019e15016d254d76f258c6ea0499ebc2a4c5d1 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 20 Jul 2023 02:12:36 +0530 Subject: [PATCH 14/31] add pattern match for windmill stored secrets --- backend/src/integrations/sync.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index f633cb88e..14ab37ea5 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1993,7 +1993,8 @@ const syncSecretsWindmill = async ({ Object.keys(secrets).forEach( (key) => { - if(key.startsWith("u/") || key.startsWith("f/")) { + const pattern = new RegExp('^([a-zA-Z0-9])\/([a-zA-Z-0-9])+\/([a-zA-Z0-9])+') + if((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { if(secretsResList.includes(key)) { modifiedFormatForUpdateSecretInjection.push({ path: key, From b3baaac5c8c73b1d9b49cee2bffb310a4e93beec Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 20 Jul 2023 12:57:16 +0530 Subject: [PATCH 15/31] map secret comments to windmill api description --- backend/src/helpers/bot.ts | 68 ++++++++++++++++++++++++++++++ backend/src/helpers/integration.ts | 9 ++++ backend/src/integrations/sync.ts | 23 ++++++---- backend/src/services/BotService.ts | 25 +++++++++++ 4 files changed, 116 insertions(+), 9 deletions(-) diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index c7c5904c7..d3db7febf 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -16,6 +16,7 @@ import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { InternalServerError } from "../utils/errors"; import Folder from "../models/folder"; import { getFolderByPath } from "../services/FolderService"; +import { environment } from "../routes/v2"; /** * Create an inactive bot with name [name] for workspace with id [workspaceId] @@ -275,3 +276,70 @@ export const decryptSymmetricHelper = async ({ return plaintext; }; + +/** + * Return decrypted comments for workspace secrets with id [workspaceId] + * and [envionment] using bot + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.environment - environment + */ +export const getSecretsCommentBotHelper = async ({ + workspaceId, + environment, + secretPath +} : { + workspaceId: Types.ObjectId; + environment: string; + secretPath: string; +}) => { + const content = {} as any; + const key = await getKey({ workspaceId: workspaceId }); + + let folderId = "root"; + const folders = await Folder.findOne({ + workspace: workspaceId, + environment, + }); + + if (!folders && secretPath !== "/") { + throw InternalServerError({ message: "Folder not found" }); + } + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw InternalServerError({ message: "Folder not found" }); + } + folderId = folder.id; + } + + const secrets = await Secret.find({ + workspace: workspaceId, + environment, + type: SECRET_SHARED, + folder: folderId, + }); + + secrets.forEach((secret: ISecret) => { + if(secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key, + }); + + const commentValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretCommentCiphertext, + iv: secret.secretCommentIV, + tag: secret.secretCommentTag, + key, + }); + + content[secretKey] = commentValue; + } + }); + + return content; +} \ No newline at end of file diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index f2dd1ba66..aaa8c9610 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -137,6 +137,14 @@ export const syncIntegrationsHelper = async ({ secretPath: integration.secretPath, }); + // get workspace, environment (shared) secrets comments + const secretComments = await BotService.getSecretComments({ + workspaceId: integration.workspace, + environment: integration.environment, + secretPath: integration.secretPath, + }) + + const integrationAuth = await IntegrationAuth.findById( integration.integrationAuth ); @@ -154,6 +162,7 @@ export const syncIntegrationsHelper = async ({ secrets, accessId: access.accessId === undefined ? null : access.accessId, accessToken: access.accessToken, + secretComments }); } }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 14ab37ea5..59445ce4f 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -55,6 +55,7 @@ import { standardRequest} from "../config/request"; * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessId - access id for integration * @param {String} obj.accessToken - access token for integration + * @param {Object} obj.secretComments - secret comments to push to integration (object where keys are secret keys and values are comment values) */ const syncSecrets = async ({ integration, @@ -62,12 +63,14 @@ const syncSecrets = async ({ secrets, accessId, accessToken, + secretComments }: { integration: IIntegration; integrationAuth: IIntegrationAuth; secrets: any; accessId: string | null; accessToken: string; + secretComments: any; }) => { switch (integration.integration) { case INTEGRATION_AZURE_KEY_VAULT: @@ -209,6 +212,7 @@ const syncSecrets = async ({ integration, secrets, accessToken, + secretComments }); break; } @@ -1953,24 +1957,24 @@ const syncSecretsCloudflarePages = async ({ * @param {IIntegrationAuth} obj.integrationAuth - integration auth details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for windmill integration + * @param {Object} obj.secretComments - secret comments to push to integration (object where keys are secret keys and values are comment values) */ const syncSecretsWindmill = async ({ integration, secrets, accessToken, + secretComments }: { integration: IIntegration; secrets: any; accessToken: string; + secretComments: any; }) => { - interface WindmilSecretUpdate { + interface WindmillSecret { path: string; value: string; is_secret: boolean; - } - - interface WindmillSecretCreate extends WindmilSecretUpdate { - description: string; + description?: string; } // get secrets stored in windmill workspace @@ -1988,8 +1992,8 @@ const syncSecretsWindmill = async ({ const secretsResList = getSecretsRes.map((secretObj: any) => (secretObj.path)); // convert the secrets to [{}] format - const modifiedFormatForCreateSecretInjection: WindmillSecretCreate[] = []; - const modifiedFormatForUpdateSecretInjection: WindmilSecretUpdate[] = []; + const modifiedFormatForCreateSecretInjection: WindmillSecret[] = []; + const modifiedFormatForUpdateSecretInjection: WindmillSecret[] = []; Object.keys(secrets).forEach( (key) => { @@ -1999,14 +2003,15 @@ const syncSecretsWindmill = async ({ modifiedFormatForUpdateSecretInjection.push({ path: key, value: secrets[key], - is_secret: true + is_secret: true, + description: secretComments[key] || "" }); } else { modifiedFormatForCreateSecretInjection.push({ path: key, value: secrets[key], is_secret: true, - description: "" + description: secretComments[key] || "" }); } }; diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts index ca31bf103..7ebf53ae8 100644 --- a/backend/src/services/BotService.ts +++ b/backend/src/services/BotService.ts @@ -5,6 +5,7 @@ import { getIsWorkspaceE2EEHelper, getKey, getSecretsBotHelper, + getSecretsCommentBotHelper, } from "../helpers/bot"; /** @@ -107,6 +108,30 @@ class BotService { tag, }); } + + /** + * Return decreypted secrets comment for workspace with id [worskpaceId] and + * environment [environment] shared to bot. + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace of secrets + * @param {String} obj.environment - environment for secrets + * @returns {Object} secretObj - object where keys are secret keys and values are comment values + */ + static async getSecretComments({ + workspaceId, + environment, + secretPath + }: { + workspaceId: Types.ObjectId; + environment: string; + secretPath: string; + }) { + return await getSecretsCommentBotHelper({ + workspaceId, + environment, + secretPath + }); + } } export default BotService; From c62504d6588c038cc99da840a8e4bfa30d543b66 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 20 Jul 2023 19:21:04 +0530 Subject: [PATCH 16/31] correct codefresh image file name --- .../integrations/{codefresh.png => Codefresh.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/public/images/integrations/{codefresh.png => Codefresh.png} (100%) diff --git a/frontend/public/images/integrations/codefresh.png b/frontend/public/images/integrations/Codefresh.png similarity index 100% rename from frontend/public/images/integrations/codefresh.png rename to frontend/public/images/integrations/Codefresh.png From d67e96507a41df71ca397819e8872dc1a612db46 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Wed, 26 Jul 2023 23:14:42 +0530 Subject: [PATCH 17/31] fix:unauthorized response for app name --- backend/src/integrations/apps.ts | 2 +- backend/src/integrations/sync.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 1b4069502..98b1f1170 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -789,7 +789,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { const authCheckForApps = async (data: any) => { const allAppResponse = data.map(async (app: any) => { return standardRequest.get( - `${INTEGRATION_WINDMILL_API_URL}/w/${app.name}/users/whoami`, + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/users/whoami`, { headers: { Authorization: `Bearer ${accessToken}`, diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index e8e382ed9..139f06ecb 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -2049,7 +2049,7 @@ const syncSecretsWindmill = async ({ // get secrets stored in windmill workspace const { data: getSecretsRes } = await standardRequest.get( - `${INTEGRATION_WINDMILL_API_URL}/w/${integration.app}/variables/list`, + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/list`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -2091,7 +2091,7 @@ const syncSecretsWindmill = async ({ // create new secrets in windmill workspace modifiedFormatForCreateSecretInjection.forEach(async (secretObj: any) => { await standardRequest.post( - `${INTEGRATION_WINDMILL_API_URL}/w/${integration.app}/variables/create`, + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/create`, secretObj, { headers: { @@ -2105,7 +2105,7 @@ const syncSecretsWindmill = async ({ // update old secrets already present in windmill workspace modifiedFormatForUpdateSecretInjection.forEach(async (secretObj: any) => { await standardRequest.post( - `${INTEGRATION_WINDMILL_API_URL}/w/${integration.app}/variables/update/${secretObj.path}`, + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/update/${secretObj.path}`, secretObj, { headers: { @@ -2127,7 +2127,7 @@ const syncSecretsWindmill = async ({ // delete all secrets from secretsToDelete List secretsToDelete.forEach(async (secret: string) => { await standardRequest.delete( - `${INTEGRATION_WINDMILL_API_URL}/w/${integration.app}/variables/delete/${secret}`, + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/delete/${secret}`, { headers: { Authorization: `Bearer ${accessToken}`, From 7457f573e98e120247f93c9edf9e85856b93863c Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Wed, 26 Jul 2023 23:43:44 +0530 Subject: [PATCH 18/31] add dash and underscores for secret pattern test --- backend/src/integrations/sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 139f06ecb..709af24d0 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -2067,7 +2067,7 @@ const syncSecretsWindmill = async ({ Object.keys(secrets).forEach( (key) => { - const pattern = new RegExp('^([a-zA-Z0-9])\/([a-zA-Z-0-9])+\/([a-zA-Z0-9])+') + const pattern = new RegExp('^([a-zA-Z0-9])\/([a-zA-Z-0-9-_])+\/([a-zA-Z0-9-_])+') if((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { if(secretsResList.includes(key)) { modifiedFormatForUpdateSecretInjection.push({ From 7b1a4fa8e4d7e9c09ee390c32fac193d2aff937c Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 27 Jul 2023 00:48:17 +0530 Subject: [PATCH 19/31] change regexp to accept deeper level paths --- backend/src/integrations/sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 709af24d0..1c7b264a7 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -2067,7 +2067,7 @@ const syncSecretsWindmill = async ({ Object.keys(secrets).forEach( (key) => { - const pattern = new RegExp('^([a-zA-Z0-9])\/([a-zA-Z-0-9-_])+\/([a-zA-Z0-9-_])+') + const pattern = new RegExp('^[uf]+[\/](?:[a-zA-Z0-9-_]+[\/])*([a-zA-Z0-9-_]+)') if((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { if(secretsResList.includes(key)) { modifiedFormatForUpdateSecretInjection.push({ From adb27bb72921bfdf74f89a5aa48ec3cd5d470c95 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 27 Jul 2023 13:11:48 +0530 Subject: [PATCH 20/31] fix: allow apps which have write access --- backend/src/integrations/apps.ts | 91 ++++++++++++++----- .../pages/integrations/windmill/create.tsx | 2 +- 2 files changed, 67 insertions(+), 26 deletions(-) diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 98b1f1170..55462ec2f 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -785,41 +785,82 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { } ); - // make calls for each app to check user is admin for that app or not - const authCheckForApps = async (data: any) => { - const allAppResponse = data.map(async (app: any) => { - return standardRequest.get( - `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/users/whoami`, + //check for write access of secrets in windmill workspaces + const writeAccessCheck = data.map(async (app: any) => { + try { + const userPath = "u/user/variable"; + const folderPath = "f/folder/variable"; + + const { data: writeUser } = await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/create`, + { + path: userPath, + value: "variable", + is_secret: true, + description: "variable description" + }, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json", }, } - ) - .then((response: any) => { - const modifiedData = { ...response.data }; - modifiedData.appName = app.name; - return modifiedData; - }) - .catch((error: any) => { - return undefined; - }); - }); + ); - const appPromiseResponses = await Promise.all(allAppResponse) - const filteredAppResponses = appPromiseResponses.filter((authRes: any) => (authRes !== undefined) && (authRes.is_admin)); - - return filteredAppResponses; - } + const { data: writeFolder } = await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/create`, + { + path: folderPath, + value: "variable", + is_secret: true, + description: "variable description" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + // is write access is allowed then delete the created secrets from workspace + if (writeUser && writeFolder) { + await standardRequest.delete( + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); - // get apps that user(auth token) is authorized for - const authorizedApps = await authCheckForApps(data); + await standardRequest.delete( + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); - const apps = authorizedApps.map((a: any) => { + return app; + } else { + return { error: "cannot write secret" }; + } + } catch (err: any) { + return { error: err.message }; + } + }); + + const appsWriteResponses = await Promise.all(writeAccessCheck); + const appsWithWriteAccess = appsWriteResponses.filter((appRes: any) => !appRes.error); + + const apps = appsWithWriteAccess.map((a: any) => { return { - name: a.appName, - appId: a.workspace_id, + name: a.name, + appId: a.id, }; }); diff --git a/frontend/src/pages/integrations/windmill/create.tsx b/frontend/src/pages/integrations/windmill/create.tsx index 815da396a..fd4662ce1 100644 --- a/frontend/src/pages/integrations/windmill/create.tsx +++ b/frontend/src/pages/integrations/windmill/create.tsx @@ -114,7 +114,7 @@ export default function WindmillCreateIntegrationPage() { placeholder="Provide a path, default is /" /> - + { - router.push(`/project/${value}/secrets`); - localStorage.setItem("projectData.id", value); - }} - position="popper" - dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700" - > -
- {workspaces - .filter((ws) => ws.organization === currentOrg?._id) - .map(({ _id, name }) => ( - - {name} - + {!router.asPath.includes("personal") && ( +
+ {(router.asPath.includes("project") || + router.asPath.includes("integrations")) && ( + +
+ +
+ + )} + + +
+
+ {currentOrg?.name.charAt(0)} +
+
+ {currentOrg?.name}{" "} + +
+
+
+ +
{user?.email}
+ {orgs?.map((org) => ( + + + ))} -
-
-
- + + + + +
+ {user?.firstName?.charAt(0)} + {user?.lastName && user?.lastName?.charAt(0)} +
+
+ +
{user?.email}
+ + Personal Settings + + - Add Project - -
- + + Documentation + + + + + + Join Slack Community + + + +
+ + +
- ) :
- - Back to organization -
)} + )} + {!router.asPath.includes("org") && + (!router.asPath.includes("personal") && currentWorkspace ? ( +
+

+ Project +

+ +
+ ) : ( + +
+ + Back to organization +
+ + ))}
- {((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? - - - - {t("nav.menu.secrets")} - - - - - - - {t("nav.menu.members")} - - - - - - - {t("nav.menu.integrations")} - - - - - - - IP Allowlist - - - - - - - Audit Logs - - - - {/* + {(router.asPath.includes("project") || router.asPath.includes("integrations")) && + currentWorkspace ? ( + + + + + {t("nav.menu.secrets")} + + + + + + + {t("nav.menu.members")} + + + + + + + {t("nav.menu.integrations")} + + + + + + + IP Allowlist + + + + + + + Audit Logs + + + + {/* { */} - - - - {t("nav.menu.project-settings")} - - - - - : + + + + {t("nav.menu.project-settings")} + + + + + ) : ( + { - {/* {workspaces.map(project => + {/* {workspaces.map(project => { Organization Settings - } + + )}
-
- {/*
+
+ {/*
@@ -549,22 +623,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
*/} - {router.asPath.includes("org") &&
null} - role="button" - tabIndex={0} - onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)} - className="w-full" - > -
- - Invite people + {router.asPath.includes("org") && ( +
null} + role="button" + tabIndex={0} + onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)} + className="w-full" + > +
+ + Invite people +
-
} + )} -
- +
+ Help & Support
@@ -586,28 +662,33 @@ export const AppLayout = ({ children }: LayoutProps) => { ))} - {subscription && subscription.slug === "starter" && !subscription.has_used_trial && ( - - )} + {subscription && + subscription.slug === "starter" && + !subscription.has_used_trial && ( + + )}
diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 693af4d6f..ca2f829d3 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -1,4 +1,3 @@ - import crypto from "crypto"; import { useEffect, useState } from "react"; @@ -9,14 +8,31 @@ import { useRouter } from "next/router"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faFolderOpen } from "@fortawesome/free-regular-svg-icons"; -import { faArrowRight, faCheckCircle, faHandPeace, faMagnifyingGlass, faNetworkWired, faPlug, faPlus, faUserPlus } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowRight, + faCheckCircle, + faHandPeace, + faMagnifyingGlass, + faNetworkWired, + faPlug, + faPlus, + faUserPlus +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; -import { Button, Checkbox, FormControl, Input, Modal, ModalContent, UpgradePlanModal } from "@app/components/v2"; +import { + Button, + Checkbox, + FormControl, + Input, + Modal, + ModalContent, + UpgradePlanModal +} from "@app/components/v2"; import { TabsObject } from "@app/components/v2/Tabs"; import { useSubscription, useUser, useWorkspace } from "@app/context"; import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api"; @@ -25,11 +41,14 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto"; import registerUserAction from "../../../api/userActions/registerUserAction"; -const features = [{ - "_id": 0, - "name": "Kubernetes Operator", - "description": "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes." -}] +const features = [ + { + _id: 0, + name: "Kubernetes Operator", + description: + "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes." + } +]; type ItemProps = { text: string; @@ -58,7 +77,11 @@ const learningItem = ({ className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`} href={link} > -
+
null} role="button" @@ -70,7 +93,11 @@ const learningItem = ({ }); } }} - className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`} + className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ + complete + ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" + : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" + } text-mineshaft-100 duration-200`} >
@@ -148,7 +175,11 @@ const learningItemSquare = ({ className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`} href={link} > -
+
null} role="button" @@ -160,23 +191,32 @@ const learningItemSquare = ({ }); } }} - className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`} + className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${ + complete + ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" + : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" + } text-mineshaft-100 duration-200`} > -
-
- +
+
+ {complete && (
)}
{complete ? "Complete!" : `About ${time}`}
-
+
{text}
{subText}
@@ -202,11 +242,14 @@ export default function Organization() { const router = useRouter(); const { workspaces } = useWorkspace(); - const orgWorkspaces = workspaces?.filter(workspace => workspace.organization === localStorage.getItem("orgData.id")) || [] + const orgWorkspaces = + workspaces?.filter( + (workspace) => workspace.organization === localStorage.getItem("orgData.id") + ) || []; const currentOrg = String(router.query.id); const { createNotification } = useNotificationContext(); const addWsUser = useAddUserToWs(); - + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addNewWs", "upgradePlan" @@ -269,7 +312,7 @@ export default function Organization() { } createNotification({ text: "Workspace created", type: "success" }); handlePopUpClose("addNewWs"); - router.push(`/project/${newWorkspaceId}/secrets`); + router.push(`/project/${newWorkspaceId}/secrets/overview`); } catch (err) { console.error(err); createNotification({ text: "Failed to create workspace", type: "error" }); @@ -278,7 +321,9 @@ export default function Organization() { const { subscription } = useSubscription(); - const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true; + const isAddingProjectsAllowed = subscription?.workspaceLimit + ? subscription.workspacesUsed < subscription.workspaceLimit + : true; useEffect(() => { onboardingCheck({ @@ -290,16 +335,16 @@ export default function Organization() { }, []); return ( -
+
{t("common.head-title", { title: t("settings.members.title") })} -
+

Projects

-
+
setSearchFilter(e.target.value)} @@ -310,7 +355,7 @@ export default function Organization() { leftIcon={} onClick={() => { if (isAddingProjectsAllowed) { - handlePopUpOpen("addNewWs") + handlePopUpOpen("addNewWs"); } else { handlePopUpOpen("upgradePlan"); } @@ -320,120 +365,169 @@ export default function Organization() { Add New Project
-
- {orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace =>
-
{workspace.name}
-
{(workspace.environments?.length || 0)} environments
- -
)} +
+ {orgWorkspaces + .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) + .map((workspace) => ( +
+
{workspace.name}
+
+ {workspace.environments?.length || 0} environments +
+ +
+ ))}
- {orgWorkspaces.length === 0 && ( -
- + {orgWorkspaces.length === 0 && ( +
+
- You are not part of any projects in this organization yet. When you are, they will appear - here. + You are not part of any projects in this organization yet. When you are, they will + appear here.
- Create a new project, or ask other organization members to give you necessary permissions. + Create a new project, or ask other organization members to give you necessary + permissions.
)}
- {((new Date()).getTime() - (new Date(user?.createdAt)).getTime()) < 30 * 24 * 60 * 60 * 1000 &&
-

Onboarding Guide

-
- {learningItemSquare({ - text: "Watch Infisical demo", - subText: "Set up Infisical in 3 min.", - complete: hasUserClickedIntro, - icon: faHandPeace, - time: "3 min", - userAction: "intro_cta_clicked", - link: "https://www.youtube.com/watch?v=PK23097-25I" - })} - {orgWorkspaces.length !== 0 && learningItemSquare({ - text: "Add your secrets", - subText: "Drop a .env file or type your secrets.", - complete: hasUserPushedSecrets, - icon: faPlus, - time: "1 min", - userAction: "first_time_secrets_pushed", - link: `/project/${orgWorkspaces[0]?._id}/secrets` - })} - {learningItemSquare({ - text: "Invite your teammates", - subText: "Infisical is better used as a team.", - complete: usersInOrg, - icon: faUserPlus, - time: "2 min", - link: `/org/${router.query.id}/members?action=invite` - })} -
{learningItemSquare({ - text: "Join Infisical Slack", - subText: "Have any questions? Ask us!", - complete: hasUserClickedSlack, - icon: faSlack, - time: "1 min", - userAction: "slack_cta_clicked", - link: "https://infisical.com/slack" - })}
-
- {orgWorkspaces.length !== 0 &&
-
-
- - {false && ( -
- -
- )} -
-
Inject secrets locally
-
- Replace .env files with a more secure and efficient alternative. -
-
-
-
- About 2 min + {new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && ( +
+

Onboarding Guide

+
+ {learningItemSquare({ + text: "Watch Infisical demo", + subText: "Set up Infisical in 3 min.", + complete: hasUserClickedIntro, + icon: faHandPeace, + time: "3 min", + userAction: "intro_cta_clicked", + link: "https://www.youtube.com/watch?v=PK23097-25I" + })} + {orgWorkspaces.length !== 0 && + learningItemSquare({ + text: "Add your secrets", + subText: "Drop a .env file or type your secrets.", + complete: hasUserPushedSecrets, + icon: faPlus, + time: "1 min", + userAction: "first_time_secrets_pushed", + link: `/project/${orgWorkspaces[0]?._id}/secrets` + })} + {learningItemSquare({ + text: "Invite your teammates", + subText: "Infisical is better used as a team.", + complete: usersInOrg, + icon: faUserPlus, + time: "2 min", + link: `/org/${router.query.id}/members?action=invite` + })} +
+ {learningItemSquare({ + text: "Join Infisical Slack", + subText: "Have any questions? Ask us!", + complete: hasUserClickedSlack, + icon: faSlack, + time: "1 min", + userAction: "slack_cta_clicked", + link: "https://infisical.com/slack" + })}
- - {false &&
} -
} - {orgWorkspaces.length !== 0 && learningItem({ - text: "Integrate Infisical with your infrastructure", - subText: "Connect Infisical to various 3rd party services and platforms.", - complete: false, - icon: faPlug, - time: "15 min", - link: "https://infisical.com/docs/integrations/overview" - })} -
} -
-

Explore More

-
- {features.map(feature =>
-
{feature.name}
-
{feature.description}
-
-
Setup time: 20 min
- - Learn more - + {orgWorkspaces.length !== 0 && ( +
+
+
+ + {false && ( +
+ +
+ )} +
+
Inject secrets locally
+
+ Replace .env files with a more secure and efficient alternative. +
+
+
+
+ About 2 min +
+
+ + {false &&
}
-
)} + )} + {orgWorkspaces.length !== 0 && + learningItem({ + text: "Integrate Infisical with your infrastructure", + subText: "Connect Infisical to various 3rd party services and platforms.", + complete: false, + icon: faPlug, + time: "15 min", + link: "https://infisical.com/docs/integrations/overview" + })} +
+ )} +
+

Explore More

+
+ {features.map((feature) => ( +
+
{feature.name}
+
+ {feature.description} +
+
+
Setup time: 20 min
+ + Learn more{" "} + + +
+
+ ))}
{ const { t } = useTranslation(); - const router = useRouter(); - - const queryEnv = router.query.env as string; - const isOverviewMode = !queryEnv; return ( <> @@ -22,7 +16,7 @@ const Dashboard = () => {
- {isOverviewMode ? : } +
); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx index 858c2f210..cfafae765 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx @@ -53,7 +53,7 @@ export const OrgIncidentContactsTable = ({ isLoading }: Props) => { const [searchContact, setSearchContact] = useState(""); - const {data: serverDetails } = useFetchServerStatus() + const { data: serverDetails } = useFetchServerStatus(); const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "addContact", "removeContact", @@ -98,7 +98,7 @@ export const OrgIncidentContactsTable = ({ - )} + {(status === "invited" || status === "verified") && + serverDetails?.emailConfigured && ( + + )} {status === "completed" && ( } +
+ {(status === "invited" || status === "verified") && + serverDetails?.emailConfigured ? ( + + This user hasn't accepted the invite yet + + ) : ( + + This user isn't part of any projects yet + + )} + {router.query.id !== "undefined" && + !( + (status === "invited" || status === "verified") && + serverDetails?.emailConfigured + ) && ( + + )}
)} @@ -282,67 +311,73 @@ export const OrgMembersTable = ({ isOpen={popUp?.addMember?.isOpen} onOpenChange={(isOpen) => { handlePopUpToggle("addMember", isOpen); - setCompleteInviteLink(undefined) + setCompleteInviteLink(undefined); }} > - {!completeInviteLink &&
- An invite is specific to an email address and expires after 1 day. -
- For security reasons, you will need to separately add members to projects. -
} - {completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"} + {!completeInviteLink && ( +
+ An invite is specific to an email address and expires after 1 day. +
+ For security reasons, you will need to separately add members to projects. +
+ )} + {completeInviteLink && + "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
} > - {!completeInviteLink &&
- ( - - - - )} - /> -
- - -
- } - { - completeInviteLink && + {!completeInviteLink && ( +
+ ( + + + + )} + /> +
+ + +
+ + )} + {completeInviteLink && (
-

{completeInviteLink}

- - - click to copy - -
- } +

{completeInviteLink}

+ + + + click to copy + + +
+ )}
); -}; \ No newline at end of file +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx index 61b0db361..bd9f601c2 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx @@ -1,14 +1,15 @@ -import { useEffect, useMemo,useState } from "react"; -import { Controller,useForm } from "react-hook-form"; +import { useEffect, useMemo, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; import { useRouter } from "next/router"; -import { - faCheck, - faCopy, - faMagnifyingGlass, - faPencil, - faPlus, - faServer, - faTrash} from "@fortawesome/free-solid-svg-icons"; +import { + faCheck, + faCopy, + faMagnifyingGlass, + faPencil, + faPlus, + faServer, + faTrash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; @@ -37,9 +38,10 @@ import { import { useOrganization, useWorkspace } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; import { - useCreateServiceAccount, - useDeleteServiceAccount, - useGetServiceAccounts} from "@app/hooks/api"; + useCreateServiceAccount, + useDeleteServiceAccount, + useGetServiceAccounts +} from "@app/hooks/api"; const serviceAccountExpiration = [ { label: "1 Day", value: 86400 }, @@ -51,317 +53,315 @@ const serviceAccountExpiration = [ ]; const addServiceAccountFormSchema = yup.object({ - name: yup.string().required().label("Name").trim(), - expiresIn: yup.string().required().label("Service Account Expiration") + name: yup.string().required().label("Name").trim(), + expiresIn: yup.string().required().label("Service Account Expiration") }); type TAddServiceAccountForm = yup.InferType; export const OrgServiceAccountsTable = () => { - const router = useRouter(); - const { currentOrg } = useOrganization(); - const { currentWorkspace } = useWorkspace(); - - const orgId = currentOrg?._id || ""; - const [step, setStep] = useState(0); - const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false); - const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false); - const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false); - const [accessKey, setAccessKey] = useState(""); - const [publicKey, setPublicKey] = useState(""); - const [privateKey, setPrivateKey] = useState(""); - const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState(""); - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "addServiceAccount", - "removeServiceAccount", - ] as const); + const router = useRouter(); + const { currentOrg } = useOrganization(); + const { currentWorkspace } = useWorkspace(); - const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId); - - const createServiceAccount = useCreateServiceAccount(); - const removeServiceAccount = useDeleteServiceAccount(); - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isAccessKeyCopied) { - timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000); - } + const orgId = currentOrg?._id || ""; + const [step, setStep] = useState(0); + const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false); + const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false); + const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false); + const [accessKey, setAccessKey] = useState(""); + const [publicKey, setPublicKey] = useState(""); + const [privateKey, setPrivateKey] = useState(""); + const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState(""); + const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + "addServiceAccount", + "removeServiceAccount" + ] as const); - if (isPublicKeyCopied) { - timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000); - } + const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = + useGetServiceAccounts(orgId); - if (isPrivateKeyCopied) { - timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000); - } + const createServiceAccount = useCreateServiceAccount(); + const removeServiceAccount = useDeleteServiceAccount(); - return () => clearTimeout(timer); - }, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ resolver: yupResolver(addServiceAccountFormSchema) }); - - const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => { - if (!currentOrg?._id) return; - - const keyPair = generateKeyPair(); - setPublicKey(keyPair.publicKey); - setPrivateKey(keyPair.privateKey); - - const serviceAccountDetails = await createServiceAccount.mutateAsync({ - name, - organizationId: currentOrg?._id, - publicKey: keyPair.publicKey, - expiresIn: Number(expiresIn) - }); - - setAccessKey(serviceAccountDetails.serviceAccountAccessKey); - - setStep(1); - reset(); + useEffect(() => { + let timer: NodeJS.Timeout; + if (isAccessKeyCopied) { + timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000); } - - const onRemoveServiceAccount = async () => { - const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id; - await removeServiceAccount.mutateAsync(serviceAccountId); - handlePopUpClose("removeServiceAccount"); + + if (isPublicKeyCopied) { + timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000); } - - const filteredServiceAccounts = useMemo( - () => - serviceAccounts.filter( - ({ name }) => - name.toLowerCase().includes(searchServiceAccountFilter) - ), - [serviceAccounts, searchServiceAccountFilter] - ); - - const renderStep = (stepToRender: number) => { - switch (stepToRender) { - case 0: - return ( -
- ( - - - - )} - /> - { - return ( - - - - ); - }} - /> -
- - -
- - ); - case 1: - return ( - <> -

Access Key

-
-

{accessKey}

- { - navigator.clipboard.writeText(accessKey); - setIsAccessKeyCopied.on(); - }} - > - - - Copy - - -
-

Public Key

-
-

{publicKey}

- { - navigator.clipboard.writeText(publicKey); - setIsPublicKeyCopied.on(); - }} - > - - - Copy - - -
-

Private Key

-
-

{privateKey}

- { - navigator.clipboard.writeText(privateKey); - setIsPrivateKeyCopied.on(); - }} - > - - - Copy - - -
- - - ); - default: - return
- } + + if (isPrivateKeyCopied) { + timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000); } - - return ( -
-
-
- setSearchServiceAccountFilter(e.target.value)} - leftIcon={} - placeholder="Search service accounts..." - /> -
- -
- - - - - - - - {isServiceAccountsLoading && } - {!isServiceAccountsLoading && ( - filteredServiceAccounts.map(({ - name, - expiresAt, - _id: serviceAccountId - }) => { - return ( - - - - - - ); - }) - )} - -
NameValid Until -
{name}{new Date(expiresAt).toUTCString()} -
- { - if (currentWorkspace?._id) { - router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`); - } - }} - className="mr-2" - > - - - handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })} - > - - -
-
- {!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && ( - - )} -
- { - handlePopUpToggle("addServiceAccount", isOpen); - reset(); - }} - > - - {renderStep(step)} - - - handlePopUpToggle("removeServiceAccount", isOpen)} - onDeleteApproved={onRemoveServiceAccount} + + return () => clearTimeout(timer); + }, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ resolver: yupResolver(addServiceAccountFormSchema) }); + + const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => { + if (!currentOrg?._id) return; + + const keyPair = generateKeyPair(); + setPublicKey(keyPair.publicKey); + setPrivateKey(keyPair.privateKey); + + const serviceAccountDetails = await createServiceAccount.mutateAsync({ + name, + organizationId: currentOrg?._id, + publicKey: keyPair.publicKey, + expiresIn: Number(expiresIn) + }); + + setAccessKey(serviceAccountDetails.serviceAccountAccessKey); + + setStep(1); + reset(); + }; + + const onRemoveServiceAccount = async () => { + const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id; + await removeServiceAccount.mutateAsync(serviceAccountId); + handlePopUpClose("removeServiceAccount"); + }; + + const filteredServiceAccounts = useMemo( + () => + serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)), + [serviceAccounts, searchServiceAccountFilter] + ); + + const renderStep = (stepToRender: number) => { + switch (stepToRender) { + case 0: + return ( +
+ ( + + + + )} /> + { + return ( + + + + ); + }} + /> +
+ + +
+ + ); + case 1: + return ( + <> +

Access Key

+
+

{accessKey}

+ { + navigator.clipboard.writeText(accessKey); + setIsAccessKeyCopied.on(); + }} + > + + + Copy + + +
+

Public Key

+
+

{publicKey}

+ { + navigator.clipboard.writeText(publicKey); + setIsPublicKeyCopied.on(); + }} + > + + + Copy + + +
+

Private Key

+
+

{privateKey}

+ { + navigator.clipboard.writeText(privateKey); + setIsPrivateKeyCopied.on(); + }} + > + + + Copy + + +
+ + ); + default: + return
; + } + }; + + return ( +
+
+
+ setSearchServiceAccountFilter(e.target.value)} + leftIcon={} + placeholder="Search service accounts..." + />
- ); -} \ No newline at end of file + +
+ + + + + + + + {isServiceAccountsLoading && ( + + )} + {!isServiceAccountsLoading && + filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => { + return ( + + + + + + ); + })} + +
NameValid Until +
{name}{new Date(expiresAt).toUTCString()} +
+ { + if (currentWorkspace?._id) { + router.push( + `/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}` + ); + } + }} + className="mr-2" + > + + + + handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId }) + } + > + + +
+
+ {!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && ( + + )} +
+ { + handlePopUpToggle("addServiceAccount", isOpen); + reset(); + }} + > + + {renderStep(step)} + + + handlePopUpToggle("removeServiceAccount", isOpen)} + onDeleteApproved={onRemoveServiceAccount} + /> +
+ ); +}; diff --git a/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx b/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx index b3b0cf03f..28f0313b2 100644 --- a/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx +++ b/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx @@ -10,90 +10,94 @@ import { Td, Th, THead, - Tr} from "@app/components/v2"; + Tr +} from "@app/components/v2"; import timeSince from "@app/ee/utilities/timeSince"; -import getRisksByOrganization, { GitRisks } from "@app/pages/api/secret-scanning/getRisksByOrganization"; +import getRisksByOrganization, { + GitRisks +} from "@app/pages/api/secret-scanning/getRisksByOrganization"; import { RiskStatusSelection } from "./RiskStatusSelection"; export const SecretScanningLogsTable = () => { - const [isLoading, setIsLoading] = useState(false); - const [gitRisks, setGitRisks] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [gitRisks, setGitRisks] = useState([]); - useEffect(() => { - const fetchRisks = async () => { - setIsLoading(true); - const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id"))) - setGitRisks(risks); - setIsLoading(false); - } + useEffect(() => { + const fetchRisks = async () => { + setIsLoading(true); + const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id"))); + setGitRisks(risks); + setIsLoading(false); + }; - fetchRisks(); - },[]) + fetchRisks(); + }, []); - - return ( - - - - - - - - - - - + + + )} + +
DateSecret TypeView RiskInfoStatusAction + return ( + + + + + + + + + + + + + + {!isLoading && + gitRisks && + gitRisks?.map((risk) => { + return ( + + + + + + + - - - {!isLoading && gitRisks && gitRisks?.map((risk) => { - return ( - - - - - - - - - ); - })} - {isLoading && } - {!isLoading && gitRisks && gitRisks?.length === 0 && ( - - - - )} - -
DateSecret TypeView RiskInfoStatusAction +
{timeSince(new Date(risk.createdAt))}{risk.ruleID} + + View Exposed Secret + + + +
+ {risk.file} +
+
+ {risk.author} +
+ {risk.email} +
+
{risk.isResolved ? "Resolved" : "Needs Attention"} + +
{timeSince(new Date(risk.createdAt))}{risk.ruleID} - - View Exposed Secret - - - -
- {risk.file}
-
- {risk.author}
- {risk.email} -
-
{risk.isResolved ? "Resolved" : "Needs Attention"} - -
- -
-
- ); -} \ No newline at end of file + ); + })} + {isLoading && } + {!isLoading && gitRisks && gitRisks?.length === 0 && ( +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx index 3caad28c5..e90d6ca8b 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx @@ -1,4 +1,4 @@ -import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { @@ -10,78 +10,63 @@ import { Td, Th, THead, - Tr} from "@app/components/v2"; + Tr +} from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { - useGetOrgPlanTable -} from "@app/hooks/api"; +import { useGetOrgPlanTable } from "@app/hooks/api"; export const CurrentPlanSection = () => { - const { currentOrg } = useOrganization(); - const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? ""); - - const displayCell = (value: null | number | string | boolean) => { - if (value === null) return "-"; - - if (typeof value === "boolean") { - if (value) return ( - - ); + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? ""); - return ( - - ); - } - - return value; + const displayCell = (value: null | number | string | boolean) => { + if (value === null) return "-"; + + if (typeof value === "boolean") { + if (value) return ; + + return ; } - return ( -
-

Current Usage

- - - - - - - - - - - {!isLoading && data && data?.rows?.length > 0 && data.rows.map(({ - name, - allowed, - used - }) => { - return ( - - - - - - ); - })} - {isLoading && } - {!isLoading && data && data?.rows?.length === 0 && ( - - - - )} - -
FeatureAllowedUsed
{name}{displayCell(allowed)}{used}
- -
-
-
- ); -} \ No newline at end of file + return value; + }; + + return ( +
+

Current Usage

+ + + + + + + + + + + {!isLoading && + data && + data?.rows?.length > 0 && + data.rows.map(({ name, allowed, used }) => { + return ( + + + + + + ); + })} + {isLoading && } + {!isLoading && data && data?.rows?.length === 0 && ( + + + + )} + +
FeatureAllowedUsed
{name}{displayCell(allowed)}{used}
+ +
+
+
+ ); +}; diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx index ffdcbc994..f057f7ff0 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx @@ -1,4 +1,4 @@ -import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { @@ -11,166 +11,129 @@ import { Td, Th, THead, - Tr, + Tr } from "@app/components/v2"; -import { useOrganization,useSubscription } from "@app/context"; -import { - useCreateCustomerPortalSession, - useGetOrgPlansTable} from "@app/hooks/api"; +import { useOrganization, useSubscription } from "@app/context"; +import { useCreateCustomerPortalSession, useGetOrgPlansTable } from "@app/hooks/api"; type Props = { - billingCycle: "monthly" | "yearly" -} + billingCycle: "monthly" | "yearly"; +}; -export const ManagePlansTable = ({ +export const ManagePlansTable = ({ billingCycle }: Props) => { + const { currentOrg } = useOrganization(); + const { subscription } = useSubscription(); + const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({ + organizationId: currentOrg?._id ?? "", billingCycle -}: Props) => { - const { currentOrg } = useOrganization(); - const { subscription } = useSubscription(); - const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({ - organizationId: currentOrg?._id ?? "", - billingCycle - }); - const createCustomerPortalSession = useCreateCustomerPortalSession(); + }); + const createCustomerPortalSession = useCreateCustomerPortalSession(); - const displayCell = (value: null | number | string | boolean) => { - if (value === null) return "Unlimited"; - - if (typeof value === "boolean") { - if (value) return ( - - ); + const displayCell = (value: null | number | string | boolean) => { + if (value === null) return "Unlimited"; - return ( - - ); - } - - return value; + if (typeof value === "boolean") { + if (value) return ; + + return ; } - return ( - - - - {subscription && !isTableDataLoading && tableData && ( - - - {tableData.head.map(({ - name, - priceLine - }) => { - return ( - - ); - })} - - )} - - - {subscription && !isTableDataLoading && tableData && tableData.rows.map(({ - name, - starter, - team, - pro, - enterprise - }) => { - return ( - - - - - - - - ); - })} - {isTableDataLoading && } - {!isTableDataLoading && tableData?.rows.length === 0 && ( - - - - )} - {subscription && !isTableDataLoading && tableData && ( - - - ) : ( - - ); - })} - - )} - -
Feature / Limit -

{name}

-

{priceLine}

-
{displayCell(name)} - {displayCell(starter)} - - {displayCell(team)} - - {displayCell(pro)} - - {displayCell(enterprise)} -
- -
- {tableData.head.map(({ - slug, - tier - }) => { - - const isCurrentPlan = slug === subscription.slug; - let subscriptionText = "Upgrade"; - - if (subscription.tier > tier) { - subscriptionText = "Downgrade" - } - - if (tier === 3) { - subscriptionText = "Contact sales" - } + return value; + }; - return isCurrentPlan ? ( - - - - -
-
- ); -} \ No newline at end of file + return ( + + + + {subscription && !isTableDataLoading && tableData && ( + + + {tableData.head.map(({ name, priceLine }) => { + return ( + + ); + })} + + )} + + + {subscription && + !isTableDataLoading && + tableData && + tableData.rows.map(({ name, starter, team, pro, enterprise }) => { + return ( + + + + + + + + ); + })} + {isTableDataLoading && } + {!isTableDataLoading && tableData?.rows.length === 0 && ( + + + + )} + {subscription && !isTableDataLoading && tableData && ( + + + ) : ( + + ); + })} + + )} + +
Feature / Limit +

{name}

+

{priceLine}

+
{displayCell(name)}{displayCell(starter)}{displayCell(team)}{displayCell(pro)}{displayCell(enterprise)}
+ +
+ {tableData.head.map(({ slug, tier }) => { + const isCurrentPlan = slug === subscription.slug; + let subscriptionText = "Upgrade"; + + if (subscription.tier > tier) { + subscriptionText = "Downgrade"; + } + + if (tier === 3) { + subscriptionText = "Contact sales"; + } + + return isCurrentPlan ? ( + + + + +
+
+ ); +}; diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsTable.tsx index 743106998..17f4fd21c 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsTable.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsTable.tsx @@ -14,78 +14,68 @@ import { Tr } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { - useDeleteOrgPmtMethod, - useGetOrgPmtMethods -} from "@app/hooks/api"; +import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api"; export const PmtMethodsTable = () => { - const { currentOrg } = useOrganization(); - const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? ""); - const deleteOrgPmtMethod = useDeleteOrgPmtMethod(); + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? ""); + const deleteOrgPmtMethod = useDeleteOrgPmtMethod(); - const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => { - if (!currentOrg?._id) return; - await deleteOrgPmtMethod.mutateAsync({ - organizationId: currentOrg._id, - pmtMethodId - }); - } + const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => { + if (!currentOrg?._id) return; + await deleteOrgPmtMethod.mutateAsync({ + organizationId: currentOrg._id, + pmtMethodId + }); + }; - return ( - - - - - - - - - - - - {!isLoading && data && data?.length > 0 && data.map(({ - _id, - brand, - exp_month, - exp_year, - funding, - last4 - }) => ( - - - - - - - - ))} - {isLoading && } - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
BrandTypeLast 4 DigitsExpiration -
{brand.charAt(0).toUpperCase() + brand.slice(1)}{funding.charAt(0).toUpperCase() + funding.slice(1)}{last4}{`${exp_month}/${exp_year}`} - { - await handleDeletePmtMethodBtnClick(_id); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - > - - -
- -
-
- ); -} \ No newline at end of file + return ( + + + + + + + + + + + + {!isLoading && + data && + data?.length > 0 && + data.map(({ _id, brand, exp_month, exp_year, funding, last4 }) => ( + + + + + + + + ))} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
BrandTypeLast 4 DigitsExpiration +
{brand.charAt(0).toUpperCase() + brand.slice(1)}{funding.charAt(0).toUpperCase() + funding.slice(1)}{last4}{`${exp_month}/${exp_year}`} + { + await handleDeletePmtMethodBtnClick(_id); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDTable.tsx index 873b41cf6..2779bb19d 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDTable.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDTable.tsx @@ -1,4 +1,4 @@ -import { faFileInvoice,faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { @@ -11,127 +11,120 @@ import { Td, Th, THead, - Tr, + Tr } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { - useDeleteOrgTaxId, - useGetOrgTaxIds -} from "@app/hooks/api"; +import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api"; const taxIDTypeLabelMap: { [key: string]: string } = { - "au_abn": "Australia ABN", - "au_arn": "Australia ARN", - "bg_uic": "Bulgaria UIC", - "br_cnpj": "Brazil CNPJ", - "br_cpf": "Brazil CPF", - "ca_bn": "Canada BN", - "ca_gst_hst": "Canada GST/HST", - "ca_pst_bc": "Canada PST BC", - "ca_pst_mb": "Canada PST MB", - "ca_pst_sk": "Canada PST SK", - "ca_qst": "Canada QST", - "ch_vat": "Switzerland VAT", - "cl_tin": "Chile TIN", - "eg_tin": "Egypt TIN", - "es_cif": "Spain CIF", - "eu_oss_vat": "EU OSS VAT", - "eu_vat": "EU VAT", - "gb_vat": "GB VAT", - "ge_vat": "Georgia VAT", - "hk_br": "Hong Kong BR", - "hu_tin": "Hungary TIN", - "id_npwp": "Indonesia NPWP", - "il_vat": "Israel VAT", - "in_gst": "India GST", - "is_vat": "Iceland VAT", - "jp_cn": "Japan CN", - "jp_rn": "Japan RN", - "jp_trn": "Japan TRN", - "ke_pin": "Kenya PIN", - "kr_brn": "South Korea BRN", - "li_uid": "Liechtenstein UID", - "mx_rfc": "Mexico RFC", - "my_frp": "Malaysia FRP", - "my_itn": "Malaysia ITN", - "my_sst": "Malaysia SST", - "no_vat": "Norway VAT", - "nz_gst": "New Zealand GST", - "ph_tin": "Philippines TIN", - "ru_inn": "Russia INN", - "ru_kpp": "Russia KPP", - "sa_vat": "Saudi Arabia VAT", - "sg_gst": "Singapore GST", - "sg_uen": "Singapore UEN", - "si_tin": "Slovenia TIN", - "th_vat": "Thailand VAT", - "tr_tin": "Turkey TIN", - "tw_vat": "Taiwan VAT", - "ua_vat": "Ukraine VAT", - "us_ein": "US EIN", - "za_vat": "South Africa VAT" + au_abn: "Australia ABN", + au_arn: "Australia ARN", + bg_uic: "Bulgaria UIC", + br_cnpj: "Brazil CNPJ", + br_cpf: "Brazil CPF", + ca_bn: "Canada BN", + ca_gst_hst: "Canada GST/HST", + ca_pst_bc: "Canada PST BC", + ca_pst_mb: "Canada PST MB", + ca_pst_sk: "Canada PST SK", + ca_qst: "Canada QST", + ch_vat: "Switzerland VAT", + cl_tin: "Chile TIN", + eg_tin: "Egypt TIN", + es_cif: "Spain CIF", + eu_oss_vat: "EU OSS VAT", + eu_vat: "EU VAT", + gb_vat: "GB VAT", + ge_vat: "Georgia VAT", + hk_br: "Hong Kong BR", + hu_tin: "Hungary TIN", + id_npwp: "Indonesia NPWP", + il_vat: "Israel VAT", + in_gst: "India GST", + is_vat: "Iceland VAT", + jp_cn: "Japan CN", + jp_rn: "Japan RN", + jp_trn: "Japan TRN", + ke_pin: "Kenya PIN", + kr_brn: "South Korea BRN", + li_uid: "Liechtenstein UID", + mx_rfc: "Mexico RFC", + my_frp: "Malaysia FRP", + my_itn: "Malaysia ITN", + my_sst: "Malaysia SST", + no_vat: "Norway VAT", + nz_gst: "New Zealand GST", + ph_tin: "Philippines TIN", + ru_inn: "Russia INN", + ru_kpp: "Russia KPP", + sa_vat: "Saudi Arabia VAT", + sg_gst: "Singapore GST", + sg_uen: "Singapore UEN", + si_tin: "Slovenia TIN", + th_vat: "Thailand VAT", + tr_tin: "Turkey TIN", + tw_vat: "Taiwan VAT", + ua_vat: "Ukraine VAT", + us_ein: "US EIN", + za_vat: "South Africa VAT" }; export const TaxIDTable = () => { - const { currentOrg } = useOrganization(); - const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? ""); - const deleteOrgTaxId = useDeleteOrgTaxId(); + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? ""); + const deleteOrgTaxId = useDeleteOrgTaxId(); - const handleDeleteTaxIdBtnClick = async (taxId: string) => { - if (!currentOrg?._id) return; - await deleteOrgTaxId.mutateAsync({ - organizationId: currentOrg._id, - taxId - }); - } + const handleDeleteTaxIdBtnClick = async (taxId: string) => { + if (!currentOrg?._id) return; + await deleteOrgTaxId.mutateAsync({ + organizationId: currentOrg._id, + taxId + }); + }; - return ( - - - - - - - - - - {!isLoading && data && data?.length > 0 && data.map(({ - _id, - type, - value - }) => ( - - - - - - ))} - {isLoading && } - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
TypeValue -
{taxIDTypeLabelMap[type]}{value} - { - await handleDeleteTaxIdBtnClick(_id); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - > - - -
- -
-
- ); -} \ No newline at end of file + return ( + + + + + + + + + + {!isLoading && + data && + data?.length > 0 && + data.map(({ _id, type, value }) => ( + + + + + + ))} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
TypeValue +
{taxIDTypeLabelMap[type]}{value} + { + await handleDeleteTaxIdBtnClick(_id); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/InvoicesTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/InvoicesTable.tsx index 612052a36..23a085280 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/InvoicesTable.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/InvoicesTable.tsx @@ -14,76 +14,67 @@ import { Tr } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { - useGetOrgInvoices -} from "@app/hooks/api"; +import { useGetOrgInvoices } from "@app/hooks/api"; export const InvoicesTable = () => { - const { currentOrg } = useOrganization(); - const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? ""); - return ( - - - - - - - - - + + + )} + +
Invoice #DateStatusAmount + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? ""); + return ( + + + + + + + + + + + + {!isLoading && + data && + data?.length > 0 && + data.map(({ _id, created, paid, number, total, invoice_pdf }) => { + const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD" + }); + const createdDate = new Date(created * 1000); + const day: number = createdDate.getDate(); + const month: number = createdDate.getMonth() + 1; + const year: number = createdDate.getFullYear(); + const formattedDate: string = `${day}/${month}/${year}`; + + return ( + + + + + + - - - {!isLoading && data && data?.length > 0 && data.map(({ - _id, - created, - paid, - number, - total, - invoice_pdf - }) => { - const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", { - style: "currency", - currency: "USD", - }); - const createdDate = new Date(created * 1000); - const day: number = createdDate.getDate(); - const month: number = createdDate.getMonth() + 1; - const year: number = createdDate.getFullYear(); - const formattedDate: string = `${day}/${month}/${year}`; - - return ( - - - - - - - - ); - })} - {isLoading && } - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
Invoice #DateStatusAmount +
{number}{formattedDate}{paid ? "Paid" : "Not Paid"}{formattedTotal} + window.open(invoice_pdf)} + size="lg" + variant="plain" + ariaLabel="update" + > + + +
{number}{formattedDate}{paid ? "Paid" : "Not Paid"}{formattedTotal} - window.open(invoice_pdf)} - size="lg" - variant="plain" - ariaLabel="update" - > - - -
- -
-
- ); -} \ No newline at end of file + ); + })} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx index 7e4b78570..dc56cce65 100644 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx +++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx @@ -1,407 +1,412 @@ import { useState } from "react"; -import { Controller,useForm } from "react-hook-form"; -import { - faKey, - faMagnifyingGlass, - faPlus, - faTrash} from "@fortawesome/free-solid-svg-icons"; +import { Controller, useForm } from "react-hook-form"; +import { faKey, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; -import { - decryptAssymmetric, - encryptAssymmetric, - verifyPrivateKey} from "@app/components/utilities/cryptography/crypto"; import { - Button, - Checkbox, - DeleteActionModal, - EmptyState, - FormControl, - IconButton, - Input, - Modal, - ModalClose, - ModalContent, - Select, - SelectItem, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr} from "@app/components/v2"; + decryptAssymmetric, + encryptAssymmetric, + verifyPrivateKey +} from "@app/components/utilities/cryptography/crypto"; +import { + Button, + Checkbox, + DeleteActionModal, + EmptyState, + FormControl, + IconButton, + Input, + Modal, + ModalClose, + ModalContent, + Select, + SelectItem, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; import { usePopUp } from "@app/hooks"; import { - useCreateServiceAccountProjectLevelPermission, - useDeleteServiceAccountProjectLevelPermission, - useGetServiceAccountById, - useGetServiceAccountProjectLevelPermissions, - useGetUserWorkspaces + useCreateServiceAccountProjectLevelPermission, + useDeleteServiceAccountProjectLevelPermission, + useGetServiceAccountById, + useGetServiceAccountProjectLevelPermissions, + useGetUserWorkspaces } from "@app/hooks/api"; import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; const createProjectLevelPermissionSchema = yup.object({ - privateKey: yup.string().required().label("Private Key"), - workspace: yup.string().required().label("Workspace"), - environment: yup.string().required().label("Environment"), - permissions: yup.object().shape({ - read: yup.boolean().required(), - write: yup.boolean().required() - }).defined().required() + privateKey: yup.string().required().label("Private Key"), + workspace: yup.string().required().label("Workspace"), + environment: yup.string().required().label("Environment"), + permissions: yup + .object() + .shape({ + read: yup.boolean().required(), + write: yup.boolean().required() + }) + .defined() + .required() }); type CreateProjectLevelPermissionForm = yup.InferType; type Props = { - serviceAccountId: string; -} + serviceAccountId: string; +}; -export const SAProjectLevelPermissionsTable = ({ - serviceAccountId -}: Props): JSX.Element => { - const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId); - const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces(); - const [searchPermissions, setSearchPermissions] = useState(""); +export const SAProjectLevelPermissionsTable = ({ serviceAccountId }: Props): JSX.Element => { + const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId); + const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces(); + const [searchPermissions, setSearchPermissions] = useState(""); - const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } = useGetServiceAccountProjectLevelPermissions(serviceAccountId); - - const createServiceAccountProjectLevelPermission = useCreateServiceAccountProjectLevelPermission(); - const deleteServiceAccountProjectLevelPermission = useDeleteServiceAccountProjectLevelPermission(); + const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } = + useGetServiceAccountProjectLevelPermissions(serviceAccountId); - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "addProjectLevelPermission", - "removeProjectLevelPermission", - ] as const); - - const [, setSelectedWorkspace] = useState(undefined); + const createServiceAccountProjectLevelPermission = + useCreateServiceAccountProjectLevelPermission(); + const deleteServiceAccountProjectLevelPermission = + useDeleteServiceAccountProjectLevelPermission(); - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ resolver: yupResolver(createProjectLevelPermissionSchema) }) + const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + "addProjectLevelPermission", + "removeProjectLevelPermission" + ] as const); - const onAddProjectLevelPermission = async ({ - privateKey, - workspace, - environment, - permissions: { read, write } - }: CreateProjectLevelPermissionForm) => { - - // TODO: clean up / modularize this function - - if (!serviceAccount) return; - - const { latestKey } = await getLatestFileKey({ - workspaceId: workspace - }); + const [, setSelectedWorkspace] = useState(undefined); - verifyPrivateKey({ - privateKey, - publicKey: serviceAccount.publicKey - }); - - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(createProjectLevelPermissionSchema) + }); - const key = decryptAssymmetric({ - ciphertext: latestKey.encryptedKey, - nonce: latestKey.nonce, - publicKey: latestKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey: serviceAccount.publicKey, - privateKey - }); - - await createServiceAccountProjectLevelPermission.mutateAsync({ - serviceAccountId, - workspaceId: workspace, - environment, - read, - write, - encryptedKey: ciphertext, - nonce - }); - handlePopUpClose("addProjectLevelPermission"); - } - - const onRemoveProjectLevelPermission = async () => { - const serviceAccountWorkspacePermissionId = (popUp?.removeProjectLevelPermission?.data as { _id: string })?._id; - await deleteServiceAccountProjectLevelPermission.mutateAsync({ - serviceAccountId, - serviceAccountWorkspacePermissionId - }); - handlePopUpClose("removeProjectLevelPermission"); - } + const onAddProjectLevelPermission = async ({ + privateKey, + workspace, + environment, + permissions: { read, write } + }: CreateProjectLevelPermissionForm) => { + // TODO: clean up / modularize this function - return ( -
-

Project-Level Permissions

-
-
- setSearchPermissions(e.target.value)} - leftIcon={} - placeholder="Search service account project-level permissions..." - /> -
- -
- - - - - - - - - - - - {isPermissionsLoading && } - {!isPermissionsLoading && serviceAccountWorkspacePermissions && ( - serviceAccountWorkspacePermissions.map(({ - _id, - workspace, - environment, - read, - write - }) => { - const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name; - return ( - - - - - - - - ); - }) - )} - {!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && ( - - - - )} - -
ProjectEnvironmentReadWrite -
{workspace.name}{environmentName} - {/**/} - - {/**/} - - handlePopUpOpen("removeProjectLevelPermission", { _id })} - > - - -
- -
-
- { - handlePopUpToggle("addProjectLevelPermission", isOpen); - }} - > - -
- {!isUserWorkspacesLoading && userWorkspaces && ( - <> - ( - - - - )} - /> - ( - - - - )} - /> - { - /* eslint-disable-next-line no-underscore-dangle */ - const environments = userWorkspaces?.find((userWorkspace) => userWorkspace._id === control?._formValues?.workspace)?.environments ?? []; - return ( - - - - ); - }} - /> - - )} - { - const options = [ - { - label: "Read (default)", - value: "read" - }, - { - label: "Write", - value: "write" - } - ]; - - return ( - - <> - {options.map(({ label, value: optionValue }) => { - return ( - { - onChange({ - ...value, - [optionValue]: state - }); - }} - > - {label} - - ); - })} - - - ); - }} - /> -
- - - - -
- -
-
- handlePopUpToggle("removeProjectLevelPermission", isOpen)} - onDeleteApproved={onRemoveProjectLevelPermission} - /> + if (!serviceAccount) return; + + const { latestKey } = await getLatestFileKey({ + workspaceId: workspace + }); + + verifyPrivateKey({ + privateKey, + publicKey: serviceAccount.publicKey + }); + + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + + const key = decryptAssymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: key, + publicKey: serviceAccount.publicKey, + privateKey + }); + + await createServiceAccountProjectLevelPermission.mutateAsync({ + serviceAccountId, + workspaceId: workspace, + environment, + read, + write, + encryptedKey: ciphertext, + nonce + }); + handlePopUpClose("addProjectLevelPermission"); + }; + + const onRemoveProjectLevelPermission = async () => { + const serviceAccountWorkspacePermissionId = ( + popUp?.removeProjectLevelPermission?.data as { _id: string } + )?._id; + await deleteServiceAccountProjectLevelPermission.mutateAsync({ + serviceAccountId, + serviceAccountWorkspacePermissionId + }); + handlePopUpClose("removeProjectLevelPermission"); + }; + + return ( +
+

Project-Level Permissions

+
+
+ setSearchPermissions(e.target.value)} + leftIcon={} + placeholder="Search service account project-level permissions..." + />
- ); -} \ No newline at end of file + +
+ + + + + + + + + + + + {isPermissionsLoading && ( + + )} + {!isPermissionsLoading && + serviceAccountWorkspacePermissions && + serviceAccountWorkspacePermissions.map( + ({ _id, workspace, environment, read, write }) => { + const environmentName = workspace.environments.find( + (env) => env.slug === environment + )?.name; + return ( + + + + + + + + ); + } + )} + {!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && ( + + + + )} + +
ProjectEnvironmentReadWrite +
{workspace.name}{environmentName} + + {/**/} + + + + {/**/} + + + handlePopUpOpen("removeProjectLevelPermission", { _id })} + > + + +
+ +
+
+ { + handlePopUpToggle("addProjectLevelPermission", isOpen); + }} + > + +
+ {!isUserWorkspacesLoading && userWorkspaces && ( + <> + ( + + + + )} + /> + ( + + + + )} + /> + { + const environments = + userWorkspaces?.find( + /* eslint-disable-next-line no-underscore-dangle */ + (userWorkspace) => userWorkspace._id === control?._formValues?.workspace + )?.environments ?? []; + return ( + + + + ); + }} + /> + + )} + { + const options = [ + { + label: "Read (default)", + value: "read" + }, + { + label: "Write", + value: "write" + } + ]; + + return ( + + <> + {options.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} + + + ); + }} + /> +
+ + + + +
+ +
+
+ handlePopUpToggle("removeProjectLevelPermission", isOpen)} + onDeleteApproved={onRemoveProjectLevelPermission} + /> +
+ ); +}; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsTable.tsx index f2c5d1208..0d3011572 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsTable.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsTable.tsx @@ -1,9 +1,5 @@ import { useState } from "react"; -import { - faContactBook, - faMagnifyingGlass, - faTrash -} from "@fortawesome/free-solid-svg-icons"; +import { faContactBook, faMagnifyingGlass, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; @@ -23,13 +19,11 @@ import { } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { - useDeleteIncidentContact, - useGetOrgIncidentContact} from "@app/hooks/api"; +import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api"; export const OrgIncidentContactsTable = () => { - const { createNotification } = useNotificationContext(); - const { currentOrg } = useOrganization(); + const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); const { data: contacts, isLoading } = useGetOrgIncidentContact(currentOrg?._id ?? ""); const [searchContact, setSearchContact] = useState(""); const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ @@ -40,71 +34,71 @@ export const OrgIncidentContactsTable = () => { const onRemoveIncidentContact = async () => { try { - const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email; - - if (!currentOrg?._id) return; - await mutateAsync({ - orgId: currentOrg._id, - email: incidentContactEmail - }); + const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email; - createNotification({ - text: "Successfully removed incident contact", - type: "success" - }); - - handlePopUpClose("removeContact"); + if (!currentOrg?._id) return; + await mutateAsync({ + orgId: currentOrg._id, + email: incidentContactEmail + }); + + createNotification({ + text: "Successfully removed incident contact", + type: "success" + }); + + handlePopUpClose("removeContact"); } catch (err) { - console.error(err); - createNotification({ - text: "Failed to remove incident contact", - type: "error" - }); + console.error(err); + createNotification({ + text: "Failed to remove incident contact", + type: "error" + }); } }; - const filteredContacts = contacts ? contacts.filter(({ email }) => - email.toLocaleLowerCase().includes(searchContact) - ) : []; + const filteredContacts = contacts + ? contacts.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact)) + : []; return (
- setSearchContact(e.target.value)} - leftIcon={} - placeholder="Search incident contact by email..." - /> - - - - - -
Email + setSearchContact(e.target.value)} + leftIcon={} + placeholder="Search incident contact by email..." + /> + + + + + + + + + {isLoading && } + {filteredContacts?.map(({ email }) => ( + + + - - - {isLoading && } - {filteredContacts?.map(({ email }) => ( - - - - - ))} - -
Email +
{email} + handlePopUpOpen("removeContact", { email })} + > + + +
{email} - handlePopUpOpen("removeContact", { email })} - > - - -
- {filteredContacts?.length === 0 && !isLoading && ( - - )} -
+ ))} + +
+ {filteredContacts?.length === 0 && !isLoading && ( + + )} +
; export const OrgServiceAccountsTable = () => { - const router = useRouter(); - const { currentOrg } = useOrganization(); - const { currentWorkspace } = useWorkspace(); - - const orgId = currentOrg?._id || ""; - const [step, setStep] = useState(0); - const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false); - const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false); - const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false); - const [accessKey] = useState(""); - const [publicKey] = useState(""); - const [privateKey] = useState(""); - const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState(""); - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "addServiceAccount", - "removeServiceAccount", - ] as const); + const router = useRouter(); + const { currentOrg } = useOrganization(); + const { currentWorkspace } = useWorkspace(); - const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId); - - // const createServiceAccount = useCreateServiceAccount(); - const removeServiceAccount = useDeleteServiceAccount(); - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isAccessKeyCopied) { - timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000); - } + const orgId = currentOrg?._id || ""; + const [step, setStep] = useState(0); + const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false); + const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false); + const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false); + const [accessKey] = useState(""); + const [publicKey] = useState(""); + const [privateKey] = useState(""); + const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState(""); + const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + "addServiceAccount", + "removeServiceAccount" + ] as const); - if (isPublicKeyCopied) { - timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000); - } + const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = + useGetServiceAccounts(orgId); - if (isPrivateKeyCopied) { - timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000); - } + // const createServiceAccount = useCreateServiceAccount(); + const removeServiceAccount = useDeleteServiceAccount(); - return () => clearTimeout(timer); - }, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]); - - // const { - // control, - // handleSubmit, - // reset, - // formState: { isSubmitting } - // } = useForm({ resolver: yupResolver(addServiceAccountFormSchema) }); - - // const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => { - // if (!currentOrg?._id) return; - - // const keyPair = generateKeyPair(); - // setPublicKey(keyPair.publicKey); - // setPrivateKey(keyPair.privateKey); - - // const serviceAccountDetails = await createServiceAccount.mutateAsync({ - // name, - // organizationId: currentOrg?._id, - // publicKey: keyPair.publicKey, - // expiresIn: Number(expiresIn) - // }); - - // setAccessKey(serviceAccountDetails.serviceAccountAccessKey); - - // setStep(1); - // reset(); - // } - - const onRemoveServiceAccount = async () => { - const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id; - await removeServiceAccount.mutateAsync(serviceAccountId); - handlePopUpClose("removeServiceAccount"); + useEffect(() => { + let timer: NodeJS.Timeout; + if (isAccessKeyCopied) { + timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000); } - - const filteredServiceAccounts = useMemo( - () => - serviceAccounts.filter( - ({ name }) => - name.toLowerCase().includes(searchServiceAccountFilter) - ), - [serviceAccounts, searchServiceAccountFilter] - ); - - const renderStep = (stepToRender: number) => { - switch (stepToRender) { - case 0: - return ( -
- We are currently revising the service account mechanism. In the meantime, - please use service tokens or API key to fetch secrets via API request. -
- //
- // ( - // - // - // - // )} - // /> - // { - // return ( - // - // - // - // ); - // }} - // /> - //
- // - // - //
- // - ); - case 1: - return ( - <> -

Access Key

-
-

{accessKey}

- { - navigator.clipboard.writeText(accessKey); - setIsAccessKeyCopied.on(); - }} - > - - - Copy - - -
-

Public Key

-
-

{publicKey}

- { - navigator.clipboard.writeText(publicKey); - setIsPublicKeyCopied.on(); - }} - > - - - Copy - - -
-

Private Key

-
-

{privateKey}

- { - navigator.clipboard.writeText(privateKey); - setIsPrivateKeyCopied.on(); - }} - > - - - Copy - - -
- - - ); - default: - return
- } + + if (isPublicKeyCopied) { + timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000); } - - return ( -
-
-

Service Accounts

- -
- setSearchServiceAccountFilter(e.target.value)} - leftIcon={} - placeholder="Search service accounts..." - /> - - - - - - - - {isServiceAccountsLoading && } - {!isServiceAccountsLoading && ( - filteredServiceAccounts.map(({ - name, - expiresAt, - _id: serviceAccountId - }) => { - return ( - - - - - - ); - }) - )} - -
NameValid Until -
{name}{new Date(expiresAt).toUTCString()} -
- { - if (currentWorkspace?._id) { - router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`); - } - }} - className="mr-2" - > - - - handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })} - > - - -
-
- {!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && ( - - )} -
- { - handlePopUpToggle("addServiceAccount", isOpen); - // reset(); + + if (isPrivateKeyCopied) { + timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000); + } + + return () => clearTimeout(timer); + }, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]); + + // const { + // control, + // handleSubmit, + // reset, + // formState: { isSubmitting } + // } = useForm({ resolver: yupResolver(addServiceAccountFormSchema) }); + + // const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => { + // if (!currentOrg?._id) return; + + // const keyPair = generateKeyPair(); + // setPublicKey(keyPair.publicKey); + // setPrivateKey(keyPair.privateKey); + + // const serviceAccountDetails = await createServiceAccount.mutateAsync({ + // name, + // organizationId: currentOrg?._id, + // publicKey: keyPair.publicKey, + // expiresIn: Number(expiresIn) + // }); + + // setAccessKey(serviceAccountDetails.serviceAccountAccessKey); + + // setStep(1); + // reset(); + // } + + const onRemoveServiceAccount = async () => { + const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id; + await removeServiceAccount.mutateAsync(serviceAccountId); + handlePopUpClose("removeServiceAccount"); + }; + + const filteredServiceAccounts = useMemo( + () => + serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)), + [serviceAccounts, searchServiceAccountFilter] + ); + + const renderStep = (stepToRender: number) => { + switch (stepToRender) { + case 0: + return ( +
+ We are currently revising the service account mechanism. In the meantime, please use + service tokens or API key to fetch secrets via API request. +
+ //
+ // ( + // + // + // + // )} + // /> + // { + // return ( + // + // + // + // ); + // }} + // /> + //
+ // + // + //
+ // + ); + case 1: + return ( + <> +

Access Key

+
+

{accessKey}

+ { + navigator.clipboard.writeText(accessKey); + setIsAccessKeyCopied.on(); }} - > - - {renderStep(step)} - - - handlePopUpToggle("removeServiceAccount", isOpen)} - onDeleteApproved={onRemoveServiceAccount} - /> -
- ); -} \ No newline at end of file + > + + + Copy + + +
+

Public Key

+
+

{publicKey}

+ { + navigator.clipboard.writeText(publicKey); + setIsPublicKeyCopied.on(); + }} + > + + + Copy + + +
+

Private Key

+
+

{privateKey}

+ { + navigator.clipboard.writeText(privateKey); + setIsPrivateKeyCopied.on(); + }} + > + + + Copy + + +
+ + ); + default: + return
; + } + }; + + return ( +
+
+

Service Accounts

+ +
+ setSearchServiceAccountFilter(e.target.value)} + leftIcon={} + placeholder="Search service accounts..." + /> + + + + + + + + {isServiceAccountsLoading && ( + + )} + {!isServiceAccountsLoading && + filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => { + return ( + + + + + + ); + })} + +
NameValid Until +
{name}{new Date(expiresAt).toUTCString()} +
+ { + if (currentWorkspace?._id) { + router.push( + `/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}` + ); + } + }} + className="mr-2" + > + + + + handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId }) + } + > + + +
+
+ {!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && ( + + )} +
+ { + handlePopUpToggle("addServiceAccount", isOpen); + // reset(); + }} + > + + {renderStep(step)} + + + handlePopUpToggle("removeServiceAccount", isOpen)} + onDeleteApproved={onRemoveServiceAccount} + /> +
+ ); +}; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx b/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx index 5efaf1281..40a309eb0 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx @@ -1,4 +1,4 @@ -import { faKey,faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; @@ -14,99 +14,91 @@ import { THead, Tr } from "@app/components/v2"; -import { - useDeleteAPIKey, - useGetMyAPIKeys} from "@app/hooks/api"; +import { useDeleteAPIKey, useGetMyAPIKeys } from "@app/hooks/api"; export const APIKeyTable = () => { - const { createNotification } = useNotificationContext(); - const { data, isLoading } = useGetMyAPIKeys(); - const { mutateAsync } = useDeleteAPIKey(); + const { createNotification } = useNotificationContext(); + const { data, isLoading } = useGetMyAPIKeys(); + const { mutateAsync } = useDeleteAPIKey(); - const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => { - try { - await mutateAsync(apiKeyDataId); - createNotification({ - text: "Successfully deleted API key", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete API key", - type: "error" - }); - } + const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => { + try { + await mutateAsync(apiKeyDataId); + createNotification({ + text: "Successfully deleted API key", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete API key", + type: "error" + }); } - - const formatDate = (dateToFormat: string) => { - const date = new Date(dateToFormat); - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - const formattedDate = `${day}/${month}/${year}`; - - return formattedDate; - } - - return ( -
- - - - - - - - - - - - {isLoading && } - {!isLoading && data && data.length > 0 && data.map(({ - _id, - name, - createdAt, - expiresAt, - lastUsed - }) => { - return ( - - - - - - - - ); - })} - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
NameLast activeCreatedExpiration -
{name}{formatDate(lastUsed)}{formatDate(createdAt)}{formatDate(expiresAt)} - { - await handleDeleteAPIKeyDataClick(_id); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - > - - -
- -
-
-
- ); -} \ No newline at end of file + }; + + const formatDate = (dateToFormat: string) => { + const date = new Date(dateToFormat); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + const formattedDate = `${day}/${month}/${year}`; + + return formattedDate; + }; + + return ( +
+ + + + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => { + return ( + + + + + + + + ); + })} + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
NameLast activeCreatedExpiration +
{name}{formatDate(lastUsed)}{formatDate(createdAt)}{formatDate(expiresAt)} + { + await handleDeleteAPIKeyDataClick(_id); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+
+ ); +}; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/SessionsSection/SessionsTable.tsx b/frontend/src/views/Settings/PersonalSettingsPage/SessionsSection/SessionsTable.tsx index b49771165..8a964b8d6 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/SessionsSection/SessionsTable.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/SessionsSection/SessionsTable.tsx @@ -14,61 +14,54 @@ import { import { useGetMySessions } from "@app/hooks/api"; export const SessionsTable = () => { - const { data, isLoading } = useGetMySessions(); + const { data, isLoading } = useGetMySessions(); - const formatDate = (dateToFormat: string) => { - const date = new Date(dateToFormat); - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - const formattedDate = `${day}/${month}/${year}`; - - return formattedDate; - } - - return ( - - - - - - - - - - - - {isLoading && } - {!isLoading && data && data.length > 0 && data.map(({ - _id, - createdAt, - lastUsed, - ip, - userAgent - }) => { - return ( - - - - - - - ); - })} - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
CreatedLast activeIP addressDevice
{formatDate(createdAt)}{formatDate(lastUsed)}{ip}{userAgent}
- -
- -
- ); -} \ No newline at end of file + const formatDate = (dateToFormat: string) => { + const date = new Date(dateToFormat); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + const formattedDate = `${day}/${month}/${year}`; + + return formattedDate; + }; + + return ( + + + + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map(({ _id, createdAt, lastUsed, ip, userAgent }) => { + return ( + + + + + + + ); + })} + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
CreatedLast activeIP addressDevice
{formatDate(createdAt)}{formatDate(lastUsed)}{ip}{userAgent}
+ +
+
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentTable.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentTable.tsx index 4e901989d..e749e4e9a 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentTable.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentTable.tsx @@ -11,79 +11,79 @@ import { Td, Th, THead, - Tr, + Tr } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { - handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>, - { - name, - slug - }: { - name: string; - slug: string; - } - ) => void; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>, + { + name, + slug + }: { + name: string; + slug: string; + } + ) => void; }; -export const EnvironmentTable = ({ - handlePopUpOpen -}: Props) => { - const { currentWorkspace, isLoading } = useWorkspace(); - return ( - - - - - - - - - - {isLoading && } - {!isLoading && currentWorkspace && currentWorkspace.environments.map(({ name, slug }) => ( - - - - - - ))} - {!isLoading && currentWorkspace && currentWorkspace.environments?.length === 0 && ( - - - - )} - -
NameSlug -
{name}{slug} - { - handlePopUpOpen("updateEnv", { name, slug }); - }} - colorSchema="primary" - variant="plain" - ariaLabel="update" - > - - - { - handlePopUpOpen("deleteEnv", { name, slug }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - > - - -
- -
-
- ); -} \ No newline at end of file +export const EnvironmentTable = ({ handlePopUpOpen }: Props) => { + const { currentWorkspace, isLoading } = useWorkspace(); + return ( + + + + + + + + + + {isLoading && } + {!isLoading && + currentWorkspace && + currentWorkspace.environments.map(({ name, slug }) => ( + + + + + + ))} + {!isLoading && currentWorkspace && currentWorkspace.environments?.length === 0 && ( + + + + )} + +
NameSlug +
{name}{slug} + { + handlePopUpOpen("updateEnv", { name, slug }); + }} + colorSchema="primary" + variant="plain" + ariaLabel="update" + > + + + { + handlePopUpOpen("deleteEnv", { name, slug }); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx index 0279cf62b..cb8a0afd4 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx @@ -18,65 +18,65 @@ import { useGetWsTags } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { - handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteTagConfirmation"]>, - { - name, - id - }: { - name: string; - id: string; - } - ) => void; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deleteTagConfirmation"]>, + { + name, + id + }: { + name: string; + id: string; + } + ) => void; }; -export const SecretTagsTable = ({ - handlePopUpOpen -}: Props) => { - const { currentWorkspace }= useWorkspace(); - const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? ""); +export const SecretTagsTable = ({ handlePopUpOpen }: Props) => { + const { currentWorkspace } = useWorkspace(); + const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? ""); - return ( - - - - - - - - - - {isLoading && } - {!isLoading && data && data.map(({ _id, name, slug }) => ( - - - - - - ))} - {!isLoading && data && data?.length === 0 && ( - - + + + )} + +
TagSlug -
{name}{slug} - - handlePopUpOpen("deleteTagConfirmation", { - name, - id: _id - }) - } - colorSchema="danger" - ariaLabel="update" - > - - -
- + return ( + + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.map(({ _id, name, slug }) => ( + + + + - )} - -
TagSlug +
{name}{slug} + + handlePopUpOpen("deleteTagConfirmation", { + name, + id: _id + }) + } + colorSchema="danger" + ariaLabel="update" + > + +
-
- ); -} \ No newline at end of file + ))} + {!isLoading && data && data?.length === 0 && ( +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx index ce39abde2..8c40cef35 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx @@ -48,7 +48,7 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => { - {isLoading && } + {isLoading && } {!isLoading && data && data.map((row) => ( diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx index 76b928ace..598af0360 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx @@ -160,7 +160,7 @@ export const WebhooksTab = () => { - {isWebhooksLoading && } + {isWebhooksLoading && } {!isWebhooksLoading && webhooks && webhooks?.length === 0 && ( From 7bbbdcc58be3af55cdb24c77cc6b41ae66ccb15c Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Thu, 27 Jul 2023 16:28:50 +0530 Subject: [PATCH 23/31] feat: implemented new overview page with improvement in dashboard --- frontend/package-lock.json | 334 +++++++- frontend/package.json | 3 + frontend/src/styles/globals.css | 35 +- .../DashboardPage/DashboardEnvOverview.tsx | 326 -------- .../src/views/DashboardPage/DashboardPage.tsx | 779 +++++++++--------- .../DashboardPage/DashboardPage.utils.ts | 29 +- .../EnvComparisonRow/EnvComparisonRow.tsx | 135 --- .../EnvComparisonRow/FolderComparisonRow.tsx | 42 - .../components/EnvComparisonRow/index.tsx | 1 - .../FolderSection/FolderSection.tsx | 131 +-- .../SecretImportSection/SecretImportItem.tsx | 4 +- .../SecretImportSection.tsx | 56 +- .../components/SecretInputRow/MaskedInput.tsx | 107 --- .../SecretInputRow/SecretInputRow.tsx | 181 ++-- .../SecretOverviewPage/SecretOverviewPage.tsx | 315 ++++++- .../FolderBreadCrumbs/FolderBreadCrumbs.tsx | 57 ++ .../components/FolderBreadCrumbs/index.tsx | 1 + .../SecretOverviewFolderRow.tsx | 48 ++ .../SecretOverviewFolderRow/index.tsx | 1 + .../SecretOverviewTableRow/SecretEditRow.tsx | 171 ++++ .../SecretOverviewTableRow.tsx | 126 +++ .../SecretOverviewTableRow/index.tsx | 1 + 22 files changed, 1729 insertions(+), 1154 deletions(-) delete mode 100644 frontend/src/views/DashboardPage/DashboardEnvOverview.tsx delete mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx delete mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx delete mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx delete mode 100644 frontend/src/views/DashboardPage/components/SecretInputRow/MaskedInput.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/FolderBreadCrumbs/index.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/SecretOverviewFolderRow/SecretOverviewFolderRow.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/SecretOverviewFolderRow/index.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5853ff650..d25ff5cf1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -67,6 +67,7 @@ "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", + "react-contenteditable": "^3.3.7", "react-dom": "^17.0.2", "react-grid-layout": "^1.3.4", "react-hook-form": "^7.43.0", @@ -75,6 +76,7 @@ "react-markdown": "^8.0.3", "react-redux": "^8.0.2", "react-table": "^7.8.0", + "sanitize-html": "^2.11.0", "set-cookie-parser": "^2.5.1", "sharp": "^0.32.0", "styled-components": "^5.3.7", @@ -99,6 +101,7 @@ "@types/jsrp": "^0.2.4", "@types/node": "18.11.9", "@types/react": "^18.0.26", + "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.7", @@ -7922,6 +7925,89 @@ "redux": "^4.0.0" } }, + "node_modules/@types/sanitize-html": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz", + "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, + "node_modules/@types/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -10745,7 +10831,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11203,7 +11288,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -12690,8 +12774,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -17860,6 +17943,11 @@ "node": ">= 0.10" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -18113,7 +18201,6 @@ "version": "8.4.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -18936,6 +19023,18 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-contenteditable": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz", + "integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, "node_modules/react-docgen": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz", @@ -20017,6 +20116,88 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/sass-loader": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", @@ -28425,6 +28606,66 @@ "redux": "^4.0.0" } }, + "@types/sanitize-html": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz", + "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==", + "dev": true, + "requires": { + "htmlparser2": "^8.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + } + } + }, "@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -30633,8 +30874,7 @@ "deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, "default-browser": { "version": "4.0.0", @@ -30963,8 +31203,7 @@ "domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" }, "domhandler": { "version": "4.3.1", @@ -32126,8 +32365,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-diff": { "version": "1.3.0", @@ -35865,6 +36103,11 @@ "dev": true, "peer": true }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -36081,7 +36324,6 @@ "version": "8.4.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", - "dev": true, "requires": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -36653,6 +36895,15 @@ "dev": true, "requires": {} }, + "react-contenteditable": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz", + "integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==", + "requires": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + } + }, "react-docgen": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz", @@ -37421,6 +37672,65 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + } + } + }, "sass-loader": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0430844e7..51028a59b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -75,6 +75,7 @@ "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", + "react-contenteditable": "^3.3.7", "react-dom": "^17.0.2", "react-grid-layout": "^1.3.4", "react-hook-form": "^7.43.0", @@ -83,6 +84,7 @@ "react-markdown": "^8.0.3", "react-redux": "^8.0.2", "react-table": "^7.8.0", + "sanitize-html": "^2.11.0", "set-cookie-parser": "^2.5.1", "sharp": "^0.32.0", "styled-components": "^5.3.7", @@ -107,6 +109,7 @@ "@types/jsrp": "^0.2.4", "@types/node": "18.11.9", "@types/react": "^18.0.26", + "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.7", diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index de4adfff4..9c1afd84d 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -13,6 +13,11 @@ .flex-3 { flex-grow: 3; } + + .min-table-row { + width: 1%; + white-space: nowrap; + } } @layer components { @@ -40,7 +45,7 @@ .breadcrumb::after, .breadcrumb::before { - content: ''; + content: ""; height: 60%; width: 100%; z-index: -1; @@ -58,18 +63,32 @@ } .breadcrumb::after { - left: 5px; - bottom: -3px; + left: 4px; + bottom: -2.5px; transform: skew(-30deg); } .breadcrumb::before { - left: 5px; - top: -3px; + left: 4px; + top: -2.5px; transform: skew(30deg); } + + .thin-scrollbar::-webkit-scrollbar { + width: 0.25rem; + background-color: transparent; + } + + .thin-scrollbar::-webkit-scrollbar-thumb { + background-color: gray; + } + + .thin-scrollbar { + scrollbar-width: thin; + scrollbar-color: gray transparent; + } } -@import '@fontsource/inter/400.css'; -@import '@fontsource/inter/500.css'; -@import '@fontsource/inter/700.css'; +@import "@fontsource/inter/400.css"; +@import "@fontsource/inter/500.css"; +@import "@fontsource/inter/700.css"; diff --git a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx deleted file mode 100644 index 05dfe938b..000000000 --- a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useRouter } from "next/router"; -import { faFolderOpen, faKey, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import NavHeader from "@app/components/navigation/NavHeader"; -import { Button, Input, TableContainer, Tooltip } from "@app/components/v2"; -import { useOrganization, useWorkspace } from "@app/context"; -import { - useGetProjectFoldersBatch, - useGetProjectSecretsByKey, - useGetUserWsEnvironments, - useGetUserWsKey -} from "@app/hooks/api"; - -import { EnvComparisonRow } from "./components/EnvComparisonRow"; -import { FolderComparisonRow } from "./components/EnvComparisonRow/FolderComparisonRow"; - -export const DashboardEnvOverview = () => { - const { t } = useTranslation(); - const router = useRouter(); - - const { currentWorkspace, isLoading } = useWorkspace(); - const { currentOrg } = useOrganization(); - const workspaceId = currentWorkspace?._id as string; - const { data: latestFileKey } = useGetUserWsKey(workspaceId); - const [searchFilter, setSearchFilter] = useState(""); - const secretPath = router.query?.secretPath as string; - - useEffect(() => { - if (!isLoading && !workspaceId && router.isReady) { - router.push(`/org/${currentOrg?._id}/overview`); - } - }, [isLoading, workspaceId, router.isReady]); - - const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ - workspaceId - }); - - const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied); - - const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecretsByKey({ - workspaceId, - env: userAvailableEnvs?.map((env) => env.slug) ?? [], - decryptFileKey: latestFileKey!, - isPaused: false, - secretPath - }); - - const folders = useGetProjectFoldersBatch({ - folders: - userAvailableEnvs?.map((env) => ({ - environment: env.slug, - workspaceId - })) ?? [], - parentFolderPath: secretPath - }); - - const foldersGroupedByEnv = useMemo(() => { - const res: Record> = {}; - folders.forEach(({ data }) => { - data?.folders - ?.filter(({ name }) => name.toLowerCase().includes(searchFilter)) - ?.forEach((folder) => { - if (!res?.[folder.name]) res[folder.name] = {}; - res[folder.name][data.environment] = true; - }); - }); - return res; - }, [folders, userAvailableEnvs, searchFilter]); - - const numSecretsMissingPerEnv = useMemo(() => { - // first get all sec in the env then subtract with total to get missing ones - const secPerEnvMissing: Record = Object.fromEntries( - (userAvailableEnvs || [])?.map(({ slug }) => [slug, 0]) - ); - Object.keys(secrets?.secrets || {}).forEach((key) => - secrets?.secrets?.[key].forEach((val) => { - secPerEnvMissing[val.env] += 1; - }) - ); - Object.keys(secPerEnvMissing).forEach((k) => { - secPerEnvMissing[k] = (secrets?.uniqueSecCount || 0) - secPerEnvMissing[k]; - }); - return secPerEnvMissing; - }, [secrets, userAvailableEnvs]); - - const onExploreEnv = (slug: string) => { - const query: Record = { ...router.query, env: slug }; - delete query.secretPath; - // the dir return will have the present directory folder id - // use that when clicking on explore to redirect user to there - const envFolder = folders.find(({ data }) => slug === data?.environment); - const dir = envFolder?.data?.dir?.pop(); - if (dir) { - query.folderId = dir.id; - } - - router.push({ - pathname: router.pathname, - query - }); - }; - - const onFolderClick = (path: string) => { - router.push({ - pathname: router.pathname, - query: { - ...router.query, - secretPath: `${router.query?.secretPath || ""}/${path}` - } - }); - }; - - const onFolderCrumbClick = (index: number) => { - const newSecPath = secretPath.split("/").filter(Boolean).slice(0, index).join("/"); - const query = { ...router.query, secretPath: `/${newSecPath}` } as Record; - // root condition - if (index === 0) delete query.secretPath; - router.push({ - pathname: router.pathname, - query - }); - }; - - if (isSecretsLoading || isEnvListLoading) { - return ( -
- loading animation -
- ); - } - - const filteredSecrets = Object.keys(secrets?.secrets || {})?.filter((secret: any) => - secret.toUpperCase().includes(searchFilter.toUpperCase()) - ); - // when secrets is not loading and secrets list is empty - const isDashboardSecretEmpty = !isSecretsLoading && !filteredSecrets?.length; - const isFoldersEmtpy = - !folders.some(({ isLoading: isFolderLoading }) => isFolderLoading) && - !Object.keys(foldersGroupedByEnv).length; - const isDashboardEmpty = isFoldersEmtpy && isDashboardSecretEmpty; - - return ( -
-
- -
-
-

Secrets Overview

-

- Inject your secrets using - - Infisical CLI - - or - - Infisical SDKs - -

-
-
-
-
onFolderCrumbClick(0)} - onKeyDown={() => null} - role="button" - tabIndex={0} - > - -
- {(secretPath || "") - .split("/") - .filter(Boolean) - .map((path, index, arr) => ( -
onFolderCrumbClick(index + 1)} - onKeyDown={() => null} - role="button" - tabIndex={0} - > - {path} -
- ))} -
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
-
-
-
-
-
{0}
-
-
-
-
Secret
-
-
- {numSecretsMissingPerEnv && - userAvailableEnvs?.map((env) => { - return ( -
-
- {env.name} - {numSecretsMissingPerEnv[env.slug] > 0 && ( -
- - - {numSecretsMissingPerEnv[env.slug]} - - -
- )} -
-
- ); - })} -
-
- {!isDashboardEmpty && ( - - - - {Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => ( - - ))} - {Object.keys(secrets?.secrets || {}) - ?.filter((secret: any) => - secret.toUpperCase().includes(searchFilter.toUpperCase()) - ) - .map((key) => ( - - ))} - -
-
- )} - {isDashboardEmpty && ( -
-
- - No secrets/folders found. - To add more secrets you can explore any environment. -
-
- )} -
-
-
-
0
-
-
- 0 - -
- {userAvailableEnvs?.map((env) => { - return ( -
- -
- ); - })} -
-
-
- ); -}; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index 262fb2a92..f991df901 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; @@ -30,7 +30,11 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger +} from "@radix-ui/react-dropdown-menu"; import { useQueryClient } from "@tanstack/react-query"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; @@ -119,12 +123,13 @@ type TDeleteSecretImport = { environment: string; secretPath: string }; * Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving * They will get it back */ -export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { +export const DashboardPage = () => { const { subscription } = useSubscription(); const { t } = useTranslation(); const router = useRouter(); const { createNotification } = useNotificationContext(); const queryClient = useQueryClient(); + const envQuery = router.query.env as string; const secretContainer = useRef(null); const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ @@ -172,8 +177,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { onSuccess: (data) => { // get an env with one of the access available const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied); - if (env && data?.map((wsenv) => wsenv.slug).includes(envFromTop)) { - setSelectedEnv(data?.filter((dp) => dp.slug === envFromTop)[0]); + if (env && data?.map((wsenv) => wsenv.slug).includes(envQuery)) { + setSelectedEnv(data?.filter((dp) => dp.slug === envQuery)[0]); } } }); @@ -293,11 +298,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { }); const { + register, control, handleSubmit, getValues, setValue, - formState: { isSubmitting, isDirty }, + formState: { isSubmitting, isDirty, errors }, reset } = method; const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" }); @@ -461,9 +467,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { } }; - const onDrawerOpen = (dto: TSecretDetailsOpen) => { - handlePopUpOpen("secretDetails", dto); - }; + const onDrawerOpen = useCallback((id: string | undefined, index: number) => { + handlePopUpOpen("secretDetails", { id, index } as TSecretDetailsOpen); + }, []); const onEnvChange = (slug: string) => { if (hasUnsavedChanges) { @@ -480,17 +486,27 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { }); }; + const handleDownloadSecret = () => { + const secretsFromImport: { key: string; value: string; comment: string }[] = []; + importedSecrets?.forEach(({ secrets: impSec }) => { + impSec.forEach((el) => { + secretsFromImport.push({ key: el.key, value: el.value, comment: el.comment }); + }); + }); + downloadSecret(getValues("secrets"), secretsFromImport, selectedEnv?.slug); + }; + // record all deleted ids // This will make final deletion easier - const onSecretDelete = (index: number, id?: string, overrideId?: string) => { + const onSecretDelete = useCallback((index: number, id?: string, overrideId?: string) => { if (id) deletedSecretIds.current.push(id); if (overrideId) deletedSecretIds.current.push(overrideId); remove(index); // just the case if this is called from drawer handlePopUpClose("secretDetails"); - }; + }, []); - const onCreateWsTag = async (tagName: string) => { + const onCreateWsTag = useCallback(async (tagName: string) => { try { await createWsTag({ workspaceID: workspaceId, @@ -509,10 +525,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { type: "error" }); } - }; + }, []); - const handleFolderOpen = (id: string) => { + const handleFolderOpen = useCallback((id: string) => { setSearchFilter(""); + console.log(router.query); router.push({ pathname: router.pathname, query: { @@ -520,10 +537,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { folderId: id } }); - }; + }, []); const isEditFolder = Boolean(popUp?.folderForm?.data); + // FOLDER SECTION const handleFolderCreate = async (name: string) => { try { await createFolder({ @@ -546,7 +564,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { } }; - const handleFolderUpdate = async (name: string) => { + const handleFolderUpdate = useCallback(async (name: string) => { const { id } = popUp?.folderForm?.data as TDeleteFolderForm; try { await updateFolder({ @@ -567,9 +585,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { type: "error" }); } - }; + }, []); - const handleFolderDelete = async () => { + const handleFolderDelete = useCallback(async () => { const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm; try { deleteFolder({ @@ -589,8 +607,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { type: "error" }); } - }; + }, []); + // SECRET IMPORT SECTION const handleSecretImportCreate = async (env: string, secretPath: string) => { try { await createSecretImport({ @@ -664,6 +683,25 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { } }; + // OPTIMIZATION HOOKS PURELY FOR PERFORMANCE AND TO AVOID RE-RENDERING + const handleCreateTagModalOpen = useCallback(() => handlePopUpOpen("addTag"), []); + const handleFolderCreatePopUpOpen = useCallback( + (id: string, name: string) => handlePopUpOpen("folderForm", { id, name }), + [] + ); + const handleFolderDeletePopUpOpen = useCallback( + (id: string, name: string) => handlePopUpOpen("deleteFolder", { id, name }), + [] + ); + const handleSecretImportDelPopUpOpen = useCallback( + (impSecEnv: string, impSecPath: string) => + handlePopUpOpen("deleteSecretImport", { + environment: impSecEnv, + secretPath: impSecPath + }), + [] + ); + // when secrets is not loading and secrets list is empty const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length; @@ -693,259 +731,259 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { return (
- -
- {/* breadcrumb row */} -
- envir.slug === envFromTop)[0].name || "" - } - isFolderMode - folders={folderData?.dir} - isProjectRelated - userAvailableEnvs={userAvailableEnvs} - onEnvChange={onEnvChange} + + {/* breadcrumb row */} +
+ envir.slug === envQuery)[0].name || ""} + isFolderMode + folders={folderData?.dir} + isProjectRelated + userAvailableEnvs={userAvailableEnvs} + onEnvChange={onEnvChange} + /> +
+
+
{isRollbackMode ? "Secret Snapshot" : ""}
+ {isRollbackMode && Boolean(snapshotSecret) && ( + + {new Date(snapshotSecret?.createdAt || "").toLocaleString()} + + )} +
+ {/* Environment, search and other action row */} +
+
+ setSearchFilter(e.target.value)} + leftIcon={} />
-
-
{isRollbackMode ? "Secret Snapshot" : ""}
- {isRollbackMode && Boolean(snapshotSecret) && ( - - {new Date(snapshotSecret?.createdAt || "").toLocaleString()} - - )} -
- {/* Environment, search and other action row */} -
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> +
+
+ + + + + + + +
+ +
+
+
-
-
- - - - - - - -
- -
-
-
-
-
- - setIsSecretValueHidden.toggle()} - > - - - -
-
- - handlePopUpOpen("secretSnapshots")} - > - - - -
-
- -
- {!isReadOnly && !isRollbackMode && ( -
- - - -
- -
-
- -
-
- -
-
- -
-
-
-
-
- )} - {isRollbackMode && ( -
+
+ + handlePopUpOpen("secretSnapshots")} > - Go back - - )} + + + +
+
-
-
- {!isEmptyPage && ( - - - - - - - handlePopUpOpen("deleteSecretImport", { - environment: impSecEnv, - secretPath: impSecPath - }) - } - secrets={secrets?.secrets} - importedSecrets={importedSecrets} - items={items} - /> - handlePopUpOpen("folderForm", { id, name })} - onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })} - folders={folderList} - search={searchFilter} - /> - {fields.map(({ id, _id }, index) => ( - onDrawerOpen({ id: _id as string, index })} - isSecretValueHidden={isSecretValueHidden} - wsTags={wsTags} - onCreateTagOpen={() => handlePopUpOpen("addTag")} - /> - ))} - {!isReadOnly && !isRollbackMode && ( - - - - )} - -
- -
-
-
+ {!isReadOnly && !isRollbackMode && ( +
+ + + +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
)} + {isRollbackMode && ( + + )} + +
+
+
+ {!isEmptyPage && ( + + + + + + + + {fields.map(({ id, _id }, index) => ( + + ))} + {!isReadOnly && !isRollbackMode && ( + + + + )} + +
+ +
+
+
+ )} + handlePopUpToggle("secretSnapshots", isOpen)} @@ -966,120 +1004,121 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index} onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)} /> - -
- {/* secrets table and drawers, modals */} - - {/* Create a new tag modal */} - { - handlePopUpToggle("addTag", open); - }} + + + +
+ {/* secrets table and drawers, modals */} + + {/* Create a new tag modal */} + { + handlePopUpToggle("addTag", open); + }} + > + - - - - - {/* Uploaded env override or not confirmation modal */} - handlePopUpToggle("uploadedSecOpts", open)} + + + + {/* Uploaded env override or not confirmation modal */} + handlePopUpToggle("uploadedSecOpts", open)} + > + handlePopUpClose("uploadedSecOpts")} + > + Keep old + , + + ]} > - handlePopUpClose("uploadedSecOpts")} - > - Keep old - , - - ]} - > -
-
Your file contains following duplicate secrets
-
- {Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {}) - ?.map((key) => key) - .join(", ")} -
-
Are you sure you want to overwrite these secrets?
+
+
Your file contains following duplicate secrets
+
+ {Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {}) + ?.map((key) => key) + .join(", ")}
- - - handlePopUpToggle("folderForm", isOpen)} +
Are you sure you want to overwrite these secrets?
+
+ + + handlePopUpToggle("folderForm", isOpen)} + > + + + + + handlePopUpToggle("addSecretImport", isOpen)} + > + - - - - - handlePopUpToggle("addSecretImport", isOpen)} + + + + handlePopUpToggle("deleteFolder", isOpen)} + onDeleteApproved={handleFolderDelete} + /> + handlePopUpToggle("deleteSecretImport", isOpen)} + onDeleteApproved={handleSecretImportDelete} + /> + handlePopUpToggle("compareSecrets", open)} + > + - - - - - handlePopUpToggle("deleteFolder", isOpen)} - onDeleteApproved={handleFolderDelete} - /> - handlePopUpToggle("deleteSecretImport", isOpen)} - onDeleteApproved={handleSecretImportDelete} - /> - handlePopUpToggle("compareSecrets", open)} - > - - - - - + + + {subscription && ( ; export type TSecretDetailsOpen = { index: number; id: string }; export type TSecOverwriteOpt = { secrets: Record }; -export const downloadSecret = (secrets: FormData["secrets"] = [], env: string = "unknown") => { - const finalSecret = secrets.map(({ key, value, valueOverride, overrideAction, comment }) => ({ - key, - value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value, - comment - })); +export const downloadSecret = ( + secrets: FormData["secrets"] = [], + importedSecrets: { key: string; value?: string; comment?: string }[] = [], + env: string = "unknown" +) => { + const importSecPos: Record = {}; + importedSecrets.forEach((el, index) => { + importSecPos[el.key] = index; + }); + const finalSecret = [...importedSecrets]; + secrets.forEach(({ key, value, valueOverride, overrideAction, comment }) => { + const newValue = { + key, + value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value, + comment + }; + // can also be zero thus failing + if (typeof importSecPos?.[key] === "undefined") { + finalSecret.push(newValue); + } else { + finalSecret[importSecPos[key]] = newValue; + } + }); let file = ""; finalSecret.forEach(({ key, value, comment }) => { diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx deleted file mode 100644 index 6c86b3fce..000000000 --- a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx +++ /dev/null @@ -1,135 +0,0 @@ -/* eslint-disable react/jsx-no-useless-fragment */ -import { useCallback, useRef, useState } from "react"; -import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { useSyntaxHighlight } from "@app/hooks"; -import { useToggle } from "@app/hooks/useToggle"; - -type Props = { - secrets: any[] | undefined; - // permission and external state's that decided to hide or show - isReadOnly?: boolean; - isSecretValueHidden: boolean; - userAvailableEnvs?: any[]; -}; - -const SEC_VAL_LINE_HEIGHT = 21; -const MAX_MULTI_LINE = 6; - -const DashboardInput = ({ - isOverridden, - isSecretValueHidden, - secret, - isReadOnly = true -}: { - isOverridden: boolean; - isSecretValueHidden: boolean; - isReadOnly?: boolean; - secret?: any; -}): JSX.Element => { - const ref = useRef(null); - const [isFocused, setIsFocused] = useToggle(); - const syntaxHighlight = useSyntaxHighlight(); - - const value = isOverridden ? secret.valueOverride : secret?.value; - const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT; - const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE); - - return ( - -
-