;
-export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
+export const Tr = ({
+ children,
+ className,
+ isHoverable,
+ isSelectable,
+ ...props
+}: TrProps): JSX.Element => (
{children}
@@ -76,7 +86,14 @@ export type ThProps = {
};
export const Th = ({ children, className }: ThProps): JSX.Element => (
- | {children} |
+
+ {children}
+ |
);
// table body
@@ -106,15 +123,15 @@ export type TBodyLoader = {
columns: number;
className?: string;
// unique key for mapping
- key: string;
+ innerKey: string;
};
-export const TableSkeleton = ({ rows = 3, columns, key, className }: TBodyLoader): JSX.Element => (
+export const TableSkeleton = ({ rows = 3, columns, innerKey, className }: TBodyLoader): JSX.Element => (
<>
{Array.apply(0, Array(rows)).map((_x, i) => (
-
+
{Array.apply(0, Array(columns)).map((_y, j) => (
- |
+ |
|
))}
diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx
index 3ae3db006..b4412b47b 100644
--- a/frontend/src/components/v2/index.tsx
+++ b/frontend/src/components/v2/index.tsx
@@ -4,7 +4,7 @@ export * from "./Checkbox";
export * from "./DeleteActionModal";
export * from "./Drawer";
export * from "./Dropdown";
-export * from "./EmailServiceSetupModal"
+export * from "./EmailServiceSetupModal";
export * from "./EmptyState";
export * from "./FormControl";
export * from "./HoverCardv2";
@@ -13,6 +13,7 @@ export * from "./Input";
export * from "./Menu";
export * from "./Modal";
export * from "./Popoverv2";
+export * from "./SecretInput";
export * from "./Select";
export * from "./Skeleton";
export * from "./Spinner";
diff --git a/frontend/src/hooks/api/secretFolders/index.tsx b/frontend/src/hooks/api/secretFolders/index.tsx
index 4c25ec3a2..3c0692eb6 100644
--- a/frontend/src/hooks/api/secretFolders/index.tsx
+++ b/frontend/src/hooks/api/secretFolders/index.tsx
@@ -1,6 +1,7 @@
export {
useCreateFolder,
useDeleteFolder,
+ useGetFoldersByEnv,
useGetProjectFolders,
useGetProjectFoldersBatch,
useUpdateFolder
diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx
index 013a28793..36d013b72 100644
--- a/frontend/src/hooks/api/secretFolders/queries.tsx
+++ b/frontend/src/hooks/api/secretFolders/queries.tsx
@@ -1,4 +1,4 @@
-import { useCallback } from "react";
+import { useCallback, useMemo } from "react";
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
@@ -9,6 +9,7 @@ import {
DeleteFolderDTO,
GetProjectFoldersBatchDTO,
GetProjectFoldersDTO,
+ TGetFoldersByEnvDTO,
TSecretFolder,
UpdateFolderDTO
} from "./types";
@@ -62,6 +63,48 @@ export const useGetProjectFolders = ({
)
});
+export const useGetFoldersByEnv = ({
+ parentFolderPath,
+ workspaceId,
+ environments,
+ parentFolderId
+}: TGetFoldersByEnvDTO) => {
+ const folders = useQueries({
+ queries: environments.map((env) => ({
+ queryKey: queryKeys.getSecretFolders(workspaceId, env, parentFolderPath || parentFolderId),
+ queryFn: async () => fetchProjectFolders(workspaceId, env, parentFolderId, parentFolderPath),
+ enabled: Boolean(workspaceId) && Boolean(env)
+ }))
+ });
+
+ const folderNames = useMemo(() => {
+ const names = new Set();
+ folders?.forEach(({ data }) => {
+ data?.folders.forEach(({ name }) => {
+ names.add(name);
+ });
+ });
+ return [...names];
+ }, [(folders || []).map((folder) => folder.data)]);
+
+ const isFolderPresentInEnv = useCallback(
+ (name: string, env: string) => {
+ const selectedEnvIndex = environments.indexOf(env);
+ if (selectedEnvIndex !== -1) {
+ return Boolean(
+ folders?.[selectedEnvIndex]?.data?.folders?.find(
+ ({ name: folderName }) => folderName === name
+ )
+ );
+ }
+ return false;
+ },
+ [(folders || []).map((folder) => folder.data)]
+ );
+
+ return { folders, folderNames, isFolderPresentInEnv };
+};
+
export const useGetProjectFoldersBatch = ({
folders = [],
isPaused,
diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts
index d6240666a..c9d707620 100644
--- a/frontend/src/hooks/api/secretFolders/types.ts
+++ b/frontend/src/hooks/api/secretFolders/types.ts
@@ -17,6 +17,13 @@ export type GetProjectFoldersBatchDTO = {
parentFolderPath?: string;
};
+export type TGetFoldersByEnvDTO = {
+ environments: string[];
+ workspaceId: string;
+ parentFolderPath?: string;
+ parentFolderId?: string;
+};
+
export type CreateFolderDTO = {
workspaceId: string;
environment: string;
diff --git a/frontend/src/hooks/api/secrets/index.ts b/frontend/src/hooks/api/secrets/index.ts
index cad1e65d6..2e31e4f0b 100644
--- a/frontend/src/hooks/api/secrets/index.ts
+++ b/frontend/src/hooks/api/secrets/index.ts
@@ -1,6 +1,7 @@
+export { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "./mutations";
export {
useBatchSecretsOp,
useGetProjectSecrets,
- useGetProjectSecretsByKey,
+ useGetProjectSecretsAllEnv,
useGetSecretVersion
} from "./queries";
diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx
new file mode 100644
index 000000000..6df00174c
--- /dev/null
+++ b/frontend/src/hooks/api/secrets/mutations.tsx
@@ -0,0 +1,172 @@
+import crypto from "crypto";
+
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import {
+ decryptAssymmetric,
+ encryptSymmetric
+} from "@app/components/utilities/cryptography/crypto";
+import { apiRequest } from "@app/config/request";
+
+import { secretKeys } from "./queries";
+import { TCreateSecretsV3DTO, TDeleteSecretsV3DTO, TUpdateSecretsV3DTO } from "./types";
+
+const encryptSecret = (randomBytes: string, key: string, value?: string, comment?: string) => {
+ // encrypt key
+ const {
+ ciphertext: secretKeyCiphertext,
+ iv: secretKeyIV,
+ tag: secretKeyTag
+ } = encryptSymmetric({
+ plaintext: key,
+ key: randomBytes
+ });
+
+ // encrypt value
+ const {
+ ciphertext: secretValueCiphertext,
+ iv: secretValueIV,
+ tag: secretValueTag
+ } = encryptSymmetric({
+ plaintext: value ?? "",
+ key: randomBytes
+ });
+
+ // encrypt comment
+ const {
+ ciphertext: secretCommentCiphertext,
+ iv: secretCommentIV,
+ tag: secretCommentTag
+ } = encryptSymmetric({
+ plaintext: comment ?? "",
+ key: randomBytes
+ });
+
+ return {
+ secretKeyCiphertext,
+ secretKeyIV,
+ secretKeyTag,
+ secretValueCiphertext,
+ secretValueIV,
+ secretValueTag,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag
+ };
+};
+
+export const useCreateSecretV3 = () => {
+ const queryClient = useQueryClient();
+ return useMutation<{}, {}, TCreateSecretsV3DTO>({
+ mutationFn: async ({
+ secretPath = "/",
+ type,
+ environment,
+ workspaceId,
+ secretName,
+ secretValue,
+ latestFileKey,
+ secretComment
+ }) => {
+ const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
+
+ const randomBytes = latestFileKey
+ ? decryptAssymmetric({
+ ciphertext: latestFileKey.encryptedKey,
+ nonce: latestFileKey.nonce,
+ publicKey: latestFileKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ })
+ : crypto.randomBytes(16).toString("hex");
+
+ const reqBody = {
+ workspaceId,
+ environment,
+ type,
+ secretPath,
+ ...encryptSecret(randomBytes, secretName, secretValue, secretComment)
+ };
+ const { data } = await apiRequest.post(`/api/v3/secrets/${secretName}`, reqBody);
+ return data;
+ },
+ onSuccess: (_, { workspaceId, environment, secretPath }) => {
+ queryClient.invalidateQueries(
+ secretKeys.getProjectSecret(workspaceId, environment, secretPath)
+ );
+ }
+ });
+};
+
+export const useUpdateSecretV3 = () => {
+ const queryClient = useQueryClient();
+ return useMutation<{}, {}, TUpdateSecretsV3DTO>({
+ mutationFn: async ({
+ secretPath = "/",
+ type,
+ environment,
+ workspaceId,
+ secretName,
+ secretValue,
+ latestFileKey
+ }) => {
+ const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
+
+ const randomBytes = latestFileKey
+ ? decryptAssymmetric({
+ ciphertext: latestFileKey.encryptedKey,
+ nonce: latestFileKey.nonce,
+ publicKey: latestFileKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ })
+ : crypto.randomBytes(16).toString("hex");
+ const { secretValueIV, secretValueTag, secretValueCiphertext } = encryptSecret(
+ randomBytes,
+ secretName,
+ secretValue,
+ ""
+ );
+
+ const reqBody = {
+ workspaceId,
+ environment,
+ type,
+ secretPath,
+ secretValueIV,
+ secretValueTag,
+ secretValueCiphertext
+ };
+ const { data } = await apiRequest.patch(`/api/v3/secrets/${secretName}`, reqBody);
+ return data;
+ },
+ onSuccess: (_, { workspaceId, environment, secretPath }) => {
+ queryClient.invalidateQueries(
+ secretKeys.getProjectSecret(workspaceId, environment, secretPath)
+ );
+ }
+ });
+};
+
+export const useDeleteSecretV3 = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TDeleteSecretsV3DTO>({
+ mutationFn: async ({ secretPath = "/", type, environment, workspaceId, secretName }) => {
+ const reqBody = {
+ workspaceId,
+ environment,
+ type,
+ secretPath
+ };
+
+ const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, {
+ data: reqBody
+ });
+ return data;
+ },
+ onSuccess: (_, { workspaceId, environment, secretPath }) => {
+ queryClient.invalidateQueries(
+ secretKeys.getProjectSecret(workspaceId, environment, secretPath)
+ );
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx
index 1541ff753..34ecd7ec4 100644
--- a/frontend/src/hooks/api/secrets/queries.tsx
+++ b/frontend/src/hooks/api/secrets/queries.tsx
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
-import { useCallback } from "react";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useCallback, useMemo } from "react";
+import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import {
decryptAssymmetric,
@@ -15,7 +15,8 @@ import {
EncryptedSecret,
EncryptedSecretVersion,
GetProjectSecretsDTO,
- GetSecretVersionsDTO
+ GetSecretVersionsDTO,
+ TGetProjectSecretsAllEnvDTO
} from "./types";
export const secretKeys = {
@@ -37,40 +38,15 @@ const fetchProjectEncryptedSecrets = async (
folderId?: string,
secretPath?: string
) => {
- if (typeof env === "string") {
- const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
- params: {
- environment: env,
- workspaceId,
- folderId: folderId || undefined,
- secretPath
- }
- });
- return data.secrets;
- }
-
- if (typeof env === "object") {
- let allEnvData: any = [];
-
- // eslint-disable-next-line no-restricted-syntax
- for (const envPoint of env) {
- // eslint-disable-next-line no-await-in-loop
- const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
- params: {
- environment: envPoint,
- workspaceId,
- folderId,
- secretPath
- }
- });
- allEnvData = allEnvData.concat(data.secrets);
+ const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
+ params: {
+ environment: env,
+ workspaceId,
+ folderId: folderId || undefined,
+ secretPath
}
-
- return allEnvData;
- // eslint-disable-next-line no-else-return
- } else {
- return null;
- }
+ });
+ return data.secrets;
};
export const useGetProjectSecrets = ({
@@ -160,22 +136,19 @@ export const useGetProjectSecrets = ({
)
});
-export const useGetProjectSecretsByKey = ({
+export const useGetProjectSecretsAllEnv = ({
workspaceId,
- env,
+ envs,
decryptFileKey,
- isPaused,
folderId,
secretPath
-}: GetProjectSecretsDTO) =>
- useQuery({
- // wait for all values to be available
- enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
- // right now secretpath is passed as folderid as only this is used in overview
- queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
- queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
- select: useCallback(
- (data: EncryptedSecret[]) => {
+}: TGetProjectSecretsAllEnvDTO) => {
+ const secrets = useQueries({
+ queries: envs.map((env) => ({
+ queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath || folderId),
+ enabled: Boolean(decryptFileKey && workspaceId && env),
+ queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
+ select: (data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
@@ -185,12 +158,11 @@ export const useGetProjectSecretsByKey = ({
privateKey: PRIVATE_KEY
});
- const sharedSecrets: Record = {};
+ const sharedSecrets: Record = {};
const personalSecrets: Record = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record = {};
- const uniqSecKeys: Record = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
@@ -198,7 +170,6 @@ export const useGetProjectSecretsByKey = ({
tag: encSecret.secretKeyTag,
key
});
- if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
@@ -226,35 +197,65 @@ export const useGetProjectSecretsByKey = ({
};
if (encSecret.type === "personal") {
- personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
+ personalSecrets[decryptedSecret.key] = {
id: encSecret._id,
value: secretValue
};
} else {
- if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
- if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
- sharedSecrets[secretKey].push(decryptedSecret);
+ if (!duplicateSecretKey?.[decryptedSecret.key]) {
+ sharedSecrets[decryptedSecret.key] = decryptedSecret;
}
- duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
+ duplicateSecretKey[decryptedSecret.key] = true;
}
});
- Object.keys(sharedSecrets).forEach((secName) => {
- sharedSecrets[secName].forEach((val) => {
- const dupKey = `${val.key}-${val.env}`;
- if (personalSecrets?.[dupKey]) {
- val.idOverride = personalSecrets[dupKey].id;
- val.valueOverride = personalSecrets[dupKey].value;
- val.overrideAction = "modified";
- }
- });
- });
- return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
- },
- [decryptFileKey]
- )
+ Object.keys(sharedSecrets).forEach((val) => {
+ if (personalSecrets?.[val]) {
+ sharedSecrets[val].idOverride = personalSecrets[val].id;
+ sharedSecrets[val].valueOverride = personalSecrets[val].value;
+ sharedSecrets[val].overrideAction = "modified";
+ }
+ });
+ return sharedSecrets;
+ }
+ }))
});
+ const secKeys = useMemo(() => {
+ const keys = new Set();
+ secrets?.forEach(({ data }) => {
+ // TODO(akhilmhdh): find out why this is unknown
+ Object.keys(data || {}).forEach((key) => keys.add(key));
+ });
+ return [...keys];
+ }, [(secrets || []).map((sec) => sec.data)]);
+
+ const getEnvSecretKeyCount = useCallback(
+ (env: string) => {
+ const selectedEnvIndex = envs.indexOf(env);
+ if (selectedEnvIndex !== -1) {
+ return Object.keys(secrets[selectedEnvIndex]?.data || {}).length;
+ }
+ return 0;
+ },
+ [(secrets || []).map((sec) => sec.data)]
+ );
+
+ const getSecretByKey = useCallback(
+ (env: string, key: string) => {
+ const selectedEnvIndex = envs.indexOf(env);
+ if (selectedEnvIndex !== -1) {
+ const sec = secrets[selectedEnvIndex]?.data?.[key];
+ return sec;
+ }
+ return undefined;
+ },
+ [(secrets || []).map((sec) => sec.data)]
+ );
+
+ return { data: secrets, secKeys, getSecretByKey, getEnvSecretKeyCount };
+};
+
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
`/api/v1/secret/${secretId}/secret-versions`,
diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts
index b36eb47f7..9e3092257 100644
--- a/frontend/src/hooks/api/secrets/types.ts
+++ b/frontend/src/hooks/api/secrets/types.ts
@@ -103,9 +103,47 @@ export type GetProjectSecretsDTO = {
onSuccess?: (data: DecryptedSecret[]) => void;
};
+export type TGetProjectSecretsAllEnvDTO = {
+ workspaceId: string;
+ envs: string[];
+ decryptFileKey: UserWsKeyPair;
+ folderId?: string;
+ secretPath?: string;
+ isPaused?: boolean;
+};
+
export type GetSecretVersionsDTO = {
secretId: string;
limit: number;
offset: number;
decryptFileKey: UserWsKeyPair;
};
+
+export type TCreateSecretsV3DTO = {
+ latestFileKey: UserWsKeyPair;
+ secretName: string;
+ secretValue: string;
+ secretComment: string;
+ secretPath: string;
+ workspaceId: string;
+ environment: string;
+ type: string;
+};
+
+export type TUpdateSecretsV3DTO = {
+ latestFileKey: UserWsKeyPair;
+ workspaceId: string;
+ environment: string;
+ type: string;
+ secretPath: string;
+ secretName: string;
+ secretValue: string;
+};
+
+export type TDeleteSecretsV3DTO = {
+ workspaceId: string;
+ environment: string;
+ type: string;
+ secretPath: string;
+ secretName: string;
+};
diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx
index 877db215d..fea50dd8a 100644
--- a/frontend/src/layouts/AppLayout/AppLayout.tsx
+++ b/frontend/src/layouts/AppLayout/AppLayout.tsx
@@ -13,7 +13,18 @@ import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
-import { faAngleDown, faArrowLeft, faArrowUpRightFromSquare, faBook, faCheck, faEnvelope, faInfinity, faMobile, faPlus, faQuestion } from "@fortawesome/free-solid-svg-icons";
+import {
+ faAngleDown,
+ faArrowLeft,
+ faArrowUpRightFromSquare,
+ faBook,
+ faCheck,
+ faEnvelope,
+ faInfinity,
+ faMobile,
+ faPlus,
+ faQuestion
+} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
@@ -41,7 +52,14 @@ import {
} from "@app/components/v2";
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
-import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useGetOrgTrialUrl, useLogoutUser, useUploadWsKey } from "@app/hooks/api";
+import {
+ fetchOrgUsers,
+ useAddUserToWs,
+ useCreateWorkspace,
+ useGetOrgTrialUrl,
+ useLogoutUser,
+ useUploadWsKey
+} from "@app/hooks/api";
interface LayoutProps {
children: React.ReactNode;
@@ -89,7 +107,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { subscription } = useSubscription();
// const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true);
- const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true;
+ const isAddingProjectsAllowed = subscription?.workspaceLimit
+ ? subscription.workspacesUsed < subscription.workspaceLimit
+ : true;
const createWs = useCreateWorkspace();
const uploadWsKey = useUploadWsKey();
@@ -110,22 +130,22 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { t } = useTranslation();
- useEffect(() => {
- const handleRouteChange = () => {
- (window).Intercom("update");
- };
-
- router.events.on("routeChangeComplete", handleRouteChange);
-
- return () => {
- router.events.off("routeChangeComplete", handleRouteChange);
- };
- }, []);
+ useEffect(() => {
+ const handleRouteChange = () => {
+ window.Intercom("update");
+ };
+
+ router.events.on("routeChangeComplete", handleRouteChange);
+
+ return () => {
+ router.events.off("routeChangeComplete", handleRouteChange);
+ };
+ }, []);
const logout = useLogoutUser();
const logOutUser = async () => {
try {
- console.log("Logging out...")
+ console.log("Logging out...");
await logout.mutateAsync();
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
@@ -145,27 +165,30 @@ export const AppLayout = ({ children }: LayoutProps) => {
const changeOrg = async (orgId) => {
localStorage.setItem("orgData.id", orgId);
- router.push(`/org/${orgId}/overview`)
- }
+ router.push(`/org/${orgId}/overview`);
+ };
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
// Placing the localstorage as much as possible
// Wait till tony integrates the azure and its launched
useEffect(() => {
-
// Put a user in an org if they're not in one yet
const putUserInOrg = async () => {
if (tempLocalStorage("orgData.id") === "") {
localStorage.setItem("orgData.id", orgs[0]?._id);
}
- if (currentOrg && (
- (workspaces?.length === 0 && router.asPath.includes("project"))
- || router.asPath.includes("/project/undefined")
- || (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration"))
- )) {
+ if (
+ currentOrg &&
+ ((workspaces?.length === 0 && router.asPath.includes("project")) ||
+ router.asPath.includes("/project/undefined") ||
+ (!orgs?.map((org) => org._id)?.includes(router.query.id) &&
+ !router.asPath.includes("project") &&
+ !router.asPath.includes("personal") &&
+ !router.asPath.includes("integration")))
+ ) {
router.push(`/org/${currentOrg?._id}/overview`);
- }
+ }
// else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
// const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
@@ -233,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
}
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" });
@@ -244,190 +267,235 @@ export const AppLayout = ({ children }: LayoutProps) => {
<>
-
+ ))}
+ >
+ );
+ }
+);
+
+FolderSection.displayName = "FolderSection";
diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx
index 162bdcd55..1e8ffb253 100644
--- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx
+++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx
@@ -9,7 +9,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
+import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks/useToggle";
@@ -138,7 +138,7 @@ export const SecretImportItem = ({
{key}
- {value}
+
|
diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx
index 6998b6ae3..c685a6dec 100644
--- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx
+++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx
@@ -1,3 +1,4 @@
+import { memo } from "react";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useWorkspace } from "@app/context";
@@ -62,32 +63,31 @@ type Props = {
items: { id: string; environment: string; secretPath: string }[];
};
-export const SecretImportSection = ({
- secrets = [],
- importedSecrets = [],
- onSecretImportDelete,
- items = []
-}: Props) => {
- const { currentWorkspace } = useWorkspace();
- const environments = currentWorkspace?.environments || [];
+export const SecretImportSection = memo(
+ ({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [] }: Props) => {
+ const { currentWorkspace } = useWorkspace();
+ const environments = currentWorkspace?.environments || [];
- return (
-
- {items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
-
- ))}
-
- );
-};
+ return (
+
+ {items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
+
+ ))}
+
+ );
+ }
+);
+
+SecretImportSection.displayName = "SecretImportSection";
diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/MaskedInput.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/MaskedInput.tsx
deleted file mode 100644
index d4663556d..000000000
--- a/frontend/src/views/DashboardPage/components/SecretInputRow/MaskedInput.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { useRef } from "react";
-import { Controller, useFormContext, useWatch } from "react-hook-form";
-
-import { useSyntaxHighlight, useToggle } from "@app/hooks";
-
-import { FormData } from "../../DashboardPage.utils";
-
-type Props = {
- isReadOnly?: boolean;
- isSecretValueHidden?: boolean;
- isOverridden?: boolean;
- index: number;
-};
-
-const SEC_VAL_LINE_HEIGHT = 21;
-const MAX_MULTI_LINE = 6;
-
-export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
- const { control } = useFormContext();
- const ref = useRef(null);
- const [isFocused, setIsFocused] = useToggle();
- const syntaxHighlight = useSyntaxHighlight();
-
- const secretValue = useWatch({ control, name: `secrets.${index}.value` });
- const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
- const value = isOverridden ? secretValueOverride : secretValue;
-
- const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
- const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
-
- return (
-
- {isOverridden ? (
- (
-
- );
-};
diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx
index 2a99400fb..899e0197f 100644
--- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx
+++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx
@@ -1,6 +1,13 @@
/* eslint-disable react/jsx-no-useless-fragment */
-import { memo, useEffect, useRef } from "react";
-import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
+import { memo, useEffect, useRef, useState } from "react";
+import {
+ Control,
+ Controller,
+ useFieldArray,
+ UseFormRegister,
+ UseFormSetValue,
+ useWatch
+} from "react-hook-form";
import {
faCheck,
faCodeBranch,
@@ -28,6 +35,7 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
+ SecretInput,
Tag,
TextArea,
Tooltip
@@ -36,24 +44,6 @@ import { useToggle } from "@app/hooks";
import { WsTag } from "@app/hooks/api/types";
import { FormData, SecretActionType } from "../../DashboardPage.utils";
-import { MaskedInput } from "./MaskedInput";
-
-type Props = {
- index: number;
- // permission and external state's that decided to hide or show
- isReadOnly?: boolean;
- isAddOnly?: boolean;
- isRollbackMode?: boolean;
- isSecretValueHidden: boolean;
- searchTerm: string;
- // to record the ids of deleted ones
- onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
- // sidebar control props
- onRowExpand: () => void;
- // tag props
- wsTags?: WsTag[];
- onCreateTagOpen: () => void;
-};
const tagColors = [
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
@@ -66,6 +56,31 @@ const tagColors = [
{ bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" }
];
+type Props = {
+ index: number;
+ // backend generated unique id
+ secUniqId?: string;
+ // permission and external state's that decided to hide or show
+ isReadOnly?: boolean;
+ isAddOnly?: boolean;
+ isRollbackMode?: boolean;
+ isSecretValueHidden: boolean;
+ searchTerm: string;
+ // to record the ids of deleted ones
+ onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
+ // sidebar control props
+ onRowExpand: (secId: string | undefined, index: number) => void;
+ // tag props
+ wsTags?: WsTag[];
+ onCreateTagOpen: () => void;
+ // rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering
+ control: Control;
+ register: UseFormRegister;
+ setValue: UseFormSetValue;
+ isKeyError?: boolean;
+ keyError?: string;
+};
+
export const SecretInputRow = memo(
({
index,
@@ -77,10 +92,15 @@ export const SecretInputRow = memo(
wsTags,
onCreateTagOpen,
onSecretDelete,
- searchTerm
+ searchTerm,
+ control,
+ register,
+ setValue,
+ isKeyError,
+ keyError,
+ secUniqId
}: Props): JSX.Element => {
const isKeySubDisabled = useRef(false);
- const { register, setValue, control } = useFormContext();
// comment management in a row
const {
fields: secretTags,
@@ -89,28 +109,40 @@ export const SecretInputRow = memo(
} = useFieldArray({ control, name: `secrets.${index}.tags` });
// to get details on a secret
- const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
- const idOverride = useWatch({ control, name: `secrets.${index}.idOverride` });
- const secComment = useWatch({ control, name: `secrets.${index}.comment` });
+ const overrideAction = useWatch({
+ control,
+ name: `secrets.${index}.overrideAction`,
+ exact: true
+ });
+ const idOverride = useWatch({ control, name: `secrets.${index}.idOverride`, exact: true });
+ const secComment = useWatch({ control, name: `secrets.${index}.comment`, exact: true });
const hasComment = Boolean(secComment);
const secKey = useWatch({
control,
name: `secrets.${index}.key`,
- disabled: isKeySubDisabled.current
+ disabled: isKeySubDisabled.current,
+ exact: true
});
const secValue = useWatch({
control,
name: `secrets.${index}.value`,
- disabled: isKeySubDisabled.current
+ disabled: isKeySubDisabled.current,
+ exact: true
});
const secValueOverride = useWatch({
control,
name: `secrets.${index}.valueOverride`,
- disabled: isKeySubDisabled.current
- })
- const secId = useWatch({ control, name: `secrets.${index}._id` });
+ disabled: isKeySubDisabled.current,
+ exact: true
+ });
+ // when secret is override by personal values
+ const isOverridden =
+ overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
+ const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue);
- const tags = useWatch({ control, name: `secrets.${index}.tags`, defaultValue: [] }) || [];
+ const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true });
+ const tags =
+ useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || [];
const selectedTagIds = tags.reduce>(
(prev, curr) => ({ ...prev, [curr.slug]: true }),
{}
@@ -126,15 +158,15 @@ export const SecretInputRow = memo(
return () => clearTimeout(timer);
}, [isInviteLinkCopied]);
+ useEffect(() => {
+ setEditorRef(isOverridden ? secValueOverride : secValue);
+ }, [isOverridden]);
+
const copyTokenToClipboard = () => {
navigator.clipboard.writeText((secValueOverride || secValue) as string);
setInviteLinkCopied.on();
};
- // when secret is override by personal values
- const isOverridden =
- overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
-
const onSecretOverride = () => {
if (isOverridden) {
// when user created a new override but then removes
@@ -193,10 +225,10 @@ export const SecretInputRow = memo(
control={control}
defaultValue=""
name={`secrets.${index}.key`}
- render={({ fieldState: { error }, field }) => (
-
+ render={({ field }) => (
+
-
+ |
- {error?.message}
+ {keyError}
)}
/>
- |
-
+ |
+
+ {isOverridden ? (
+ (
+ {
+ console.log(val);
+ onChange(val);
+ setEditorRef(html);
+ }}
+ onBlur={(html) => {
+ setEditorRef(html);
+ onBlur();
+ }}
+ />
+ )}
+ />
+ ) : (
+ (
+ {
+ onChange(val);
+ setEditorRef(html);
+ }}
+ value={editorRef}
+ onBlur={(html) => {
+ setEditorRef(html);
+ onBlur();
+ }}
+ />
+ )}
+ />
+ )}
+
|
@@ -251,7 +332,7 @@ export const SecretInputRow = memo(
{slug}
))}
-
+
onRowExpand(secUniqId, index)}
ariaLabel="expand"
>
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 = ({
}
onClick={() => {
- if (serverDetails?.emailConfigured){
+ if (serverDetails?.emailConfigured) {
handlePopUpOpen("addContact");
} else {
handlePopUpOpen("setUpEmail");
@@ -119,7 +119,7 @@ export const OrgIncidentContactsTable = ({
- {isLoading && }
+ {isLoading && }
{filteredContacts?.map(({ email }) => (
| {email} |
diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx
index 3a39560ca..5a0a6fd83 100644
--- a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx
+++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx
@@ -1,7 +1,14 @@
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
-import { faCheck, faCopy, faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
+import {
+ faCheck,
+ faCopy,
+ faMagnifyingGlass,
+ faPlus,
+ faTrash,
+ faUsers
+} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
@@ -10,7 +17,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import {
Button,
DeleteActionModal,
- EmailServiceSetupModal, EmptyState,
+ EmailServiceSetupModal,
+ EmptyState,
FormControl,
IconButton,
Input,
@@ -29,7 +37,7 @@ import {
Tr,
UpgradePlanModal
} from "@app/components/v2";
-import { useOrganization , useWorkspace } from "@app/context";
+import { useOrganization, useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetSSOConfig } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
@@ -47,8 +55,8 @@ type Props = {
onGrantAccess: (userId: string, publicKey: string) => Promise;
// the current user id to block remove org button
userId: string;
- completeInviteLink: string | undefined,
- setCompleteInviteLink: Dispatch>
+ completeInviteLink: string | undefined;
+ setCompleteInviteLink: Dispatch>;
};
const addMemberFormSchema = yup.object({
@@ -76,7 +84,7 @@ export const OrgMembersTable = ({
const { currentOrg } = useOrganization();
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
const [searchMemberFilter, setSearchMemberFilter] = useState("");
- const {data: serverDetails } = useFetchServerStatus()
+ const { data: serverDetails } = useFetchServerStatus();
const { workspaces } = useWorkspace();
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -85,7 +93,7 @@ export const OrgMembersTable = ({
"upgradePlan",
"setUpEmail"
] as const);
-
+
useEffect(() => {
if (router.query.action === "invite") {
handlePopUpOpen("addMember");
@@ -101,11 +109,11 @@ export const OrgMembersTable = ({
const onAddMember = async ({ email }: TAddMemberForm) => {
await onInviteMember(email);
- if (serverDetails?.emailConfigured){
- handlePopUpClose("addMember");
- }
-
- reset();
+ if (serverDetails?.emailConfigured) {
+ handlePopUpClose("addMember");
+ }
+
+ reset();
};
const onRemoveOrgMemberApproved = async () => {
@@ -118,7 +126,7 @@ export const OrgMembersTable = ({
() => members.find(({ user }) => userId === user?._id)?.role === "owner",
[userId, members]
);
-
+
const filterdUser = useMemo(
() =>
members.filter(
@@ -163,10 +171,10 @@ export const OrgMembersTable = ({
text: "You cannot invite users when SAML SSO is configured for your organization",
type: "error"
});
-
+
return;
}
-
+
if (isMoreUserNotAllowed) {
handlePopUpOpen("upgradePlan");
} else {
@@ -190,7 +198,7 @@ export const OrgMembersTable = ({
- {isLoading && }
+ {isLoading && }
{!isLoading &&
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
const name = user ? `${user.firstName} ${user.lastName}` : "-";
@@ -219,11 +227,17 @@ export const OrgMembersTable = ({
member
)}
- {((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && (
-
- )}
+ {(status === "invited" || status === "verified") &&
+ serverDetails?.emailConfigured && (
+
+ )}
{status === "completed" && (
| |