feat: added new secret input component and updated toolbar key special prop to innerKey

This commit is contained in:
akhilmhdh
2023-07-27 16:36:34 +05:30
parent b6b3c8a736
commit bca14dd5c4
34 changed files with 2969 additions and 2505 deletions
@@ -58,15 +58,13 @@ export default function NavHeader({
return ( return (
<div className="flex flex-row items-center pt-6"> <div className="flex flex-row items-center pt-6">
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-black text-sm"> <div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm text-black">
{currentOrg?.name?.charAt(0)} {currentOrg?.name?.charAt(0)}
</div> </div>
<Link <Link passHref legacyBehavior href={`/org/${currentOrg?._id}/overview`}>
passHref <a className="pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary">
legacyBehavior {currentOrg?.name}
href={`/org/${currentOrg?._id}/overview`} </a>
>
<a className="text-sm font-semibold text-primary/80 hover:text-primary pl-0.5">{currentOrg?.name}</a>
</Link> </Link>
{isProjectRelated && ( {isProjectRelated && (
<> <>
@@ -85,7 +83,7 @@ export default function NavHeader({
<Link <Link
passHref passHref
legacyBehavior legacyBehavior
href={{ pathname: "/project/[id]/secrets", query: { id: router.query.id } }} href={{ pathname: "/project/[id]/secrets/overview", query: { id: router.query.id } }}
> >
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a> <a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
</Link> </Link>
@@ -126,7 +124,11 @@ export default function NavHeader({
{index + 1 === folders?.length ? ( {index + 1 === folders?.length ? (
<span className="text-sm font-semibold text-bunker-300">{name}</span> <span className="text-sm font-semibold text-bunker-300">{name}</span>
) : ( ) : (
<Link passHref legacyBehavior href={{ pathname: "/project/[id]/secrets", query }}> <Link
passHref
legacyBehavior
href={{ pathname: "/project/[id]/secrets/[env]", query }}
>
<a className="text-sm font-semibold text-primary/80 hover:text-primary"> <a className="text-sm font-semibold text-primary/80 hover:text-primary">
{name === "root" ? selectedEnv?.name : name} {name === "root" ? selectedEnv?.name : name}
</a> </a>
@@ -0,0 +1,92 @@
/* eslint-disable react/no-danger */
import { HTMLAttributes } from "react";
import ContentEditable from "react-contenteditable";
import sanitizeHtml from "sanitize-html";
import { useToggle } from "@app/hooks";
const REGEX = /\${([^}]+)}/g;
const stripSpanTags = (str: string) => str.replace(/<\/?span[^>]*>/g, "");
const replaceContentWithDot = (str: string) => {
let finalStr = "";
let isHtml = false;
for (let i = 0; i < str.length; i += 1) {
const char = str.at(i);
if (char === "<" || char === ">") {
isHtml = char === "<";
finalStr += char;
} else if (!isHtml && char !== "\n") {
finalStr += "&#8226;";
} else {
finalStr += char;
}
}
return finalStr;
};
const syntaxHighlight = (orgContent?: string | null, isVisible?: boolean) => {
if (!orgContent) return "";
if (!isVisible) return replaceContentWithDot(orgContent);
const content = stripSpanTags(orgContent);
const newContent = content.replace(
REGEX,
(_a, b) =>
`<span class="ph-no-capture text-yellow">&#36;&#123;<span class="ph-no-capture text-yello-200/80">${b}</span>&#125;</span>`
);
return newContent;
};
const sanitizeConf = {
allowedTags: ["div", "span", "br", "p"]
};
type Props = Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "onBlur"> & {
value?: string | null;
isVisible?: boolean;
isDisabled?: boolean;
onChange?: (val: string, html: string) => void;
onBlur?: (sanitizedHtml: string) => void;
};
export const SecretInput = ({
value,
isVisible,
onChange,
onBlur,
isDisabled,
...props
}: Props) => {
const [isSecretFocused, setIsSecretFocused] = useToggle();
return (
<div
className="thin-scrollbar relative overflow-y-auto overflow-x-hidden"
style={{ maxHeight: `${21 * 7}px` }}
>
<div
dangerouslySetInnerHTML={{
__html: syntaxHighlight(value, isVisible || isSecretFocused)
}}
className="absolute top-0 left-0 z-0 h-full w-full text-ellipsis whitespace-pre-line break-all"
/>
<ContentEditable
className="relative z-10 h-full w-full text-ellipsis whitespace-pre-line break-all text-transparent caret-white outline-none"
role="textbox"
onChange={(evt) => {
if (onChange) onChange(evt.currentTarget.innerText.trim(), evt.currentTarget.innerHTML);
}}
onFocus={() => setIsSecretFocused.on()}
disabled={isDisabled}
spellCheck={false}
onBlur={(evt) => {
if (onBlur) onBlur(sanitizeHtml(evt.currentTarget.innerHTML || "", sanitizeConf));
setIsSecretFocused.off();
}}
html={isVisible || isSecretFocused ? value || "" : syntaxHighlight(value, false)}
{...props}
/>
</div>
);
};
@@ -0,0 +1 @@
export { SecretInput } from "./SecretInput";
@@ -52,7 +52,7 @@ export const Loading: Story = {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
<TableSkeleton columns={3} key="story-book-table" /> <TableSkeleton columns={3} innerKey="story-book-table" />
</TBody> </TBody>
</Table> </Table>
</TableContainer> </TableContainer>
+28 -11
View File
@@ -33,10 +33,7 @@ export type TableProps = {
export const Table = ({ children, className }: TableProps): JSX.Element => ( export const Table = ({ children, className }: TableProps): JSX.Element => (
<table <table
className={twMerge( className={twMerge("w-full bg-mineshaft-800 p-2 text-left text-sm text-gray-300", className)}
"w-full bg-mineshaft-800 p-2 text-left text-sm text-gray-300",
className
)}
> >
{children} {children}
</table> </table>
@@ -58,11 +55,24 @@ export const THead = ({ children, className }: THeadProps): JSX.Element => (
export type TrProps = { export type TrProps = {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
isHoverable?: boolean;
isSelectable?: boolean;
} & HTMLAttributes<HTMLTableRowElement>; } & HTMLAttributes<HTMLTableRowElement>;
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => ( export const Tr = ({
children,
className,
isHoverable,
isSelectable,
...props
}: TrProps): JSX.Element => (
<tr <tr
className={twMerge("border border-solid border-mineshaft-700 cursor-default", className)} className={twMerge(
"cursor-default border border-solid border-mineshaft-700",
isHoverable && "hover:bg-mineshaft-600",
isSelectable && "cursor-pointer",
className
)}
{...props} {...props}
> >
{children} {children}
@@ -76,7 +86,14 @@ export type ThProps = {
}; };
export const Th = ({ children, className }: ThProps): JSX.Element => ( export const Th = ({ children, className }: ThProps): JSX.Element => (
<th className={twMerge("bg-mineshaft-800 px-5 pt-4 pb-3.5 font-semibold border-b-2 border-mineshaft-600", className)}>{children}</th> <th
className={twMerge(
"border-b-2 border-mineshaft-600 bg-mineshaft-800 px-5 pt-4 pb-3.5 font-semibold",
className
)}
>
{children}
</th>
); );
// table body // table body
@@ -106,15 +123,15 @@ export type TBodyLoader = {
columns: number; columns: number;
className?: string; className?: string;
// unique key for mapping // 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(rows)).map((_x, i) => (
<Tr key={`${key}-skeleton-rows-${i + 1}`}> <Tr key={`${innerKey}-skeleton-rows-${i + 1}`}>
{Array.apply(0, Array(columns)).map((_y, j) => ( {Array.apply(0, Array(columns)).map((_y, j) => (
<Td key={`${key}-skeleton-rows-${i + 1}-column-${j + 1}`}> <Td key={`${innerKey}-skeleton-rows-${i + 1}-column-${j + 1}`}>
<Skeleton className={className} /> <Skeleton className={className} />
</Td> </Td>
))} ))}
+2 -1
View File
@@ -4,7 +4,7 @@ export * from "./Checkbox";
export * from "./DeleteActionModal"; export * from "./DeleteActionModal";
export * from "./Drawer"; export * from "./Drawer";
export * from "./Dropdown"; export * from "./Dropdown";
export * from "./EmailServiceSetupModal" export * from "./EmailServiceSetupModal";
export * from "./EmptyState"; export * from "./EmptyState";
export * from "./FormControl"; export * from "./FormControl";
export * from "./HoverCardv2"; export * from "./HoverCardv2";
@@ -13,6 +13,7 @@ export * from "./Input";
export * from "./Menu"; export * from "./Menu";
export * from "./Modal"; export * from "./Modal";
export * from "./Popoverv2"; export * from "./Popoverv2";
export * from "./SecretInput";
export * from "./Select"; export * from "./Select";
export * from "./Skeleton"; export * from "./Skeleton";
export * from "./Spinner"; export * from "./Spinner";
@@ -1,6 +1,7 @@
export { export {
useCreateFolder, useCreateFolder,
useDeleteFolder, useDeleteFolder,
useGetFoldersByEnv,
useGetProjectFolders, useGetProjectFolders,
useGetProjectFoldersBatch, useGetProjectFoldersBatch,
useUpdateFolder useUpdateFolder
@@ -1,4 +1,4 @@
import { useCallback } from "react"; import { useCallback, useMemo } from "react";
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
@@ -9,6 +9,7 @@ import {
DeleteFolderDTO, DeleteFolderDTO,
GetProjectFoldersBatchDTO, GetProjectFoldersBatchDTO,
GetProjectFoldersDTO, GetProjectFoldersDTO,
TGetFoldersByEnvDTO,
TSecretFolder, TSecretFolder,
UpdateFolderDTO UpdateFolderDTO
} from "./types"; } 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<string>();
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 = ({ export const useGetProjectFoldersBatch = ({
folders = [], folders = [],
isPaused, isPaused,
@@ -17,6 +17,13 @@ export type GetProjectFoldersBatchDTO = {
parentFolderPath?: string; parentFolderPath?: string;
}; };
export type TGetFoldersByEnvDTO = {
environments: string[];
workspaceId: string;
parentFolderPath?: string;
parentFolderId?: string;
};
export type CreateFolderDTO = { export type CreateFolderDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
+2 -1
View File
@@ -1,6 +1,7 @@
export { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "./mutations";
export { export {
useBatchSecretsOp, useBatchSecretsOp,
useGetProjectSecrets, useGetProjectSecrets,
useGetProjectSecretsByKey, useGetProjectSecretsAllEnv,
useGetSecretVersion useGetSecretVersion
} from "./queries"; } from "./queries";
@@ -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)
);
}
});
};
+54 -69
View File
@@ -1,5 +1,5 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { useCallback } from "react"; import { useCallback, useMemo } from "react";
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
@@ -38,7 +38,6 @@ const fetchProjectEncryptedSecrets = async (
folderId?: string, folderId?: string,
secretPath?: string secretPath?: string
) => { ) => {
if (typeof env === "string") {
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", { const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
params: { params: {
environment: env, environment: env,
@@ -48,30 +47,6 @@ const fetchProjectEncryptedSecrets = async (
} }
}); });
return data.secrets; 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);
}
return allEnvData;
// eslint-disable-next-line no-else-return
} else {
return null;
}
}; };
export const useGetProjectSecrets = ({ export const useGetProjectSecrets = ({
@@ -167,31 +142,13 @@ export const useGetProjectSecretsAllEnv = ({
decryptFileKey, decryptFileKey,
folderId, folderId,
secretPath secretPath
}: TGetProjectSecretsAllEnvDTO) => }: TGetProjectSecretsAllEnvDTO) => {
useQueries({ const secrets = useQueries({
queries: envs.map((env) => ({ queries: envs.map((env) => ({
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath || folderId), queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath || folderId),
enabled: Boolean(decryptFileKey && workspaceId && env), enabled: Boolean(decryptFileKey && workspaceId && env),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath)
}))
});
export const useGetProjectSecretsByKey = ({
workspaceId,
env,
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), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
select: useCallback( select: (data: EncryptedSecret[]) => {
(data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey; const latestKey = decryptFileKey;
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -201,12 +158,11 @@ export const useGetProjectSecretsByKey = ({
privateKey: PRIVATE_KEY privateKey: PRIVATE_KEY
}); });
const sharedSecrets: Record<string, DecryptedSecret[]> = {}; const sharedSecrets: Record<string, DecryptedSecret> = {};
const personalSecrets: Record<string, { id: string; value: string }> = {}; const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard // this used for add-only mode in dashboard
// type won't be there thus only one key is shown // type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {}; const duplicateSecretKey: Record<string, boolean> = {};
const uniqSecKeys: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => { data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({ const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext, ciphertext: encSecret.secretKeyCiphertext,
@@ -214,7 +170,6 @@ export const useGetProjectSecretsByKey = ({
tag: encSecret.secretKeyTag, tag: encSecret.secretKeyTag,
key key
}); });
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
const secretValue = decryptSymmetric({ const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext, ciphertext: encSecret.secretValueCiphertext,
@@ -242,34 +197,64 @@ export const useGetProjectSecretsByKey = ({
}; };
if (encSecret.type === "personal") { if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { personalSecrets[decryptedSecret.key] = {
id: encSecret._id, id: encSecret._id,
value: secretValue value: secretValue
}; };
} else { } else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) { if (!duplicateSecretKey?.[decryptedSecret.key]) {
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = []; sharedSecrets[decryptedSecret.key] = decryptedSecret;
sharedSecrets[secretKey].push(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 }; Object.keys(sharedSecrets).forEach((val) => {
}, if (personalSecrets?.[val]) {
[decryptFileKey] 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<string>();
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 fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>( const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
+29
View File
@@ -118,3 +118,32 @@ export type GetSecretVersionsDTO = {
offset: number; offset: number;
decryptFileKey: UserWsKeyPair; 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;
};
+159 -78
View File
@@ -13,7 +13,18 @@ import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
@@ -41,7 +52,14 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks"; 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 { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
@@ -89,7 +107,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
// const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true); // 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 createWs = useCreateWorkspace();
const uploadWsKey = useUploadWsKey(); const uploadWsKey = useUploadWsKey();
@@ -112,7 +132,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
useEffect(() => { useEffect(() => {
const handleRouteChange = () => { const handleRouteChange = () => {
(window).Intercom("update"); window.Intercom("update");
}; };
router.events.on("routeChangeComplete", handleRouteChange); router.events.on("routeChangeComplete", handleRouteChange);
@@ -125,7 +145,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
const logout = useLogoutUser(); const logout = useLogoutUser();
const logOutUser = async () => { const logOutUser = async () => {
try { try {
console.log("Logging out...") console.log("Logging out...");
await logout.mutateAsync(); await logout.mutateAsync();
localStorage.removeItem("protectedKey"); localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV"); localStorage.removeItem("protectedKeyIV");
@@ -145,25 +165,28 @@ export const AppLayout = ({ children }: LayoutProps) => {
const changeOrg = async (orgId) => { const changeOrg = async (orgId) => {
localStorage.setItem("orgData.id", 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 // TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
// Placing the localstorage as much as possible // Placing the localstorage as much as possible
// Wait till tony integrates the azure and its launched // Wait till tony integrates the azure and its launched
useEffect(() => { useEffect(() => {
// Put a user in an org if they're not in one yet // Put a user in an org if they're not in one yet
const putUserInOrg = async () => { const putUserInOrg = async () => {
if (tempLocalStorage("orgData.id") === "") { if (tempLocalStorage("orgData.id") === "") {
localStorage.setItem("orgData.id", orgs[0]?._id); localStorage.setItem("orgData.id", orgs[0]?._id);
} }
if (currentOrg && ( if (
(workspaces?.length === 0 && router.asPath.includes("project")) currentOrg &&
|| router.asPath.includes("/project/undefined") ((workspaces?.length === 0 && router.asPath.includes("project")) ||
|| (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration")) 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`); 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")) { // else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
@@ -233,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
} }
createNotification({ text: "Workspace created", type: "success" }); createNotification({ text: "Workspace created", type: "success" });
handlePopUpClose("addNewWs"); handlePopUpClose("addNewWs");
router.push(`/project/${newWorkspaceId}/secrets`); router.push(`/project/${newWorkspaceId}/secrets/overview`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createNotification({ text: "Failed to create workspace", type: "error" }); createNotification({ text: "Failed to create workspace", type: "error" });
@@ -244,82 +267,115 @@ export const AppLayout = ({ children }: LayoutProps) => {
<> <>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row"> <div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60 dark"> <aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]"> <nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div> <div>
{!router.asPath.includes("personal") && <div className="h-12 px-3 flex items-center pt-6 cursor-default"> {!router.asPath.includes("personal") && (
{(router.asPath.includes("project") || router.asPath.includes("integrations")) && <Link href={`/org/${currentOrg?._id}/overview`}><div className="pl-1 pr-2 text-mineshaft-400 hover:text-mineshaft-100 duration-200"> <div className="flex h-12 cursor-default items-center px-3 pt-6">
{(router.asPath.includes("project") ||
router.asPath.includes("integrations")) && (
<Link href={`/org/${currentOrg?._id}/overview`}>
<div className="pl-1 pr-2 text-mineshaft-400 duration-200 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faArrowLeft} />
</div></Link>} </div>
</Link>
)}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600"> <DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
<div className="mr-auto flex items-center hover:bg-mineshaft-600 py-1.5 pl-1.5 pr-2 rounded-md"> <div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
<div className="w-5 h-5 rounded-md bg-primary flex justify-center items-center text-sm">{currentOrg?.name.charAt(0)}</div> <div className="flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm">
<div className="pl-3 text-mineshaft-100 text-sm">{currentOrg?.name} <FontAwesomeIcon icon={faAngleDown} className="text-xs pl-1 pt-1 text-mineshaft-300" /></div> {currentOrg?.name.charAt(0)}
</div>
<div className="pl-3 text-sm text-mineshaft-100">
{currentOrg?.name}{" "}
<FontAwesomeIcon
icon={faAngleDown}
className="pl-1 pt-1 text-xs text-mineshaft-300"
/>
</div>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div> <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
{orgs?.map(org => <DropdownMenuItem key={org._id}> {orgs?.map((org) => (
<DropdownMenuItem key={org._id}>
<Button <Button
onClick={() => changeOrg(org?._id)} onClick={() => changeOrg(org?._id)}
variant="plain" variant="plain"
colorSchema="secondary" colorSchema="secondary"
size="xs" size="xs"
className="w-full flex items-center justify-start p-0 font-normal" className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="mr-3 text-primary"/>} leftIcon={
currentOrg._id === org._id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
> >
<div className="w-full flex justify-between items-center">{org.name}</div> <div className="flex w-full items-center justify-between">
{org.name}
</div>
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
)} ))}
<div className="h-1 mt-1 border-t border-mineshaft-600"/> <div className="mt-1 h-1 border-t border-mineshaft-600" />
<button <button type="button" onClick={logOutUser} className="w-full">
type="button"
onClick={logOutUser}
className="w-full"
>
<DropdownMenuItem>Log Out</DropdownMenuItem> <DropdownMenuItem>Log Out</DropdownMenuItem>
</button> </button>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="hover:bg-primary-400 hover:text-black data-[state=open]:text-black data-[state=open]:bg-primary-400 p-1"> <DropdownMenuTrigger
<div className="child w-6 h-6 rounded-full bg-mineshaft hover:bg-mineshaft-500 pr-1 text-xs text-mineshaft-300 flex justify-center items-center"> asChild
{user?.firstName?.charAt(0)}{user?.lastName && user?.lastName?.charAt(0)} className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
>
<div className="child flex h-6 w-6 items-center justify-center rounded-full bg-mineshaft pr-1 text-xs text-mineshaft-300 hover:bg-mineshaft-500">
{user?.firstName?.charAt(0)}
{user?.lastName && user?.lastName?.charAt(0)}
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div> <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
<Link href="/personal-settings"><DropdownMenuItem>Personal Settings</DropdownMenuItem></Link> <Link href="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a <a
href="https://infisical.com/docs/documentation/getting-started/introduction" href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-full mt-3 text-sm text-mineshaft-300 font-normal leading-[1.2rem] hover:text-mineshaft-100" className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
> >
<DropdownMenuItem>Documentation<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></DropdownMenuItem> <DropdownMenuItem>
Documentation
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a> </a>
<a <a
href="https://infisical.com/slack" href="https://infisical.com/slack"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-full mt-3 text-sm text-mineshaft-300 font-normal leading-[1.2rem] hover:text-mineshaft-100" className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
> >
<DropdownMenuItem>Join Slack Community<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></DropdownMenuItem> <DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a> </a>
<div className="h-1 mt-1 border-t border-mineshaft-600"/> <div className="mt-1 h-1 border-t border-mineshaft-600" />
<button <button type="button" onClick={logOutUser} className="w-full">
type="button"
onClick={logOutUser}
className="w-full"
>
<DropdownMenuItem>Log Out</DropdownMenuItem> <DropdownMenuItem>Log Out</DropdownMenuItem>
</button> </button>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div>} </div>
{!router.asPath.includes("org") && (!router.asPath.includes("personal") && currentWorkspace ? ( )}
{!router.asPath.includes("org") &&
(!router.asPath.includes("personal") && currentWorkspace ? (
<div className="mt-5 mb-4 w-full p-3"> <div className="mt-5 mb-4 w-full p-3">
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400"> <p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
Project Project
@@ -329,13 +385,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
value={currentWorkspace?._id} value={currentWorkspace?._id}
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium" className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
onValueChange={(value) => { onValueChange={(value) => {
router.push(`/project/${value}/secrets`); router.push(`/project/${value}/secrets/overview`);
localStorage.setItem("projectData.id", value); localStorage.setItem("projectData.id", value);
}} }}
position="popper" position="popper"
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700" dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
> >
<div className='h-full no-scrollbar no-scrollbar::-webkit-scrollbar'> <div className="no-scrollbar::-webkit-scrollbar h-full no-scrollbar">
{workspaces {workspaces
.filter((ws) => ws.organization === currentOrg?._id) .filter((ws) => ws.organization === currentOrg?._id)
.map(({ _id, name }) => ( .map(({ _id, name }) => (
@@ -357,7 +413,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
size="sm" size="sm"
onClick={() => { onClick={() => {
if (isAddingProjectsAllowed) { if (isAddingProjectsAllowed) {
handlePopUpOpen("addNewWs") handlePopUpOpen("addNewWs");
} else { } else {
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
} }
@@ -369,16 +425,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
</div> </div>
</Select> </Select>
</div> </div>
) : <Link href={`/org/${currentOrg?._id}/overview`}><div className="pr-2 my-6 flex justify-center items-center text-mineshaft-300 hover:text-mineshaft-100 cursor-default text-sm"> ) : (
<FontAwesomeIcon icon={faArrowLeft} className="pr-3"/> <Link href={`/org/${currentOrg?._id}/overview`}>
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
Back to organization Back to organization
</div></Link>)} </div>
</Link>
))}
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}> <div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
{((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu> {(router.asPath.includes("project") || router.asPath.includes("integrations")) &&
<Link href={`/project/${currentWorkspace?._id}/secrets`} passHref> currentWorkspace ? (
<Menu>
<Link href={`/project/${currentWorkspace?._id}/secrets/overview`} passHref>
<a> <a>
<MenuItem <MenuItem
isSelected={router.asPath.includes(`/project/${currentWorkspace?._id}/secrets`)} isSelected={router.asPath.includes(
`/project/${currentWorkspace?._id}/secrets/overview`
)}
icon="system-outline-90-lock-closed" icon="system-outline-90-lock-closed"
> >
{t("nav.menu.secrets")} {t("nav.menu.secrets")}
@@ -388,7 +452,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
<Link href={`/project/${currentWorkspace?._id}/members`} passHref> <Link href={`/project/${currentWorkspace?._id}/members`} passHref>
<a> <a>
<MenuItem <MenuItem
isSelected={router.asPath === `/project/${currentWorkspace?._id}/members`} isSelected={
router.asPath === `/project/${currentWorkspace?._id}/members`
}
icon="system-outline-96-groups" icon="system-outline-96-groups"
> >
{t("nav.menu.members")} {t("nav.menu.members")}
@@ -420,7 +486,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref> <Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
<a> <a>
<MenuItem <MenuItem
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`} isSelected={
router.asPath === `/project/${currentWorkspace?._id}/audit-logs`
}
icon="system-outline-168-view-headline" icon="system-outline-168-view-headline"
> >
Audit Logs Audit Logs
@@ -451,7 +519,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
</a> </a>
</Link> </Link>
</Menu> </Menu>
: <Menu className="mt-4"> ) : (
<Menu className="mt-4">
<Link href={`/org/${currentOrg?._id}/overview`} passHref> <Link href={`/org/${currentOrg?._id}/overview`} passHref>
<a> <a>
<MenuItem <MenuItem
@@ -462,7 +531,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem> </MenuItem>
</a> </a>
</Link> </Link>
{/* {workspaces.map(project => <Link key={project._id} href={`/project/${project?._id}/secrets`} passHref> {/* {workspaces.map(project => <Link key={project._id} href={`/project/${project?._id}/secrets/overview`} passHref>
<a> <a>
<SubMenuItem <SubMenuItem
isSelected={false} isSelected={false}
@@ -508,19 +577,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
<Link href={`/org/${currentOrg?._id}/settings`} passHref> <Link href={`/org/${currentOrg?._id}/settings`} passHref>
<a> <a>
<MenuItem <MenuItem
isSelected={ isSelected={router.asPath === `/org/${currentOrg?._id}/settings`}
router.asPath === `/org/${currentOrg?._id}/settings`
}
icon="system-outline-109-slider-toggle-settings" icon="system-outline-109-slider-toggle-settings"
> >
Organization Settings Organization Settings
</MenuItem> </MenuItem>
</a> </a>
</Link> </Link>
</Menu>} </Menu>
)}
</div> </div>
</div> </div>
<div className={`relative mt-10 ${subscription && subscription.slug === "starter" && !subscription.has_used_trial ? "mb-2" : "mb-4"} w-full px-3 text-mineshaft-400 cursor-default text-sm flex flex-col items-center`}> <div
className={`relative mt-10 ${
subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2"
: "mb-4"
} flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400`}
>
{/* <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[9.9rem] ${router.asPath.includes("org") ? "bottom-[8.4rem]" : "bottom-[5.4rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-30`}/> {/* <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[9.9rem] ${router.asPath.includes("org") ? "bottom-[8.4rem]" : "bottom-[5.4rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-30`}/>
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[10.7rem] ${router.asPath.includes("org") ? "bottom-[8.15rem]" : "bottom-[5.15rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-50`}/> <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[10.7rem] ${router.asPath.includes("org") ? "bottom-[8.15rem]" : "bottom-[5.15rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-50`}/>
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/> <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/>
@@ -549,22 +623,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
</a> </a>
</div> </div>
</div> */} </div> */}
{router.asPath.includes("org") && <div {router.asPath.includes("org") && (
<div
onKeyDown={() => null} onKeyDown={() => null}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)} onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)}
className="w-full" className="w-full"
> >
<div className="hover:text-mineshaft-200 duration-200 mb-3 pl-5 w-full"> <div className="mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faPlus} className="mr-3"/> <FontAwesomeIcon icon={faPlus} className="mr-3" />
Invite people Invite people
</div> </div>
</div>} </div>
)}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div className="hover:text-mineshaft-200 duration-200 mb-2 pl-5 w-full"> <div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="px-[0.1rem] mr-3"/> <FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support Help & Support
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -586,7 +662,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && ( {subscription &&
subscription.slug === "starter" &&
!subscription.has_used_trial && (
<button <button
type="button" type="button"
onClick={async () => { onClick={async () => {
@@ -600,10 +678,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
window.location.href = url; window.location.href = url;
}} }}
className="w-full mt-1.5" className="mt-1.5 w-full"
> >
<div className="hover:text-primary-400 text-mineshaft-300 duration-200 flex justify-left items-center py-1 bg-mineshaft-600 rounded-md hover:bg-mineshaft-500 mb-1.5 mt-1.5 pl-4 w-full"> <div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
<FontAwesomeIcon icon={faInfinity} className="mr-3 ml-0.5 py-2 text-primary"/> <FontAwesomeIcon
icon={faInfinity}
className="mr-3 ml-0.5 py-2 text-primary"
/>
Start Free Pro Trial Start Free Pro Trial
</div> </div>
</button> </button>
+155 -61
View File
@@ -1,4 +1,3 @@
import crypto from "crypto"; import crypto from "crypto";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -9,14 +8,31 @@ import { useRouter } from "next/router";
import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { faFolderOpen } from "@fortawesome/free-regular-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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; 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 { TabsObject } from "@app/components/v2/Tabs";
import { useSubscription, useUser, useWorkspace } from "@app/context"; import { useSubscription, useUser, useWorkspace } from "@app/context";
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api"; 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 { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
import registerUserAction from "../../../api/userActions/registerUserAction"; import registerUserAction from "../../../api/userActions/registerUserAction";
const features = [{ const features = [
"_id": 0, {
"name": "Kubernetes Operator", _id: 0,
"description": "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes." name: "Kubernetes Operator",
}] description:
"Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes."
}
];
type ItemProps = { type ItemProps = {
text: string; text: string;
@@ -58,7 +77,11 @@ const learningItem = ({
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`} className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
href={link} href={link}
> >
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} mb-3 rounded-md`}> <div
className={`${
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} mb-3 rounded-md`}
>
<div <div
onKeyDown={() => null} onKeyDown={() => null}
role="button" 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`}
> >
<div className="mr-4 flex flex-row items-center"> <div className="mr-4 flex flex-row items-center">
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" /> <FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
@@ -148,7 +175,11 @@ const learningItemSquare = ({
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`} className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
href={link} href={link}
> >
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} rounded-md w-full`}> <div
className={`${
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} w-full rounded-md`}
>
<div <div
onKeyDown={() => null} onKeyDown={() => null}
role="button" 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`}
> >
<div className="flex flex-col items-center w-full px-6 py-4"> <div className="flex w-full flex-col items-center px-6 py-4">
<div className="flex flex-row items-start justify-between w-full"> <div className="flex w-full flex-row items-start justify-between">
<FontAwesomeIcon icon={icon} className="w-16 text-5xl text-mineshaft-200 group-hover:text-mineshaft-100 duration-100 pt-2" /> <FontAwesomeIcon
icon={icon}
className="w-16 pt-2 text-5xl text-mineshaft-200 duration-100 group-hover:text-mineshaft-100"
/>
{complete && ( {complete && (
<div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700"> <div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" /> <FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
</div> </div>
)} )}
<div <div
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "text-primary font-semibold" : ""}`} className={`text-right text-sm font-normal text-mineshaft-300 ${
complete ? "font-semibold text-primary" : ""
}`}
> >
{complete ? "Complete!" : `About ${time}`} {complete ? "Complete!" : `About ${time}`}
</div> </div>
</div> </div>
<div className="flex flex-col items-start justify-start w-full pt-4"> <div className="flex w-full flex-col items-start justify-start pt-4">
<div className="mt-0.5 text-lg font-medium">{text}</div> <div className="mt-0.5 text-lg font-medium">{text}</div>
<div className="text-sm font-normal text-mineshaft-300">{subText}</div> <div className="text-sm font-normal text-mineshaft-300">{subText}</div>
</div> </div>
@@ -202,7 +242,10 @@ export default function Organization() {
const router = useRouter(); const router = useRouter();
const { workspaces } = useWorkspace(); 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 currentOrg = String(router.query.id);
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const addWsUser = useAddUserToWs(); const addWsUser = useAddUserToWs();
@@ -269,7 +312,7 @@ export default function Organization() {
} }
createNotification({ text: "Workspace created", type: "success" }); createNotification({ text: "Workspace created", type: "success" });
handlePopUpClose("addNewWs"); handlePopUpClose("addNewWs");
router.push(`/project/${newWorkspaceId}/secrets`); router.push(`/project/${newWorkspaceId}/secrets/overview`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createNotification({ text: "Failed to create workspace", type: "error" }); createNotification({ text: "Failed to create workspace", type: "error" });
@@ -278,7 +321,9 @@ export default function Organization() {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true; const isAddingProjectsAllowed = subscription?.workspaceLimit
? subscription.workspacesUsed < subscription.workspaceLimit
: true;
useEffect(() => { useEffect(() => {
onboardingCheck({ onboardingCheck({
@@ -290,16 +335,16 @@ export default function Organization() {
}, []); }, []);
return ( return (
<div className="flex max-w-7xl mx-auto flex-col justify-start bg-bunker-800 md:h-screen"> <div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
<Head> <Head>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title> <title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Head> </Head>
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4"> <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
<p className="mr-4 font-semibold text-white">Projects</p> <p className="mr-4 font-semibold text-white">Projects</p>
<div className="w-full flex flex-row mt-6"> <div className="mt-6 flex w-full flex-row">
<Input <Input
className="h-[2.3rem] text-sm bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80" className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by project name..." placeholder="Search by project name..."
value={searchFilter} value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)} onChange={(e) => setSearchFilter(e.target.value)}
@@ -310,7 +355,7 @@ export default function Organization() {
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
if (isAddingProjectsAllowed) { if (isAddingProjectsAllowed) {
handlePopUpOpen("addNewWs") handlePopUpOpen("addNewWs");
} else { } else {
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
} }
@@ -320,34 +365,57 @@ export default function Organization() {
Add New Project Add New Project
</Button> </Button>
</div> </div>
<div className="mt-4 w-full grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> <div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between"> {orgWorkspaces
<div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div> .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
<div className="text-sm text-mineshaft-300 mt-0 pb-6">{(workspace.environments?.length || 0)} environments</div> .map((workspace) => (
<button type="button" onClick={() => { <div
router.push(`/project/${workspace._id}/secrets`); key={workspace._id}
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">{workspace.name}</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
<button
type="button"
onClick={() => {
router.push(`/project/${workspace._id}/secrets/overview`);
localStorage.setItem("projectData.id", workspace._id); localStorage.setItem("projectData.id", workspace._id);
}}> }}
<div className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80">Explore <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200" /></div> >
<div className="group ml-auto w-max cursor-default rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
Explore{" "}
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
/>
</div>
</button> </button>
</div>)} </div>
))}
</div> </div>
{orgWorkspaces.length === 0 && ( {orgWorkspaces.length === 0 && (
<div className="w-full rounded-md bg-mineshaft-800 border border-mineshaft-700 px-4 py-6 text-mineshaft-300 text-base"> <div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon icon={faFolderOpen} className="w-full text-center text-5xl mb-4 mt-2 text-mineshaft-400" /> <FontAwesomeIcon
icon={faFolderOpen}
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
/>
<div className="text-center font-light"> <div className="text-center font-light">
You are not part of any projects in this organization yet. When you are, they will appear You are not part of any projects in this organization yet. When you are, they will
here. appear here.
</div> </div>
<div className="mt-0.5 text-center font-light"> <div className="mt-0.5 text-center font-light">
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.
</div> </div>
</div> </div>
)} )}
</div> </div>
{((new Date()).getTime() - (new Date(user?.createdAt)).getTime()) < 30 * 24 * 60 * 60 * 1000 && <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4"> {new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && (
<p className="mr-4 font-semibold text-white mb-4">Onboarding Guide</p> <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 w-full mb-3"> <p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{learningItemSquare({ {learningItemSquare({
text: "Watch Infisical demo", text: "Watch Infisical demo",
subText: "Set up Infisical in 3 min.", subText: "Set up Infisical in 3 min.",
@@ -357,7 +425,8 @@ export default function Organization() {
userAction: "intro_cta_clicked", userAction: "intro_cta_clicked",
link: "https://www.youtube.com/watch?v=PK23097-25I" link: "https://www.youtube.com/watch?v=PK23097-25I"
})} })}
{orgWorkspaces.length !== 0 && learningItemSquare({ {orgWorkspaces.length !== 0 &&
learningItemSquare({
text: "Add your secrets", text: "Add your secrets",
subText: "Drop a .env file or type your secrets.", subText: "Drop a .env file or type your secrets.",
complete: hasUserPushedSecrets, complete: hasUserPushedSecrets,
@@ -374,7 +443,8 @@ export default function Organization() {
time: "2 min", time: "2 min",
link: `/org/${router.query.id}/members?action=invite` link: `/org/${router.query.id}/members?action=invite`
})} })}
<div className="block xl:hidden 2xl:block">{learningItemSquare({ <div className="block xl:hidden 2xl:block">
{learningItemSquare({
text: "Join Infisical Slack", text: "Join Infisical Slack",
subText: "Have any questions? Ask us!", subText: "Have any questions? Ask us!",
complete: hasUserClickedSlack, complete: hasUserClickedSlack,
@@ -382,15 +452,20 @@ export default function Organization() {
time: "1 min", time: "1 min",
userAction: "slack_cta_clicked", userAction: "slack_cta_clicked",
link: "https://infisical.com/slack" link: "https://infisical.com/slack"
})}</div> })}
</div> </div>
{orgWorkspaces.length !== 0 && <div className="group text-mineshaft-100 relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200"> </div>
{orgWorkspaces.length !== 0 && (
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
<div className="mb-4 flex w-full flex-row items-center pr-4"> <div className="mb-4 flex w-full flex-row items-center pr-4">
<div className="mr-4 flex w-full flex-row items-center"> <div className="mr-4 flex w-full flex-row items-center">
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" /> <FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
{false && ( {false && (
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700"> <div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-green" /> <FontAwesomeIcon
icon={faCheckCircle}
className="h-5 w-5 text-4xl text-green"
/>
</div> </div>
)} )}
<div className="flex flex-col items-start pl-0.5"> <div className="flex flex-col items-start pl-0.5">
@@ -400,14 +475,18 @@ export default function Organization() {
</div> </div>
</div> </div>
</div> </div>
<div className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}> <div
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}
>
About 2 min About 2 min
</div> </div>
</div> </div>
<TabsObject /> <TabsObject />
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />} {false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>} </div>
{orgWorkspaces.length !== 0 && learningItem({ )}
{orgWorkspaces.length !== 0 &&
learningItem({
text: "Integrate Infisical with your infrastructure", text: "Integrate Infisical with your infrastructure",
subText: "Connect Infisical to various 3rd party services and platforms.", subText: "Connect Infisical to various 3rd party services and platforms.",
complete: false, complete: false,
@@ -415,25 +494,40 @@ export default function Organization() {
time: "15 min", time: "15 min",
link: "https://infisical.com/docs/integrations/overview" link: "https://infisical.com/docs/integrations/overview"
})} })}
</div>} </div>
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4 pb-6"> )}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 pb-6 text-3xl">
<p className="mr-4 font-semibold text-white">Explore More</p> <p className="mr-4 font-semibold text-white">Explore More</p>
<div className="mt-4 w-full grid grid-flow-dense gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 4fr))" }}> <div
{features.map(feature => <div key={feature._id} className="h-44 w-96 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between"> className="mt-4 grid w-full grid-flow-dense gap-4"
<div className="text-lg text-mineshaft-100 mt-0">{feature.name}</div> style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 4fr))" }}
<div className="text-[15px] font-light text-mineshaft-300 mb-4 mt-2">{feature.description}</div> >
<div className="w-full flex items-center"> {features.map((feature) => (
<div className="text-mineshaft-300 text-[15px] font-light">Setup time: 20 min</div> <div
key={feature._id}
className="flex h-44 w-96 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">{feature.name}</div>
<div className="mb-4 mt-2 text-[15px] font-light text-mineshaft-300">
{feature.description}
</div>
<div className="flex w-full items-center">
<div className="text-[15px] font-light text-mineshaft-300">Setup time: 20 min</div>
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80" className="group ml-auto w-max cursor-default rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200"
href="https://infisical.com/docs/documentation/getting-started/kubernetes" href="https://infisical.com/docs/documentation/getting-started/kubernetes"
> >
Learn more <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200"/> Learn more{" "}
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
/>
</a> </a>
</div> </div>
</div>)} </div>
))}
</div> </div>
</div> </div>
<Modal <Modal
@@ -1,16 +1,10 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router";
import { DashboardPage } from "@app/views/DashboardPage"; import { DashboardPage } from "@app/views/DashboardPage";
import { DashboardEnvOverview } from "@app/views/DashboardPage/DashboardEnvOverview";
const Dashboard = () => { const Dashboard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter();
const queryEnv = router.query.env as string;
const isOverviewMode = !queryEnv;
return ( return (
<> <>
@@ -22,7 +16,7 @@ const Dashboard = () => {
<meta name="og:description" content={String(t("dashboard.og-description"))} /> <meta name="og:description" content={String(t("dashboard.og-description"))} />
</Head> </Head>
<div className="h-full"> <div className="h-full">
{isOverviewMode ? <DashboardEnvOverview /> : <DashboardPage envFromTop={queryEnv} />} <DashboardPage />
</div> </div>
</> </>
); );
@@ -53,7 +53,7 @@ export const OrgIncidentContactsTable = ({
isLoading isLoading
}: Props) => { }: Props) => {
const [searchContact, setSearchContact] = useState(""); const [searchContact, setSearchContact] = useState("");
const {data: serverDetails } = useFetchServerStatus() const { data: serverDetails } = useFetchServerStatus();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact", "addContact",
"removeContact", "removeContact",
@@ -98,7 +98,7 @@ export const OrgIncidentContactsTable = ({
<Button <Button
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
if (serverDetails?.emailConfigured){ if (serverDetails?.emailConfigured) {
handlePopUpOpen("addContact"); handlePopUpOpen("addContact");
} else { } else {
handlePopUpOpen("setUpEmail"); handlePopUpOpen("setUpEmail");
@@ -119,7 +119,7 @@ export const OrgIncidentContactsTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={2} key="incident-contact" />} {isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
{filteredContacts?.map(({ email }) => ( {filteredContacts?.map(({ email }) => (
<Tr key={email}> <Tr key={email}>
<Td className="w-full">{email}</Td> <Td className="w-full">{email}</Td>
@@ -1,7 +1,14 @@
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react"; import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
@@ -10,7 +17,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmailServiceSetupModal, EmptyState, EmailServiceSetupModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -29,7 +37,7 @@ import {
Tr, Tr,
UpgradePlanModal UpgradePlanModal
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization , useWorkspace } from "@app/context"; import { useOrganization, useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { useGetSSOConfig } from "@app/hooks/api"; import { useGetSSOConfig } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
@@ -47,8 +55,8 @@ type Props = {
onGrantAccess: (userId: string, publicKey: string) => Promise<void>; onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
// the current user id to block remove org button // the current user id to block remove org button
userId: string; userId: string;
completeInviteLink: string | undefined, completeInviteLink: string | undefined;
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>> setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>;
}; };
const addMemberFormSchema = yup.object({ const addMemberFormSchema = yup.object({
@@ -76,7 +84,7 @@ export const OrgMembersTable = ({
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? ""); const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
const [searchMemberFilter, setSearchMemberFilter] = useState(""); const [searchMemberFilter, setSearchMemberFilter] = useState("");
const {data: serverDetails } = useFetchServerStatus() const { data: serverDetails } = useFetchServerStatus();
const { workspaces } = useWorkspace(); const { workspaces } = useWorkspace();
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -101,7 +109,7 @@ export const OrgMembersTable = ({
const onAddMember = async ({ email }: TAddMemberForm) => { const onAddMember = async ({ email }: TAddMemberForm) => {
await onInviteMember(email); await onInviteMember(email);
if (serverDetails?.emailConfigured){ if (serverDetails?.emailConfigured) {
handlePopUpClose("addMember"); handlePopUpClose("addMember");
} }
@@ -190,7 +198,7 @@ export const OrgMembersTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={5} key="org-members" />} {isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
{!isLoading && {!isLoading &&
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => { filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
const name = user ? `${user.firstName} ${user.lastName}` : "-"; const name = user ? `${user.firstName} ${user.lastName}` : "-";
@@ -219,8 +227,14 @@ export const OrgMembersTable = ({
<SelectItem value="member">member</SelectItem> <SelectItem value="member">member</SelectItem>
</Select> </Select>
)} )}
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && ( {(status === "invited" || status === "verified") &&
<Button className='w-40' colorSchema="primary" variant="outline_bg" onClick={() => onInviteMember(email)}> serverDetails?.emailConfigured && (
<Button
className="w-40"
colorSchema="primary"
variant="outline_bg"
onClick={() => onInviteMember(email)}
>
Resend Invite Resend Invite
</Button> </Button>
)} )}
@@ -241,18 +255,33 @@ export const OrgMembersTable = ({
</Tag> </Tag>
)) ))
) : ( ) : (
<div className='flex flex-row'> <div className="flex flex-row">
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured) {(status === "invited" || status === "verified") &&
? <Tag colorSchema="red">This user hasn&apos;t accepted the invite yet</Tag> serverDetails?.emailConfigured ? (
: <Tag colorSchema="red">This user isn&apos;t part of any projects yet</Tag>} <Tag colorSchema="red">
{router.query.id !== "undefined" && !((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && <button This user hasn&apos;t accepted the invite yet
</Tag>
) : (
<Tag colorSchema="red">
This user isn&apos;t part of any projects yet
</Tag>
)}
{router.query.id !== "undefined" &&
!(
(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured
) && (
<button
type="button" type="button"
onClick={() => router.push(`/project/${workspaces[0]?._id}/members`)} onClick={() =>
className='text-sm bg-mineshaft w-max px-1.5 py-0.5 hover:bg-primary duration-200 hover:text-black cursor-pointer rounded-sm' router.push(`/project/${workspaces[0]?._id}/members`)
}
className="w-max cursor-pointer rounded-sm bg-mineshaft px-1.5 py-0.5 text-sm duration-200 hover:bg-primary hover:text-black"
> >
<FontAwesomeIcon icon={faPlus} className="mr-1" /> <FontAwesomeIcon icon={faPlus} className="mr-1" />
Add to projects Add to projects
</button>} </button>
)}
</div> </div>
)} )}
</Td> </Td>
@@ -282,23 +311,27 @@ export const OrgMembersTable = ({
isOpen={popUp?.addMember?.isOpen} isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("addMember", isOpen); handlePopUpToggle("addMember", isOpen);
setCompleteInviteLink(undefined) setCompleteInviteLink(undefined);
}} }}
> >
<ModalContent <ModalContent
title={`Invite others to ${orgName}`} title={`Invite others to ${orgName}`}
subTitle={ subTitle={
<div> <div>
{!completeInviteLink && <div> {!completeInviteLink && (
<div>
An invite is specific to an email address and expires after 1 day. An invite is specific to an email address and expires after 1 day.
<br /> <br />
For security reasons, you will need to separately add members to projects. For security reasons, you will need to separately add members to projects.
</div>} </div>
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"} )}
{completeInviteLink &&
"This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div> </div>
} }
> >
{!completeInviteLink && <form onSubmit={handleSubmit(onAddMember)} > {!completeInviteLink && (
<form onSubmit={handleSubmit(onAddMember)}>
<Controller <Controller
control={control} control={control}
defaultValue="" defaultValue=""
@@ -327,9 +360,9 @@ export const OrgMembersTable = ({
Cancel Cancel
</Button> </Button>
</div> </div>
</form>} </form>
{ )}
completeInviteLink && {completeInviteLink && (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400"> <div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{completeInviteLink}</p> <p className="mr-4 break-all">{completeInviteLink}</p>
<IconButton <IconButton
@@ -339,10 +372,12 @@ export const OrgMembersTable = ({
onClick={copyTokenToClipboard} onClick={copyTokenToClipboard}
> >
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} /> <FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span> <span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
click to copy
</span>
</IconButton> </IconButton>
</div> </div>
} )}
</ModalContent> </ModalContent>
</Modal> </Modal>
<DeleteActionModal <DeleteActionModal
@@ -1,5 +1,5 @@
import { useEffect, useMemo,useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Controller,useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { import {
faCheck, faCheck,
@@ -8,7 +8,8 @@ import {
faPencil, faPencil,
faPlus, faPlus,
faServer, faServer,
faTrash} from "@fortawesome/free-solid-svg-icons"; faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
@@ -39,7 +40,8 @@ import { usePopUp, useToggle } from "@app/hooks";
import { import {
useCreateServiceAccount, useCreateServiceAccount,
useDeleteServiceAccount, useDeleteServiceAccount,
useGetServiceAccounts} from "@app/hooks/api"; useGetServiceAccounts
} from "@app/hooks/api";
const serviceAccountExpiration = [ const serviceAccountExpiration = [
{ label: "1 Day", value: 86400 }, { label: "1 Day", value: 86400 },
@@ -73,10 +75,11 @@ export const OrgServiceAccountsTable = () => {
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState(""); const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addServiceAccount", "addServiceAccount",
"removeServiceAccount", "removeServiceAccount"
] as const); ] as const);
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId); const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
useGetServiceAccounts(orgId);
const createServiceAccount = useCreateServiceAccount(); const createServiceAccount = useCreateServiceAccount();
const removeServiceAccount = useDeleteServiceAccount(); const removeServiceAccount = useDeleteServiceAccount();
@@ -123,20 +126,17 @@ export const OrgServiceAccountsTable = () => {
setStep(1); setStep(1);
reset(); reset();
} };
const onRemoveServiceAccount = async () => { const onRemoveServiceAccount = async () => {
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id; const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
await removeServiceAccount.mutateAsync(serviceAccountId); await removeServiceAccount.mutateAsync(serviceAccountId);
handlePopUpClose("removeServiceAccount"); handlePopUpClose("removeServiceAccount");
} };
const filteredServiceAccounts = useMemo( const filteredServiceAccounts = useMemo(
() => () =>
serviceAccounts.filter( serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
({ name }) =>
name.toLowerCase().includes(searchServiceAccountFilter)
),
[serviceAccounts, searchServiceAccountFilter] [serviceAccounts, searchServiceAccountFilter]
); );
@@ -206,7 +206,7 @@ export const OrgServiceAccountsTable = () => {
return ( return (
<> <>
<p>Access Key</p> <p>Access Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]"> <div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{accessKey}</p> <p className="mr-4 break-all">{accessKey}</p>
<IconButton <IconButton
ariaLabel="copy icon" ariaLabel="copy icon"
@@ -224,7 +224,7 @@ export const OrgServiceAccountsTable = () => {
</IconButton> </IconButton>
</div> </div>
<p className="mt-4">Public Key</p> <p className="mt-4">Public Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]"> <div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{publicKey}</p> <p className="mr-4 break-all">{publicKey}</p>
<IconButton <IconButton
ariaLabel="copy icon" ariaLabel="copy icon"
@@ -242,7 +242,7 @@ export const OrgServiceAccountsTable = () => {
</IconButton> </IconButton>
</div> </div>
<p className="mt-4">Private Key</p> <p className="mt-4">Private Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]"> <div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{privateKey}</p> <p className="mr-4 break-all">{privateKey}</p>
<IconButton <IconButton
ariaLabel="copy icon" ariaLabel="copy icon"
@@ -259,13 +259,12 @@ export const OrgServiceAccountsTable = () => {
</span> </span>
</IconButton> </IconButton>
</div> </div>
</> </>
); );
default: default:
return <div /> return <div />;
}
} }
};
return ( return (
<div className="w-full"> <div className="w-full">
@@ -297,13 +296,11 @@ export const OrgServiceAccountsTable = () => {
<Th aria-label="actions" /> <Th aria-label="actions" />
</THead> </THead>
<TBody> <TBody>
{isServiceAccountsLoading && <TableSkeleton columns={5} key="org-service-accounts" />} {isServiceAccountsLoading && (
{!isServiceAccountsLoading && ( <TableSkeleton columns={5} innerKey="org-service-accounts" />
filteredServiceAccounts.map(({ )}
name, {!isServiceAccountsLoading &&
expiresAt, filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
_id: serviceAccountId
}) => {
return ( return (
<Tr key={`org-service-account-${serviceAccountId}`}> <Tr key={`org-service-account-${serviceAccountId}`}>
<Td>{name}</Td> <Td>{name}</Td>
@@ -315,7 +312,9 @@ export const OrgServiceAccountsTable = () => {
colorSchema="secondary" colorSchema="secondary"
onClick={() => { onClick={() => {
if (currentWorkspace?._id) { if (currentWorkspace?._id) {
router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`); router.push(
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
);
} }
}} }}
className="mr-2" className="mr-2"
@@ -325,7 +324,9 @@ export const OrgServiceAccountsTable = () => {
<IconButton <IconButton
ariaLabel="delete" ariaLabel="delete"
colorSchema="danger" colorSchema="danger"
onClick={() => handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })} onClick={() =>
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
}
> >
<FontAwesomeIcon icon={faTrash} /> <FontAwesomeIcon icon={faTrash} />
</IconButton> </IconButton>
@@ -333,8 +334,7 @@ export const OrgServiceAccountsTable = () => {
</Td> </Td>
</Tr> </Tr>
); );
}) })}
)}
</TBody> </TBody>
</Table> </Table>
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && ( {!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
@@ -364,4 +364,4 @@ export const OrgServiceAccountsTable = () => {
/> />
</div> </div>
); );
} };
@@ -10,9 +10,12 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr} from "@app/components/v2"; Tr
} from "@app/components/v2";
import timeSince from "@app/ee/utilities/timeSince"; 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"; import { RiskStatusSelection } from "./RiskStatusSelection";
@@ -23,14 +26,13 @@ export const SecretScanningLogsTable = () => {
useEffect(() => { useEffect(() => {
const fetchRisks = async () => { const fetchRisks = async () => {
setIsLoading(true); setIsLoading(true);
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id"))) const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")));
setGitRisks(risks); setGitRisks(risks);
setIsLoading(false); setIsLoading(false);
} };
fetchRisks(); fetchRisks();
},[]) }, []);
return ( return (
<TableContainer className="mt-8"> <TableContainer className="mt-8">
@@ -47,7 +49,9 @@ export const SecretScanningLogsTable = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{!isLoading && gitRisks && gitRisks?.map((risk) => { {!isLoading &&
gitRisks &&
gitRisks?.map((risk) => {
return ( return (
<Tr key={risk.ruleID} className="h-10"> <Tr key={risk.ruleID} className="h-10">
<Td>{timeSince(new Date(risk.createdAt))}</Td> <Td>{timeSince(new Date(risk.createdAt))}</Td>
@@ -56,7 +60,8 @@ export const SecretScanningLogsTable = () => {
<a <a
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`} href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
target="_blank" target="_blank"
className="text-red-500" rel="noreferrer" className="text-red-500"
rel="noreferrer"
> >
View Exposed Secret View Exposed Secret
</a> </a>
@@ -68,27 +73,26 @@ export const SecretScanningLogsTable = () => {
</a> </a>
</div> </div>
<div className="text-xs"> <div className="text-xs">
<span>{risk.file}</span><br/> <span>{risk.file}</span>
<br/> <br />
<span className="font-bold">{risk.author}</span><br/> <br />
<span className="font-bold">{risk.author}</span>
<br />
<span>{risk.email}</span> <span>{risk.email}</span>
</div> </div>
</Td> </Td>
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td> <Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
<Td> <Td>
<RiskStatusSelection riskId={risk._id} currentSelection={risk.status}/> <RiskStatusSelection riskId={risk._id} currentSelection={risk.status} />
</Td> </Td>
</Tr> </Tr>
); );
})} })}
{isLoading && <TableSkeleton columns={7} key="gitRisks" />} {isLoading && <TableSkeleton columns={7} innerKey="gitRisks" />}
{!isLoading && gitRisks && gitRisks?.length === 0 && ( {!isLoading && gitRisks && gitRisks?.length === 0 && (
<Tr> <Tr>
<Td colSpan={7}> <Td colSpan={7}>
<EmptyState <EmptyState title="No risks detected." icon={faCheck} />
title="No risks detected."
icon={faCheck}
/>
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -96,4 +100,4 @@ export const SecretScanningLogsTable = () => {
</Table> </Table>
</TableContainer> </TableContainer>
); );
} };
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@@ -10,11 +10,10 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr} from "@app/components/v2"; Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import { useGetOrgPlanTable } from "@app/hooks/api";
useGetOrgPlanTable
} from "@app/hooks/api";
export const CurrentPlanSection = () => { export const CurrentPlanSection = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -24,27 +23,17 @@ export const CurrentPlanSection = () => {
if (value === null) return "-"; if (value === null) return "-";
if (typeof value === "boolean") { if (typeof value === "boolean") {
if (value) return ( if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
<FontAwesomeIcon
icon={faCircleCheck}
color='#2ecc71'
/>
);
return ( return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
<FontAwesomeIcon
icon={faCircleXmark}
color='#e74c3c'
/>
);
} }
return value; return value;
} };
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Current Usage</h2> <h2 className="mb-8 flex-1 text-xl font-semibold text-white">Current Usage</h2>
<TableContainer className="mt-4"> <TableContainer className="mt-4">
<Table> <Table>
<THead> <THead>
@@ -55,11 +44,10 @@ export const CurrentPlanSection = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{!isLoading && data && data?.rows?.length > 0 && data.rows.map(({ {!isLoading &&
name, data &&
allowed, data?.rows?.length > 0 &&
used data.rows.map(({ name, allowed, used }) => {
}) => {
return ( return (
<Tr key={`current-plan-row-${name}`} className="h-12"> <Tr key={`current-plan-row-${name}`} className="h-12">
<Td>{name}</Td> <Td>{name}</Td>
@@ -68,14 +56,11 @@ export const CurrentPlanSection = () => {
</Tr> </Tr>
); );
})} })}
{isLoading && <TableSkeleton columns={5} key="invoices" />} {isLoading && <TableSkeleton columns={5} innerKey="invoices" />}
{!isLoading && data && data?.rows?.length === 0 && ( {!isLoading && data && data?.rows?.length === 0 && (
<Tr> <Tr>
<Td colSpan={3}> <Td colSpan={3}>
<EmptyState <EmptyState title="No plan details found" icon={faFileInvoice} />
title="No plan details found"
icon={faFileInvoice}
/>
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -84,4 +69,4 @@ export const CurrentPlanSection = () => {
</TableContainer> </TableContainer>
</div> </div>
); );
} };
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@@ -11,20 +11,16 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr, Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization,useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import { useCreateCustomerPortalSession, useGetOrgPlansTable } from "@app/hooks/api";
useCreateCustomerPortalSession,
useGetOrgPlansTable} from "@app/hooks/api";
type Props = { type Props = {
billingCycle: "monthly" | "yearly" billingCycle: "monthly" | "yearly";
} };
export const ManagePlansTable = ({ export const ManagePlansTable = ({ billingCycle }: Props) => {
billingCycle
}: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({ const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
@@ -37,23 +33,13 @@ export const ManagePlansTable = ({
if (value === null) return "Unlimited"; if (value === null) return "Unlimited";
if (typeof value === "boolean") { if (typeof value === "boolean") {
if (value) return ( if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
<FontAwesomeIcon
icon={faCircleCheck}
color='#2ecc71'
/>
);
return ( return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
<FontAwesomeIcon
icon={faCircleXmark}
color='#e74c3c'
/>
);
} }
return value; return value;
} };
return ( return (
<TableContainer> <TableContainer>
@@ -62,14 +48,11 @@ export const ManagePlansTable = ({
{subscription && !isTableDataLoading && tableData && ( {subscription && !isTableDataLoading && tableData && (
<Tr> <Tr>
<Th className="">Feature / Limit</Th> <Th className="">Feature / Limit</Th>
{tableData.head.map(({ {tableData.head.map(({ name, priceLine }) => {
name,
priceLine
}) => {
return ( return (
<Th <Th
key={`plans-feature-head-${billingCycle}-${name}`} key={`plans-feature-head-${billingCycle}-${name}`}
className="text-center flex-1" className="flex-1 text-center"
> >
<p>{name}</p> <p>{name}</p>
<p>{priceLine}</p> <p>{priceLine}</p>
@@ -80,68 +63,46 @@ export const ManagePlansTable = ({
)} )}
</THead> </THead>
<TBody> <TBody>
{subscription && !isTableDataLoading && tableData && tableData.rows.map(({ {subscription &&
name, !isTableDataLoading &&
starter, tableData &&
team, tableData.rows.map(({ name, starter, team, pro, enterprise }) => {
pro,
enterprise
}) => {
return ( return (
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}> <Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
<Td>{displayCell(name)}</Td> <Td>{displayCell(name)}</Td>
<Td className="text-center"> <Td className="text-center">{displayCell(starter)}</Td>
{displayCell(starter)} <Td className="text-center">{displayCell(team)}</Td>
</Td> <Td className="text-center">{displayCell(pro)}</Td>
<Td className="text-center"> <Td className="text-center">{displayCell(enterprise)}</Td>
{displayCell(team)}
</Td>
<Td className="text-center">
{displayCell(pro)}
</Td>
<Td className="text-center">
{displayCell(enterprise)}
</Td>
</Tr> </Tr>
); );
})} })}
{isTableDataLoading && <TableSkeleton columns={5} key="cloud-products" />} {isTableDataLoading && <TableSkeleton columns={5} innerKey="cloud-products" />}
{!isTableDataLoading && tableData?.rows.length === 0 && ( {!isTableDataLoading && tableData?.rows.length === 0 && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={5}>
<EmptyState <EmptyState title="No cloud product details found" icon={faFileInvoice} />
title="No cloud product details found"
icon={faFileInvoice}
/>
</Td> </Td>
</Tr> </Tr>
)} )}
{subscription && !isTableDataLoading && tableData && ( {subscription && !isTableDataLoading && tableData && (
<Tr className="h-12"> <Tr className="h-12">
<Td /> <Td />
{tableData.head.map(({ {tableData.head.map(({ slug, tier }) => {
slug,
tier
}) => {
const isCurrentPlan = slug === subscription.slug; const isCurrentPlan = slug === subscription.slug;
let subscriptionText = "Upgrade"; let subscriptionText = "Upgrade";
if (subscription.tier > tier) { if (subscription.tier > tier) {
subscriptionText = "Downgrade" subscriptionText = "Downgrade";
} }
if (tier === 3) { if (tier === 3) {
subscriptionText = "Contact sales" subscriptionText = "Contact sales";
} }
return isCurrentPlan ? ( return isCurrentPlan ? (
<Td> <Td>
<Button <Button colorSchema="secondary" className="w-full" isDisabled>
colorSchema="secondary"
className="w-full"
isDisabled
>
Current Current
</Button> </Button>
</Td> </Td>
@@ -152,7 +113,9 @@ export const ManagePlansTable = ({
if (!currentOrg?._id) return; if (!currentOrg?._id) return;
if (tier !== 3) { if (tier !== 3) {
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id); const { url } = await createCustomerPortalSession.mutateAsync(
currentOrg._id
);
window.location.href = url; window.location.href = url;
return; return;
} }
@@ -173,4 +136,4 @@ export const ManagePlansTable = ({
</Table> </Table>
</TableContainer> </TableContainer>
); );
} };
@@ -14,10 +14,7 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
useDeleteOrgPmtMethod,
useGetOrgPmtMethods
} from "@app/hooks/api";
export const PmtMethodsTable = () => { export const PmtMethodsTable = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -30,7 +27,7 @@ export const PmtMethodsTable = () => {
organizationId: currentOrg._id, organizationId: currentOrg._id,
pmtMethodId pmtMethodId
}); });
} };
return ( return (
<TableContainer className="mt-4"> <TableContainer className="mt-4">
@@ -45,14 +42,10 @@ export const PmtMethodsTable = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{!isLoading && data && data?.length > 0 && data.map(({ {!isLoading &&
_id, data &&
brand, data?.length > 0 &&
exp_month, data.map(({ _id, brand, exp_month, exp_year, funding, last4 }) => (
exp_year,
funding,
last4
}) => (
<Tr key={`pmt-method-${_id}`} className="h-10"> <Tr key={`pmt-method-${_id}`} className="h-10">
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td> <Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td> <Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
@@ -73,14 +66,11 @@ export const PmtMethodsTable = () => {
</Td> </Td>
</Tr> </Tr>
))} ))}
{isLoading && <TableSkeleton columns={5} key="pmt-methods" />} {isLoading && <TableSkeleton columns={5} innerKey="pmt-methods" />}
{!isLoading && data && data?.length === 0 && ( {!isLoading && data && data?.length === 0 && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={5}>
<EmptyState <EmptyState title="No payment methods on file" icon={faCreditCard} />
title="No payment methods on file"
icon={faCreditCard}
/>
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -88,4 +78,4 @@ export const PmtMethodsTable = () => {
</Table> </Table>
</TableContainer> </TableContainer>
); );
} };
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@@ -11,65 +11,62 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr, Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
useDeleteOrgTaxId,
useGetOrgTaxIds
} from "@app/hooks/api";
const taxIDTypeLabelMap: { [key: string]: string } = { const taxIDTypeLabelMap: { [key: string]: string } = {
"au_abn": "Australia ABN", au_abn: "Australia ABN",
"au_arn": "Australia ARN", au_arn: "Australia ARN",
"bg_uic": "Bulgaria UIC", bg_uic: "Bulgaria UIC",
"br_cnpj": "Brazil CNPJ", br_cnpj: "Brazil CNPJ",
"br_cpf": "Brazil CPF", br_cpf: "Brazil CPF",
"ca_bn": "Canada BN", ca_bn: "Canada BN",
"ca_gst_hst": "Canada GST/HST", ca_gst_hst: "Canada GST/HST",
"ca_pst_bc": "Canada PST BC", ca_pst_bc: "Canada PST BC",
"ca_pst_mb": "Canada PST MB", ca_pst_mb: "Canada PST MB",
"ca_pst_sk": "Canada PST SK", ca_pst_sk: "Canada PST SK",
"ca_qst": "Canada QST", ca_qst: "Canada QST",
"ch_vat": "Switzerland VAT", ch_vat: "Switzerland VAT",
"cl_tin": "Chile TIN", cl_tin: "Chile TIN",
"eg_tin": "Egypt TIN", eg_tin: "Egypt TIN",
"es_cif": "Spain CIF", es_cif: "Spain CIF",
"eu_oss_vat": "EU OSS VAT", eu_oss_vat: "EU OSS VAT",
"eu_vat": "EU VAT", eu_vat: "EU VAT",
"gb_vat": "GB VAT", gb_vat: "GB VAT",
"ge_vat": "Georgia VAT", ge_vat: "Georgia VAT",
"hk_br": "Hong Kong BR", hk_br: "Hong Kong BR",
"hu_tin": "Hungary TIN", hu_tin: "Hungary TIN",
"id_npwp": "Indonesia NPWP", id_npwp: "Indonesia NPWP",
"il_vat": "Israel VAT", il_vat: "Israel VAT",
"in_gst": "India GST", in_gst: "India GST",
"is_vat": "Iceland VAT", is_vat: "Iceland VAT",
"jp_cn": "Japan CN", jp_cn: "Japan CN",
"jp_rn": "Japan RN", jp_rn: "Japan RN",
"jp_trn": "Japan TRN", jp_trn: "Japan TRN",
"ke_pin": "Kenya PIN", ke_pin: "Kenya PIN",
"kr_brn": "South Korea BRN", kr_brn: "South Korea BRN",
"li_uid": "Liechtenstein UID", li_uid: "Liechtenstein UID",
"mx_rfc": "Mexico RFC", mx_rfc: "Mexico RFC",
"my_frp": "Malaysia FRP", my_frp: "Malaysia FRP",
"my_itn": "Malaysia ITN", my_itn: "Malaysia ITN",
"my_sst": "Malaysia SST", my_sst: "Malaysia SST",
"no_vat": "Norway VAT", no_vat: "Norway VAT",
"nz_gst": "New Zealand GST", nz_gst: "New Zealand GST",
"ph_tin": "Philippines TIN", ph_tin: "Philippines TIN",
"ru_inn": "Russia INN", ru_inn: "Russia INN",
"ru_kpp": "Russia KPP", ru_kpp: "Russia KPP",
"sa_vat": "Saudi Arabia VAT", sa_vat: "Saudi Arabia VAT",
"sg_gst": "Singapore GST", sg_gst: "Singapore GST",
"sg_uen": "Singapore UEN", sg_uen: "Singapore UEN",
"si_tin": "Slovenia TIN", si_tin: "Slovenia TIN",
"th_vat": "Thailand VAT", th_vat: "Thailand VAT",
"tr_tin": "Turkey TIN", tr_tin: "Turkey TIN",
"tw_vat": "Taiwan VAT", tw_vat: "Taiwan VAT",
"ua_vat": "Ukraine VAT", ua_vat: "Ukraine VAT",
"us_ein": "US EIN", us_ein: "US EIN",
"za_vat": "South Africa VAT" za_vat: "South Africa VAT"
}; };
export const TaxIDTable = () => { export const TaxIDTable = () => {
@@ -83,7 +80,7 @@ export const TaxIDTable = () => {
organizationId: currentOrg._id, organizationId: currentOrg._id,
taxId taxId
}); });
} };
return ( return (
<TableContainer className="mt-4"> <TableContainer className="mt-4">
@@ -96,11 +93,10 @@ export const TaxIDTable = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{!isLoading && data && data?.length > 0 && data.map(({ {!isLoading &&
_id, data &&
type, data?.length > 0 &&
value data.map(({ _id, type, value }) => (
}) => (
<Tr key={`tax-id-${_id}`} className="h-10"> <Tr key={`tax-id-${_id}`} className="h-10">
<Td>{taxIDTypeLabelMap[type]}</Td> <Td>{taxIDTypeLabelMap[type]}</Td>
<Td>{value}</Td> <Td>{value}</Td>
@@ -119,14 +115,11 @@ export const TaxIDTable = () => {
</Td> </Td>
</Tr> </Tr>
))} ))}
{isLoading && <TableSkeleton columns={3} key="tax-ids" />} {isLoading && <TableSkeleton columns={3} innerKey="tax-ids" />}
{!isLoading && data && data?.length === 0 && ( {!isLoading && data && data?.length === 0 && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={5}>
<EmptyState <EmptyState title="No Tax IDs on file" icon={faFileInvoice} />
title="No Tax IDs on file"
icon={faFileInvoice}
/>
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -134,4 +127,4 @@ export const TaxIDTable = () => {
</Table> </Table>
</TableContainer> </TableContainer>
); );
} };
@@ -14,9 +14,7 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import { useGetOrgInvoices } from "@app/hooks/api";
useGetOrgInvoices
} from "@app/hooks/api";
export const InvoicesTable = () => { export const InvoicesTable = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -34,17 +32,13 @@ export const InvoicesTable = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{!isLoading && data && data?.length > 0 && data.map(({ {!isLoading &&
_id, data &&
created, data?.length > 0 &&
paid, data.map(({ _id, created, paid, number, total, invoice_pdf }) => {
number,
total,
invoice_pdf
}) => {
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", { const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD"
}); });
const createdDate = new Date(created * 1000); const createdDate = new Date(created * 1000);
const day: number = createdDate.getDate(); const day: number = createdDate.getDate();
@@ -71,14 +65,11 @@ export const InvoicesTable = () => {
</Tr> </Tr>
); );
})} })}
{isLoading && <TableSkeleton columns={5} key="invoices" />} {isLoading && <TableSkeleton columns={5} innerKey="invoices" />}
{!isLoading && data && data?.length === 0 && ( {!isLoading && data && data?.length === 0 && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={5}>
<EmptyState <EmptyState title="No invoices on file" icon={faFileInvoice} />
title="No invoices on file"
icon={faFileInvoice}
/>
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -86,4 +77,4 @@ export const InvoicesTable = () => {
</Table> </Table>
</TableContainer> </TableContainer>
); );
} };
@@ -1,10 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { Controller,useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { import { faKey, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
faKey,
faMagnifyingGlass,
faPlus,
faTrash} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
@@ -12,7 +8,8 @@ import * as yup from "yup";
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric, encryptAssymmetric,
verifyPrivateKey} from "@app/components/utilities/cryptography/crypto"; verifyPrivateKey
} from "@app/components/utilities/cryptography/crypto";
import { import {
Button, Button,
Checkbox, Checkbox,
@@ -33,7 +30,8 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr} from "@app/components/v2"; Tr
} from "@app/components/v2";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import {
useCreateServiceAccountProjectLevelPermission, useCreateServiceAccountProjectLevelPermission,
@@ -48,33 +46,38 @@ const createProjectLevelPermissionSchema = yup.object({
privateKey: yup.string().required().label("Private Key"), privateKey: yup.string().required().label("Private Key"),
workspace: yup.string().required().label("Workspace"), workspace: yup.string().required().label("Workspace"),
environment: yup.string().required().label("Environment"), environment: yup.string().required().label("Environment"),
permissions: yup.object().shape({ permissions: yup
.object()
.shape({
read: yup.boolean().required(), read: yup.boolean().required(),
write: yup.boolean().required() write: yup.boolean().required()
}).defined().required() })
.defined()
.required()
}); });
type CreateProjectLevelPermissionForm = yup.InferType<typeof createProjectLevelPermissionSchema>; type CreateProjectLevelPermissionForm = yup.InferType<typeof createProjectLevelPermissionSchema>;
type Props = { type Props = {
serviceAccountId: string; serviceAccountId: string;
} };
export const SAProjectLevelPermissionsTable = ({ export const SAProjectLevelPermissionsTable = ({ serviceAccountId }: Props): JSX.Element => {
serviceAccountId
}: Props): JSX.Element => {
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId); const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces(); const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
const [searchPermissions, setSearchPermissions] = useState(""); const [searchPermissions, setSearchPermissions] = useState("");
const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } = useGetServiceAccountProjectLevelPermissions(serviceAccountId); const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } =
useGetServiceAccountProjectLevelPermissions(serviceAccountId);
const createServiceAccountProjectLevelPermission = useCreateServiceAccountProjectLevelPermission(); const createServiceAccountProjectLevelPermission =
const deleteServiceAccountProjectLevelPermission = useDeleteServiceAccountProjectLevelPermission(); useCreateServiceAccountProjectLevelPermission();
const deleteServiceAccountProjectLevelPermission =
useDeleteServiceAccountProjectLevelPermission();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addProjectLevelPermission", "addProjectLevelPermission",
"removeProjectLevelPermission", "removeProjectLevelPermission"
] as const); ] as const);
const [, setSelectedWorkspace] = useState<undefined | string>(undefined); const [, setSelectedWorkspace] = useState<undefined | string>(undefined);
@@ -84,7 +87,9 @@ export const SAProjectLevelPermissionsTable = ({
handleSubmit, handleSubmit,
reset, reset,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<CreateProjectLevelPermissionForm>({ resolver: yupResolver(createProjectLevelPermissionSchema) }) } = useForm<CreateProjectLevelPermissionForm>({
resolver: yupResolver(createProjectLevelPermissionSchema)
});
const onAddProjectLevelPermission = async ({ const onAddProjectLevelPermission = async ({
privateKey, privateKey,
@@ -92,7 +97,6 @@ export const SAProjectLevelPermissionsTable = ({
environment, environment,
permissions: { read, write } permissions: { read, write }
}: CreateProjectLevelPermissionForm) => { }: CreateProjectLevelPermissionForm) => {
// TODO: clean up / modularize this function // TODO: clean up / modularize this function
if (!serviceAccount) return; if (!serviceAccount) return;
@@ -131,16 +135,18 @@ export const SAProjectLevelPermissionsTable = ({
nonce nonce
}); });
handlePopUpClose("addProjectLevelPermission"); handlePopUpClose("addProjectLevelPermission");
} };
const onRemoveProjectLevelPermission = async () => { const onRemoveProjectLevelPermission = async () => {
const serviceAccountWorkspacePermissionId = (popUp?.removeProjectLevelPermission?.data as { _id: string })?._id; const serviceAccountWorkspacePermissionId = (
popUp?.removeProjectLevelPermission?.data as { _id: string }
)?._id;
await deleteServiceAccountProjectLevelPermission.mutateAsync({ await deleteServiceAccountProjectLevelPermission.mutateAsync({
serviceAccountId, serviceAccountId,
serviceAccountWorkspacePermissionId serviceAccountWorkspacePermissionId
}); });
handlePopUpClose("removeProjectLevelPermission"); handlePopUpClose("removeProjectLevelPermission");
} };
return ( return (
<div className="w-full bg-white/5 p-6"> <div className="w-full bg-white/5 p-6">
@@ -157,7 +163,7 @@ export const SAProjectLevelPermissionsTable = ({
<Button <Button
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
handlePopUpOpen("addProjectLevelPermission") handlePopUpOpen("addProjectLevelPermission");
reset(); reset();
}} }}
> >
@@ -176,33 +182,29 @@ export const SAProjectLevelPermissionsTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isPermissionsLoading && <TableSkeleton columns={6} key="service-account-project-level-permissions" />} {isPermissionsLoading && (
{!isPermissionsLoading && serviceAccountWorkspacePermissions && ( <TableSkeleton columns={6} innerKey="service-account-project-level-permissions" />
serviceAccountWorkspacePermissions.map(({ )}
_id, {!isPermissionsLoading &&
workspace, serviceAccountWorkspacePermissions &&
environment, serviceAccountWorkspacePermissions.map(
read, ({ _id, workspace, environment, read, write }) => {
write const environmentName = workspace.environments.find(
}) => { (env) => env.slug === environment
const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name; )?.name;
return ( return (
<Tr key={`service-account-project-level-permission-${_id}`} className="w-full"> <Tr key={`service-account-project-level-permission-${_id}`} className="w-full">
<Td>{workspace.name}</Td> <Td>{workspace.name}</Td>
<Td>{environmentName}</Td> <Td>{environmentName}</Td>
<Td> <Td>
<Checkbox <Checkbox id="isReadPermissionEnabled" isChecked={read} isDisabled>
id="isReadPermissionEnabled" {/**/}
isChecked={read} </Checkbox>
isDisabled
>{/**/}</Checkbox>
</Td> </Td>
<Td> <Td>
<Checkbox <Checkbox id="isWritePermissionEnabled" isChecked={write} isDisabled>
id="isWritePermissionEnabled" {/**/}
isChecked={write} </Checkbox>
isDisabled
>{/**/}</Checkbox>
</Td> </Td>
<Td> <Td>
<IconButton <IconButton
@@ -215,7 +217,7 @@ export const SAProjectLevelPermissionsTable = ({
</Td> </Td>
</Tr> </Tr>
); );
}) }
)} )}
{!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && ( {!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
<Tr> <Tr>
@@ -259,10 +261,7 @@ export const SAProjectLevelPermissionsTable = ({
name="workspace" name="workspace"
defaultValue={String(userWorkspaces?.[0]?._id)} defaultValue={String(userWorkspaces?.[0]?._id)}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl label="Project" errorText={error?.message}>
label="Project"
errorText={error?.message}
>
<Select <Select
defaultValue={field.value} defaultValue={field.value}
{...field} {...field}
@@ -270,12 +269,15 @@ export const SAProjectLevelPermissionsTable = ({
onChange(e); onChange(e);
setSelectedWorkspace(e); setSelectedWorkspace(e);
}} }}
className="w-full border border-mine-shaft-500" className="border-mine-shaft-500 w-full border"
> >
{userWorkspaces && userWorkspaces.length > 0 ? ( {userWorkspaces && userWorkspaces.length > 0 ? (
userWorkspaces.map((userWorkspace) => { userWorkspaces.map((userWorkspace) => {
return ( return (
<SelectItem value={userWorkspace._id} key={`project-${userWorkspace._id}`}> <SelectItem
value={userWorkspace._id}
key={`project-${userWorkspace._id}`}
>
{userWorkspace.name} {userWorkspace.name}
</SelectItem> </SelectItem>
); );
@@ -294,23 +296,26 @@ export const SAProjectLevelPermissionsTable = ({
name="environment" name="environment"
defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)} defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)}
render={({ field: { onChange, ...field } }) => { render={({ field: { onChange, ...field } }) => {
const environments =
userWorkspaces?.find(
/* eslint-disable-next-line no-underscore-dangle */ /* eslint-disable-next-line no-underscore-dangle */
const environments = userWorkspaces?.find((userWorkspace) => userWorkspace._id === control?._formValues?.workspace)?.environments ?? []; (userWorkspace) => userWorkspace._id === control?._formValues?.workspace
)?.environments ?? [];
return ( return (
<FormControl <FormControl label="Environment" className="mt-4">
label="Environment"
className="mt-4"
>
<Select <Select
defaultValue={field.value} defaultValue={field.value}
{...field} {...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full border border-mine-shaft-500" className="border-mine-shaft-500 w-full border"
> >
{environments.length > 0 ? ( {environments.length > 0 ? (
environments.map((environment) => { environments.map((environment) => {
return ( return (
<SelectItem value={environment.slug} key={`environment-${environment.slug}`}> <SelectItem
value={environment.slug}
key={`environment-${environment.slug}`}
>
{environment.name} {environment.name}
</SelectItem> </SelectItem>
); );
@@ -334,7 +339,7 @@ export const SAProjectLevelPermissionsTable = ({
read: true, read: true,
write: false write: false
}} }}
render={({ field: { onChange, value }, fieldState: { error }}) => { render={({ field: { onChange, value }, fieldState: { error } }) => {
const options = [ const options = [
{ {
label: "Read (default)", label: "Read (default)",
@@ -404,4 +409,4 @@ export const SAProjectLevelPermissionsTable = ({
/> />
</div> </div>
); );
} };
@@ -1,9 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { import { faContactBook, faMagnifyingGlass, faTrash } from "@fortawesome/free-solid-svg-icons";
faContactBook,
faMagnifyingGlass,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -23,9 +19,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
useDeleteIncidentContact,
useGetOrgIncidentContact} from "@app/hooks/api";
export const OrgIncidentContactsTable = () => { export const OrgIncidentContactsTable = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -63,9 +57,9 @@ export const OrgIncidentContactsTable = () => {
} }
}; };
const filteredContacts = contacts ? contacts.filter(({ email }) => const filteredContacts = contacts
email.toLocaleLowerCase().includes(searchContact) ? contacts.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
) : []; : [];
return ( return (
<div> <div>
@@ -84,7 +78,7 @@ export const OrgIncidentContactsTable = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={2} key="incident-contact" />} {isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
{filteredContacts?.map(({ email }) => ( {filteredContacts?.map(({ email }) => (
<Tr key={email}> <Tr key={email}>
<Td className="w-full">{email}</Td> <Td className="w-full">{email}</Td>
@@ -1,8 +1,4 @@
import { useEffect, useMemo,useState } from "react"; import { useEffect, useMemo, useState } from "react";
import {
// Controller,
// useForm
} from "react-hook-form";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { import {
faCheck, faCheck,
@@ -11,7 +7,8 @@ import {
faPencil, faPencil,
faPlus, faPlus,
faServer, faServer,
faTrash} from "@fortawesome/free-solid-svg-icons"; faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { yupResolver } from "@hookform/resolvers/yup"; // import { yupResolver } from "@hookform/resolvers/yup";
@@ -21,13 +18,13 @@ import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
// FormControl, // FormControl,
IconButton, IconButton,
Input, Input,
Modal, Modal,
ModalContent, ModalContent,
// Select, // Select,
// SelectItem, // SelectItem,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -45,6 +42,10 @@ import {
useGetServiceAccounts useGetServiceAccounts
} from "@app/hooks/api"; } from "@app/hooks/api";
import // Controller,
// useForm
"react-hook-form";
// const serviceAccountExpiration = [ // const serviceAccountExpiration = [
// { label: "1 Day", value: 86400 }, // { label: "1 Day", value: 86400 },
// { label: "7 Days", value: 604800 }, // { label: "7 Days", value: 604800 },
@@ -77,10 +78,11 @@ export const OrgServiceAccountsTable = () => {
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState(""); const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addServiceAccount", "addServiceAccount",
"removeServiceAccount", "removeServiceAccount"
] as const); ] as const);
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId); const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
useGetServiceAccounts(orgId);
// const createServiceAccount = useCreateServiceAccount(); // const createServiceAccount = useCreateServiceAccount();
const removeServiceAccount = useDeleteServiceAccount(); const removeServiceAccount = useDeleteServiceAccount();
@@ -133,14 +135,11 @@ export const OrgServiceAccountsTable = () => {
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id; const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
await removeServiceAccount.mutateAsync(serviceAccountId); await removeServiceAccount.mutateAsync(serviceAccountId);
handlePopUpClose("removeServiceAccount"); handlePopUpClose("removeServiceAccount");
} };
const filteredServiceAccounts = useMemo( const filteredServiceAccounts = useMemo(
() => () =>
serviceAccounts.filter( serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
({ name }) =>
name.toLowerCase().includes(searchServiceAccountFilter)
),
[serviceAccounts, searchServiceAccountFilter] [serviceAccounts, searchServiceAccountFilter]
); );
@@ -149,8 +148,8 @@ export const OrgServiceAccountsTable = () => {
case 0: case 0:
return ( return (
<div> <div>
We are currently revising the service account mechanism. In the meantime, We are currently revising the service account mechanism. In the meantime, please use
please use service tokens or API key to fetch secrets via API request. service tokens or API key to fetch secrets via API request.
</div> </div>
// <form onSubmit={handleSubmit(onAddServiceAccount)}> // <form onSubmit={handleSubmit(onAddServiceAccount)}>
// <Controller // <Controller
@@ -214,7 +213,7 @@ export const OrgServiceAccountsTable = () => {
return ( return (
<> <>
<p>Access Key</p> <p>Access Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]"> <div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{accessKey}</p> <p className="mr-4 break-all">{accessKey}</p>
<IconButton <IconButton
ariaLabel="copy icon" ariaLabel="copy icon"
@@ -232,7 +231,7 @@ export const OrgServiceAccountsTable = () => {
</IconButton> </IconButton>
</div> </div>
<p className="mt-4">Public Key</p> <p className="mt-4">Public Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]"> <div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{publicKey}</p> <p className="mr-4 break-all">{publicKey}</p>
<IconButton <IconButton
ariaLabel="copy icon" ariaLabel="copy icon"
@@ -250,7 +249,7 @@ export const OrgServiceAccountsTable = () => {
</IconButton> </IconButton>
</div> </div>
<p className="mt-4">Private Key</p> <p className="mt-4">Private Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]"> <div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{privateKey}</p> <p className="mr-4 break-all">{privateKey}</p>
<IconButton <IconButton
ariaLabel="copy icon" ariaLabel="copy icon"
@@ -267,17 +266,16 @@ export const OrgServiceAccountsTable = () => {
</span> </span>
</IconButton> </IconButton>
</div> </div>
</> </>
); );
default: default:
return <div /> return <div />;
}
} }
};
return ( return (
<div className="w-full"> <div className="w-full">
<div className="flex justify-between mb-4"> <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p> <p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
<Button <Button
colorSchema="secondary" colorSchema="secondary"
@@ -305,13 +303,11 @@ export const OrgServiceAccountsTable = () => {
<Th aria-label="actions" /> <Th aria-label="actions" />
</THead> </THead>
<TBody> <TBody>
{isServiceAccountsLoading && <TableSkeleton columns={5} key="org-service-accounts" />} {isServiceAccountsLoading && (
{!isServiceAccountsLoading && ( <TableSkeleton columns={5} innerKey="org-service-accounts" />
filteredServiceAccounts.map(({ )}
name, {!isServiceAccountsLoading &&
expiresAt, filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
_id: serviceAccountId
}) => {
return ( return (
<Tr key={`org-service-account-${serviceAccountId}`}> <Tr key={`org-service-account-${serviceAccountId}`}>
<Td>{name}</Td> <Td>{name}</Td>
@@ -323,7 +319,9 @@ export const OrgServiceAccountsTable = () => {
colorSchema="secondary" colorSchema="secondary"
onClick={() => { onClick={() => {
if (currentWorkspace?._id) { if (currentWorkspace?._id) {
router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`); router.push(
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
);
} }
}} }}
className="mr-2" className="mr-2"
@@ -333,7 +331,9 @@ export const OrgServiceAccountsTable = () => {
<IconButton <IconButton
ariaLabel="delete" ariaLabel="delete"
colorSchema="danger" colorSchema="danger"
onClick={() => handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })} onClick={() =>
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
}
> >
<FontAwesomeIcon icon={faTrash} /> <FontAwesomeIcon icon={faTrash} />
</IconButton> </IconButton>
@@ -341,8 +341,7 @@ export const OrgServiceAccountsTable = () => {
</Td> </Td>
</Tr> </Tr>
); );
}) })}
)}
</TBody> </TBody>
</Table> </Table>
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && ( {!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
@@ -372,4 +371,4 @@ export const OrgServiceAccountsTable = () => {
/> />
</div> </div>
); );
} };
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -14,9 +14,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { import { useDeleteAPIKey, useGetMyAPIKeys } from "@app/hooks/api";
useDeleteAPIKey,
useGetMyAPIKeys} from "@app/hooks/api";
export const APIKeyTable = () => { export const APIKeyTable = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -37,7 +35,7 @@ export const APIKeyTable = () => {
type: "error" type: "error"
}); });
} }
} };
const formatDate = (dateToFormat: string) => { const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat); const date = new Date(dateToFormat);
@@ -48,7 +46,7 @@ export const APIKeyTable = () => {
const formattedDate = `${day}/${month}/${year}`; const formattedDate = `${day}/${month}/${year}`;
return formattedDate; return formattedDate;
} };
return ( return (
<div> <div>
@@ -64,14 +62,11 @@ export const APIKeyTable = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={4} key="api-keys" />} {isLoading && <TableSkeleton columns={4} innerKey="api-keys" />}
{!isLoading && data && data.length > 0 && data.map(({ {!isLoading &&
_id, data &&
name, data.length > 0 &&
createdAt, data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => {
expiresAt,
lastUsed
}) => {
return ( return (
<Tr className="h-10" key={`api-key-${_id}`}> <Tr className="h-10" key={`api-key-${_id}`}>
<Td>{name}</Td> <Td>{name}</Td>
@@ -97,10 +92,7 @@ export const APIKeyTable = () => {
{!isLoading && data && data?.length === 0 && ( {!isLoading && data && data?.length === 0 && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={5}>
<EmptyState <EmptyState title="No API Keys on file" icon={faKey} />
title="No API Keys on file"
icon={faKey}
/>
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -109,4 +101,4 @@ export const APIKeyTable = () => {
</TableContainer> </TableContainer>
</div> </div>
); );
} };
@@ -25,7 +25,7 @@ export const SessionsTable = () => {
const formattedDate = `${day}/${month}/${year}`; const formattedDate = `${day}/${month}/${year}`;
return formattedDate; return formattedDate;
} };
return ( return (
<TableContainer className="mt-4"> <TableContainer className="mt-4">
@@ -39,14 +39,11 @@ export const SessionsTable = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={4} key="sesssions" />} {isLoading && <TableSkeleton columns={4} innerKey="sesssions" />}
{!isLoading && data && data.length > 0 && data.map(({ {!isLoading &&
_id, data &&
createdAt, data.length > 0 &&
lastUsed, data.map(({ _id, createdAt, lastUsed, ip, userAgent }) => {
ip,
userAgent
}) => {
return ( return (
<Tr className="h-10" key={`session-${_id}`}> <Tr className="h-10" key={`session-${_id}`}>
<Td>{formatDate(createdAt)}</Td> <Td>{formatDate(createdAt)}</Td>
@@ -59,16 +56,12 @@ export const SessionsTable = () => {
{!isLoading && data && data?.length === 0 && ( {!isLoading && data && data?.length === 0 && (
<Tr> <Tr>
<Td colSpan={4}> <Td colSpan={4}>
<EmptyState <EmptyState title="No sessions on file" icon={faServer} />
title="No sessions on file"
icon={faServer}
/>
</Td> </Td>
</Tr> </Tr>
)} )}
</TBody> </TBody>
</Table> </Table>
</TableContainer> </TableContainer>
); );
} };
@@ -11,7 +11,7 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr, Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -29,9 +29,7 @@ type Props = {
) => void; ) => void;
}; };
export const EnvironmentTable = ({ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
handlePopUpOpen
}: Props) => {
const { currentWorkspace, isLoading } = useWorkspace(); const { currentWorkspace, isLoading } = useWorkspace();
return ( return (
<TableContainer> <TableContainer>
@@ -44,8 +42,10 @@ export const EnvironmentTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={3} key="project-envs" />} {isLoading && <TableSkeleton columns={3} innerKey="project-envs" />}
{!isLoading && currentWorkspace && currentWorkspace.environments.map(({ name, slug }) => ( {!isLoading &&
currentWorkspace &&
currentWorkspace.environments.map(({ name, slug }) => (
<Tr key={name}> <Tr key={name}>
<Td>{name}</Td> <Td>{name}</Td>
<Td>{slug}</Td> <Td>{slug}</Td>
@@ -86,4 +86,4 @@ export const EnvironmentTable = ({
</Table> </Table>
</TableContainer> </TableContainer>
); );
} };
@@ -30,10 +30,8 @@ type Props = {
) => void; ) => void;
}; };
export const SecretTagsTable = ({ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
handlePopUpOpen const { currentWorkspace } = useWorkspace();
}: Props) => {
const { currentWorkspace }= useWorkspace();
const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? ""); const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? "");
return ( return (
@@ -47,8 +45,10 @@ export const SecretTagsTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={3} key="secret-tags" />} {isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />}
{!isLoading && data && data.map(({ _id, name, slug }) => ( {!isLoading &&
data &&
data.map(({ _id, name, slug }) => (
<Tr key={name}> <Tr key={name}>
<Td>{name}</Td> <Td>{name}</Td>
<Td>{slug}</Td> <Td>{slug}</Td>
@@ -79,4 +79,4 @@ export const SecretTagsTable = ({
</Table> </Table>
</TableContainer> </TableContainer>
); );
} };
@@ -48,7 +48,7 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />} {isLoading && <TableSkeleton columns={4} innerKey="project-service-tokens" />}
{!isLoading && {!isLoading &&
data && data &&
data.map((row) => ( data.map((row) => (
@@ -160,7 +160,7 @@ export const WebhooksTab = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isWebhooksLoading && <TableSkeleton columns={5} key="webhooks-loading" />} {isWebhooksLoading && <TableSkeleton columns={5} innerKey="webhooks-loading" />}
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && ( {!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={5}>