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