feat: completed breadcrumb and settings changes

This commit is contained in:
=
2025-07-02 01:16:49 +05:30
parent 48ac6b4aff
commit 6b3279cbe5
66 changed files with 338 additions and 1187 deletions
@@ -1,6 +1,6 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
import React from "react"; import React from "react";
import { faCaretDown, faEllipsis, faSlash } from "@fortawesome/free-solid-svg-icons"; import { faEllipsis, faSlash, faSort } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, ReactNode } from "@tanstack/react-router"; import { Link, ReactNode } from "@tanstack/react-router";
import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link"; import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link";
@@ -141,11 +141,11 @@ const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[]
<DropdownMenuTrigger> <DropdownMenuTrigger>
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbSegment> <BreadcrumbSegment>
{el.label} <FontAwesomeIcon icon={faCaretDown} size="sm" /> {el.label} <FontAwesomeIcon icon={faSort} size="sm" />
</BreadcrumbSegment> </BreadcrumbSegment>
</BreadcrumbItem> </BreadcrumbItem>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent side="right" align="start">
{el?.dropdownTitle && <DropdownMenuLabel>{el.dropdownTitle}</DropdownMenuLabel>} {el?.dropdownTitle && <DropdownMenuLabel>{el.dropdownTitle}</DropdownMenuLabel>}
{el.links.map((i, dropIndex) => ( {el.links.map((i, dropIndex) => (
<Link <Link
+10 -8
View File
@@ -40,14 +40,16 @@ export const KmsLayout = () => {
> >
{({ isActive }) => <MenuItem isSelected={isActive}>KMIP</MenuItem>} {({ isActive }) => <MenuItem isSelected={isActive}>KMIP</MenuItem>}
</Link> </Link>
<Link {
to="/projects/$projectId/cert-manager/settings" // <Link
params={{ // to="/projects/$projectId/kms/settings"
projectId: currentWorkspace.id // params={{
}} // projectId: currentWorkspace.id
> // }}
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>} // >
</Link> // {({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
// </Link>
}
</Menu> </Menu>
</div> </div>
</nav> </nav>
@@ -73,14 +73,16 @@ export const PkiManagerLayout = () => {
> >
{({ isActive }) => <MenuItem isSelected={isActive}>Alerting</MenuItem>} {({ isActive }) => <MenuItem isSelected={isActive}>Alerting</MenuItem>}
</Link> </Link>
<Link {
to="/projects/$projectId/cert-manager/settings" // <Link
params={{ // to="/projects/$projectId/cert-manager/settings"
projectId: currentWorkspace.id // params={{
}} // projectId: currentWorkspace.id
> // }}
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>} // >
</Link> // {({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
// </Link>
}
</Menu> </Menu>
</div> </div>
</nav> </nav>
@@ -20,7 +20,7 @@ export const ProjectGeneralLayout = () => {
> >
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]"> <nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white"> <div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
Project General Project Overview
</div> </div>
<div className="mt-2 flex-grow"> <div className="mt-2 flex-grow">
<Menu> <Menu>
@@ -51,7 +51,7 @@ export const SecretManagerLayout = () => {
> >
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]"> <nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white"> <div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
Project Overview Secret Manager
</div> </div>
<div className="mt-2 flex-grow"> <div className="mt-2 flex-grow">
<Menu> <Menu>
@@ -110,9 +110,7 @@ export const SecretManagerLayout = () => {
projectId: currentWorkspace.id projectId: currentWorkspace.id
}} }}
> >
{({ isActive }) => ( {({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
<MenuItem isSelected={isActive}>{t("nav.menu.project-settings")}</MenuItem>
)}
</Link> </Link>
</Menu> </Menu>
</div> </div>
@@ -41,7 +41,7 @@ export const SecretScanningLayout = () => {
{({ isActive }) => <MenuItem isSelected={isActive}>Findings</MenuItem>} {({ isActive }) => <MenuItem isSelected={isActive}>Findings</MenuItem>}
</Link> </Link>
<Link <Link
to="/projects/$projectId/cert-manager/settings" to="/projects/$projectId/secret-scanning/settings"
params={{ params={{
projectId: currentWorkspace.id projectId: currentWorkspace.id
}} }}
+2 -28
View File
@@ -46,40 +46,14 @@ export const SshLayout = () => {
}} }}
> >
{({ isActive }) => ( {({ isActive }) => (
<MenuItem isSelected={isActive}> Certificate Templates</MenuItem> <MenuItem isSelected={isActive}> Certificate Authorities</MenuItem>
)} )}
</Link> </Link>
) )
} }
</ProjectPermissionCan> </ProjectPermissionCan>
<Link <Link
to="/projects/$projectId/cert-manager/certificates" to="/projects/$projectId/ssh/settings"
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={{ params={{
projectId: currentWorkspace.id projectId: currentWorkspace.id
}} }}
@@ -18,6 +18,9 @@ export const Route = createFileRoute(
projectId: params.projectId projectId: params.projectId
} }
}) })
},
{
label: params.caName
} }
] ]
}; };
@@ -5,5 +5,15 @@ import { CertificatesPage } from "./CertificatesPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_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"
}
]
};
}
}); });
@@ -18,6 +18,9 @@ export const Route = createFileRoute(
projectId: params.projectId projectId: params.projectId
} }
}) })
},
{
label: "Details"
} }
] ]
}; };
@@ -18,6 +18,9 @@ export const Route = createFileRoute(
projectId: params.projectId projectId: params.projectId
} }
}) })
},
{
label: params.subscriberName
} }
] ]
}; };
@@ -5,5 +5,15 @@ import { PkiSubscribersPage } from "./PkiSubscribersPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_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"
}
]
};
}
}); });
@@ -5,5 +5,15 @@ import { PkiTemplateListPage } from "./PkiTemplateListPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_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 { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; const tabs = [
{
const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }]; name: "General",
key: "tab-project-general",
Component: () => <div className="text-white">Coming soon</div>
}
];
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
+1 -6
View File
@@ -5,10 +5,5 @@ import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout"
)({ )({
component: PkiManagerLayout, component: PkiManagerLayout
beforeLoad: async () => {
return {
breadcrumbs: []
};
}
}); });
+11 -1
View File
@@ -5,5 +5,15 @@ import { KmipPage } from "./KmipPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_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"
}
]
};
}
}); });
+11 -1
View File
@@ -5,5 +5,15 @@ import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_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 { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; const tabs = [
{
const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }]; name: "General",
key: "tab-project-general",
Component: () => <div className="text-white">Coming soon...</div>
}
];
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -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";
@@ -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/projects"
});
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>
);
};
@@ -1 +0,0 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
@@ -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>
);
};
@@ -1 +0,0 @@
export { ProjectGeneralTab } from "./ProjectGeneralTab";
@@ -1 +0,0 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
+1 -6
View File
@@ -5,10 +5,5 @@ import { KmsLayout } from "@app/layouts/KmsLayout";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout"
)({ )({
component: KmsLayout, component: KmsLayout
beforeLoad: async () => {
return {
breadcrumbs: []
};
}
}); });
@@ -21,7 +21,12 @@ export const Route = createFileRoute(
}, },
beforeLoad: ({ context }) => { beforeLoad: ({ context }) => {
return { return {
breadcrumbs: [] breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control"
}
]
}; };
} }
}); });
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
@@ -6,9 +6,23 @@ export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/groups/$groupId" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/groups/$groupId"
)({ )({
component: GroupDetailsByIDPage, component: GroupDetailsByIDPage,
beforeLoad: ({ context }) => { beforeLoad: ({ context, params }) => {
return { return {
breadcrumbs: [] breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Group"
}
]
}; };
} }
}); });
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
@@ -6,9 +6,23 @@ export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/identities/$identityId" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/identities/$identityId"
)({ )({
component: IdentityDetailsByIDPage, component: IdentityDetailsByIDPage,
beforeLoad: ({ context }) => { beforeLoad: ({ context, params }) => {
return { return {
breadcrumbs: [] breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Machine Identity"
}
]
}; };
} }
}); });
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
@@ -6,9 +6,23 @@ export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/members/$membershipId" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/members/$membershipId"
)({ )({
component: MemberDetailsByIDPage, component: MemberDetailsByIDPage,
beforeLoad: ({ context }) => { beforeLoad: ({ context, params }) => {
return { return {
breadcrumbs: [] breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "User"
}
]
}; };
} }
}); });
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
@@ -6,9 +6,23 @@ export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/roles/$roleSlug" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/roles/$roleSlug"
)({ )({
component: RoleDetailsBySlugPage, component: RoleDetailsBySlugPage,
beforeLoad: ({}) => { beforeLoad: ({ context, params }) => {
return { return {
breadcrumbs: [] breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Roles"
}
]
}; };
} }
}); });
@@ -1,3 +1,37 @@
import { Helmet } from "react-helmet";
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 }];
export const SettingsPage = () => { export const SettingsPage = () => {
return <div>Settings</div>; const { t } = useTranslation();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet>
<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")} />
<Tabs defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, Component }) => (
<TabPanel value={key} key={key}>
<Component />
</TabPanel>
))}
</Tabs>
</div>
</div>
);
}; };
@@ -1,25 +1,19 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from '@tanstack/react-router'
import { SettingsPage } from "./SettingsPage"; import { SettingsPage } from './SettingsPage'
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/settings" '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/settings',
)({ )({
component: SettingsPage, component: SettingsPage,
beforeLoad: ({ context, params }) => { beforeLoad: ({ context }) => {
return { return {
breadcrumbs: [ breadcrumbs: [
...context.breadcrumbs ...context.breadcrumbs,
// { {
// label: "Access Control", label: 'Settings',
// link: linkOptions({ },
// to: "/ssh/$projectId/access-management", ],
// params: { }
// projectId: params.projectId },
// } })
// })
// }
]
};
}
});
@@ -1,4 +1,4 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -16,5 +16,18 @@ export const Route = createFileRoute(
validateSearch: zodValidator(SecretOverviewPageQuerySchema), validateSearch: zodValidator(SecretOverviewPageQuerySchema),
search: { search: {
middlewares: [stripSearchParams({ secretPath: "/", search: "" })] middlewares: [stripSearchParams({ secretPath: "/", search: "" })]
} },
beforeLoad: ({ context, params }) => ({
...context,
breadcrumbs: [
...context.breadcrumbs,
{
label: "Secrets",
link: linkOptions({
to: "/projects/$projectId/secret-manager/overview",
params
})
}
]
})
}); });
@@ -25,6 +25,13 @@ export const Route = createFileRoute(
return { return {
breadcrumbs: [ breadcrumbs: [
...context.breadcrumbs, ...context.breadcrumbs,
{
label: "Secrets",
link: linkOptions({
to: "/projects/$projectId/secret-manager/overview",
params
})
},
{ {
type: BreadcrumbTypes.Dropdown, type: BreadcrumbTypes.Dropdown,
label: context.project.environments.find((el) => el.slug === params.envSlug)?.name || "", label: context.project.environments.find((el) => el.slug === params.envSlug)?.name || "",
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { EncryptionTab } from "./components/EncryptionTab"; import { EncryptionTab } from "./components/EncryptionTab";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
@@ -18,22 +18,17 @@ export const SettingsPage = () => {
{ {
name: "Encryption", name: "Encryption",
key: "tab-project-encryption", key: "tab-project-encryption",
isHidden: isHidden: currentWorkspace?.version !== ProjectVersion.V3,
currentWorkspace?.version !== ProjectVersion.V3 ||
currentWorkspace?.type !== ProjectType.SecretManager,
Component: EncryptionTab Component: EncryptionTab
}, },
{ {
name: "Workflow Integrations", name: "Workflow Integrations",
key: "tab-workflow-integrations", key: "tab-workflow-integrations",
isHidden: currentWorkspace?.type !== ProjectType.SecretManager,
Component: WorkflowIntegrationTab Component: WorkflowIntegrationTab
}, },
{ {
name: "Webhooks", name: "Webhooks",
key: "tab-project-webhooks", key: "tab-project-webhooks",
isHidden: currentWorkspace?.type !== ProjectType.SecretManager,
Component: WebhooksTab Component: WebhooksTab
} }
]; ];
@@ -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";
@@ -1,190 +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, Tooltip } 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) => (
<Tooltip
className="max-w-sm"
content="This project is protected from deletion. To delete it, disable delete protection first."
isDisabled={!currentWorkspace?.hasDeleteProtection}
>
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting || currentWorkspace?.hasDeleteProtection}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
</Tooltip>
)}
</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>
);
};
@@ -1 +0,0 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
@@ -1,12 +1,9 @@
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { AutoCapitalizationSection } from "../AutoCapitalizationSection"; import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection"; import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { DeleteProjectProtection } from "../DeleteProjectProtection"; import { DeleteProjectProtection } from "../DeleteProjectProtection";
import { DeleteProjectSection } from "../DeleteProjectSection";
import { EnvironmentSection } from "../EnvironmentSection"; import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection"; import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection"; import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
@@ -16,24 +13,18 @@ import { SecretTagsSection } from "../SecretTagsSection";
export const ProjectGeneralTab = () => { export const ProjectGeneralTab = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
return ( return (
<div> <div>
<ProjectOverviewChangeSection showSlugField /> <EnvironmentSection />
{isSecretManager && <EnvironmentSection />} <SecretTagsSection />
{isSecretManager && <SecretTagsSection />} <AutoCapitalizationSection />
{isSecretManager && <AutoCapitalizationSection />} <SecretSharingSection />
{isSecretManager && <SecretSharingSection />} <SecretSnapshotsLegacySection />
{isSecretManager && <SecretSnapshotsLegacySection />} <PointInTimeVersionLimitSection />
{isSecretManager && <PointInTimeVersionLimitSection />} <BackfillSecretReferenceSecretion />
<AuditLogsRetentionSection /> {currentWorkspace?.version !== ProjectVersion.V3 && <RebuildSecretIndicesSection />}
{isSecretManager && <BackfillSecretReferenceSecretion />}
{currentWorkspace?.version !== ProjectVersion.V3 && isSecretManager && (
<RebuildSecretIndicesSection />
)}
<DeleteProjectProtection /> <DeleteProjectProtection />
<DeleteProjectSection />
</div> </div>
); );
}; };
@@ -1,5 +1,4 @@
export { AutoCapitalizationSection } from "./AutoCapitalizationSection"; export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection"; export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
export { DeleteProjectSection } from "./DeleteProjectSection";
export { EnvironmentSection } from "./EnvironmentSection"; export { EnvironmentSection } from "./EnvironmentSection";
export { SecretTagsSection } from "./SecretTagsSection"; export { SecretTagsSection } from "./SecretTagsSection";
@@ -13,12 +13,12 @@ export const Route = createFileRoute(
{ {
label: "Data Sources", label: "Data Sources",
link: linkOptions({ link: linkOptions({
to: "/secret-scanning/$projectId/data-sources", to: "/projects/$projectId/secret-scanning/data-sources",
params params
}) })
}, },
{ {
label: "Data Source" label: "Details"
} }
] ]
}; };
@@ -5,15 +5,15 @@ import { SecretScanningDataSourcesPage } from "./SecretScanningDataSourcesPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/data-sources/" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/data-sources/"
)({ )({
component: SecretScanningDataSourcesPage component: SecretScanningDataSourcesPage,
// beforeLoad: ({ context }) => { beforeLoad: ({ context }) => {
// return { return {
// breadcrumbs: [ breadcrumbs: [
// ...context.breadcrumbs, ...context.breadcrumbs,
// { {
// label: "Data Sources" label: "Data Sources"
// } }
// ] ]
// }; };
// } }
}); });
@@ -6,7 +6,6 @@ import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab"; import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab";
export const SettingsPage = () => { export const SettingsPage = () => {
@@ -22,30 +21,16 @@ export const SettingsPage = () => {
<Tabs defaultValue="tab-project-general"> <Tabs defaultValue="tab-project-general">
<TabList> <TabList>
<Tab value="tab-project-general">General</Tab> <Tab value="tab-project-general">General</Tab>
</TabList>
<TabPanel value="tab-project-general">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretScanningConfigActions.Read} I={ProjectPermissionSecretScanningConfigActions.Read}
a={ProjectPermissionSub.SecretScanningConfigs} a={ProjectPermissionSub.SecretScanningConfigs}
renderGuardBanner
> >
{(isAllowed) => <ProjectScanningConfigTab />
isAllowed && <Tab value="tab-project-scanning-config">Scanning Configuration</Tab>
}
</ProjectPermissionCan> </ProjectPermissionCan>
</TabList>
<TabPanel value="tab-project-general">
<ProjectGeneralTab />
</TabPanel> </TabPanel>
<ProjectPermissionCan
I={ProjectPermissionSecretScanningConfigActions.Read}
a={ProjectPermissionSub.SecretScanningConfigs}
>
{(isAllowed) =>
isAllowed && (
<TabPanel value="tab-project-scanning-config">
<ProjectScanningConfigTab />
</TabPanel>
)
}
</ProjectPermissionCan>
</Tabs> </Tabs>
</div> </div>
</div> </div>
@@ -5,10 +5,5 @@ import { SecretScanningLayout } from "@app/layouts/SecretScanningLayout";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout"
)({ )({
component: SecretScanningLayout, component: SecretScanningLayout
beforeLoad: async () => {
return {
breadcrumbs: []
};
}
}); });
@@ -5,7 +5,6 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { ProjectSshTab } from "./components/ProjectSshTab"; import { ProjectSshTab } from "./components/ProjectSshTab";
export const SettingsPage = () => { export const SettingsPage = () => {
@@ -21,25 +20,16 @@ export const SettingsPage = () => {
<Tabs defaultValue="tab-project-general"> <Tabs defaultValue="tab-project-general">
<TabList> <TabList>
<Tab value="tab-project-general">General</Tab> <Tab value="tab-project-general">General</Tab>
</TabList>
<TabPanel value="tab-project-general">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Project} a={ProjectPermissionSub.Project}
renderGuardBanner
> >
{(isAllowed) => isAllowed && <Tab value="tab-project-ssh">SSH Settings</Tab>} <ProjectSshTab />
</ProjectPermissionCan> </ProjectPermissionCan>
</TabList>
<TabPanel value="tab-project-general">
<ProjectGeneralTab />
</TabPanel> </TabPanel>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
{(isAllowed) =>
isAllowed && (
<TabPanel value="tab-project-ssh">
<ProjectSshTab />
</TabPanel>
)
}
</ProjectPermissionCan>
</Tabs> </Tabs>
</div> </div>
</div> </div>
@@ -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";
@@ -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>
);
};
@@ -1 +0,0 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
@@ -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>
);
};
@@ -1 +0,0 @@
export { ProjectGeneralTab } from "./ProjectGeneralTab";
@@ -1 +0,0 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
@@ -11,13 +11,14 @@ export const Route = createFileRoute(
breadcrumbs: [ breadcrumbs: [
...context.breadcrumbs, ...context.breadcrumbs,
{ {
label: "SSH Certificate Authorities", label: "Certificate Authorities",
link: linkOptions({ link: linkOptions({
to: "/ssh/$projectId/overview", to: "/projects/$projectId/ssh/cas",
params: { params
projectId: params.projectId
}
}) })
},
{
label: "Details"
} }
] ]
}; };
+16 -2
View File
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SshCasPage } from "./SshCasPage"; import { SshCasPage } from "./SshCasPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/cas" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/cas"
)({ )({
component: SshCasPage component: SshCasPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificate Authorities",
link: linkOptions({
to: "/projects/$projectId/ssh/cas",
params
})
}
]
};
}
}); });
@@ -1,9 +1,26 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SshHostGroupDetailsByIDPage } from "./SshHostGroupDetailsByIDPage"; import { SshHostGroupDetailsByIDPage } from "./SshHostGroupDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ssh-host-groups/$sshHostGroupId" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ssh-host-groups/$sshHostGroupId"
)({ )({
component: SshHostGroupDetailsByIDPage component: SshHostGroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Hosts",
link: linkOptions({
to: "/projects/$projectId/ssh/overview",
params
})
},
{
label: "Host Group"
}
]
};
}
}); });
+11 -1
View File
@@ -5,5 +5,15 @@ import { SshHostsPage } from "./SshHostsPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/overview" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/overview"
)({ )({
component: SshHostsPage component: SshHostsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Hosts"
}
]
};
}
}); });
+1 -6
View File
@@ -5,10 +5,5 @@ import { SshLayout } from "@app/layouts/SshLayout";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout" "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout"
)({ )({
component: SshLayout, component: SshLayout
beforeLoad: async () => {
return {
breadcrumbs: []
};
}
}); });