feat: renamed old frontend and fixed some minor bugs

This commit is contained in:
=
2025-01-06 14:56:49 +05:30
parent 8c902d7699
commit 54377a44d3
2577 changed files with 35240 additions and 35116 deletions
@@ -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;
-72
View File
@@ -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;
}
@@ -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")} />}
@@ -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}
@@ -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")}
@@ -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>
+222
View File
@@ -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>
);
};
@@ -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"
@@ -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) => {
+1 -1
View File
@@ -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>
);
+3 -3
View File
@@ -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>
);
};
-112
View File
@@ -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;
-45
View File
@@ -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&apos;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>
@@ -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;
@@ -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
@@ -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
}
};
+1 -1
View File
@@ -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"
}
};
+1 -1
View File
@@ -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: {}
};
+12 -66
View File
@@ -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"
}
};
+3 -3
View File
@@ -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