mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 14:34:09 +00:00
feat: renamed old frontend and fixed some minor bugs
This commit is contained in:
@@ -1,92 +0,0 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, Checkbox, PopoverContent } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
|
||||
import { WsTag } from "../../hooks/api/tags/types";
|
||||
import { ProjectPermissionCan } from "../permissions";
|
||||
|
||||
interface Props {
|
||||
wsTags: WsTag[] | undefined;
|
||||
secKey: string;
|
||||
selectedTagIds: Record<string, boolean>;
|
||||
handleSelectTag: (wsTag: WsTag) => void;
|
||||
handleTagOnMouseEnter: (wsTag: WsTag) => void;
|
||||
handleTagOnMouseLeave: () => void;
|
||||
checkIfTagIsVisible: (wsTag: WsTag) => boolean;
|
||||
handleOnCreateTagOpen: () => void;
|
||||
}
|
||||
|
||||
const AddTagPopoverContent = ({
|
||||
wsTags,
|
||||
secKey,
|
||||
selectedTagIds,
|
||||
handleSelectTag,
|
||||
handleTagOnMouseEnter,
|
||||
handleTagOnMouseLeave,
|
||||
checkIfTagIsVisible,
|
||||
handleOnCreateTagOpen
|
||||
}: Props) => {
|
||||
return (
|
||||
<PopoverContent
|
||||
side="left"
|
||||
className="relative max-h-96 w-auto min-w-[200px] overflow-y-auto overflow-x-hidden border border-mineshaft-600 bg-mineshaft-800 p-2 text-bunker-200"
|
||||
hideCloseBtn
|
||||
>
|
||||
<div className=" text-center text-sm font-medium text-bunker-200">
|
||||
Add tags to {secKey || "this secret"}
|
||||
</div>
|
||||
<div className="absolute left-0 mt-2 w-full border-t border-mineshaft-600" />
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
{wsTags?.map((wsTag: WsTag) => (
|
||||
<div
|
||||
key={`tag-${wsTag.id}`}
|
||||
className="relative mt-4 flex h-[32px] items-center justify-start rounded-md bg-none p-2 hover:border hover:border-mineshaft-600 hover:bg-mineshaft-700 hover:text-bunker-200"
|
||||
onClick={() => handleSelectTag(wsTag)}
|
||||
onMouseEnter={() => handleTagOnMouseEnter(wsTag)}
|
||||
onMouseLeave={() => handleTagOnMouseLeave()}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onKeyDown={() => {}}
|
||||
>
|
||||
{(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && (
|
||||
<Checkbox
|
||||
id="autoCapitalization"
|
||||
isChecked={selectedTagIds?.[wsTag.slug]}
|
||||
className="absolute top-[50%] left-[10px] translate-y-[-50%] "
|
||||
checkIndicatorBg={`${
|
||||
!selectedTagIds?.[wsTag.slug] ? "text-transparent" : "text-mineshaft-800"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<div className="ml-7 flex items-center gap-3">
|
||||
<div
|
||||
className="h-[10px] w-[10px] rounded-full"
|
||||
style={{ background: wsTag?.color ? wsTag.color : "#bec2c8" }}
|
||||
>
|
||||
{" "}
|
||||
</div>
|
||||
<span>{wsTag.slug}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handleOnCreateTagOpen()}
|
||||
isDisabled={!isAllowed}
|
||||
size="xs"
|
||||
className="mt-2"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} className="ml-1 mr-2" />}
|
||||
>
|
||||
Add new tag
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTagPopoverContent;
|
||||
@@ -1,72 +0,0 @@
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { publicPaths } from "@app/const";
|
||||
import checkAuth from "@app/pages/api/auth/CheckAuth";
|
||||
|
||||
// #TODO: finish spinner only when the data loads fully
|
||||
// #TODO: Redirect somewhere if the page does not exist
|
||||
|
||||
type Prop = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function RouteGuard({ children }: Prop): JSX.Element {
|
||||
const router = useRouter();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [authorized, setAuthorized] = useState(false);
|
||||
|
||||
/**
|
||||
* redirect to login page if accessing a private page and not logged in
|
||||
*/
|
||||
async function authCheck(url: string) {
|
||||
// Make sure that we don't redirect when the user is on the following pages.
|
||||
const path = `/${url.split("?")[0].split("/")[1]}`;
|
||||
|
||||
// Check if the user is authenticated
|
||||
const response = await checkAuth();
|
||||
// #TODO: figure our why sometimes it doesn't output a response
|
||||
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
|
||||
if (!publicPaths.includes(path)) {
|
||||
try {
|
||||
if (response.status !== 200) {
|
||||
router.push("/login");
|
||||
console.log("Unauthorized to access.");
|
||||
setAuthorized(false);
|
||||
} else {
|
||||
setAuthorized(true);
|
||||
console.log("Authorized to access.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error (probably the authCheck route is stuck again...):", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// on initial load - run auth check
|
||||
(async () => {
|
||||
await authCheck(router.asPath);
|
||||
})();
|
||||
|
||||
// on route change start - hide page content by setting authorized to false
|
||||
// #TODO: add the loading page when not yet authorized.
|
||||
const hideContent = () => setAuthorized(false);
|
||||
// const onError = () => setAuthorized(true)
|
||||
router.events.on("routeChangeStart", hideContent);
|
||||
// router.events.on("routeChangeError", onError);
|
||||
|
||||
// on route change complete - run auth check
|
||||
router.events.on("routeChangeComplete", authCheck);
|
||||
|
||||
// unsubscribe from events in useEffect return function
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", hideContent);
|
||||
router.events.off("routeChangeComplete", authCheck);
|
||||
// router.events.off("routeChangeError", onError);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return children as JSX.Element;
|
||||
}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import ReactCodeInput from "react-code-input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function CodeInputStep({
|
||||
fields={6}
|
||||
onChange={setCode}
|
||||
{...props}
|
||||
className="mt-6 mb-2"
|
||||
className="mb-2 mt-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-4 block w-max md:hidden">
|
||||
@@ -108,7 +108,7 @@ export default function CodeInputStep({
|
||||
fields={6}
|
||||
onChange={setCode}
|
||||
{...propsPhone}
|
||||
className="mt-2 mb-2"
|
||||
className="mb-2 mt-2"
|
||||
/>
|
||||
</div>
|
||||
{codeError && <Error text={t("signup.step2-code-error")} />}
|
||||
+4
-4
@@ -32,7 +32,7 @@ export default function DonwloadBackupPDFStep({
|
||||
name
|
||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
|
||||
const handleBackupKeyGenerate = async () => {
|
||||
@@ -61,17 +61,17 @@ export default function DonwloadBackupPDFStep({
|
||||
<p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||
<FontAwesomeIcon
|
||||
icon={faWarning}
|
||||
className="ml-2 mr-3 mb-6 pt-1 text-6xl text-bunker-200"
|
||||
className="mb-6 ml-2 mr-3 pt-1 text-6xl text-bunker-200"
|
||||
/>
|
||||
{t("signup.step4-message")}
|
||||
</p>
|
||||
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
|
||||
<div className="m-2 mx-auto mt-4 flex w-full w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6 lg:w-1/6">
|
||||
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
|
||||
<span className="mb-2">
|
||||
{t("signup.step4-description1")} {t("signup.step4-description3")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx-auto mt-0 mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mt-4 md:mb-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
|
||||
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
|
||||
<div className="text-l w-full py-1 text-lg">
|
||||
<Button
|
||||
onClick={handleBackupKeyGenerate}
|
||||
+7
-8
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import axios from "axios";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -27,8 +27,7 @@ export default function EnterEmailStep({
|
||||
setEmail,
|
||||
incrementStep
|
||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
||||
|
||||
const { mutateAsync, isLoading } = useSendVerificationEmail();
|
||||
const { mutateAsync, isPending } = useSendVerificationEmail();
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -51,7 +50,7 @@ export default function EnterEmailStep({
|
||||
if (!emailCheckBool) {
|
||||
try {
|
||||
await mutateAsync({ email: email.toLowerCase() });
|
||||
setEmail(email.toLowerCase())
|
||||
setEmail(email.toLowerCase());
|
||||
incrementStep();
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
@@ -96,8 +95,8 @@ export default function EnterEmailStep({
|
||||
className="h-14"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
isLoading={isPending}
|
||||
isDisabled={isPending}
|
||||
>
|
||||
{" "}
|
||||
{String(t("signup.step1-submit"))}{" "}
|
||||
@@ -106,7 +105,7 @@ export default function EnterEmailStep({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mb-48 mt-2 flex w-full max-w-md flex-col items-center justify-center pt-2 md:mb-16 md:pb-2">
|
||||
<Link href="/login">
|
||||
<Link to="/login">
|
||||
<button type="button" className="w-max pb-3 duration-200 hover:opacity-90">
|
||||
<span className="cursor-pointer text-sm text-mineshaft-400 duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
{t("signup.already-have-account")}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { RegionSelect } from "@app/components/navigation/RegionSelect";
|
||||
import { useServerConfig } from "@app/context";
|
||||
@@ -94,7 +94,7 @@ export default function InitialSignupStep({
|
||||
{t("signup.create-policy")}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-row text-xs text-bunker-400">
|
||||
<Link href="/login">
|
||||
<Link to="/login">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
{t("signup.already-have-account")}
|
||||
</span>
|
||||
@@ -0,0 +1,222 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ReactCodeInput from "react-code-input";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { t } from "i18next";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import TotpRegistration from "@app/components/mfa/TotpRegistration";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { useSendMfaToken } from "@app/hooks/api";
|
||||
import { checkUserTotpMfa, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
|
||||
// The style for the verification code input
|
||||
const codeInputProps = {
|
||||
inputStyle: {
|
||||
fontFamily: "monospace",
|
||||
margin: "4px",
|
||||
MozAppearance: "textfield",
|
||||
width: "48px",
|
||||
borderRadius: "5px",
|
||||
fontSize: "24px",
|
||||
height: "48px",
|
||||
paddingLeft: "7",
|
||||
backgroundColor: "#0d1117",
|
||||
color: "white",
|
||||
border: "1px solid #2d2f33",
|
||||
textAlign: "center",
|
||||
outlineColor: "#8ca542",
|
||||
borderColor: "#2d2f33"
|
||||
}
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
successCallback: () => void | Promise<void>;
|
||||
closeMfa?: () => void;
|
||||
hideLogo?: boolean;
|
||||
email: string;
|
||||
method: MfaMethod;
|
||||
};
|
||||
|
||||
export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Props) => {
|
||||
const [mfaCode, setMfaCode] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
|
||||
const [shouldShowTotpRegistration, setShouldShowTotpRegistration] = useState(false);
|
||||
|
||||
const sendMfaToken = useSendMfaToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (method === MfaMethod.TOTP) {
|
||||
checkUserTotpMfa().then((isVerified) => {
|
||||
if (!isVerified) {
|
||||
SecurityClient.setMfaToken("");
|
||||
setShouldShowTotpRegistration(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const verifyMfa = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { token } = await verifyMfaToken({
|
||||
email,
|
||||
mfaCode,
|
||||
mfaMethod: method
|
||||
});
|
||||
|
||||
SecurityClient.setMfaToken("");
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
await successCallback();
|
||||
if (closeMfa) {
|
||||
closeMfa();
|
||||
}
|
||||
} catch {
|
||||
if (triesLeft) {
|
||||
setTriesLeft((left) => {
|
||||
if (triesLeft === 1) {
|
||||
navigate({ to: "/" });
|
||||
|
||||
SecurityClient.setMfaToken("");
|
||||
SecurityClient.setToken("");
|
||||
}
|
||||
return (left as number) - 1;
|
||||
});
|
||||
} else {
|
||||
setTriesLeft(2);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendMfaCode = async () => {
|
||||
try {
|
||||
setIsLoadingResend(true);
|
||||
await sendMfaToken.mutateAsync({ email });
|
||||
setIsLoadingResend(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setIsLoadingResend(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (shouldShowTotpRegistration) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-center text-lg font-bold text-white">
|
||||
Your organization requires mobile authentication to be configured.
|
||||
</div>
|
||||
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||
<TotpRegistration
|
||||
shouldCenterQr
|
||||
onComplete={async () => {
|
||||
setShouldShowTotpRegistration(false);
|
||||
await successCallback();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||
{!hideLogo && (
|
||||
<Link href="/">
|
||||
<div className="mb-4 flex justify-center">
|
||||
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
{method === MfaMethod.EMAIL && (
|
||||
<>
|
||||
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
|
||||
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email}</p>
|
||||
</>
|
||||
)}
|
||||
{method === MfaMethod.TOTP && (
|
||||
<>
|
||||
<p className="text-l mb-4 flex max-w-xs justify-center text-center font-bold text-bunker-100">
|
||||
Authenticator MFA Required
|
||||
</p>
|
||||
<p className="text-l flex max-w-xs justify-center text-center text-bunker-300">
|
||||
Open the authenticator app on your mobile device to get your verification code or enter
|
||||
a recovery code.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<form onSubmit={verifyMfa}>
|
||||
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
||||
{method === MfaMethod.EMAIL && (
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setMfaCode}
|
||||
className="mb-2 mt-6"
|
||||
{...codeInputProps}
|
||||
/>
|
||||
)}
|
||||
{method === MfaMethod.TOTP && (
|
||||
<div className="mb-4 mt-6">
|
||||
<Input value={mfaCode} onChange={(e) => setMfaCode(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{typeof triesLeft === "number" && (
|
||||
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
||||
)}
|
||||
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||
<div className="text-l w-full py-1 text-lg">
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
isFullWidth
|
||||
className="h-14"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{String(t("mfa.verify"))}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{method === MfaMethod.TOTP && (
|
||||
<div className="mt-2 flex flex-row justify-center text-sm text-bunker-400">
|
||||
<Link href="/verify-email">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
Lost your recovery codes? Reset your account
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{method === MfaMethod.EMAIL && (
|
||||
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
||||
<div className="text-md mt-2 flex flex-row text-bunker-400">
|
||||
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
{isLoadingResend
|
||||
? t("signup.step2-resend-progress")
|
||||
: t("signup.step2-resend-submit")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { useAddUsersToOrg } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
@@ -14,7 +14,7 @@ import { Button, EmailServiceSetupModal } from "../v2";
|
||||
*/
|
||||
export default function TeamInviteStep(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const [emails, setEmails] = useState("");
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
|
||||
// Redirect user to the getting started page
|
||||
const redirectToHome = async () => {
|
||||
router.push(`/org/${localStorage.getItem("orgData.id")}/${ProjectType.SecretManager}/overview`);
|
||||
navigate({ to: `/organization/${ProjectType.SecretManager}/overview` as const });
|
||||
};
|
||||
|
||||
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
|
||||
@@ -49,19 +49,19 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
<p className="mb-6 mt-4 flex justify-center text-center text-bunker-400 md:mx-8">
|
||||
{t("signup.step5-subtitle")}
|
||||
</p>
|
||||
<div className="mx-auto mb-6 w-max rounded-xl border border-mineshaft-500 bg-mineshaft-800 px-8 pt-6 pb-4 drop-shadow-xl">
|
||||
<div className="mx-auto mb-6 w-max rounded-xl border border-mineshaft-500 bg-mineshaft-800 px-8 pb-4 pt-6 drop-shadow-xl">
|
||||
<div>
|
||||
<div className="pl-1 pb-1 text-sm font-medium text-bunker-300">
|
||||
<div className="pb-1 pl-1 text-sm font-medium text-bunker-300">
|
||||
<span>Emails</span>
|
||||
</div>
|
||||
<textarea
|
||||
className="h-20 w-full min-w-[30rem] rounded-md border border-mineshaft-500 bg-mineshaft-900/70 py-1 px-2 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 placeholder:text-bunker-400 focus:ring-2"
|
||||
className="h-20 w-full min-w-[30rem] rounded-md border border-mineshaft-500 bg-mineshaft-900/70 px-2 py-1 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 placeholder:text-bunker-400 focus:ring-2"
|
||||
value={emails}
|
||||
onChange={(e) => setEmails(e.target.value)}
|
||||
placeholder="[email protected], [email protected]..."
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-2 flex w-full flex-row items-end justify-end text-sm md:mt-4 md:mb-2 md:min-w-[30rem] md:max-w-md">
|
||||
<div className="mx-auto mt-2 flex w-full flex-row items-end justify-end text-sm md:mb-2 md:mt-4 md:min-w-[30rem] md:max-w-md">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (serverDetails?.emailConfigured) {
|
||||
@@ -85,7 +85,7 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-28 mx-auto mt-4 mb-2 flex max-h-24 min-w-[20rem] max-w-max flex-row items-center justify-center px-4 text-lg md:p-2">
|
||||
<div className="mx-auto mb-2 mt-4 flex max-h-24 min-w-[20rem] max-w-max flex-row items-center justify-center px-4 text-lg md:p-2">
|
||||
<Button
|
||||
onClick={redirectToHome}
|
||||
size="sm"
|
||||
+10
-11
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -8,9 +8,9 @@ import jsrp from "jsrp";
|
||||
import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import { initProjectHelper } from "@app/helpers/project";
|
||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import ProjectService from "@app/services/ProjectService";
|
||||
|
||||
import InputField from "../basic/InputField";
|
||||
import checkPassword from "../utilities/checks/password/checkPassword";
|
||||
@@ -121,7 +121,7 @@ export default function UserInfoStep({
|
||||
password
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
|
||||
try {
|
||||
// TODO: moduralize into KeyService
|
||||
const derivedKey = await deriveArgonKey({
|
||||
@@ -198,12 +198,11 @@ export default function UserInfoStep({
|
||||
const userOrgs = await fetchOrganizations();
|
||||
|
||||
const orgId = userOrgs[0]?.id;
|
||||
const project = await ProjectService.initProject({
|
||||
await initProjectHelper({
|
||||
projectName: "Example Project"
|
||||
});
|
||||
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
localStorage.setItem("projectData.id", project.id);
|
||||
|
||||
incrementStep();
|
||||
} catch (error) {
|
||||
@@ -224,7 +223,7 @@ export default function UserInfoStep({
|
||||
{t("signup.step3-message")}
|
||||
</p>
|
||||
<div className="mx-auto mb-36 h-full w-max rounded-xl py-6 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800 md:px-8">
|
||||
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
|
||||
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
|
||||
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
|
||||
Your Name
|
||||
</p>
|
||||
@@ -237,12 +236,12 @@ export default function UserInfoStep({
|
||||
className="h-12"
|
||||
/>
|
||||
{nameError && (
|
||||
<p className="mt-1 ml-1 w-full text-left text-xs text-red-600">
|
||||
<p className="ml-1 mt-1 w-full text-left text-xs text-red-600">
|
||||
Please, specify your name
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
|
||||
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
|
||||
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
|
||||
Organization Name
|
||||
</p>
|
||||
@@ -255,12 +254,12 @@ export default function UserInfoStep({
|
||||
className="h-12"
|
||||
/>
|
||||
{organizationNameError && (
|
||||
<p className="mt-1 ml-1 w-full text-left text-xs text-red-600">
|
||||
<p className="ml-1 mt-1 w-full text-left text-xs text-red-600">
|
||||
Please, specify your organization name
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
|
||||
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
|
||||
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
|
||||
Where did you hear about us? <span className="font-light">(optional)</span>
|
||||
</p>
|
||||
@@ -271,7 +270,7 @@ export default function UserInfoStep({
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex max-h-60 w-1/4 w-full min-w-[20rem] flex-col items-center justify-center rounded-lg py-2 lg:w-1/6">
|
||||
<div className="mt-2 flex max-h-60 w-full min-w-[20rem] flex-col items-center justify-center rounded-lg py-2 lg:w-1/6">
|
||||
<InputField
|
||||
label={t("section.password.password")}
|
||||
onChangeHandler={async (pass: string) => {
|
||||
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
const Error = ({ text }: { text: string }): JSX.Element => {
|
||||
return (
|
||||
<div className="relative m-auto flex w-fit flex-row items-center justify-center rounded-full">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} className="mx-2 mt-1.5 mb-2 text-red" />
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} className="mx-2 mb-2 mt-1.5 text-red" />
|
||||
{text && <p className="relative top-0 mr-2 py-1 text-sm text-red">{text}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ const InputField = ({
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
required={isRequired}
|
||||
className="text-md min-w-16 w-full rounded-md border border-gray-600 bg-bunker-800 p-2 text-gray-400 outline-none"
|
||||
className="text-md w-full min-w-16 rounded-md border border-gray-600 bg-bunker-800 p-2 text-gray-400 outline-none"
|
||||
name={name}
|
||||
readOnly
|
||||
autoComplete={autoComplete}
|
||||
@@ -79,7 +79,7 @@ const InputField = ({
|
||||
: ""
|
||||
} ${
|
||||
error ? "focus:ring-red/50" : "focus:ring-primary/50"
|
||||
} text-md min-w-16 peer relative w-full rounded-md bg-mineshaft-900 p-2 text-gray-400 outline-none duration-200 focus:ring-4`}
|
||||
} text-md peer relative w-full min-w-16 rounded-md bg-mineshaft-900 p-2 text-gray-400 outline-none duration-200 focus:ring-4`}
|
||||
name={name}
|
||||
spellCheck="false"
|
||||
autoComplete={autoComplete}
|
||||
@@ -121,7 +121,7 @@ const InputField = ({
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
{error && <p className="mx-0 mt-0.5 mb-2 max-w-xs text-xs text-red">{errorText}</p>}
|
||||
{error && <p className="mx-0 mb-2 mt-0.5 max-w-xs text-xs text-red">{errorText}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { faAngleDown, faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
|
||||
interface ListBoxProps {
|
||||
isSelected: string;
|
||||
onChange: (arg: string) => void;
|
||||
data: string[] | null;
|
||||
text?: string;
|
||||
buttonAction?: () => void;
|
||||
isFull?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the component that we use for drop down lists.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.isSelected - the item that is currently selected
|
||||
* @param {function} obj.onChange - what happends if you select the item inside a list
|
||||
* @param {string[]} obj.data - all the options available
|
||||
* @param {string} obj.text - the text that shows us in front of the select option
|
||||
* @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button
|
||||
* @returns
|
||||
*/
|
||||
const ListBox = ({
|
||||
isSelected,
|
||||
onChange,
|
||||
data,
|
||||
text,
|
||||
buttonAction,
|
||||
isFull
|
||||
}: ListBoxProps): JSX.Element => {
|
||||
return (
|
||||
<Listbox value={isSelected} onChange={onChange}>
|
||||
<div className="relative">
|
||||
<Listbox.Button
|
||||
className={`relative text-gray-400 ${
|
||||
isFull ? "w-full" : "w-52"
|
||||
} focus-visible:ring-offset-orange-300 cursor-default rounded-md bg-white/[0.07] py-2.5 pl-3 pr-10 text-left shadow-md duration-200 hover:bg-white/[0.11] focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 sm:text-sm`}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
{text}
|
||||
<span className="ml-1 block cursor-pointer truncate font-semibold text-gray-300">
|
||||
{" "}
|
||||
{isSelected}
|
||||
</span>
|
||||
</div>
|
||||
{data && (
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex cursor-pointer items-center pr-2">
|
||||
<FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Button>
|
||||
{data && (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="no-scrollbar::-webkit-scrollbar absolute z-[70] mt-1 max-h-60 w-full overflow-auto rounded-md border border-mineshaft-700 bg-bunker p-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 no-scrollbar focus:outline-none sm:text-sm">
|
||||
{data.map((person, personIdx) => (
|
||||
<Listbox.Option
|
||||
key={`${person}.${personIdx + 1}`}
|
||||
className={({ active, selected }) =>
|
||||
`relative my-0.5 cursor-default select-none rounded-md py-2 pl-10 pr-4 ${
|
||||
selected ? "bg-white/10 font-bold text-gray-400" : ""
|
||||
} ${
|
||||
active && !selected
|
||||
? "cursor-pointer bg-white/5 text-mineshaft-200"
|
||||
: "text-gray-400"
|
||||
} `
|
||||
}
|
||||
value={person}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate text-primary${
|
||||
selected ? "font-medium" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{person}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 text-primary">
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
{buttonAction && (
|
||||
<button type="button" onClick={buttonAction} className="w-full cursor-pointer">
|
||||
<div className="relative my-0.5 mt-2 flex cursor-pointer select-none justify-start rounded-md py-2 pl-10 pr-4 text-gray-400 duration-200 hover:bg-lime-300 hover:font-semibold hover:text-black">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 pr-4">
|
||||
<FontAwesomeIcon icon={faPlus} className="text-lg" />
|
||||
</span>
|
||||
Add Project
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
)}
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListBox;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
interface ToggleProps {
|
||||
enabled: boolean;
|
||||
setEnabled: (value: boolean) => void;
|
||||
addOverride: (value: string | undefined, id: string) => void;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a typical 'iPhone' toggle (e.g., user for overriding secrets with personal values)
|
||||
* @param obj
|
||||
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
||||
* @param {function} obj.setEnabled - change the state of the toggle
|
||||
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
||||
* @param {number} obj.id - id of a certain secret
|
||||
* @returns
|
||||
*/
|
||||
const Toggle = ({ enabled, setEnabled, addOverride, id }: ToggleProps): JSX.Element => {
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (enabled === false) {
|
||||
addOverride("", id);
|
||||
} else {
|
||||
addOverride(undefined, id);
|
||||
}
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
className={`${
|
||||
enabled ? "bg-primary" : "bg-bunker-400"
|
||||
} relative inline-flex h-5 w-9 items-center rounded-full`}
|
||||
>
|
||||
<span className="sr-only">Enable notifications</span>
|
||||
<span
|
||||
className={`${
|
||||
enabled ? "translate-x-[1.26rem]" : "translate-x-0.5"
|
||||
} inline-block h-3.5 w-3.5 transform rounded-full bg-bunker-800 transition`}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toggle;
|
||||
@@ -1,148 +0,0 @@
|
||||
/* eslint-disable react/button-has-type */
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import Image from "next/image";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const classNames = require("classnames");
|
||||
|
||||
type ButtonProps = {
|
||||
text?: string;
|
||||
onButtonPressed: () => void;
|
||||
loading?: boolean;
|
||||
color?: string;
|
||||
size: string;
|
||||
icon?: IconProp;
|
||||
active?: boolean;
|
||||
iconDisabled?: IconProp;
|
||||
textDisabled?: string;
|
||||
type?: ButtonHTMLAttributes<any>["type"];
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the main butto component in the app.
|
||||
* @param {object} props
|
||||
* @param {string} props.text - text inside the button
|
||||
* @param {function} props.onButtonPressed - the action that happens when the button is clicked
|
||||
* @param {boolean} props.loading - if a button is currently in the laoding state
|
||||
* @param {string} props.color - button color
|
||||
* @param {string} props.size - button size
|
||||
* @param {FontAwesomeIconProps} props.icon - the icon inside the button
|
||||
* @param {boolean} props.active - if the button is active or disabled
|
||||
* @param {FontAwesomeIconProps} props.text - the icon inside the button when it is disabled
|
||||
* @param {string} props.textDisable - text inside the button when it is disabled
|
||||
* @returns
|
||||
*/
|
||||
const Button = ({
|
||||
active,
|
||||
text,
|
||||
textDisabled,
|
||||
color,
|
||||
size,
|
||||
onButtonPressed,
|
||||
loading,
|
||||
icon,
|
||||
iconDisabled,
|
||||
type = "button"
|
||||
}: ButtonProps): JSX.Element => {
|
||||
// Check if the button show always be 'active' - then true;
|
||||
// or if it should switch between 'active' and 'disabled' - then give the status
|
||||
const activityStatus = active || (text !== "" && textDisabled === undefined);
|
||||
|
||||
const styleButton = classNames(
|
||||
"group m-auto md:m-0 inline-block rounded-md duration-200",
|
||||
|
||||
// Setting background colors and hover modes
|
||||
color === "mineshaft" &&
|
||||
activityStatus &&
|
||||
"bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60",
|
||||
color === "mineshaft" && !activityStatus && "bg-mineshaft",
|
||||
(color === "primary" || !color) &&
|
||||
activityStatus &&
|
||||
"bg-primary border border-primary-400 opacity-80 hover:opacity-100",
|
||||
(color === "primary" || !color) && !activityStatus && "bg-primary",
|
||||
color === "red" && "bg-red-800 border border-red",
|
||||
|
||||
// Changing the opacity when active vs when not
|
||||
activityStatus ? "opacity-100 cursor-pointer" : "opacity-40",
|
||||
|
||||
// Setting the button sizes
|
||||
size === "md" && "h-10 w-full px-2 md:px-4",
|
||||
size === "lg" && "h-12 w-full px-2 md:px-8",
|
||||
!size && "md:py-1 px-3 md:px-8",
|
||||
size === "icon-md" && "h-10 w-10 flex items-center justify-center",
|
||||
size === "icon-sm" && "h-9 w-9 flex items-center justify-center"
|
||||
);
|
||||
|
||||
const styleMainDiv = classNames(
|
||||
"relative font-medium flex items-center",
|
||||
|
||||
// Setting the text color for the text and icon
|
||||
color === "mineshaft" && "text-bunker-200",
|
||||
color !== "mineshaft" && color !== "red" && color !== "none" && "text-black",
|
||||
color === "red" && "text-gray-200",
|
||||
color === "none" && "text-gray-200 text-xl",
|
||||
activityStatus && color !== "red" && color !== "mineshaft" && color !== "none"
|
||||
? "group-hover:text-black"
|
||||
: "",
|
||||
|
||||
size === "icon" && "flex items-center justify-center"
|
||||
);
|
||||
|
||||
const textStyle = classNames(
|
||||
"relative duration-200 text-center w-full",
|
||||
|
||||
// Show the loading sign if the loading indicator is on
|
||||
loading ? "opacity-0" : "opacity-100",
|
||||
size === "md" && "text-sm",
|
||||
size === "lg" && "text-lg"
|
||||
);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
disabled={!activityStatus}
|
||||
type={type}
|
||||
onClick={onButtonPressed}
|
||||
className={styleButton}
|
||||
>
|
||||
<div className={styleMainDiv}>
|
||||
<div
|
||||
className={`${
|
||||
loading === true ? "opacity-100" : "opacity-0"
|
||||
} absolute flex w-full items-center bg-primary px-3 duration-200`}
|
||||
>
|
||||
<Image
|
||||
src="/images/loading/loadingblack.gif"
|
||||
height={25}
|
||||
width={42}
|
||||
alt="loading animation"
|
||||
className="rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
{icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
className={`my-auto flex font-extrabold ${size === "icon-sm" ? "text-sm" : "text-sm"} ${
|
||||
(text || textDisabled) && "mr-2"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{iconDisabled && (
|
||||
<FontAwesomeIcon
|
||||
icon={iconDisabled as IconProp}
|
||||
className={`my-auto flex font-extrabold ${size === "icon-sm" ? "text-sm" : "text-md"} ${
|
||||
(text || textDisabled) && "mr-2"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{(text || textDisabled) && (
|
||||
<p className={textStyle}>{activityStatus ? text : textDisabled}</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -1,100 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
selectedIntegrationOption: IntegrationOption | null;
|
||||
integrationOptionPress: (integrationOption: IntegrationOption) => void;
|
||||
};
|
||||
|
||||
const ActivateBotDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
integrationOptionPress
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// type check
|
||||
if (!selectedIntegrationOption) return;
|
||||
|
||||
// start integration or probe for PAT
|
||||
integrationOptionPress(selectedIntegrationOption);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
{t("integrations.grant-access-to-secrets")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("integrations.why-infisical-needs-access")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 max-w-max">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text={t("integrations.grant-access-button") as string}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivateBotDialog;
|
||||
@@ -1,149 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
import ListBox from "../Listbox";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: () => void;
|
||||
data: any;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
};
|
||||
|
||||
const AddProjectMemberDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
submitModal,
|
||||
data,
|
||||
email,
|
||||
setEmail
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="z-50">
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
{data?.length > 0 ? (
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
{t("section.members.add-dialog.add-member-to-project")}
|
||||
</Dialog.Title>
|
||||
) : (
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="z-50 mb-4 text-lg font-medium text-mineshaft-300"
|
||||
>
|
||||
{t("section.members.add-dialog.already-all-invited")}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
<div className="mt-2 mb-4">
|
||||
{data?.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("section.members.add-dialog.user-will-email")}
|
||||
</p>
|
||||
<div className="">
|
||||
<Trans
|
||||
i18nKey="section.members.add-dialog.looking-add"
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() => router.push(`/org/${router.query.id}/members`)}
|
||||
aria-label="add member"
|
||||
/>,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() =>
|
||||
router.push(`/org/${router.query.id}/members?action=invite`)
|
||||
}
|
||||
aria-label="add member"
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("section.members.add-dialog.add-user-org-first")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
{data?.length > 0 && (
|
||||
<ListBox
|
||||
isSelected={email || data[0]}
|
||||
onChange={setEmail}
|
||||
data={data}
|
||||
isFull
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-max">
|
||||
{data?.length > 0 ? (
|
||||
<div className="mt-6 flex w-max flex-col justify-start">
|
||||
<Button
|
||||
onButtonPressed={submitModal}
|
||||
color="mineshaft"
|
||||
text={t("section.members.add-member") as string}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onButtonPressed={() =>
|
||||
router.push(`/org/${localStorage.getItem("orgData.id")}/members`)
|
||||
}
|
||||
color="mineshaft"
|
||||
text={t("section.members.add-dialog.add-user-to-org") as string}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddProjectMemberDialog;
|
||||
@@ -1,140 +0,0 @@
|
||||
import { FormEventHandler, Fragment, useEffect, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
|
||||
type FormFields = { name: string; slug: string };
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
isEditMode?: boolean;
|
||||
// on edit mode load up initial values
|
||||
initialValues?: FormFields;
|
||||
onClose: () => void;
|
||||
onCreateSubmit: (data: FormFields) => void;
|
||||
onEditSubmit: (data: FormFields) => void;
|
||||
};
|
||||
|
||||
// TODO: Migrate to better form management and validation. Preferable react-hook-form + yup
|
||||
/**
|
||||
* The dialog modal for when the user wants to create a new workspace
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const AddUpdateEnvironmentDialog = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCreateSubmit,
|
||||
onEditSubmit,
|
||||
initialValues,
|
||||
isEditMode
|
||||
}: Props) => {
|
||||
const [formInput, setFormInput] = useState<FormFields>({
|
||||
name: "",
|
||||
slug: ""
|
||||
});
|
||||
|
||||
// This use effect can be removed when the unmount is happening from outside the component
|
||||
// When unmount happens outside state gets unmounted also
|
||||
useEffect(() => {
|
||||
setFormInput(initialValues || { name: "", slug: "" });
|
||||
}, [isOpen]);
|
||||
|
||||
// REFACTOR: Move to react-hook-form with yup for better form management
|
||||
const onInputChange = (fieldName: string, fieldValue: string) => {
|
||||
setFormInput((state) => ({ ...state, [fieldName]: fieldValue }));
|
||||
};
|
||||
|
||||
const onFormSubmit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
name: formInput.name,
|
||||
slug: formInput.slug.toLowerCase()
|
||||
};
|
||||
if (isEditMode) {
|
||||
onEditSubmit(data);
|
||||
return;
|
||||
}
|
||||
onCreateSubmit(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-out duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
{isEditMode ? "Update environment" : "Create a new environment"}
|
||||
</Dialog.Title>
|
||||
<form onSubmit={onFormSubmit}>
|
||||
<div className="mt-4 max-h-28">
|
||||
<InputField
|
||||
label="Environment Name"
|
||||
onChangeHandler={(val) => onInputChange("name", val)}
|
||||
type="varName"
|
||||
value={formInput.name}
|
||||
placeholder=""
|
||||
isRequired
|
||||
// error={error.length > 0}
|
||||
// errorText={error}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 max-h-28">
|
||||
<InputField
|
||||
label="Environment Slug"
|
||||
onChangeHandler={(val) => onInputChange("slug", val)}
|
||||
type="varName"
|
||||
value={formInput.slug}
|
||||
placeholder=""
|
||||
isRequired
|
||||
// error={error.length > 0}
|
||||
// errorText={error}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Slugs are shorthands used in cli to access environment
|
||||
</p>
|
||||
<div className="mt-4 max-w-min">
|
||||
<Button
|
||||
onButtonPressed={() => null}
|
||||
type="submit"
|
||||
color="mineshaft"
|
||||
text={isEditMode ? "Update" : "Create"}
|
||||
active={formInput.name !== "" && formInput.slug !== ""}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
import { Checkbox } from "../table/Checkbox";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: (email: string, addAllUser: boolean) => void;
|
||||
workspaceName: string;
|
||||
setWorkspaceName: (workspaceName: string) => void;
|
||||
error: boolean;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The dialog modal for when the user wants to create a new workspace
|
||||
*/
|
||||
const AddWorkspaceDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
submitModal,
|
||||
workspaceName,
|
||||
setWorkspaceName,
|
||||
error,
|
||||
loading
|
||||
}: Props) => {
|
||||
const [addAllUsers, setAddAllUsers] = useState(true);
|
||||
const submit = () => {
|
||||
submitModal(workspaceName, addAllUsers);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-[100]" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
Create a new project
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
This project will contain your secrets and configs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 max-h-28">
|
||||
<InputField
|
||||
label="Project Name"
|
||||
onChangeHandler={setWorkspaceName}
|
||||
type="varName"
|
||||
value={workspaceName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 ml-1">
|
||||
<Checkbox addAllUsers={addAllUsers} setAddAllUsers={setAddAllUsers} />
|
||||
</div>
|
||||
<div className="mt-4 max-w-min">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
loading={loading}
|
||||
color="mineshaft"
|
||||
text="Create"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddWorkspaceDialog;
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import InputField from "../InputField";
|
||||
|
||||
// REFACTOR: Move all these modals into one reusable one
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
onSubmit: () => void;
|
||||
deleteKey?: string;
|
||||
};
|
||||
|
||||
const DeleteActionModal = ({ isOpen, onClose, title, onSubmit, deleteKey }: Props) => {
|
||||
const [deleteInputField, setDeleteInputField] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setDeleteInputField("");
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-grey p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">This action is irrevertible.</p>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<InputField
|
||||
isRequired
|
||||
label={`Type ${deleteKey} to delete the resource`}
|
||||
onChangeHandler={(val) => setDeleteInputField(val)}
|
||||
value={deleteInputField}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-alizarin hover:text-semibold inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onSubmit}
|
||||
disabled={Boolean(deleteKey) && deleteInputField !== deleteKey}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:border-white hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteActionModal;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: () => void;
|
||||
};
|
||||
|
||||
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-[80]" onClose={() => {}}>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-70"
|
||||
onClick={onClose}
|
||||
onKeyDown={onClose}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Close"
|
||||
/>
|
||||
</Transition.Child>
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-mineshaft-600 bg-bunker p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-bunker-200">
|
||||
{t("dashboard:sidebar.delete-key-dialog.title")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-bunker-300">
|
||||
{t("dashboard:sidebar.delete-key-dialog.confirm-delete-message")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-start">
|
||||
<button
|
||||
type="button"
|
||||
className="text-semibold inline-flex justify-center rounded-md border border-transparent bg-red-500 px-4 py-2 text-sm font-medium text-bunker-100 opacity-80 duration-200 hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:bg-mineshaft-500 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: (userIdToBeDeleted: string) => void;
|
||||
userIdToBeDeleted: string;
|
||||
};
|
||||
|
||||
const DeleteUserDialog = ({ isOpen, closeModal, submitModal, userIdToBeDeleted }: Props) => {
|
||||
const submit = () => {
|
||||
submitModal(userIdToBeDeleted);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl border border-gray-700 bg-grey p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
Are you sure you want to remove this user from the workspace?
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">This action is irrevertible.</p>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-alizarin hover:text-semibold inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={submit}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:border-white hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={submit}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteUserDialog;
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
// REFACTOR: Move all these modals into one reusable one
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onClose: () => void;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const UpgradePlanModal = ({ isOpen, onClose, text }: Props) => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 drop-shadow-xl" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-mineshaft-500 bg-bunker p-6 pt-5 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-xl font-medium leading-6 text-primary">
|
||||
Unleash Infisical's Full Power
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="mb-1 text-sm text-bunker-300">{text}</p>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Upgrade and get access to this, as well as to other powerful enhancements.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-semibold inline-flex justify-center rounded-md border border-transparent bg-primary px-4 py-2 text-sm font-medium text-black opacity-80 duration-200 hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() => router.push(`/org/${currentOrg?.id}/billing`)}
|
||||
>
|
||||
Upgrade Now
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:border-red hover:text-red focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Maybe Later
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpgradePlanModal;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
interface PopupProps {
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
titleText: string;
|
||||
emoji: string;
|
||||
textLine1: string;
|
||||
textLine2: string;
|
||||
setCheckDocsPopUpVisible: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the notification that pops up at the bottom right when a user performs a certain action
|
||||
* @param {object} org
|
||||
* @param {string} org.buttonText - text of the button inside the notification
|
||||
* @param {string} org.buttonLink - where the button leads to
|
||||
* @param {string} org.titleText - the text at the top of a notification
|
||||
* @param {string} org.emoji - the emoji in the notification title
|
||||
* @param {string} org.textLine1 - first line of the text in the notification
|
||||
* @param {string} org.textLine2 - second line of the text in the notification
|
||||
* @param {string} org.setCheckDocsPopUpVisible - the functions that closes the popup
|
||||
* @returns
|
||||
*/
|
||||
const BottonRightPopup = ({
|
||||
buttonText,
|
||||
buttonLink,
|
||||
titleText,
|
||||
emoji,
|
||||
textLine1,
|
||||
textLine2,
|
||||
setCheckDocsPopUpVisible
|
||||
}: PopupProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className="absolute bottom-0 right-0 z-[100] mr-6 mb-6 flex max-w-xl flex-col items-start rounded-md border border-gray-600/50 bg-bunker pt-3 pb-4 text-gray-200 drop-shadow-xl"
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between border-b border-gray-600/70 px-6 pb-3">
|
||||
<div className="mr-2 mt-0.5 flex flex-row text-xl font-bold">
|
||||
<div>{titleText}</div>
|
||||
<div className="ml-2.5">{emoji}</div>
|
||||
</div>
|
||||
<button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)} type="button">
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
className="cursor-pointer text-2xl text-gray-400 duration-200 hover:text-red"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-4 mb-0.5 block px-6 text-gray-300 sm:inline">{textLine1}</div>
|
||||
<div className="mb-4 block px-6 sm:inline">{textLine2}</div>
|
||||
<div className="flex w-full flex-row px-6">
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a
|
||||
className="flex w-full justify-center rounded-md bg-white/10 p-2 font-bold duration-200 hover:bg-primary hover:text-black"
|
||||
href={buttonLink}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{buttonText}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottonRightPopup;
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { faInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Tooltip } from "@app/components/v2/Tooltip";
|
||||
|
||||
const GlobPatternExamples = () => {
|
||||
const [showTip, setShowTip] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
isOpen={showTip}
|
||||
onOpenChange={setShowTip}
|
||||
content={
|
||||
<div>
|
||||
<h4 className="mb-2">Here are some examples of glob patterns:</h4>
|
||||
<div className="ol-listStyleType">
|
||||
<li>
|
||||
<code className="text-primary">/</code> - Matches all files and directories in the
|
||||
current directory
|
||||
</li>
|
||||
<li>
|
||||
<code className="text-primary">**/*</code> - Matches all files and directories in the
|
||||
current directory and its subdirectories
|
||||
</li>
|
||||
<li>
|
||||
<code className="text-primary">{"/{dir1,dir2}"}</code> - Matches all files and
|
||||
directories in dir1 and dir2
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
position="right"
|
||||
className="text-xs"
|
||||
>
|
||||
<div
|
||||
className="flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[1px] border-mineshaft-300"
|
||||
onMouseEnter={() => setShowTip(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfo} className="h-2 w-2" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobPatternExamples;
|
||||
@@ -1,32 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||
type Props = {
|
||||
addAllUsers: boolean;
|
||||
setAddAllUsers: (arg: boolean) => void;
|
||||
};
|
||||
|
||||
export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
|
||||
<div className="flex flex-row items-center">
|
||||
{addAllUsers === true ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 accent-primary"
|
||||
checked
|
||||
readOnly
|
||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onKeyDown={() => setAddAllUsers(!addAllUsers)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="add all users"
|
||||
className="h-4 w-4 rounded-sm border border-gray-600 bg-bunker"
|
||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<label className="ml-2 text-sm text-gray-500">
|
||||
Add all members of my organization to this project.
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { faCheckSquare, faPlus, faSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Tag } from "public/data/frequentInterfaces";
|
||||
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
/**
|
||||
* This is the menu that is used to add more tags to a secret
|
||||
* @param {object} obj
|
||||
* @param {Tag[]} obj.allTags - all available tags for a vertain project
|
||||
* @param {Tag[]} obj.currentTags - currently selected tags for a certain secret
|
||||
* @param {function} obj.modifyTags - modify tags for a certain secret
|
||||
* @param {Tag[]} obj.position - currently selected tags for a certain secret
|
||||
*/
|
||||
const AddTagsMenu = ({
|
||||
allTags,
|
||||
currentTags,
|
||||
modifyTags,
|
||||
id
|
||||
}: {
|
||||
allTags: Tag[];
|
||||
currentTags: Tag[];
|
||||
modifyTags: (value: Tag[], id: string) => void;
|
||||
id: string;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Menu as="div" className="relative ml-2 inline-block text-left">
|
||||
<Menu.Button
|
||||
as="div"
|
||||
className="flex items-center justify-center font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||
>
|
||||
<div className="flex cursor-pointer items-center rounded-sm bg-mineshaft/30 text-sm text-mineshaft-200/50 duration-200 hover:bg-mineshaft/70">
|
||||
<FontAwesomeIcon icon={faPlus} className="p-[0.28rem]" />
|
||||
{currentTags?.length > 2 && <span className="pr-2">{currentTags.length - 2}</span>}
|
||||
</div>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-[90] mt-0.5 w-[12rem] origin-top-right space-y-1 rounded-md border border-mineshaft-500 bg-mineshaft-600 p-1 text-sm shadow-lg ring-1 ring-black ring-opacity-5 drop-shadow-xl focus:outline-none">
|
||||
{allTags?.map((tag) => {
|
||||
return (
|
||||
<Menu.Item key={tag.id}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${
|
||||
currentTags?.map((currentTag) => currentTag.name).includes(tag.name)
|
||||
? "cursor-default opacity-30"
|
||||
: "hover:bg-mineshaft-700"
|
||||
} flex w-full items-center rounded-sm bg-mineshaft-800 px-2 py-0.5 text-left text-bunker-200`}
|
||||
onClick={() => {
|
||||
if (!currentTags?.map((currentTag) => currentTag.name).includes(tag.name)) {
|
||||
modifyTags(currentTags.concat([tag]), id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentTags?.map((currentTag) => currentTag.name).includes(tag.name) ? (
|
||||
<FontAwesomeIcon icon={faCheckSquare} className="mr-2 text-xs text-primary" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faSquare} className="mr-2 text-xs" />
|
||||
)}{" "}
|
||||
{tag.name}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded-sm bg-mineshaft-800 px-2 py-0.5 text-left text-bunker-200 duration-200 hover:bg-primary hover:text-black"
|
||||
onClick={() => router.push(`/${ProjectType.SecretManager}/${String(router.query.id)}/settings`)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-2 text-xs" />
|
||||
Add more tags
|
||||
</button>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTagsMenu;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* This is the text field where people can add comments to particular secrets.
|
||||
*/
|
||||
const CommentField = ({
|
||||
comment,
|
||||
modifyComment,
|
||||
id
|
||||
}: {
|
||||
comment: string;
|
||||
modifyComment: (value: string, id: string) => void;
|
||||
id: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="relative mt-4 px-4 pt-6">
|
||||
<p className="pl-0.5 text-sm text-bunker-300">{t("dashboard.sidebar.comments")}</p>
|
||||
<textarea
|
||||
className="h-32 w-full rounded-md border border-mineshaft-500 bg-bunker-800 px-2 py-1.5 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 placeholder:text-bunker-400 focus:ring-2 dark:[color-scheme:dark]"
|
||||
value={comment}
|
||||
onChange={(e) => modifyComment(e.target.value, id)}
|
||||
placeholder="Leave any comments here..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentField;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Button, Modal, ModalContent } from "../v2";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onOverwriteConfirm: (preserve: "old" | "new") => void;
|
||||
duplicateKeys: string[];
|
||||
};
|
||||
|
||||
const ConfirmEnvOverwriteModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
duplicateKeys,
|
||||
onOverwriteConfirm
|
||||
}: Props): JSX.Element => {
|
||||
return (
|
||||
<Modal isOpen={isOpen}>
|
||||
<ModalContent
|
||||
title="Duplicate Secrets"
|
||||
footerContent={
|
||||
<div className="flex items-center gap-4">
|
||||
<Button colorSchema="primary" onClick={() => onOverwriteConfirm("old")}>
|
||||
Keep Old
|
||||
</Button>
|
||||
<Button colorSchema="danger" onClick={() => onOverwriteConfirm("new")}>
|
||||
Overwrite
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-gray-400">Your file contains the following duplicate secrets:</p>
|
||||
<p className="text-sm text-gray-500">{duplicateKeys.join(", ")}</p>
|
||||
<p className="text-md text-gray-400">Are you sure you want to overwrite these secrets?</p>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmEnvOverwriteModal;
|
||||
@@ -1,274 +0,0 @@
|
||||
import { memo, SyntheticEvent, useRef } from "react";
|
||||
import {
|
||||
faCircle,
|
||||
faCodeBranch,
|
||||
faExclamationCircle,
|
||||
faEye
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
import { HoverObject } from "../v2/HoverCard";
|
||||
import { PopoverObject } from "../v2/Popover/Popover";
|
||||
|
||||
const REGEX = /([$]{.*?})/g;
|
||||
|
||||
interface DashboardInputFieldProps {
|
||||
id: string;
|
||||
onChangeHandler: (value: string, id: string) => void;
|
||||
value: string | undefined;
|
||||
type: "varName" | "value" | "comment";
|
||||
blurred?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
isCapitalized?: boolean;
|
||||
overrideEnabled?: boolean;
|
||||
modifyValueOverride?: (value: string | undefined, id: string) => void;
|
||||
isSideBarOpen?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component renders the input fields on the dashboard
|
||||
* @param {object} obj - the order number of a keyPair
|
||||
* @param {number} obj.position - the order number of a keyPair
|
||||
* @param {function} obj.onChangeHandler - what happens when the input is modified
|
||||
* @param {string} obj.type - whether the input field is for a Key Name or for a Key Value
|
||||
* @param {string} obj.value - value of the InputField
|
||||
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
||||
* @param {boolean} obj.isDuplicate - if the key name is duplicated
|
||||
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
|
||||
*
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
|
||||
const DashboardInputField = ({
|
||||
id,
|
||||
onChangeHandler,
|
||||
type,
|
||||
value,
|
||||
blurred,
|
||||
isDuplicate,
|
||||
isCapitalized,
|
||||
overrideEnabled,
|
||||
modifyValueOverride,
|
||||
isSideBarOpen
|
||||
}: DashboardInputFieldProps) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
if (ref.current === null) return;
|
||||
|
||||
ref.current.scrollTop = e.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||
};
|
||||
|
||||
if (type === "varName") {
|
||||
const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== "";
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative h-10 w-full flex-col ${error && value !== "" ? "bg-red/[0.15]" : ""} ${
|
||||
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`group relative flex h-full flex-col items-center justify-center ${
|
||||
error ? "w-max" : "w-full"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
onChange={(e) =>
|
||||
onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id)
|
||||
}
|
||||
type={type}
|
||||
value={value}
|
||||
className={`ph-no-capture min-w-16 peer z-10 h-full w-full bg-transparent px-2 font-mono text-sm caret-bunker-200 outline-none ${
|
||||
error ? "text-red-600 focus:text-red-500" : "text-bunker-300 focus:text-bunker-100"
|
||||
} duration-200`}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
{startsWithNumber && (
|
||||
<div className="absolute right-2 top-2 z-50 text-red">
|
||||
<HoverObject
|
||||
text="Secret names should not start with a number"
|
||||
icon={faExclamationCircle}
|
||||
color="red"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isDuplicate && value !== "" && !startsWithNumber && (
|
||||
<div className="absolute right-2 top-2 z-50 text-red">
|
||||
<HoverObject
|
||||
text="Secret names should be unique"
|
||||
icon={faExclamationCircle}
|
||||
color="red"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!error && (
|
||||
<div
|
||||
className={`absolute right-0 top-0 z-50 bg-mineshaft-800 text-red group-hover:bg-mineshaft-700 ${
|
||||
overrideEnabled ? "visible" : "invisible group-hover:visible"
|
||||
} duration-0 flex h-10 cursor-pointer items-center px-2`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (modifyValueOverride) {
|
||||
if (overrideEnabled === false) {
|
||||
modifyValueOverride("", id);
|
||||
} else {
|
||||
modifyValueOverride(undefined, id);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HoverObject
|
||||
text={
|
||||
overrideEnabled
|
||||
? "This secret is overriden with your personal value"
|
||||
: "You can override this secret with a personal value"
|
||||
}
|
||||
icon={faCodeBranch}
|
||||
color={overrideEnabled ? "primary" : "bunker-400"}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (type === "comment") {
|
||||
const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== "";
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<PopoverObject text={value || ""} onChangeHandler={onChangeHandler} id={id}>
|
||||
<div
|
||||
title={value}
|
||||
className={`relative h-10 w-full flex-col overflow-hidden ${
|
||||
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`group relative flex h-full flex-col items-center justify-center ${
|
||||
error ? "w-max" : "w-full"
|
||||
}`}
|
||||
>
|
||||
{value?.split("\n")[0] ? (
|
||||
<span className="ph-no-capture min-w-16 placeholder w-full truncate break-all bg-transparent px-2 text-xs leading-tight text-bunker-300 outline-none duration-200 placeholder:text-bunker-400 focus:text-bunker-100 placeholder:focus:text-transparent">
|
||||
{value?.split("\n")[0]}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-bunker-400">-</span>
|
||||
)}
|
||||
{value?.split("\n")[1] && (
|
||||
<span className="ph-no-capture min-w-16 placeholder w-full truncate break-all bg-transparent px-2 text-xs leading-tight text-bunker-300 outline-none duration-200 placeholder:text-bunker-400 focus:text-bunker-100 placeholder:focus:text-transparent">
|
||||
{value?.split("\n")[1]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverObject>
|
||||
);
|
||||
}
|
||||
if (type === "value") {
|
||||
return (
|
||||
<div className="w-full flex-col">
|
||||
<div className="group relative flex w-full flex-col justify-center whitespace-pre">
|
||||
{overrideEnabled === true && (
|
||||
<div className="absolute top-[0.1rem] right-[0.1rem] z-0 w-min rounded-sm bg-primary-500 px-1 text-xxs text-black opacity-80">
|
||||
Override enabled
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeHandler(e.target.value, id)}
|
||||
onScroll={syncScroll}
|
||||
className={`${
|
||||
blurred ? "text-transparent focus:text-transparent active:text-transparent" : ""
|
||||
} ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none duration-200 no-scrollbar`}
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${
|
||||
blurred && !overrideEnabled
|
||||
? "text-bunker-800 duration-200 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400"
|
||||
: ""
|
||||
} ${overrideEnabled ? "text-primary-300" : "text-gray-400"}
|
||||
absolute z-0 flex flex-row whitespace-pre font-mono ${
|
||||
blurred ? "invisible" : "visible"
|
||||
} ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar mt-0.5 h-10 w-full overflow-x-scroll bg-transparent px-2 py-2 text-sm outline-none duration-100 no-scrollbar peer-focus:visible`}
|
||||
>
|
||||
{value?.split(REGEX).map((word) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={id}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">
|
||||
{word.slice(2, word.length - 1)}
|
||||
</span>
|
||||
{word.slice(word.length - 1, word.length) === "}" ? (
|
||||
<span className="ph-no-capture text-yellow">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="ph-no-capture text-yellow-400">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span key={`${word}_${id + 1}`} className="ph-no-capture">
|
||||
{word}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{blurred && (
|
||||
<div
|
||||
className={`peer absolute z-0 flex flex-row items-center justify-between pr-2 ${
|
||||
isSideBarOpen ? "bg-mineshaft-700 duration-200" : "bg-mineshaft-800"
|
||||
} h-10 w-full text-clip text-bunker-400 duration-100 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden`}
|
||||
>
|
||||
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
||||
{value?.split("").map(() => (
|
||||
<FontAwesomeIcon
|
||||
key={guidGenerator()}
|
||||
className="mr-0.5 text-xxs"
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
{value?.split("").length === 0 && <span className="text-bunker-400/80">EMPTY</span>}
|
||||
</div>
|
||||
<div className="invisible z-[100] cursor-default group-hover:visible">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>Something Wrong</>;
|
||||
};
|
||||
|
||||
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
|
||||
return (
|
||||
prev.value === next.value &&
|
||||
prev.type === next.type &&
|
||||
prev.id === next.id &&
|
||||
prev.blurred === next.blurred &&
|
||||
prev.isCapitalized === next.isCapitalized &&
|
||||
prev.overrideEnabled === next.overrideEnabled &&
|
||||
prev.isDuplicate === next.isDuplicate &&
|
||||
prev.isSideBarOpen === next.isSideBarOpen
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DashboardInputField, inputPropsAreEqual);
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import Button from "../basic/buttons/Button";
|
||||
|
||||
type Props = {
|
||||
onSubmit: () => void;
|
||||
isPlain?: boolean;
|
||||
};
|
||||
|
||||
export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
!isPlain
|
||||
? "ml-2 h-[2.5rem] w-[4.5rem] rounded-md bg-[#9B3535] opacity-70 duration-200 hover:opacity-100"
|
||||
: "justfy-center mr-2 flex h-[2.35rem] w-[1.5rem] cursor-pointer items-center"
|
||||
}`}
|
||||
>
|
||||
{isPlain ? (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onSubmit}
|
||||
className="invisible group-hover:visible"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="mt-0.5 pl-2 pr-6 text-lg text-bunker-300 hover:text-red"
|
||||
icon={faXmark}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button text={String(t("Delete"))} color="red" size="md" onButtonPressed={onSubmit} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
|
||||
import Button from "../basic/buttons/Button";
|
||||
import downloadDotEnv from "../utilities/secrets/downloadDotEnv";
|
||||
import downloadYaml from "../utilities/secrets/downloadYaml";
|
||||
|
||||
/**
|
||||
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to downlaod
|
||||
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
||||
*/
|
||||
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string }) => {
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<Menu.Button
|
||||
as="div"
|
||||
className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||
>
|
||||
<Button color="mineshaft" size="icon-md" icon={faDownload} onButtonPressed={() => {}} />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-[90] mt-0.5 w-[12rem] origin-top-right space-y-2 rounded-md border border-mineshaft-500 bg-bunker p-2 shadow-lg ring-1 ring-black ring-opacity-5 drop-shadow-xl focus:outline-none">
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
onButtonPressed={() => downloadDotEnv({ data, env })}
|
||||
size="md"
|
||||
text="Download as .env"
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
onButtonPressed={() => downloadYaml({ data, env })}
|
||||
size="md"
|
||||
text="Download as .yml"
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadSecretMenu;
|
||||
@@ -1,256 +0,0 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { type ChangeEvent, type DragEvent, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Image from "next/image";
|
||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { parseDocument, Scalar, YAMLMap } from "yaml";
|
||||
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
|
||||
import Button from "../basic/buttons/Button";
|
||||
import Error from "../basic/Error";
|
||||
import { createNotification } from "../notifications";
|
||||
import { parseDotEnv } from "../utilities/parseSecrets";
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
|
||||
interface DropZoneProps {
|
||||
// TODO: change Data type from any
|
||||
setData: (data: any) => void;
|
||||
setErrorDragAndDrop: (hasError: boolean) => void;
|
||||
createNewFile: () => void;
|
||||
errorDragAndDrop: boolean;
|
||||
setButtonReady: (isReady: boolean) => void;
|
||||
keysExist: boolean;
|
||||
numCurrentRows: number;
|
||||
}
|
||||
|
||||
const DropZone = ({
|
||||
setData,
|
||||
setErrorDragAndDrop,
|
||||
createNewFile,
|
||||
errorDragAndDrop,
|
||||
setButtonReady,
|
||||
keysExist,
|
||||
numCurrentRows
|
||||
}: DropZoneProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// set dropEffect to copy i.e copy of the source item
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
};
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const getSecrets = (file: ArrayBuffer, fileType: string) => {
|
||||
let secrets;
|
||||
switch (fileType) {
|
||||
case "env": {
|
||||
const keyPairs = parseDotEnv(file);
|
||||
secrets = Object.keys(keyPairs).map((key, index) => ({
|
||||
id: guidGenerator(),
|
||||
pos: numCurrentRows + index,
|
||||
key,
|
||||
value: keyPairs[key as keyof typeof keyPairs].value,
|
||||
comment: keyPairs[key as keyof typeof keyPairs].comments.join("\n"),
|
||||
type: SecretType.Shared,
|
||||
tags: []
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case "json": {
|
||||
const keyPairs = JSON.parse(String(file));
|
||||
secrets = Object.keys(keyPairs).map((key, index) => ({
|
||||
id: guidGenerator(),
|
||||
pos: numCurrentRows + index,
|
||||
key,
|
||||
value: keyPairs[key as keyof typeof keyPairs],
|
||||
comment: "",
|
||||
type: SecretType.Shared,
|
||||
tags: []
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case "yml": {
|
||||
const parsedFile = parseDocument(file.toString());
|
||||
const keyPairs = parsedFile.contents!.toJSON();
|
||||
|
||||
secrets = Object.keys(keyPairs).map((key, index) => {
|
||||
const fileContent = parsedFile.contents as YAMLMap<Scalar, Scalar>;
|
||||
const comment =
|
||||
fileContent!.items
|
||||
.find((item) => item.key.value === key)
|
||||
?.key?.commentBefore?.split("\n")
|
||||
.map((cmnt) => cmnt.trim())
|
||||
.join("\n") ?? "";
|
||||
return {
|
||||
id: guidGenerator(),
|
||||
pos: numCurrentRows + index,
|
||||
key,
|
||||
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? "",
|
||||
comment,
|
||||
type: SecretType.Shared,
|
||||
tags: []
|
||||
};
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
secrets = "";
|
||||
createNotification({
|
||||
text: "The file you are dropping should have one of the following extensions: .env, .yml, .json.",
|
||||
type: "error"
|
||||
});
|
||||
break;
|
||||
}
|
||||
return secrets;
|
||||
};
|
||||
|
||||
// This function function immediately parses the file after it is dropped
|
||||
const handleDrop = async (e: DragEvent) => {
|
||||
setLoading(true);
|
||||
setTimeout(() => setLoading(false), 5000);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
const reader = new FileReader();
|
||||
if (file === undefined) {
|
||||
createNotification({
|
||||
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
|
||||
type: "error"
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const fileType = file.name.split(".")[1];
|
||||
|
||||
reader.onload = (event) => {
|
||||
if (event.target === null || event.target.result === null) return;
|
||||
// parse function's argument looks like to be ArrayBuffer
|
||||
try {
|
||||
const newData = getSecrets(event.target.result as ArrayBuffer, fileType);
|
||||
setData(newData);
|
||||
setButtonReady(true);
|
||||
} catch (error) {
|
||||
console.log("Error while dropping the file: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
// If something is wrong show an error
|
||||
try {
|
||||
reader.readAsText(file);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setErrorDragAndDrop(true);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// This function is used when the user manually selects a file from the in-browser dircetory (not drag and drop)
|
||||
const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setLoading(true);
|
||||
setTimeout(() => setLoading(false), 5000);
|
||||
if (e.currentTarget.files === null) return;
|
||||
const file = e.currentTarget.files[0];
|
||||
const fileType = file.name.split(".")[1];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
if (event.target === null || event.target.result === null) return;
|
||||
const { result } = event.target;
|
||||
const newData = getSecrets(result as ArrayBuffer, fileType);
|
||||
setData(newData);
|
||||
setButtonReady(true);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
return loading ? (
|
||||
<div className="mb-16 flex items-center justify-center pt-16">
|
||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
) : keysExist ? (
|
||||
<div
|
||||
className="relative mx-auto mb-4 mt-1 flex w-full max-w-[calc(100%-1rem)] flex-col items-center justify-center rounded-md bg-mineshaft-900 py-8 px-2 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="absolute h-full w-full opacity-0"
|
||||
accept=""
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
|
||||
<div className="flex flex-row">
|
||||
<FontAwesomeIcon icon={faUpload} className="mr-6 text-3xl text-bunker-300" />
|
||||
<p className="mt-1 text-bunker-300">{t("common.drop-zone-keys")}</p>
|
||||
</div>
|
||||
{errorDragAndDrop && (
|
||||
<div className="mt-8 max-w-xl opacity-80">
|
||||
<Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="relative mb-16 flex w-full max-w-2xl flex-col items-center justify-center rounded-md bg-bunker px-4 pt-16 opacity-80 outline-dashed outline-2 outline-gray-700 duration-200 hover:opacity-100"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpload} className="mb-8 text-7xl" />
|
||||
<p className="">{t("common.drop-zone")}</p>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="absolute h-full w-full opacity-0"
|
||||
accept=".txt,.env,.yml"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<div className="mb-6 mt-5 flex w-full flex-row items-center justify-center">
|
||||
<div className="w-1/5 border-t border-gray-700" />
|
||||
<p className="mx-4 text-xs text-gray-400">OR</p>
|
||||
<div className="w-1/5 border-t border-gray-700" />
|
||||
</div>
|
||||
<div className="z-10 mb-6">
|
||||
<Button
|
||||
color="mineshaft"
|
||||
text={String(t("dashboard.add-secret"))}
|
||||
onButtonPressed={createNewFile}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
{errorDragAndDrop ? (
|
||||
<div className="opacity-80">
|
||||
<Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-3">
|
||||
{/* <p className="text-xs text-gray-500"> If you are expecting to see a file here, contact your administrator for permission. </p> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropZone;
|
||||
@@ -1,106 +0,0 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faShuffle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
|
||||
/**
|
||||
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
|
||||
* @returns the popup-menu for randomly generating secrets
|
||||
*/
|
||||
const GenerateSecretMenu = ({
|
||||
modifyValue,
|
||||
id
|
||||
}: {
|
||||
modifyValue: (value: string, id: string) => void;
|
||||
id: string;
|
||||
}) => {
|
||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<div className="rounded-md bg-bunker-800 py-1 px-2 hover:bg-bunker-500">
|
||||
<FontAwesomeIcon icon={faShuffle} className="text-bunker-300" />
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-50 mt-0.5 w-[20rem] origin-top-right rounded-md border border-mineshaft-500 bg-bunker px-1 py-1 shadow-lg ring-1 ring-black ring-opacity-5 drop-shadow-xl focus:outline-none">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (randomStringLength > 32) {
|
||||
setRandomStringLength(32);
|
||||
} else if (randomStringLength < 2) {
|
||||
setRandomStringLength(2);
|
||||
} else {
|
||||
modifyValue(
|
||||
[...Array(randomStringLength)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(""),
|
||||
id
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="relative flex w-full cursor-pointer select-none flex-row items-center justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/10 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faShuffle} />
|
||||
<div className="flex w-full flex-row justify-between text-sm">
|
||||
<p>{t("dashboard.sidebar.generate-random-hex")}</p>
|
||||
<p>{t("dashboard.sidebar.digits")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-[0.4rem] right-[3.3rem] flex w-16 flex-row rounded-md border border-chicago-700 bg-bunker-800 text-bunker-200 ">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="m-0.5 cursor-pointer rounded-md px-1 hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) => setRandomStringLength(parseInt(e.target.value, 10))}
|
||||
value={randomStringLength}
|
||||
className="peer z-20 w-full bg-transparent text-center text-sm outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="m-0.5 cursor-pointer rounded-md px-1 pb-0.5 hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateSecretMenu;
|
||||
@@ -1,241 +0,0 @@
|
||||
import { faEllipsis, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { SecretDataProps, Tag } from "public/data/frequentInterfaces";
|
||||
|
||||
import AddTagsMenu from "./AddTagsMenu";
|
||||
import DashboardInputField from "./DashboardInputField";
|
||||
import { DeleteActionButton } from "./DeleteActionButton";
|
||||
|
||||
interface KeyPairProps {
|
||||
keyPair: SecretDataProps;
|
||||
modifyKey: (value: string, id: string) => void;
|
||||
modifyValue: (value: string, id: string) => void;
|
||||
modifyValueOverride: (value: string | undefined, id: string) => void;
|
||||
modifyComment: (value: string, id: string) => void;
|
||||
modifyTags: (value: Tag[], id: string) => void;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
sidebarSecretId: string;
|
||||
isSnapshot: boolean;
|
||||
isCapitalized: boolean;
|
||||
deleteRow?: (props: DeleteRowFunctionProps) => void;
|
||||
tags: Tag[];
|
||||
togglePITSidebar?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export interface DeleteRowFunctionProps {
|
||||
ids: string[];
|
||||
secretName: string;
|
||||
}
|
||||
|
||||
const colors = [
|
||||
"bg-[#f1c40f]/40",
|
||||
"bg-[#cb1c8d]/40",
|
||||
"bg-[#badc58]/40",
|
||||
"bg-[#ff5400]/40",
|
||||
"bg-[#3AB0FF]/40",
|
||||
"bg-[#6F1AB6]/40",
|
||||
"bg-[#C40B13]/40",
|
||||
"bg-[#332FD0]/40"
|
||||
];
|
||||
|
||||
const colorsText = [
|
||||
"text-[#fcf0c3]/70",
|
||||
"text-[#f2c6e3]/70",
|
||||
"text-[#eef6d5]/70",
|
||||
"text-[#ffddcc]/70",
|
||||
"text-[#f0fffd]/70",
|
||||
"text-[#FFE5F1]/70",
|
||||
"text-[#FFDEDE]/70",
|
||||
"text-[#DFF6FF]/70"
|
||||
];
|
||||
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
|
||||
* @param {function} obj.modifyComment - modify the comment of a certain environment variable
|
||||
* @param {function} obj.modifyTags - modify the tags of a certain environment variable
|
||||
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
|
||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @param {function} obj.togglePITSidebar - open or close the Point-in-time recovery sidebar
|
||||
* @param {Tag[]} obj.tags - tags for a certain secret
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
keyPair,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
modifyValueOverride,
|
||||
modifyComment,
|
||||
modifyTags,
|
||||
isBlurred,
|
||||
isDuplicate,
|
||||
toggleSidebar,
|
||||
sidebarSecretId,
|
||||
isCapitalized,
|
||||
isSnapshot,
|
||||
deleteRow,
|
||||
togglePITSidebar,
|
||||
tags
|
||||
}: KeyPairProps) => {
|
||||
const tagData = tags.map((tag, index) => {
|
||||
return {
|
||||
...tag,
|
||||
color: colors[index % colors.length],
|
||||
colorText: colorsText[index % colorsText.length]
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group flex flex-col items-center border-b border-mineshaft-500 duration-100 hover:bg-white/[0.03] ${
|
||||
isSnapshot && "pointer-events-none"
|
||||
} ${keyPair.id === sidebarSecretId && "bg-mineshaft-700 duration-200"}`}
|
||||
>
|
||||
<div className="relative mr-auto flex max-h-14 w-full flex-row items-center justify-between">
|
||||
<div className="flex w-[23%] flex-row items-center border-r border-mineshaft-600">
|
||||
<div className="flex h-10 w-14 cursor-default items-center justify-center text-xs text-bunker-400">
|
||||
{keyPair.pos + 1}
|
||||
</div>
|
||||
<div className="flex max-h-16 w-full items-center">
|
||||
<DashboardInputField
|
||||
isCapitalized={isCapitalized}
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
id={keyPair.id ? keyPair.id : keyPair.idOverride || ""}
|
||||
value={keyPair.key}
|
||||
isDuplicate={isDuplicate}
|
||||
overrideEnabled={keyPair.valueOverride !== undefined}
|
||||
modifyValueOverride={modifyValueOverride}
|
||||
isSideBarOpen={keyPair.id === sidebarSecretId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-5/12 border-r border-mineshaft-600">
|
||||
<div className="mt-4 flex max-h-10 items-center rounded-lg md:mt-0">
|
||||
<DashboardInputField
|
||||
onChangeHandler={
|
||||
keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue
|
||||
}
|
||||
type="value"
|
||||
id={keyPair.id ? keyPair.id : keyPair.idOverride || ""}
|
||||
value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value}
|
||||
blurred={isBlurred}
|
||||
overrideEnabled={keyPair.valueOverride !== undefined}
|
||||
isSideBarOpen={keyPair.id === sidebarSecretId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[calc(10%)] border-r border-mineshaft-600">
|
||||
<div className="flex max-h-16 items-center">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyComment}
|
||||
type="comment"
|
||||
id={keyPair.id ? keyPair.id : keyPair.idOverride || ""}
|
||||
value={keyPair.comment}
|
||||
isDuplicate={isDuplicate}
|
||||
isSideBarOpen={keyPair.id === sidebarSecretId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-r-scroll no-scrollbar::-webkit-scrollbar flex h-10 w-2/12 items-center overflow-visible no-scrollbar">
|
||||
<div className="flex max-h-16 items-center">
|
||||
{keyPair.tags?.map(
|
||||
(tag, index) =>
|
||||
index < 2 && (
|
||||
<div
|
||||
key={keyPair.pos}
|
||||
className={`ml-2 px-1.5 ${
|
||||
tagData.filter((tagDp) => tagDp.id === tag.id)[0]?.color
|
||||
} rounded-sm text-sm ${
|
||||
tagData.filter((tagDp) => tagDp.id === tag.id)[0]?.colorText
|
||||
} flex items-center`}
|
||||
>
|
||||
<span className="mb-0.5 cursor-default">{tag.name}</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
className="ml-1 cursor-pointer p-1"
|
||||
onClick={() =>
|
||||
modifyTags(
|
||||
keyPair.tags.filter((ttag) => ttag.id !== tag.id),
|
||||
keyPair.id
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<AddTagsMenu
|
||||
allTags={tags}
|
||||
currentTags={keyPair.tags}
|
||||
modifyTags={modifyTags}
|
||||
id={keyPair.id ? keyPair.id : keyPair.idOverride || ""}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (togglePITSidebar) {
|
||||
togglePITSidebar(false);
|
||||
}
|
||||
toggleSidebar(keyPair.id);
|
||||
}}
|
||||
className={`invisible z-50 ml-auto flex h-[2.35rem] w-[1.5rem] cursor-pointer flex-row items-center justify-center rounded-md group-hover:visible group-hover:bg-mineshaft-700 ${
|
||||
isSnapshot ?? "invisible"
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg text-bunker-300 hover:text-primary"
|
||||
icon={faEllipsis}
|
||||
/>
|
||||
</div>
|
||||
<div className={`z-50 group-hover:bg-mineshaft-700 ${isSnapshot ?? "invisible"}`}>
|
||||
{keyPair.key || keyPair.value ? (
|
||||
<DeleteActionButton
|
||||
onSubmit={() => {
|
||||
if (deleteRow) {
|
||||
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key });
|
||||
}
|
||||
}}
|
||||
isPlain
|
||||
/>
|
||||
) : (
|
||||
<div className="justfy-center mr-2 flex h-[2.35rem] w-[1.5rem] cursor-pointer items-center">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (deleteRow) {
|
||||
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key });
|
||||
}
|
||||
}}
|
||||
className="invisible group-hover:visible"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="mt-0.5 pl-2 pr-6 text-lg text-bunker-300 hover:text-red"
|
||||
icon={faXmark}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyPair;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormLabel, Tooltip } from "../v2";
|
||||
|
||||
// To give users example of possible values of TTL
|
||||
export const FormLabelToolTip = ({ label, linkToMore, content }: { label: string, linkToMore: string, content: string }) => (
|
||||
<div>
|
||||
<FormLabel
|
||||
label={label}
|
||||
icon={
|
||||
<Tooltip
|
||||
content={
|
||||
<span>
|
||||
{content}{" "}
|
||||
<a
|
||||
href={linkToMore}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-700"
|
||||
>
|
||||
More
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faQuestionCircle}
|
||||
size="sm"
|
||||
className="relative bottom-1 right-1"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -1,12 +1,35 @@
|
||||
import { FormLabelToolTip } from "./FormLabelToolTip";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormLabel, Tooltip } from "../v2";
|
||||
|
||||
// To give users example of possible values of TTL
|
||||
export const TtlFormLabel = ({ label }: { label: string }) => (
|
||||
<div>
|
||||
<FormLabelToolTip
|
||||
<FormLabel
|
||||
label={label}
|
||||
content="1m, 2h, 3d. "
|
||||
linkToMore="https://github.com/vercel/ms?tab=readme-ov-file#examples"
|
||||
icon={
|
||||
<Tooltip
|
||||
content={
|
||||
<span>
|
||||
<a
|
||||
href="https://github.com/vercel/ms?tab=readme-ov-file#examples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-700"
|
||||
>
|
||||
More
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faQuestionCircle}
|
||||
size="sm"
|
||||
className="relative bottom-1 right-1"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { faRocketchat } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useCreateUserWish } from "@app/hooks/api/userEngagement";
|
||||
|
||||
const formSchema = z.object({
|
||||
text: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
type TFormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const WishForm = () => {
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<TFormData>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
const { mutateAsync } = useCreateUserWish();
|
||||
const [isOpen, setIsOpen] = useToggle(false);
|
||||
|
||||
const createWish = async (data: TFormData) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
text: data.text
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Your wish has been sent to the Infisical team!",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
setIsOpen.off();
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "An error occured while sending your wish to the Infisical team.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
onOpenChange={() => {
|
||||
setIsOpen.toggle();
|
||||
reset();
|
||||
}}
|
||||
open={isOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="text-md mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faRocketchat} className="mr-2" />
|
||||
Request a feature
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
hideCloseBtn
|
||||
align="start"
|
||||
alignOffset={20}
|
||||
className="mb-1 w-auto border border-mineshaft-600 bg-mineshaft-900 p-4 drop-shadow-2xl"
|
||||
sticky="always"
|
||||
>
|
||||
<form onSubmit={handleSubmit(createWish)}>
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
isError={Boolean(errors?.text)}
|
||||
errorText={errors?.text?.message}
|
||||
>
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 bg-black/10 text-sm focus:ring-0"
|
||||
variant="outline"
|
||||
placeholder="Wish for anything! Help us improve the platform."
|
||||
reSize="none"
|
||||
rows={6}
|
||||
cols={40}
|
||||
{...register("text")}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button
|
||||
className="w-min"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button } from "../v2";
|
||||
|
||||
@@ -9,10 +7,10 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const NoEnvironmentsBanner = ({ projectId }: IProps) => {
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
|
||||
<div className="mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
|
||||
<div className="flex w-full flex-col text-sm">
|
||||
<span className="mb-2 text-lg font-semibold">
|
||||
No environments in your project was found
|
||||
@@ -22,7 +20,7 @@ export const NoEnvironmentsBanner = ({ projectId }: IProps) => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Button onClick={() => router.push(`/${ProjectType.SecretManager}/${projectId}/settings#environments`)}>
|
||||
<Button onClick={() => navigate({ to: `/project/${projectId}/settings#environments` })}>
|
||||
Add environments
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { useGetOrgTrialUrl } from "@app/hooks/api";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { Button } from "../../v2/Button";
|
||||
import { Modal, ModalContent } from "../../v2/Modal";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
@@ -13,10 +13,10 @@ type Props = {
|
||||
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
|
||||
const { mutateAsync, isPending } = useGetOrgTrialUrl();
|
||||
const link =
|
||||
subscription && subscription.slug !== null
|
||||
? `/org/${currentOrg?.id}/billing`
|
||||
? ("/organization/billing" as const)
|
||||
: "https://infisical.com/scheduledemo";
|
||||
|
||||
const handleUpgradeBtnClick = async () => {
|
||||
@@ -50,7 +50,7 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
|
||||
</p>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isLoading={isPending}
|
||||
colorSchema="primary"
|
||||
onClick={handleUpgradeBtnClick}
|
||||
className="mr-4"
|
||||
@@ -14,8 +14,8 @@ type Props = {
|
||||
};
|
||||
|
||||
const TotpRegistration = ({ onComplete, shouldCenterQr }: Props) => {
|
||||
const { data: registration, isLoading } = useGetUserTotpRegistration();
|
||||
const { mutateAsync: verifyUserTotp, isLoading: isVerifyLoading } =
|
||||
const { data: registration, isPending } = useGetUserTotpRegistration();
|
||||
const { mutateAsync: verifyUserTotp, isPending: isVerifyLoading } =
|
||||
useVerifyUserTotpRegistration();
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState("");
|
||||
const [totp, setTotp] = useState("");
|
||||
@@ -47,7 +47,7 @@ const TotpRegistration = ({ onComplete, shouldCenterQr }: Props) => {
|
||||
generateQRCode();
|
||||
}, [registration]);
|
||||
|
||||
if (isLoading) {
|
||||
if (isPending) {
|
||||
return <ContentLoader />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faAngleRight, faCheck, faCopy, faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
@@ -61,7 +58,10 @@ export default function NavHeader({
|
||||
const [isCopied, { timedToggle: toggleIsCopied }] = useToggle(false);
|
||||
const [isHoveringCopyButton, setIsHoveringCopyButton] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const routerEnvSlug = useParams({
|
||||
strict: false,
|
||||
select: (el) => el.envSlug
|
||||
});
|
||||
|
||||
const secretPathSegments = secretPath.split("/").filter(Boolean);
|
||||
|
||||
@@ -71,13 +71,10 @@ export default function NavHeader({
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={`/org/${currentOrg?.id}/${ProjectType.SecretManager}/overview`}
|
||||
to="/organization/secret-manager/overview"
|
||||
className="truncate pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary"
|
||||
>
|
||||
<a className="truncate pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary">
|
||||
{currentOrg?.name}
|
||||
</a>
|
||||
{currentOrg?.name}
|
||||
</Link>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
@@ -96,14 +93,11 @@ export default function NavHeader({
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
{pageName === "Secrets" ? (
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{
|
||||
pathname: `/${ProjectType.SecretManager}/[id]/secrets/overview`,
|
||||
query: { id: router.query.id }
|
||||
}}
|
||||
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
className="text-sm font-semibold text-primary/80 hover:text-primary"
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
||||
{pageName}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">{pageName}</div>
|
||||
@@ -131,29 +125,21 @@ export default function NavHeader({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isFolderMode && Boolean(secretPathSegments.length) && (
|
||||
{isFolderMode && routerEnvSlug && Boolean(secretPathSegments.length) && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{
|
||||
pathname: `/${ProjectType.SecretManager}/[id]/secrets/[env]`,
|
||||
query: { id: router.query.id, env: router.query.env }
|
||||
}}
|
||||
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
|
||||
params={{ projectId: currentWorkspace.id, envSlug: routerEnvSlug }}
|
||||
className="text-sm font-semibold text-primary/80 hover:text-primary"
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
||||
{userAvailableEnvs?.find(({ slug }) => slug === currentEnv)?.name}
|
||||
</a>
|
||||
{userAvailableEnvs?.find(({ slug }) => slug === currentEnv)?.name}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{isFolderMode &&
|
||||
secretPathSegments?.map((folderName, index) => {
|
||||
const query: ParsedUrlQuery & { secretPath: string } = {
|
||||
...router.query,
|
||||
secretPath: `/${secretPathSegments.slice(0, index + 1).join("/")}`
|
||||
};
|
||||
const newSecretPath = `/${secretPathSegments.slice(0, index + 1).join("/")}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -184,7 +170,7 @@ export default function NavHeader({
|
||||
onClick={() => {
|
||||
if (isCopied) return;
|
||||
|
||||
navigator.clipboard.writeText(query.secretPath);
|
||||
navigator.clipboard.writeText(newSecretPath);
|
||||
|
||||
createNotification({
|
||||
text: "Copied secret path to clipboard",
|
||||
@@ -205,21 +191,18 @@ export default function NavHeader({
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{
|
||||
pathname: `/${ProjectType.SecretManager}/[id]/secrets/[env]`,
|
||||
query
|
||||
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id,
|
||||
envSlug: routerEnvSlug || ""
|
||||
}}
|
||||
search={(query) => ({ ...query, secretPath: newSecretPath })}
|
||||
className={twMerge(
|
||||
"text-sm font-semibold transition-all hover:text-primary",
|
||||
isHoveringCopyButton ? "text-primary" : "text-primary/80"
|
||||
)}
|
||||
>
|
||||
<a
|
||||
className={twMerge(
|
||||
"text-sm font-semibold transition-all hover:text-primary",
|
||||
isHoveringCopyButton ? "text-primary" : "text-primary/80"
|
||||
)}
|
||||
>
|
||||
{folderName}
|
||||
</a>
|
||||
{folderName}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { faAngleRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
|
||||
import { Select, SelectItem, Tooltip } from "../v2";
|
||||
|
||||
/**
|
||||
* This is the component at the top of almost every page.
|
||||
* It shows how to navigate to a certain page.
|
||||
* It future these links should also be clickable and hoverable
|
||||
* @param {object} obj
|
||||
* @param {string} obj.pageName - Name of the page
|
||||
* @param {boolean} obj.isProjectRelated - whether or not this page is related to project (determine if it's 2 or 3 navigation steps)
|
||||
* @param {boolean} obj.isOrganizationRelated - whether or not this page is related to organization (determine if it's 2 or 3 navigation steps)
|
||||
* @param {string} obj.currentEnv - current environment inside a project
|
||||
* @param {string} obj.userAvailableEnvs - environments that are available to a user in this project (used for the dropdown)
|
||||
* @param {string} obj.onEnvChange - the action that happens when an env is changed
|
||||
* @returns
|
||||
*/
|
||||
export default function NavHeaderSecrets({
|
||||
pageName,
|
||||
isProjectRelated,
|
||||
isOrganizationRelated,
|
||||
isSnapshot,
|
||||
currentEnv,
|
||||
userAvailableEnvs,
|
||||
onEnvChange
|
||||
}: {
|
||||
pageName: string;
|
||||
isProjectRelated?: boolean;
|
||||
isOrganizationRelated?: boolean;
|
||||
isSnapshot: boolean;
|
||||
currentEnv?: string;
|
||||
userAvailableEnvs?: any[];
|
||||
onEnvChange?: (slug: string) => void;
|
||||
}): JSX.Element {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${!isSnapshot && "absolute"} ml-6 flex cursor-default flex-row items-center pt-6`}
|
||||
>
|
||||
<div className="mr-3 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="text-md font-medium text-bunker-300">{currentOrg?.name}</div>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-md font-medium text-bunker-300">{currentWorkspace?.name}</div>
|
||||
</>
|
||||
)}
|
||||
{isOrganizationRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-md font-medium text-bunker-300">Organization Settings</div>
|
||||
</>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
{pageName === "Secrets" ? (
|
||||
<a
|
||||
className="text-md font-medium text-primary/80 hover:text-primary"
|
||||
href={`${router.asPath.split("?")[0]}`}
|
||||
>
|
||||
{pageName}
|
||||
</a>
|
||||
) : (
|
||||
<div className="text-md text-gray-400">{pageName}</div>
|
||||
)}
|
||||
{currentEnv && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-sm text-gray-400" />
|
||||
<div className="rounded-md pl-3 hover:bg-bunker-100/10">
|
||||
<Tooltip content="Select environment">
|
||||
<Select
|
||||
value={userAvailableEnvs?.filter((uae) => uae.name === currentEnv)[0]?.slug}
|
||||
onValueChange={(value) => {
|
||||
if (value && onEnvChange) onEnvChange(value);
|
||||
}}
|
||||
className="text-md bg-transparent pl-0 font-medium text-primary/80 hover:text-primary"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
||||
>
|
||||
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useLocation } from "@tanstack/react-router";
|
||||
|
||||
import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
@@ -73,10 +73,10 @@ const regions = [
|
||||
];
|
||||
|
||||
export const RegionSelect = () => {
|
||||
const router = useRouter();
|
||||
const location = useLocation();
|
||||
|
||||
const handleRegionSelect = (value: Region) => {
|
||||
router.push(`https://${value}.infisical.com/${router.pathname}`);
|
||||
window.location.assign(`https://${value}.infisical.com/${location.pathname}`);
|
||||
};
|
||||
|
||||
const shouldDisplay =
|
||||
@@ -116,7 +116,7 @@ export const RegionSelect = () => {
|
||||
title="Infisical Cloud data regions"
|
||||
subTitle="Select the closest region to you and your team. Contact Infisical if you need to migrate regions."
|
||||
>
|
||||
{regions.map(({ value, label, location, flag }) => (
|
||||
{regions.map(({ value, label, location: regionLocation, flag }) => (
|
||||
<div className="mb-6" key={value}>
|
||||
<p className="font-medium">
|
||||
<span className="mr-2 inline-block w-4">{flag}</span>
|
||||
@@ -133,7 +133,7 @@ export const RegionSelect = () => {
|
||||
</li>
|
||||
<li>
|
||||
<FontAwesomeIcon size="xs" className="mr-0.5 text-green" icon={faCheck} /> Hosted
|
||||
in {location}
|
||||
in {regionLocation}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,6 @@ export const NotificationContent = ({
|
||||
)}
|
||||
>
|
||||
{callToAction}
|
||||
|
||||
{copyActions && (
|
||||
<div className="flex h-7 flex-row items-center gap-2">
|
||||
{copyActions.map((action) => (
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { FC } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import z from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string().nonempty({ message: "Name is required" })
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
interface CreateOrgModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: ""
|
||||
}
|
||||
});
|
||||
|
||||
const { mutateAsync: createOrg } = useCreateOrg({
|
||||
invalidate: false
|
||||
});
|
||||
const { mutateAsync: selectOrg } = useSelectOrganization();
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
const organization = await createOrg({
|
||||
name
|
||||
});
|
||||
|
||||
await selectOrg({
|
||||
organizationId: organization.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created organization",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const,
|
||||
params: {
|
||||
organizationId: organization.id
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem("orgData.id", organization.id);
|
||||
|
||||
reset();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to created organization",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen}>
|
||||
<ModalContent
|
||||
title="Create Organization"
|
||||
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="Acme Corp" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex w-full gap-4">
|
||||
<Button
|
||||
className=""
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button className="" size="sm" variant="outline_bg" type="button" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateOrgModal } from "./CreateOrgModal";
|
||||
@@ -1,16 +1,37 @@
|
||||
import { FunctionComponent, ReactNode } from "react";
|
||||
import { BoundCanProps, Can } from "@casl/react";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
|
||||
|
||||
import { Tooltip } from "../v2";
|
||||
|
||||
export const OrgPermissionGuardBanner = () => {
|
||||
return (
|
||||
<div className="container mx-auto flex h-full items-center justify-center">
|
||||
<div className="flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your admin to gain access
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
label?: ReactNode;
|
||||
// this prop is used when there exist already a tooltip as helper text for users
|
||||
// so when permission is allowed same tooltip will be reused to show helpertext
|
||||
renderTooltip?: boolean;
|
||||
allowedLabel?: string;
|
||||
renderGuardBanner?: boolean;
|
||||
} & BoundCanProps<TOrgPermission>;
|
||||
|
||||
export const OrgPermissionCan: FunctionComponent<Props> = ({
|
||||
@@ -19,6 +40,7 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
|
||||
passThrough = true,
|
||||
renderTooltip,
|
||||
allowedLabel,
|
||||
renderGuardBanner,
|
||||
...props
|
||||
}) => {
|
||||
const { permission } = useOrgPermission();
|
||||
@@ -36,10 +58,14 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
|
||||
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
||||
}
|
||||
|
||||
if (isAllowed && renderTooltip) {
|
||||
if (isAllowed && renderTooltip && allowedLabel) {
|
||||
return <Tooltip content={allowedLabel}>{finalChild}</Tooltip>;
|
||||
}
|
||||
|
||||
if (!isAllowed && renderGuardBanner) {
|
||||
return <OrgPermissionGuardBanner />;
|
||||
}
|
||||
|
||||
if (!isAllowed) return null;
|
||||
|
||||
return finalChild;
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
import { FunctionComponent, ReactNode } from "react";
|
||||
import { AbilityTuple, MongoAbility } from "@casl/ability";
|
||||
import { Can } from "@casl/react";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext";
|
||||
|
||||
import { Tooltip } from "../v2/Tooltip";
|
||||
|
||||
export const ProjectPermissionGuardBanner = () => {
|
||||
return (
|
||||
<div className="container mx-auto flex h-full items-center justify-center">
|
||||
<div className="flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your admin to gain access
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Props<T extends AbilityTuple> = {
|
||||
label?: ReactNode;
|
||||
// this prop is used when there exist already a tooltip as helper text for users
|
||||
@@ -17,6 +37,7 @@ type Props<T extends AbilityTuple> = {
|
||||
I: T[0];
|
||||
a: T[1];
|
||||
ability?: MongoAbility<T>;
|
||||
renderGuardBanner?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectPermissionCan: FunctionComponent<Props<ProjectPermissionSet>> = ({
|
||||
@@ -25,6 +46,7 @@ export const ProjectPermissionCan: FunctionComponent<Props<ProjectPermissionSet>
|
||||
passThrough = true,
|
||||
renderTooltip,
|
||||
allowedLabel,
|
||||
renderGuardBanner,
|
||||
...props
|
||||
}) => {
|
||||
const { permission } = useProjectPermission();
|
||||
@@ -39,10 +61,14 @@ export const ProjectPermissionCan: FunctionComponent<Props<ProjectPermissionSet>
|
||||
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
||||
}
|
||||
|
||||
if (isAllowed && renderTooltip) {
|
||||
if (isAllowed && renderTooltip && allowedLabel) {
|
||||
return <Tooltip content={allowedLabel}>{finalChild}</Tooltip>;
|
||||
}
|
||||
|
||||
if (!isAllowed && renderGuardBanner) {
|
||||
return <ProjectPermissionGuardBanner />;
|
||||
}
|
||||
|
||||
if (!isAllowed) return null;
|
||||
|
||||
return finalChild;
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import z from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -67,7 +67,7 @@ interface NewProjectModalProps {
|
||||
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange" | "projectType">;
|
||||
|
||||
const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { permission } = useOrgPermission();
|
||||
const { user } = useUser();
|
||||
@@ -85,7 +85,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
||||
});
|
||||
|
||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
|
||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg.id, {
|
||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
createNotification({ text: "Project created", type: "success" });
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
router.push(getProjectHomePage(project));
|
||||
navigate({ to: getProjectHomePage(project), params: { projectId: project.id } });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create project", type: "error" });
|
||||
@@ -304,7 +304,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
|
||||
<div className="absolute bottom-0 right-0 mb-6 mr-6 flex items-start justify-end">
|
||||
<ModalClose>
|
||||
<Button colorSchema="secondary" variant="plain" className="py-2">
|
||||
Cancel
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/* credits: https://iamkate.com/code/tree-views/ */
|
||||
.tree {
|
||||
--spacing: 1.5rem;
|
||||
--radius: 4px;
|
||||
}
|
||||
|
||||
.tree li {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);
|
||||
}
|
||||
|
||||
.tree ul {
|
||||
margin-left: calc(var(--radius) - var(--spacing));
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.tree ul li {
|
||||
border-left: 2px solid #888;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.tree ul li:last-child {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.tree ul li::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: calc(var(--spacing) / -1);
|
||||
left: -2px;
|
||||
width: calc(var(--spacing) + 2px);
|
||||
height: calc(var(--spacing) + 13px);
|
||||
border: solid #888;
|
||||
border-radius: 0 0 0 8px;
|
||||
border-width: 0 0 2px 2px;
|
||||
transition: all 200ms linear;
|
||||
}
|
||||
|
||||
.details[open] summary ~ * {
|
||||
animation: sweep 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes sweep {
|
||||
0% {
|
||||
opacity: 0;
|
||||
margin-left: -10px;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree summary {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.tree summary::marker,
|
||||
.tree summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree summary:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tree summary:focus-visible {
|
||||
outline: 1px dotted #000;
|
||||
}
|
||||
|
||||
.tree li::after,
|
||||
.tree summary::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: calc(var(--spacing) / 2 - var(--radius));
|
||||
left: calc(var(--spacing) - var(--radius) - 1px);
|
||||
width: calc(2 * var(--radius));
|
||||
height: calc(2 * var(--radius));
|
||||
border-radius: 50%;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.tree summary::before {
|
||||
z-index: 1;
|
||||
background: #ddd 0 0;
|
||||
}
|
||||
|
||||
.tree details[open] > summary::before {
|
||||
background-position: calc(-2 * var(--radius)) 0;
|
||||
}
|
||||
|
||||
.collapsibleContent {
|
||||
/*overflow-y: hidden;*/
|
||||
}
|
||||
.collapsibleContent[data-state="open"] {
|
||||
animation: slideDown 300ms ease-out;
|
||||
}
|
||||
.collapsibleContent[data-state="closed"] {
|
||||
animation: slideUp 300ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
0% {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
height: var(--radix-collapsible-content-height);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
height: var(--radix-collapsible-content-height);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useState } from "react";
|
||||
import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetSecretReferenceTree } from "@app/hooks/api";
|
||||
import { TSecretReferenceTraceNode } from "@app/hooks/api/types";
|
||||
|
||||
import style from "./SecretReferenceDetails.module.css";
|
||||
|
||||
type Props = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretKey: string;
|
||||
};
|
||||
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/;
|
||||
export const hasSecretReference = (value: string | undefined) =>
|
||||
value ? INTERPOLATION_SYNTAX_REG.test(value) : false;
|
||||
|
||||
export const SecretReferenceNode = ({
|
||||
node,
|
||||
isRoot,
|
||||
secretKey
|
||||
}: {
|
||||
node: TSecretReferenceTraceNode;
|
||||
isRoot?: boolean;
|
||||
secretKey?: string;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasChildren = node.children.length > 0;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Collapsible.Root open={isOpen} className="" onOpenChange={setIsOpen}>
|
||||
<Collapsible.Trigger
|
||||
className={twMerge(
|
||||
hasChildren && "decoration-bunker-4ø00 underline-offset-4 data-[state=open]:underline",
|
||||
"[&>svg]:data-[state=open]:rotate-[90deg] [&>svg]:data-[state=open]:text-yellow-500"
|
||||
)}
|
||||
disabled={!hasChildren}
|
||||
>
|
||||
{hasChildren && (
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className="d mr-2 text-mineshaft-400 transition-transform duration-300 ease-linear"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
{isRoot
|
||||
? secretKey
|
||||
: `${node.environment}${
|
||||
node.secretPath === "/" ? "" : node.secretPath.split("/").join(".")
|
||||
}.${node.key}`}
|
||||
<Tooltip className="max-w-md break-words" content={node.value}>
|
||||
<span
|
||||
className={twMerge(
|
||||
"ml-1 px-1 text-xs text-mineshaft-400",
|
||||
!node.value && "text-red-400"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={node.value ? faEye : faEyeSlash} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content className={twMerge("mt-4", style.collapsibleContent)}>
|
||||
{hasChildren && (
|
||||
<ul>
|
||||
{node.children.map((el, index) => (
|
||||
<SecretReferenceNode node={el} key={`${el.key}-${index + 1}`} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
|
||||
const { data, isPending } = useGetSecretReferenceTree({
|
||||
secretPath,
|
||||
environmentSlug: environment,
|
||||
projectId,
|
||||
secretKey
|
||||
});
|
||||
|
||||
const tree = data?.tree;
|
||||
const secretValue = data?.value;
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Spinner size="xs" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControl label="Expanded value">
|
||||
<SecretInput
|
||||
key="value-overriden"
|
||||
isReadOnly
|
||||
value={secretValue}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-700 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mb-2" label="Reference Tree" />
|
||||
<div className="thin-scrollbar relative max-h-96 overflow-auto rounded-md border border-mineshaft-600 bg-bunker-700 py-6 text-sm text-mineshaft-200">
|
||||
{tree && (
|
||||
<ul className={style.tree}>
|
||||
<SecretReferenceNode node={tree} isRoot secretKey={secretKey} />
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-mineshaft-400">
|
||||
Click a secret key to view its sub-references.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { hasSecretReference, SecretReferenceTree } from "./SecretReferenceDetails";
|
||||
@@ -171,7 +171,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
Tag Color
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex items-center justify-center rounded border border-mineshaft-500 bg-mineshaft-900 p-2 ">
|
||||
<div className="flex items-center justify-center rounded border border-mineshaft-500 bg-mineshaft-900 p-2">
|
||||
<div
|
||||
className="h-6 w-6 rounded-full"
|
||||
style={{ background: `${selectedTagColor}` }}
|
||||
@@ -185,7 +185,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
<div key={`tag-color-${$tagColor.id}`}>
|
||||
<Tooltip content={`${$tagColor.name}`}>
|
||||
<div
|
||||
className=" flex h-[26px] w-[26px] items-center justify-center rounded-full border-2 border-transparent bg-[#bec2c8] p-2 hover:border-black hover:shadow-lg hover:ring-2 hover:ring-offset-2"
|
||||
className="flex h-[26px] w-[26px] items-center justify-center rounded-full border-2 border-transparent bg-[#bec2c8] p-2 hover:border-black hover:shadow-lg hover:ring-2 hover:ring-offset-2"
|
||||
key={`tag-${$tagColor.id}`}
|
||||
style={{ backgroundColor: `${$tagColor.hex}` }}
|
||||
onClick={() => setValue("color", $tagColor.hex)}
|
||||
@@ -204,7 +204,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="tags-hex-wrapper flex flex-grow items-center px-2">
|
||||
<div className="relative flex items-center rounded-md ">
|
||||
<div className="relative flex items-center rounded-md">
|
||||
{isValidHexColor(selectedTagColor) && (
|
||||
<div
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full"
|
||||
@@ -231,7 +231,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
<div className="mx-4 h-8 border border-mineshaft-500" />
|
||||
<div className="flex h-7 w-7 items-center justify-center">
|
||||
<div
|
||||
className={`flex h-7 w-7 cursor-pointer items-center justify-center rounded-sm border border-mineshaft-500 bg-transparent bg-mineshaft-900 p-2 hover:ring-2 hover:ring-offset-1 ${
|
||||
className={`flex h-7 w-7 cursor-pointer items-center justify-center rounded-sm border border-mineshaft-500 bg-mineshaft-900 bg-transparent p-2 hover:ring-2 hover:ring-offset-1 ${
|
||||
showHexInput ? "tags-conic-bg rounded-full" : ""
|
||||
}`}
|
||||
onClick={() => setShowHexInput((prev) => !prev)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAuthToken, setAuthToken, setMfaTempToken, setSignupTempToken } from "@app/reactQuery";
|
||||
import { setAuthToken, setMfaTempToken, setSignupTempToken } from "@app/hooks/api/reactQuery";
|
||||
|
||||
export const PROVIDER_AUTH_TOKEN_KEY = "infisical__provider-auth-token";
|
||||
|
||||
@@ -23,16 +23,4 @@ export default class SecurityClient {
|
||||
static setToken(tokenStr: string) {
|
||||
setAuthToken(tokenStr);
|
||||
}
|
||||
|
||||
static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
|
||||
const req = new Request(resource, options);
|
||||
|
||||
const token = getAuthToken();
|
||||
|
||||
if (token) {
|
||||
req.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
|
||||
return fetch(req);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params):
|
||||
const clientProof = clientOldPassword.getProof();
|
||||
|
||||
clientNewPassword.init({ username: email, password: newPassword }, async () => {
|
||||
clientNewPassword.createVerifier(async (err, result) => {
|
||||
clientNewPassword.createVerifier(async (_err, result) => {
|
||||
try {
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password: newPassword,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import { decryptPrivateKeyHelper } from "@app/helpers/key";
|
||||
import { login1, login2 } from "@app/hooks/api/auth/queries";
|
||||
import KeyService from "@app/services/KeyService";
|
||||
|
||||
import Telemetry from "./telemetry/Telemetry";
|
||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||
@@ -101,7 +101,7 @@ const attemptLogin = async ({
|
||||
// set JWT token
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = await KeyService.decryptPrivateKey({
|
||||
const privateKey = await decryptPrivateKeyHelper({
|
||||
encryptionVersion,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import { decryptPrivateKeyHelper } from "@app/helpers/key";
|
||||
import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||
import KeyService from "@app/services/KeyService";
|
||||
|
||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||
import SecurityClient from "./SecurityClient";
|
||||
@@ -71,7 +71,7 @@ const attemptLoginMfa = async ({
|
||||
SecurityClient.setToken(token);
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
|
||||
const privateKey = await KeyService.decryptPrivateKey({
|
||||
const privateKey = await decryptPrivateKeyHelper({
|
||||
encryptionVersion,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import { decryptPrivateKeyHelper } from "@app/helpers/key";
|
||||
import { login1, login2 } from "@app/hooks/api/auth/queries";
|
||||
import KeyService from "@app/services/KeyService";
|
||||
|
||||
import Telemetry from "./telemetry/Telemetry";
|
||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||
@@ -86,7 +86,7 @@ const attemptLogin = async ({
|
||||
// set JWT token
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = await KeyService.decryptPrivateKey({
|
||||
const privateKey = await decryptPrivateKeyHelper({
|
||||
encryptionVersion,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import { decryptPrivateKeyHelper } from "@app/helpers/key";
|
||||
import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||
import KeyService from "@app/services/KeyService";
|
||||
|
||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||
import SecurityClient from "./SecurityClient";
|
||||
@@ -27,7 +27,7 @@ const attemptLoginMfa = async ({
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
mfaToken: string;
|
||||
}): Promise<Boolean> => {
|
||||
}): Promise<boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.init(
|
||||
{
|
||||
@@ -63,7 +63,7 @@ const attemptLoginMfa = async ({
|
||||
SecurityClient.setToken(token);
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
|
||||
const privateKey = await KeyService.decryptPrivateKey({
|
||||
const privateKey = await decryptPrivateKeyHelper({
|
||||
encryptionVersion,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import argon2 from "argon2-browser";
|
||||
// @ts-expect-error to avoid wasm dependencies
|
||||
// eslint-disable-next-line
|
||||
import argon2 from "argon2-browser/dist/argon2-bundled.min.js";
|
||||
import nacl from "tweetnacl";
|
||||
import { decodeBase64, decodeUTF8, encodeBase64, encodeUTF8 } from "tweetnacl-util";
|
||||
|
||||
import aes from "./aes-256-gcm";
|
||||
|
||||
const nacl = require("tweetnacl");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
|
||||
/**
|
||||
* Return new base64, NaCl, public-private key pair.
|
||||
* @returns {Object} obj
|
||||
@@ -15,8 +16,8 @@ const generateKeyPair = () => {
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
return {
|
||||
publicKey: nacl.util.encodeBase64(pair.publicKey),
|
||||
privateKey: nacl.util.encodeBase64(pair.secretKey)
|
||||
publicKey: encodeBase64(pair.publicKey),
|
||||
privateKey: encodeBase64(pair.secretKey)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -34,8 +35,8 @@ type EncryptAsymmetricProps = {
|
||||
* @param {String} - base64-encoded Nacl public key
|
||||
*/
|
||||
const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => {
|
||||
const derivedPublicKey = nacl.util.encodeBase64(
|
||||
nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(privateKey)).publicKey
|
||||
const derivedPublicKey = encodeBase64(
|
||||
nacl.box.keyPair.fromSecretKey(decodeBase64(privateKey)).publicKey
|
||||
);
|
||||
|
||||
if (derivedPublicKey !== publicKey) {
|
||||
@@ -108,15 +109,15 @@ const encryptAssymmetric = ({
|
||||
} => {
|
||||
const nonce = nacl.randomBytes(24);
|
||||
const ciphertext = nacl.box(
|
||||
nacl.util.decodeUTF8(plaintext),
|
||||
decodeUTF8(plaintext),
|
||||
nonce,
|
||||
nacl.util.decodeBase64(publicKey),
|
||||
nacl.util.decodeBase64(privateKey)
|
||||
decodeBase64(publicKey),
|
||||
decodeBase64(privateKey)
|
||||
);
|
||||
|
||||
return {
|
||||
ciphertext: nacl.util.encodeBase64(ciphertext),
|
||||
nonce: nacl.util.encodeBase64(nonce)
|
||||
ciphertext: encodeBase64(ciphertext),
|
||||
nonce: encodeBase64(nonce)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -143,13 +144,13 @@ const decryptAssymmetric = ({
|
||||
privateKey
|
||||
}: DecryptAsymmetricProps): string => {
|
||||
const plaintext = nacl.box.open(
|
||||
nacl.util.decodeBase64(ciphertext),
|
||||
nacl.util.decodeBase64(nonce),
|
||||
nacl.util.decodeBase64(publicKey),
|
||||
nacl.util.decodeBase64(privateKey)
|
||||
decodeBase64(ciphertext),
|
||||
decodeBase64(nonce),
|
||||
decodeBase64(publicKey),
|
||||
decodeBase64(privateKey)
|
||||
);
|
||||
|
||||
return nacl.util.encodeUTF8(plaintext);
|
||||
return encodeUTF8(plaintext!);
|
||||
};
|
||||
|
||||
type EncryptSymmetricProps = {
|
||||
|
||||
@@ -73,7 +73,7 @@ const issueBackupKey = async ({
|
||||
},
|
||||
async () => {
|
||||
clientKey.createVerifier(
|
||||
async (err: any, result: { salt: string; verifier: string }) => {
|
||||
async (_err: any, result: { salt: string; verifier: string }) => {
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: String(localStorage.getItem("PRIVATE_KEY")),
|
||||
secret: generatedKey
|
||||
@@ -104,7 +104,7 @@ const issueBackupKey = async ({
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setBackupKeyError(true);
|
||||
console.log("Failed to issue a backup key");
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/* eslint-disable prefer-template */
|
||||
/* eslint-disable prefer-rest-params */
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable vars-on-top */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
|
||||
import { INTERCOMid as APPid } from "@app/components/utilities/config";
|
||||
|
||||
// Loads Intercom with the snippet
|
||||
// This must be run before boot, it initializes window.Intercom
|
||||
|
||||
// prettier-ignore
|
||||
export const load = () => {
|
||||
(function(){
|
||||
var w=window;
|
||||
var ic=w.Intercom;
|
||||
|
||||
if(typeof ic==="function"){
|
||||
ic("reattach_activator");
|
||||
ic("update",w.intercomSettings);
|
||||
} else {
|
||||
var d=document;
|
||||
var i=function() {
|
||||
i.c(arguments);
|
||||
};
|
||||
i.q=[];
|
||||
i.c=function(args) {
|
||||
i.q.push(args);
|
||||
};
|
||||
w.Intercom=i;
|
||||
var l=function() {
|
||||
var s=d.createElement("script");
|
||||
s.type="text/javascript";
|
||||
s.async=true;
|
||||
s.src="https://widget.intercom.io/widget/" + APPid;
|
||||
var x=d.getElementsByTagName("script")[0];
|
||||
x.parentNode.insertBefore(s, x);
|
||||
};
|
||||
if (document.readyState==="complete") {
|
||||
l();
|
||||
} else if (w.attachEvent) {
|
||||
w.attachEvent("onload",l);
|
||||
} else {
|
||||
w.addEventListener("load",l,false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// Initializes Intercom
|
||||
export const boot = (options = {}) => {
|
||||
window && window.Intercom && window.Intercom("boot", { appid: APPid, ...options });
|
||||
};
|
||||
|
||||
export const update = () => {
|
||||
window && window.Intercom && window.Intercom("update");
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useUser } from "@app/context";
|
||||
|
||||
import { boot as bootIntercom, load as loadIntercom, update as updateIntercom } from "./intercom";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const IntercomProvider = ({ children }: { children: any }) => {
|
||||
const { user } = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
loadIntercom();
|
||||
bootIntercom({
|
||||
name: `${user?.firstName || ""} ${user?.lastName || ""}`,
|
||||
email: user?.email || "",
|
||||
created_at: Math.floor((new Date(user?.createdAt)?.getTime() || 0) / 1000)
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleRouteChange = (url: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
updateIntercom();
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on("routeChangeStart", handleRouteChange);
|
||||
|
||||
// If the component is unmounted, unsubscribe
|
||||
// from the event with the `off` method:
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleRouteChange);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
return children;
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
|
||||
/**
|
||||
* This function downloads the secrets as a .env file
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
|
||||
* @returns
|
||||
*/
|
||||
const checkOverrides = async ({ data }: { data: SecretDataProps[] }) => {
|
||||
let secrets: SecretDataProps[] = data!.map((secret) => Object.create(secret));
|
||||
const overridenSecrets = data!.filter((secret) =>
|
||||
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
|
||||
? SecretType.Shared
|
||||
: SecretType.Personal
|
||||
);
|
||||
if (overridenSecrets.length) {
|
||||
overridenSecrets.forEach((secret) => {
|
||||
const index = secrets!.findIndex(
|
||||
(_secret) =>
|
||||
_secret.key === secret.key &&
|
||||
(secret.valueOverride === undefined || secret?.value !== secret?.valueOverride)
|
||||
);
|
||||
secrets![index].value = secret.value;
|
||||
});
|
||||
secrets = secrets!.filter(
|
||||
(secret) => secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
|
||||
);
|
||||
}
|
||||
return secrets;
|
||||
};
|
||||
|
||||
export default checkOverrides;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
|
||||
import checkOverrides from "./checkOverrides";
|
||||
|
||||
/**
|
||||
* This function downloads the secrets as a .env file
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
||||
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
||||
*/
|
||||
const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
|
||||
if (!data) return;
|
||||
const secrets = await checkOverrides({ data });
|
||||
|
||||
const file = secrets!
|
||||
.map(
|
||||
(item: SecretDataProps) =>
|
||||
`${
|
||||
item.comment
|
||||
? `${item.comment
|
||||
.split("\n")
|
||||
.map((comment) => "# ".concat(comment))
|
||||
.join("\n")}\n`
|
||||
: ""
|
||||
}${[item.key, item.value].join("=")}`
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const blob = new Blob([file]);
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
const alink = document.createElement("a");
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = `${env}.env`;
|
||||
alink.click();
|
||||
};
|
||||
|
||||
export default downloadDotEnv;
|
||||
@@ -1,43 +0,0 @@
|
||||
// import YAML from 'yaml';
|
||||
// import { YAMLSeq } from 'yaml/types';
|
||||
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
|
||||
// import { envMapping } from "../../../public/data/frequentConstants";
|
||||
// import checkOverrides from './checkOverrides';
|
||||
|
||||
/**
|
||||
* This function downloads the secrets as a .yml file
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
||||
* @param {string} obj.env - used for naming the file
|
||||
* @returns
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
|
||||
// if (!data) return;
|
||||
// const doc = new YAML.Document();
|
||||
// doc.contents = new YAMLSeq();
|
||||
// const secrets = await checkOverrides({ data });
|
||||
// secrets.forEach((secret) => {
|
||||
// const pair = YAML.createNode({ [secret.key]: secret.value });
|
||||
// pair.commentBefore = secret.comment
|
||||
// .split('\n')
|
||||
// .map((line) => (line ? ' '.concat(line) : ''))
|
||||
// .join('\n');
|
||||
// doc.add(pair);
|
||||
// });
|
||||
// const file = doc
|
||||
// .toString()
|
||||
// .split('\n')
|
||||
// .map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
|
||||
// .join('\n');
|
||||
// const blob = new Blob([file]);
|
||||
// const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
// const alink = document.createElement('a');
|
||||
// alink.href = fileDownloadUrl;
|
||||
// alink.download = envMapping[env] + '.yml';
|
||||
// alink.click();
|
||||
};
|
||||
|
||||
export default downloadYaml;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
/**
|
||||
* Encypt secrets before pushing the to the DB
|
||||
* @param {object} obj
|
||||
* @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
|
||||
* @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
|
||||
* @returns
|
||||
*/
|
||||
const encryptSecrets = async ({
|
||||
secretsToEncrypt,
|
||||
env
|
||||
}: {
|
||||
secretsToEncrypt: SecretDataProps[];
|
||||
workspaceId: string;
|
||||
env: string;
|
||||
}) => {
|
||||
let secrets;
|
||||
try {
|
||||
secrets = secretsToEncrypt.map((secret) => {
|
||||
const result = {
|
||||
id: secret.id,
|
||||
createdAt: "",
|
||||
environment: env,
|
||||
secretKey: secret.key,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment,
|
||||
tags: secret.tags
|
||||
};
|
||||
|
||||
return result;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error while encrypting secrets");
|
||||
}
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
export default encryptSecrets;
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./Accordion";
|
||||
|
||||
const meta: Meta<typeof Accordion> = {
|
||||
title: "Components/Accordion",
|
||||
component: Accordion,
|
||||
tags: ["v2"],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Accordion>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args) => (
|
||||
<div className="flex w-full justify-center">
|
||||
<Accordion {...args}>
|
||||
<AccordionItem value="section-1">
|
||||
<AccordionTrigger>Section 1</AccordionTrigger>
|
||||
<AccordionContent>Description of Section 1</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="section-2">
|
||||
<AccordionTrigger>Section 2</AccordionTrigger>
|
||||
<AccordionContent>Description of Section 2</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export const AccordionTrigger = forwardRef<
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
className={twMerge(
|
||||
"group flex h-11 flex-1 items-center justify-between py-2 px-4 outline-none hover:text-primary data-[state=open]:text-primary",
|
||||
"group flex h-11 flex-1 items-center justify-between px-4 py-2 outline-none hover:text-primary data-[state=open]:text-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -51,13 +51,13 @@ export const AccordionContent = forwardRef<
|
||||
>(({ children, className, ...props }, forwardedRef) => (
|
||||
<AccordionPrimitive.Content
|
||||
className={twMerge(
|
||||
"overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp",
|
||||
"overflow-hidden data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<div className="py-2 px-4 text-sm">{children}</div>
|
||||
<div className="px-4 py-2 text-sm">{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Alert, AlertDescription } from "./Alert";
|
||||
|
||||
const meta: Meta<typeof Alert> = {
|
||||
title: "Components/Alert",
|
||||
component: Alert,
|
||||
tags: ["v2"]
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Alert>;
|
||||
|
||||
const ExampleComponent = () => <AlertDescription>this is a description</AlertDescription>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: <ExampleComponent />
|
||||
}
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
children: <ExampleComponent />,
|
||||
variant: "warning"
|
||||
}
|
||||
};
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
children: <ExampleComponent />,
|
||||
variant: "danger"
|
||||
}
|
||||
};
|
||||
|
||||
export const WithCustomIcon: Story = {
|
||||
args: {
|
||||
children: <ExampleComponent />,
|
||||
variant: "warning",
|
||||
icon: <FontAwesomeIcon icon={faPlus} />
|
||||
}
|
||||
};
|
||||
|
||||
export const WithOutIcon: Story = {
|
||||
args: {
|
||||
children: <ExampleComponent />,
|
||||
variant: "warning",
|
||||
icon: null
|
||||
}
|
||||
};
|
||||
|
||||
export const WithOutTitle: Story = {
|
||||
args: {
|
||||
children: <ExampleComponent />,
|
||||
variant: "warning",
|
||||
hideTitle: true
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
faInfoCircle
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { type VariantProps, cva } from "cva";
|
||||
import { cva, type VariantProps } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
const alertVariants = cva(
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Button } from "./Button";
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: "Components/Button",
|
||||
component: Button,
|
||||
tags: ["v2"],
|
||||
argTypes: {
|
||||
isRounded: {
|
||||
defaultValue: true,
|
||||
type: "boolean"
|
||||
},
|
||||
isFullWidth: {
|
||||
defaultValue: false,
|
||||
type: "boolean"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical"
|
||||
}
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical",
|
||||
colorSchema: "secondary",
|
||||
variant: "outline"
|
||||
}
|
||||
};
|
||||
|
||||
export const Star: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical",
|
||||
variant: "star"
|
||||
}
|
||||
};
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical",
|
||||
colorSchema: "danger",
|
||||
variant: "solid"
|
||||
}
|
||||
};
|
||||
|
||||
export const Plain: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical",
|
||||
variant: "plain"
|
||||
}
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical",
|
||||
disabled: true
|
||||
}
|
||||
};
|
||||
|
||||
export const FullWidth: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical",
|
||||
isFullWidth: true
|
||||
}
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical",
|
||||
isLoading: true
|
||||
}
|
||||
};
|
||||
|
||||
export const LeftIcon: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical",
|
||||
leftIcon: <FontAwesomeIcon icon={faPlus} className="pr-0.5" />
|
||||
}
|
||||
};
|
||||
|
||||
export const RightIcon: Story = {
|
||||
args: {
|
||||
children: "Hello Infisical",
|
||||
rightIcon: <FontAwesomeIcon icon={faPlus} className="pr-0.5" />
|
||||
}
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Card, CardBody, CardFooter, CardProps, CardTitle } from "./Card";
|
||||
|
||||
const meta: Meta<typeof Card> = {
|
||||
title: "Components/Card",
|
||||
component: Card,
|
||||
tags: ["v2"],
|
||||
argTypes: {
|
||||
isRounded: {
|
||||
type: "boolean",
|
||||
defaultValue: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Card>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
const Template = (args: CardProps) => (
|
||||
<div className="w-96">
|
||||
<Card {...args}>
|
||||
<CardTitle subTitle="Please add your subtitle here">Title</CardTitle>
|
||||
<CardBody>Content</CardBody>
|
||||
<CardFooter>Footer</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args) => <Template {...args} />
|
||||
};
|
||||
|
||||
export const Hoverable: Story = {
|
||||
render: (args) => <Template {...args} />,
|
||||
args: {
|
||||
isHoverable: true
|
||||
}
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Checkbox } from "./Checkbox";
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: "Components/Checkbox",
|
||||
component: Checkbox,
|
||||
tags: ["v2"],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Checkbox>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Simple: Story = {
|
||||
args: {
|
||||
children: "Accept the condition"
|
||||
}
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: "Accept the condition",
|
||||
isDisabled: true
|
||||
}
|
||||
};
|
||||
|
||||
export const Required: Story = {
|
||||
args: {
|
||||
children: "Accept the condition",
|
||||
isRequired: true
|
||||
}
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { faCaretDown, faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import { ByComparator } from "@headlessui/react/dist/types";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type ComboBoxProps<T extends object> = {
|
||||
value?: T;
|
||||
className?: string;
|
||||
items: {
|
||||
value: T;
|
||||
key: string;
|
||||
label: string;
|
||||
}[];
|
||||
by: ByComparator<T>;
|
||||
defaultValue?: T;
|
||||
displayValue: (value: T) => string;
|
||||
onSelectChange: (value: T) => void;
|
||||
onFilter: (value: { value: T }, filterQuery: string) => boolean;
|
||||
};
|
||||
|
||||
// TODO(akhilmhdh): This is a very temporary one due to limitation of present situation
|
||||
// don't mind the api for now will be switched to aria later
|
||||
export const ComboBox = <T extends object>({
|
||||
onSelectChange,
|
||||
onFilter,
|
||||
displayValue,
|
||||
by,
|
||||
items,
|
||||
...props
|
||||
}: ComboBoxProps<T>) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const filteredResult =
|
||||
query === "" ? items.slice(0, 20) : items.filter((el) => onFilter(el, query)).slice(0, 20);
|
||||
|
||||
return (
|
||||
<Combobox by={by} {...props} onChange={onSelectChange}>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
displayValue={displayValue}
|
||||
className=" inline-flex w-full items-center justify-between rounded-md bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200"
|
||||
/>
|
||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<FontAwesomeIcon icon={faCaretDown} size="sm" aria-hidden="true" />
|
||||
</Combobox.Button>
|
||||
<Transition
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => setQuery("")}
|
||||
>
|
||||
<Combobox.Options className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md bg-mineshaft-900 py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
|
||||
{filteredResult.map(({ value, key, label }) => (
|
||||
<Combobox.Option
|
||||
key={key}
|
||||
value={value}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 transition-all hover:bg-mineshaft-500 ${
|
||||
active ? "text-primary" : "text-white"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{label}
|
||||
{selected ? (
|
||||
<div className={twMerge("absolute top-2 left-3 text-primary")}>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { ComboBox } from "./ComboBox";
|
||||
@@ -40,7 +40,7 @@ export const ContentLoader = ({ text, frequency = 2000, className }: Props) => {
|
||||
loading="lazy"
|
||||
/>
|
||||
{text && isTextArray && (
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
className="text-primary"
|
||||
key={`content-loader-${pos}`}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const CreatableSelect = <T,>({
|
||||
control: ({ isFocused }) =>
|
||||
twMerge(
|
||||
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
|
||||
"border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
|
||||
"w-full rounded-md border bg-mineshaft-900 p-0.5 font-inter text-mineshaft-200 hover:cursor-pointer"
|
||||
),
|
||||
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
|
||||
input: () => "pl-1 py-0.5",
|
||||
@@ -58,7 +58,7 @@ export const CreatableSelect = <T,>({
|
||||
twMerge(
|
||||
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
||||
isSelected && "text-mineshaft-200",
|
||||
"hover:cursor-pointer text-xs px-3 py-2"
|
||||
"px-3 py-2 text-xs hover:cursor-pointer"
|
||||
),
|
||||
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
||||
}}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { DatePicker } from "./DatePicker";
|
||||
|
||||
const meta: Meta<typeof DatePicker> = {
|
||||
title: "Components/DatePicker",
|
||||
component: DatePicker,
|
||||
tags: ["v2"],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DatePicker>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Drawer, DrawerContent, DrawerContentProps, DrawerTrigger } from "./Drawer";
|
||||
|
||||
const meta: Meta<typeof Drawer> = {
|
||||
title: "Components/Drawer",
|
||||
component: Drawer,
|
||||
tags: ["v2"],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DrawerContent>;
|
||||
|
||||
const Template = (args: DrawerContentProps) => (
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>
|
||||
<Button>Open</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent {...args}>Hello world</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args) => <Template {...args} />,
|
||||
args: {
|
||||
title: "Title",
|
||||
subTitle: "Something as subtitle",
|
||||
footerContent: "footer content"
|
||||
}
|
||||
};
|
||||
@@ -59,7 +59,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="close"
|
||||
className="absolute top-4 right-6 rounded text-bunker-400 hover:text-bunker-50"
|
||||
className="absolute right-6 top-4 rounded text-bunker-400 hover:text-bunker-50"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
|
||||
</IconButton>
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { faPlus, faTrash, faUndo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { IconButton } from "../IconButton";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "./Dropdown";
|
||||
|
||||
const meta: Meta<typeof DropdownMenu> = {
|
||||
title: "Components/DropdownMenu",
|
||||
component: DropdownMenu,
|
||||
tags: ["v2"],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DropdownMenuContent>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args) => (
|
||||
<div className="flex w-full justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton ariaLabel="add">
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" {...args}>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
<DropdownMenuItem>Undo</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Icons: Story = {
|
||||
render: (args) => (
|
||||
<div className="flex w-full justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton ariaLabel="add">
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" {...args}>
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faTrash} size="sm" />}>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUndo} size="sm" />}>
|
||||
Undo
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithDivider: Story = {
|
||||
render: (args) => (
|
||||
<div className="flex w-full justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton ariaLabel="add">
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" {...args}>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Undo</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Redo</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Group: Story = {
|
||||
render: (args) => (
|
||||
<div className="flex w-full justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton ariaLabel="add">
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" {...args}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Group</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Undo</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Group#2</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Undo</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
@@ -14,7 +14,7 @@ export type DropdownMenuTriggerProps = DropdownMenuPrimitive.DropdownMenuTrigger
|
||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
// item container
|
||||
export type DropdownMenuContentProps = DropdownMenuPrimitive.MenuContentProps;
|
||||
export type DropdownMenuContentProps = DropdownMenuPrimitive.DropdownMenuContentProps;
|
||||
export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
|
||||
({ children, className, ...props }, forwardedRef) => {
|
||||
return (
|
||||
@@ -24,7 +24,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={twMerge(
|
||||
"z-30 min-w-[220px] overflow-y-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
"z-30 min-w-[220px] overflow-y-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -38,7 +38,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
||||
DropdownMenuContent.displayName = "DropdownMenuContent";
|
||||
|
||||
// item container
|
||||
export type DropdownSubMenuContentProps = DropdownMenuPrimitive.MenuSubContentProps;
|
||||
export type DropdownSubMenuContentProps = DropdownMenuPrimitive.DropdownMenuSubContentProps;
|
||||
export const DropdownSubMenuContent = forwardRef<HTMLDivElement, DropdownSubMenuContentProps>(
|
||||
({ children, className, ...props }, forwardedRef) => {
|
||||
return (
|
||||
@@ -48,7 +48,7 @@ export const DropdownSubMenuContent = forwardRef<HTMLDivElement, DropdownSubMenu
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={twMerge(
|
||||
"z-30 min-w-[220px] rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
"z-30 min-w-[220px] rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -62,17 +62,17 @@ export const DropdownSubMenuContent = forwardRef<HTMLDivElement, DropdownSubMenu
|
||||
DropdownSubMenuContent.displayName = "DropdownMenuContent";
|
||||
|
||||
// item label component
|
||||
export type DropdownLabelProps = DropdownMenuPrimitive.MenuLabelProps;
|
||||
export type DropdownLabelProps = DropdownMenuPrimitive.DropdownMenuLabelProps;
|
||||
export const DropdownMenuLabel = ({ className, ...props }: DropdownLabelProps) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
{...props}
|
||||
className={twMerge("px-4 pt-2 pb-1 text-xs text-bunker-400", className)}
|
||||
className={twMerge("px-4 pb-1 pt-2 text-xs text-bunker-400", className)}
|
||||
/>
|
||||
);
|
||||
|
||||
// dropdown items
|
||||
export type DropdownMenuItemProps<T extends ElementType> =
|
||||
DropdownMenuPrimitive.MenuContentProps & {
|
||||
DropdownMenuPrimitive.DropdownMenuContentProps & {
|
||||
icon?: ReactNode;
|
||||
as?: T;
|
||||
inputRef?: Ref<T>;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { EmptyState } from "./EmptyState";
|
||||
|
||||
const meta: Meta<typeof EmptyState> = {
|
||||
title: "Components/EmptyState",
|
||||
component: EmptyState,
|
||||
tags: ["v2"],
|
||||
argTypes: {},
|
||||
args: {
|
||||
title: "No members found"
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EmptyState>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args) => <EmptyState {...args} />
|
||||
};
|
||||
@@ -41,12 +41,12 @@ export const FilterableSelect = <T,>({
|
||||
}}
|
||||
classNames={{
|
||||
container: ({ isDisabled }) =>
|
||||
twMerge("w-full text-sm font-inter", isDisabled && "!pointer-events-auto opacity-50"),
|
||||
twMerge("w-full font-inter text-sm", isDisabled && "!pointer-events-auto opacity-50"),
|
||||
control: ({ isFocused, isDisabled }) =>
|
||||
twMerge(
|
||||
isFocused ? "border-primary-400/50" : "border-mineshaft-600 ",
|
||||
`border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 ${
|
||||
isDisabled ? "!cursor-not-allowed" : "hover:border-gray-400 hover:cursor-pointer"
|
||||
isFocused ? "border-primary-400/50" : "border-mineshaft-600",
|
||||
`w-full rounded-md border bg-mineshaft-900 p-0.5 font-inter text-mineshaft-200 ${
|
||||
isDisabled ? "!cursor-not-allowed" : "hover:cursor-pointer hover:border-gray-400"
|
||||
} `
|
||||
),
|
||||
placeholder: () =>
|
||||
@@ -72,7 +72,7 @@ export const FilterableSelect = <T,>({
|
||||
twMerge(
|
||||
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
||||
isSelected && "text-mineshaft-200",
|
||||
"hover:cursor-pointer rounded text-xs px-3 py-2"
|
||||
"rounded px-3 py-2 text-xs hover:cursor-pointer"
|
||||
),
|
||||
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
||||
}}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
// Be careful on dep cycle
|
||||
import { Input } from "../Input/Input";
|
||||
import { FormControl } from "./FormControl";
|
||||
|
||||
const meta: Meta<typeof FormControl> = {
|
||||
title: "Components/FormControl",
|
||||
component: FormControl,
|
||||
tags: ["v2"],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FormControl>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Basic: Story = {
|
||||
args: {
|
||||
children: <Input />,
|
||||
label: "Email",
|
||||
id: "email",
|
||||
helperText: "Type something.."
|
||||
}
|
||||
};
|
||||
|
||||
export const RequiredInput: Story = {
|
||||
args: {
|
||||
...Basic.args,
|
||||
isRequired: true
|
||||
}
|
||||
};
|
||||
|
||||
export const ErrorInput: Story = {
|
||||
args: {
|
||||
...Basic.args,
|
||||
errorText: "Some random error",
|
||||
isError: true
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardContentProps,
|
||||
HoverCardTrigger
|
||||
} from "./HoverCardv2";
|
||||
|
||||
const meta: Meta<typeof HoverCardContent> = {
|
||||
title: "Components/HoverCard",
|
||||
component: HoverCard,
|
||||
tags: ["v2"]
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof HoverCardContent>;
|
||||
|
||||
const Template = (args: HoverCardContentProps) => (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<Button>Hello world</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent {...args} sideOffset={10}>
|
||||
<div className="bg-mineshaft-600 p-4 text-gray-400">
|
||||
<h1>This is hover card</h1>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
render: (args) => <Template {...args} />
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { IconButton } from "./IconButton";
|
||||
|
||||
const meta: Meta<typeof IconButton> = {
|
||||
title: "Components/IconButton",
|
||||
component: IconButton,
|
||||
tags: ["v2"],
|
||||
argTypes: {
|
||||
isRounded: {
|
||||
defaultValue: true,
|
||||
type: "boolean"
|
||||
},
|
||||
ariaLabel: {
|
||||
defaultValue: "Some buttons..."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof IconButton>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: <FontAwesomeIcon icon={faPlus} />
|
||||
}
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: <FontAwesomeIcon icon={faPlus} />,
|
||||
colorSchema: "secondary",
|
||||
variant: "outline"
|
||||
}
|
||||
};
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
children: <FontAwesomeIcon icon={faPlus} />,
|
||||
colorSchema: "danger",
|
||||
variant: "solid"
|
||||
}
|
||||
};
|
||||
|
||||
export const Plain: Story = {
|
||||
args: {
|
||||
children: <FontAwesomeIcon icon={faPlus} />,
|
||||
variant: "plain"
|
||||
}
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: <FontAwesomeIcon icon={faPlus} />,
|
||||
disabled: true
|
||||
}
|
||||
};
|
||||
@@ -294,7 +294,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md text-white"
|
||||
className="h-full w-full max-w-60 flex-col items-center justify-center rounded-md text-white"
|
||||
ref={popoverContentRef}
|
||||
>
|
||||
{suggestions.map((item, i) => {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { faEye, faMailBulk } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Input } from "./Input";
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: "Components/Input",
|
||||
component: Input,
|
||||
tags: ["v2"],
|
||||
argTypes: {
|
||||
isRounded: {
|
||||
defaultValue: true,
|
||||
type: "boolean"
|
||||
},
|
||||
placeholder: {
|
||||
defaultValue: "Type anything",
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Input>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Filled: Story = {
|
||||
args: {}
|
||||
};
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
variant: "outline"
|
||||
}
|
||||
};
|
||||
|
||||
export const Plain: Story = {
|
||||
args: {
|
||||
variant: "plain"
|
||||
}
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
isError: true
|
||||
}
|
||||
};
|
||||
|
||||
export const AutoWidth: Story = {
|
||||
args: {
|
||||
isFullWidth: false,
|
||||
className: "w-auto"
|
||||
}
|
||||
};
|
||||
|
||||
export const RightIcon: Story = {
|
||||
args: {
|
||||
rightIcon: <FontAwesomeIcon icon={faEye} />
|
||||
}
|
||||
};
|
||||
|
||||
export const LeftIcon: Story = {
|
||||
args: {
|
||||
leftIcon: <FontAwesomeIcon icon={faMailBulk} />
|
||||
}
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Menu, MenuGroup, MenuItem } from "./Menu";
|
||||
|
||||
const meta: Meta<typeof Menu> = {
|
||||
title: "Components/Menu",
|
||||
component: Menu,
|
||||
tags: ["v2"],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Menu>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args) => (
|
||||
<Menu {...args}>
|
||||
<MenuItem>Secrets</MenuItem>
|
||||
<MenuItem>Members</MenuItem>
|
||||
<MenuItem>Integrations</MenuItem>
|
||||
</Menu>
|
||||
),
|
||||
args: {}
|
||||
};
|
||||
|
||||
export const SelectedItem: Story = {
|
||||
render: (args) => (
|
||||
<Menu {...args}>
|
||||
<MenuItem isSelected>Secrets</MenuItem>
|
||||
<MenuItem>Members</MenuItem>
|
||||
<MenuItem>Integrations</MenuItem>
|
||||
</Menu>
|
||||
),
|
||||
args: {}
|
||||
};
|
||||
|
||||
export const GroupedItem: Story = {
|
||||
render: (args) => (
|
||||
<Menu {...args}>
|
||||
<MenuGroup title="Group 1">
|
||||
<MenuItem>Secrets</MenuItem>
|
||||
<MenuItem>Members</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Group 2">
|
||||
<MenuItem>Secrets</MenuItem>
|
||||
<MenuItem>Members</MenuItem>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
),
|
||||
args: {}
|
||||
};
|
||||
|
||||
export const DisabledItem: Story = {
|
||||
render: (args) => (
|
||||
<Menu {...args}>
|
||||
<MenuGroup title="Group 1">
|
||||
<MenuItem isDisabled>Secrets</MenuItem>
|
||||
<MenuItem>Members</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Group 2">
|
||||
<MenuItem>Secrets</MenuItem>
|
||||
<MenuItem>Members</MenuItem>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
),
|
||||
args: {}
|
||||
};
|
||||
|
||||
export const WithIcons: Story = {
|
||||
render: (args) => (
|
||||
<Menu {...args}>
|
||||
<MenuGroup title="Group 1">
|
||||
<MenuItem isDisabled icon="system-outline-90-lock-closed">
|
||||
Secrets
|
||||
</MenuItem>
|
||||
<MenuItem icon="system-outline-96-groups">Members</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Group 2">
|
||||
<MenuItem icon="system-outline-90-lock-closed">Secrets</MenuItem>
|
||||
<MenuItem icon="system-outline-96-groups">Members</MenuItem>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
),
|
||||
args: {}
|
||||
};
|
||||
|
||||
export const WithDescription: Story = {
|
||||
render: (args) => (
|
||||
<Menu {...args}>
|
||||
<MenuItem
|
||||
isDisabled
|
||||
icon="system-outline-90-lock-closed"
|
||||
description="Some random description"
|
||||
>
|
||||
Secrets
|
||||
</MenuItem>
|
||||
<MenuItem icon="system-outline-96-groups" description="Some random description">
|
||||
Members
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
),
|
||||
args: {}
|
||||
};
|
||||
@@ -1,10 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/* eslint-disable global-require */
|
||||
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
|
||||
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
import { motion } from "framer-motion";
|
||||
import Lottie from "lottie-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type MenuProps = {
|
||||
@@ -40,8 +36,7 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
inputRef,
|
||||
...props
|
||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
|
||||
const iconRef = useRef();
|
||||
|
||||
const iconRef = useRef<DotLottie | null>(null);
|
||||
return (
|
||||
<div onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}>
|
||||
<li
|
||||
@@ -65,17 +60,17 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
isSelected ? "visisble" : "invisible"
|
||||
} absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`}
|
||||
/>
|
||||
{/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */}
|
||||
{icon && (
|
||||
<Lottie
|
||||
lottieRef={iconRef}
|
||||
style={{ width: 22, height: 22 }}
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
animationData={require(`../../../../public/lotties/${icon}.json`)}
|
||||
loop={false}
|
||||
autoplay={false}
|
||||
className="my-auto ml-[0.1rem] mr-3"
|
||||
/>
|
||||
<div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3">
|
||||
<DotLottieReact
|
||||
dotLottieRefCallback={(el) => {
|
||||
iconRef.current = el;
|
||||
}}
|
||||
src={`/lotties/${icon}.json`}
|
||||
loop
|
||||
className="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
</Item>
|
||||
@@ -86,55 +81,6 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
);
|
||||
};
|
||||
|
||||
export const SubMenuItem = <T extends ElementType = "button">({
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
isDisabled,
|
||||
isSelected,
|
||||
as: Item = "button",
|
||||
description,
|
||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
inputRef,
|
||||
...props
|
||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
|
||||
const iconRef = useRef();
|
||||
|
||||
return (
|
||||
<a onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}>
|
||||
<li
|
||||
className={twMerge(
|
||||
"duration-50 group mt-0.5 flex cursor-pointer flex-col rounded px-1 py-1 font-inter text-sm text-mineshaft-300 transition-all hover:bg-mineshaft-700 hover:text-mineshaft-100",
|
||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<motion.span className="flex w-full flex-row items-center justify-start rounded-sm pl-6">
|
||||
<Item
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="relative flex items-center"
|
||||
ref={inputRef}
|
||||
{...props}
|
||||
>
|
||||
<Lottie
|
||||
lottieRef={iconRef}
|
||||
style={{ width: 16, height: 16 }}
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
animationData={require(`../../../../public/lotties/${icon}.json`)}
|
||||
loop={false}
|
||||
autoplay={false}
|
||||
className="my-auto ml-[0.1rem] mr-3"
|
||||
/>
|
||||
<span className="flex-grow text-left text-sm">{children}</span>
|
||||
</Item>
|
||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||
</motion.span>
|
||||
</li>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
MenuItem.displayName = "MenuItem";
|
||||
|
||||
export type MenuGroupProps = {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Modal, ModalContent, ModalContentProps, ModalTrigger } from "./Modal";
|
||||
|
||||
const meta: Meta<typeof Modal> = {
|
||||
title: "Components/Modal",
|
||||
component: Modal,
|
||||
tags: ["v2"],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ModalContent>;
|
||||
|
||||
const Template = (args: ModalContentProps) => (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button>Open</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent {...args}>Hello world</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args) => <Template {...args} />,
|
||||
args: {
|
||||
title: "Title",
|
||||
subTitle: "Something as subtitle",
|
||||
footerContent: "footer content"
|
||||
}
|
||||
};
|
||||
@@ -40,7 +40,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
<Card
|
||||
isRounded
|
||||
className={twMerge(
|
||||
"thin-scrollbar fixed top-1/2 left-1/2 z-30 max-w-xl -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl dark:[color-scheme:dark]",
|
||||
"thin-scrollbar fixed left-1/2 top-1/2 z-30 max-w-xl -translate-x-2/4 -translate-y-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl dark:[color-scheme:dark]",
|
||||
className
|
||||
)}
|
||||
style={{ maxHeight: "90%" }}
|
||||
@@ -57,7 +57,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="close"
|
||||
className="absolute top-4 right-6 rounded text-bunker-400 hover:text-bunker-50"
|
||||
className="absolute right-6 top-4 rounded text-bunker-400 hover:text-bunker-50"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
|
||||
</IconButton>
|
||||
@@ -72,7 +72,7 @@ ModalContent.displayName = "ModalContent";
|
||||
|
||||
export type ModalProps = Omit<DialogPrimitive.DialogProps, "open"> & { isOpen?: boolean };
|
||||
export const Modal = ({ isOpen, ...props }: ModalProps) => (
|
||||
<DialogPrimitive.Root open={isOpen} {...props} modal/>
|
||||
<DialogPrimitive.Root open={isOpen} {...props} />
|
||||
);
|
||||
|
||||
export const ModalTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
@@ -4,23 +4,23 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Props = {
|
||||
icon?: IconDefinition;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
icon?: IconDefinition;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const NoticeBanner = ({ icon = faWarning, title, children, className }: Props) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} className="pr-6 text-4xl text-white/80" />
|
||||
<div className="flex w-full flex-col text-sm">
|
||||
<div className="mb-2 text-lg font-semibold">{title}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} className="pr-6 text-4xl text-white/80" />
|
||||
<div className="flex w-full flex-col text-sm">
|
||||
<div className="mb-2 text-lg font-semibold">{title}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// eslint-disable-next-line
|
||||
import { useArgs } from "@storybook/client-api";
|
||||
import type { Meta } from "@storybook/react";
|
||||
|
||||
import { Pagination, PaginationProps } from "./Pagination";
|
||||
|
||||
const meta: Meta<typeof Pagination> = {
|
||||
title: "Components/Pagination",
|
||||
component: Pagination,
|
||||
tags: ["v2"],
|
||||
args: {
|
||||
count: 50
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
// type Story = StoryObj<typeof Pagination>;
|
||||
|
||||
export const Primary = (args: PaginationProps) => {
|
||||
const [, updateArgs] = useArgs();
|
||||
|
||||
return (
|
||||
<Pagination
|
||||
{...args}
|
||||
onChangePage={(page) => updateArgs({ page })}
|
||||
onChangePerPage={(perPage) => updateArgs({ perPage, page: 1 })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -49,7 +49,7 @@ export const Pagination = ({
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full items-center justify-end border-t border-mineshaft-600 bg-mineshaft-800 py-3 px-4 text-white",
|
||||
"flex w-full items-center justify-end border-t border-mineshaft-600 bg-mineshaft-800 px-4 py-3 text-white",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const PopoverObject = ({ children, text, onChangeHandler, id }: Props) =>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
className="z-[100] min-h-fit w-[460px] rounded border border-chicago-700 bg-mineshaft-600 p-3 shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2)] will-change-[transform,opacity] focus:shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2),0_0_0_2px_theme(colors.violet7)] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
|
||||
className="z-[100] min-h-fit w-[460px] rounded border border-chicago-700 bg-mineshaft-600 p-3 shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2)] will-change-[transform,opacity] focus:shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2),0_0_0_2px_theme(colors.violet7)] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=top]:animate-slideDownAndFade"
|
||||
sideOffset={5}
|
||||
hideWhenDetached
|
||||
side="left"
|
||||
@@ -32,13 +32,13 @@ export const PopoverObject = ({ children, text, onChangeHandler, id }: Props) =>
|
||||
onChange={(e) => onChangeHandler(e.target.value, id)}
|
||||
// type={type}
|
||||
value={text}
|
||||
className="ph-no-capture placeholder peer z-10 h-[20rem] w-full rounded-md border border-mineshaft-500 bg-bunker-600 py-2.5 px-2 text-sm text-bunker-300 caret-bunker-200 outline-none duration-200 placeholder:text-bunker-400 focus:text-bunker-100 placeholder:focus:text-transparent dark:[color-scheme:dark]"
|
||||
className="ph-no-capture placeholder peer z-10 h-[20rem] w-full rounded-md border border-mineshaft-500 bg-bunker-600 px-2 py-2.5 text-sm text-bunker-300 caret-bunker-200 outline-none duration-200 placeholder:text-bunker-400 focus:text-bunker-100 placeholder:focus:text-transparent dark:[color-scheme:dark]"
|
||||
spellCheck="false"
|
||||
placeholder="–"
|
||||
/>
|
||||
</div>
|
||||
<Popover.Close
|
||||
className="hover:bg-violet4 focus:shadow-violet7 absolute top-[5px] right-[5px] inline-flex h-[25px] w-[25px] cursor-default items-center justify-center rounded-full text-bunker-300 outline-none hover:text-white focus:shadow-[0_0_0_2px]"
|
||||
className="hover:bg-violet4 focus:shadow-violet7 absolute right-[5px] top-[5px] inline-flex h-[25px] w-[25px] cursor-default items-center justify-center rounded-full text-bunker-300 outline-none hover:text-white focus:shadow-[0_0_0_2px]"
|
||||
aria-label="Close"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user