mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 13:26:31 +00:00
feat: added new secret input component and updated toolbar key special prop to innerKey
This commit is contained in:
@@ -58,15 +58,13 @@ export default function NavHeader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center pt-6">
|
<div className="flex flex-row items-center pt-6">
|
||||||
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-black text-sm">
|
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm text-black">
|
||||||
{currentOrg?.name?.charAt(0)}
|
{currentOrg?.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link passHref legacyBehavior href={`/org/${currentOrg?._id}/overview`}>
|
||||||
passHref
|
<a className="pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary">
|
||||||
legacyBehavior
|
{currentOrg?.name}
|
||||||
href={`/org/${currentOrg?._id}/overview`}
|
</a>
|
||||||
>
|
|
||||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary pl-0.5">{currentOrg?.name}</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
{isProjectRelated && (
|
{isProjectRelated && (
|
||||||
<>
|
<>
|
||||||
@@ -85,7 +83,7 @@ export default function NavHeader({
|
|||||||
<Link
|
<Link
|
||||||
passHref
|
passHref
|
||||||
legacyBehavior
|
legacyBehavior
|
||||||
href={{ pathname: "/project/[id]/secrets", query: { id: router.query.id } }}
|
href={{ pathname: "/project/[id]/secrets/overview", query: { id: router.query.id } }}
|
||||||
>
|
>
|
||||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -126,7 +124,11 @@ export default function NavHeader({
|
|||||||
{index + 1 === folders?.length ? (
|
{index + 1 === folders?.length ? (
|
||||||
<span className="text-sm font-semibold text-bunker-300">{name}</span>
|
<span className="text-sm font-semibold text-bunker-300">{name}</span>
|
||||||
) : (
|
) : (
|
||||||
<Link passHref legacyBehavior href={{ pathname: "/project/[id]/secrets", query }}>
|
<Link
|
||||||
|
passHref
|
||||||
|
legacyBehavior
|
||||||
|
href={{ pathname: "/project/[id]/secrets/[env]", query }}
|
||||||
|
>
|
||||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
||||||
{name === "root" ? selectedEnv?.name : name}
|
{name === "root" ? selectedEnv?.name : name}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/* eslint-disable react/no-danger */
|
||||||
|
import { HTMLAttributes } from "react";
|
||||||
|
import ContentEditable from "react-contenteditable";
|
||||||
|
import sanitizeHtml from "sanitize-html";
|
||||||
|
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
const REGEX = /\${([^}]+)}/g;
|
||||||
|
const stripSpanTags = (str: string) => str.replace(/<\/?span[^>]*>/g, "");
|
||||||
|
const replaceContentWithDot = (str: string) => {
|
||||||
|
let finalStr = "";
|
||||||
|
let isHtml = false;
|
||||||
|
for (let i = 0; i < str.length; i += 1) {
|
||||||
|
const char = str.at(i);
|
||||||
|
|
||||||
|
if (char === "<" || char === ">") {
|
||||||
|
isHtml = char === "<";
|
||||||
|
finalStr += char;
|
||||||
|
} else if (!isHtml && char !== "\n") {
|
||||||
|
finalStr += "•";
|
||||||
|
} 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">${<span class="ph-no-capture text-yello-200/80">${b}</span>}</span>`
|
||||||
|
);
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeConf = {
|
||||||
|
allowedTags: ["div", "span", "br", "p"]
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "onBlur"> & {
|
||||||
|
value?: string | null;
|
||||||
|
isVisible?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
onChange?: (val: string, html: string) => void;
|
||||||
|
onBlur?: (sanitizedHtml: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretInput = ({
|
||||||
|
value,
|
||||||
|
isVisible,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
isDisabled,
|
||||||
|
...props
|
||||||
|
}: Props) => {
|
||||||
|
const [isSecretFocused, setIsSecretFocused] = useToggle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="thin-scrollbar relative overflow-y-auto overflow-x-hidden"
|
||||||
|
style={{ maxHeight: `${21 * 7}px` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: syntaxHighlight(value, isVisible || isSecretFocused)
|
||||||
|
}}
|
||||||
|
className="absolute top-0 left-0 z-0 h-full w-full text-ellipsis whitespace-pre-line break-all"
|
||||||
|
/>
|
||||||
|
<ContentEditable
|
||||||
|
className="relative z-10 h-full w-full text-ellipsis whitespace-pre-line break-all text-transparent caret-white outline-none"
|
||||||
|
role="textbox"
|
||||||
|
onChange={(evt) => {
|
||||||
|
if (onChange) onChange(evt.currentTarget.innerText.trim(), evt.currentTarget.innerHTML);
|
||||||
|
}}
|
||||||
|
onFocus={() => setIsSecretFocused.on()}
|
||||||
|
disabled={isDisabled}
|
||||||
|
spellCheck={false}
|
||||||
|
onBlur={(evt) => {
|
||||||
|
if (onBlur) onBlur(sanitizeHtml(evt.currentTarget.innerHTML || "", sanitizeConf));
|
||||||
|
setIsSecretFocused.off();
|
||||||
|
}}
|
||||||
|
html={isVisible || isSecretFocused ? value || "" : syntaxHighlight(value, false)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretInput } from "./SecretInput";
|
||||||
@@ -52,7 +52,7 @@ export const Loading: Story = {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
<TableSkeleton columns={3} key="story-book-table" />
|
<TableSkeleton columns={3} innerKey="story-book-table" />
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
@@ -33,10 +33,7 @@ export type TableProps = {
|
|||||||
|
|
||||||
export const Table = ({ children, className }: TableProps): JSX.Element => (
|
export const Table = ({ children, className }: TableProps): JSX.Element => (
|
||||||
<table
|
<table
|
||||||
className={twMerge(
|
className={twMerge("w-full bg-mineshaft-800 p-2 text-left text-sm text-gray-300", className)}
|
||||||
"w-full bg-mineshaft-800 p-2 text-left text-sm text-gray-300",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
@@ -58,11 +55,24 @@ export const THead = ({ children, className }: THeadProps): JSX.Element => (
|
|||||||
export type TrProps = {
|
export type TrProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
isHoverable?: boolean;
|
||||||
|
isSelectable?: boolean;
|
||||||
} & HTMLAttributes<HTMLTableRowElement>;
|
} & HTMLAttributes<HTMLTableRowElement>;
|
||||||
|
|
||||||
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
|
export const Tr = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
isHoverable,
|
||||||
|
isSelectable,
|
||||||
|
...props
|
||||||
|
}: TrProps): JSX.Element => (
|
||||||
<tr
|
<tr
|
||||||
className={twMerge("border border-solid border-mineshaft-700 cursor-default", className)}
|
className={twMerge(
|
||||||
|
"cursor-default border border-solid border-mineshaft-700",
|
||||||
|
isHoverable && "hover:bg-mineshaft-600",
|
||||||
|
isSelectable && "cursor-pointer",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -76,7 +86,14 @@ export type ThProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Th = ({ children, className }: ThProps): JSX.Element => (
|
export const Th = ({ children, className }: ThProps): JSX.Element => (
|
||||||
<th className={twMerge("bg-mineshaft-800 px-5 pt-4 pb-3.5 font-semibold border-b-2 border-mineshaft-600", className)}>{children}</th>
|
<th
|
||||||
|
className={twMerge(
|
||||||
|
"border-b-2 border-mineshaft-600 bg-mineshaft-800 px-5 pt-4 pb-3.5 font-semibold",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
);
|
);
|
||||||
|
|
||||||
// table body
|
// table body
|
||||||
@@ -106,15 +123,15 @@ export type TBodyLoader = {
|
|||||||
columns: number;
|
columns: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
// unique key for mapping
|
// unique key for mapping
|
||||||
key: string;
|
innerKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TableSkeleton = ({ rows = 3, columns, key, className }: TBodyLoader): JSX.Element => (
|
export const TableSkeleton = ({ rows = 3, columns, innerKey, className }: TBodyLoader): JSX.Element => (
|
||||||
<>
|
<>
|
||||||
{Array.apply(0, Array(rows)).map((_x, i) => (
|
{Array.apply(0, Array(rows)).map((_x, i) => (
|
||||||
<Tr key={`${key}-skeleton-rows-${i + 1}`}>
|
<Tr key={`${innerKey}-skeleton-rows-${i + 1}`}>
|
||||||
{Array.apply(0, Array(columns)).map((_y, j) => (
|
{Array.apply(0, Array(columns)).map((_y, j) => (
|
||||||
<Td key={`${key}-skeleton-rows-${i + 1}-column-${j + 1}`}>
|
<Td key={`${innerKey}-skeleton-rows-${i + 1}-column-${j + 1}`}>
|
||||||
<Skeleton className={className} />
|
<Skeleton className={className} />
|
||||||
</Td>
|
</Td>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export * from "./Checkbox";
|
|||||||
export * from "./DeleteActionModal";
|
export * from "./DeleteActionModal";
|
||||||
export * from "./Drawer";
|
export * from "./Drawer";
|
||||||
export * from "./Dropdown";
|
export * from "./Dropdown";
|
||||||
export * from "./EmailServiceSetupModal"
|
export * from "./EmailServiceSetupModal";
|
||||||
export * from "./EmptyState";
|
export * from "./EmptyState";
|
||||||
export * from "./FormControl";
|
export * from "./FormControl";
|
||||||
export * from "./HoverCardv2";
|
export * from "./HoverCardv2";
|
||||||
@@ -13,6 +13,7 @@ export * from "./Input";
|
|||||||
export * from "./Menu";
|
export * from "./Menu";
|
||||||
export * from "./Modal";
|
export * from "./Modal";
|
||||||
export * from "./Popoverv2";
|
export * from "./Popoverv2";
|
||||||
|
export * from "./SecretInput";
|
||||||
export * from "./Select";
|
export * from "./Select";
|
||||||
export * from "./Skeleton";
|
export * from "./Skeleton";
|
||||||
export * from "./Spinner";
|
export * from "./Spinner";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
useDeleteFolder,
|
useDeleteFolder,
|
||||||
|
useGetFoldersByEnv,
|
||||||
useGetProjectFolders,
|
useGetProjectFolders,
|
||||||
useGetProjectFoldersBatch,
|
useGetProjectFoldersBatch,
|
||||||
useUpdateFolder
|
useUpdateFolder
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
DeleteFolderDTO,
|
DeleteFolderDTO,
|
||||||
GetProjectFoldersBatchDTO,
|
GetProjectFoldersBatchDTO,
|
||||||
GetProjectFoldersDTO,
|
GetProjectFoldersDTO,
|
||||||
|
TGetFoldersByEnvDTO,
|
||||||
TSecretFolder,
|
TSecretFolder,
|
||||||
UpdateFolderDTO
|
UpdateFolderDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -62,6 +63,48 @@ export const useGetProjectFolders = ({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useGetFoldersByEnv = ({
|
||||||
|
parentFolderPath,
|
||||||
|
workspaceId,
|
||||||
|
environments,
|
||||||
|
parentFolderId
|
||||||
|
}: TGetFoldersByEnvDTO) => {
|
||||||
|
const folders = useQueries({
|
||||||
|
queries: environments.map((env) => ({
|
||||||
|
queryKey: queryKeys.getSecretFolders(workspaceId, env, parentFolderPath || parentFolderId),
|
||||||
|
queryFn: async () => fetchProjectFolders(workspaceId, env, parentFolderId, parentFolderPath),
|
||||||
|
enabled: Boolean(workspaceId) && Boolean(env)
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const folderNames = useMemo(() => {
|
||||||
|
const names = new Set<string>();
|
||||||
|
folders?.forEach(({ data }) => {
|
||||||
|
data?.folders.forEach(({ name }) => {
|
||||||
|
names.add(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return [...names];
|
||||||
|
}, [(folders || []).map((folder) => folder.data)]);
|
||||||
|
|
||||||
|
const isFolderPresentInEnv = useCallback(
|
||||||
|
(name: string, env: string) => {
|
||||||
|
const selectedEnvIndex = environments.indexOf(env);
|
||||||
|
if (selectedEnvIndex !== -1) {
|
||||||
|
return Boolean(
|
||||||
|
folders?.[selectedEnvIndex]?.data?.folders?.find(
|
||||||
|
({ name: folderName }) => folderName === name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[(folders || []).map((folder) => folder.data)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { folders, folderNames, isFolderPresentInEnv };
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetProjectFoldersBatch = ({
|
export const useGetProjectFoldersBatch = ({
|
||||||
folders = [],
|
folders = [],
|
||||||
isPaused,
|
isPaused,
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ export type GetProjectFoldersBatchDTO = {
|
|||||||
parentFolderPath?: string;
|
parentFolderPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetFoldersByEnvDTO = {
|
||||||
|
environments: string[];
|
||||||
|
workspaceId: string;
|
||||||
|
parentFolderPath?: string;
|
||||||
|
parentFolderId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateFolderDTO = {
|
export type CreateFolderDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
export { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "./mutations";
|
||||||
export {
|
export {
|
||||||
useBatchSecretsOp,
|
useBatchSecretsOp,
|
||||||
useGetProjectSecrets,
|
useGetProjectSecrets,
|
||||||
useGetProjectSecretsByKey,
|
useGetProjectSecretsAllEnv,
|
||||||
useGetSecretVersion
|
useGetSecretVersion
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptSymmetric
|
||||||
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { secretKeys } from "./queries";
|
||||||
|
import { TCreateSecretsV3DTO, TDeleteSecretsV3DTO, TUpdateSecretsV3DTO } from "./types";
|
||||||
|
|
||||||
|
const encryptSecret = (randomBytes: string, key: string, value?: string, comment?: string) => {
|
||||||
|
// encrypt key
|
||||||
|
const {
|
||||||
|
ciphertext: secretKeyCiphertext,
|
||||||
|
iv: secretKeyIV,
|
||||||
|
tag: secretKeyTag
|
||||||
|
} = encryptSymmetric({
|
||||||
|
plaintext: key,
|
||||||
|
key: randomBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
// encrypt value
|
||||||
|
const {
|
||||||
|
ciphertext: secretValueCiphertext,
|
||||||
|
iv: secretValueIV,
|
||||||
|
tag: secretValueTag
|
||||||
|
} = encryptSymmetric({
|
||||||
|
plaintext: value ?? "",
|
||||||
|
key: randomBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
// encrypt comment
|
||||||
|
const {
|
||||||
|
ciphertext: secretCommentCiphertext,
|
||||||
|
iv: secretCommentIV,
|
||||||
|
tag: secretCommentTag
|
||||||
|
} = encryptSymmetric({
|
||||||
|
plaintext: comment ?? "",
|
||||||
|
key: randomBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateSecretV3 = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<{}, {}, TCreateSecretsV3DTO>({
|
||||||
|
mutationFn: async ({
|
||||||
|
secretPath = "/",
|
||||||
|
type,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
secretName,
|
||||||
|
secretValue,
|
||||||
|
latestFileKey,
|
||||||
|
secretComment
|
||||||
|
}) => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
|
|
||||||
|
const randomBytes = latestFileKey
|
||||||
|
? decryptAssymmetric({
|
||||||
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
|
nonce: latestFileKey.nonce,
|
||||||
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
})
|
||||||
|
: crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
|
const reqBody = {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath,
|
||||||
|
...encryptSecret(randomBytes, secretName, secretValue, secretComment)
|
||||||
|
};
|
||||||
|
const { data } = await apiRequest.post(`/api/v3/secrets/${secretName}`, reqBody);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId, environment, secretPath }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretKeys.getProjectSecret(workspaceId, environment, secretPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateSecretV3 = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<{}, {}, TUpdateSecretsV3DTO>({
|
||||||
|
mutationFn: async ({
|
||||||
|
secretPath = "/",
|
||||||
|
type,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
secretName,
|
||||||
|
secretValue,
|
||||||
|
latestFileKey
|
||||||
|
}) => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
|
|
||||||
|
const randomBytes = latestFileKey
|
||||||
|
? decryptAssymmetric({
|
||||||
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
|
nonce: latestFileKey.nonce,
|
||||||
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
})
|
||||||
|
: crypto.randomBytes(16).toString("hex");
|
||||||
|
const { secretValueIV, secretValueTag, secretValueCiphertext } = encryptSecret(
|
||||||
|
randomBytes,
|
||||||
|
secretName,
|
||||||
|
secretValue,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
const reqBody = {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueCiphertext
|
||||||
|
};
|
||||||
|
const { data } = await apiRequest.patch(`/api/v3/secrets/${secretName}`, reqBody);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId, environment, secretPath }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretKeys.getProjectSecret(workspaceId, environment, secretPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteSecretV3 = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TDeleteSecretsV3DTO>({
|
||||||
|
mutationFn: async ({ secretPath = "/", type, environment, workspaceId, secretName }) => {
|
||||||
|
const reqBody = {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, {
|
||||||
|
data: reqBody
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId, environment, secretPath }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretKeys.getProjectSecret(workspaceId, environment, secretPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -38,40 +38,15 @@ const fetchProjectEncryptedSecrets = async (
|
|||||||
folderId?: string,
|
folderId?: string,
|
||||||
secretPath?: string
|
secretPath?: string
|
||||||
) => {
|
) => {
|
||||||
if (typeof env === "string") {
|
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
|
||||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
|
params: {
|
||||||
params: {
|
environment: env,
|
||||||
environment: env,
|
workspaceId,
|
||||||
workspaceId,
|
folderId: folderId || undefined,
|
||||||
folderId: folderId || undefined,
|
secretPath
|
||||||
secretPath
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return data.secrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof env === "object") {
|
|
||||||
let allEnvData: any = [];
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
for (const envPoint of env) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
|
|
||||||
params: {
|
|
||||||
environment: envPoint,
|
|
||||||
workspaceId,
|
|
||||||
folderId,
|
|
||||||
secretPath
|
|
||||||
}
|
|
||||||
});
|
|
||||||
allEnvData = allEnvData.concat(data.secrets);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return allEnvData;
|
return data.secrets;
|
||||||
// eslint-disable-next-line no-else-return
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetProjectSecrets = ({
|
export const useGetProjectSecrets = ({
|
||||||
@@ -167,31 +142,13 @@ export const useGetProjectSecretsAllEnv = ({
|
|||||||
decryptFileKey,
|
decryptFileKey,
|
||||||
folderId,
|
folderId,
|
||||||
secretPath
|
secretPath
|
||||||
}: TGetProjectSecretsAllEnvDTO) =>
|
}: TGetProjectSecretsAllEnvDTO) => {
|
||||||
useQueries({
|
const secrets = useQueries({
|
||||||
queries: envs.map((env) => ({
|
queries: envs.map((env) => ({
|
||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath || folderId),
|
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath || folderId),
|
||||||
enabled: Boolean(decryptFileKey && workspaceId && env),
|
enabled: Boolean(decryptFileKey && workspaceId && env),
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath)
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
||||||
}))
|
select: (data: EncryptedSecret[]) => {
|
||||||
});
|
|
||||||
|
|
||||||
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[]) => {
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const latestKey = decryptFileKey;
|
const latestKey = decryptFileKey;
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
@@ -201,12 +158,11 @@ export const useGetProjectSecretsByKey = ({
|
|||||||
privateKey: PRIVATE_KEY
|
privateKey: PRIVATE_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
|
const sharedSecrets: Record<string, DecryptedSecret> = {};
|
||||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||||
// this used for add-only mode in dashboard
|
// this used for add-only mode in dashboard
|
||||||
// type won't be there thus only one key is shown
|
// type won't be there thus only one key is shown
|
||||||
const duplicateSecretKey: Record<string, boolean> = {};
|
const duplicateSecretKey: Record<string, boolean> = {};
|
||||||
const uniqSecKeys: Record<string, boolean> = {};
|
|
||||||
data.forEach((encSecret: EncryptedSecret) => {
|
data.forEach((encSecret: EncryptedSecret) => {
|
||||||
const secretKey = decryptSymmetric({
|
const secretKey = decryptSymmetric({
|
||||||
ciphertext: encSecret.secretKeyCiphertext,
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
@@ -214,7 +170,6 @@ export const useGetProjectSecretsByKey = ({
|
|||||||
tag: encSecret.secretKeyTag,
|
tag: encSecret.secretKeyTag,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
|
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
const secretValue = decryptSymmetric({
|
||||||
ciphertext: encSecret.secretValueCiphertext,
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
@@ -242,35 +197,65 @@ export const useGetProjectSecretsByKey = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (encSecret.type === "personal") {
|
if (encSecret.type === "personal") {
|
||||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
personalSecrets[decryptedSecret.key] = {
|
||||||
id: encSecret._id,
|
id: encSecret._id,
|
||||||
value: secretValue
|
value: secretValue
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
if (!duplicateSecretKey?.[decryptedSecret.key]) {
|
||||||
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
|
sharedSecrets[decryptedSecret.key] = decryptedSecret;
|
||||||
sharedSecrets[secretKey].push(decryptedSecret);
|
|
||||||
}
|
}
|
||||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
duplicateSecretKey[decryptedSecret.key] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Object.keys(sharedSecrets).forEach((secName) => {
|
|
||||||
sharedSecrets[secName].forEach((val) => {
|
|
||||||
const dupKey = `${val.key}-${val.env}`;
|
|
||||||
if (personalSecrets?.[dupKey]) {
|
|
||||||
val.idOverride = personalSecrets[dupKey].id;
|
|
||||||
val.valueOverride = personalSecrets[dupKey].value;
|
|
||||||
val.overrideAction = "modified";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
Object.keys(sharedSecrets).forEach((val) => {
|
||||||
},
|
if (personalSecrets?.[val]) {
|
||||||
[decryptFileKey]
|
sharedSecrets[val].idOverride = personalSecrets[val].id;
|
||||||
)
|
sharedSecrets[val].valueOverride = personalSecrets[val].value;
|
||||||
|
sharedSecrets[val].overrideAction = "modified";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sharedSecrets;
|
||||||
|
}
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secKeys = useMemo(() => {
|
||||||
|
const keys = new Set<string>();
|
||||||
|
secrets?.forEach(({ data }) => {
|
||||||
|
// TODO(akhilmhdh): find out why this is unknown
|
||||||
|
Object.keys(data || {}).forEach((key) => keys.add(key));
|
||||||
|
});
|
||||||
|
return [...keys];
|
||||||
|
}, [(secrets || []).map((sec) => sec.data)]);
|
||||||
|
|
||||||
|
const getEnvSecretKeyCount = useCallback(
|
||||||
|
(env: string) => {
|
||||||
|
const selectedEnvIndex = envs.indexOf(env);
|
||||||
|
if (selectedEnvIndex !== -1) {
|
||||||
|
return Object.keys(secrets[selectedEnvIndex]?.data || {}).length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
[(secrets || []).map((sec) => sec.data)]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSecretByKey = useCallback(
|
||||||
|
(env: string, key: string) => {
|
||||||
|
const selectedEnvIndex = envs.indexOf(env);
|
||||||
|
if (selectedEnvIndex !== -1) {
|
||||||
|
const sec = secrets[selectedEnvIndex]?.data?.[key];
|
||||||
|
return sec;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
[(secrets || []).map((sec) => sec.data)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { data: secrets, secKeys, getSecretByKey, getEnvSecretKeyCount };
|
||||||
|
};
|
||||||
|
|
||||||
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||||
const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
|
const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
|
||||||
`/api/v1/secret/${secretId}/secret-versions`,
|
`/api/v1/secret/${secretId}/secret-versions`,
|
||||||
|
|||||||
@@ -118,3 +118,32 @@ export type GetSecretVersionsDTO = {
|
|||||||
offset: number;
|
offset: number;
|
||||||
decryptFileKey: UserWsKeyPair;
|
decryptFileKey: UserWsKeyPair;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCreateSecretsV3DTO = {
|
||||||
|
latestFileKey: UserWsKeyPair;
|
||||||
|
secretName: string;
|
||||||
|
secretValue: string;
|
||||||
|
secretComment: string;
|
||||||
|
secretPath: string;
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateSecretsV3DTO = {
|
||||||
|
latestFileKey: UserWsKeyPair;
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
type: string;
|
||||||
|
secretPath: string;
|
||||||
|
secretName: string;
|
||||||
|
secretValue: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSecretsV3DTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
type: string;
|
||||||
|
secretPath: string;
|
||||||
|
secretName: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,7 +13,18 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faAngleDown, faArrowLeft, faArrowUpRightFromSquare, faBook, faCheck, faEnvelope, faInfinity, faMobile, faPlus, faQuestion } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faAngleDown,
|
||||||
|
faArrowLeft,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBook,
|
||||||
|
faCheck,
|
||||||
|
faEnvelope,
|
||||||
|
faInfinity,
|
||||||
|
faMobile,
|
||||||
|
faPlus,
|
||||||
|
faQuestion
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||||
@@ -41,7 +52,14 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useGetOrgTrialUrl, useLogoutUser, useUploadWsKey } from "@app/hooks/api";
|
import {
|
||||||
|
fetchOrgUsers,
|
||||||
|
useAddUserToWs,
|
||||||
|
useCreateWorkspace,
|
||||||
|
useGetOrgTrialUrl,
|
||||||
|
useLogoutUser,
|
||||||
|
useUploadWsKey
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -89,7 +107,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
// const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true);
|
// const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true);
|
||||||
|
|
||||||
const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true;
|
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||||
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
|
: true;
|
||||||
|
|
||||||
const createWs = useCreateWorkspace();
|
const createWs = useCreateWorkspace();
|
||||||
const uploadWsKey = useUploadWsKey();
|
const uploadWsKey = useUploadWsKey();
|
||||||
@@ -110,22 +130,22 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = () => {
|
const handleRouteChange = () => {
|
||||||
(window).Intercom("update");
|
window.Intercom("update");
|
||||||
};
|
};
|
||||||
|
|
||||||
router.events.on("routeChangeComplete", handleRouteChange);
|
router.events.on("routeChangeComplete", handleRouteChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off("routeChangeComplete", handleRouteChange);
|
router.events.off("routeChangeComplete", handleRouteChange);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const logout = useLogoutUser();
|
const logout = useLogoutUser();
|
||||||
const logOutUser = async () => {
|
const logOutUser = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("Logging out...")
|
console.log("Logging out...");
|
||||||
await logout.mutateAsync();
|
await logout.mutateAsync();
|
||||||
localStorage.removeItem("protectedKey");
|
localStorage.removeItem("protectedKey");
|
||||||
localStorage.removeItem("protectedKeyIV");
|
localStorage.removeItem("protectedKeyIV");
|
||||||
@@ -145,27 +165,30 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const changeOrg = async (orgId) => {
|
const changeOrg = async (orgId) => {
|
||||||
localStorage.setItem("orgData.id", orgId);
|
localStorage.setItem("orgData.id", orgId);
|
||||||
router.push(`/org/${orgId}/overview`)
|
router.push(`/org/${orgId}/overview`);
|
||||||
}
|
};
|
||||||
|
|
||||||
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
|
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
|
||||||
// Placing the localstorage as much as possible
|
// Placing the localstorage as much as possible
|
||||||
// Wait till tony integrates the azure and its launched
|
// Wait till tony integrates the azure and its launched
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
// Put a user in an org if they're not in one yet
|
// Put a user in an org if they're not in one yet
|
||||||
const putUserInOrg = async () => {
|
const putUserInOrg = async () => {
|
||||||
if (tempLocalStorage("orgData.id") === "") {
|
if (tempLocalStorage("orgData.id") === "") {
|
||||||
localStorage.setItem("orgData.id", orgs[0]?._id);
|
localStorage.setItem("orgData.id", orgs[0]?._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentOrg && (
|
if (
|
||||||
(workspaces?.length === 0 && router.asPath.includes("project"))
|
currentOrg &&
|
||||||
|| router.asPath.includes("/project/undefined")
|
((workspaces?.length === 0 && router.asPath.includes("project")) ||
|
||||||
|| (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration"))
|
router.asPath.includes("/project/undefined") ||
|
||||||
)) {
|
(!orgs?.map((org) => org._id)?.includes(router.query.id) &&
|
||||||
|
!router.asPath.includes("project") &&
|
||||||
|
!router.asPath.includes("personal") &&
|
||||||
|
!router.asPath.includes("integration")))
|
||||||
|
) {
|
||||||
router.push(`/org/${currentOrg?._id}/overview`);
|
router.push(`/org/${currentOrg?._id}/overview`);
|
||||||
}
|
}
|
||||||
// else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
|
// else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
|
||||||
|
|
||||||
// const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
|
// const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
|
||||||
@@ -233,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
}
|
}
|
||||||
createNotification({ text: "Workspace created", type: "success" });
|
createNotification({ text: "Workspace created", type: "success" });
|
||||||
handlePopUpClose("addNewWs");
|
handlePopUpClose("addNewWs");
|
||||||
router.push(`/project/${newWorkspaceId}/secrets`);
|
router.push(`/project/${newWorkspaceId}/secrets/overview`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||||
@@ -244,190 +267,235 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<>
|
<>
|
||||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||||
<aside className="w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60 dark">
|
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||||
<div>
|
<div>
|
||||||
{!router.asPath.includes("personal") && <div className="h-12 px-3 flex items-center pt-6 cursor-default">
|
{!router.asPath.includes("personal") && (
|
||||||
{(router.asPath.includes("project") || router.asPath.includes("integrations")) && <Link href={`/org/${currentOrg?._id}/overview`}><div className="pl-1 pr-2 text-mineshaft-400 hover:text-mineshaft-100 duration-200">
|
<div className="flex h-12 cursor-default items-center px-3 pt-6">
|
||||||
<FontAwesomeIcon icon={faArrowLeft} />
|
{(router.asPath.includes("project") ||
|
||||||
</div></Link>}
|
router.asPath.includes("integrations")) && (
|
||||||
<DropdownMenu>
|
<Link href={`/org/${currentOrg?._id}/overview`}>
|
||||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
<div className="pl-1 pr-2 text-mineshaft-400 duration-200 hover:text-mineshaft-100">
|
||||||
<div className="mr-auto flex items-center hover:bg-mineshaft-600 py-1.5 pl-1.5 pr-2 rounded-md">
|
<FontAwesomeIcon icon={faArrowLeft} />
|
||||||
<div className="w-5 h-5 rounded-md bg-primary flex justify-center items-center text-sm">{currentOrg?.name.charAt(0)}</div>
|
</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>
|
</Link>
|
||||||
</div>
|
)}
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenu>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
||||||
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div>
|
<div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
|
||||||
{orgs?.map(org => <DropdownMenuItem key={org._id}>
|
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm">
|
||||||
<Button
|
{currentOrg?.name.charAt(0)}
|
||||||
onClick={() => changeOrg(org?._id)}
|
</div>
|
||||||
variant="plain"
|
<div className="pl-3 text-sm text-mineshaft-100">
|
||||||
colorSchema="secondary"
|
{currentOrg?.name}{" "}
|
||||||
size="xs"
|
<FontAwesomeIcon
|
||||||
className="w-full flex items-center justify-start p-0 font-normal"
|
icon={faAngleDown}
|
||||||
leftIcon={currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="mr-3 text-primary"/>}
|
className="pl-1 pt-1 text-xs text-mineshaft-300"
|
||||||
>
|
/>
|
||||||
<div className="w-full flex justify-between items-center">{org.name}</div>
|
</div>
|
||||||
</Button>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
)}
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
<div className="h-1 mt-1 border-t border-mineshaft-600"/>
|
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
|
||||||
<button
|
{orgs?.map((org) => (
|
||||||
type="button"
|
<DropdownMenuItem key={org._id}>
|
||||||
onClick={logOutUser}
|
<Button
|
||||||
className="w-full"
|
onClick={() => changeOrg(org?._id)}
|
||||||
>
|
variant="plain"
|
||||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
colorSchema="secondary"
|
||||||
</button>
|
size="xs"
|
||||||
</DropdownMenuContent>
|
className="flex w-full items-center justify-start p-0 font-normal"
|
||||||
</DropdownMenu>
|
leftIcon={
|
||||||
<DropdownMenu>
|
currentOrg._id === org._id && (
|
||||||
<DropdownMenuTrigger asChild className="hover:bg-primary-400 hover:text-black data-[state=open]:text-black data-[state=open]:bg-primary-400 p-1">
|
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
|
||||||
<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)}
|
}
|
||||||
</div>
|
>
|
||||||
</DropdownMenuTrigger>
|
<div className="flex w-full items-center justify-between">
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
{org.name}
|
||||||
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div>
|
</div>
|
||||||
<Link href="/personal-settings"><DropdownMenuItem>Personal Settings</DropdownMenuItem></Link>
|
</Button>
|
||||||
<a
|
</DropdownMenuItem>
|
||||||
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"
|
|
||||||
>
|
|
||||||
<DropdownMenuItem>Documentation<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></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"
|
|
||||||
>
|
|
||||||
<DropdownMenuItem>Join Slack Community<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></DropdownMenuItem>
|
|
||||||
</a>
|
|
||||||
<div className="h-1 mt-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 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
|
|
||||||
</p>
|
|
||||||
<Select
|
|
||||||
defaultValue={currentWorkspace?._id}
|
|
||||||
value={currentWorkspace?._id}
|
|
||||||
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
|
|
||||||
onValueChange={(value) => {
|
|
||||||
router.push(`/project/${value}/secrets`);
|
|
||||||
localStorage.setItem("projectData.id", value);
|
|
||||||
}}
|
|
||||||
position="popper"
|
|
||||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
|
||||||
>
|
|
||||||
<div className='h-full no-scrollbar no-scrollbar::-webkit-scrollbar'>
|
|
||||||
{workspaces
|
|
||||||
.filter((ws) => ws.organization === currentOrg?._id)
|
|
||||||
.map(({ _id, name }) => (
|
|
||||||
<SelectItem
|
|
||||||
key={`ws-layout-list-${_id}`}
|
|
||||||
value={_id}
|
|
||||||
className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
<button type="button" onClick={logOutUser} className="w-full">
|
||||||
<div className="w-full">
|
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||||
<Button
|
</button>
|
||||||
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
</DropdownMenuContent>
|
||||||
colorSchema="primary"
|
</DropdownMenu>
|
||||||
variant="outline_bg"
|
<DropdownMenu>
|
||||||
size="sm"
|
<DropdownMenuTrigger
|
||||||
onClick={() => {
|
asChild
|
||||||
if (isAddingProjectsAllowed) {
|
className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
|
||||||
handlePopUpOpen("addNewWs")
|
>
|
||||||
} else {
|
<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">
|
||||||
handlePopUpOpen("upgradePlan");
|
{user?.firstName?.charAt(0)}
|
||||||
}
|
{user?.lastName && user?.lastName?.charAt(0)}
|
||||||
}}
|
</div>
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<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="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||||
>
|
>
|
||||||
Add Project
|
<DropdownMenuItem>
|
||||||
</Button>
|
Documentation
|
||||||
</div>
|
<FontAwesomeIcon
|
||||||
</Select>
|
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="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="mb-[0.06rem] pl-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</a>
|
||||||
|
<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>
|
</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"/>
|
{!router.asPath.includes("org") &&
|
||||||
Back to organization
|
(!router.asPath.includes("personal") && currentWorkspace ? (
|
||||||
</div></Link>)}
|
<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
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
defaultValue={currentWorkspace?._id}
|
||||||
|
value={currentWorkspace?._id}
|
||||||
|
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
|
||||||
|
onValueChange={(value) => {
|
||||||
|
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="no-scrollbar::-webkit-scrollbar h-full no-scrollbar">
|
||||||
|
{workspaces
|
||||||
|
.filter((ws) => ws.organization === currentOrg?._id)
|
||||||
|
.map(({ _id, name }) => (
|
||||||
|
<SelectItem
|
||||||
|
key={`ws-layout-list-${_id}`}
|
||||||
|
value={_id}
|
||||||
|
className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
||||||
|
<div className="w-full">
|
||||||
|
<Button
|
||||||
|
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (isAddingProjectsAllowed) {
|
||||||
|
handlePopUpOpen("addNewWs");
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add Project
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<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 className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
|
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
|
||||||
{((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu>
|
{(router.asPath.includes("project") || router.asPath.includes("integrations")) &&
|
||||||
<Link href={`/project/${currentWorkspace?._id}/secrets`} passHref>
|
currentWorkspace ? (
|
||||||
<a>
|
<Menu>
|
||||||
<MenuItem
|
<Link href={`/project/${currentWorkspace?._id}/secrets/overview`} passHref>
|
||||||
isSelected={router.asPath.includes(`/project/${currentWorkspace?._id}/secrets`)}
|
<a>
|
||||||
icon="system-outline-90-lock-closed"
|
<MenuItem
|
||||||
>
|
isSelected={router.asPath.includes(
|
||||||
{t("nav.menu.secrets")}
|
`/project/${currentWorkspace?._id}/secrets/overview`
|
||||||
</MenuItem>
|
)}
|
||||||
</a>
|
icon="system-outline-90-lock-closed"
|
||||||
</Link>
|
>
|
||||||
<Link href={`/project/${currentWorkspace?._id}/members`} passHref>
|
{t("nav.menu.secrets")}
|
||||||
<a>
|
</MenuItem>
|
||||||
<MenuItem
|
</a>
|
||||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/members`}
|
</Link>
|
||||||
icon="system-outline-96-groups"
|
<Link href={`/project/${currentWorkspace?._id}/members`} passHref>
|
||||||
>
|
<a>
|
||||||
{t("nav.menu.members")}
|
<MenuItem
|
||||||
</MenuItem>
|
isSelected={
|
||||||
</a>
|
router.asPath === `/project/${currentWorkspace?._id}/members`
|
||||||
</Link>
|
}
|
||||||
<Link href={`/integrations/${currentWorkspace?._id}`} passHref>
|
icon="system-outline-96-groups"
|
||||||
<a>
|
>
|
||||||
<MenuItem
|
{t("nav.menu.members")}
|
||||||
isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`}
|
</MenuItem>
|
||||||
icon="system-outline-82-extension"
|
</a>
|
||||||
>
|
</Link>
|
||||||
{t("nav.menu.integrations")}
|
<Link href={`/integrations/${currentWorkspace?._id}`} passHref>
|
||||||
</MenuItem>
|
<a>
|
||||||
</a>
|
<MenuItem
|
||||||
</Link>
|
isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`}
|
||||||
<Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
|
icon="system-outline-82-extension"
|
||||||
<a>
|
>
|
||||||
<MenuItem
|
{t("nav.menu.integrations")}
|
||||||
isSelected={
|
</MenuItem>
|
||||||
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
|
</a>
|
||||||
}
|
</Link>
|
||||||
icon="system-outline-126-verified"
|
<Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
|
||||||
>
|
<a>
|
||||||
IP Allowlist
|
<MenuItem
|
||||||
</MenuItem>
|
isSelected={
|
||||||
</a>
|
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
|
||||||
</Link>
|
}
|
||||||
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
icon="system-outline-126-verified"
|
||||||
<a>
|
>
|
||||||
<MenuItem
|
IP Allowlist
|
||||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
|
</MenuItem>
|
||||||
icon="system-outline-168-view-headline"
|
</a>
|
||||||
>
|
</Link>
|
||||||
Audit Logs
|
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
||||||
</MenuItem>
|
<a>
|
||||||
</a>
|
<MenuItem
|
||||||
</Link>
|
isSelected={
|
||||||
{/* <Link href={`/project/${currentWorkspace?._id}/secret-scanning`} passHref>
|
router.asPath === `/project/${currentWorkspace?._id}/audit-logs`
|
||||||
|
}
|
||||||
|
icon="system-outline-168-view-headline"
|
||||||
|
>
|
||||||
|
Audit Logs
|
||||||
|
</MenuItem>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
{/* <Link href={`/project/${currentWorkspace?._id}/secret-scanning`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/secret-scanning`}
|
isSelected={router.asPath === `/project/${currentWorkspace?._id}/secret-scanning`}
|
||||||
@@ -438,20 +506,21 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link> */}
|
</Link> */}
|
||||||
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
|
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isSelected={
|
isSelected={
|
||||||
router.asPath === `/project/${currentWorkspace?._id}/settings`
|
router.asPath === `/project/${currentWorkspace?._id}/settings`
|
||||||
}
|
}
|
||||||
icon="system-outline-109-slider-toggle-settings"
|
icon="system-outline-109-slider-toggle-settings"
|
||||||
>
|
>
|
||||||
{t("nav.menu.project-settings")}
|
{t("nav.menu.project-settings")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</Menu>
|
</Menu>
|
||||||
: <Menu className="mt-4">
|
) : (
|
||||||
|
<Menu className="mt-4">
|
||||||
<Link href={`/org/${currentOrg?._id}/overview`} passHref>
|
<Link href={`/org/${currentOrg?._id}/overview`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -462,7 +531,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{/* {workspaces.map(project => <Link key={project._id} href={`/project/${project?._id}/secrets`} passHref>
|
{/* {workspaces.map(project => <Link key={project._id} href={`/project/${project?._id}/secrets/overview`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<SubMenuItem
|
<SubMenuItem
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
@@ -508,20 +577,25 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<Link href={`/org/${currentOrg?._id}/settings`} passHref>
|
<Link href={`/org/${currentOrg?._id}/settings`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isSelected={
|
isSelected={router.asPath === `/org/${currentOrg?._id}/settings`}
|
||||||
router.asPath === `/org/${currentOrg?._id}/settings`
|
|
||||||
}
|
|
||||||
icon="system-outline-109-slider-toggle-settings"
|
icon="system-outline-109-slider-toggle-settings"
|
||||||
>
|
>
|
||||||
Organization Settings
|
Organization Settings
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</Menu>}
|
</Menu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`relative mt-10 ${subscription && subscription.slug === "starter" && !subscription.has_used_trial ? "mb-2" : "mb-4"} w-full px-3 text-mineshaft-400 cursor-default text-sm flex flex-col items-center`}>
|
<div
|
||||||
{/* <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`}/>
|
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-[10.7rem] ${router.asPath.includes("org") ? "bottom-[8.15rem]" : "bottom-[5.15rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-50`}/>
|
||||||
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/>
|
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/>
|
||||||
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[12.3rem] ${router.asPath.includes("org") ? "bottom-[7.65rem]" : "bottom-[4.65rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-90`}/>
|
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[12.3rem] ${router.asPath.includes("org") ? "bottom-[7.65rem]" : "bottom-[4.65rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-90`}/>
|
||||||
@@ -549,22 +623,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
{router.asPath.includes("org") && <div
|
{router.asPath.includes("org") && (
|
||||||
onKeyDown={() => null}
|
<div
|
||||||
role="button"
|
onKeyDown={() => null}
|
||||||
tabIndex={0}
|
role="button"
|
||||||
onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)}
|
tabIndex={0}
|
||||||
className="w-full"
|
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">
|
||||||
Invite people
|
<FontAwesomeIcon icon={faPlus} className="mr-3" />
|
||||||
|
Invite people
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
)}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="hover:text-mineshaft-200 duration-200 mb-2 pl-5 w-full">
|
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||||
<FontAwesomeIcon icon={faQuestion} className="px-[0.1rem] mr-3"/>
|
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||||
Help & Support
|
Help & Support
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -586,28 +662,33 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
{subscription &&
|
||||||
<button
|
subscription.slug === "starter" &&
|
||||||
type="button"
|
!subscription.has_used_trial && (
|
||||||
onClick={async () => {
|
<button
|
||||||
if (!subscription || !currentOrg) return;
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
// direct user to start pro trial
|
if (!subscription || !currentOrg) return;
|
||||||
const url = await mutateAsync({
|
|
||||||
orgId: currentOrg._id,
|
// direct user to start pro trial
|
||||||
success_url: window.location.href
|
const url = await mutateAsync({
|
||||||
});
|
orgId: currentOrg._id,
|
||||||
|
success_url: window.location.href
|
||||||
window.location.href = url;
|
});
|
||||||
}}
|
|
||||||
className="w-full mt-1.5"
|
window.location.href = url;
|
||||||
>
|
}}
|
||||||
<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">
|
className="mt-1.5 w-full"
|
||||||
<FontAwesomeIcon icon={faInfinity} className="mr-3 ml-0.5 py-2 text-primary"/>
|
>
|
||||||
Start Free Pro Trial
|
<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">
|
||||||
</div>
|
<FontAwesomeIcon
|
||||||
</button>
|
icon={faInfinity}
|
||||||
)}
|
className="mr-3 ml-0.5 py-2 text-primary"
|
||||||
|
/>
|
||||||
|
Start Free Pro Trial
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -9,14 +8,31 @@ import { useRouter } from "next/router";
|
|||||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
|
import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faArrowRight, faCheckCircle, faHandPeace, faMagnifyingGlass, faNetworkWired, faPlug, faPlus, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faArrowRight,
|
||||||
|
faCheckCircle,
|
||||||
|
faHandPeace,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faNetworkWired,
|
||||||
|
faPlug,
|
||||||
|
faPlus,
|
||||||
|
faUserPlus
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||||
import { Button, Checkbox, FormControl, Input, Modal, ModalContent, UpgradePlanModal } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
import { TabsObject } from "@app/components/v2/Tabs";
|
import { TabsObject } from "@app/components/v2/Tabs";
|
||||||
import { useSubscription, useUser, useWorkspace } from "@app/context";
|
import { useSubscription, useUser, useWorkspace } from "@app/context";
|
||||||
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api";
|
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api";
|
||||||
@@ -25,11 +41,14 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
|||||||
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
|
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
|
||||||
import registerUserAction from "../../../api/userActions/registerUserAction";
|
import registerUserAction from "../../../api/userActions/registerUserAction";
|
||||||
|
|
||||||
const features = [{
|
const features = [
|
||||||
"_id": 0,
|
{
|
||||||
"name": "Kubernetes Operator",
|
_id: 0,
|
||||||
"description": "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes."
|
name: "Kubernetes Operator",
|
||||||
}]
|
description:
|
||||||
|
"Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
type ItemProps = {
|
type ItemProps = {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -58,7 +77,11 @@ const learningItem = ({
|
|||||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||||
href={link}
|
href={link}
|
||||||
>
|
>
|
||||||
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} mb-3 rounded-md`}>
|
<div
|
||||||
|
className={`${
|
||||||
|
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||||
|
} mb-3 rounded-md`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -70,7 +93,11 @@ const learningItem = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`}
|
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
|
||||||
|
complete
|
||||||
|
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
||||||
|
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
|
||||||
|
} text-mineshaft-100 duration-200`}
|
||||||
>
|
>
|
||||||
<div className="mr-4 flex flex-row items-center">
|
<div className="mr-4 flex flex-row items-center">
|
||||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||||
@@ -148,7 +175,11 @@ const learningItemSquare = ({
|
|||||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||||
href={link}
|
href={link}
|
||||||
>
|
>
|
||||||
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} rounded-md w-full`}>
|
<div
|
||||||
|
className={`${
|
||||||
|
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||||
|
} w-full rounded-md`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -160,23 +191,32 @@ const learningItemSquare = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`}
|
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${
|
||||||
|
complete
|
||||||
|
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
||||||
|
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
|
||||||
|
} text-mineshaft-100 duration-200`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center w-full px-6 py-4">
|
<div className="flex w-full flex-col items-center px-6 py-4">
|
||||||
<div className="flex flex-row items-start justify-between w-full">
|
<div className="flex w-full flex-row items-start justify-between">
|
||||||
<FontAwesomeIcon icon={icon} className="w-16 text-5xl text-mineshaft-200 group-hover:text-mineshaft-100 duration-100 pt-2" />
|
<FontAwesomeIcon
|
||||||
|
icon={icon}
|
||||||
|
className="w-16 pt-2 text-5xl text-mineshaft-200 duration-100 group-hover:text-mineshaft-100"
|
||||||
|
/>
|
||||||
{complete && (
|
{complete && (
|
||||||
<div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
<div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "text-primary font-semibold" : ""}`}
|
className={`text-right text-sm font-normal text-mineshaft-300 ${
|
||||||
|
complete ? "font-semibold text-primary" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{complete ? "Complete!" : `About ${time}`}
|
{complete ? "Complete!" : `About ${time}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start justify-start w-full pt-4">
|
<div className="flex w-full flex-col items-start justify-start pt-4">
|
||||||
<div className="mt-0.5 text-lg font-medium">{text}</div>
|
<div className="mt-0.5 text-lg font-medium">{text}</div>
|
||||||
<div className="text-sm font-normal text-mineshaft-300">{subText}</div>
|
<div className="text-sm font-normal text-mineshaft-300">{subText}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,11 +242,14 @@ export default function Organization() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { workspaces } = useWorkspace();
|
const { workspaces } = useWorkspace();
|
||||||
const orgWorkspaces = workspaces?.filter(workspace => workspace.organization === localStorage.getItem("orgData.id")) || []
|
const orgWorkspaces =
|
||||||
|
workspaces?.filter(
|
||||||
|
(workspace) => workspace.organization === localStorage.getItem("orgData.id")
|
||||||
|
) || [];
|
||||||
const currentOrg = String(router.query.id);
|
const currentOrg = String(router.query.id);
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const addWsUser = useAddUserToWs();
|
const addWsUser = useAddUserToWs();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
@@ -269,7 +312,7 @@ export default function Organization() {
|
|||||||
}
|
}
|
||||||
createNotification({ text: "Workspace created", type: "success" });
|
createNotification({ text: "Workspace created", type: "success" });
|
||||||
handlePopUpClose("addNewWs");
|
handlePopUpClose("addNewWs");
|
||||||
router.push(`/project/${newWorkspaceId}/secrets`);
|
router.push(`/project/${newWorkspaceId}/secrets/overview`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||||
@@ -278,7 +321,9 @@ export default function Organization() {
|
|||||||
|
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true;
|
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||||
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
|
: true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onboardingCheck({
|
onboardingCheck({
|
||||||
@@ -290,16 +335,16 @@ export default function Organization() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-w-7xl mx-auto flex-col justify-start bg-bunker-800 md:h-screen">
|
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||||
<p className="mr-4 font-semibold text-white">Projects</p>
|
<p className="mr-4 font-semibold text-white">Projects</p>
|
||||||
<div className="w-full flex flex-row mt-6">
|
<div className="mt-6 flex w-full flex-row">
|
||||||
<Input
|
<Input
|
||||||
className="h-[2.3rem] text-sm bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
placeholder="Search by project name..."
|
placeholder="Search by project name..."
|
||||||
value={searchFilter}
|
value={searchFilter}
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
@@ -310,7 +355,7 @@ export default function Organization() {
|
|||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isAddingProjectsAllowed) {
|
if (isAddingProjectsAllowed) {
|
||||||
handlePopUpOpen("addNewWs")
|
handlePopUpOpen("addNewWs");
|
||||||
} else {
|
} else {
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}
|
}
|
||||||
@@ -320,120 +365,169 @@ export default function Organization() {
|
|||||||
Add New Project
|
Add New Project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 w-full grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
{orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
{orgWorkspaces
|
||||||
<div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div>
|
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||||
<div className="text-sm text-mineshaft-300 mt-0 pb-6">{(workspace.environments?.length || 0)} environments</div>
|
.map((workspace) => (
|
||||||
<button type="button" onClick={() => {
|
<div
|
||||||
router.push(`/project/${workspace._id}/secrets`);
|
key={workspace._id}
|
||||||
localStorage.setItem("projectData.id", 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="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="mt-0 text-lg text-mineshaft-100">{workspace.name}</div>
|
||||||
</button>
|
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||||
</div>)}
|
{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 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 && (
|
{orgWorkspaces.length === 0 && (
|
||||||
<div className="w-full rounded-md bg-mineshaft-800 border border-mineshaft-700 px-4 py-6 text-mineshaft-300 text-base">
|
<div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||||
<FontAwesomeIcon icon={faFolderOpen} className="w-full text-center text-5xl mb-4 mt-2 text-mineshaft-400" />
|
<FontAwesomeIcon
|
||||||
|
icon={faFolderOpen}
|
||||||
|
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
|
||||||
|
/>
|
||||||
<div className="text-center font-light">
|
<div className="text-center font-light">
|
||||||
You are not part of any projects in this organization yet. When you are, they will appear
|
You are not part of any projects in this organization yet. When you are, they will
|
||||||
here.
|
appear here.
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 text-center font-light">
|
<div className="mt-0.5 text-center font-light">
|
||||||
Create a new project, or ask other organization members to give you necessary permissions.
|
Create a new project, or ask other organization members to give you necessary
|
||||||
|
permissions.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{((new Date()).getTime() - (new Date(user?.createdAt)).getTime()) < 30 * 24 * 60 * 60 * 1000 && <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
{new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && (
|
||||||
<p className="mr-4 font-semibold text-white mb-4">Onboarding Guide</p>
|
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 w-full mb-3">
|
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
||||||
{learningItemSquare({
|
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
text: "Watch Infisical demo",
|
{learningItemSquare({
|
||||||
subText: "Set up Infisical in 3 min.",
|
text: "Watch Infisical demo",
|
||||||
complete: hasUserClickedIntro,
|
subText: "Set up Infisical in 3 min.",
|
||||||
icon: faHandPeace,
|
complete: hasUserClickedIntro,
|
||||||
time: "3 min",
|
icon: faHandPeace,
|
||||||
userAction: "intro_cta_clicked",
|
time: "3 min",
|
||||||
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
userAction: "intro_cta_clicked",
|
||||||
})}
|
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
||||||
{orgWorkspaces.length !== 0 && learningItemSquare({
|
})}
|
||||||
text: "Add your secrets",
|
{orgWorkspaces.length !== 0 &&
|
||||||
subText: "Drop a .env file or type your secrets.",
|
learningItemSquare({
|
||||||
complete: hasUserPushedSecrets,
|
text: "Add your secrets",
|
||||||
icon: faPlus,
|
subText: "Drop a .env file or type your secrets.",
|
||||||
time: "1 min",
|
complete: hasUserPushedSecrets,
|
||||||
userAction: "first_time_secrets_pushed",
|
icon: faPlus,
|
||||||
link: `/project/${orgWorkspaces[0]?._id}/secrets`
|
time: "1 min",
|
||||||
})}
|
userAction: "first_time_secrets_pushed",
|
||||||
{learningItemSquare({
|
link: `/project/${orgWorkspaces[0]?._id}/secrets`
|
||||||
text: "Invite your teammates",
|
})}
|
||||||
subText: "Infisical is better used as a team.",
|
{learningItemSquare({
|
||||||
complete: usersInOrg,
|
text: "Invite your teammates",
|
||||||
icon: faUserPlus,
|
subText: "Infisical is better used as a team.",
|
||||||
time: "2 min",
|
complete: usersInOrg,
|
||||||
link: `/org/${router.query.id}/members?action=invite`
|
icon: faUserPlus,
|
||||||
})}
|
time: "2 min",
|
||||||
<div className="block xl:hidden 2xl:block">{learningItemSquare({
|
link: `/org/${router.query.id}/members?action=invite`
|
||||||
text: "Join Infisical Slack",
|
})}
|
||||||
subText: "Have any questions? Ask us!",
|
<div className="block xl:hidden 2xl:block">
|
||||||
complete: hasUserClickedSlack,
|
{learningItemSquare({
|
||||||
icon: faSlack,
|
text: "Join Infisical Slack",
|
||||||
time: "1 min",
|
subText: "Have any questions? Ask us!",
|
||||||
userAction: "slack_cta_clicked",
|
complete: hasUserClickedSlack,
|
||||||
link: "https://infisical.com/slack"
|
icon: faSlack,
|
||||||
})}</div>
|
time: "1 min",
|
||||||
</div>
|
userAction: "slack_cta_clicked",
|
||||||
{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">
|
link: "https://infisical.com/slack"
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col items-start pl-0.5">
|
|
||||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
|
||||||
<div className="text-sm font-normal">
|
|
||||||
Replace .env files with a more secure and efficient alternative.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}>
|
|
||||||
About 2 min
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TabsObject />
|
{orgWorkspaces.length !== 0 && (
|
||||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
<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>}
|
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||||
{orgWorkspaces.length !== 0 && learningItem({
|
<div className="mr-4 flex w-full flex-row items-center">
|
||||||
text: "Integrate Infisical with your infrastructure",
|
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||||
subText: "Connect Infisical to various 3rd party services and platforms.",
|
{false && (
|
||||||
complete: 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">
|
||||||
icon: faPlug,
|
<FontAwesomeIcon
|
||||||
time: "15 min",
|
icon={faCheckCircle}
|
||||||
link: "https://infisical.com/docs/integrations/overview"
|
className="h-5 w-5 text-4xl text-green"
|
||||||
})}
|
/>
|
||||||
</div>}
|
</div>
|
||||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4 pb-6">
|
)}
|
||||||
<p className="mr-4 font-semibold text-white">Explore More</p>
|
<div className="flex flex-col items-start pl-0.5">
|
||||||
<div className="mt-4 w-full grid grid-flow-dense gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 4fr))" }}>
|
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||||
{features.map(feature => <div key={feature._id} className="h-44 w-96 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
<div className="text-sm font-normal">
|
||||||
<div className="text-lg text-mineshaft-100 mt-0">{feature.name}</div>
|
Replace .env files with a more secure and efficient alternative.
|
||||||
<div className="text-[15px] font-light text-mineshaft-300 mb-4 mt-2">{feature.description}</div>
|
</div>
|
||||||
<div className="w-full flex items-center">
|
</div>
|
||||||
<div className="text-mineshaft-300 text-[15px] font-light">Setup time: 20 min</div>
|
</div>
|
||||||
<a
|
<div
|
||||||
target="_blank"
|
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}
|
||||||
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"
|
About 2 min
|
||||||
href="https://infisical.com/docs/documentation/getting-started/kubernetes"
|
</div>
|
||||||
>
|
</div>
|
||||||
Learn more <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200"/>
|
<TabsObject />
|
||||||
</a>
|
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||||
</div>
|
</div>
|
||||||
</div>)}
|
)}
|
||||||
|
{orgWorkspaces.length !== 0 &&
|
||||||
|
learningItem({
|
||||||
|
text: "Integrate Infisical with your infrastructure",
|
||||||
|
subText: "Connect Infisical to various 3rd party services and platforms.",
|
||||||
|
complete: false,
|
||||||
|
icon: faPlug,
|
||||||
|
time: "15 min",
|
||||||
|
link: "https://infisical.com/docs/integrations/overview"
|
||||||
|
})}
|
||||||
|
</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 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 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 duration-200 group-hover:pl-2 group-hover:pr-0"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
+1
-7
@@ -1,16 +1,10 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { DashboardPage } from "@app/views/DashboardPage";
|
import { DashboardPage } from "@app/views/DashboardPage";
|
||||||
import { DashboardEnvOverview } from "@app/views/DashboardPage/DashboardEnvOverview";
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const queryEnv = router.query.env as string;
|
|
||||||
const isOverviewMode = !queryEnv;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -22,7 +16,7 @@ const Dashboard = () => {
|
|||||||
<meta name="og:description" content={String(t("dashboard.og-description"))} />
|
<meta name="og:description" content={String(t("dashboard.og-description"))} />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{isOverviewMode ? <DashboardEnvOverview /> : <DashboardPage envFromTop={queryEnv} />}
|
<DashboardPage />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
+3
-3
@@ -53,7 +53,7 @@ export const OrgIncidentContactsTable = ({
|
|||||||
isLoading
|
isLoading
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [searchContact, setSearchContact] = useState("");
|
const [searchContact, setSearchContact] = useState("");
|
||||||
const {data: serverDetails } = useFetchServerStatus()
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"addContact",
|
"addContact",
|
||||||
"removeContact",
|
"removeContact",
|
||||||
@@ -98,7 +98,7 @@ export const OrgIncidentContactsTable = ({
|
|||||||
<Button
|
<Button
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (serverDetails?.emailConfigured){
|
if (serverDetails?.emailConfigured) {
|
||||||
handlePopUpOpen("addContact");
|
handlePopUpOpen("addContact");
|
||||||
} else {
|
} else {
|
||||||
handlePopUpOpen("setUpEmail");
|
handlePopUpOpen("setUpEmail");
|
||||||
@@ -119,7 +119,7 @@ export const OrgIncidentContactsTable = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
|
{isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
|
||||||
{filteredContacts?.map(({ email }) => (
|
{filteredContacts?.map(({ email }) => (
|
||||||
<Tr key={email}>
|
<Tr key={email}>
|
||||||
<Td className="w-full">{email}</Td>
|
<Td className="w-full">{email}</Td>
|
||||||
|
|||||||
+120
-85
@@ -1,7 +1,14 @@
|
|||||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
|
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faCheck, faCopy, faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCheck,
|
||||||
|
faCopy,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faTrash,
|
||||||
|
faUsers
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -10,7 +17,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmailServiceSetupModal, EmptyState,
|
EmailServiceSetupModal,
|
||||||
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
@@ -29,7 +37,7 @@ import {
|
|||||||
Tr,
|
Tr,
|
||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization , useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useGetSSOConfig } from "@app/hooks/api";
|
import { useGetSSOConfig } from "@app/hooks/api";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
@@ -47,8 +55,8 @@ type Props = {
|
|||||||
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
|
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
|
||||||
// the current user id to block remove org button
|
// the current user id to block remove org button
|
||||||
userId: string;
|
userId: string;
|
||||||
completeInviteLink: string | undefined,
|
completeInviteLink: string | undefined;
|
||||||
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>
|
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addMemberFormSchema = yup.object({
|
const addMemberFormSchema = yup.object({
|
||||||
@@ -76,7 +84,7 @@ export const OrgMembersTable = ({
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
|
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
|
||||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||||
const {data: serverDetails } = useFetchServerStatus()
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
const { workspaces } = useWorkspace();
|
const { workspaces } = useWorkspace();
|
||||||
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
@@ -85,7 +93,7 @@ export const OrgMembersTable = ({
|
|||||||
"upgradePlan",
|
"upgradePlan",
|
||||||
"setUpEmail"
|
"setUpEmail"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.query.action === "invite") {
|
if (router.query.action === "invite") {
|
||||||
handlePopUpOpen("addMember");
|
handlePopUpOpen("addMember");
|
||||||
@@ -101,11 +109,11 @@ export const OrgMembersTable = ({
|
|||||||
|
|
||||||
const onAddMember = async ({ email }: TAddMemberForm) => {
|
const onAddMember = async ({ email }: TAddMemberForm) => {
|
||||||
await onInviteMember(email);
|
await onInviteMember(email);
|
||||||
if (serverDetails?.emailConfigured){
|
if (serverDetails?.emailConfigured) {
|
||||||
handlePopUpClose("addMember");
|
handlePopUpClose("addMember");
|
||||||
}
|
}
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemoveOrgMemberApproved = async () => {
|
const onRemoveOrgMemberApproved = async () => {
|
||||||
@@ -118,7 +126,7 @@ export const OrgMembersTable = ({
|
|||||||
() => members.find(({ user }) => userId === user?._id)?.role === "owner",
|
() => members.find(({ user }) => userId === user?._id)?.role === "owner",
|
||||||
[userId, members]
|
[userId, members]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterdUser = useMemo(
|
const filterdUser = useMemo(
|
||||||
() =>
|
() =>
|
||||||
members.filter(
|
members.filter(
|
||||||
@@ -163,10 +171,10 @@ export const OrgMembersTable = ({
|
|||||||
text: "You cannot invite users when SAML SSO is configured for your organization",
|
text: "You cannot invite users when SAML SSO is configured for your organization",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMoreUserNotAllowed) {
|
if (isMoreUserNotAllowed) {
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
} else {
|
} else {
|
||||||
@@ -190,7 +198,7 @@ export const OrgMembersTable = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={5} key="org-members" />}
|
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
||||||
const name = user ? `${user.firstName} ${user.lastName}` : "-";
|
const name = user ? `${user.firstName} ${user.lastName}` : "-";
|
||||||
@@ -219,11 +227,17 @@ export const OrgMembersTable = ({
|
|||||||
<SelectItem value="member">member</SelectItem>
|
<SelectItem value="member">member</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && (
|
{(status === "invited" || status === "verified") &&
|
||||||
<Button className='w-40' colorSchema="primary" variant="outline_bg" onClick={() => onInviteMember(email)}>
|
serverDetails?.emailConfigured && (
|
||||||
Resend Invite
|
<Button
|
||||||
</Button>
|
className="w-40"
|
||||||
)}
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => onInviteMember(email)}
|
||||||
|
>
|
||||||
|
Resend Invite
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{status === "completed" && (
|
{status === "completed" && (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
@@ -241,18 +255,33 @@ export const OrgMembersTable = ({
|
|||||||
</Tag>
|
</Tag>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className='flex flex-row'>
|
<div className="flex flex-row">
|
||||||
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured)
|
{(status === "invited" || status === "verified") &&
|
||||||
? <Tag colorSchema="red">This user hasn't accepted the invite yet</Tag>
|
serverDetails?.emailConfigured ? (
|
||||||
: <Tag colorSchema="red">This user isn't part of any projects yet</Tag>}
|
<Tag colorSchema="red">
|
||||||
{router.query.id !== "undefined" && !((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && <button
|
This user hasn't accepted the invite yet
|
||||||
type="button"
|
</Tag>
|
||||||
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'
|
<Tag colorSchema="red">
|
||||||
>
|
This user isn't part of any projects yet
|
||||||
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
</Tag>
|
||||||
Add to projects
|
)}
|
||||||
</button>}
|
{router.query.id !== "undefined" &&
|
||||||
|
!(
|
||||||
|
(status === "invited" || status === "verified") &&
|
||||||
|
serverDetails?.emailConfigured
|
||||||
|
) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
@@ -282,67 +311,73 @@ export const OrgMembersTable = ({
|
|||||||
isOpen={popUp?.addMember?.isOpen}
|
isOpen={popUp?.addMember?.isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("addMember", isOpen);
|
handlePopUpToggle("addMember", isOpen);
|
||||||
setCompleteInviteLink(undefined)
|
setCompleteInviteLink(undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Invite others to ${orgName}`}
|
title={`Invite others to ${orgName}`}
|
||||||
subTitle={
|
subTitle={
|
||||||
<div>
|
<div>
|
||||||
{!completeInviteLink && <div>
|
{!completeInviteLink && (
|
||||||
An invite is specific to an email address and expires after 1 day.
|
<div>
|
||||||
<br />
|
An invite is specific to an email address and expires after 1 day.
|
||||||
For security reasons, you will need to separately add members to projects.
|
<br />
|
||||||
</div>}
|
For security reasons, you will need to separately add members to projects.
|
||||||
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
|
</div>
|
||||||
|
)}
|
||||||
|
{completeInviteLink &&
|
||||||
|
"This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!completeInviteLink && <form onSubmit={handleSubmit(onAddMember)} >
|
{!completeInviteLink && (
|
||||||
<Controller
|
<form onSubmit={handleSubmit(onAddMember)}>
|
||||||
control={control}
|
<Controller
|
||||||
defaultValue=""
|
control={control}
|
||||||
name="email"
|
defaultValue=""
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="email"
|
||||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
render={({ field, fieldState: { error } }) => (
|
||||||
<Input {...field} />
|
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||||
</FormControl>
|
<Input {...field} />
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
<div className="mt-8 flex items-center">
|
/>
|
||||||
<Button
|
<div className="mt-8 flex items-center">
|
||||||
className="mr-4"
|
<Button
|
||||||
size="sm"
|
className="mr-4"
|
||||||
type="submit"
|
size="sm"
|
||||||
isLoading={isSubmitting}
|
type="submit"
|
||||||
isDisabled={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
>
|
isDisabled={isSubmitting}
|
||||||
Add Member
|
>
|
||||||
</Button>
|
Add Member
|
||||||
<Button
|
</Button>
|
||||||
colorSchema="secondary"
|
<Button
|
||||||
variant="plain"
|
colorSchema="secondary"
|
||||||
onClick={() => handlePopUpClose("addMember")}
|
variant="plain"
|
||||||
>
|
onClick={() => handlePopUpClose("addMember")}
|
||||||
Cancel
|
>
|
||||||
</Button>
|
Cancel
|
||||||
</div>
|
</Button>
|
||||||
</form>}
|
</div>
|
||||||
{
|
</form>
|
||||||
completeInviteLink &&
|
)}
|
||||||
|
{completeInviteLink && (
|
||||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
<p className="mr-4 break-all">{completeInviteLink}</p>
|
<p className="mr-4 break-all">{completeInviteLink}</p>
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="copy icon"
|
ariaLabel="copy icon"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
className="group relative"
|
className="group relative"
|
||||||
onClick={copyTokenToClipboard}
|
onClick={copyTokenToClipboard}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
|
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
</IconButton>
|
click to copy
|
||||||
</div>
|
</span>
|
||||||
}
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
@@ -363,4 +398,4 @@ export const OrgMembersTable = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+313
-313
@@ -1,14 +1,15 @@
|
|||||||
import { useEffect, useMemo,useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Controller,useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
faCopy,
|
faCopy,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faPencil,
|
faPencil,
|
||||||
faPlus,
|
faPlus,
|
||||||
faServer,
|
faServer,
|
||||||
faTrash} from "@fortawesome/free-solid-svg-icons";
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -37,9 +38,10 @@ import {
|
|||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateServiceAccount,
|
useCreateServiceAccount,
|
||||||
useDeleteServiceAccount,
|
useDeleteServiceAccount,
|
||||||
useGetServiceAccounts} from "@app/hooks/api";
|
useGetServiceAccounts
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
const serviceAccountExpiration = [
|
const serviceAccountExpiration = [
|
||||||
{ label: "1 Day", value: 86400 },
|
{ label: "1 Day", value: 86400 },
|
||||||
@@ -51,317 +53,315 @@ const serviceAccountExpiration = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const addServiceAccountFormSchema = yup.object({
|
const addServiceAccountFormSchema = yup.object({
|
||||||
name: yup.string().required().label("Name").trim(),
|
name: yup.string().required().label("Name").trim(),
|
||||||
expiresIn: yup.string().required().label("Service Account Expiration")
|
expiresIn: yup.string().required().label("Service Account Expiration")
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
||||||
|
|
||||||
export const OrgServiceAccountsTable = () => {
|
export const OrgServiceAccountsTable = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const orgId = currentOrg?._id || "";
|
|
||||||
const [step, setStep] = useState(0);
|
|
||||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
|
||||||
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
|
||||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
|
||||||
const [accessKey, setAccessKey] = useState("");
|
|
||||||
const [publicKey, setPublicKey] = useState("");
|
|
||||||
const [privateKey, setPrivateKey] = useState("");
|
|
||||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
|
||||||
"addServiceAccount",
|
|
||||||
"removeServiceAccount",
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
|
const orgId = currentOrg?._id || "";
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
const createServiceAccount = useCreateServiceAccount();
|
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||||
const removeServiceAccount = useDeleteServiceAccount();
|
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
||||||
|
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||||
useEffect(() => {
|
const [accessKey, setAccessKey] = useState("");
|
||||||
let timer: NodeJS.Timeout;
|
const [publicKey, setPublicKey] = useState("");
|
||||||
if (isAccessKeyCopied) {
|
const [privateKey, setPrivateKey] = useState("");
|
||||||
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
||||||
}
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"addServiceAccount",
|
||||||
|
"removeServiceAccount"
|
||||||
|
] as const);
|
||||||
|
|
||||||
if (isPublicKeyCopied) {
|
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
|
||||||
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
useGetServiceAccounts(orgId);
|
||||||
}
|
|
||||||
|
|
||||||
if (isPrivateKeyCopied) {
|
const createServiceAccount = useCreateServiceAccount();
|
||||||
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
const removeServiceAccount = useDeleteServiceAccount();
|
||||||
}
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
useEffect(() => {
|
||||||
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isAccessKeyCopied) {
|
||||||
const {
|
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
|
||||||
|
|
||||||
const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
const keyPair = generateKeyPair();
|
|
||||||
setPublicKey(keyPair.publicKey);
|
|
||||||
setPrivateKey(keyPair.privateKey);
|
|
||||||
|
|
||||||
const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
|
||||||
name,
|
|
||||||
organizationId: currentOrg?._id,
|
|
||||||
publicKey: keyPair.publicKey,
|
|
||||||
expiresIn: Number(expiresIn)
|
|
||||||
});
|
|
||||||
|
|
||||||
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
|
||||||
|
|
||||||
setStep(1);
|
|
||||||
reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRemoveServiceAccount = async () => {
|
if (isPublicKeyCopied) {
|
||||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
||||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
|
||||||
handlePopUpClose("removeServiceAccount");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredServiceAccounts = useMemo(
|
if (isPrivateKeyCopied) {
|
||||||
() =>
|
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
||||||
serviceAccounts.filter(
|
|
||||||
({ name }) =>
|
|
||||||
name.toLowerCase().includes(searchServiceAccountFilter)
|
|
||||||
),
|
|
||||||
[serviceAccounts, searchServiceAccountFilter]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderStep = (stepToRender: number) => {
|
|
||||||
switch (stepToRender) {
|
|
||||||
case 0:
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onAddServiceAccount)}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="name"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="expiresIn"
|
|
||||||
defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
label="Expiration"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{serviceAccountExpiration.map(({ label, value }) => (
|
|
||||||
<SelectItem value={String(value)} key={label}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Create Service Account
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpClose("addServiceAccount")}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
case 1:
|
|
||||||
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]">
|
|
||||||
<p className="mr-4 break-all">{accessKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(accessKey);
|
|
||||||
setIsAccessKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isAccessKeyCopied ? 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">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</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]">
|
|
||||||
<p className="mr-4 break-all">{publicKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(publicKey);
|
|
||||||
setIsPublicKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</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]">
|
|
||||||
<p className="mr-4 break-all">{privateKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(privateKey);
|
|
||||||
setIsPrivateKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <div />
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return () => clearTimeout(timer);
|
||||||
<div className="w-full">
|
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
||||||
<div className="mb-4 flex">
|
|
||||||
<div className="mr-4 flex-1">
|
const {
|
||||||
<Input
|
control,
|
||||||
value={searchServiceAccountFilter}
|
handleSubmit,
|
||||||
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
reset,
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
formState: { isSubmitting }
|
||||||
placeholder="Search service accounts..."
|
} = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||||
/>
|
|
||||||
</div>
|
const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||||
<Button
|
if (!currentOrg?._id) return;
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
const keyPair = generateKeyPair();
|
||||||
setStep(0);
|
setPublicKey(keyPair.publicKey);
|
||||||
reset();
|
setPrivateKey(keyPair.privateKey);
|
||||||
handlePopUpOpen("addServiceAccount");
|
|
||||||
}}
|
const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||||
>
|
name,
|
||||||
Add Service Account
|
organizationId: currentOrg?._id,
|
||||||
</Button>
|
publicKey: keyPair.publicKey,
|
||||||
</div>
|
expiresIn: Number(expiresIn)
|
||||||
<TableContainer>
|
});
|
||||||
<Table>
|
|
||||||
<THead>
|
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||||
<Th>Name</Th>
|
|
||||||
<Th className="w-full">Valid Until</Th>
|
setStep(1);
|
||||||
<Th aria-label="actions" />
|
reset();
|
||||||
</THead>
|
};
|
||||||
<TBody>
|
|
||||||
{isServiceAccountsLoading && <TableSkeleton columns={5} key="org-service-accounts" />}
|
const onRemoveServiceAccount = async () => {
|
||||||
{!isServiceAccountsLoading && (
|
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||||
filteredServiceAccounts.map(({
|
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||||
name,
|
handlePopUpClose("removeServiceAccount");
|
||||||
expiresAt,
|
};
|
||||||
_id: serviceAccountId
|
|
||||||
}) => {
|
const filteredServiceAccounts = useMemo(
|
||||||
return (
|
() =>
|
||||||
<Tr key={`org-service-account-${serviceAccountId}`}>
|
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
|
||||||
<Td>{name}</Td>
|
[serviceAccounts, searchServiceAccountFilter]
|
||||||
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
);
|
||||||
<Td>
|
|
||||||
<div className="flex">
|
const renderStep = (stepToRender: number) => {
|
||||||
<IconButton
|
switch (stepToRender) {
|
||||||
ariaLabel="edit"
|
case 0:
|
||||||
colorSchema="secondary"
|
return (
|
||||||
onClick={() => {
|
<form onSubmit={handleSubmit(onAddServiceAccount)}>
|
||||||
if (currentWorkspace?._id) {
|
<Controller
|
||||||
router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`);
|
control={control}
|
||||||
}
|
defaultValue=""
|
||||||
}}
|
name="name"
|
||||||
className="mr-2"
|
render={({ field, fieldState: { error } }) => (
|
||||||
>
|
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
<Input {...field} />
|
||||||
</IconButton>
|
</FormControl>
|
||||||
<IconButton
|
)}
|
||||||
ariaLabel="delete"
|
|
||||||
colorSchema="danger"
|
|
||||||
onClick={() => handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
|
||||||
<EmptyState title="No service accounts found" icon={faServer} />
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
<Modal
|
|
||||||
isOpen={popUp?.addServiceAccount?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
handlePopUpToggle("addServiceAccount", isOpen);
|
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title="Add Service Account"
|
|
||||||
subTitle="A service account represents a machine identity such as a VM or application client."
|
|
||||||
>
|
|
||||||
{renderStep(step)}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.removeServiceAccount.isOpen}
|
|
||||||
deleteKey="remove"
|
|
||||||
title="Do you want to remove this service account from the org?"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
|
||||||
onDeleteApproved={onRemoveServiceAccount}
|
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresIn"
|
||||||
|
defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Expiration"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{serviceAccountExpiration.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value)} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Create Service Account
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpClose("addServiceAccount")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>Access Key</p>
|
||||||
|
<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"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(accessKey);
|
||||||
|
setIsAccessKeyCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isAccessKeyCopied ? 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">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4">Public Key</p>
|
||||||
|
<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"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(publicKey);
|
||||||
|
setIsPublicKeyCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4">Private Key</p>
|
||||||
|
<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"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(privateKey);
|
||||||
|
setIsPrivateKeyCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex">
|
||||||
|
<div className="mr-4 flex-1">
|
||||||
|
<Input
|
||||||
|
value={searchServiceAccountFilter}
|
||||||
|
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search service accounts..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
<Button
|
||||||
}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
setStep(0);
|
||||||
|
reset();
|
||||||
|
handlePopUpOpen("addServiceAccount");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Service Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th className="w-full">Valid Until</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{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>
|
||||||
|
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="edit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
if (currentWorkspace?._id) {
|
||||||
|
router.push(
|
||||||
|
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mr-2"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
||||||
|
<EmptyState title="No service accounts found" icon={faServer} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addServiceAccount?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addServiceAccount", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add Service Account"
|
||||||
|
subTitle="A service account represents a machine identity such as a VM or application client."
|
||||||
|
>
|
||||||
|
{renderStep(step)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeServiceAccount.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this service account from the org?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
||||||
|
onDeleteApproved={onRemoveServiceAccount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,90 +10,94 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr} from "@app/components/v2";
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
import timeSince from "@app/ee/utilities/timeSince";
|
import timeSince from "@app/ee/utilities/timeSince";
|
||||||
import getRisksByOrganization, { GitRisks } from "@app/pages/api/secret-scanning/getRisksByOrganization";
|
import getRisksByOrganization, {
|
||||||
|
GitRisks
|
||||||
|
} from "@app/pages/api/secret-scanning/getRisksByOrganization";
|
||||||
|
|
||||||
import { RiskStatusSelection } from "./RiskStatusSelection";
|
import { RiskStatusSelection } from "./RiskStatusSelection";
|
||||||
|
|
||||||
export const SecretScanningLogsTable = () => {
|
export const SecretScanningLogsTable = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRisks = async () => {
|
const fetchRisks = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
|
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")));
|
||||||
setGitRisks(risks);
|
setGitRisks(risks);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
fetchRisks();
|
fetchRisks();
|
||||||
},[])
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
return (
|
<TableContainer className="mt-8">
|
||||||
<TableContainer className="mt-8">
|
<Table>
|
||||||
<Table>
|
<THead>
|
||||||
<THead>
|
<Tr>
|
||||||
<Tr>
|
<Th className="flex-1">Date</Th>
|
||||||
<Th className="flex-1">Date</Th>
|
<Th className="flex-1">Secret Type</Th>
|
||||||
<Th className="flex-1">Secret Type</Th>
|
<Th className="flex-1">View Risk</Th>
|
||||||
<Th className="flex-1">View Risk</Th>
|
<Th className="flex-1">Info</Th>
|
||||||
<Th className="flex-1">Info</Th>
|
<Th className="flex-1">Status</Th>
|
||||||
<Th className="flex-1">Status</Th>
|
<Th className="flex-1">Action</Th>
|
||||||
<Th className="flex-1">Action</Th>
|
<Th className="w-5" />
|
||||||
<Th className="w-5" />
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{!isLoading &&
|
||||||
|
gitRisks &&
|
||||||
|
gitRisks?.map((risk) => {
|
||||||
|
return (
|
||||||
|
<Tr key={risk.ruleID} className="h-10">
|
||||||
|
<Td>{timeSince(new Date(risk.createdAt))}</Td>
|
||||||
|
<Td>{risk.ruleID}</Td>
|
||||||
|
<Td>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
View Exposed Secret
|
||||||
|
</a>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="font-bold">
|
||||||
|
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
||||||
|
{risk.repositoryFullName}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
<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} />
|
||||||
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
);
|
||||||
<TBody>
|
})}
|
||||||
{!isLoading && gitRisks && gitRisks?.map((risk) => {
|
{isLoading && <TableSkeleton columns={7} innerKey="gitRisks" />}
|
||||||
return (
|
{!isLoading && gitRisks && gitRisks?.length === 0 && (
|
||||||
<Tr key={risk.ruleID} className="h-10">
|
<Tr>
|
||||||
<Td>{timeSince(new Date(risk.createdAt))}</Td>
|
<Td colSpan={7}>
|
||||||
<Td>{risk.ruleID}</Td>
|
<EmptyState title="No risks detected." icon={faCheck} />
|
||||||
<Td>
|
</Td>
|
||||||
<a
|
</Tr>
|
||||||
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
)}
|
||||||
target="_blank"
|
</TBody>
|
||||||
className="text-red-500" rel="noreferrer"
|
</Table>
|
||||||
>
|
</TableContainer>
|
||||||
View Exposed Secret
|
);
|
||||||
</a>
|
};
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<div className="font-bold">
|
|
||||||
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
|
||||||
{risk.repositoryFullName}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs">
|
|
||||||
<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}/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isLoading && <TableSkeleton columns={7} key="gitRisks" />}
|
|
||||||
{!isLoading && gitRisks && gitRisks?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={7}>
|
|
||||||
<EmptyState
|
|
||||||
title="No risks detected."
|
|
||||||
icon={faCheck}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+55
-70
@@ -1,4 +1,4 @@
|
|||||||
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -10,78 +10,63 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr} from "@app/components/v2";
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useGetOrgPlanTable } from "@app/hooks/api";
|
||||||
useGetOrgPlanTable
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const CurrentPlanSection = () => {
|
export const CurrentPlanSection = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? "");
|
const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? "");
|
||||||
|
|
||||||
const displayCell = (value: null | number | string | boolean) => {
|
|
||||||
if (value === null) return "-";
|
|
||||||
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
if (value) return (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCircleCheck}
|
|
||||||
color='#2ecc71'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const displayCell = (value: null | number | string | boolean) => {
|
||||||
<FontAwesomeIcon
|
if (value === null) return "-";
|
||||||
icon={faCircleXmark}
|
|
||||||
color='#e74c3c'
|
if (typeof value === "boolean") {
|
||||||
/>
|
if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
|
||||||
);
|
|
||||||
}
|
return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return value;
|
||||||
<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>
|
|
||||||
<TableContainer className="mt-4">
|
return (
|
||||||
<Table>
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<THead>
|
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Current Usage</h2>
|
||||||
<Tr>
|
<TableContainer className="mt-4">
|
||||||
<Th className="w-1/3">Feature</Th>
|
<Table>
|
||||||
<Th className="w-1/3">Allowed</Th>
|
<THead>
|
||||||
<Th className="w-1/3">Used</Th>
|
<Tr>
|
||||||
</Tr>
|
<Th className="w-1/3">Feature</Th>
|
||||||
</THead>
|
<Th className="w-1/3">Allowed</Th>
|
||||||
<TBody>
|
<Th className="w-1/3">Used</Th>
|
||||||
{!isLoading && data && data?.rows?.length > 0 && data.rows.map(({
|
</Tr>
|
||||||
name,
|
</THead>
|
||||||
allowed,
|
<TBody>
|
||||||
used
|
{!isLoading &&
|
||||||
}) => {
|
data &&
|
||||||
return (
|
data?.rows?.length > 0 &&
|
||||||
<Tr key={`current-plan-row-${name}`} className="h-12">
|
data.rows.map(({ name, allowed, used }) => {
|
||||||
<Td>{name}</Td>
|
return (
|
||||||
<Td>{displayCell(allowed)}</Td>
|
<Tr key={`current-plan-row-${name}`} className="h-12">
|
||||||
<Td>{used}</Td>
|
<Td>{name}</Td>
|
||||||
</Tr>
|
<Td>{displayCell(allowed)}</Td>
|
||||||
);
|
<Td>{used}</Td>
|
||||||
})}
|
</Tr>
|
||||||
{isLoading && <TableSkeleton columns={5} key="invoices" />}
|
);
|
||||||
{!isLoading && data && data?.rows?.length === 0 && (
|
})}
|
||||||
<Tr>
|
{isLoading && <TableSkeleton columns={5} innerKey="invoices" />}
|
||||||
<Td colSpan={3}>
|
{!isLoading && data && data?.rows?.length === 0 && (
|
||||||
<EmptyState
|
<Tr>
|
||||||
title="No plan details found"
|
<Td colSpan={3}>
|
||||||
icon={faFileInvoice}
|
<EmptyState title="No plan details found" icon={faFileInvoice} />
|
||||||
/>
|
</Td>
|
||||||
</Td>
|
</Tr>
|
||||||
</Tr>
|
)}
|
||||||
)}
|
</TBody>
|
||||||
</TBody>
|
</Table>
|
||||||
</Table>
|
</TableContainer>
|
||||||
</TableContainer>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|||||||
+117
-154
@@ -1,4 +1,4 @@
|
|||||||
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,166 +11,129 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization,useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import { useCreateCustomerPortalSession, useGetOrgPlansTable } from "@app/hooks/api";
|
||||||
useCreateCustomerPortalSession,
|
|
||||||
useGetOrgPlansTable} from "@app/hooks/api";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
billingCycle: "monthly" | "yearly"
|
billingCycle: "monthly" | "yearly";
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ManagePlansTable = ({
|
export const ManagePlansTable = ({ billingCycle }: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
|
||||||
|
organizationId: currentOrg?._id ?? "",
|
||||||
billingCycle
|
billingCycle
|
||||||
}: Props) => {
|
});
|
||||||
const { currentOrg } = useOrganization();
|
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||||
const { subscription } = useSubscription();
|
|
||||||
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
|
|
||||||
organizationId: currentOrg?._id ?? "",
|
|
||||||
billingCycle
|
|
||||||
});
|
|
||||||
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
|
||||||
|
|
||||||
const displayCell = (value: null | number | string | boolean) => {
|
const displayCell = (value: null | number | string | boolean) => {
|
||||||
if (value === null) return "Unlimited";
|
if (value === null) return "Unlimited";
|
||||||
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
if (value) return (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCircleCheck}
|
|
||||||
color='#2ecc71'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
if (typeof value === "boolean") {
|
||||||
<FontAwesomeIcon
|
if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
|
||||||
icon={faCircleXmark}
|
|
||||||
color='#e74c3c'
|
return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return value;
|
||||||
<TableContainer>
|
};
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
{subscription && !isTableDataLoading && tableData && (
|
|
||||||
<Tr>
|
|
||||||
<Th className="">Feature / Limit</Th>
|
|
||||||
{tableData.head.map(({
|
|
||||||
name,
|
|
||||||
priceLine
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Th
|
|
||||||
key={`plans-feature-head-${billingCycle}-${name}`}
|
|
||||||
className="text-center flex-1"
|
|
||||||
>
|
|
||||||
<p>{name}</p>
|
|
||||||
<p>{priceLine}</p>
|
|
||||||
</Th>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{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>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isTableDataLoading && <TableSkeleton columns={5} key="cloud-products" />}
|
|
||||||
{!isTableDataLoading && tableData?.rows.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={5}>
|
|
||||||
<EmptyState
|
|
||||||
title="No cloud product details found"
|
|
||||||
icon={faFileInvoice}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
{subscription && !isTableDataLoading && tableData && (
|
|
||||||
<Tr className="h-12">
|
|
||||||
<Td />
|
|
||||||
{tableData.head.map(({
|
|
||||||
slug,
|
|
||||||
tier
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const isCurrentPlan = slug === subscription.slug;
|
|
||||||
let subscriptionText = "Upgrade";
|
|
||||||
|
|
||||||
if (subscription.tier > tier) {
|
|
||||||
subscriptionText = "Downgrade"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tier === 3) {
|
|
||||||
subscriptionText = "Contact sales"
|
|
||||||
}
|
|
||||||
|
|
||||||
return isCurrentPlan ? (
|
return (
|
||||||
<Td>
|
<TableContainer>
|
||||||
<Button
|
<Table>
|
||||||
colorSchema="secondary"
|
<THead>
|
||||||
className="w-full"
|
{subscription && !isTableDataLoading && tableData && (
|
||||||
isDisabled
|
<Tr>
|
||||||
>
|
<Th className="">Feature / Limit</Th>
|
||||||
Current
|
{tableData.head.map(({ name, priceLine }) => {
|
||||||
</Button>
|
return (
|
||||||
</Td>
|
<Th
|
||||||
) : (
|
key={`plans-feature-head-${billingCycle}-${name}`}
|
||||||
<Td>
|
className="flex-1 text-center"
|
||||||
<Button
|
>
|
||||||
onClick={async () => {
|
<p>{name}</p>
|
||||||
if (!currentOrg?._id) return;
|
<p>{priceLine}</p>
|
||||||
|
</Th>
|
||||||
if (tier !== 3) {
|
);
|
||||||
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
|
})}
|
||||||
window.location.href = url;
|
</Tr>
|
||||||
return;
|
)}
|
||||||
}
|
</THead>
|
||||||
|
<TBody>
|
||||||
window.location.href = "https://infisical.com/scheduledemo";
|
{subscription &&
|
||||||
}}
|
!isTableDataLoading &&
|
||||||
color="mineshaft"
|
tableData &&
|
||||||
className="w-full"
|
tableData.rows.map(({ name, starter, team, pro, enterprise }) => {
|
||||||
>
|
return (
|
||||||
{subscriptionText}
|
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
|
||||||
</Button>
|
<Td>{displayCell(name)}</Td>
|
||||||
</Td>
|
<Td className="text-center">{displayCell(starter)}</Td>
|
||||||
);
|
<Td className="text-center">{displayCell(team)}</Td>
|
||||||
})}
|
<Td className="text-center">{displayCell(pro)}</Td>
|
||||||
</Tr>
|
<Td className="text-center">{displayCell(enterprise)}</Td>
|
||||||
)}
|
</Tr>
|
||||||
</TBody>
|
);
|
||||||
</Table>
|
})}
|
||||||
</TableContainer>
|
{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} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{subscription && !isTableDataLoading && tableData && (
|
||||||
|
<Tr className="h-12">
|
||||||
|
<Td />
|
||||||
|
{tableData.head.map(({ slug, tier }) => {
|
||||||
|
const isCurrentPlan = slug === subscription.slug;
|
||||||
|
let subscriptionText = "Upgrade";
|
||||||
|
|
||||||
|
if (subscription.tier > tier) {
|
||||||
|
subscriptionText = "Downgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tier === 3) {
|
||||||
|
subscriptionText = "Contact sales";
|
||||||
|
}
|
||||||
|
|
||||||
|
return isCurrentPlan ? (
|
||||||
|
<Td>
|
||||||
|
<Button colorSchema="secondary" className="w-full" isDisabled>
|
||||||
|
Current
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
) : (
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
if (tier !== 3) {
|
||||||
|
const { url } = await createCustomerPortalSession.mutateAsync(
|
||||||
|
currentOrg._id
|
||||||
|
);
|
||||||
|
window.location.href = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = "https://infisical.com/scheduledemo";
|
||||||
|
}}
|
||||||
|
color="mineshaft"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{subscriptionText}
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+61
-71
@@ -14,78 +14,68 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
|
||||||
useDeleteOrgPmtMethod,
|
|
||||||
useGetOrgPmtMethods
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const PmtMethodsTable = () => {
|
export const PmtMethodsTable = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? "");
|
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? "");
|
||||||
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
|
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
|
||||||
|
|
||||||
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
|
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
await deleteOrgPmtMethod.mutateAsync({
|
await deleteOrgPmtMethod.mutateAsync({
|
||||||
organizationId: currentOrg._id,
|
organizationId: currentOrg._id,
|
||||||
pmtMethodId
|
pmtMethodId
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="flex-1">Brand</Th>
|
<Th className="flex-1">Brand</Th>
|
||||||
<Th className="flex-1">Type</Th>
|
<Th className="flex-1">Type</Th>
|
||||||
<Th className="flex-1">Last 4 Digits</Th>
|
<Th className="flex-1">Last 4 Digits</Th>
|
||||||
<Th className="flex-1">Expiration</Th>
|
<Th className="flex-1">Expiration</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{!isLoading && data && data?.length > 0 && data.map(({
|
{!isLoading &&
|
||||||
_id,
|
data &&
|
||||||
brand,
|
data?.length > 0 &&
|
||||||
exp_month,
|
data.map(({ _id, brand, exp_month, exp_year, funding, last4 }) => (
|
||||||
exp_year,
|
<Tr key={`pmt-method-${_id}`} className="h-10">
|
||||||
funding,
|
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
|
||||||
last4
|
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
|
||||||
}) => (
|
<Td>{last4}</Td>
|
||||||
<Tr key={`pmt-method-${_id}`} className="h-10">
|
<Td>{`${exp_month}/${exp_year}`}</Td>
|
||||||
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
|
<Td>
|
||||||
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
|
<IconButton
|
||||||
<Td>{last4}</Td>
|
onClick={async () => {
|
||||||
<Td>{`${exp_month}/${exp_year}`}</Td>
|
await handleDeletePmtMethodBtnClick(_id);
|
||||||
<Td>
|
}}
|
||||||
<IconButton
|
size="lg"
|
||||||
onClick={async () => {
|
colorSchema="danger"
|
||||||
await handleDeletePmtMethodBtnClick(_id);
|
variant="plain"
|
||||||
}}
|
ariaLabel="update"
|
||||||
size="lg"
|
>
|
||||||
colorSchema="danger"
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
variant="plain"
|
</IconButton>
|
||||||
ariaLabel="update"
|
</Td>
|
||||||
>
|
</Tr>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
))}
|
||||||
</IconButton>
|
{isLoading && <TableSkeleton columns={5} innerKey="pmt-methods" />}
|
||||||
</Td>
|
{!isLoading && data && data?.length === 0 && (
|
||||||
</Tr>
|
<Tr>
|
||||||
))}
|
<Td colSpan={5}>
|
||||||
{isLoading && <TableSkeleton columns={5} key="pmt-methods" />}
|
<EmptyState title="No payment methods on file" icon={faCreditCard} />
|
||||||
{!isLoading && data && data?.length === 0 && (
|
</Td>
|
||||||
<Tr>
|
</Tr>
|
||||||
<Td colSpan={5}>
|
)}
|
||||||
<EmptyState
|
</TBody>
|
||||||
title="No payment methods on file"
|
</Table>
|
||||||
icon={faCreditCard}
|
</TableContainer>
|
||||||
/>
|
);
|
||||||
</Td>
|
};
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+109
-116
@@ -1,4 +1,4 @@
|
|||||||
import { faFileInvoice,faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,127 +11,120 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
|
||||||
useDeleteOrgTaxId,
|
|
||||||
useGetOrgTaxIds
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
const taxIDTypeLabelMap: { [key: string]: string } = {
|
const taxIDTypeLabelMap: { [key: string]: string } = {
|
||||||
"au_abn": "Australia ABN",
|
au_abn: "Australia ABN",
|
||||||
"au_arn": "Australia ARN",
|
au_arn: "Australia ARN",
|
||||||
"bg_uic": "Bulgaria UIC",
|
bg_uic: "Bulgaria UIC",
|
||||||
"br_cnpj": "Brazil CNPJ",
|
br_cnpj: "Brazil CNPJ",
|
||||||
"br_cpf": "Brazil CPF",
|
br_cpf: "Brazil CPF",
|
||||||
"ca_bn": "Canada BN",
|
ca_bn: "Canada BN",
|
||||||
"ca_gst_hst": "Canada GST/HST",
|
ca_gst_hst: "Canada GST/HST",
|
||||||
"ca_pst_bc": "Canada PST BC",
|
ca_pst_bc: "Canada PST BC",
|
||||||
"ca_pst_mb": "Canada PST MB",
|
ca_pst_mb: "Canada PST MB",
|
||||||
"ca_pst_sk": "Canada PST SK",
|
ca_pst_sk: "Canada PST SK",
|
||||||
"ca_qst": "Canada QST",
|
ca_qst: "Canada QST",
|
||||||
"ch_vat": "Switzerland VAT",
|
ch_vat: "Switzerland VAT",
|
||||||
"cl_tin": "Chile TIN",
|
cl_tin: "Chile TIN",
|
||||||
"eg_tin": "Egypt TIN",
|
eg_tin: "Egypt TIN",
|
||||||
"es_cif": "Spain CIF",
|
es_cif: "Spain CIF",
|
||||||
"eu_oss_vat": "EU OSS VAT",
|
eu_oss_vat: "EU OSS VAT",
|
||||||
"eu_vat": "EU VAT",
|
eu_vat: "EU VAT",
|
||||||
"gb_vat": "GB VAT",
|
gb_vat: "GB VAT",
|
||||||
"ge_vat": "Georgia VAT",
|
ge_vat: "Georgia VAT",
|
||||||
"hk_br": "Hong Kong BR",
|
hk_br: "Hong Kong BR",
|
||||||
"hu_tin": "Hungary TIN",
|
hu_tin: "Hungary TIN",
|
||||||
"id_npwp": "Indonesia NPWP",
|
id_npwp: "Indonesia NPWP",
|
||||||
"il_vat": "Israel VAT",
|
il_vat: "Israel VAT",
|
||||||
"in_gst": "India GST",
|
in_gst: "India GST",
|
||||||
"is_vat": "Iceland VAT",
|
is_vat: "Iceland VAT",
|
||||||
"jp_cn": "Japan CN",
|
jp_cn: "Japan CN",
|
||||||
"jp_rn": "Japan RN",
|
jp_rn: "Japan RN",
|
||||||
"jp_trn": "Japan TRN",
|
jp_trn: "Japan TRN",
|
||||||
"ke_pin": "Kenya PIN",
|
ke_pin: "Kenya PIN",
|
||||||
"kr_brn": "South Korea BRN",
|
kr_brn: "South Korea BRN",
|
||||||
"li_uid": "Liechtenstein UID",
|
li_uid: "Liechtenstein UID",
|
||||||
"mx_rfc": "Mexico RFC",
|
mx_rfc: "Mexico RFC",
|
||||||
"my_frp": "Malaysia FRP",
|
my_frp: "Malaysia FRP",
|
||||||
"my_itn": "Malaysia ITN",
|
my_itn: "Malaysia ITN",
|
||||||
"my_sst": "Malaysia SST",
|
my_sst: "Malaysia SST",
|
||||||
"no_vat": "Norway VAT",
|
no_vat: "Norway VAT",
|
||||||
"nz_gst": "New Zealand GST",
|
nz_gst: "New Zealand GST",
|
||||||
"ph_tin": "Philippines TIN",
|
ph_tin: "Philippines TIN",
|
||||||
"ru_inn": "Russia INN",
|
ru_inn: "Russia INN",
|
||||||
"ru_kpp": "Russia KPP",
|
ru_kpp: "Russia KPP",
|
||||||
"sa_vat": "Saudi Arabia VAT",
|
sa_vat: "Saudi Arabia VAT",
|
||||||
"sg_gst": "Singapore GST",
|
sg_gst: "Singapore GST",
|
||||||
"sg_uen": "Singapore UEN",
|
sg_uen: "Singapore UEN",
|
||||||
"si_tin": "Slovenia TIN",
|
si_tin: "Slovenia TIN",
|
||||||
"th_vat": "Thailand VAT",
|
th_vat: "Thailand VAT",
|
||||||
"tr_tin": "Turkey TIN",
|
tr_tin: "Turkey TIN",
|
||||||
"tw_vat": "Taiwan VAT",
|
tw_vat: "Taiwan VAT",
|
||||||
"ua_vat": "Ukraine VAT",
|
ua_vat: "Ukraine VAT",
|
||||||
"us_ein": "US EIN",
|
us_ein: "US EIN",
|
||||||
"za_vat": "South Africa VAT"
|
za_vat: "South Africa VAT"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TaxIDTable = () => {
|
export const TaxIDTable = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? "");
|
const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? "");
|
||||||
const deleteOrgTaxId = useDeleteOrgTaxId();
|
const deleteOrgTaxId = useDeleteOrgTaxId();
|
||||||
|
|
||||||
const handleDeleteTaxIdBtnClick = async (taxId: string) => {
|
const handleDeleteTaxIdBtnClick = async (taxId: string) => {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
await deleteOrgTaxId.mutateAsync({
|
await deleteOrgTaxId.mutateAsync({
|
||||||
organizationId: currentOrg._id,
|
organizationId: currentOrg._id,
|
||||||
taxId
|
taxId
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="flex-1">Type</Th>
|
<Th className="flex-1">Type</Th>
|
||||||
<Th className="flex-1">Value</Th>
|
<Th className="flex-1">Value</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{!isLoading && data && data?.length > 0 && data.map(({
|
{!isLoading &&
|
||||||
_id,
|
data &&
|
||||||
type,
|
data?.length > 0 &&
|
||||||
value
|
data.map(({ _id, type, value }) => (
|
||||||
}) => (
|
<Tr key={`tax-id-${_id}`} className="h-10">
|
||||||
<Tr key={`tax-id-${_id}`} className="h-10">
|
<Td>{taxIDTypeLabelMap[type]}</Td>
|
||||||
<Td>{taxIDTypeLabelMap[type]}</Td>
|
<Td>{value}</Td>
|
||||||
<Td>{value}</Td>
|
<Td>
|
||||||
<Td>
|
<IconButton
|
||||||
<IconButton
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await handleDeleteTaxIdBtnClick(_id);
|
||||||
await handleDeleteTaxIdBtnClick(_id);
|
}}
|
||||||
}}
|
size="lg"
|
||||||
size="lg"
|
colorSchema="danger"
|
||||||
colorSchema="danger"
|
variant="plain"
|
||||||
variant="plain"
|
ariaLabel="update"
|
||||||
ariaLabel="update"
|
>
|
||||||
>
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
</IconButton>
|
||||||
</IconButton>
|
</Td>
|
||||||
</Td>
|
</Tr>
|
||||||
</Tr>
|
))}
|
||||||
))}
|
{isLoading && <TableSkeleton columns={3} innerKey="tax-ids" />}
|
||||||
{isLoading && <TableSkeleton columns={3} key="tax-ids" />}
|
{!isLoading && data && data?.length === 0 && (
|
||||||
{!isLoading && data && data?.length === 0 && (
|
<Tr>
|
||||||
<Tr>
|
<Td colSpan={5}>
|
||||||
<Td colSpan={5}>
|
<EmptyState title="No Tax IDs on file" icon={faFileInvoice} />
|
||||||
<EmptyState
|
</Td>
|
||||||
title="No Tax IDs on file"
|
</Tr>
|
||||||
icon={faFileInvoice}
|
)}
|
||||||
/>
|
</TBody>
|
||||||
</Td>
|
</Table>
|
||||||
</Tr>
|
</TableContainer>
|
||||||
)}
|
);
|
||||||
</TBody>
|
};
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+61
-70
@@ -14,76 +14,67 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useGetOrgInvoices } from "@app/hooks/api";
|
||||||
useGetOrgInvoices
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const InvoicesTable = () => {
|
export const InvoicesTable = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? "");
|
const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? "");
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-8">
|
<TableContainer className="mt-8">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="flex-1">Invoice #</Th>
|
<Th className="flex-1">Invoice #</Th>
|
||||||
<Th className="flex-1">Date</Th>
|
<Th className="flex-1">Date</Th>
|
||||||
<Th className="flex-1">Status</Th>
|
<Th className="flex-1">Status</Th>
|
||||||
<Th className="flex-1">Amount</Th>
|
<Th className="flex-1">Amount</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{!isLoading &&
|
||||||
|
data &&
|
||||||
|
data?.length > 0 &&
|
||||||
|
data.map(({ _id, created, paid, number, total, invoice_pdf }) => {
|
||||||
|
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD"
|
||||||
|
});
|
||||||
|
const createdDate = new Date(created * 1000);
|
||||||
|
const day: number = createdDate.getDate();
|
||||||
|
const month: number = createdDate.getMonth() + 1;
|
||||||
|
const year: number = createdDate.getFullYear();
|
||||||
|
const formattedDate: string = `${day}/${month}/${year}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`invoice-${_id}`} className="h-10">
|
||||||
|
<Td>{number}</Td>
|
||||||
|
<Td>{formattedDate}</Td>
|
||||||
|
<Td>{paid ? "Paid" : "Not Paid"}</Td>
|
||||||
|
<Td>{formattedTotal}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => window.open(invoice_pdf)}
|
||||||
|
size="lg"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
);
|
||||||
<TBody>
|
})}
|
||||||
{!isLoading && data && data?.length > 0 && data.map(({
|
{isLoading && <TableSkeleton columns={5} innerKey="invoices" />}
|
||||||
_id,
|
{!isLoading && data && data?.length === 0 && (
|
||||||
created,
|
<Tr>
|
||||||
paid,
|
<Td colSpan={5}>
|
||||||
number,
|
<EmptyState title="No invoices on file" icon={faFileInvoice} />
|
||||||
total,
|
</Td>
|
||||||
invoice_pdf
|
</Tr>
|
||||||
}) => {
|
)}
|
||||||
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
|
</TBody>
|
||||||
style: "currency",
|
</Table>
|
||||||
currency: "USD",
|
</TableContainer>
|
||||||
});
|
);
|
||||||
const createdDate = new Date(created * 1000);
|
};
|
||||||
const day: number = createdDate.getDate();
|
|
||||||
const month: number = createdDate.getMonth() + 1;
|
|
||||||
const year: number = createdDate.getFullYear();
|
|
||||||
const formattedDate: string = `${day}/${month}/${year}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr key={`invoice-${_id}`} className="h-10">
|
|
||||||
<Td>{number}</Td>
|
|
||||||
<Td>{formattedDate}</Td>
|
|
||||||
<Td>{paid ? "Paid" : "Not Paid"}</Td>
|
|
||||||
<Td>{formattedTotal}</Td>
|
|
||||||
<Td>
|
|
||||||
<IconButton
|
|
||||||
onClick={async () => window.open(invoice_pdf)}
|
|
||||||
size="lg"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faDownload} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isLoading && <TableSkeleton columns={5} key="invoices" />}
|
|
||||||
{!isLoading && data && data?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={5}>
|
|
||||||
<EmptyState
|
|
||||||
title="No invoices on file"
|
|
||||||
icon={faFileInvoice}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+386
-381
@@ -1,407 +1,412 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller,useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import { faKey, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
faKey,
|
|
||||||
faMagnifyingGlass,
|
|
||||||
faPlus,
|
|
||||||
faTrash} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
|
||||||
encryptAssymmetric,
|
|
||||||
verifyPrivateKey} from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
decryptAssymmetric,
|
||||||
Checkbox,
|
encryptAssymmetric,
|
||||||
DeleteActionModal,
|
verifyPrivateKey
|
||||||
EmptyState,
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
FormControl,
|
import {
|
||||||
IconButton,
|
Button,
|
||||||
Input,
|
Checkbox,
|
||||||
Modal,
|
DeleteActionModal,
|
||||||
ModalClose,
|
EmptyState,
|
||||||
ModalContent,
|
FormControl,
|
||||||
Select,
|
IconButton,
|
||||||
SelectItem,
|
Input,
|
||||||
Table,
|
Modal,
|
||||||
TableContainer,
|
ModalClose,
|
||||||
TableSkeleton,
|
ModalContent,
|
||||||
TBody,
|
Select,
|
||||||
Td,
|
SelectItem,
|
||||||
Th,
|
Table,
|
||||||
THead,
|
TableContainer,
|
||||||
Tr} from "@app/components/v2";
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateServiceAccountProjectLevelPermission,
|
useCreateServiceAccountProjectLevelPermission,
|
||||||
useDeleteServiceAccountProjectLevelPermission,
|
useDeleteServiceAccountProjectLevelPermission,
|
||||||
useGetServiceAccountById,
|
useGetServiceAccountById,
|
||||||
useGetServiceAccountProjectLevelPermissions,
|
useGetServiceAccountProjectLevelPermissions,
|
||||||
useGetUserWorkspaces
|
useGetUserWorkspaces
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
||||||
|
|
||||||
const createProjectLevelPermissionSchema = yup.object({
|
const createProjectLevelPermissionSchema = yup.object({
|
||||||
privateKey: yup.string().required().label("Private Key"),
|
privateKey: yup.string().required().label("Private Key"),
|
||||||
workspace: yup.string().required().label("Workspace"),
|
workspace: yup.string().required().label("Workspace"),
|
||||||
environment: yup.string().required().label("Environment"),
|
environment: yup.string().required().label("Environment"),
|
||||||
permissions: yup.object().shape({
|
permissions: yup
|
||||||
read: yup.boolean().required(),
|
.object()
|
||||||
write: yup.boolean().required()
|
.shape({
|
||||||
}).defined().required()
|
read: yup.boolean().required(),
|
||||||
|
write: yup.boolean().required()
|
||||||
|
})
|
||||||
|
.defined()
|
||||||
|
.required()
|
||||||
});
|
});
|
||||||
|
|
||||||
type CreateProjectLevelPermissionForm = yup.InferType<typeof createProjectLevelPermissionSchema>;
|
type CreateProjectLevelPermissionForm = yup.InferType<typeof createProjectLevelPermissionSchema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
serviceAccountId: string;
|
serviceAccountId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const SAProjectLevelPermissionsTable = ({
|
export const SAProjectLevelPermissionsTable = ({ serviceAccountId }: Props): JSX.Element => {
|
||||||
serviceAccountId
|
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
|
||||||
}: Props): JSX.Element => {
|
const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
|
||||||
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
|
const [searchPermissions, setSearchPermissions] = useState("");
|
||||||
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 { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const createServiceAccountProjectLevelPermission =
|
||||||
"addProjectLevelPermission",
|
useCreateServiceAccountProjectLevelPermission();
|
||||||
"removeProjectLevelPermission",
|
const deleteServiceAccountProjectLevelPermission =
|
||||||
] as const);
|
useDeleteServiceAccountProjectLevelPermission();
|
||||||
|
|
||||||
const [, setSelectedWorkspace] = useState<undefined | string>(undefined);
|
|
||||||
|
|
||||||
const {
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
control,
|
"addProjectLevelPermission",
|
||||||
handleSubmit,
|
"removeProjectLevelPermission"
|
||||||
reset,
|
] as const);
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<CreateProjectLevelPermissionForm>({ resolver: yupResolver(createProjectLevelPermissionSchema) })
|
|
||||||
|
|
||||||
const onAddProjectLevelPermission = async ({
|
const [, setSelectedWorkspace] = useState<undefined | string>(undefined);
|
||||||
privateKey,
|
|
||||||
workspace,
|
|
||||||
environment,
|
|
||||||
permissions: { read, write }
|
|
||||||
}: CreateProjectLevelPermissionForm) => {
|
|
||||||
|
|
||||||
// TODO: clean up / modularize this function
|
|
||||||
|
|
||||||
if (!serviceAccount) return;
|
|
||||||
|
|
||||||
const { latestKey } = await getLatestFileKey({
|
|
||||||
workspaceId: workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
verifyPrivateKey({
|
const {
|
||||||
privateKey,
|
control,
|
||||||
publicKey: serviceAccount.publicKey
|
handleSubmit,
|
||||||
});
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
} = useForm<CreateProjectLevelPermissionForm>({
|
||||||
|
resolver: yupResolver(createProjectLevelPermissionSchema)
|
||||||
|
});
|
||||||
|
|
||||||
const key = decryptAssymmetric({
|
const onAddProjectLevelPermission = async ({
|
||||||
ciphertext: latestKey.encryptedKey,
|
privateKey,
|
||||||
nonce: latestKey.nonce,
|
workspace,
|
||||||
publicKey: latestKey.sender.publicKey,
|
environment,
|
||||||
privateKey: PRIVATE_KEY
|
permissions: { read, write }
|
||||||
});
|
}: CreateProjectLevelPermissionForm) => {
|
||||||
|
// TODO: clean up / modularize this function
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: key,
|
|
||||||
publicKey: serviceAccount.publicKey,
|
|
||||||
privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
await createServiceAccountProjectLevelPermission.mutateAsync({
|
|
||||||
serviceAccountId,
|
|
||||||
workspaceId: workspace,
|
|
||||||
environment,
|
|
||||||
read,
|
|
||||||
write,
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
nonce
|
|
||||||
});
|
|
||||||
handlePopUpClose("addProjectLevelPermission");
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRemoveProjectLevelPermission = async () => {
|
|
||||||
const serviceAccountWorkspacePermissionId = (popUp?.removeProjectLevelPermission?.data as { _id: string })?._id;
|
|
||||||
await deleteServiceAccountProjectLevelPermission.mutateAsync({
|
|
||||||
serviceAccountId,
|
|
||||||
serviceAccountWorkspacePermissionId
|
|
||||||
});
|
|
||||||
handlePopUpClose("removeProjectLevelPermission");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
if (!serviceAccount) return;
|
||||||
<div className="w-full bg-white/5 p-6">
|
|
||||||
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
|
const { latestKey } = await getLatestFileKey({
|
||||||
<div className="mb-4 flex">
|
workspaceId: workspace
|
||||||
<div className="mr-4 flex-1">
|
});
|
||||||
<Input
|
|
||||||
value={searchPermissions}
|
verifyPrivateKey({
|
||||||
onChange={(e) => setSearchPermissions(e.target.value)}
|
privateKey,
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
publicKey: serviceAccount.publicKey
|
||||||
placeholder="Search service account project-level permissions..."
|
});
|
||||||
/>
|
|
||||||
</div>
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
const key = decryptAssymmetric({
|
||||||
onClick={() => {
|
ciphertext: latestKey.encryptedKey,
|
||||||
handlePopUpOpen("addProjectLevelPermission")
|
nonce: latestKey.nonce,
|
||||||
reset();
|
publicKey: latestKey.sender.publicKey,
|
||||||
}}
|
privateKey: PRIVATE_KEY
|
||||||
>
|
});
|
||||||
Add Permission
|
|
||||||
</Button>
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
</div>
|
plaintext: key,
|
||||||
<TableContainer>
|
publicKey: serviceAccount.publicKey,
|
||||||
<Table>
|
privateKey
|
||||||
<THead>
|
});
|
||||||
<Tr>
|
|
||||||
<Th>Project</Th>
|
await createServiceAccountProjectLevelPermission.mutateAsync({
|
||||||
<Th>Environment</Th>
|
serviceAccountId,
|
||||||
<Th>Read</Th>
|
workspaceId: workspace,
|
||||||
<Th>Write</Th>
|
environment,
|
||||||
<Th aria-label="actions" />
|
read,
|
||||||
</Tr>
|
write,
|
||||||
</THead>
|
encryptedKey: ciphertext,
|
||||||
<TBody>
|
nonce
|
||||||
{isPermissionsLoading && <TableSkeleton columns={6} key="service-account-project-level-permissions" />}
|
});
|
||||||
{!isPermissionsLoading && serviceAccountWorkspacePermissions && (
|
handlePopUpClose("addProjectLevelPermission");
|
||||||
serviceAccountWorkspacePermissions.map(({
|
};
|
||||||
_id,
|
|
||||||
workspace,
|
const onRemoveProjectLevelPermission = async () => {
|
||||||
environment,
|
const serviceAccountWorkspacePermissionId = (
|
||||||
read,
|
popUp?.removeProjectLevelPermission?.data as { _id: string }
|
||||||
write
|
)?._id;
|
||||||
}) => {
|
await deleteServiceAccountProjectLevelPermission.mutateAsync({
|
||||||
const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name;
|
serviceAccountId,
|
||||||
return (
|
serviceAccountWorkspacePermissionId
|
||||||
<Tr key={`service-account-project-level-permission-${_id}`} className="w-full">
|
});
|
||||||
<Td>{workspace.name}</Td>
|
handlePopUpClose("removeProjectLevelPermission");
|
||||||
<Td>{environmentName}</Td>
|
};
|
||||||
<Td>
|
|
||||||
<Checkbox
|
return (
|
||||||
id="isReadPermissionEnabled"
|
<div className="w-full bg-white/5 p-6">
|
||||||
isChecked={read}
|
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
|
||||||
isDisabled
|
<div className="mb-4 flex">
|
||||||
>{/**/}</Checkbox>
|
<div className="mr-4 flex-1">
|
||||||
</Td>
|
<Input
|
||||||
<Td>
|
value={searchPermissions}
|
||||||
<Checkbox
|
onChange={(e) => setSearchPermissions(e.target.value)}
|
||||||
id="isWritePermissionEnabled"
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
isChecked={write}
|
placeholder="Search service account project-level permissions..."
|
||||||
isDisabled
|
/>
|
||||||
>{/**/}</Checkbox>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="delete"
|
|
||||||
colorSchema="danger"
|
|
||||||
onClick={() => handlePopUpOpen("removeProjectLevelPermission", { _id })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
{!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={7} className="py-6 text-center text-bunker-400">
|
|
||||||
<EmptyState title="No permissions found" icon={faKey} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
<Modal
|
|
||||||
isOpen={popUp?.addProjectLevelPermission?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
handlePopUpToggle("addProjectLevelPermission", isOpen);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title="Add a Project-Level Permission"
|
|
||||||
subTitle="The service account will be granted scoped access to the specified project and environment"
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit(onAddProjectLevelPermission)}>
|
|
||||||
{!isUserWorkspacesLoading && userWorkspaces && (
|
|
||||||
<>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="privateKey"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Service Account Private Key"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="workspace"
|
|
||||||
defaultValue={String(userWorkspaces?.[0]?._id)}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Project"
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => {
|
|
||||||
onChange(e);
|
|
||||||
setSelectedWorkspace(e);
|
|
||||||
}}
|
|
||||||
className="w-full border border-mine-shaft-500"
|
|
||||||
>
|
|
||||||
{userWorkspaces && userWorkspaces.length > 0 ? (
|
|
||||||
userWorkspaces.map((userWorkspace) => {
|
|
||||||
return (
|
|
||||||
<SelectItem value={userWorkspace._id} key={`project-${userWorkspace._id}`}>
|
|
||||||
{userWorkspace.name}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<SelectItem value="none" key="target-app-none">
|
|
||||||
No projects found
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="environment"
|
|
||||||
defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)}
|
|
||||||
render={({ field: { onChange, ...field } }) => {
|
|
||||||
/* eslint-disable-next-line no-underscore-dangle */
|
|
||||||
const environments = userWorkspaces?.find((userWorkspace) => userWorkspace._id === control?._formValues?.workspace)?.environments ?? [];
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
label="Environment"
|
|
||||||
className="mt-4"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full border border-mine-shaft-500"
|
|
||||||
>
|
|
||||||
{environments.length > 0 ? (
|
|
||||||
environments.map((environment) => {
|
|
||||||
return (
|
|
||||||
<SelectItem value={environment.slug} key={`environment-${environment.slug}`}>
|
|
||||||
{environment.name}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<SelectItem value="none" key="target-app-none">
|
|
||||||
No environments found
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="permissions"
|
|
||||||
defaultValue={{
|
|
||||||
read: true,
|
|
||||||
write: false
|
|
||||||
}}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error }}) => {
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: "Read (default)",
|
|
||||||
value: "read"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Write",
|
|
||||||
value: "write"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
label="Permissions"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{options.map(({ label, value: optionValue }) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
id={value[optionValue]}
|
|
||||||
key={optionValue}
|
|
||||||
className="data-[state=checked]:bg-primary"
|
|
||||||
isChecked={value[optionValue]}
|
|
||||||
isDisabled={optionValue === "read"}
|
|
||||||
onCheckedChange={(state) => {
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
[optionValue]: state
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Checkbox>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
type="submit"
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
<ModalClose asChild>
|
|
||||||
<Button variant="plain" colorSchema="secondary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ModalClose>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.removeProjectLevelPermission.isOpen}
|
|
||||||
deleteKey="remove"
|
|
||||||
title="Do you want to remove this permission from the service account?"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeProjectLevelPermission", isOpen)}
|
|
||||||
onDeleteApproved={onRemoveProjectLevelPermission}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<Button
|
||||||
}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("addProjectLevelPermission");
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Permission
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Project</Th>
|
||||||
|
<Th>Environment</Th>
|
||||||
|
<Th>Read</Th>
|
||||||
|
<Th>Write</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{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>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Checkbox id="isWritePermissionEnabled" isChecked={write} isDisabled>
|
||||||
|
{/**/}
|
||||||
|
</Checkbox>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => handlePopUpOpen("removeProjectLevelPermission", { _id })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={7} className="py-6 text-center text-bunker-400">
|
||||||
|
<EmptyState title="No permissions found" icon={faKey} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addProjectLevelPermission?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addProjectLevelPermission", isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add a Project-Level Permission"
|
||||||
|
subTitle="The service account will be granted scoped access to the specified project and environment"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onAddProjectLevelPermission)}>
|
||||||
|
{!isUserWorkspacesLoading && userWorkspaces && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="privateKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Service Account Private Key"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="workspace"
|
||||||
|
defaultValue={String(userWorkspaces?.[0]?._id)}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Project" errorText={error?.message}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
onChange(e);
|
||||||
|
setSelectedWorkspace(e);
|
||||||
|
}}
|
||||||
|
className="border-mine-shaft-500 w-full border"
|
||||||
|
>
|
||||||
|
{userWorkspaces && userWorkspaces.length > 0 ? (
|
||||||
|
userWorkspaces.map((userWorkspace) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={userWorkspace._id}
|
||||||
|
key={`project-${userWorkspace._id}`}
|
||||||
|
>
|
||||||
|
{userWorkspace.name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No projects found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="environment"
|
||||||
|
defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)}
|
||||||
|
render={({ field: { onChange, ...field } }) => {
|
||||||
|
const environments =
|
||||||
|
userWorkspaces?.find(
|
||||||
|
/* eslint-disable-next-line no-underscore-dangle */
|
||||||
|
(userWorkspace) => userWorkspace._id === control?._formValues?.workspace
|
||||||
|
)?.environments ?? [];
|
||||||
|
return (
|
||||||
|
<FormControl label="Environment" className="mt-4">
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="border-mine-shaft-500 w-full border"
|
||||||
|
>
|
||||||
|
{environments.length > 0 ? (
|
||||||
|
environments.map((environment) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={environment.slug}
|
||||||
|
key={`environment-${environment.slug}`}
|
||||||
|
>
|
||||||
|
{environment.name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No environments found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="permissions"
|
||||||
|
defaultValue={{
|
||||||
|
read: true,
|
||||||
|
write: false
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: "Read (default)",
|
||||||
|
value: "read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Write",
|
||||||
|
value: "write"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Permissions"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{options.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={value[optionValue]}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
isDisabled={optionValue === "read"}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button variant="plain" colorSchema="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeProjectLevelPermission.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this permission from the service account?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeProjectLevelPermission", isOpen)}
|
||||||
|
onDeleteApproved={onRemoveProjectLevelPermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+60
-66
@@ -1,9 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import { faContactBook, faMagnifyingGlass, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
faContactBook,
|
|
||||||
faMagnifyingGlass,
|
|
||||||
faTrash
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
@@ -23,13 +19,11 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
|
||||||
useDeleteIncidentContact,
|
|
||||||
useGetOrgIncidentContact} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const OrgIncidentContactsTable = () => {
|
export const OrgIncidentContactsTable = () => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data: contacts, isLoading } = useGetOrgIncidentContact(currentOrg?._id ?? "");
|
const { data: contacts, isLoading } = useGetOrgIncidentContact(currentOrg?._id ?? "");
|
||||||
const [searchContact, setSearchContact] = useState("");
|
const [searchContact, setSearchContact] = useState("");
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
@@ -40,71 +34,71 @@ export const OrgIncidentContactsTable = () => {
|
|||||||
|
|
||||||
const onRemoveIncidentContact = async () => {
|
const onRemoveIncidentContact = async () => {
|
||||||
try {
|
try {
|
||||||
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
||||||
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
await mutateAsync({
|
|
||||||
orgId: currentOrg._id,
|
|
||||||
email: incidentContactEmail
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
if (!currentOrg?._id) return;
|
||||||
text: "Successfully removed incident contact",
|
await mutateAsync({
|
||||||
type: "success"
|
orgId: currentOrg._id,
|
||||||
});
|
email: incidentContactEmail
|
||||||
|
});
|
||||||
handlePopUpClose("removeContact");
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully removed incident contact",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("removeContact");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to remove incident contact",
|
text: "Failed to remove incident contact",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredContacts = contacts ? contacts.filter(({ email }) =>
|
const filteredContacts = contacts
|
||||||
email.toLocaleLowerCase().includes(searchContact)
|
? contacts.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
|
||||||
) : [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
value={searchContact}
|
value={searchContact}
|
||||||
onChange={(e) => setSearchContact(e.target.value)}
|
onChange={(e) => setSearchContact(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search incident contact by email..."
|
placeholder="Search incident contact by email..."
|
||||||
/>
|
/>
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Email</Th>
|
<Th>Email</Th>
|
||||||
<Th aria-label="actions" />
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
|
||||||
|
{filteredContacts?.map(({ email }) => (
|
||||||
|
<Tr key={email}>
|
||||||
|
<Td className="w-full">{email}</Td>
|
||||||
|
<Td className="mr-4">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => handlePopUpOpen("removeContact", { email })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
))}
|
||||||
<TBody>
|
</TBody>
|
||||||
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
|
</Table>
|
||||||
{filteredContacts?.map(({ email }) => (
|
{filteredContacts?.length === 0 && !isLoading && (
|
||||||
<Tr key={email}>
|
<EmptyState title="No incident contacts found" icon={faContactBook} />
|
||||||
<Td className="w-full">{email}</Td>
|
)}
|
||||||
<Td className="mr-4">
|
</TableContainer>
|
||||||
<IconButton
|
|
||||||
ariaLabel="delete"
|
|
||||||
colorSchema="danger"
|
|
||||||
onClick={() => handlePopUpOpen("removeContact", { email })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{filteredContacts?.length === 0 && !isLoading && (
|
|
||||||
<EmptyState title="No incident contacts found" icon={faContactBook} />
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.removeContact.isOpen}
|
isOpen={popUp.removeContact.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
|
|||||||
+322
-323
@@ -1,17 +1,14 @@
|
|||||||
import { useEffect, useMemo,useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
|
||||||
// Controller,
|
|
||||||
// useForm
|
|
||||||
} from "react-hook-form";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
faCopy,
|
faCopy,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faPencil,
|
faPencil,
|
||||||
faPlus,
|
faPlus,
|
||||||
faServer,
|
faServer,
|
||||||
faTrash} from "@fortawesome/free-solid-svg-icons";
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
// import { yupResolver } from "@hookform/resolvers/yup";
|
// import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
@@ -21,13 +18,13 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
// FormControl,
|
// FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
// Select,
|
// Select,
|
||||||
// SelectItem,
|
// SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -40,11 +37,15 @@ import {
|
|||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
// useCreateServiceAccount,
|
// useCreateServiceAccount,
|
||||||
useDeleteServiceAccount,
|
useDeleteServiceAccount,
|
||||||
useGetServiceAccounts
|
useGetServiceAccounts
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
import // Controller,
|
||||||
|
// useForm
|
||||||
|
"react-hook-form";
|
||||||
|
|
||||||
// const serviceAccountExpiration = [
|
// const serviceAccountExpiration = [
|
||||||
// { label: "1 Day", value: 86400 },
|
// { label: "1 Day", value: 86400 },
|
||||||
// { label: "7 Days", value: 604800 },
|
// { label: "7 Days", value: 604800 },
|
||||||
@@ -62,314 +63,312 @@ import {
|
|||||||
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
||||||
|
|
||||||
export const OrgServiceAccountsTable = () => {
|
export const OrgServiceAccountsTable = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const orgId = currentOrg?._id || "";
|
|
||||||
const [step, setStep] = useState(0);
|
|
||||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
|
||||||
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
|
||||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
|
||||||
const [accessKey] = useState("");
|
|
||||||
const [publicKey] = useState("");
|
|
||||||
const [privateKey] = useState("");
|
|
||||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
|
||||||
"addServiceAccount",
|
|
||||||
"removeServiceAccount",
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
|
const orgId = currentOrg?._id || "";
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
// const createServiceAccount = useCreateServiceAccount();
|
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||||
const removeServiceAccount = useDeleteServiceAccount();
|
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
||||||
|
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||||
useEffect(() => {
|
const [accessKey] = useState("");
|
||||||
let timer: NodeJS.Timeout;
|
const [publicKey] = useState("");
|
||||||
if (isAccessKeyCopied) {
|
const [privateKey] = useState("");
|
||||||
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
||||||
}
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"addServiceAccount",
|
||||||
|
"removeServiceAccount"
|
||||||
|
] as const);
|
||||||
|
|
||||||
if (isPublicKeyCopied) {
|
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
|
||||||
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
useGetServiceAccounts(orgId);
|
||||||
}
|
|
||||||
|
|
||||||
if (isPrivateKeyCopied) {
|
// const createServiceAccount = useCreateServiceAccount();
|
||||||
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
const removeServiceAccount = useDeleteServiceAccount();
|
||||||
}
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
useEffect(() => {
|
||||||
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isAccessKeyCopied) {
|
||||||
// const {
|
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
||||||
// control,
|
|
||||||
// handleSubmit,
|
|
||||||
// reset,
|
|
||||||
// formState: { isSubmitting }
|
|
||||||
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
|
||||||
|
|
||||||
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
|
||||||
// if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
// const keyPair = generateKeyPair();
|
|
||||||
// setPublicKey(keyPair.publicKey);
|
|
||||||
// setPrivateKey(keyPair.privateKey);
|
|
||||||
|
|
||||||
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
|
||||||
// name,
|
|
||||||
// organizationId: currentOrg?._id,
|
|
||||||
// publicKey: keyPair.publicKey,
|
|
||||||
// expiresIn: Number(expiresIn)
|
|
||||||
// });
|
|
||||||
|
|
||||||
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
|
||||||
|
|
||||||
// setStep(1);
|
|
||||||
// reset();
|
|
||||||
// }
|
|
||||||
|
|
||||||
const onRemoveServiceAccount = async () => {
|
|
||||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
|
||||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
|
||||||
handlePopUpClose("removeServiceAccount");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredServiceAccounts = useMemo(
|
if (isPublicKeyCopied) {
|
||||||
() =>
|
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
||||||
serviceAccounts.filter(
|
|
||||||
({ name }) =>
|
|
||||||
name.toLowerCase().includes(searchServiceAccountFilter)
|
|
||||||
),
|
|
||||||
[serviceAccounts, searchServiceAccountFilter]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderStep = (stepToRender: number) => {
|
|
||||||
switch (stepToRender) {
|
|
||||||
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.
|
|
||||||
</div>
|
|
||||||
// <form onSubmit={handleSubmit(onAddServiceAccount)}>
|
|
||||||
// <Controller
|
|
||||||
// control={control}
|
|
||||||
// defaultValue=""
|
|
||||||
// name="name"
|
|
||||||
// render={({ field, fieldState: { error } }) => (
|
|
||||||
// <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
// <Input {...field} />
|
|
||||||
// </FormControl>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// <Controller
|
|
||||||
// control={control}
|
|
||||||
// name="expiresIn"
|
|
||||||
// defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
|
||||||
// render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
|
||||||
// return (
|
|
||||||
// <FormControl
|
|
||||||
// label="Expiration"
|
|
||||||
// errorText={error?.message}
|
|
||||||
// isError={Boolean(error)}
|
|
||||||
// >
|
|
||||||
// <Select
|
|
||||||
// defaultValue={field.value}
|
|
||||||
// {...field}
|
|
||||||
// onValueChange={(e) => onChange(e)}
|
|
||||||
// className="w-full"
|
|
||||||
// >
|
|
||||||
// {serviceAccountExpiration.map(({ label, value }) => (
|
|
||||||
// <SelectItem value={String(value)} key={label}>
|
|
||||||
// {label}
|
|
||||||
// </SelectItem>
|
|
||||||
// ))}
|
|
||||||
// </Select>
|
|
||||||
// </FormControl>
|
|
||||||
// );
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// <div className="mt-8 flex items-center">
|
|
||||||
// <Button
|
|
||||||
// className="mr-4"
|
|
||||||
// size="sm"
|
|
||||||
// type="submit"
|
|
||||||
// isLoading={isSubmitting}
|
|
||||||
// isDisabled={isSubmitting}
|
|
||||||
// >
|
|
||||||
// Create Service Account
|
|
||||||
// </Button>
|
|
||||||
// <Button
|
|
||||||
// colorSchema="secondary"
|
|
||||||
// variant="plain"
|
|
||||||
// onClick={() => handlePopUpClose("addServiceAccount")}
|
|
||||||
// >
|
|
||||||
// Cancel
|
|
||||||
// </Button>
|
|
||||||
// </div>
|
|
||||||
// </form>
|
|
||||||
);
|
|
||||||
case 1:
|
|
||||||
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]">
|
|
||||||
<p className="mr-4 break-all">{accessKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(accessKey);
|
|
||||||
setIsAccessKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isAccessKeyCopied ? 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">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</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]">
|
|
||||||
<p className="mr-4 break-all">{publicKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(publicKey);
|
|
||||||
setIsPublicKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</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]">
|
|
||||||
<p className="mr-4 break-all">{privateKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(privateKey);
|
|
||||||
setIsPrivateKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <div />
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (isPrivateKeyCopied) {
|
||||||
<div className="w-full">
|
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
||||||
<div className="flex justify-between mb-4">
|
}
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
|
|
||||||
<Button
|
return () => clearTimeout(timer);
|
||||||
colorSchema="secondary"
|
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
// const {
|
||||||
setStep(0);
|
// control,
|
||||||
// reset();
|
// handleSubmit,
|
||||||
handlePopUpOpen("addServiceAccount");
|
// reset,
|
||||||
}}
|
// formState: { isSubmitting }
|
||||||
>
|
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||||
Add Service Account
|
|
||||||
</Button>
|
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||||
</div>
|
// if (!currentOrg?._id) return;
|
||||||
<Input
|
|
||||||
value={searchServiceAccountFilter}
|
// const keyPair = generateKeyPair();
|
||||||
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
// setPublicKey(keyPair.publicKey);
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
// setPrivateKey(keyPair.privateKey);
|
||||||
placeholder="Search service accounts..."
|
|
||||||
/>
|
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||||
<TableContainer className="mt-4">
|
// name,
|
||||||
<Table>
|
// organizationId: currentOrg?._id,
|
||||||
<THead>
|
// publicKey: keyPair.publicKey,
|
||||||
<Th>Name</Th>
|
// expiresIn: Number(expiresIn)
|
||||||
<Th className="w-full">Valid Until</Th>
|
// });
|
||||||
<Th aria-label="actions" />
|
|
||||||
</THead>
|
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||||
<TBody>
|
|
||||||
{isServiceAccountsLoading && <TableSkeleton columns={5} key="org-service-accounts" />}
|
// setStep(1);
|
||||||
{!isServiceAccountsLoading && (
|
// reset();
|
||||||
filteredServiceAccounts.map(({
|
// }
|
||||||
name,
|
|
||||||
expiresAt,
|
const onRemoveServiceAccount = async () => {
|
||||||
_id: serviceAccountId
|
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||||
}) => {
|
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||||
return (
|
handlePopUpClose("removeServiceAccount");
|
||||||
<Tr key={`org-service-account-${serviceAccountId}`}>
|
};
|
||||||
<Td>{name}</Td>
|
|
||||||
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
const filteredServiceAccounts = useMemo(
|
||||||
<Td>
|
() =>
|
||||||
<div className="flex">
|
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
|
||||||
<IconButton
|
[serviceAccounts, searchServiceAccountFilter]
|
||||||
ariaLabel="edit"
|
);
|
||||||
colorSchema="secondary"
|
|
||||||
onClick={() => {
|
const renderStep = (stepToRender: number) => {
|
||||||
if (currentWorkspace?._id) {
|
switch (stepToRender) {
|
||||||
router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`);
|
case 0:
|
||||||
}
|
return (
|
||||||
}}
|
<div>
|
||||||
className="mr-2"
|
We are currently revising the service account mechanism. In the meantime, please use
|
||||||
>
|
service tokens or API key to fetch secrets via API request.
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
</div>
|
||||||
</IconButton>
|
// <form onSubmit={handleSubmit(onAddServiceAccount)}>
|
||||||
<IconButton
|
// <Controller
|
||||||
ariaLabel="delete"
|
// control={control}
|
||||||
colorSchema="danger"
|
// defaultValue=""
|
||||||
onClick={() => handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })}
|
// name="name"
|
||||||
>
|
// render={({ field, fieldState: { error } }) => (
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
// <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
</IconButton>
|
// <Input {...field} />
|
||||||
</div>
|
// </FormControl>
|
||||||
</Td>
|
// )}
|
||||||
</Tr>
|
// />
|
||||||
);
|
// <Controller
|
||||||
})
|
// control={control}
|
||||||
)}
|
// name="expiresIn"
|
||||||
</TBody>
|
// defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
||||||
</Table>
|
// render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
// return (
|
||||||
<EmptyState title="No service accounts found" icon={faServer} />
|
// <FormControl
|
||||||
)}
|
// label="Expiration"
|
||||||
</TableContainer>
|
// errorText={error?.message}
|
||||||
<Modal
|
// isError={Boolean(error)}
|
||||||
isOpen={popUp?.addServiceAccount?.isOpen}
|
// >
|
||||||
onOpenChange={(isOpen) => {
|
// <Select
|
||||||
handlePopUpToggle("addServiceAccount", isOpen);
|
// defaultValue={field.value}
|
||||||
// reset();
|
// {...field}
|
||||||
|
// onValueChange={(e) => onChange(e)}
|
||||||
|
// className="w-full"
|
||||||
|
// >
|
||||||
|
// {serviceAccountExpiration.map(({ label, value }) => (
|
||||||
|
// <SelectItem value={String(value)} key={label}>
|
||||||
|
// {label}
|
||||||
|
// </SelectItem>
|
||||||
|
// ))}
|
||||||
|
// </Select>
|
||||||
|
// </FormControl>
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// <div className="mt-8 flex items-center">
|
||||||
|
// <Button
|
||||||
|
// className="mr-4"
|
||||||
|
// size="sm"
|
||||||
|
// type="submit"
|
||||||
|
// isLoading={isSubmitting}
|
||||||
|
// isDisabled={isSubmitting}
|
||||||
|
// >
|
||||||
|
// Create Service Account
|
||||||
|
// </Button>
|
||||||
|
// <Button
|
||||||
|
// colorSchema="secondary"
|
||||||
|
// variant="plain"
|
||||||
|
// onClick={() => handlePopUpClose("addServiceAccount")}
|
||||||
|
// >
|
||||||
|
// Cancel
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// </form>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>Access Key</p>
|
||||||
|
<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"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(accessKey);
|
||||||
|
setIsAccessKeyCopied.on();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<FontAwesomeIcon icon={isAccessKeyCopied ? faCheck : faCopy} />
|
||||||
title="Add Service Account"
|
<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">
|
||||||
subTitle="A service account represents a machine identity such as a VM or application client."
|
Copy
|
||||||
>
|
</span>
|
||||||
{renderStep(step)}
|
</IconButton>
|
||||||
</ModalContent>
|
</div>
|
||||||
</Modal>
|
<p className="mt-4">Public Key</p>
|
||||||
<DeleteActionModal
|
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
isOpen={popUp.removeServiceAccount.isOpen}
|
<p className="mr-4 break-all">{publicKey}</p>
|
||||||
deleteKey="remove"
|
<IconButton
|
||||||
title="Do you want to remove this service account from the org?"
|
ariaLabel="copy icon"
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
colorSchema="secondary"
|
||||||
onDeleteApproved={onRemoveServiceAccount}
|
className="group relative"
|
||||||
/>
|
onClick={() => {
|
||||||
</div>
|
navigator.clipboard.writeText(publicKey);
|
||||||
);
|
setIsPublicKeyCopied.on();
|
||||||
}
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4">Private Key</p>
|
||||||
|
<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"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(privateKey);
|
||||||
|
setIsPrivateKeyCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
setStep(0);
|
||||||
|
// reset();
|
||||||
|
handlePopUpOpen("addServiceAccount");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Service Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={searchServiceAccountFilter}
|
||||||
|
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search service accounts..."
|
||||||
|
/>
|
||||||
|
<TableContainer className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th className="w-full">Valid Until</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{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>
|
||||||
|
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="edit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
if (currentWorkspace?._id) {
|
||||||
|
router.push(
|
||||||
|
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mr-2"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
||||||
|
<EmptyState title="No service accounts found" icon={faServer} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addServiceAccount?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addServiceAccount", isOpen);
|
||||||
|
// reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add Service Account"
|
||||||
|
subTitle="A service account represents a machine identity such as a VM or application client."
|
||||||
|
>
|
||||||
|
{renderStep(step)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeServiceAccount.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this service account from the org?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
||||||
|
onDeleteApproved={onRemoveServiceAccount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { faKey,faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
@@ -14,99 +14,91 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import { useDeleteAPIKey, useGetMyAPIKeys } from "@app/hooks/api";
|
||||||
useDeleteAPIKey,
|
|
||||||
useGetMyAPIKeys} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const APIKeyTable = () => {
|
export const APIKeyTable = () => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { data, isLoading } = useGetMyAPIKeys();
|
const { data, isLoading } = useGetMyAPIKeys();
|
||||||
const { mutateAsync } = useDeleteAPIKey();
|
const { mutateAsync } = useDeleteAPIKey();
|
||||||
|
|
||||||
const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => {
|
const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => {
|
||||||
try {
|
try {
|
||||||
await mutateAsync(apiKeyDataId);
|
await mutateAsync(apiKeyDataId);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully deleted API key",
|
text: "Successfully deleted API key",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to delete API key",
|
text: "Failed to delete API key",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
const formatDate = (dateToFormat: string) => {
|
|
||||||
const date = new Date(dateToFormat);
|
const formatDate = (dateToFormat: string) => {
|
||||||
const year = date.getFullYear();
|
const date = new Date(dateToFormat);
|
||||||
const month = date.getMonth() + 1;
|
const year = date.getFullYear();
|
||||||
const day = date.getDate();
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
const formattedDate = `${day}/${month}/${year}`;
|
|
||||||
|
const formattedDate = `${day}/${month}/${year}`;
|
||||||
return formattedDate;
|
|
||||||
}
|
return formattedDate;
|
||||||
|
};
|
||||||
return (
|
|
||||||
<div>
|
return (
|
||||||
<TableContainer className="">
|
<div>
|
||||||
<Table>
|
<TableContainer className="">
|
||||||
<THead>
|
<Table>
|
||||||
<Tr>
|
<THead>
|
||||||
<Th className="flex-1">Name</Th>
|
<Tr>
|
||||||
<Th className="flex-1">Last active</Th>
|
<Th className="flex-1">Name</Th>
|
||||||
<Th className="flex-1">Created</Th>
|
<Th className="flex-1">Last active</Th>
|
||||||
<Th className="flex-1">Expiration</Th>
|
<Th className="flex-1">Created</Th>
|
||||||
<Th className="w-5" />
|
<Th className="flex-1">Expiration</Th>
|
||||||
</Tr>
|
<Th className="w-5" />
|
||||||
</THead>
|
</Tr>
|
||||||
<TBody>
|
</THead>
|
||||||
{isLoading && <TableSkeleton columns={4} key="api-keys" />}
|
<TBody>
|
||||||
{!isLoading && data && data.length > 0 && data.map(({
|
{isLoading && <TableSkeleton columns={4} innerKey="api-keys" />}
|
||||||
_id,
|
{!isLoading &&
|
||||||
name,
|
data &&
|
||||||
createdAt,
|
data.length > 0 &&
|
||||||
expiresAt,
|
data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => {
|
||||||
lastUsed
|
return (
|
||||||
}) => {
|
<Tr className="h-10" key={`api-key-${_id}`}>
|
||||||
return (
|
<Td>{name}</Td>
|
||||||
<Tr className="h-10" key={`api-key-${_id}`}>
|
<Td>{formatDate(lastUsed)}</Td>
|
||||||
<Td>{name}</Td>
|
<Td>{formatDate(createdAt)}</Td>
|
||||||
<Td>{formatDate(lastUsed)}</Td>
|
<Td>{formatDate(expiresAt)}</Td>
|
||||||
<Td>{formatDate(createdAt)}</Td>
|
<Td>
|
||||||
<Td>{formatDate(expiresAt)}</Td>
|
<IconButton
|
||||||
<Td>
|
onClick={async () => {
|
||||||
<IconButton
|
await handleDeleteAPIKeyDataClick(_id);
|
||||||
onClick={async () => {
|
}}
|
||||||
await handleDeleteAPIKeyDataClick(_id);
|
size="lg"
|
||||||
}}
|
colorSchema="danger"
|
||||||
size="lg"
|
variant="plain"
|
||||||
colorSchema="danger"
|
ariaLabel="update"
|
||||||
variant="plain"
|
>
|
||||||
ariaLabel="update"
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
>
|
</IconButton>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
</Td>
|
||||||
</IconButton>
|
</Tr>
|
||||||
</Td>
|
);
|
||||||
</Tr>
|
})}
|
||||||
);
|
{!isLoading && data && data?.length === 0 && (
|
||||||
})}
|
<Tr>
|
||||||
{!isLoading && data && data?.length === 0 && (
|
<Td colSpan={5}>
|
||||||
<Tr>
|
<EmptyState title="No API Keys on file" icon={faKey} />
|
||||||
<Td colSpan={5}>
|
</Td>
|
||||||
<EmptyState
|
</Tr>
|
||||||
title="No API Keys on file"
|
)}
|
||||||
icon={faKey}
|
</TBody>
|
||||||
/>
|
</Table>
|
||||||
</Td>
|
</TableContainer>
|
||||||
</Tr>
|
</div>
|
||||||
)}
|
);
|
||||||
</TBody>
|
};
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+50
-57
@@ -14,61 +14,54 @@ import {
|
|||||||
import { useGetMySessions } from "@app/hooks/api";
|
import { useGetMySessions } from "@app/hooks/api";
|
||||||
|
|
||||||
export const SessionsTable = () => {
|
export const SessionsTable = () => {
|
||||||
const { data, isLoading } = useGetMySessions();
|
const { data, isLoading } = useGetMySessions();
|
||||||
|
|
||||||
const formatDate = (dateToFormat: string) => {
|
const formatDate = (dateToFormat: string) => {
|
||||||
const date = new Date(dateToFormat);
|
const date = new Date(dateToFormat);
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = date.getMonth() + 1;
|
const month = date.getMonth() + 1;
|
||||||
const day = date.getDate();
|
const day = date.getDate();
|
||||||
|
|
||||||
const formattedDate = `${day}/${month}/${year}`;
|
const formattedDate = `${day}/${month}/${year}`;
|
||||||
|
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Created</Th>
|
<Th>Created</Th>
|
||||||
<Th>Last active</Th>
|
<Th>Last active</Th>
|
||||||
<Th>IP address</Th>
|
<Th>IP address</Th>
|
||||||
<Th>Device</Th>
|
<Th>Device</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={4} key="sesssions" />}
|
{isLoading && <TableSkeleton columns={4} innerKey="sesssions" />}
|
||||||
{!isLoading && data && data.length > 0 && data.map(({
|
{!isLoading &&
|
||||||
_id,
|
data &&
|
||||||
createdAt,
|
data.length > 0 &&
|
||||||
lastUsed,
|
data.map(({ _id, createdAt, lastUsed, ip, userAgent }) => {
|
||||||
ip,
|
return (
|
||||||
userAgent
|
<Tr className="h-10" key={`session-${_id}`}>
|
||||||
}) => {
|
<Td>{formatDate(createdAt)}</Td>
|
||||||
return (
|
<Td>{formatDate(lastUsed)}</Td>
|
||||||
<Tr className="h-10" key={`session-${_id}`}>
|
<Td>{ip}</Td>
|
||||||
<Td>{formatDate(createdAt)}</Td>
|
<Td>{userAgent}</Td>
|
||||||
<Td>{formatDate(lastUsed)}</Td>
|
</Tr>
|
||||||
<Td>{ip}</Td>
|
);
|
||||||
<Td>{userAgent}</Td>
|
})}
|
||||||
</Tr>
|
{!isLoading && data && data?.length === 0 && (
|
||||||
);
|
<Tr>
|
||||||
})}
|
<Td colSpan={4}>
|
||||||
{!isLoading && data && data?.length === 0 && (
|
<EmptyState title="No sessions on file" icon={faServer} />
|
||||||
<Tr>
|
</Td>
|
||||||
<Td colSpan={4}>
|
</Tr>
|
||||||
<EmptyState
|
)}
|
||||||
title="No sessions on file"
|
</TBody>
|
||||||
icon={faServer}
|
</Table>
|
||||||
/>
|
</TableContainer>
|
||||||
</Td>
|
);
|
||||||
</Tr>
|
};
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+69
-69
@@ -11,79 +11,79 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>,
|
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
slug
|
slug
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EnvironmentTable = ({
|
export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||||
handlePopUpOpen
|
const { currentWorkspace, isLoading } = useWorkspace();
|
||||||
}: Props) => {
|
return (
|
||||||
const { currentWorkspace, isLoading } = useWorkspace();
|
<TableContainer>
|
||||||
return (
|
<Table>
|
||||||
<TableContainer>
|
<THead>
|
||||||
<Table>
|
<Tr>
|
||||||
<THead>
|
<Th>Name</Th>
|
||||||
<Tr>
|
<Th>Slug</Th>
|
||||||
<Th>Name</Th>
|
<Th aria-label="button" />
|
||||||
<Th>Slug</Th>
|
</Tr>
|
||||||
<Th aria-label="button" />
|
</THead>
|
||||||
</Tr>
|
<TBody>
|
||||||
</THead>
|
{isLoading && <TableSkeleton columns={3} innerKey="project-envs" />}
|
||||||
<TBody>
|
{!isLoading &&
|
||||||
{isLoading && <TableSkeleton columns={3} key="project-envs" />}
|
currentWorkspace &&
|
||||||
{!isLoading && currentWorkspace && currentWorkspace.environments.map(({ name, slug }) => (
|
currentWorkspace.environments.map(({ name, slug }) => (
|
||||||
<Tr key={name}>
|
<Tr key={name}>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
<Td>{slug}</Td>
|
<Td>{slug}</Td>
|
||||||
<Td className="flex items-center justify-end">
|
<Td className="flex items-center justify-end">
|
||||||
<IconButton
|
<IconButton
|
||||||
className="mr-3 py-2"
|
className="mr-3 py-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("updateEnv", { name, slug });
|
handlePopUpOpen("updateEnv", { name, slug });
|
||||||
}}
|
}}
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("deleteEnv", { name, slug });
|
handlePopUpOpen("deleteEnv", { name, slug });
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
))}
|
))}
|
||||||
{!isLoading && currentWorkspace && currentWorkspace.environments?.length === 0 && (
|
{!isLoading && currentWorkspace && currentWorkspace.environments?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={3}>
|
<Td colSpan={3}>
|
||||||
<EmptyState title="No environments found" />
|
<EmptyState title="No environments found" />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+57
-57
@@ -18,65 +18,65 @@ import { useGetWsTags } from "@app/hooks/api";
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["deleteTagConfirmation"]>,
|
popUpName: keyof UsePopUpState<["deleteTagConfirmation"]>,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
id
|
id
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretTagsTable = ({
|
export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||||
handlePopUpOpen
|
const { currentWorkspace } = useWorkspace();
|
||||||
}: Props) => {
|
const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? "");
|
||||||
const { currentWorkspace }= useWorkspace();
|
|
||||||
const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Tag</Th>
|
<Th>Tag</Th>
|
||||||
<Th>Slug</Th>
|
<Th>Slug</Th>
|
||||||
<Th aria-label="button" />
|
<Th aria-label="button" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={3} key="secret-tags" />}
|
{isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />}
|
||||||
{!isLoading && data && data.map(({ _id, name, slug }) => (
|
{!isLoading &&
|
||||||
<Tr key={name}>
|
data &&
|
||||||
<Td>{name}</Td>
|
data.map(({ _id, name, slug }) => (
|
||||||
<Td>{slug}</Td>
|
<Tr key={name}>
|
||||||
<Td className="flex items-center justify-end">
|
<Td>{name}</Td>
|
||||||
<IconButton
|
<Td>{slug}</Td>
|
||||||
onClick={() =>
|
<Td className="flex items-center justify-end">
|
||||||
handlePopUpOpen("deleteTagConfirmation", {
|
<IconButton
|
||||||
name,
|
onClick={() =>
|
||||||
id: _id
|
handlePopUpOpen("deleteTagConfirmation", {
|
||||||
})
|
name,
|
||||||
}
|
id: _id
|
||||||
colorSchema="danger"
|
})
|
||||||
ariaLabel="update"
|
}
|
||||||
>
|
colorSchema="danger"
|
||||||
<FontAwesomeIcon icon={faTrashCan} />
|
ariaLabel="update"
|
||||||
</IconButton>
|
>
|
||||||
</Td>
|
<FontAwesomeIcon icon={faTrashCan} />
|
||||||
</Tr>
|
</IconButton>
|
||||||
))}
|
|
||||||
{!isLoading && data && data?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={3}>
|
|
||||||
<EmptyState title="No secret tags found" icon={faTags} />
|
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
))}
|
||||||
</TBody>
|
{!isLoading && data && data?.length === 0 && (
|
||||||
</Table>
|
<Tr>
|
||||||
</TableContainer>
|
<Td colSpan={3}>
|
||||||
);
|
<EmptyState title="No secret tags found" icon={faTags} />
|
||||||
}
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
|
{isLoading && <TableSkeleton columns={4} innerKey="project-service-tokens" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data &&
|
data &&
|
||||||
data.map((row) => (
|
data.map((row) => (
|
||||||
|
|||||||
+1
-1
@@ -160,7 +160,7 @@ export const WebhooksTab = () => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isWebhooksLoading && <TableSkeleton columns={5} key="webhooks-loading" />}
|
{isWebhooksLoading && <TableSkeleton columns={5} innerKey="webhooks-loading" />}
|
||||||
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
|
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={5}>
|
<Td colSpan={5}>
|
||||||
|
|||||||
Reference in New Issue
Block a user