mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 21:37:59 +00:00
Move ssh out to org level
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -38,21 +38,17 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
useGetAccessRequestsCount,
|
||||
useGetOrgTrialUrl,
|
||||
useGetSecretApprovalRequestCount,
|
||||
useLogoutUser,
|
||||
useSelectOrganization
|
||||
} from "@app/hooks/api";
|
||||
import { useGetOrgTrialUrl, useLogoutUser, useSelectOrganization } from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { InsecureConnectionBanner } from "@app/layouts/AppLayout/components/InsecureConnectionBanner";
|
||||
import { ProjectSelect } from "@app/layouts/AppLayout/components/ProjectSelect";
|
||||
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||
import { Mfa } from "@app/views/Login/Mfa";
|
||||
import { CreateOrgModal } from "@app/views/Org/components";
|
||||
|
||||
import { ProjectSidebarItem } from "./components/ProjectSidebarItems";
|
||||
import { WishForm } from "./components/WishForm/WishForm";
|
||||
|
||||
interface LayoutProps {
|
||||
@@ -87,7 +83,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
|
||||
const { workspaces, currentWorkspace } = useWorkspace();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { orgs, currentOrg } = useOrganization();
|
||||
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
@@ -96,15 +92,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
const { user } = useUser();
|
||||
const { subscription } = useSubscription();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||
|
||||
const pendingRequestsCount = useMemo(() => {
|
||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
}, [secretApprovalReqCount, accessApprovalRequestCount]);
|
||||
|
||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||
|
||||
@@ -152,41 +139,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
if (tempLocalStorage("orgData.id") === "" && orgs?.[0]?.id) {
|
||||
localStorage.setItem("orgData.id", orgs?.[0]?.id);
|
||||
}
|
||||
|
||||
if (
|
||||
currentOrg &&
|
||||
((workspaces?.length === 0 && router.asPath.includes("project")) ||
|
||||
router.asPath.includes("/project/undefined") ||
|
||||
(!orgs?.map((org) => org.id)?.includes(router.query.id as string) &&
|
||||
!router.asPath.includes("project") &&
|
||||
!router.asPath.includes("personal") &&
|
||||
!router.asPath.includes("secret-scanning") &&
|
||||
!router.asPath.includes("integration")))
|
||||
) {
|
||||
router.push(`/org/${currentOrg?.id}/overview`);
|
||||
}
|
||||
// else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
|
||||
|
||||
// const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
|
||||
|
||||
// let intendedWorkspaceId;
|
||||
// if (pathSegments.length >= 2 && pathSegments[0] === "dashboard") {
|
||||
// [, intendedWorkspaceId] = pathSegments;
|
||||
// } else if (pathSegments.length >= 3 && pathSegments[0] === "settings") {
|
||||
// [, , intendedWorkspaceId] = pathSegments;
|
||||
// } else {
|
||||
// const lastPathSegments = router.asPath.split("/").pop();
|
||||
// if (lastPathSegments !== undefined) {
|
||||
// [intendedWorkspaceId] = lastPathSegments.split("?");
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!intendedWorkspaceId) return;
|
||||
|
||||
// if (!["callback", "create", "authorize"].includes(intendedWorkspaceId)) {
|
||||
// localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||
// }
|
||||
// }
|
||||
};
|
||||
putUserInOrg();
|
||||
}, [router.query.id]);
|
||||
@@ -214,9 +166,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
<div>
|
||||
{!router.asPath.includes("personal") && (
|
||||
<div className="flex h-12 cursor-default items-center px-3 pt-6">
|
||||
{(router.asPath.includes("project") ||
|
||||
router.asPath.includes("integrations")) && (
|
||||
<Link href={`/org/${currentOrg?.id}/overview`}>
|
||||
{(currentWorkspace || router.asPath.includes("integrations")) && (
|
||||
<Link href={`/org/${currentOrg?.id}/${currentWorkspace?.type}/overview`}>
|
||||
<div className="pl-1 pr-2 text-mineshaft-400 duration-200 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} />
|
||||
</div>
|
||||
@@ -379,7 +330,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
(!router.asPath.includes("personal") && currentWorkspace ? (
|
||||
<ProjectSelect />
|
||||
) : (
|
||||
<Link href={`/org/${currentOrg?.id}/overview`}>
|
||||
<Link href={`/org/${currentOrg?.id}/${currentWorkspace?.type}/overview`}>
|
||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
|
||||
Back to organization
|
||||
@@ -387,132 +338,61 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</Link>
|
||||
))}
|
||||
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
|
||||
{(router.asPath.includes("project") || router.asPath.includes("integrations")) &&
|
||||
currentWorkspace ? (
|
||||
<Menu>
|
||||
<Link href={`/project/${currentWorkspace?.id}/secrets/overview`} passHref>
|
||||
<ProjectSidebarItem />
|
||||
{router.pathname.startsWith("/org") && (
|
||||
<Menu className="mt-4">
|
||||
<Link
|
||||
href={`/org/${currentOrg?.id}/${ProjectType.SecretManager}/overview`}
|
||||
passHref
|
||||
>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes(
|
||||
`/project/${currentWorkspace?.id}/secrets`
|
||||
`/${ProjectType.SecretManager}/overview`
|
||||
)}
|
||||
icon="system-outline-90-lock-closed"
|
||||
icon="system-outline-165-view-carousel"
|
||||
>
|
||||
{t("nav.menu.secrets")}
|
||||
Secret Management
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/certificates`} passHref>
|
||||
<Link
|
||||
href={`/org/${currentOrg?.id}/${ProjectType.CertificateManager}/overview`}
|
||||
passHref
|
||||
>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/certificates`
|
||||
}
|
||||
icon="system-outline-90-lock-closed"
|
||||
isSelected={router.asPath.includes(
|
||||
`/${ProjectType.CertificateManager}/overview`
|
||||
)}
|
||||
icon="note"
|
||||
>
|
||||
Internal PKI
|
||||
Cert Management
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/kms`} passHref>
|
||||
<Link href={`/org/${currentOrg?.id}/${ProjectType.KMS}/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/project/${currentWorkspace?.id}/kms`}
|
||||
icon="system-outline-90-lock-closed"
|
||||
isSelected={router.asPath.includes(`/${ProjectType.KMS}/overview`)}
|
||||
icon="unlock"
|
||||
>
|
||||
Key Management
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/ssh`} passHref>
|
||||
<Link href={`/org/${currentOrg?.id}/ssh/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/project/${currentWorkspace?.id}/ssh`}
|
||||
icon="system-outline-90-lock-closed"
|
||||
isSelected={
|
||||
router.asPath === `/org/${currentWorkspace?.id}/ssh/overview`
|
||||
}
|
||||
icon="system-regular-126-verified-hover-verified"
|
||||
>
|
||||
SSH
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/members`
|
||||
}
|
||||
icon="system-outline-96-groups"
|
||||
>
|
||||
Access Control
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/integrations/${currentWorkspace?.id}`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/integrations")}
|
||||
icon="system-outline-82-extension"
|
||||
>
|
||||
{t("nav.menu.integrations")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/secret-rotation`} passHref>
|
||||
<a className="relative">
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/secret-rotation`
|
||||
}
|
||||
icon="rotation"
|
||||
>
|
||||
Secret Rotation
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/approval`} passHref>
|
||||
<a className="relative">
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/approval`
|
||||
}
|
||||
icon="system-outline-189-domain-verification"
|
||||
>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open ||
|
||||
accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<span className="ml-2 rounded border border-primary-400 bg-primary-600 py-0.5 px-1 text-xs font-semibold text-black">
|
||||
{pendingRequestsCount}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/settings`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/settings`
|
||||
}
|
||||
icon="system-outline-109-slider-toggle-settings"
|
||||
>
|
||||
{t("nav.menu.project-settings")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
</Menu>
|
||||
) : (
|
||||
<Menu className="mt-4">
|
||||
<Link href={`/org/${currentOrg?.id}/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/overview")}
|
||||
icon="system-outline-165-view-carousel"
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?.id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -5,6 +5,7 @@ import { faBugs, faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
@@ -62,7 +63,7 @@ const ErrorPage = ({ error }: { error: Error | null }) => {
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
// we need to go to /org/${orgId}/overview, but we need to do a full page reload to ensure that the error the user is facing is properly reset.
|
||||
window.location.assign(`/org/${orgId}/overview`)
|
||||
window.location.assign(`/org/${orgId}/${ProjectType.SecretManager}/overview`)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHome} className="mr-2" />
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Fragment, useMemo } 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 guidGenerator from "@app/components/utilities/randomId";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import { useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api";
|
||||
|
||||
const supportOptions = (t: TFunction) => [
|
||||
[
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faSlack} />,
|
||||
t("nav.support.slack"),
|
||||
"https://infisical.com/slack"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faBook} />,
|
||||
t("nav.support.docs"),
|
||||
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faGithub} />,
|
||||
t("nav.support.issue"),
|
||||
"https://github.com/Infisical/infisical-cli/issues"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" 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 const Navbar = () => {
|
||||
const router = useRouter();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { currentOrg, orgs } = useOrganization();
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
const { user } = useUser();
|
||||
|
||||
const logout = useLogoutUser();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// remove this memo
|
||||
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
|
||||
|
||||
const closeApp = async () => {
|
||||
try {
|
||||
console.log("Logging out...");
|
||||
await logout.mutateAsync();
|
||||
localStorage.removeItem("protectedKey");
|
||||
localStorage.removeItem("protectedKeyIV");
|
||||
localStorage.removeItem("protectedKeyTag");
|
||||
localStorage.removeItem("publicKey");
|
||||
localStorage.removeItem("encryptedPrivateKey");
|
||||
localStorage.removeItem("iv");
|
||||
localStorage.removeItem("tag");
|
||||
localStorage.removeItem("PRIVATE_KEY");
|
||||
localStorage.removeItem("orgData.id");
|
||||
localStorage.removeItem("projectData.id");
|
||||
router.push("/login");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-[70] border-b border-mineshaft-500 bg-mineshaft-900 text-white">
|
||||
<div className="flex w-full justify-between px-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="mx-2 text-2xl font-semibold text-white">
|
||||
Infisical
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative z-40 mx-2 flex items-center justify-start">
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mr-4 flex items-center rounded-md px-3 py-2 text-sm text-gray-200 duration-200 hover:bg-white/10"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBook} className="mr-2 text-xl" />
|
||||
Docs
|
||||
</a>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div className="mr-4">
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 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">
|
||||
<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 z-20 mt-0.5 w-64 origin-top-right rounded-md border border-mineshaft-700 bg-bunker px-2 py-1.5 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{supportOptionsList.map(([icon, text, url]) => (
|
||||
<a
|
||||
key={guidGenerator()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md py-0.5 font-normal text-gray-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/10 hover:text-gray-200">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<Menu as="div" className="relative mr-4 inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md py-2 pr-2 pl-2 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">
|
||||
{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="w-68 absolute right-0 z-[125] mt-0.5 origin-top-right divide-y divide-mineshaft-700 rounded-md border border-mineshaft-700 bg-mineshaft-900 shadow-lg ring-1 ring-black ring-opacity-5 drop-shadow-2xl focus:outline-none">
|
||||
<div className="px-1 py-1">
|
||||
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t("nav.user.signed-in-as")}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push("/personal-settings")}
|
||||
className="mx-1 my-1 flex cursor-pointer flex-row items-center rounded-md px-1 hover:bg-white/5"
|
||||
>
|
||||
<div className="flex h-8 w-9 items-center justify-center rounded-full bg-white/10 text-gray-300">
|
||||
{user?.firstName?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<p className="px-2 pt-1 text-sm text-gray-300">
|
||||
{" "}
|
||||
{user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
<p className="px-2 pb-1 text-xs text-gray-400">{user?.email}</p>
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="mr-1 cursor-pointer rounded-md p-2 text-lg text-gray-400 hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t("nav.user.current-organization")}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}`)}
|
||||
className="mt-2 flex cursor-pointer flex-row items-center rounded-md px-2 py-1 hover:bg-white/5"
|
||||
>
|
||||
<div className="flex h-7 w-8 items-center justify-center rounded-md bg-white/10 text-gray-300">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="px-2 text-sm text-gray-300">{currentOrg?.name}</p>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="cursor-pointer rounded-md p-2 text-lg text-gray-400 hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{subscription && subscription.slug !== null && (
|
||||
<button
|
||||
// onClick={buttonAction}
|
||||
type="button"
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
|
||||
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/5 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faCoins} />
|
||||
<div className="text-sm">{t("nav.user.usage-billing")}</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
// onClick={buttonAction}
|
||||
className="mb-2 w-full cursor-pointer"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
|
||||
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 pl-10 pr-4 text-gray-400 duration-200 hover:bg-primary/100 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="ml-1" />
|
||||
</span>
|
||||
<div className="ml-1 text-sm">{t("nav.user.invite")}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{orgs && orgs?.length > 1 && (
|
||||
<div className="px-1 pt-1">
|
||||
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t("nav.user.other-organizations")}
|
||||
</div>
|
||||
<div className="mt-3 mb-2 flex flex-col items-start px-1">
|
||||
{orgs
|
||||
?.filter((org: { id: string }) => org.id !== currentOrg?.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 w-full cursor-pointer flex-row items-center justify-start rounded-md p-1.5 hover:bg-white/5"
|
||||
>
|
||||
<div className="flex h-7 w-8 items-center justify-center rounded-md bg-white/10 text-gray-300">
|
||||
{org.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="px-2 text-sm text-gray-300">{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 cursor-pointer select-none items-center justify-start">
|
||||
<FontAwesomeIcon
|
||||
className="ml-1.5 mr-3 text-lg"
|
||||
icon={faRightFromBracket}
|
||||
/>
|
||||
{t("common.logout")}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||
<div className="mx-auto w-full border-t border-mineshaft-500 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
// direct user to start pro trial
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}}
|
||||
className="mx-auto py-4 text-center text-sm"
|
||||
>
|
||||
You are currently on the <span className="font-semibold">Starter</span> plan. Unlock the
|
||||
full power of Infisical on the{" "}
|
||||
<span className="font-semibold">Pro Free Trial →</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -16,10 +16,11 @@ import {
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { getProjectHomePage, getProjectTitle } from "@app/helpers/project";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||
import { Workspace } from "@app/hooks/api/workspace/types";
|
||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type TWorkspaceWithFaveProp = Workspace & { isFavorite: boolean };
|
||||
|
||||
@@ -138,6 +139,7 @@ export const ProjectSelect = () => {
|
||||
|
||||
const { options, value } = useMemo(() => {
|
||||
const projectOptions = workspaces
|
||||
.filter((el) => el.type === currentWorkspace?.type)
|
||||
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||
...w,
|
||||
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
||||
@@ -164,7 +166,9 @@ export const ProjectSelect = () => {
|
||||
|
||||
return (
|
||||
<div className="mt-5 mb-4 w-full p-3">
|
||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">Project</p>
|
||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
|
||||
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"}
|
||||
</p>
|
||||
<FilterableSelect
|
||||
className="text-sm"
|
||||
value={value}
|
||||
@@ -189,7 +193,7 @@ export const ProjectSelect = () => {
|
||||
// todo(akhi): this is not using react query because react query in overview is throwing error when envs are not exact same count
|
||||
// to reproduce change this back to router.push and switch between two projects with different env count
|
||||
// look into this on dashboard revamp
|
||||
window.location.assign(`/project/${project.id}/secrets/overview`);
|
||||
window.location.assign(getProjectHomePage(project));
|
||||
}}
|
||||
options={options}
|
||||
components={{
|
||||
@@ -206,6 +210,7 @@ export const ProjectSelect = () => {
|
||||
<NewProjectModal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||
projectType={currentWorkspace?.type || ProjectType.SecretManager}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Menu, MenuItem } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const ProjectSidebarItem = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||
|
||||
const pendingRequestsCount =
|
||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
|
||||
if (
|
||||
!currentWorkspace ||
|
||||
router.asPath.startsWith("personal") ||
|
||||
router.asPath.startsWith("integrations")
|
||||
) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
|
||||
const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager;
|
||||
const isCmek = currentWorkspace?.type === ProjectType.KMS;
|
||||
const isSsh = currentWorkspace?.type === ProjectType.SSH;
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
{isSecretManager && (
|
||||
<Link
|
||||
href={`/${ProjectType.SecretManager}/${currentWorkspace?.id}/secrets/overview`}
|
||||
passHref
|
||||
>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes(
|
||||
`/${ProjectType.SecretManager}/${currentWorkspace?.id}/secrets`
|
||||
)}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
{t("nav.menu.secrets")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isCertManager && (
|
||||
<Link
|
||||
href={`/${ProjectType.CertificateManager}/${currentWorkspace?.id}/certificates`}
|
||||
passHref
|
||||
>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath ===
|
||||
`/${ProjectType.CertificateManager}/${currentWorkspace?.id}/certificates`
|
||||
}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isCmek && (
|
||||
<Link href={`/${ProjectType.KMS}/${currentWorkspace?.id}/kms`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/${ProjectType.KMS}/${currentWorkspace?.id}/kms`}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isSsh && (
|
||||
<Link href={`/${ProjectType.SSH}/${currentWorkspace?.id}/ssh`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/${ProjectType.SSH}/${currentWorkspace?.id}/ssh`}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={`/${currentWorkspace.type}/${currentWorkspace?.id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.endsWith(`/${currentWorkspace?.id}/members`)}
|
||||
icon="system-outline-96-groups"
|
||||
>
|
||||
Access Control
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
{isSecretManager && (
|
||||
<Link href={`/integrations/${currentWorkspace?.id}`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/integrations")}
|
||||
icon="system-outline-82-extension"
|
||||
>
|
||||
{t("nav.menu.integrations")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isSecretManager && (
|
||||
<Link
|
||||
href={`/${ProjectType.SecretManager}/${currentWorkspace?.id}/secret-rotation`}
|
||||
passHref
|
||||
>
|
||||
<a className="relative">
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath ===
|
||||
`/${ProjectType.SecretManager}/${currentWorkspace?.id}/secret-rotation`
|
||||
}
|
||||
icon="rotation"
|
||||
>
|
||||
Secret Rotation
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isSecretManager && (
|
||||
<Link href={`/secret-manager/${currentWorkspace?.id}/approval`} passHref>
|
||||
<a className="relative">
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/${ProjectType.SecretManager}/${currentWorkspace?.id}/approval`
|
||||
}
|
||||
icon="system-outline-189-domain-verification"
|
||||
>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<span className="ml-2 rounded border border-primary-400 bg-primary-600 py-0.5 px-1 text-xs font-semibold text-black">
|
||||
{pendingRequestsCount}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={`/${currentWorkspace.type}/${currentWorkspace?.id}/settings`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.endsWith(`/${currentWorkspace?.id}/settings`)}
|
||||
icon="system-outline-109-slider-toggle-settings"
|
||||
>
|
||||
{t("nav.menu.project-settings")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ProjectSidebarItem } from "./ProjectSidebarItems";
|
||||
@@ -65,7 +65,7 @@ export const WishForm = () => {
|
||||
<PopoverTrigger asChild>
|
||||
<div className="text-md mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faRocketchat} className="mr-2" />
|
||||
Make a wish
|
||||
Request a feature
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
|
||||
Reference in New Issue
Block a user