mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 23:27:35 +00:00
feat: completed project layout base
This commit is contained in:
@@ -13,7 +13,7 @@ import { OrgPermissionSet } from "./types";
|
|||||||
|
|
||||||
export const useOrgPermission = () => {
|
export const useOrgPermission = () => {
|
||||||
const organizationId = useRouteContext({
|
const organizationId = useRouteContext({
|
||||||
from: "/_authenticate/_org_details",
|
from: "/_authenticate/_ctx-org-details",
|
||||||
select: (el) => el.organizationId
|
select: (el) => el.organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organiza
|
|||||||
|
|
||||||
export const useOrganization = () => {
|
export const useOrganization = () => {
|
||||||
const organizationId = useRouteContext({
|
const organizationId = useRouteContext({
|
||||||
from: "/_authenticate/_org_details",
|
from: "/_authenticate/_ctx-org-details",
|
||||||
select: (el) => el.organizationId
|
select: (el) => el.organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +1,76 @@
|
|||||||
import { createContext, ReactNode, useContext } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
|
import { unpackRules } from "@casl/ability/extra";
|
||||||
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
|
import { useParams } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { useGetUserProjectPermissions } from "@app/hooks/api";
|
import {
|
||||||
import { TProjectMembership } from "@app/hooks/api/users/types";
|
conditionsMatcher,
|
||||||
|
fetchUserProjectPermissions,
|
||||||
|
roleQueryKeys
|
||||||
|
} from "@app/hooks/api/roles/queries";
|
||||||
|
import { groupBy } from "@app/lib/fn/array";
|
||||||
|
import { omit } from "@app/lib/fn/object";
|
||||||
|
|
||||||
import { useWorkspace } from "../WorkspaceContext";
|
import { ProjectPermissionSet } from "./types";
|
||||||
import { TProjectPermission } from "./types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectPermissionContext = createContext<null | {
|
|
||||||
permission: TProjectPermission;
|
|
||||||
membership: TProjectMembership;
|
|
||||||
}>(null);
|
|
||||||
|
|
||||||
export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => {
|
|
||||||
const { currentWorkspace, isLoading: isWsLoading } = useWorkspace();
|
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
|
||||||
const { data: permission, isLoading } = useGetUserProjectPermissions({ workspaceId });
|
|
||||||
|
|
||||||
if ((isLoading && currentWorkspace) || isWsLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
|
|
||||||
<img
|
|
||||||
src="/images/loading/loading.gif"
|
|
||||||
height={70}
|
|
||||||
width={120}
|
|
||||||
decoding="async"
|
|
||||||
loading="lazy"
|
|
||||||
alt="infisical loading indicator"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!permission && currentWorkspace) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
|
|
||||||
Failed to load user permissions
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProjectPermissionContext.Provider value={permission!}>
|
|
||||||
{children}
|
|
||||||
</ProjectPermissionContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useProjectPermission = () => {
|
export const useProjectPermission = () => {
|
||||||
const ctx = useContext(ProjectPermissionContext);
|
const projectId = useParams({
|
||||||
if (!ctx) {
|
strict: false,
|
||||||
|
select: (el) => el?.projectId
|
||||||
|
});
|
||||||
|
if (!projectId) {
|
||||||
throw new Error("useProjectPermission to be used within <ProjectPermissionContext>");
|
throw new Error("useProjectPermission to be used within <ProjectPermissionContext>");
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasProjectRole = (role: string) => ctx?.membership?.roles?.includes(role) || false;
|
const {
|
||||||
|
data: { permission, membership }
|
||||||
|
} = useSuspenseQuery({
|
||||||
|
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId: projectId }),
|
||||||
|
queryFn: () => fetchUserProjectPermissions({ workspaceId: projectId }),
|
||||||
|
select: (data) => {
|
||||||
|
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
|
||||||
|
const negatedRules = groupBy(
|
||||||
|
rule.filter((i) => i.inverted && i.conditions),
|
||||||
|
(i) => `${i.subject}-${JSON.stringify(i.conditions)}`
|
||||||
|
);
|
||||||
|
const ability = createMongoAbility<ProjectPermissionSet>(rule, {
|
||||||
|
// this allows in frontend to skip some rules using *
|
||||||
|
conditionsMatcher: (rules) => {
|
||||||
|
return (entity) => {
|
||||||
|
// skip validation if its negated rules
|
||||||
|
const isNegatedRule =
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
negatedRules?.[`${entity.__caslSubjectType__}-${JSON.stringify(rules)}`];
|
||||||
|
if (isNegatedRule) {
|
||||||
|
const baseMatcher = conditionsMatcher(rules);
|
||||||
|
return baseMatcher(entity);
|
||||||
|
}
|
||||||
|
|
||||||
return { ...ctx, hasProjectRole };
|
const rulesStrippedOfWildcard = omit(
|
||||||
|
rules,
|
||||||
|
Object.keys(entity).filter((el) => entity[el]?.includes("*"))
|
||||||
|
);
|
||||||
|
const baseMatcher = conditionsMatcher(rulesStrippedOfWildcard);
|
||||||
|
return baseMatcher(entity);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission: ability,
|
||||||
|
membership: {
|
||||||
|
...data.membership,
|
||||||
|
roles: data.membership.roles.map(({ role }) => role)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasProjectRole = useCallback(
|
||||||
|
(role: string) => membership?.roles?.includes(role) || false,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { permission, membership, hasProjectRole };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext";
|
export { useProjectPermission } from "./ProjectPermissionContext";
|
||||||
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||||
export {
|
export {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subs
|
|||||||
|
|
||||||
export const useSubscription = () => {
|
export const useSubscription = () => {
|
||||||
const organizationId = useRouteContext({
|
const organizationId = useRouteContext({
|
||||||
from: "/_authenticate/_org_details",
|
from: "/_authenticate/_ctx-org-details",
|
||||||
select: (el) => el.organizationId
|
select: (el) => el.organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,22 @@
|
|||||||
import { createContext, ReactNode, useContext, useEffect, useMemo } from "react";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useParams } from "@tanstack/react-router";
|
import { useParams } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { workspaceKeys } from "@app/hooks/api";
|
||||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
|
||||||
import { Workspace } from "@app/hooks/api/workspace/types";
|
|
||||||
|
|
||||||
type TWorkspaceContext = {
|
|
||||||
workspaces: Workspace[];
|
|
||||||
currentWorkspace?: Workspace;
|
|
||||||
isLoading: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const WorkspaceContext = createContext<TWorkspaceContext | null>(null);
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
|
|
||||||
const { data: ws, isLoading } = useGetUserWorkspaces();
|
|
||||||
const params = useParams({ strict: false });
|
|
||||||
const workspaceId = params.id;
|
|
||||||
|
|
||||||
// memorize the workspace details for the context
|
|
||||||
const value = useMemo<TWorkspaceContext>(() => {
|
|
||||||
const wsId = workspaceId || localStorage.getItem("projectData.id");
|
|
||||||
return {
|
|
||||||
workspaces: ws || [],
|
|
||||||
currentWorkspace: (ws || []).find(({ id }) => id === wsId),
|
|
||||||
isLoading
|
|
||||||
};
|
|
||||||
}, [ws, workspaceId, isLoading]);
|
|
||||||
|
|
||||||
const shouldTriggerNoProjectAccess = !value.isLoading && !value.currentWorkspace;
|
|
||||||
|
|
||||||
if (shouldTriggerNoProjectAccess) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800 text-primary-50">
|
|
||||||
You do not have sufficient access to this project.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useWorkspace = () => {
|
export const useWorkspace = () => {
|
||||||
const ctx = useContext(WorkspaceContext);
|
const params = useParams({
|
||||||
if (!ctx) {
|
strict: false
|
||||||
throw new Error("useWorkspace has to be used within <WorkspaceContext.Provider>");
|
});
|
||||||
|
if (!params.projectId) {
|
||||||
|
throw new Error("Missing project id");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx;
|
const { data: currentWorkspace } = useSuspenseQuery({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
|
||||||
|
queryFn: () => fetchWorkspaceById(params.projectId as string),
|
||||||
|
staleTime: Infinity
|
||||||
|
});
|
||||||
|
|
||||||
|
return { currentWorkspace };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
|
export { useWorkspace } from "./WorkspaceContext";
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ export {
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionProvider,
|
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission
|
useProjectPermission
|
||||||
} from "./ProjectPermissionContext";
|
} from "./ProjectPermissionContext";
|
||||||
export { useServerConfig } from "./ServerConfigContext";
|
export { useServerConfig } from "./ServerConfigContext";
|
||||||
export { useSubscription } from "./SubscriptionContext";
|
export { useSubscription } from "./SubscriptionContext";
|
||||||
export { useUser } from "./UserContext";
|
export { useUser } from "./UserContext";
|
||||||
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
|
export { useWorkspace } from "./WorkspaceContext";
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
|
|||||||
};
|
};
|
||||||
export const getProjectHomePage = (workspace: Workspace) => {
|
export const getProjectHomePage = (workspace: Workspace) => {
|
||||||
if (workspace.type === ProjectType.SecretManager) {
|
if (workspace.type === ProjectType.SecretManager) {
|
||||||
return `/${workspace.type}/$projectId/secrets/overview` as const;
|
return `/${workspace.type}/$projectId/overview` as const;
|
||||||
}
|
}
|
||||||
if (workspace.type === ProjectType.CertificateManager) {
|
if (workspace.type === ProjectType.CertificateManager) {
|
||||||
return `/${workspace.type}/$projectId/certificates` as const;
|
return `/${workspace.type}/$projectId/certificates` as const;
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermissionDTO) => {
|
export const fetchUserProjectPermissions = async ({
|
||||||
|
workspaceId
|
||||||
|
}: TGetUserProjectPermissionDTO) => {
|
||||||
const { data } = await apiRequest.get<{
|
const { data } = await apiRequest.get<{
|
||||||
data: {
|
data: {
|
||||||
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
||||||
@@ -144,7 +146,7 @@ const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermiss
|
|||||||
export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPermissionDTO) =>
|
export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPermissionDTO) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId }),
|
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId }),
|
||||||
queryFn: () => getUserProjectPermissions({ workspaceId }),
|
queryFn: () => fetchUserProjectPermissions({ workspaceId }),
|
||||||
enabled: Boolean(workspaceId),
|
enabled: Boolean(workspaceId),
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
|
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
Workspace
|
Workspace
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
export const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
const { data } = await apiRequest.get<{ workspace: Workspace }>(
|
const { data } = await apiRequest.get<{ workspace: Workspace }>(
|
||||||
`/api/v1/workspace/${workspaceId}`
|
`/api/v1/workspace/${workspaceId}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
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 { Mfa } from "@app/components/auth/Mfa";
|
||||||
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
|
import { Menu, MenuItem } from "@app/components/v2";
|
||||||
|
import { useUser, useWorkspace } from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useGetAccessRequestsCount,
|
||||||
|
useGetSecretApprovalRequestCount,
|
||||||
|
useSelectOrganization
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||||
|
import { SidebarFooter } from "../OrganizationLayout/components/SidebarFooter";
|
||||||
|
import { SidebarHeader } from "./components/SidebarHeader";
|
||||||
|
import { ProjectSelect } from "./components/ProjectSelect";
|
||||||
|
|
||||||
|
// 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 { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const handleOrgChange = async (orgId: string) => {
|
||||||
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||||
|
organizationId: orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMfaEnabled) {
|
||||||
|
SecurityClient.setMfaToken(token);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
|
toggleShowMfa.on();
|
||||||
|
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// await navigateUserToOrg(router, orgId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldShowMfa) {
|
||||||
|
return (
|
||||||
|
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||||
|
<Mfa
|
||||||
|
email={user.email as string}
|
||||||
|
method={requiredMfaMethod}
|
||||||
|
successCallback={mfaSuccessCallback}
|
||||||
|
closeMfa={() => toggleShowMfa.off()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
|
||||||
|
const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager;
|
||||||
|
const isCmek = currentWorkspace?.type === ProjectType.KMS;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||||
|
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||||
|
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||||
|
<aside 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 justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||||
|
<div>
|
||||||
|
<SidebarHeader onChangeOrg={handleOrgChange} />
|
||||||
|
<ProjectSelect />
|
||||||
|
<div className="px-1">
|
||||||
|
<Menu>
|
||||||
|
{isSecretManager && (
|
||||||
|
<Link
|
||||||
|
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive} icon="system-outline-90-lock-closed">
|
||||||
|
{t("nav.menu.secrets")}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{isCertManager && (
|
||||||
|
<Link
|
||||||
|
to={`/${ProjectType.CertificateManager}/$projectId/overview` as const}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive} icon="system-outline-90-lock-closed">
|
||||||
|
Overview
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{isCmek && (
|
||||||
|
<Link
|
||||||
|
to={`/${ProjectType.KMS}/$projectId/overview` as const}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive} icon="system-outline-90-lock-closed">
|
||||||
|
Overview
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
to={`/${currentWorkspace.type}/$projectId/members` as const}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive} icon="system-outline-96-groups">
|
||||||
|
Access Control
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
{isSecretManager && (
|
||||||
|
<Link
|
||||||
|
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive} icon="system-outline-82-extension">
|
||||||
|
{t("nav.menu.integrations")}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{isSecretManager && (
|
||||||
|
<Link
|
||||||
|
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive} icon="rotation">
|
||||||
|
Secret Rotation
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{isSecretManager && (
|
||||||
|
<Link
|
||||||
|
to={`/${ProjectType.SecretManager}/$projectId/approval` as const}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem
|
||||||
|
isSelected={isActive}
|
||||||
|
icon="system-outline-189-domain-verification"
|
||||||
|
>
|
||||||
|
Approvals
|
||||||
|
{Boolean(
|
||||||
|
secretApprovalReqCount?.open ||
|
||||||
|
accessApprovalRequestCount?.pendingCount
|
||||||
|
) && (
|
||||||
|
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
|
||||||
|
{pendingRequestsCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
to={`/${currentWorkspace.type}/$projectId/settings` as const}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem
|
||||||
|
isSelected={isActive}
|
||||||
|
icon="system-outline-109-slider-toggle-settings"
|
||||||
|
>
|
||||||
|
{t("nav.menu.project-settings")}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SidebarFooter />
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</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,219 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { components, MenuProps, OptionProps } from "react-select";
|
||||||
|
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { faChevronRight, faPlus, faStar as faSolidStar } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { NewProjectModal } from "@app/components/projects";
|
||||||
|
import { Button, FilterableSelect } from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useSubscription,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { getProjectHomePage, getProjectTitle } from "@app/helpers/project";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||||
|
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||||
|
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||||
|
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
type TWorkspaceWithFaveProp = Workspace & { isFavorite: boolean };
|
||||||
|
|
||||||
|
const ProjectsMenu = ({ children, ...props }: MenuProps<TWorkspaceWithFaveProp>) => {
|
||||||
|
return (
|
||||||
|
<components.Menu {...props}>
|
||||||
|
{children}
|
||||||
|
<hr className="mb-2 h-px border-0 bg-mineshaft-500" />
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Workspace}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
className="w-full bg-mineshaft-700 pt-2 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={() => props.clearValue()}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add Project
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</components.Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectOption = ({
|
||||||
|
isSelected,
|
||||||
|
children,
|
||||||
|
data,
|
||||||
|
...props
|
||||||
|
}: OptionProps<TWorkspaceWithFaveProp>) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||||
|
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg.id);
|
||||||
|
|
||||||
|
const removeProjectFromFavorites = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
await updateUserProjectFavorites({
|
||||||
|
orgId: currentOrg!.id,
|
||||||
|
projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove project from favorites.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addProjectToFavorites = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
await updateUserProjectFavorites({
|
||||||
|
orgId: currentOrg!.id,
|
||||||
|
projectFavorites: [...(projectFavorites || []), projectId]
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to add project to favorites.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<components.Option
|
||||||
|
isSelected={isSelected}
|
||||||
|
data={data}
|
||||||
|
{...props}
|
||||||
|
className={twMerge(props.className, isSelected && "bg-mineshaft-500")}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center">
|
||||||
|
{isSelected && (
|
||||||
|
<FontAwesomeIcon className="mr-2 text-primary" icon={faChevronRight} size="xs" />
|
||||||
|
)}
|
||||||
|
<p className="truncate">{children}</p>
|
||||||
|
{data.isFavorite ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSolidStar}
|
||||||
|
className="ml-auto text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await removeProjectFromFavorites(data.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faStar}
|
||||||
|
className="ml-auto text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await addProjectToFavorites(data.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</components.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectSelect = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: workspaces = [] } = useGetUserWorkspaces({ type: currentWorkspace.type });
|
||||||
|
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg.id!);
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||||
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
|
"addNewWs",
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { options, value } = useMemo(() => {
|
||||||
|
const projectOptions = workspaces
|
||||||
|
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||||
|
...w,
|
||||||
|
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
||||||
|
}))
|
||||||
|
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite));
|
||||||
|
|
||||||
|
const currentOption = projectOptions.find((option) => option.id === currentWorkspace?.id);
|
||||||
|
|
||||||
|
if (!currentOption) {
|
||||||
|
return {
|
||||||
|
options: projectOptions,
|
||||||
|
value: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
options: [
|
||||||
|
currentOption,
|
||||||
|
...projectOptions.filter((option) => option.id !== currentOption.id)
|
||||||
|
],
|
||||||
|
value: currentOption
|
||||||
|
};
|
||||||
|
}, [workspaces, projectFavorites, currentWorkspace]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4 mt-5 w-full p-3">
|
||||||
|
<p className="mb-1 ml-1.5 text-xs font-semibold uppercase text-gray-400">
|
||||||
|
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"}
|
||||||
|
</p>
|
||||||
|
<FilterableSelect
|
||||||
|
className="text-sm"
|
||||||
|
value={value}
|
||||||
|
filterOption={(option, inputValue) =>
|
||||||
|
option.data.name.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
// hacky use of null as indication to create project
|
||||||
|
if (!newValue) {
|
||||||
|
if (isAddingProjectsAllowed) {
|
||||||
|
handlePopUpOpen("addNewWs");
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = newValue as TWorkspaceWithFaveProp;
|
||||||
|
localStorage.setItem("projectData.id", project.id);
|
||||||
|
// 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(getProjectHomePage(project));
|
||||||
|
}}
|
||||||
|
options={options}
|
||||||
|
components={{
|
||||||
|
Option: ProjectOption,
|
||||||
|
Menu: ProjectsMenu
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You have exceeded the number of projects allowed on the free plan."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NewProjectModal
|
||||||
|
isOpen={popUp.addNewWs.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||||
|
projectType={currentWorkspace?.type || ProjectType.SecretManager}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ProjectSelect";
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import {
|
||||||
|
faAngleDown,
|
||||||
|
faArrowLeft,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faCheck
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization, useUser, useWorkspace } from "@app/context";
|
||||||
|
import { useGetOrganizations, useLogoutUser } from "@app/hooks/api";
|
||||||
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
|
|
||||||
|
type Prop = {
|
||||||
|
onChangeOrg: (orgId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarHeader = ({ onChangeOrg }: Prop) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { user } = useUser();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data: orgs } = useGetOrganizations();
|
||||||
|
|
||||||
|
const logout = useLogoutUser();
|
||||||
|
const logOutUser = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Logging out...");
|
||||||
|
await logout.mutateAsync();
|
||||||
|
navigate({ to: "/login" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-12 cursor-default items-center px-3 pt-6">
|
||||||
|
<Link to={`/organization/$organizationId/${currentWorkspace.type}/overview`} params={{}}>
|
||||||
|
<div className="pl-1 pr-2 text-mineshaft-400 duration-200 hover:text-mineshaft-100">
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="max-w-[160px] data-[state=open]:bg-mineshaft-600">
|
||||||
|
<div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
|
||||||
|
<div className="flex h-5 w-5 min-w-[20px] items-center justify-center rounded-md bg-primary text-sm">
|
||||||
|
{currentOrg?.name.charAt(0)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="overflow-hidden truncate text-ellipsis pl-2 text-sm text-mineshaft-100"
|
||||||
|
style={{ maxWidth: "140px" }}
|
||||||
|
>
|
||||||
|
{currentOrg?.name}
|
||||||
|
</div>
|
||||||
|
<FontAwesomeIcon icon={faAngleDown} className="pl-1 pt-1 text-xs text-mineshaft-300" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
|
||||||
|
{orgs?.map((org) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem key={org.id}>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
if (currentOrg?.id === org.id) return;
|
||||||
|
|
||||||
|
if (org.authEnforced) {
|
||||||
|
// org has an org-level auth method enabled (e.g. SAML)
|
||||||
|
// -> logout + redirect to SAML SSO
|
||||||
|
|
||||||
|
await logout.mutateAsync();
|
||||||
|
if (org.orgAuthMethod === AuthMethod.OIDC) {
|
||||||
|
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
|
||||||
|
} else {
|
||||||
|
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
|
||||||
|
}
|
||||||
|
window.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeOrg(org?.id);
|
||||||
|
}}
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
size="xs"
|
||||||
|
className="flex w-full items-center justify-start p-0 font-normal"
|
||||||
|
leftIcon={
|
||||||
|
currentOrg?.id === org.id && (
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
|
||||||
|
{org.name}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
|
<button type="button" onClick={logOutUser} className="w-full">
|
||||||
|
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="child flex items-center justify-center rounded-full bg-mineshaft pr-1 text-mineshaft-300 hover:bg-mineshaft-500"
|
||||||
|
style={{ fontSize: "11px", width: "26px", height: "26px" }}
|
||||||
|
>
|
||||||
|
{user?.firstName?.charAt(0)}
|
||||||
|
{user?.lastName && user?.lastName?.charAt(0)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
|
||||||
|
<Link to="/personal-settings">
|
||||||
|
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Documentation
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] pl-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/slack"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Join Slack Community
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] pl-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</a>
|
||||||
|
{user?.superAdmin && (
|
||||||
|
<Link to="/admin">
|
||||||
|
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||||
|
Server Admin Console
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link to={`/org/${currentOrg?.id}/admin`}>
|
||||||
|
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||||
|
Organization Admin Console
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
|
<button type="button" onClick={logOutUser} className="w-full">
|
||||||
|
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SidebarHeader } from "./SidebarHeader";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectLayout } from "./ProjectLayout";
|
||||||
+875
-700
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -4,7 +4,8 @@ import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organiza
|
|||||||
import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
|
import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
|
||||||
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
|
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticate/_org_details")({
|
// Route context to fill in organization's data like details, subscription etc
|
||||||
|
export const Route = createFileRoute("/_authenticate/_ctx-org-details")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ context }) => {
|
||||||
const organizationId = context.organizationId!;
|
const organizationId = context.organizationId!;
|
||||||
await context.queryClient.ensureQueryData({
|
await context.queryClient.ensureQueryData({
|
||||||
+1
-1
@@ -2,6 +2,6 @@ import { createFileRoute } from "@tanstack/react-router";
|
|||||||
|
|
||||||
import { OrganizationLayout } from "@app/layouts/OrganizationLayout";
|
import { OrganizationLayout } from "@app/layouts/OrganizationLayout";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticate/_org_details/_org-layout")({
|
export const Route = createFileRoute("/_authenticate/_ctx-org-details/organization/_layout-org")({
|
||||||
component: OrganizationLayout
|
component: OrganizationLayout
|
||||||
});
|
});
|
||||||
+1
-1
@@ -40,7 +40,7 @@ const OrgAdminPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/"
|
"/_authenticate/_ctx-org-details/organization/_layout-org/$organizationId/admin/"
|
||||||
)({
|
)({
|
||||||
component: OrgAdminPage
|
component: OrgAdminPage
|
||||||
});
|
});
|
||||||
+1
-1
@@ -25,7 +25,7 @@ const AuditLogsPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_org_details/_org-layout/organization/$organizationId/audit-logs/"
|
"/_authenticate/_ctx-org-details/organization/_layout-org/$organizationId/audit-logs/"
|
||||||
)({
|
)({
|
||||||
component: AuditLogsPage
|
component: AuditLogsPage
|
||||||
});
|
});
|
||||||
+1
-1
@@ -44,7 +44,7 @@ const BillingRoute = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/"
|
"/_authenticate/_ctx-org-details/organization/_layout-org/$organizationId/billing/"
|
||||||
)({
|
)({
|
||||||
component: BillingRoute
|
component: BillingRoute
|
||||||
});
|
});
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import { ProductOverview } from "../secret-manager/overview";
|
|||||||
const CertManagerOverviewPage = () => <ProductOverview type={ProjectType.CertificateManager} />;
|
const CertManagerOverviewPage = () => <ProductOverview type={ProjectType.CertificateManager} />;
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_org_details/_org-layout/organization/$organizationId/cert-manager/overview"
|
"/_authenticate/_ctx-org-details/organization/_layout-org/$organizationId/cert-manager/overview"
|
||||||
)({
|
)({
|
||||||
component: CertManagerOverviewPage
|
component: CertManagerOverviewPage
|
||||||
});
|
});
|
||||||
+2
-2
@@ -39,7 +39,7 @@ const GroupPage = withPermission(
|
|||||||
() => {
|
() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const params = useParams({
|
const params = useParams({
|
||||||
from: "/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/"
|
from: "/_authenticate/_ctx-org-details/organization/_layout-org/$organizationId/groups/$groupId/"
|
||||||
});
|
});
|
||||||
const groupId = params.groupId as string;
|
const groupId = params.groupId as string;
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
@@ -214,7 +214,7 @@ const GroupDetailPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/"
|
"/_authenticate/_ctx-org-details/organization/_layout-org/$organizationId/groups/$groupId/"
|
||||||
)({
|
)({
|
||||||
component: GroupDetailPage
|
component: GroupDetailPage
|
||||||
});
|
});
|
||||||
+2
-2
@@ -48,7 +48,7 @@ export const IdentitySection = withPermission(
|
|||||||
() => {
|
() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const params = useParams({
|
const params = useParams({
|
||||||
from: "/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/"
|
from: "/_authenticate/_ctx-org-details/organization/_layout-org/$organizationId/identities/$identityId/"
|
||||||
});
|
});
|
||||||
const identityId = params.identityId as string;
|
const identityId = params.identityId as string;
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
@@ -381,7 +381,7 @@ const IdentityDetailPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/"
|
"/_authenticate/_ctx-org-details/organization/_layout-org/$organizationId/identities/$identityId/"
|
||||||
)({
|
)({
|
||||||
component: IdentityDetailPage
|
component: IdentityDetailPage
|
||||||
});
|
});
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import { ProductOverview } from "../secret-manager/overview";
|
|||||||
const KeyManagerOverviewPage = () => <ProductOverview type={ProjectType.KMS} />;
|
const KeyManagerOverviewPage = () => <ProductOverview type={ProjectType.KMS} />;
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview"
|
"/_authenticate/_ctx-org-details/organization/_layout-org/$organizationId/kms/overview"
|
||||||
)({
|
)({
|
||||||
component: KeyManagerOverviewPage
|
component: KeyManagerOverviewPage
|
||||||
});
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user