Move ssh out to org level

This commit is contained in:
Tuan Dang
2024-12-15 20:43:13 -08:00
214 changed files with 6277 additions and 2125 deletions
+35 -155
View File
@@ -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 &rarr;</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>
);
@@ -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