feat: seperated layouts for each product line

This commit is contained in:
=
2025-07-02 01:16:48 +05:30
parent c66510f473
commit d82d22a198
18 changed files with 597 additions and 240 deletions
@@ -0,0 +1,62 @@
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
export const KmsLayout = () => {
const { currentWorkspace } = useWorkspace();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
KMS
</div>
<div className="mt-2 flex-grow">
<Menu>
<Link
to="/projects/$projectId/kms/overview"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Overview</MenuItem>}
</Link>
<Link
to="/projects/$projectId/kms/kmip"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>KMIP</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 dark:[color-scheme:dark]">
<Outlet />
</div>
</div>
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export { KmsLayout } from "./KmsLayout";
@@ -0,0 +1,102 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
export const PkiManagerLayout = () => {
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
PKI Manager
</div>
<div className="mt-2 flex-grow">
<Menu>
<Link
to="/projects/$projectId/cert-manager/subscribers"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Subscribers</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/certificate-templates"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}> Certificate Templates</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/cert-manager/certificates"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}> Certificates</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/certificate-authorities"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}> Certificates Authorities</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/cert-manager/alerting"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Alerting</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 dark:[color-scheme:dark]">
<Outlet />
</div>
</div>
</div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 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>
</>
);
};
@@ -0,0 +1 @@
export { PkiManagerLayout } from "./PkiManagerLayout";
@@ -0,0 +1,137 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useGetAccessRequestsCount,
useGetSecretApprovalRequestCount,
useGetSecretRotations
} from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
// This is a generic layout shared by all types of projects.
// If the product layout differs significantly, create a new layout as needed.
export const ProjectGeneralLayout = () => {
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
workspaceId
});
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({
projectSlug
});
// we only show the secret rotations v1 tab if they have existing rotations
const { data: secretRotations } = useGetSecretRotations({
workspaceId,
options: {
refetchOnMount: false
}
});
const pendingRequestsCount =
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
Project General
</div>
<div className="mt-2 flex-grow">
<Menu>
<Link
to={`/projects/$projectId/${ProjectType.SecretManager}/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>{t("nav.menu.secrets")}</MenuItem>
)}
</Link>
<Link
to={`/projects/$projectId/${ProjectType.SecretManager}/integrations` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>{t("nav.menu.integrations")}</MenuItem>
)}
</Link>
{Boolean(secretRotations?.length) && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Secret Rotation</MenuItem>}
</Link>
)}
<Link
to={`/projects/$projectId/${ProjectType.SecretManager}/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
Approvals
{Boolean(
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</MenuItem>
)}
</Link>
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>{t("nav.menu.project-settings")}</MenuItem>
)}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 dark:[color-scheme:dark]">
<Outlet />
</div>
</div>
</div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 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>
</>
);
};
@@ -0,0 +1 @@
export { ProjectGeneralLayout } from "./ProjectGeneralLayout";
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { faDotCircle, faHome, faMobile, faWindowMaximize } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import {
@@ -20,6 +20,8 @@ import { useProjectPermission, useWorkspace } from "@app/context";
import { AssumePrivilegeModeBanner } from "./components/AssumePrivilegeModeBanner";
import { useLocalStorageState } from "@app/hooks";
import { ShouldWrap } from "@app/components/utilities/ShouldWrapComponent";
import { getCurrentProductFromUrl } from "@app/helpers/project";
import { ProjectType } from "@app/hooks/api/workspace/types";
enum SidebarStyle {
Expanded = "expanded",
@@ -31,6 +33,7 @@ const MAX_SIDEBAR_SIZE = "220px";
// This is a generic layout shared by all types of projects.
// If the product layout differs significantly, create a new layout as needed.
export const ProjectLayout = () => {
const location = useLocation();
const { currentWorkspace } = useWorkspace();
const [sidebarStyle, setSidebarStyle] = useLocalStorageState(
"project-sidebar-style",
@@ -45,6 +48,13 @@ export const ProjectLayout = () => {
const maxSidebarWidth =
sidebarStyle === SidebarStyle.Collapsed ? MIN_SIDEBAR_SIZE : MAX_SIDEBAR_SIZE;
const currentProductType = getCurrentProductFromUrl(location.pathname);
const isSecretManager = currentProductType === ProjectType.SecretManager;
const isPki = currentProductType === ProjectType.CertificateManager;
const isKms = currentProductType === ProjectType.KMS;
const isSsh = currentProductType === ProjectType.SSH;
const isSecretScanning = currentProductType === ProjectType.SecretScanning;
return (
<>
<div
@@ -76,23 +86,18 @@ export const ProjectLayout = () => {
to="/projects/$projectId/secret-manager/overview"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={
<Lottie
className="inline-block h-6 w-6 shrink-0"
icon="sliding-carousel"
/>
}
>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
Secret Manager
</MenuItem>
)}
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isSecretManager}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="sliding-carousel" />
}
>
{isSecretManager && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
Secret Manager
</MenuItem>
</Link>
</ShouldWrap>
<ShouldWrap
@@ -105,18 +110,14 @@ export const ProjectLayout = () => {
to="/projects/$projectId/cert-manager/subscribers"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="note" />}
>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
PKI Manager
</MenuItem>
)}
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isPki}
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="note" />}
>
{isPki && <div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />}
PKI Manager
</MenuItem>
</Link>
</ShouldWrap>
<ShouldWrap
@@ -129,20 +130,14 @@ export const ProjectLayout = () => {
to="/projects/$projectId/kms/overview"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="unlock" />
}
>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
KMS
</MenuItem>
)}
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isKms}
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="unlock" />}
>
{isKms && <div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />}
KMS
</MenuItem>
</Link>
</ShouldWrap>
<ShouldWrap
@@ -155,20 +150,16 @@ export const ProjectLayout = () => {
to="/projects/$projectId/ssh/overview"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="verified" />
}
>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
SSH
</MenuItem>
)}
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isSsh}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="verified" />
}
>
{isSsh && <div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />}
SSH
</MenuItem>
</Link>
</ShouldWrap>
<ShouldWrap
@@ -178,23 +169,21 @@ export const ProjectLayout = () => {
position="right"
>
<Link
to="/projects/$projectId/secret-scanning/findings"
to="/projects/$projectId/secret-scanning/data-sources"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="secret-scan" />
}
>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
Secret Scanning
</MenuItem>
)}
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isSecretScanning}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="secret-scan" />
}
>
{isSecretScanning && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
Secret Scanning
</MenuItem>
</Link>
</ShouldWrap>
</Menu>
@@ -11,10 +11,7 @@ import {
useGetSecretApprovalRequestCount,
useGetSecretRotations
} from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
// This is a generic layout shared by all types of projects.
// If the product layout differs significantly, create a new layout as needed.
export const SecretManagerLayout = () => {
const { currentWorkspace } = useWorkspace();
@@ -59,7 +56,7 @@ export const SecretManagerLayout = () => {
<div className="mt-2 flex-grow">
<Menu>
<Link
to={`/projects/$projectId/${ProjectType.SecretManager}/overview` as const}
to="/projects/$projectId/secret-manager/overview"
params={{
projectId: currentWorkspace.id
}}
@@ -69,7 +66,7 @@ export const SecretManagerLayout = () => {
)}
</Link>
<Link
to={`/projects/$projectId/${ProjectType.SecretManager}/integrations` as const}
to="/projects/$projectId/secret-manager/integrations"
params={{
projectId: currentWorkspace.id
}}
@@ -80,7 +77,7 @@ export const SecretManagerLayout = () => {
</Link>
{Boolean(secretRotations?.length) && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
to="/projects/$projectId/secret-manager/secret-rotation"
params={{
projectId: currentWorkspace.id
}}
@@ -89,7 +86,7 @@ export const SecretManagerLayout = () => {
</Link>
)}
<Link
to={`/projects/$projectId/${ProjectType.SecretManager}/approval` as const}
to="/projects/$projectId/secret-manager/approval"
params={{
projectId: currentWorkspace.id
}}
@@ -0,0 +1,62 @@
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
export const SecretScanningLayout = () => {
const { currentWorkspace } = useWorkspace();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
Secret Scanning
</div>
<div className="mt-2 flex-grow">
<Menu>
<Link
to="/projects/$projectId/secret-scanning/data-sources"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}> Data Sources</MenuItem>}
</Link>
<Link
to="/projects/$projectId/secret-scanning/findings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Findings</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 dark:[color-scheme:dark]">
<Outlet />
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { SecretScanningLayout } from "./SecretScanningLayout";
@@ -0,0 +1,100 @@
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionCan } from "@app/components/permissions";
export const SshLayout = () => {
const { currentWorkspace } = useWorkspace();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
SSH
</div>
<div className="mt-2 flex-grow">
<Menu>
<Link
to="/projects/$projectId/ssh/overview"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Hosts</MenuItem>}
</Link>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to="/projects/$projectId/ssh/cas"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}> Certificate Templates</MenuItem>
)}
</Link>
)
}
</ProjectPermissionCan>
<Link
to="/projects/$projectId/cert-manager/certificates"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}> Certificates</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/certificate-authorities"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}> Certificates Authorities</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/cert-manager/alerting"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Alerting</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 dark:[color-scheme:dark]">
<Outlet />
</div>
</div>
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export { SshLayout } from "./SshLayout";