mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 10:35:17 +00:00
second iteration of the new sidebar
This commit is contained in:
@@ -1,385 +0,0 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faBookOpen,
|
||||
faFileLines,
|
||||
faGear,
|
||||
faKey,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faPlus,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
|
||||
import getOrganizationUsers from "@app/pages/api/organization/GetOrgUsers";
|
||||
import getUser from "@app/pages/api/user/getUser";
|
||||
import addUserToWorkspace from "@app/pages/api/workspace/addUserToWorkspace";
|
||||
import createWorkspace from "@app/pages/api/workspace/createWorkspace";
|
||||
import getWorkspaces from "@app/pages/api/workspace/getWorkspaces";
|
||||
import uploadKeys from "@app/pages/api/workspace/uploadKeys";
|
||||
|
||||
import NavBarDashboard from "../navigation/NavBarDashboard";
|
||||
import onboardingCheck from "../utilities/checks/OnboardingCheck";
|
||||
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
|
||||
import { decryptAssymmetric, encryptAssymmetric } from "../utilities/cryptography/crypto";
|
||||
import Button from "./buttons/Button";
|
||||
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
|
||||
import Listbox from "./Listbox";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
const router = useRouter();
|
||||
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
|
||||
const [workspaceSelected, setWorkspaceSelected] = useState("∞");
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
// TODO: what to do about the fact that 2ids can have the same name
|
||||
|
||||
/**
|
||||
* When a user creates a new workspace, redirect them to the page of the new workspace.
|
||||
* @param {*} workspaceName
|
||||
*/
|
||||
const submitModal = async (workspaceName: string, addAllUsers: boolean) => {
|
||||
setLoading(true);
|
||||
// timeout code.
|
||||
setTimeout(() => setLoading(false), 1500);
|
||||
|
||||
try {
|
||||
const workspaces = await getWorkspaces();
|
||||
const currentWorkspaces = workspaces.map((workspace) => workspace.name);
|
||||
if (!currentWorkspaces.includes(workspaceName)) {
|
||||
const newWorkspace = await createWorkspace({
|
||||
workspaceName,
|
||||
organizationId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
const newWorkspaceId = newWorkspace._id;
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
|
||||
const myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadKeys(newWorkspaceId, myUser._id, ciphertext, nonce);
|
||||
|
||||
if (addAllUsers) {
|
||||
console.log("adding other users");
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
orgUsers.map(async (user: any) => {
|
||||
if (user.status === "accepted" && user.email !== myUser.email) {
|
||||
const result = await addUserToWorkspace(user.user.email, newWorkspaceId);
|
||||
if (result?.invitee && result?.latestKey) {
|
||||
const TEMP_PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY");
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: result.latestKey.encryptedKey,
|
||||
nonce: result.latestKey.nonce,
|
||||
publicKey: result.latestKey.sender.publicKey,
|
||||
privateKey: TEMP_PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: result.invitee.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
uploadKeys(newWorkspaceId, result.invitee._id, inviteeCipherText, inviteeNonce);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setWorkspaceMapping((prevState) => ({
|
||||
...prevState,
|
||||
[workspaceName]: newWorkspaceId
|
||||
}));
|
||||
setWorkspaceSelected(workspaceName);
|
||||
setIsOpen(false);
|
||||
setNewWorkspaceName("");
|
||||
} else {
|
||||
console.error("A project with this name already exists.");
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
href: `/dashboard/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.secrets"),
|
||||
emoji: <FontAwesomeIcon icon={faKey} />
|
||||
},
|
||||
{
|
||||
href: `/users/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.members"),
|
||||
emoji: <FontAwesomeIcon icon={faUser} />
|
||||
},
|
||||
{
|
||||
href: `/integrations/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.integrations"),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />
|
||||
},
|
||||
{
|
||||
href: `/activity/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: "Audit Logs",
|
||||
emoji: <FontAwesomeIcon icon={faFileLines} />
|
||||
},
|
||||
{
|
||||
href: `/settings/project/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.project-settings"),
|
||||
emoji: <FontAwesomeIcon icon={faGear} />
|
||||
}
|
||||
],
|
||||
[t, workspaceMapping, workspaceSelected]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
|
||||
const putUserInWorkSpace = async () => {
|
||||
if (tempLocalStorage("orgData.id") === "") {
|
||||
const userOrgs = await getOrganizations();
|
||||
localStorage.setItem("orgData.id", userOrgs[0]._id);
|
||||
}
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
const userWorkspaces = orgUserProjects;
|
||||
if (
|
||||
(userWorkspaces.length === 0 &&
|
||||
router.asPath !== "/noprojects" &&
|
||||
!router.asPath.includes("home") &&
|
||||
!router.asPath.includes("settings")) ||
|
||||
router.asPath === "/dashboard/undefined"
|
||||
) {
|
||||
router.push("/noprojects");
|
||||
} else if (router.asPath !== "/noprojects") {
|
||||
const intendedWorkspaceId = router.asPath
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0];
|
||||
|
||||
if (!["callback", "create", "authorize"].includes(intendedWorkspaceId)) {
|
||||
localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||
}
|
||||
|
||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||
if (
|
||||
!["callback", "create", "authorize"].includes(intendedWorkspaceId) &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
) {
|
||||
router.push(`/dashboard/${userWorkspaces[0]._id}`);
|
||||
} else {
|
||||
setWorkspaceMapping(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
|
||||
) as any
|
||||
);
|
||||
setWorkspaceSelected(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
|
||||
)[router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
putUserInWorkSpace();
|
||||
onboardingCheck({ setTotalOnboardingActionsDone });
|
||||
}, [router.query.id]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (
|
||||
workspaceMapping[workspaceSelected as any] &&
|
||||
`${workspaceMapping[workspaceSelected as any]}` !==
|
||||
router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]
|
||||
) {
|
||||
localStorage.setItem("projectData.id", `${workspaceMapping[workspaceSelected as any]}`);
|
||||
router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}, [workspaceSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-screen w-full flex-col overflow-x-hidden hidden md:flex">
|
||||
<NavBarDashboard />
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row dark">
|
||||
<aside className="w-full border-r border-mineshaft-500 bg-bunker-600 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
{/* <div className="py-6"></div> */}
|
||||
<div>
|
||||
<div className="mt-6 mb-6 flex h-20 w-full flex-col items-center justify-center bg-bunker-600 px-4">
|
||||
<div className="ml-1 mb-1 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t("nav.menu.project")}
|
||||
</div>
|
||||
{Object.keys(workspaceMapping).length > 0 ? (
|
||||
<Listbox
|
||||
isSelected={workspaceSelected}
|
||||
onChange={setWorkspaceSelected}
|
||||
data={Object.keys(workspaceMapping)}
|
||||
buttonAction={openModal}
|
||||
text=""
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
text="Add Project"
|
||||
onButtonPressed={openModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ul>
|
||||
{Object.keys(workspaceMapping).length > 0 &&
|
||||
menuItems.map(({ href, title, emoji }) => (
|
||||
<li className="mx-2 mt-0.5" key={title}>
|
||||
{router.asPath.split("/")[1] === href.split("/")[1] &&
|
||||
(["project", "billing", "org", "personal"].includes(
|
||||
router.asPath.split("/")[2]
|
||||
)
|
||||
? router.asPath.split("/")[2] === href.split("/")[2]
|
||||
: true) ? (
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
) : router.asPath === "/noprojects" ? (
|
||||
<div className="flex rounded p-2.5 text-sm text-white">
|
||||
<p className="flex w-10 items-center justify-center text-lg">{emoji}</p>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={href}>
|
||||
<div className="flex cursor-pointer rounded p-2.5 text-sm text-white hover:bg-primary-50/5">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-40 mb-4 w-full px-2">
|
||||
{router.asPath.split("/")[1] === "home" ? (
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
|
||||
totalOnboardingActionsDone === 1 ? "14" : ""
|
||||
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
|
||||
totalOnboardingActionsDone === 3 ? "43" : ""
|
||||
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
|
||||
totalOnboardingActionsDone === 5 ? "71" : ""
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
|
||||
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
|
||||
totalOnboardingActionsDone === 1 ? "14" : ""
|
||||
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
|
||||
totalOnboardingActionsDone === 3 ? "43" : ""
|
||||
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
|
||||
totalOnboardingActionsDone === 5 ? "71" : ""
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<AddWorkspaceDialog
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
submitModal={submitModal}
|
||||
workspaceName={newWorkspaceName}
|
||||
setWorkspaceName={setNewWorkspaceName}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 z-[200] md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -182,7 +182,7 @@ const DropZone = ({
|
||||
|
||||
return loading ? (
|
||||
<div className="mb-16 flex items-center justify-center pt-16">
|
||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="google logo" />
|
||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
) : keysExist ? (
|
||||
<div
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/router";
|
||||
import axios from "axios"
|
||||
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
import Error from "../basic/Error";
|
||||
import attemptCliLogin from "../utilities/attemptCliLogin";
|
||||
@@ -84,9 +85,11 @@ export default function InitialLoginStep({
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
|
||||
router.push(`/org/${userOrg}/overview`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
|
||||
import { Button, Input } from "../v2";
|
||||
|
||||
/**
|
||||
* 1st step of login - user enters their username and password
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.email - email of user
|
||||
* @param {Function} obj.setEmail - function to set the email of user
|
||||
* @param {String} obj.password - password of user
|
||||
* @param {String} obj.setPassword - function to set the password of user
|
||||
* @param {Function} obj.setStep - function to set the login flow step
|
||||
* @returns
|
||||
*/
|
||||
export default function LoginStep({
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
setStep
|
||||
}: {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
password: string;
|
||||
setPassword: (password: string) => void;
|
||||
setStep: (step: number) => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const isLoginSuccessful = await attemptLogin({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||
// case: login was successful
|
||||
|
||||
if (isLoginSuccessful.mfaEnabled) {
|
||||
// case: login requires MFA step
|
||||
setStep(2);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setLoginError(true);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="w-full mx-auto h-full px-6">
|
||||
<p className="text-xl w-max mx-auto flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 mb-6">
|
||||
{t("login.login")}
|
||||
</p>
|
||||
<div className="flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
type="email"
|
||||
placeholder="Enter your email..."
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="password"
|
||||
placeholder="Enter your password..."
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
||||
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] px-2 mt-4 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
onClick={async () => handleLogin()}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-14'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
> {String(t("login.login"))} </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-bunker-400 text-sm flex flex-row w-max mx-auto">
|
||||
<Link href="/verify-email">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.forgot-password")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
{t("common.maintenance-alert")}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import axios from "axios"
|
||||
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa"
|
||||
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
|
||||
import { useSendMfaToken } from "@app/hooks/api/auth";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
import Error from "../basic/Error";
|
||||
import { Button } from "../v2";
|
||||
@@ -101,7 +102,7 @@ export default function MFAStep({
|
||||
// cli page
|
||||
router.push("/cli-redirect");
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
const isLoginSuccessful = await attemptLoginMfa({
|
||||
email,
|
||||
password,
|
||||
@@ -111,7 +112,11 @@ export default function MFAStep({
|
||||
|
||||
if (isLoginSuccessful) {
|
||||
setIsLoading(false);
|
||||
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/org/${userOrg}/overview`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +185,7 @@ export default function MFAStep({
|
||||
className='h-14'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
> {String(t("mfa.verify"))} </Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,7 +193,7 @@ export default function MFAStep({
|
||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
||||
<div className="mt-2 text-bunker-400 text-md flex flex-row">
|
||||
<button disabled={isLoading} onClick={handleResendMfaCode} type="button">
|
||||
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>
|
||||
{isLoadingResend
|
||||
? t("signup.step2-resend-progress")
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/router";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
import SecurityClient from "../utilities/SecurityClient";
|
||||
import { Button, Input } from "../v2";
|
||||
@@ -50,7 +51,9 @@ export default function PasswordInputStep({
|
||||
}
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrg = userOrgs[0]._id;
|
||||
router.push(`/org/${userOrg?._id}/overview`);
|
||||
}
|
||||
} catch (err) {
|
||||
setLoginError(true);
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faBook,
|
||||
faCoins,
|
||||
faEnvelope,
|
||||
faGear,
|
||||
faPlus,
|
||||
faRightFromBracket
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import {TFunction} from "i18next"
|
||||
|
||||
import logout from "@app/pages/api/auth/Logout";
|
||||
|
||||
import getOrganization from "../../pages/api/organization/GetOrg";
|
||||
import getOrganizations from "../../pages/api/organization/getOrgs";
|
||||
import getUser from "../../pages/api/user/getUser";
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
|
||||
const supportOptions = (t: TFunction) => [
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
|
||||
t("nav.support.slack"),
|
||||
"https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
|
||||
t("nav.support.docs"),
|
||||
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
|
||||
t("nav.support.issue"),
|
||||
"https://github.com/Infisical/infisical-cli/issues"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
|
||||
t("nav.support.email"),
|
||||
"mailto:[email protected]"
|
||||
]
|
||||
];
|
||||
|
||||
export interface ICurrentOrg {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the navigation bar in the main app.
|
||||
* It has two main components: support options and user menu (inlcudes billing, logout, org/user settings)
|
||||
* @returns NavBar
|
||||
*/
|
||||
export default function Navbar() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<IUser | undefined>();
|
||||
const [orgs, setOrgs] = useState([]);
|
||||
const [currentOrg, setCurrentOrg] = useState<ICurrentOrg | undefined>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const userData = await getUser();
|
||||
setUser(userData);
|
||||
const orgsData = await getOrganizations();
|
||||
setOrgs(orgsData);
|
||||
const currentUserOrg = await getOrganization({
|
||||
orgId: String(localStorage.getItem("orgData.id"))
|
||||
});
|
||||
setCurrentOrg(currentUserOrg);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const closeApp = async () => {
|
||||
await logout();
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[71]">
|
||||
<div className="m-auto flex justify-start items-center mx-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
|
||||
</div>
|
||||
<a href="#" className="text-2xl text-white font-semibold mx-2">
|
||||
Infisical
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex justify-start items-center mx-2 z-40">
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBook} className="text-xl mr-2" />
|
||||
Docs
|
||||
</a>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div className="mr-4">
|
||||
<Menu.Button className="inline-flex w-full justify-center px-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
|
||||
</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 mt-0.5 w-64 origin-top-right rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none px-2 py-1.5">
|
||||
{supportOptionsList.map(([icon, text, url]) => (
|
||||
<a
|
||||
key={guidGenerator()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="font-normal text-gray-300 duration-200 rounded-md w-full flex items-center py-0.5"
|
||||
>
|
||||
<div className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<Menu as="div" className="relative inline-block text-left mr-4">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center pr-2 pl-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
{user?.firstName} {user?.lastName}
|
||||
<FontAwesomeIcon
|
||||
icon={faAngleDown}
|
||||
className="ml-2 mt-1 text-sm text-gray-300 hover:text-lime-100"
|
||||
/>
|
||||
</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 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[999] ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 z-[100]">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t("nav.user.signed-in-as")}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/personal/${router.query.id}`)}
|
||||
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||
>
|
||||
<div className="bg-white/10 h-8 w-9 rounded-full flex items-center justify-center text-gray-300">
|
||||
{user?.firstName?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<p className="text-gray-300 px-2 pt-1 text-sm">
|
||||
{" "}
|
||||
{user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
<p className="text-gray-400 px-2 pb-1 text-xs"> {user?.email}</p>
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="text-lg text-gray-400 p-2 mr-1 rounded-md cursor-pointer hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t("nav.user.current-organization")}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}`)}
|
||||
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||
>
|
||||
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<p className="text-gray-300 px-2 text-sm">{currentOrg?.name}</p>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
// onClick={buttonAction}
|
||||
type="button"
|
||||
className="cursor-pointer w-full"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
|
||||
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faCoins} />
|
||||
<div className="text-sm">{t("nav.user.usage-billing")}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
// onClick={buttonAction}
|
||||
className="cursor-pointer w-full mb-2"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
|
||||
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
|
||||
>
|
||||
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
||||
<FontAwesomeIcon icon={faPlus} className="ml-1" />
|
||||
</span>
|
||||
<div className="text-sm ml-1">{t("nav.user.invite")}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{orgs?.length > 1 && (
|
||||
<div className="px-1 pt-1">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t("nav.user.other-organizations")}
|
||||
</div>
|
||||
<div className="flex flex-col items-start px-1 mt-3 mb-2">
|
||||
{orgs
|
||||
.filter(
|
||||
(org: { _id: string }) => org._id !== localStorage.getItem("orgData.id")
|
||||
)
|
||||
.map((org: { _id: string; name: string }) => (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={guidGenerator()}
|
||||
onClick={() => {
|
||||
localStorage.setItem("orgData.id", org._id);
|
||||
router.reload();
|
||||
}}
|
||||
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md"
|
||||
>
|
||||
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
|
||||
{org.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<p className="text-gray-300 px-2 text-sm">{org.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeApp}
|
||||
className={`${
|
||||
active ? "bg-red font-semibold text-white" : "text-gray-400"
|
||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||
>
|
||||
<div className="relative flex justify-start items-center cursor-pointer select-none">
|
||||
<FontAwesomeIcon
|
||||
className="text-lg ml-1.5 mr-3"
|
||||
icon={faRightFromBracket}
|
||||
/>
|
||||
{t("common.logout")}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -58,10 +58,16 @@ export default function NavHeader({
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center pt-6">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-black text-sm">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-bunker-300">{currentOrg?.name}</div>
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={`/org/${currentOrg?._id}/overview`}
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary pl-0.5">{currentOrg?.name}</a>
|
||||
</Link>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
|
||||
@@ -79,7 +85,7 @@ export default function NavHeader({
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{ pathname: "/dashboard/[id]", query: { id: router.query.id } }}
|
||||
href={{ pathname: "/project/[id]/secrets", query: { id: router.query.id } }}
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
||||
</Link>
|
||||
@@ -120,7 +126,7 @@ export default function NavHeader({
|
||||
{index + 1 === folders?.length ? (
|
||||
<span className="text-sm font-semibold text-bunker-300">{name}</span>
|
||||
) : (
|
||||
<Link passHref legacyBehavior href={{ pathname: "/dashboard/[id]", query }}>
|
||||
<Link passHref legacyBehavior href={{ pathname: "/project/[id]/secrets", query }}>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
||||
{name === "root" ? selectedEnv?.name : name}
|
||||
</a>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import addUserToOrg from "@app/pages/api/organization/addUserToOrg";
|
||||
import getWorkspaces from "@app/pages/api/workspace/getWorkspaces";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
import { Button, EmailServiceSetupModal } from "../v2";
|
||||
|
||||
@@ -21,9 +21,9 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
|
||||
// Redirect user to the getting started page
|
||||
const redirectToHome = async () => {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const userWorkspace = userWorkspaces[0]._id;
|
||||
router.push(`/dashboard/${userWorkspace}`);
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrg = userOrgs[0]._id;
|
||||
router.push(`/org/${userOrg?._id}/overview`);
|
||||
};
|
||||
|
||||
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
|
||||
|
||||
@@ -5,7 +5,6 @@ interface OnboardingCheckProps {
|
||||
setTotalOnboardingActionsDone?: (value: number) => void;
|
||||
setHasUserClickedSlack?: (value: boolean) => void;
|
||||
setHasUserClickedIntro?: (value: boolean) => void;
|
||||
setHasUserStarred?: (value: boolean) => void;
|
||||
setHasUserPushedSecrets?: (value: boolean) => void;
|
||||
setUsersInOrg?: (value: boolean) => void;
|
||||
}
|
||||
@@ -17,7 +16,6 @@ const onboardingCheck = async ({
|
||||
setTotalOnboardingActionsDone,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserClickedIntro,
|
||||
setHasUserStarred,
|
||||
setHasUserPushedSecrets,
|
||||
setUsersInOrg
|
||||
}: OnboardingCheckProps) => {
|
||||
@@ -46,14 +44,6 @@ const onboardingCheck = async ({
|
||||
}
|
||||
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
|
||||
|
||||
const userActionStar = await checkUserAction({
|
||||
action: "star_cta_clicked"
|
||||
});
|
||||
if (userActionStar) {
|
||||
countActions += 1;
|
||||
}
|
||||
if (setHasUserStarred) setHasUserStarred(!!userActionStar);
|
||||
|
||||
const orgId = localStorage.getItem("orgData.id");
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: orgId || ""
|
||||
|
||||
@@ -54,7 +54,6 @@ export const load = () => {
|
||||
|
||||
// Initializes Intercom
|
||||
export const boot = (options = {}) => {
|
||||
console.log("boot", { app_id: APP_ID, ...options })
|
||||
window &&
|
||||
window.Intercom &&
|
||||
window.Intercom("boot", { app_id: APP_ID, ...options });
|
||||
|
||||
@@ -21,7 +21,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={twMerge(
|
||||
"min-w-[220px] bg-bunker will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
"min-w-[220px] z-30 bg-mineshaft-900 border border-mineshaft-600 will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -62,7 +62,7 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
||||
<DropdownMenuPrimitive.Item
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"text-sm block font-inter px-4 py-2 data-[highlighted]:bg-gray-700 outline-none cursor-pointer",
|
||||
"text-xs text-mineshaft-200 block font-inter px-4 py-2 data-[highlighted]:bg-mineshaft-700 rounded-sm outline-none cursor-pointer",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
>
|
||||
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm">
|
||||
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
|
||||
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.2rem] rounded-md h-7 bg-primary`}/>
|
||||
<div className={`${isSelected ? "visisble" : "invisible"} -left-[0.28rem] absolute w-[0.07rem] rounded-md h-5 bg-primary`}/>
|
||||
{/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */}
|
||||
<Lottie
|
||||
lottieRef={iconRef}
|
||||
@@ -76,6 +76,53 @@ 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(
|
||||
"group px-1 py-1 mt-0.5 font-inter flex flex-col text-sm text-mineshaft-300 hover:text-mineshaft-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
|
||||
isDisabled && "hover:bg-transparent cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm pl-6">
|
||||
<Item type="button" role="menuitem" className="flex items-center relative" 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 = {
|
||||
|
||||
Reference in New Issue
Block a user