mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 02:29:35 +00:00
merge
This commit is contained in:
@@ -4,7 +4,6 @@ import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { useAddUsersToOrg } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { Button, EmailServiceSetupModal } from "../v2";
|
||||
@@ -23,7 +22,7 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
|
||||
// Redirect user to the getting started page
|
||||
const redirectToHome = async () => {
|
||||
navigate({ to: `/organization/${ProjectType.SecretManager}/overview` as const });
|
||||
navigate({ to: "/organization/projects" as const });
|
||||
};
|
||||
|
||||
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function NavHeader({
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<Link
|
||||
to="/organization/secret-manager/overview"
|
||||
to="/organization/projects"
|
||||
className="truncate pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary"
|
||||
>
|
||||
{currentOrg?.name}
|
||||
@@ -93,7 +93,7 @@ export default function NavHeader({
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
{pageName === "Secrets" ? (
|
||||
<Link
|
||||
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
|
||||
to="/projects/$projectId/secret-manager/overview"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
className="text-sm font-semibold text-primary/80 hover:text-primary"
|
||||
>
|
||||
@@ -129,7 +129,7 @@ export default function NavHeader({
|
||||
<div className="flex items-center space-x-3">
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||
<Link
|
||||
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
|
||||
to={`/projects/$projectId/${ProjectType.SecretManager}/secrets/$envSlug` as const}
|
||||
params={{ projectId: currentWorkspace.id, envSlug: routerEnvSlug }}
|
||||
className="text-sm font-semibold text-primary/80 hover:text-primary"
|
||||
>
|
||||
@@ -191,7 +191,7 @@ export default function NavHeader({
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
|
||||
to={`/projects/$projectId/${ProjectType.SecretManager}/secrets/$envSlug` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id,
|
||||
envSlug: routerEnvSlug || ""
|
||||
|
||||
@@ -70,7 +70,7 @@ export const SecretDashboardPathBreadcrumb = ({
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
|
||||
to={`/projects/$projectId/${ProjectType.SecretManager}/secrets/$envSlug` as const}
|
||||
params={{
|
||||
projectId,
|
||||
envSlug: environmentSlug
|
||||
|
||||
@@ -7,7 +7,6 @@ import z from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { GenericResourceNameSchema } from "@app/lib/schemas";
|
||||
|
||||
const schema = z
|
||||
@@ -59,10 +58,7 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const,
|
||||
params: {
|
||||
organizationId: organization.id
|
||||
}
|
||||
to: "/organization/projects"
|
||||
});
|
||||
|
||||
localStorage.setItem("orgData.id", organization.id);
|
||||
|
||||
@@ -5,12 +5,14 @@ import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useUpdateProject } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const baseFormSchema = z.object({
|
||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
defaultProduct: z.nativeEnum(ProjectType).default(ProjectType.SecretManager),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -50,6 +52,7 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
reset({
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? "",
|
||||
defaultProduct: currentWorkspace.defaultProduct,
|
||||
...(showSlugField && { slug: currentWorkspace.slug })
|
||||
});
|
||||
}
|
||||
@@ -63,6 +66,7 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
projectID: currentWorkspace.id,
|
||||
newProjectName: data.name,
|
||||
newProjectDescription: data.description,
|
||||
defaultProduct: data.defaultProduct,
|
||||
...(showSlugField &&
|
||||
"slug" in data && {
|
||||
newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined
|
||||
@@ -212,6 +216,31 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultProduct"
|
||||
defaultValue={ProjectType.SecretManager}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Default Product"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full max-w-md bg-mineshaft-800"
|
||||
>
|
||||
<SelectItem value={ProjectType.SecretManager}>Secrets Manager</SelectItem>
|
||||
<SelectItem value={ProjectType.CertificateManager}>PKI Manager</SelectItem>
|
||||
<SelectItem value={ProjectType.KMS}>KMS</SelectItem>
|
||||
<SelectItem value={ProjectType.SSH}>SSH</SelectItem>
|
||||
<SelectItem value={ProjectType.SecretScanning}>Secret Scanning</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
|
||||
@@ -35,7 +35,6 @@ import { getProjectHomePage } from "@app/helpers/project";
|
||||
import { useCreateWorkspace, useGetExternalKmsList, useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
@@ -53,12 +52,11 @@ type TAddProjectFormData = z.infer<typeof formSchema>;
|
||||
interface NewProjectModalProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
projectType: ProjectType;
|
||||
}
|
||||
|
||||
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange" | "projectType">;
|
||||
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange">;
|
||||
|
||||
const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { permission } = useOrgPermission();
|
||||
@@ -72,7 +70,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
OrgPermissionSubjects.ProjectTemplates
|
||||
);
|
||||
|
||||
const { data: projectTemplates = [] } = useListProjectTemplates(projectType, {
|
||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
||||
});
|
||||
|
||||
@@ -115,15 +113,17 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
projectName: name,
|
||||
projectDescription: description,
|
||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||
template,
|
||||
type: projectType
|
||||
template
|
||||
});
|
||||
await refetchWorkspaces();
|
||||
|
||||
createNotification({ text: "Project created", type: "success" });
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
navigate({ to: getProjectHomePage(project), params: { projectId: project.id } });
|
||||
navigate({
|
||||
to: getProjectHomePage(project.defaultProduct),
|
||||
params: { projectId: project.id }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create project", type: "error" });
|
||||
@@ -270,18 +270,14 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const NewProjectModal: FC<NewProjectModalProps> = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
projectType
|
||||
}) => {
|
||||
export const NewProjectModal: FC<NewProjectModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Create a new project"
|
||||
subTitle="This project will contain your secrets and configurations."
|
||||
>
|
||||
<NewProjectForm onOpenChange={onOpenChange} projectType={projectType} />
|
||||
<NewProjectForm onOpenChange={onOpenChange} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
|
||||
import { ProjectTemplatesTab } from "./components";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab }
|
||||
];
|
||||
|
||||
export const ProjectSettings = () => {
|
||||
const [selectedTab, setSelectedTab] = useState(tabs[0].key);
|
||||
|
||||
return (
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabs.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./ProjectTemplatesTab";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./ProjectSettings";
|
||||
+18
-1
@@ -4,7 +4,7 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
|
||||
import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
TOnePassVault,
|
||||
useOnePassConnectionListVaults
|
||||
@@ -32,6 +32,7 @@ export const OnePassSyncFields = () => {
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.vaultId", "");
|
||||
setValue("destinationConfig.valueLabel", "");
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -69,6 +70,22 @@ export const OnePassSyncFields = () => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isOptional
|
||||
label="Value Label"
|
||||
tooltipText="It's the label of the 1Password item field which will hold your secret value. For example, if you were to sync Infisical secret 'foo: bar', the 1Password item equivalent would have an item title of 'foo', and a field on that item 'value: bar'. The field label 'value' is what gets changed by this option."
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="value" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="destinationConfig.valueLabel"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+10
-2
@@ -6,7 +6,15 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const OnePassSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.OnePass }>();
|
||||
const vaultId = watch("destinationConfig.vaultId");
|
||||
const [vaultId, valueLabel] = watch([
|
||||
"destinationConfig.vaultId",
|
||||
"destinationConfig.valueLabel"
|
||||
]);
|
||||
|
||||
return <GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>;
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Value Key">{valueLabel || "value"}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@ export const OnePassSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.OnePass),
|
||||
destinationConfig: z.object({
|
||||
vaultId: z.string().trim().min(1, "Vault ID required")
|
||||
vaultId: z.string().trim().min(1, "Vault ID required"),
|
||||
valueLabel: z.string().trim().optional()
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ComponentType, ReactNode } from "react";
|
||||
|
||||
type ShouldWrapProps<T extends Record<string, any>> = {
|
||||
children: ReactNode;
|
||||
wrapper: ComponentType<T & { children: ReactNode }>;
|
||||
isWrapped?: boolean;
|
||||
} & T;
|
||||
|
||||
export const ShouldWrap = <T extends Record<string, any>>({
|
||||
children,
|
||||
wrapper: Wrapper,
|
||||
isWrapped = false,
|
||||
...wrapperProps
|
||||
}: ShouldWrapProps<T>) => {
|
||||
if (isWrapped) {
|
||||
return <Wrapper {...(wrapperProps as any)}>{children}</Wrapper>;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React from "react";
|
||||
import { faCaretDown, faChevronRight, faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEllipsis, faSort } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, ReactNode } from "@tanstack/react-router";
|
||||
import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link";
|
||||
@@ -40,7 +40,7 @@ const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWitho
|
||||
({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={twMerge("inline-flex items-center gap-1.5 font-medium", className)}
|
||||
className={twMerge("inline-flex items-center gap-1.5 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -78,13 +78,8 @@ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWit
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||
|
||||
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={twMerge("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <FontAwesomeIcon icon={faChevronRight} />}
|
||||
<li role="presentation" aria-hidden="true" className={twMerge("", className)} {...props}>
|
||||
{children ?? <p className="px-2 text-lg text-mineshaft-400/70">/</p>}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
@@ -139,12 +134,12 @@ const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[]
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbSegment>
|
||||
{el.label} <FontAwesomeIcon icon={faCaretDown} size="sm" />
|
||||
<BreadcrumbSegment className="rounded-md px-2 py-1 py-2 hover:bg-mineshaft-600">
|
||||
{el.label} <FontAwesomeIcon icon={faSort} size="sm" />
|
||||
</BreadcrumbSegment>
|
||||
</BreadcrumbItem>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuContent side="right" align="start">
|
||||
{el?.dropdownTitle && <DropdownMenuLabel>{el.dropdownTitle}</DropdownMenuLabel>}
|
||||
{el.links.map((i, dropIndex) => (
|
||||
<Link
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ButtonHTMLAttributes, forwardRef, ReactNode } from "react";
|
||||
import { cva, VariantProps } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Lottie } from "../Lottie";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
@@ -189,11 +191,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
{...props}
|
||||
>
|
||||
{isLoading && (
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
width={36}
|
||||
alt="loading animation"
|
||||
className="absolute rounded-xl opacity-80"
|
||||
<Lottie
|
||||
icon={
|
||||
variant === "solid" && colorSchema === "primary"
|
||||
? "infisical_loading_bw"
|
||||
: "infisical_loading"
|
||||
}
|
||||
isAutoPlay
|
||||
className="absolute w-8 rounded-xl opacity-80 shadow-sm"
|
||||
/>
|
||||
)}
|
||||
{leftIcon && (
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Lottie } from "../Lottie";
|
||||
|
||||
type Props = {
|
||||
text?: string | string[];
|
||||
frequency?: number;
|
||||
@@ -31,14 +33,7 @@ export const ContentLoader = ({ text, frequency = 2000, className }: Props) => {
|
||||
className
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="loading animation"
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Lottie icon="infisical_loading" className="h-32 w-32" />
|
||||
{text && isTextArray && (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
|
||||
@@ -20,6 +20,7 @@ type Props = {
|
||||
children?: ReactNode;
|
||||
deletionMessage?: ReactNode;
|
||||
buttonColorSchema?: "danger" | "primary" | "secondary" | "gray" | null;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const DeleteActionModal = ({
|
||||
@@ -34,6 +35,7 @@ export const DeleteActionModal = ({
|
||||
formContent,
|
||||
deletionMessage,
|
||||
buttonColorSchema = "danger",
|
||||
isDisabled,
|
||||
children
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
@@ -70,7 +72,7 @@ export const DeleteActionModal = ({
|
||||
<Button
|
||||
className="mr-4"
|
||||
colorSchema={buttonColorSchema}
|
||||
isDisabled={!(deleteKey === inputData) || isLoading}
|
||||
isDisabled={!(deleteKey === inputData) || isLoading || isDisabled}
|
||||
onClick={onDelete}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
|
||||
@@ -7,7 +7,7 @@ interface IProps {
|
||||
export const Divider = ({ className }: IProps): JSX.Element => {
|
||||
return (
|
||||
<div className={twMerge("flex items-center px-2 opacity-50", className)}>
|
||||
<div aria-hidden="true" className="h-5 w-full grow border border-t border-mineshaft-200" />
|
||||
<div aria-hidden="true" className="h-1 w-full grow border-t border-mineshaft-300" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { forwardRef, ReactNode, useRef } from "react";
|
||||
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
|
||||
|
||||
export type LottieProps = {
|
||||
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
|
||||
children?: ReactNode;
|
||||
icon?: string;
|
||||
iconMode?: Mode;
|
||||
className?: string;
|
||||
isAutoPlay?: boolean;
|
||||
};
|
||||
|
||||
export const Lottie = forwardRef<HTMLDivElement, LottieProps>(
|
||||
({ children, icon, iconMode, isAutoPlay, ...props }, ref): JSX.Element => {
|
||||
const iconRef = useRef<DotLottie | null>(null);
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.stop()}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<DotLottieReact
|
||||
dotLottieRefCallback={(el) => {
|
||||
iconRef.current = el;
|
||||
}}
|
||||
mode={iconMode}
|
||||
src={`/lotties/${icon}.json`}
|
||||
loop
|
||||
autoplay={isAutoPlay}
|
||||
className="h-full w-full"
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { Lottie } from "./Lottie";
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
|
||||
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
|
||||
import { ComponentPropsWithRef, ElementType, ReactNode, Ref } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type MenuProps = {
|
||||
@@ -14,9 +13,8 @@ export const Menu = ({ children, className }: MenuProps): JSX.Element => {
|
||||
export type MenuItemProps<T extends ElementType> = {
|
||||
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
|
||||
as?: T;
|
||||
children: ReactNode;
|
||||
icon?: string;
|
||||
iconMode?: Mode;
|
||||
children?: ReactNode;
|
||||
leftIcon?: ReactNode;
|
||||
description?: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
isSelected?: boolean;
|
||||
@@ -26,7 +24,7 @@ export type MenuItemProps<T extends ElementType> = {
|
||||
|
||||
export const MenuItem = <T extends ElementType = "button">({
|
||||
children,
|
||||
icon,
|
||||
leftIcon,
|
||||
iconMode,
|
||||
className,
|
||||
isDisabled,
|
||||
@@ -37,41 +35,21 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
inputRef,
|
||||
...props
|
||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
|
||||
const iconRef = useRef<DotLottie | null>(null);
|
||||
return (
|
||||
<Item
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={twMerge(
|
||||
"duration-50 group relative mt-0.5 flex w-full cursor-pointer items-center rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
|
||||
"duration-50 group relative mt-0.5 flex w-full cursor-pointer items-center rounded px-2 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
|
||||
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
|
||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||
className
|
||||
)}
|
||||
ref={inputRef}
|
||||
onMouseEnter={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.stop()}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
isSelected ? "visisble" : "invisible"
|
||||
} absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`}
|
||||
/>
|
||||
{icon && (
|
||||
<div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3">
|
||||
<DotLottieReact
|
||||
dotLottieRefCallback={(el) => {
|
||||
iconRef.current = el;
|
||||
}}
|
||||
mode={iconMode}
|
||||
src={`/lotties/${icon}.json`}
|
||||
loop
|
||||
className="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
{leftIcon}
|
||||
{children && <span className="flex-grow whitespace-nowrap text-left">{children}</span>}
|
||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||
</Item>
|
||||
);
|
||||
@@ -82,11 +60,14 @@ MenuItem.displayName = "MenuItem";
|
||||
export type MenuGroupProps = {
|
||||
children: ReactNode;
|
||||
title: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => (
|
||||
export const MenuGroup = ({ children, title, className }: MenuGroupProps): JSX.Element => (
|
||||
<>
|
||||
<li className="px-2 pt-3 text-xs uppercase text-gray-400">{title}</li>
|
||||
<li className={twMerge("px-2 pt-3 text-xs font-medium uppercase text-gray-400", className)}>
|
||||
{title}
|
||||
</li>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from "./ConfirmActionModal";
|
||||
export * from "./ContentLoader";
|
||||
export * from "./DatePicker";
|
||||
export * from "./DeleteActionModal";
|
||||
export * from "./Divider";
|
||||
export * from "./Drawer";
|
||||
export * from "./Dropdown";
|
||||
export * from "./Editor";
|
||||
@@ -22,6 +23,7 @@ export * from "./GenericFieldLabel";
|
||||
export * from "./HoverCardv2";
|
||||
export * from "./IconButton";
|
||||
export * from "./Input";
|
||||
export * from "./Lottie";
|
||||
export * from "./Menu";
|
||||
export * from "./Modal";
|
||||
export * from "./NoticeBanner";
|
||||
|
||||
+132
-132
@@ -74,276 +74,276 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
},
|
||||
SecretManager: {
|
||||
ApprovalPage: setRoute(
|
||||
"/secret-manager/$projectId/approval",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval"
|
||||
"/projects/$projectId/secret-manager/approval",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/approval"
|
||||
),
|
||||
SecretDashboardPage: setRoute(
|
||||
"/secret-manager/$projectId/secrets/$envSlug",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
|
||||
"/projects/$projectId/secret-manager/secrets/$envSlug",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/secrets/$envSlug"
|
||||
),
|
||||
RollbackPreviewPage: setRoute(
|
||||
"/secret-manager/$projectId/commits/$environment/$folderId/$commitId/restore",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
|
||||
"/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId/restore",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
|
||||
),
|
||||
CommitDetailsPage: setRoute(
|
||||
"/secret-manager/$projectId/commits/$environment/$folderId/$commitId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
|
||||
"/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId"
|
||||
),
|
||||
CommitsPage: setRoute(
|
||||
"/secret-manager/$projectId/commits/$environment/$folderId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId"
|
||||
"/projects/$projectId/secret-manager/commits/$environment/$folderId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId"
|
||||
),
|
||||
OverviewPage: setRoute(
|
||||
"/secret-manager/$projectId/overview",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview"
|
||||
"/projects/$projectId/secret-manager/overview",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/overview"
|
||||
),
|
||||
IntegrationsListPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/"
|
||||
"/projects/$projectId/secret-manager/integrations",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/"
|
||||
),
|
||||
IntegrationDetailsByIDPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/$integrationId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
|
||||
"/projects/$projectId/secret-manager/integrations/$integrationId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/$integrationId"
|
||||
),
|
||||
SecretSyncDetailsByIDPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/secret-syncs/$destination/$syncId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
|
||||
"/projects/$projectId/secret-manager/integrations/secret-syncs/$destination/$syncId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
|
||||
),
|
||||
Integratons: {
|
||||
SelectIntegrationAuth: setRoute(
|
||||
"/secret-manager/$projectId/integrations/select-integration-auth",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth"
|
||||
"/projects/$projectId/secret-manager/integrations/select-integration-auth",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/select-integration-auth"
|
||||
),
|
||||
HerokuOauthCallbackPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/heroku/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
|
||||
"/projects/$projectId/secret-manager/integrations/heroku/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/heroku/oauth2/callback"
|
||||
),
|
||||
HerokuConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/heroku/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create"
|
||||
"/projects/$projectId/secret-manager/integrations/heroku/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/heroku/create"
|
||||
),
|
||||
AwsParameterStoreConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/aws-parameter-store/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
|
||||
"/projects/$projectId/secret-manager/integrations/aws-parameter-store/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-parameter-store/create"
|
||||
),
|
||||
AwsSecretManagerConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/aws-secret-manager/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
|
||||
"/projects/$projectId/secret-manager/integrations/aws-secret-manager/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-secret-manager/create"
|
||||
),
|
||||
AzureAppConfigurationsOauthCallbackPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
|
||||
"/projects/$projectId/secret-manager/integrations/azure-app-configuration/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
|
||||
),
|
||||
AzureAppConfigurationsConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/azure-app-configuration/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
|
||||
"/projects/$projectId/secret-manager/integrations/azure-app-configuration/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/create"
|
||||
),
|
||||
AzureDevopsConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/azure-devops/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
|
||||
"/projects/$projectId/secret-manager/integrations/azure-devops/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-devops/create"
|
||||
),
|
||||
AzureKeyVaultAuthorizePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/azure-key-vault/authorize",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
|
||||
"/projects/$projectId/secret-manager/integrations/azure-key-vault/authorize",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/authorize"
|
||||
),
|
||||
AzureKeyVaultOauthCallbackPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
|
||||
"/projects/$projectId/secret-manager/integrations/azure-key-vault/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
|
||||
),
|
||||
AzureKeyVaultConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/azure-key-vault/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
|
||||
"/projects/$projectId/secret-manager/integrations/azure-key-vault/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/create"
|
||||
),
|
||||
BitbucketOauthCallbackPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/bitbucket/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
|
||||
"/projects/$projectId/secret-manager/integrations/bitbucket/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
|
||||
),
|
||||
BitbucketConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/bitbucket/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create"
|
||||
"/projects/$projectId/secret-manager/integrations/bitbucket/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/create"
|
||||
),
|
||||
ChecklyConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/checkly/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create"
|
||||
"/projects/$projectId/secret-manager/integrations/checkly/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/checkly/create"
|
||||
),
|
||||
CircleConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/circleci/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create"
|
||||
"/projects/$projectId/secret-manager/integrations/circleci/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/circleci/create"
|
||||
),
|
||||
CloudflarePagesConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/cloudflare-pages/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
|
||||
"/projects/$projectId/secret-manager/integrations/cloudflare-pages/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-pages/create"
|
||||
),
|
||||
DigitalOceanAppPlatformConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/digital-ocean-app-platform/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
|
||||
"/projects/$projectId/secret-manager/integrations/digital-ocean-app-platform/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
|
||||
),
|
||||
CloudflareWorkersConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/cloudflare-workers/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
|
||||
"/projects/$projectId/secret-manager/integrations/cloudflare-workers/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-workers/create"
|
||||
),
|
||||
CodefreshConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/codefresh/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create"
|
||||
"/projects/$projectId/secret-manager/integrations/codefresh/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/codefresh/create"
|
||||
),
|
||||
GcpSecretManagerConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/gcp-secret-manager/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
|
||||
"/projects/$projectId/secret-manager/integrations/gcp-secret-manager/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gcp-secret-manager/create"
|
||||
),
|
||||
GcpSecretManagerOauthCallbackPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/gcp-secret-manager/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
|
||||
"/projects/$projectId/secret-manager/integrations/gcp-secret-manager/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
|
||||
),
|
||||
GithubConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/github/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/create"
|
||||
"/projects/$projectId/secret-manager/integrations/github/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/github/create"
|
||||
),
|
||||
GithubOauthCallbackPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/github/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
|
||||
"/projects/$projectId/secret-manager/integrations/github/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/github/oauth2/callback"
|
||||
),
|
||||
GitlabConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/gitlab/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create"
|
||||
"/projects/$projectId/secret-manager/integrations/gitlab/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gitlab/create"
|
||||
),
|
||||
GitlabOauthCallbackPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/gitlab/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
|
||||
"/projects/$projectId/secret-manager/integrations/gitlab/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gitlab/oauth2/callback"
|
||||
),
|
||||
VercelOauthCallbackPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/vercel/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
|
||||
"/projects/$projectId/secret-manager/integrations/vercel/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/vercel/oauth2/callback"
|
||||
),
|
||||
VercelConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/vercel/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create"
|
||||
"/projects/$projectId/secret-manager/integrations/vercel/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/vercel/create"
|
||||
),
|
||||
FlyioConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/flyio/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create"
|
||||
"/projects/$projectId/secret-manager/integrations/flyio/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/flyio/create"
|
||||
),
|
||||
HashicorpVaultConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/hashicorp-vault/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
|
||||
"/projects/$projectId/secret-manager/integrations/hashicorp-vault/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/hashicorp-vault/create"
|
||||
),
|
||||
HasuraCloudConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/hasura-cloud/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
|
||||
"/projects/$projectId/secret-manager/integrations/hasura-cloud/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/hasura-cloud/create"
|
||||
),
|
||||
LaravelForgeConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/laravel-forge/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
|
||||
"/projects/$projectId/secret-manager/integrations/laravel-forge/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/laravel-forge/create"
|
||||
),
|
||||
NorthflankConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/northflank/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/northflank/create"
|
||||
"/projects/$projectId/secret-manager/integrations/northflank/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/northflank/create"
|
||||
),
|
||||
RailwayConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/railway/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/railway/create"
|
||||
"/projects/$projectId/secret-manager/integrations/railway/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/railway/create"
|
||||
),
|
||||
RenderConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/render/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/render/create"
|
||||
"/projects/$projectId/secret-manager/integrations/render/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/render/create"
|
||||
),
|
||||
RundeckConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/rundeck/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/rundeck/create"
|
||||
"/projects/$projectId/secret-manager/integrations/rundeck/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/rundeck/create"
|
||||
),
|
||||
WindmillConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/windmill/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create"
|
||||
"/projects/$projectId/secret-manager/integrations/windmill/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/windmill/create"
|
||||
),
|
||||
TravisCIConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/travisci/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/travisci/create"
|
||||
"/projects/$projectId/secret-manager/integrations/travisci/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/travisci/create"
|
||||
),
|
||||
TerraformCloudConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/terraform-cloud/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
|
||||
"/projects/$projectId/secret-manager/integrations/terraform-cloud/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/terraform-cloud/create"
|
||||
),
|
||||
TeamcityConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/teamcity/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/teamcity/create"
|
||||
"/projects/$projectId/secret-manager/integrations/teamcity/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/teamcity/create"
|
||||
),
|
||||
SupabaseConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/supabase/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/supabase/create"
|
||||
"/projects/$projectId/secret-manager/integrations/supabase/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/supabase/create"
|
||||
),
|
||||
OctopusDeployCloudConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/octopus-deploy/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
|
||||
"/projects/$projectId/secret-manager/integrations/octopus-deploy/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/octopus-deploy/create"
|
||||
),
|
||||
DatabricksConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/databricks/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create"
|
||||
"/projects/$projectId/secret-manager/integrations/databricks/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/databricks/create"
|
||||
),
|
||||
QoveryConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/qovery/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/qovery/create"
|
||||
"/projects/$projectId/secret-manager/integrations/qovery/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/qovery/create"
|
||||
),
|
||||
Cloud66ConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/cloud-66/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create"
|
||||
"/projects/$projectId/secret-manager/integrations/cloud-66/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloud-66/create"
|
||||
),
|
||||
NetlifyConfigurePage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/netlify/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/create"
|
||||
"/projects/$projectId/secret-manager/integrations/netlify/create",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/netlify/create"
|
||||
),
|
||||
NetlifyOuathCallbackPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/netlify/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
|
||||
"/projects/$projectId/secret-manager/integrations/netlify/oauth2/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/netlify/oauth2/callback"
|
||||
)
|
||||
}
|
||||
},
|
||||
CertManager: {
|
||||
CertAuthDetailsByIDPage: setRoute(
|
||||
"/cert-manager/$projectId/ca/$caName",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName"
|
||||
"/projects/$projectId/cert-manager/ca/$caName",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/ca/$caName"
|
||||
),
|
||||
SubscribersPage: setRoute(
|
||||
"/cert-manager/$projectId/subscribers",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers"
|
||||
"/projects/$projectId/cert-manager/subscribers",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers"
|
||||
),
|
||||
CertificatesPage: setRoute(
|
||||
"/cert-manager/$projectId/certificates",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates"
|
||||
"/projects/$projectId/cert-manager/certificates",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificates"
|
||||
),
|
||||
CertificateAuthoritiesPage: setRoute(
|
||||
"/cert-manager/$projectId/certificate-authorities",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities"
|
||||
"/projects/$projectId/cert-manager/certificate-authorities",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-authorities"
|
||||
),
|
||||
AlertingPage: setRoute(
|
||||
"/cert-manager/$projectId/alerting",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting"
|
||||
"/projects/$projectId/cert-manager/alerting",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/alerting"
|
||||
),
|
||||
PkiCollectionDetailsByIDPage: setRoute(
|
||||
"/cert-manager/$projectId/pki-collections/$collectionId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
|
||||
"/projects/$projectId/cert-manager/pki-collections/$collectionId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/pki-collections/$collectionId"
|
||||
),
|
||||
PkiSubscriberDetailsByIDPage: setRoute(
|
||||
"/cert-manager/$projectId/subscribers/$subscriberName",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName"
|
||||
"/projects/$projectId/cert-manager/subscribers/$subscriberName",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/$subscriberName"
|
||||
)
|
||||
},
|
||||
Ssh: {
|
||||
SshCaByIDPage: setRoute(
|
||||
"/ssh/$projectId/ca/$caId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId"
|
||||
"/projects/$projectId/ssh/ca/$caId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ca/$caId"
|
||||
),
|
||||
SshHostGroupDetailsByIDPage: setRoute(
|
||||
"/ssh/$projectId/ssh-host-groups/$sshHostGroupId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
|
||||
"/projects/$projectId/ssh/ssh-host-groups/$sshHostGroupId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ssh-host-groups/$sshHostGroupId"
|
||||
)
|
||||
},
|
||||
SecretScanning: {
|
||||
DataSourceByIdPage: setRoute(
|
||||
"/secret-scanning/$projectId/data-sources/$type/$dataSourceId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId"
|
||||
"/projects/$projectId/secret-scanning/data-sources/$type/$dataSourceId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/data-sources/$type/$dataSourceId"
|
||||
),
|
||||
FindingsPage: setRoute(
|
||||
"/secret-scanning/$projectId/findings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/findings"
|
||||
"/projects/$projectId/secret-scanning/findings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/findings"
|
||||
)
|
||||
},
|
||||
Public: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { createWorkspace } from "@app/hooks/api/workspace/queries";
|
||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const secretsToBeAdded = [
|
||||
{
|
||||
@@ -42,8 +42,7 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
|
||||
const {
|
||||
data: { project }
|
||||
} = await createWorkspace({
|
||||
projectName,
|
||||
type: ProjectType.SecretManager
|
||||
projectName
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -60,14 +59,14 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
|
||||
|
||||
return project;
|
||||
};
|
||||
export const getProjectHomePage = (workspace: Workspace) => {
|
||||
switch (workspace.type) {
|
||||
export const getProjectHomePage = (type: ProjectType) => {
|
||||
switch (type) {
|
||||
case ProjectType.CertificateManager:
|
||||
return `/${workspace.type}/$projectId/subscribers` as const;
|
||||
return `/projects/$projectId/${type}/subscribers` as const;
|
||||
case ProjectType.SecretScanning:
|
||||
return `/${workspace.type}/$projectId/data-sources` as const;
|
||||
return `/projects/$projectId/${type}/data-sources` as const;
|
||||
default:
|
||||
return `/${workspace.type}/$projectId/overview` as const;
|
||||
return `/projects/$projectId/${type}/overview` as const;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -81,3 +80,8 @@ export const getProjectTitle = (type: ProjectType) => {
|
||||
};
|
||||
return titleConvert[type];
|
||||
};
|
||||
|
||||
export const getCurrentProductFromUrl = (location: string) => {
|
||||
const type = Object.values(ProjectType).find((el) => location.includes(`/${el}`));
|
||||
return type;
|
||||
};
|
||||
|
||||
@@ -25,8 +25,8 @@ export const accessApprovalKeys = {
|
||||
requestedBy?: string,
|
||||
bypassReason?: string
|
||||
) => [{ projectSlug, envSlug, requestedBy, bypassReason }, "access-approvals-requests"] as const,
|
||||
getAccessApprovalRequestCount: (projectSlug: string) =>
|
||||
[{ projectSlug }, "access-approval-request-count"] as const
|
||||
getAccessApprovalRequestCount: (projectSlug: string, policyId?: string) =>
|
||||
[{ projectSlug }, "access-approval-request-count", ...(policyId ? [policyId] : [])] as const
|
||||
};
|
||||
|
||||
export const fetchPolicyApprovalCount = async ({
|
||||
@@ -87,21 +87,22 @@ const fetchApprovalRequests = async ({
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchAccessRequestsCount = async (projectSlug: string) => {
|
||||
const fetchAccessRequestsCount = async (projectSlug: string, policyId?: string) => {
|
||||
const { data } = await apiRequest.get<TAccessRequestCount>(
|
||||
"/api/v1/access-approvals/requests/count",
|
||||
{ params: { projectSlug } }
|
||||
{ params: { projectSlug, policyId } }
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetAccessRequestsCount = ({
|
||||
projectSlug,
|
||||
policyId,
|
||||
options = {}
|
||||
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
||||
useQuery({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug),
|
||||
queryFn: () => fetchAccessRequestsCount(projectSlug),
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug, policyId),
|
||||
queryFn: () => fetchAccessRequestsCount(projectSlug, policyId),
|
||||
...options,
|
||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||
});
|
||||
|
||||
@@ -147,6 +147,7 @@ export type TCreateAccessRequestDTO = {
|
||||
|
||||
export type TGetAccessApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
policyId?: string;
|
||||
envSlug?: string;
|
||||
authorUserId?: string;
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ export type IdentityMembershipOrg = {
|
||||
export type IdentityMembership = {
|
||||
id: string;
|
||||
identity: Identity;
|
||||
project: Pick<Workspace, "id" | "name" | "type">;
|
||||
project: Pick<Workspace, "id" | "name" | "defaultProduct">;
|
||||
roles: Array<
|
||||
{
|
||||
id: string;
|
||||
|
||||
@@ -6,17 +6,14 @@ import {
|
||||
TProjectTemplate,
|
||||
TProjectTemplateResponse
|
||||
} from "@app/hooks/api/projectTemplates/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const projectTemplateKeys = {
|
||||
all: ["project-template"] as const,
|
||||
list: (projectType?: ProjectType) =>
|
||||
[...projectTemplateKeys.all, "list", ...(projectType ? [projectType] : [])] as const,
|
||||
list: () => [...projectTemplateKeys.all, "list"] as const,
|
||||
byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const
|
||||
};
|
||||
|
||||
export const useListProjectTemplates = (
|
||||
type?: ProjectType,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TProjectTemplate[],
|
||||
@@ -28,11 +25,9 @@ export const useListProjectTemplates = (
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: projectTemplateKeys.list(type),
|
||||
queryKey: projectTemplateKeys.list(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates", {
|
||||
params: { type }
|
||||
});
|
||||
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates");
|
||||
|
||||
return data.projectTemplates;
|
||||
},
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export type TProjectTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ProjectType;
|
||||
description?: string;
|
||||
roles: Pick<TProjectRole, "slug" | "name" | "permissions">[];
|
||||
environments?: { name: string; slug: string; position: number }[] | null;
|
||||
@@ -16,7 +14,6 @@ export type TListProjectTemplates = { projectTemplates: TProjectTemplate[] };
|
||||
export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate };
|
||||
|
||||
export type TCreateProjectTemplateDTO = {
|
||||
type: ProjectType;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
@@ -34,9 +34,10 @@ export const secretApprovalRequestKeys = {
|
||||
] as const,
|
||||
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
|
||||
[{ id }, "secret-approval-request-detail"] as const,
|
||||
count: ({ workspaceId }: TGetSecretApprovalRequestCount) => [
|
||||
count: ({ workspaceId, policyId }: TGetSecretApprovalRequestCount) => [
|
||||
{ workspaceId },
|
||||
"secret-approval-request-count"
|
||||
"secret-approval-request-count",
|
||||
...(policyId ? [policyId] : [])
|
||||
]
|
||||
};
|
||||
|
||||
@@ -204,10 +205,13 @@ export const useGetSecretApprovalRequestDetails = ({
|
||||
enabled: Boolean(id) && (options?.enabled ?? true)
|
||||
});
|
||||
|
||||
const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => {
|
||||
const fetchSecretApprovalRequestCount = async ({
|
||||
workspaceId,
|
||||
policyId
|
||||
}: TGetSecretApprovalRequestCount) => {
|
||||
const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequestCount }>(
|
||||
"/api/v1/secret-approval-requests/count",
|
||||
{ params: { workspaceId } }
|
||||
{ params: { workspaceId, policyId } }
|
||||
);
|
||||
|
||||
return data.approvals;
|
||||
@@ -215,6 +219,7 @@ const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprov
|
||||
|
||||
export const useGetSecretApprovalRequestCount = ({
|
||||
workspaceId,
|
||||
policyId,
|
||||
options = {}
|
||||
}: TGetSecretApprovalRequestCount & {
|
||||
options?: Omit<
|
||||
@@ -228,8 +233,8 @@ export const useGetSecretApprovalRequestCount = ({
|
||||
>;
|
||||
}) =>
|
||||
useQuery({
|
||||
queryKey: secretApprovalRequestKeys.count({ workspaceId }),
|
||||
queryKey: secretApprovalRequestKeys.count({ workspaceId, policyId }),
|
||||
refetchInterval: 15000,
|
||||
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }),
|
||||
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId, policyId }),
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true)
|
||||
});
|
||||
|
||||
@@ -118,6 +118,7 @@ export type TGetSecretApprovalRequestList = {
|
||||
|
||||
export type TGetSecretApprovalRequestCount = {
|
||||
workspaceId: string;
|
||||
policyId?: string;
|
||||
};
|
||||
|
||||
export type TGetSecretApprovalRequestDetails = {
|
||||
|
||||
@@ -6,6 +6,7 @@ export type TOnePassSync = TRootSecretSync & {
|
||||
destination: SecretSync.OnePass;
|
||||
destinationConfig: {
|
||||
vaultId: string;
|
||||
valueLabel?: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.OnePass;
|
||||
|
||||
@@ -154,16 +154,14 @@ export const useGetWorkspaceById = (
|
||||
|
||||
export const useGetUserWorkspaces = ({
|
||||
includeRoles,
|
||||
type = "all",
|
||||
options = {}
|
||||
}: {
|
||||
includeRoles?: boolean;
|
||||
type?: ProjectType | "all";
|
||||
options?: { enabled?: boolean };
|
||||
} = {}) =>
|
||||
useQuery({
|
||||
queryKey: workspaceKeys.getAllUserWorkspace(type),
|
||||
queryFn: () => fetchUserWorkspaces(includeRoles, type),
|
||||
queryKey: workspaceKeys.getAllUserWorkspace(),
|
||||
queryFn: () => fetchUserWorkspaces(includeRoles),
|
||||
...options
|
||||
});
|
||||
|
||||
@@ -257,17 +255,16 @@ export const useCreateWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ data: { project: Workspace } }, object, CreateWorkspaceDTO>({
|
||||
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) =>
|
||||
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
|
||||
createWorkspace({
|
||||
projectName,
|
||||
projectDescription,
|
||||
kmsKeyId,
|
||||
template,
|
||||
type
|
||||
template
|
||||
}),
|
||||
onSuccess: (dto) => {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getAllUserWorkspace(dto.data.project.type)
|
||||
queryKey: workspaceKeys.getAllUserWorkspace()
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -283,7 +280,8 @@ export const useUpdateProject = () => {
|
||||
newProjectDescription,
|
||||
newSlug,
|
||||
secretSharing,
|
||||
showSnapshotsLegacy
|
||||
showSnapshotsLegacy,
|
||||
defaultProduct
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
|
||||
`/api/v1/workspace/${projectID}`,
|
||||
@@ -291,6 +289,7 @@ export const useUpdateProject = () => {
|
||||
name: newProjectName,
|
||||
description: newProjectDescription,
|
||||
slug: newSlug,
|
||||
defaultProduct,
|
||||
secretSharing,
|
||||
showSnapshotsLegacy
|
||||
}
|
||||
@@ -351,8 +350,8 @@ export const useUpdateWorkspaceVersionLimit = () => {
|
||||
});
|
||||
return data.workspace;
|
||||
},
|
||||
onSuccess: (dto) => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) });
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -370,8 +369,8 @@ export const useUpdateWorkspaceAuditLogsRetention = () => {
|
||||
);
|
||||
return data.workspace;
|
||||
},
|
||||
onSuccess: (dto) => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) });
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -384,8 +383,8 @@ export const useDeleteWorkspace = () => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/workspace/${workspaceID}`);
|
||||
return data.workspace;
|
||||
},
|
||||
onSuccess: (dto) => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) });
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["org-admin-projects"]
|
||||
});
|
||||
|
||||
@@ -12,8 +12,7 @@ export const workspaceKeys = {
|
||||
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
|
||||
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||
getAllUserWorkspace: (type?: string) =>
|
||||
type ? ["workspaces", { type }] : (["workspaces"] as const),
|
||||
getAllUserWorkspace: () => ["workspaces"] as const,
|
||||
getWorkspaceAuditLogs: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-audit-logs"] as const,
|
||||
getWorkspaceUsers: (
|
||||
|
||||
@@ -24,7 +24,7 @@ export type Workspace = {
|
||||
__v: number;
|
||||
id: string;
|
||||
name: string;
|
||||
type: ProjectType;
|
||||
defaultProduct: ProjectType;
|
||||
description?: string;
|
||||
orgId: string;
|
||||
version: ProjectVersion;
|
||||
@@ -71,7 +71,6 @@ export type CreateWorkspaceDTO = {
|
||||
projectDescription?: string;
|
||||
kmsKeyId?: string;
|
||||
template?: string;
|
||||
type: ProjectType;
|
||||
};
|
||||
|
||||
export type UpdateProjectDTO = {
|
||||
@@ -81,6 +80,7 @@ export type UpdateProjectDTO = {
|
||||
newSlug?: string;
|
||||
secretSharing?: boolean;
|
||||
showSnapshotsLegacy?: boolean;
|
||||
defaultProduct?: ProjectType;
|
||||
};
|
||||
|
||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||
@@ -181,7 +181,6 @@ export enum ProjectIdentityOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
export type TSearchProjectsDTO = {
|
||||
type?: ProjectType;
|
||||
name?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { useDebounce } from "./useDebounce";
|
||||
export * from "./useGetProjectTypeFromRoute";
|
||||
export { useLocalStorageState } from "./useLocalStorageState";
|
||||
export { usePagination } from "./usePagination";
|
||||
export { usePersistentState } from "./usePersistentState";
|
||||
export { usePopUp } from "./usePopUp";
|
||||
|
||||
@@ -8,15 +8,10 @@ export const useGetProjectTypeFromRoute = () => {
|
||||
|
||||
return useMemo(() => {
|
||||
const segments = location.pathname.split("/");
|
||||
const type = segments?.[2];
|
||||
if (!type) return ProjectType.SecretManager;
|
||||
|
||||
let type: ProjectType | undefined;
|
||||
|
||||
// location of project type can vary in router path, so we need to check all possible values
|
||||
segments.forEach((segment) => {
|
||||
if (Object.values(ProjectType).includes(segment as ProjectType))
|
||||
type = segment as ProjectType;
|
||||
});
|
||||
|
||||
return type;
|
||||
// second element would be /projects/$projectId/<type>
|
||||
return Object.values(ProjectType).find((el) => el === type) || ProjectType.SecretManager;
|
||||
}, [location]);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
||||
|
||||
type SetStateAction<T> = T | ((prevState: T) => T);
|
||||
|
||||
const dispatchStorageEvent = (key: string, newValue: string | null): void => {
|
||||
window.dispatchEvent(new StorageEvent("storage", { key, newValue }));
|
||||
};
|
||||
|
||||
const setLocalStorageItem = (key: string, value: unknown): void => {
|
||||
const stringifiedValue = JSON.stringify(value);
|
||||
window.localStorage.setItem(key, stringifiedValue);
|
||||
dispatchStorageEvent(key, stringifiedValue);
|
||||
};
|
||||
|
||||
const removeLocalStorageItem = (key: string): void => {
|
||||
window.localStorage.removeItem(key);
|
||||
dispatchStorageEvent(key, null);
|
||||
};
|
||||
|
||||
const getLocalStorageItem = (key: string): string | null => {
|
||||
return window.localStorage.getItem(key);
|
||||
};
|
||||
|
||||
const useLocalStorageSubscribe = (callback: (e: StorageEvent) => void) => {
|
||||
window.addEventListener("storage", callback);
|
||||
return () => window.removeEventListener("storage", callback);
|
||||
};
|
||||
|
||||
const getLocalStorageServerSnapshot = (): never => {
|
||||
throw Error("useLocalStorage is a client-only hook");
|
||||
};
|
||||
|
||||
export const useLocalStorageState = <T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: SetStateAction<T>) => void] => {
|
||||
const getSnapshot = () => getLocalStorageItem(key);
|
||||
|
||||
const store = useSyncExternalStore(
|
||||
useLocalStorageSubscribe,
|
||||
getSnapshot,
|
||||
getLocalStorageServerSnapshot
|
||||
);
|
||||
|
||||
const setState = useCallback(
|
||||
(v: SetStateAction<T>): void => {
|
||||
try {
|
||||
const nextState =
|
||||
typeof v === "function" ? (v as (prevState: T) => T)(JSON.parse(store || "null")) : v;
|
||||
|
||||
if (nextState === undefined || nextState === null) {
|
||||
removeLocalStorageItem(key);
|
||||
} else {
|
||||
setLocalStorageItem(key, nextState);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
[key, store]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (getLocalStorageItem(key) === null && typeof initialValue !== "undefined") {
|
||||
setLocalStorageItem(key, initialValue);
|
||||
}
|
||||
}, [key, initialValue]);
|
||||
|
||||
return [store ? JSON.parse(store) : initialValue, setState];
|
||||
};
|
||||
@@ -1,20 +1,8 @@
|
||||
import { faArrowLeft, faInfo, faQuestion } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Menu,
|
||||
MenuGroup,
|
||||
MenuItem
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
|
||||
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
|
||||
const generalTabs = [
|
||||
{
|
||||
@@ -69,21 +57,13 @@ export const AdminSidebar = () => {
|
||||
<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 className="flex-grow">
|
||||
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
|
||||
<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
|
||||
</div>
|
||||
</Link>
|
||||
<Menu>
|
||||
<MenuGroup title="General">
|
||||
{generalTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem isSelected={Boolean(isActive)} icon={tab.icon}>
|
||||
{tab.label}
|
||||
</MenuItem>
|
||||
<MenuItem isSelected={Boolean(isActive)}>{tab.label}</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
@@ -93,48 +73,29 @@ export const AdminSidebar = () => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem isSelected={Boolean(isActive)} icon={tab.icon}>
|
||||
{tab.label}
|
||||
</MenuItem>
|
||||
<MenuItem isSelected={Boolean(isActive)}>{tab.label}</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<div className="relative mb-4 mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||
Help & Support
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{envConfig.PLATFORM_VERSION && (
|
||||
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||
Version: {envConfig.PLATFORM_VERSION}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<FontAwesomeIcon
|
||||
className="mx-1 inline-block shrink-0"
|
||||
icon={faChevronLeft}
|
||||
flip="vertical"
|
||||
/>
|
||||
}
|
||||
>
|
||||
Back to organization
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</Menu>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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 py-3.5 text-lg text-white">KMS</div>
|
||||
<div className="flex-1">
|
||||
<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/kms/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 pt-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { KmsLayout } from "./KmsLayout";
|
||||
@@ -1,121 +1,48 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Outlet, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||
import { Banner } from "@app/components/page-frames/Banner";
|
||||
import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2";
|
||||
import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context";
|
||||
import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
||||
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
|
||||
import { SidebarHeader } from "./components/SidebarHeader";
|
||||
import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar";
|
||||
import { Navbar } from "./components/NavBar";
|
||||
import { OrgSidebar } from "./components/OrgSidebar";
|
||||
|
||||
export const OrganizationLayout = () => {
|
||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||
const location = useLocation();
|
||||
const { config } = useServerConfig();
|
||||
const { permission } = useOrgPermission();
|
||||
|
||||
const shouldShowProductsSidebar = permission.can(
|
||||
OrgPermissionSecretShareAction.ManageSettings,
|
||||
OrgPermissionSubjects.SecretShare
|
||||
);
|
||||
|
||||
const isOrganizationSpecificPage = location.pathname.startsWith("/organization");
|
||||
const breadcrumbs =
|
||||
isOrganizationSpecificPage && matches && "breadcrumbs" in matches
|
||||
? matches.breadcrumbs
|
||||
: undefined;
|
||||
const projectId = useParams({
|
||||
strict: false,
|
||||
select: (el) => el?.projectId
|
||||
});
|
||||
const isInsideProject = Boolean(projectId);
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isSecretSharingPage = (
|
||||
[
|
||||
linkOptions({ to: "/organization/secret-sharing" }).to,
|
||||
linkOptions({ to: "/organization/secret-sharing/settings" }).to
|
||||
] as string[]
|
||||
).includes(location.pathname);
|
||||
|
||||
const isProjectOverviewOrSettingsPage = (
|
||||
[
|
||||
linkOptions({ to: "/organization/secret-manager/overview" }).to,
|
||||
linkOptions({ to: "/organization/secret-manager/settings" }).to,
|
||||
linkOptions({ to: "/organization/cert-manager/overview" }).to,
|
||||
linkOptions({ to: "/organization/cert-manager/settings" }).to,
|
||||
linkOptions({ to: "/organization/kms/overview" }).to,
|
||||
linkOptions({ to: "/organization/kms/settings" }).to,
|
||||
linkOptions({ to: "/organization/ssh/overview" }).to,
|
||||
linkOptions({ to: "/organization/ssh/settings" }).to,
|
||||
linkOptions({ to: "/organization/secret-scanning/overview" }).to,
|
||||
linkOptions({ to: "/organization/secret-scanning/settings" }).to
|
||||
] as string[]
|
||||
).includes(location.pathname);
|
||||
|
||||
const shouldShowOrgSidebar =
|
||||
location.pathname.startsWith("/organization") &&
|
||||
(!isSecretSharingPage || shouldShowProductsSidebar);
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
let SideBarComponent = <DefaultSideBar />;
|
||||
|
||||
if (isSecretSharingPage) {
|
||||
SideBarComponent = <SecretSharingSideBar />;
|
||||
} else if (isProjectOverviewOrSettingsPage) {
|
||||
SideBarComponent = (
|
||||
<ProjectOverviewSideBar type={location.pathname.split("/")[2] as ProjectType} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Banner />
|
||||
<div
|
||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||
>
|
||||
<Navbar />
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<MinimizedOrgSidebar />
|
||||
<AnimatePresence mode="popLayout">
|
||||
{shouldShowOrgSidebar && (
|
||||
<motion.div
|
||||
key="menu-list-items"
|
||||
initial={{ x: -150 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
||||
{!isProjectOverviewOrSettingsPage && (
|
||||
<div className="p-2 pt-3">
|
||||
<SidebarHeader />
|
||||
</div>
|
||||
)}
|
||||
{SideBarComponent}
|
||||
</nav>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<OrgSidebar isHidden={isInsideProject} />
|
||||
<main
|
||||
className={twMerge(
|
||||
"flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]",
|
||||
!isOrganizationSpecificPage && "overflow-hidden p-0"
|
||||
"flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 pt-8 dark:[color-scheme:dark]",
|
||||
isInsideProject && "p-0"
|
||||
)}
|
||||
>
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
|
||||
export const DefaultSideBar = () => (
|
||||
<Menu>
|
||||
<MenuGroup title="Organization Control">
|
||||
<Link to="/organization/access-management">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="groups">
|
||||
Access Control
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/app-connections">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
|
||||
App Connections
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/gateways">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="gateway" iconMode="reverse">
|
||||
Gateways
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/sso">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="check">
|
||||
Single Sign-On (SSO)
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Other">
|
||||
<Link to="/organization/audit-logs">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="moving-block">
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/billing">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="spinning-coin">
|
||||
Usage & Billing
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
||||
Organization Settings
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
);
|
||||
@@ -1,99 +0,0 @@
|
||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
|
||||
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type TProjectOverviewSideBarProps = {
|
||||
type: ProjectType;
|
||||
};
|
||||
|
||||
export const ProjectOverviewSideBar = ({ type }: TProjectOverviewSideBarProps) => {
|
||||
const matchRoute = useMatchRoute();
|
||||
|
||||
const isOverviewActive = !!matchRoute({
|
||||
to: `/organization/${type}/overview`,
|
||||
fuzzy: false
|
||||
});
|
||||
|
||||
let label: string;
|
||||
let icon: string;
|
||||
let link: string;
|
||||
|
||||
switch (type) {
|
||||
case ProjectType.CertificateManager:
|
||||
label = "Cert Management";
|
||||
icon = "note";
|
||||
link = "https://infisical.com/docs/documentation/platform/pki/overview";
|
||||
break;
|
||||
case ProjectType.SecretManager:
|
||||
label = "Secret Management";
|
||||
icon = "sliding-carousel";
|
||||
link = "https://infisical.com/docs/documentation/getting-started/introduction";
|
||||
break;
|
||||
case ProjectType.KMS:
|
||||
label = "KMS";
|
||||
icon = "unlock";
|
||||
link = "https://infisical.com/docs/documentation/platform/kms/overview";
|
||||
break;
|
||||
case ProjectType.SSH:
|
||||
label = "SSH";
|
||||
icon = "verified";
|
||||
link = "https://infisical.com/docs/documentation/platform/ssh/overview";
|
||||
break;
|
||||
case ProjectType.SecretScanning:
|
||||
label = "Secret Scanning";
|
||||
icon = "secret-scan";
|
||||
link = "https://infisical.com/docs/documentation/platform/secret-scanning/overview";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown project type");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-2 pt-3">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={link}
|
||||
className="flex w-full items-center rounded-md border border-mineshaft-600 p-2 pl-3 transition-all duration-150 hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-mineshaft-500 p-4">
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="h-3.5 w-3.5 text-mineshaft-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="-mt-1 flex flex-grow flex-col text-white">
|
||||
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
||||
Infisical Docs
|
||||
</div>
|
||||
<div className="text-xs leading-[10px] text-mineshaft-400">
|
||||
{type === ProjectType.SecretManager ? "Get Started" : `${label} Overview`}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<Menu>
|
||||
<MenuGroup title="Overview">
|
||||
<Link to={`/organization/${type}/overview`}>
|
||||
<MenuItem isSelected={isOverviewActive} icon={icon}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Other">
|
||||
<Link to={`/organization/${type}/settings`}>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
||||
Settings
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
|
||||
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
|
||||
export const SecretSharingSideBar = () => {
|
||||
const matchRoute = useMatchRoute();
|
||||
const isOverviewActive = !!matchRoute({
|
||||
to: "/organization/secret-sharing",
|
||||
fuzzy: false
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuGroup title="Overview">
|
||||
<Link to="/organization/secret-sharing">
|
||||
{() => (
|
||||
<MenuItem isSelected={isOverviewActive} icon="lock-closed">
|
||||
Secret Sharing
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Other">
|
||||
<Link to="/organization/secret-sharing/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
||||
Settings
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./DefaultSideBar";
|
||||
export * from "./ProjectOverviewSideBar";
|
||||
export * from "./SecretSharingSideBar";
|
||||
@@ -1 +0,0 @@
|
||||
export { MinimizedOrgSidebar } from "./MinimizedOrgSidebar";
|
||||
@@ -0,0 +1,378 @@
|
||||
import { useState } from "react";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBook,
|
||||
faCheck,
|
||||
faCubes,
|
||||
faEnvelope,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faSignOut,
|
||||
faSort,
|
||||
faUser,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
||||
|
||||
import { Mfa } from "@app/components/auth/Mfa";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
BreadcrumbContainer,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalContent,
|
||||
TBreadcrumbFormat,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetOrganizations, useLogoutUser, workspaceKeys } from "@app/hooks/api";
|
||||
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
||||
import { SubscriptionPlan } from "@app/hooks/api/types";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||
|
||||
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
||||
|
||||
const getPlan = (subscription: SubscriptionPlan) => {
|
||||
if (subscription.groups) return "Enterprise";
|
||||
if (subscription.pitRecovery) return "Pro";
|
||||
return "Free";
|
||||
};
|
||||
|
||||
export const INFISICAL_SUPPORT_OPTIONS = [
|
||||
[
|
||||
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
|
||||
"Support Forum",
|
||||
"https://infisical.com/slack"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
|
||||
"Read Docs",
|
||||
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
|
||||
"GitHub Issues",
|
||||
"https://github.com/Infisical/infisical/issues"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
|
||||
"Email Support",
|
||||
"mailto:[email protected]"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={5} className="pr-4 text-sm" icon={faUsers} />,
|
||||
"Instance Admins",
|
||||
"server-admins"
|
||||
]
|
||||
];
|
||||
|
||||
export const Navbar = () => {
|
||||
const { user } = useUser();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const [showAdminsModal, setShowAdminsModal] = useState(false);
|
||||
|
||||
const { data: orgs } = useGetOrganizations();
|
||||
const navigate = useNavigate();
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
||||
|
||||
const handleOrgChange = async (orgId: string) => {
|
||||
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
|
||||
|
||||
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(token);
|
||||
if (mfaMethod) {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
|
||||
return;
|
||||
}
|
||||
await router.invalidate();
|
||||
await navigateUserToOrg(navigate, orgId);
|
||||
};
|
||||
|
||||
const logout = useLogoutUser();
|
||||
const logOutUser = async () => {
|
||||
try {
|
||||
console.log("Logging out...");
|
||||
await logout.mutateAsync();
|
||||
navigate({ to: "/login" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyToken = async () => {
|
||||
try {
|
||||
await window.navigator.clipboard.writeText(getAuthToken());
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Copied current login session token to clipboard"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({ type: "error", text: "Failed to copy user token to clipboard" });
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="z-10 flex h-12 items-center border-b border-mineshaft-600 bg-mineshaft-800 px-4 py-2">
|
||||
<div>
|
||||
<Link to="/organization/projects">
|
||||
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<p className="pl-2 pr-3 text-lg text-mineshaft-400/70">/</p>
|
||||
<DropdownMenu modal={false}>
|
||||
<Link to="/organization/projects">
|
||||
<div className="flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary-400">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faCubes} className="pr-1 text-xs" />
|
||||
</div>
|
||||
<div className="max-w-32 overflow-hidden text-ellipsis">{currentOrg?.name}</div>
|
||||
<div className="mr-2 rounded border border-mineshaft-500 px-1 text-xs text-bunker-300">
|
||||
{getPlan(subscription)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="switch-org"
|
||||
className="rounded-md px-2 py-2 hover:bg-mineshaft-600"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSort} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
side="bottom"
|
||||
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "220px" }}
|
||||
>
|
||||
<div className="px-2 py-1 text-xs capitalize text-mineshaft-400">organizations</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;
|
||||
}
|
||||
|
||||
handleOrgChange(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" />
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faSignOut} />} onClick={logOutUser}>
|
||||
Log Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<p className="px-2 text-lg text-mineshaft-400/70">/</p>
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex-grow" />
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger>
|
||||
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2 py-1 hover:bg-mineshaft-600">
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="text-mineshaft-200" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="bottom" className="mt-3 p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
|
||||
if (url === "server-admins" && isInfisicalCloud()) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
{url === "server-admins" ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdminsModal(true)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
{envConfig.PLATFORM_VERSION && (
|
||||
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||
Version: {envConfig.PLATFORM_VERSION}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="rounded-r-md border border-mineshaft-500 px-2 py-1 hover:bg-mineshaft-600">
|
||||
<FontAwesomeIcon icon={faUserCircle} className="text-mineshaft-200" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="mt-3 p-1">
|
||||
<div className="cursor-default px-1 py-1">
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800 p-1 px-2 transition-all duration-150">
|
||||
<div className="p-1 pr-3">
|
||||
<FontAwesomeIcon icon={faUser} className="text-xl text-mineshaft-400" />
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col text-white">
|
||||
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
||||
{user?.firstName} {user?.lastName}
|
||||
</div>
|
||||
<div className="text-xs text-mineshaft-300">{user.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<DropdownMenuItem onClick={handleCopyToken}>
|
||||
Copy Token
|
||||
<Tooltip
|
||||
content="This token is linked to your current login session and can only access resources within the organization you're currently logged into."
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="pl-1.5 text-xs" />
|
||||
</Tooltip>
|
||||
</DropdownMenuItem>
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
|
||||
Log Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
|
||||
<ModalContent title="Server Administrators" subTitle="View all server administrators">
|
||||
<div className="mb-2">
|
||||
<ServerAdminsPanel />
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Navbar } from "./Navbar";
|
||||
@@ -0,0 +1,237 @@
|
||||
import {
|
||||
faBook,
|
||||
faCheckCircle,
|
||||
faCog,
|
||||
faCubes,
|
||||
faDoorClosed,
|
||||
faInfinity,
|
||||
faMoneyBill,
|
||||
faPlug,
|
||||
faShare,
|
||||
faUserCog,
|
||||
faUsers,
|
||||
faUserTie
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||
import { Menu, MenuGroup, MenuItem, Tooltip } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetOrgTrialUrl } from "@app/hooks/api";
|
||||
|
||||
type Props = {
|
||||
isHidden?: boolean;
|
||||
};
|
||||
|
||||
export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { user } = useUser();
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{!isHidden && (
|
||||
<motion.aside
|
||||
key="org-sidebar"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: -240 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -240 }}
|
||||
layout
|
||||
className="dark z-10 w-60 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-800 to-mineshaft-900"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
||||
<Menu>
|
||||
<MenuGroup title="Overview">
|
||||
<Link to="/organization/projects">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCubes} />
|
||||
</div>
|
||||
Projects
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/access-management">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Access Control
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/billing">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faMoneyBill} className="mr-4" />
|
||||
</div>
|
||||
Usage & Billing
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/audit-logs">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} className="mr-4" />
|
||||
</div>
|
||||
Audit Logs
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/sso">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="mr-4" />
|
||||
</div>
|
||||
SSO Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} className="mr-4" />
|
||||
</div>
|
||||
Organization Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Resources">
|
||||
<Link to="/organization/app-connections">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPlug} className="mr-4" />
|
||||
</div>
|
||||
App Connections
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/gateways">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faDoorClosed} className="mr-4" />
|
||||
</div>
|
||||
Gateways
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
<div className="flex-grow" />
|
||||
<Menu>
|
||||
{subscription &&
|
||||
subscription.slug === "starter" &&
|
||||
!subscription.has_used_trial && (
|
||||
<Tooltip content="Start Free Pro Trial">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<FontAwesomeIcon
|
||||
className="mx-1 inline-block shrink-0"
|
||||
icon={faInfinity}
|
||||
/>
|
||||
}
|
||||
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;
|
||||
}}
|
||||
>
|
||||
Pro Trial
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Link to="/organization/secret-sharing">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faShare} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Share Secret
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<Link to="/organization/admin">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faUserCog} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Organization Admin
|
||||
</MenuItem>
|
||||
</Link>
|
||||
{user.superAdmin && (
|
||||
<Link to="/admin">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon
|
||||
className="mx-1 inline-block shrink-0"
|
||||
icon={faUserTie}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Server Console
|
||||
</MenuItem>
|
||||
</Link>
|
||||
)}
|
||||
</Menu>
|
||||
</nav>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<CreateOrgModal
|
||||
isOpen={popUp?.createOrg?.isOpen}
|
||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgSidebar } from "./OrgSidebar";
|
||||
@@ -11,10 +11,9 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
|
||||
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/NavBar/Navbar";
|
||||
|
||||
export const PersonalSettingsLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -27,7 +26,7 @@ export const PersonalSettingsLayout = () => {
|
||||
<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 className="flex-grow">
|
||||
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
|
||||
<Link to="/organization/projects">
|
||||
<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
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
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 py-3.5 text-lg text-white">
|
||||
PKI Manager
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<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 pt-8">
|
||||
<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,53 @@
|
||||
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 ProjectGeneralLayout = () => {
|
||||
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 py-3.5 text-lg text-white">
|
||||
Project Overview
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<Link
|
||||
to="/projects/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}>Access Control</MenuItem>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}>Project Settings</MenuItem>}
|
||||
</Link>
|
||||
</Menu>
|
||||
</div>
|
||||
</nav>
|
||||
</motion.div>
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ProjectGeneralLayout } from "./ProjectGeneralLayout";
|
||||
@@ -1,401 +1,317 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faDotCircle, faMobile, faWindowMaximize } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet, useRouterState } from "@tanstack/react-router";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { ShouldWrap } from "@app/components/utilities/ShouldWrapComponent";
|
||||
import {
|
||||
Badge,
|
||||
BreadcrumbContainer,
|
||||
Divider,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Lottie,
|
||||
Menu,
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
TBreadcrumbFormat
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import {
|
||||
useGetAccessRequestsCount,
|
||||
useGetSecretApprovalRequestCount,
|
||||
useGetSecretRotations
|
||||
} from "@app/hooks/api";
|
||||
import { useGetSecretScanningUnresolvedFindingCount } from "@app/hooks/api/secretScanningV2";
|
||||
import { useProjectPermission, useWorkspace } from "@app/context";
|
||||
import { getCurrentProductFromUrl } from "@app/helpers/project";
|
||||
import { useLocalStorageState } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { AssumePrivilegeModeBanner } from "./components/AssumePrivilegeModeBanner";
|
||||
import { ProjectSelect } from "./components/ProjectSelect";
|
||||
|
||||
enum SidebarStyle {
|
||||
Expanded = "expanded",
|
||||
Collapsed = "collapsed",
|
||||
ExpandOnHover = "expand-on-hover"
|
||||
}
|
||||
const MIN_SIDEBAR_SIZE = "55px";
|
||||
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 matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const [sidebarStyle, setSidebarStyle] = useLocalStorageState(
|
||||
"project-sidebar-style",
|
||||
SidebarStyle.ExpandOnHover
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
|
||||
const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager;
|
||||
const isCmek = currentWorkspace?.type === ProjectType.KMS;
|
||||
const isSSH = currentWorkspace?.type === ProjectType.SSH;
|
||||
const isSecretScanning = currentWorkspace?.type === ProjectType.SecretScanning;
|
||||
const minSidebarWidth =
|
||||
sidebarStyle === SidebarStyle.Expanded ? MAX_SIDEBAR_SIZE : MIN_SIDEBAR_SIZE;
|
||||
const maxSidebarWidth =
|
||||
sidebarStyle === SidebarStyle.Collapsed ? MIN_SIDEBAR_SIZE : MAX_SIDEBAR_SIZE;
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
|
||||
workspaceId,
|
||||
options: { enabled: isSecretManager }
|
||||
});
|
||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({
|
||||
projectSlug,
|
||||
options: { enabled: isSecretManager }
|
||||
});
|
||||
|
||||
// we only show the secret rotations v1 tab if they have existing rotations
|
||||
const { data: secretRotations } = useGetSecretRotations({
|
||||
workspaceId,
|
||||
options: {
|
||||
enabled: isSecretManager && Boolean(subscription.secretRotation),
|
||||
refetchOnMount: false
|
||||
}
|
||||
});
|
||||
|
||||
const pendingRequestsCount =
|
||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
|
||||
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(workspaceId, {
|
||||
enabled:
|
||||
isSecretScanning &&
|
||||
subscription.secretScanning &&
|
||||
permission.can(
|
||||
ProjectPermissionSecretScanningFindingActions.Read,
|
||||
ProjectPermissionSub.SecretScanningFindings
|
||||
),
|
||||
refetchInterval: 30000
|
||||
});
|
||||
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 className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<div
|
||||
className="dark relative hidden w-full flex-col overflow-x-hidden md:flex"
|
||||
style={{
|
||||
height: "calc(100vh - 3rem)",
|
||||
paddingLeft: !(sidebarStyle === SidebarStyle.Expanded) ? MIN_SIDEBAR_SIZE : "0px"
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-grow flex-col overflow-y-auto overflow-x-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"
|
||||
style={{ width: minSidebarWidth, height: "calc(100vh - 3rem)" }}
|
||||
whileHover={{
|
||||
width: maxSidebarWidth
|
||||
}}
|
||||
className={twMerge(
|
||||
"dark group z-10 w-full overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60",
|
||||
!(sidebarStyle === SidebarStyle.Expanded) && "absolute bottom-0 left-0"
|
||||
)}
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div>
|
||||
<ProjectSelect />
|
||||
<div className="px-1">
|
||||
<Menu>
|
||||
<MenuGroup title="Main Menu">
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
<Menu>
|
||||
<ShouldWrap
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
content="Secret Manager"
|
||||
position="right"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/secret-manager/overview"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
isSelected={isSecretManager}
|
||||
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="vault" />}
|
||||
>
|
||||
{isSecretManager && (
|
||||
<Link
|
||||
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="lock-closed">
|
||||
{t("nav.menu.secrets")}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{isCertManager && (
|
||||
<>
|
||||
<Link
|
||||
to={
|
||||
`/${ProjectType.CertificateManager}/$projectId/subscribers` as const
|
||||
}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="pki-subscriber">
|
||||
Subscribers
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to={
|
||||
`/${ProjectType.CertificateManager}/$projectId/certificate-templates` as const
|
||||
}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem
|
||||
iconMode="reverse"
|
||||
isSelected={isActive}
|
||||
icon="pki-template"
|
||||
>
|
||||
Certificate Templates
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to={
|
||||
`/${ProjectType.CertificateManager}/$projectId/certificates` as const
|
||||
}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="certificate">
|
||||
Certificates
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to={
|
||||
`/${ProjectType.CertificateManager}/$projectId/certificate-authorities` as const
|
||||
}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="certificate-authority">
|
||||
Certificate Authorities
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to={`/${ProjectType.CertificateManager}/$projectId/alerting` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="notification-bell">
|
||||
Alerting
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{isCmek && (
|
||||
<Link
|
||||
to={`/${ProjectType.KMS}/$projectId/overview` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="lock-closed">
|
||||
Overview
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{isCmek && (
|
||||
<Link
|
||||
to={`/${ProjectType.KMS}/$projectId/kmip` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="key-user" iconMode="reverse">
|
||||
KMIP
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{isSSH && (
|
||||
<>
|
||||
<Link
|
||||
to={`/${ProjectType.SSH}/$projectId/overview` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="server">
|
||||
Hosts
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
{/* <Link
|
||||
to={`/${ProjectType.SSH}/$projectId/certificates` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="certificate" iconMode="reverse">
|
||||
Certificates
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link> */}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.SshCertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) =>
|
||||
isAllowed && (
|
||||
<Link
|
||||
to={`/${ProjectType.SSH}/$projectId/cas` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem
|
||||
isSelected={isActive}
|
||||
icon="certificate-authority"
|
||||
iconMode="reverse"
|
||||
>
|
||||
Certificate Authorities
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
|
||||
)}
|
||||
Secret Manager
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</ShouldWrap>
|
||||
<ShouldWrap
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
content="PKI Manager"
|
||||
position="right"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/cert-manager/subscribers"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
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
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
content="KMS"
|
||||
position="right"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/kms/overview"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
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
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
content="SSH"
|
||||
position="right"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/ssh/overview"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
isSelected={isSsh}
|
||||
leftIcon={
|
||||
<Lottie className="inline-block h-6 w-6 shrink-0" icon="terminal" />
|
||||
}
|
||||
>
|
||||
{isSsh && <div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />}
|
||||
SSH
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</ShouldWrap>
|
||||
<ShouldWrap
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
content="Secret Scanning"
|
||||
position="right"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/secret-scanning/data-sources"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
isSelected={isSecretScanning}
|
||||
leftIcon={
|
||||
<Lottie className="inline-block h-6 w-6 shrink-0" icon="secret-scan" />
|
||||
}
|
||||
>
|
||||
{isSecretScanning && (
|
||||
<Link
|
||||
to={`/${ProjectType.SecretScanning}/$projectId/data-sources` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="blocks">
|
||||
Data Sources
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
|
||||
)}
|
||||
{isSecretScanning && (
|
||||
<Link
|
||||
to={`/${ProjectType.SecretScanning}/$projectId/findings` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="search">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span>Findings</span>
|
||||
{Boolean(unresolvedFindings) && (
|
||||
<Badge variant="primary" className="mr-2">
|
||||
{unresolvedFindings}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{isSecretManager && (
|
||||
<Link
|
||||
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
|
||||
{t("nav.menu.integrations")}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{isSecretManager && Boolean(secretRotations?.length) && (
|
||||
<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="circular-check">
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open ||
|
||||
accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<Badge variant="primary" className="ml-1.5">
|
||||
{pendingRequestsCount}
|
||||
</Badge>
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Other">
|
||||
<Link
|
||||
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
Secret Scanning
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</ShouldWrap>
|
||||
</Menu>
|
||||
<Divider />
|
||||
<Menu>
|
||||
<ShouldWrap
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
content="Access Control"
|
||||
position="right"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/access-management"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
isSelected={isActive}
|
||||
leftIcon={
|
||||
<Lottie className="inline-block h-6 w-6 shrink-0" icon="groups" />
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="groups">
|
||||
Access Control
|
||||
</MenuItem>
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to={`/${currentWorkspace.type}/$projectId/settings` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
Access Control
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</ShouldWrap>
|
||||
<ShouldWrap
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
content="Project Settings"
|
||||
position="right"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/settings"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
isSelected={isActive}
|
||||
leftIcon={
|
||||
<Lottie className="inline-block h-6 w-6 shrink-0" icon="settings-cog" />
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
||||
{t("nav.menu.project-settings")}
|
||||
</MenuItem>
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
Project Settings
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</ShouldWrap>
|
||||
</Menu>
|
||||
<div className="flex-grow" />
|
||||
<Menu>
|
||||
<DropdownMenu>
|
||||
<ShouldWrap
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
content="Sidebar Control"
|
||||
position="right"
|
||||
>
|
||||
<DropdownMenuTrigger className="w-full">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<FontAwesomeIcon
|
||||
className="mx-1 inline-block shrink-0"
|
||||
icon={faWindowMaximize}
|
||||
flip="vertical"
|
||||
/>
|
||||
}
|
||||
>
|
||||
Sidebar Control
|
||||
</MenuItem>
|
||||
</DropdownMenuTrigger>
|
||||
</ShouldWrap>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
iconPos="right"
|
||||
icon={
|
||||
sidebarStyle === SidebarStyle.Expanded && (
|
||||
<FontAwesomeIcon icon={faDotCircle} size="sm" />
|
||||
)
|
||||
}
|
||||
onClick={() => setSidebarStyle(SidebarStyle.Expanded)}
|
||||
>
|
||||
Expanded
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
iconPos="right"
|
||||
icon={
|
||||
sidebarStyle === SidebarStyle.Collapsed && (
|
||||
<FontAwesomeIcon icon={faDotCircle} size="sm" />
|
||||
)
|
||||
}
|
||||
onClick={() => setSidebarStyle(SidebarStyle.Collapsed)}
|
||||
>
|
||||
Collapsed
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
iconPos="right"
|
||||
icon={
|
||||
sidebarStyle === SidebarStyle.ExpandOnHover && (
|
||||
<FontAwesomeIcon icon={faDotCircle} size="sm" />
|
||||
)
|
||||
}
|
||||
onClick={() => setSidebarStyle(SidebarStyle.ExpandOnHover)}
|
||||
>
|
||||
Expand on hover
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Menu>
|
||||
</nav>
|
||||
</motion.div>
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
-13
@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useProjectPermission, useWorkspace } from "@app/context";
|
||||
import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project";
|
||||
import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
@@ -36,20 +37,11 @@ export const AssumePrivilegeModeBanner = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
let page: string;
|
||||
const url = getProjectHomePage(
|
||||
getCurrentProductFromUrl(window.location.href) || ProjectType.SecretManager
|
||||
);
|
||||
|
||||
switch (currentWorkspace.type) {
|
||||
case ProjectType.SecretScanning:
|
||||
page = "data-sources";
|
||||
break;
|
||||
case ProjectType.CertificateManager:
|
||||
page = "subscribers";
|
||||
break;
|
||||
default:
|
||||
page = "overview";
|
||||
}
|
||||
|
||||
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/${page}`;
|
||||
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { ComponentPropsWithRef, ElementType, useRef } from "react";
|
||||
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { MenuItemProps } from "@app/components/v2";
|
||||
|
||||
export const MenuIconButton = <T extends ElementType = "button">({
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
isDisabled,
|
||||
isSelected,
|
||||
as: Item = "div",
|
||||
description,
|
||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
inputRef,
|
||||
lottieIconMode = "forward",
|
||||
...props
|
||||
}: MenuItemProps<T> &
|
||||
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
|
||||
const iconRef = useRef<DotLottie | null>(null);
|
||||
return (
|
||||
<div className="px-1">
|
||||
<Item
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={twMerge(
|
||||
"group relative my-1 flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
|
||||
isSelected && "bg-bunker-8g0 hover:bg-mineshaft-500",
|
||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||
className
|
||||
)}
|
||||
onMouseEnter={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.stop()}
|
||||
ref={inputRef}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={`${isSelected ? "opacity-100" : "opacity-0"} absolute left-0 h-full w-0.5 rounded-bl rounded-tl bg-primary transition-all duration-150`}
|
||||
/>
|
||||
{icon && (
|
||||
<div className="my-auto mb-2 h-6 w-6">
|
||||
<DotLottieReact
|
||||
dotLottieRefCallback={(el) => {
|
||||
iconRef.current = el;
|
||||
}}
|
||||
src={`/lotties/${icon}.json`}
|
||||
loop
|
||||
className="h-full w-full"
|
||||
mode={lottieIconMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="flex-grow justify-center break-words text-center"
|
||||
style={{ fontSize: "10px" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// export const MenuIconButton = <T extends ElementType = "button">({
|
||||
// children,
|
||||
// icon,
|
||||
// className,
|
||||
// isDisabled,
|
||||
// isSelected,
|
||||
// as: Item = "div",
|
||||
// description,
|
||||
// // wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
// inputRef,
|
||||
// lottieIconMode = "forward",
|
||||
// ...props
|
||||
// }: MenuItemProps<T> &
|
||||
// ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
|
||||
// const iconRef = useRef<DotLottie | null>(null);
|
||||
// return (
|
||||
// <div className="px-1">
|
||||
// <Item
|
||||
// type="button"
|
||||
// role="menuitem"
|
||||
// className={twMerge(
|
||||
// "group relative my-1 flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
|
||||
// isSelected && "bg-mineshaft-600 hover:bg-mineshaft-700",
|
||||
// isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||
// className
|
||||
// )}
|
||||
// onMouseEnter={() => iconRef.current?.play()}
|
||||
// onMouseLeave={() => iconRef.current?.stop()}
|
||||
// ref={inputRef}
|
||||
// {...props}
|
||||
// >
|
||||
// <div
|
||||
// className={`${"opacity-0"} absolute left-0 h-full w-1 bg-primary transition-all duration-150`}
|
||||
// />
|
||||
// {icon && (
|
||||
// <div className="my-auto mb-2 h-6 w-6">
|
||||
// <DotLottieReact
|
||||
// dotLottieRefCallback={(el) => {
|
||||
// iconRef.current = el;
|
||||
// }}
|
||||
// src={`/lotties/${icon}.json`}
|
||||
// loop
|
||||
// className="h-full w-full"
|
||||
// mode={lottieIconMode}
|
||||
// />
|
||||
// </div>
|
||||
// )}
|
||||
// <div
|
||||
// className="flex-grow justify-center break-words text-center"
|
||||
// style={{ fontSize: "10px" }}
|
||||
// >
|
||||
// {children}
|
||||
// </div>
|
||||
// </Item>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ComponentPropsWithRef, ElementType, useRef } from "react";
|
||||
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { MenuItemProps } from "@app/components/v2";
|
||||
|
||||
export const MenuIconButton = <T extends ElementType = "button">({
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
isDisabled,
|
||||
isSelected,
|
||||
as: Item = "div",
|
||||
description,
|
||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
inputRef,
|
||||
lottieIconMode = "forward",
|
||||
...props
|
||||
}: MenuItemProps<T> &
|
||||
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
|
||||
const iconRef = useRef<DotLottie | null>(null);
|
||||
return (
|
||||
<div>
|
||||
<Item
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={twMerge(
|
||||
"group relative flex w-full cursor-pointer flex-col items-center justify-center p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
|
||||
isSelected && "rounded-none bg-bunker-800 hover:bg-mineshaft-600",
|
||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||
className
|
||||
)}
|
||||
onMouseEnter={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.stop()}
|
||||
ref={inputRef}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
isSelected ? "opacity-100" : "opacity-0"
|
||||
} absolute left-0 h-full w-0.5 bg-primary transition-all duration-150`}
|
||||
/>
|
||||
{icon && (
|
||||
<div className="my-auto mb-2 h-6 w-6">
|
||||
<DotLottieReact
|
||||
dotLottieRefCallback={(el) => {
|
||||
iconRef.current = el;
|
||||
}}
|
||||
src={`/lotties/${icon}.json`}
|
||||
loop
|
||||
className="h-full w-full"
|
||||
mode={lottieIconMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="flex-grow justify-center break-words text-center"
|
||||
style={{ fontSize: "10px" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { MenuIconButton } from "./MenuIconButton";
|
||||
@@ -1,15 +1,29 @@
|
||||
import { useMemo } from "react";
|
||||
import { components, MenuProps, OptionProps } from "react-select";
|
||||
import { useMemo, useState } from "react";
|
||||
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faChevronRight, faPlus, faStar as faSolidStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faCheck,
|
||||
faCube,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSort,
|
||||
faStar as faSolidStar
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Link, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Input,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -17,49 +31,39 @@ import {
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { getProjectTitle } from "@app/helpers/project";
|
||||
import { getCurrentProductFromUrl, getProjectHomePage } 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";
|
||||
import { 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>) => {
|
||||
// TODO(pta): add search to project select
|
||||
export const ProjectSelect = () => {
|
||||
const [searchProject, setSearchProject] = useState("");
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||
const { data: workspaces = [] } = useGetUserWorkspaces();
|
||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg.id);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||
|
||||
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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeProjectFromFavorites = async (projectId: string) => {
|
||||
try {
|
||||
await updateUserProjectFavorites({
|
||||
@@ -74,63 +78,6 @@ const ProjectOption = ({
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -140,7 +87,7 @@ export const ProjectSelect = () => {
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const { options, value } = useMemo(() => {
|
||||
const projects = useMemo(() => {
|
||||
const projectOptions = workspaces
|
||||
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||
...w,
|
||||
@@ -148,70 +95,136 @@ export const ProjectSelect = () => {
|
||||
}))
|
||||
.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
|
||||
};
|
||||
return projectOptions;
|
||||
}, [workspaces, projectFavorites, currentWorkspace]);
|
||||
|
||||
return (
|
||||
<div className="mt-2 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;
|
||||
// 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.type}/${project.id}/overview`);
|
||||
}}
|
||||
options={options}
|
||||
components={{
|
||||
Option: ProjectOption,
|
||||
Menu: ProjectsMenu
|
||||
}}
|
||||
/>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<DropdownMenu modal={false}>
|
||||
<Link
|
||||
to={getProjectHomePage(
|
||||
getCurrentProductFromUrl(window.location.href) || currentWorkspace.defaultProduct
|
||||
)}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary-400">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faCube} className="text-xs" />
|
||||
</div>
|
||||
<Tooltip content={currentWorkspace.name}>
|
||||
<div className="max-w-32 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{currentWorkspace?.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="switch-org"
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSort} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
side="bottom"
|
||||
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "220px" }}
|
||||
>
|
||||
<div className="px-2 py-1 text-xs capitalize text-mineshaft-400">Projects</div>
|
||||
<div className="mb-1 border-b border-b-mineshaft-600 py-1 pb-1">
|
||||
<Input
|
||||
value={searchProject}
|
||||
onChange={(evt) => setSearchProject(evt.target.value || "")}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
size="xs"
|
||||
variant="plain"
|
||||
placeholder="Search projects"
|
||||
/>
|
||||
</div>
|
||||
<div className="thin-scrollbar max-h-80 overflow-auto">
|
||||
{projects
|
||||
?.filter((el) => el.name?.toLowerCase().includes(searchProject.toLowerCase()))
|
||||
?.map((workspace) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={workspace.id}
|
||||
onClick={async () => {
|
||||
// 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
|
||||
const url = linkOptions({
|
||||
to: getProjectHomePage(workspace.defaultProduct),
|
||||
params: {
|
||||
projectId: workspace.id
|
||||
}
|
||||
});
|
||||
window.location.assign(url.to.replaceAll("$projectId", workspace.id));
|
||||
}}
|
||||
icon={
|
||||
currentWorkspace?.id === workspace.id && (
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-1 items-center justify-between overflow-hidden">
|
||||
<Tooltip content={workspace.name}>
|
||||
<div className="max-w-40 overflow-hidden truncate whitespace-nowrap">
|
||||
{workspace.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon
|
||||
icon={workspace.isFavorite ? faSolidStar : faStar}
|
||||
className="text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await (
|
||||
workspace.isFavorite
|
||||
? removeProjectFromFavorites
|
||||
: addProjectToFavorites
|
||||
)(workspace.id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() =>
|
||||
handlePopUpOpen(isAddingProjectsAllowed ? "addNewWs" : "upgradePlan")
|
||||
}
|
||||
>
|
||||
New Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useUser, useWorkspace } from "@app/context";
|
||||
import { useOrganization, useUser } from "@app/context";
|
||||
import { useGetOrganizations, useLogoutUser } from "@app/hooks/api";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
|
||||
@@ -24,7 +24,6 @@ type Prop = {
|
||||
|
||||
export const SidebarHeader = ({ onChangeOrg }: Prop) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { user } = useUser();
|
||||
const navigate = useNavigate();
|
||||
const { data: orgs } = useGetOrganizations();
|
||||
@@ -42,7 +41,7 @@ export const SidebarHeader = ({ onChangeOrg }: Prop) => {
|
||||
|
||||
return (
|
||||
<div className="flex h-12 cursor-default items-center px-3 pt-6">
|
||||
<Link to={`/organization/${currentWorkspace.type}/overview` as const}>
|
||||
<Link to="/organization/projects">
|
||||
<div className="pl-1 pr-2 text-mineshaft-400 duration-200 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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";
|
||||
|
||||
export const SecretManagerLayout = () => {
|
||||
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 py-3.5 text-lg text-white">
|
||||
Secrets Manager
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<Link
|
||||
to="/projects/$projectId/secret-manager/overview"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>{t("nav.menu.secrets")}</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/$projectId/secret-manager/integrations"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>{t("nav.menu.integrations")}</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
{Boolean(secretRotations?.length) && (
|
||||
<Link
|
||||
to="/projects/$projectId/secret-manager/secret-rotation"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}>Secret Rotation</MenuItem>}
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
to="/projects/$projectId/secret-manager/approval"
|
||||
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="/projects/$projectId/secret-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 pt-8">
|
||||
<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 { SecretManagerLayout } from "./SecretManagerLayout";
|
||||
@@ -0,0 +1,61 @@
|
||||
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 py-3.5 text-lg text-white">
|
||||
Secret Scanning
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<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/secret-scanning/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 pt-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretScanningLayout } from "./SecretScanningLayout";
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Menu, MenuItem } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
|
||||
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 py-3.5 text-lg text-white">SSH</div>
|
||||
<div className="flex-1">
|
||||
<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 Authorities</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</ProjectPermissionCan>
|
||||
<Link
|
||||
to="/projects/$projectId/ssh/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 pt-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SshLayout } from "./SshLayout";
|
||||
@@ -5,7 +5,7 @@ import lottieWasmUrl from "@lottiefiles/dotlottie-web/dist/dotlottie-player.wasm
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
import NProgress from "nprogress";
|
||||
|
||||
import { ContentLoader } from "./components/v2";
|
||||
import { Lottie } from "./components/v2";
|
||||
import { queryClient } from "./hooks/api/reactQuery";
|
||||
import { ErrorPage } from "./pages/public/ErrorPage/ErrorPage";
|
||||
import { NotFoundPage } from "./pages/public/NotFoundPage/NotFoundPage";
|
||||
@@ -62,8 +62,8 @@ const router = createRouter({
|
||||
routeTree,
|
||||
context: { serverConfig: null, queryClient },
|
||||
defaultPendingComponent: () => (
|
||||
<div className="bg-bunker-800">
|
||||
<ContentLoader />
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
|
||||
<Lottie icon="infisical_loading" className="h-32 w-32" />
|
||||
</div>
|
||||
),
|
||||
defaultNotFoundComponent: NotFoundPage,
|
||||
|
||||
@@ -7,7 +7,7 @@ export const Route = createFileRoute("/_authenticate/_inject-org-details/admin/_
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.user.superAdmin) {
|
||||
throw redirect({
|
||||
to: "/organization/secret-manager/overview"
|
||||
to: "/organization/projects"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useServerConfig } from "@app/context";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { queryClient } from "@app/hooks/api/reactQuery";
|
||||
import { userKeys } from "@app/hooks/api/users";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: string) => {
|
||||
const userOrgs = await fetchOrganizations();
|
||||
@@ -13,7 +12,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
|
||||
|
||||
if (organizationId) {
|
||||
localStorage.setItem("orgData.id", organizationId);
|
||||
navigate({ to: "/organization/secret-manager/overview" });
|
||||
navigate({ to: "/organization/projects" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,7 +20,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
|
||||
// user is part of at least 1 non-auth enforced org
|
||||
const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id;
|
||||
localStorage.setItem("orgData.id", userOrg);
|
||||
navigate({ to: `/organization/${ProjectType.SecretManager}/overview` as const });
|
||||
navigate({ to: "/organization/projects" });
|
||||
} else {
|
||||
// user is not part of any non-auth enforced orgs
|
||||
localStorage.removeItem("orgData.id");
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { isLoggedIn } from "@app/hooks/api/reactQuery";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
@@ -204,7 +203,7 @@ export const SignupInvitePage = () => {
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
|
||||
navigate({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||
to: "/organization/projects"
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { useVerifySignupEmailVerificationCode } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const SignUpPage = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
@@ -73,7 +72,7 @@ export const SignUpPage = () => {
|
||||
|
||||
if (!serverDetails?.emailConfigured && step === 4) {
|
||||
navigate({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||
to: "/organization/projects"
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
+1
-2
@@ -18,7 +18,6 @@ import { useToggle } from "@app/hooks";
|
||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
@@ -207,7 +206,7 @@ export const UserInfoSSOStep = ({
|
||||
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
navigate({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||
to: "/organization/projects"
|
||||
});
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
useGetPkiCollectionById,
|
||||
useUpdatePkiCollection
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z.object({
|
||||
@@ -83,7 +82,7 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/pki-collections/$collectionId` as const,
|
||||
to: "/projects/$projectId/cert-manager/pki-collections/$collectionId",
|
||||
params: {
|
||||
projectId,
|
||||
collectionId
|
||||
|
||||
@@ -61,7 +61,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
|
||||
key={`pki-collection-${pkiCollection.id}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/pki-collections/$collectionId` as const,
|
||||
to: `/projects/$projectId/${ProjectType.CertificateManager}/pki-collections/$collectionId` as const,
|
||||
params: {
|
||||
projectId,
|
||||
collectionId: pkiCollection.id
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AlertingPage } from "./AlertingPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/alerting"
|
||||
)({
|
||||
component: AlertingPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -71,7 +71,7 @@ const Page = () => {
|
||||
|
||||
handlePopUpClose("deleteCa");
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/certificate-authorities` as const,
|
||||
to: `/projects/$projectId/${ProjectType.CertificateManager}/certificate-authorities` as const,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/ca/$caName"
|
||||
)({
|
||||
component: CertAuthDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -13,11 +13,14 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Certificate Authorities",
|
||||
link: linkOptions({
|
||||
to: "/cert-manager/$projectId/certificate-authorities",
|
||||
to: "/projects/$projectId/cert-manager/certificate-authorities",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: params.caName
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
key={`ca-${ca.id}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/ca/$caName` as const,
|
||||
to: `/projects/$projectId/${ProjectType.CertificateManager}/ca/$caName` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
caName: ca.name
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CertificateAuthoritiesPage } from "./CertificateAuthoritiesPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-authorities"
|
||||
)({
|
||||
component: CertificateAuthoritiesPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CertificatesPage } from "./CertificatesPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificates"
|
||||
)({
|
||||
component: CertificatesPage
|
||||
component: CertificatesPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Certificates"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ export const PkiCollectionPage = () => {
|
||||
});
|
||||
handlePopUpClose("deletePkiCollection");
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const,
|
||||
to: `/projects/$projectId/${ProjectType.CertificateManager}/certificates` as const,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/pki-collections/$collectionId"
|
||||
)({
|
||||
component: PkiCollectionDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -13,11 +13,14 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Certificate Collections",
|
||||
link: linkOptions({
|
||||
to: "/cert-manager/$projectId/certificates",
|
||||
to: "/projects/$projectId/cert-manager/certificates",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Details"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ const Page = () => {
|
||||
|
||||
handlePopUpClose("deletePkiSubscriber");
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/subscribers` as const,
|
||||
to: `/projects/$projectId/${ProjectType.CertificateManager}/subscribers` as const,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/$subscriberName"
|
||||
)({
|
||||
component: PkiSubscriberDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -13,11 +13,14 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Subscribers",
|
||||
link: linkOptions({
|
||||
to: "/cert-manager/$projectId/subscribers",
|
||||
to: "/projects/$projectId/cert-manager/subscribers",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: params.subscriberName
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => {
|
||||
key={`pki-subscriber-${subscriber.id}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/subscribers/$subscriberName` as const,
|
||||
to: `/projects/$projectId/${ProjectType.CertificateManager}/subscribers/$subscriberName` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
subscriberName: subscriber.name
|
||||
|
||||
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { PkiSubscribersPage } from "./PkiSubscribersPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/"
|
||||
)({
|
||||
component: PkiSubscribersPage
|
||||
component: PkiSubscribersPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Subscribers"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { PkiTemplateListPage } from "./PkiTemplateListPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-templates/"
|
||||
)({
|
||||
component: PkiTemplateListPage
|
||||
component: PkiTemplateListPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Certificate Templates"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,9 +3,13 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
|
||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
|
||||
const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }];
|
||||
const tabs = [
|
||||
{
|
||||
name: "General",
|
||||
key: "tab-project-general",
|
||||
Component: () => <div className="text-white">Coming soon</div>
|
||||
}
|
||||
];
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
-184
@@ -1,184 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { LeaveProjectModal } from "@app/components/v2/LeaveProjectModal";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useDeleteWorkspace, useGetWorkspaceUsers, useLeaveProject } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
export const DeleteProjectSection = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteWorkspace",
|
||||
"leaveWorkspace"
|
||||
] as const);
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const { hasProjectRole, membership } = useProjectPermission();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
const [isLeaving, setIsLeaving] = useToggle();
|
||||
const deleteWorkspace = useDeleteWorkspace();
|
||||
const leaveProject = useLeaveProject();
|
||||
const { data: members, isPending: isMembersLoading } = useGetWorkspaceUsers(
|
||||
currentWorkspace?.id || ""
|
||||
);
|
||||
|
||||
// If isNoAccessMember is true, then the user can't read the workspace members. So we need to handle this case separately.
|
||||
const isNoAccessMember = hasProjectRole("no-access");
|
||||
|
||||
const isOnlyAdminMember = useMemo(() => {
|
||||
if (!members || !membership || !hasProjectRole("admin")) return false;
|
||||
|
||||
const adminMembers = members.filter(
|
||||
(member) => member.roles.map((r) => r.role).includes("admin") && member.id !== membership.id // exclude the current user
|
||||
);
|
||||
|
||||
return !adminMembers.length;
|
||||
}, [members, membership]);
|
||||
|
||||
const handleDeleteWorkspaceSubmit = async () => {
|
||||
setIsDeleting.on();
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await deleteWorkspace.mutateAsync({
|
||||
workspaceID: currentWorkspace?.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted project",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||
});
|
||||
handlePopUpClose("deleteWorkspace");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete project",
|
||||
type: "error"
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting.off();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLeaveWorkspaceSubmit = async () => {
|
||||
try {
|
||||
setIsLeaving.on();
|
||||
|
||||
if (!currentWorkspace?.id || !currentOrg?.id) return;
|
||||
|
||||
// If there's no members, and the user has access to read members, something went wrong.
|
||||
if (!members && !isNoAccessMember) return;
|
||||
|
||||
// If the user has elevated permissions and can read members:
|
||||
if (!isNoAccessMember) {
|
||||
if (!members) return;
|
||||
|
||||
if (members.length < 2) {
|
||||
createNotification({
|
||||
text: "You can't leave the project as you are the only member",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
// If the user has access to read members, and there's less than 1 admin member excluding the current user, they can't leave the project.
|
||||
if (isOnlyAdminMember) {
|
||||
createNotification({
|
||||
text: "You can't leave a project with no admin members left. Promote another member to admin first.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's actually a no-access member, then we don't really care about the members.
|
||||
|
||||
await leaveProject.mutateAsync({
|
||||
workspaceId: currentWorkspace.id
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to leave project",
|
||||
type: "error"
|
||||
});
|
||||
} finally {
|
||||
setIsLeaving.off();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
|
||||
<div className="space-x-4">
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Project}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isLoading={isDeleting}
|
||||
isDisabled={!isAllowed || isDeleting}
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
onClick={() => handlePopUpOpen("deleteWorkspace")}
|
||||
>
|
||||
{`Delete ${currentWorkspace?.name}`}
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isOnlyAdminMember && (
|
||||
<Button
|
||||
disabled={isMembersLoading || (members && members?.length < 2)}
|
||||
isLoading={isLeaving}
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
onClick={() => handlePopUpOpen("leaveWorkspace")}
|
||||
>
|
||||
{`Leave ${currentWorkspace?.name}`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteWorkspace.isOpen}
|
||||
title="Are you sure you want to delete this project?"
|
||||
subTitle={`Permanently delete ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Delete Project"
|
||||
onDeleteApproved={handleDeleteWorkspaceSubmit}
|
||||
/>
|
||||
|
||||
<LeaveProjectModal
|
||||
isOpen={popUp.leaveWorkspace.isOpen}
|
||||
title="Are you sure you want to leave this project?"
|
||||
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and its contents.`}
|
||||
onChange={(isOpen) => handlePopUpToggle("leaveWorkspace", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Leave Project"
|
||||
onLeaveApproved={handleLeaveWorkspaceSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||
|
||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
|
||||
export const ProjectGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
<AuditLogsRetentionSection />
|
||||
<DeleteProjectSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SettingsPage } from "./SettingsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/settings"
|
||||
)({
|
||||
component: SettingsPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -1,41 +1,9 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { workspaceKeys } from "@app/hooks/api";
|
||||
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
|
||||
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
|
||||
import { ProjectLayout } from "@app/layouts/ProjectLayout";
|
||||
import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout"
|
||||
)({
|
||||
component: ProjectLayout,
|
||||
beforeLoad: async ({ params, context }) => {
|
||||
const project = await context.queryClient.ensureQueryData({
|
||||
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
|
||||
queryFn: () => fetchWorkspaceById(params.projectId)
|
||||
});
|
||||
|
||||
await context.queryClient.ensureQueryData({
|
||||
queryKey: roleQueryKeys.getUserProjectPermissions({
|
||||
workspaceId: params.projectId
|
||||
}),
|
||||
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
|
||||
});
|
||||
|
||||
return {
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Cert Managers",
|
||||
link: linkOptions({ to: "/organization/cert-manager/overview" })
|
||||
},
|
||||
{
|
||||
label: project.name,
|
||||
link: linkOptions({
|
||||
to: "/cert-manager/$projectId/subscribers",
|
||||
params: { projectId: project.id }
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
component: PkiManagerLayout
|
||||
});
|
||||
|
||||
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { KmipPage } from "./KmipPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/kmip"
|
||||
)({
|
||||
component: KmipPage
|
||||
component: KmipPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "KMIP"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { OverviewPage } from "./OverviewPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/overview"
|
||||
)({
|
||||
component: OverviewPage
|
||||
component: OverviewPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Overview"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,9 +3,13 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
|
||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
|
||||
const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }];
|
||||
const tabs = [
|
||||
{
|
||||
name: "General",
|
||||
key: "tab-project-general",
|
||||
Component: () => <div className="text-white">Coming soon...</div>
|
||||
}
|
||||
];
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -16,7 +20,7 @@ export const SettingsPage = () => {
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
</Helmet>
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title={t("settings.project.title")} />
|
||||
<PageHeader title="Settings" />
|
||||
<Tabs defaultValue={tabs[0].key}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
|
||||
-129
@@ -1,129 +0,0 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useProjectPermission, useSubscription, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { useUpdateWorkspaceAuditLogsRetention } from "@app/hooks/api/workspace/queries";
|
||||
|
||||
const formSchema = z.object({
|
||||
auditLogsRetentionDays: z.coerce.number().min(0)
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
export const AuditLogsRetentionSection = () => {
|
||||
const { mutateAsync: updateAuditLogsRetention } = useUpdateWorkspaceAuditLogsRetention();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { membership } = useProjectPermission();
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
auditLogsRetentionDays:
|
||||
currentWorkspace?.auditLogsRetentionDays ?? subscription?.auditLogsRetentionDays ?? 0
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentWorkspace) return null;
|
||||
|
||||
const handleAuditLogsRetentionSubmit = async ({ auditLogsRetentionDays }: TForm) => {
|
||||
try {
|
||||
if (!subscription?.auditLogs) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "You can only configure audit logs retention if you upgrade your plan."
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscription && auditLogsRetentionDays > subscription?.auditLogsRetentionDays) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"To update your audit logs retention period to a higher value, upgrade your plan."
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await updateAuditLogsRetention({
|
||||
auditLogsRetentionDays,
|
||||
projectSlug: currentWorkspace.slug
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated audit logs retention period",
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed updating audit logs retention period",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// render only for dedicated/self-hosted instances of Infisical
|
||||
if (
|
||||
window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-semibold">Audit Logs Retention</p>
|
||||
</div>
|
||||
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
|
||||
Set the number of days to keep your project audit logs.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(handleAuditLogsRetentionSubmit)} autoComplete="off">
|
||||
<div className="max-w-xs">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={0}
|
||||
name="auditLogsRetentionDays"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Number of days"
|
||||
>
|
||||
<Input {...field} type="number" min={1} step={1} isDisabled={!isAdmin} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={!isAdmin || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { AuditLogsRetentionSection } from "./AuditLogsRetentionSection";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user