mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: completed breadcrumb and settings changes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
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 { Link, ReactNode } from "@tanstack/react-router";
|
||||
import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link";
|
||||
@@ -141,11 +141,11 @@ const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[]
|
||||
<DropdownMenuTrigger>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbSegment>
|
||||
{el.label} <FontAwesomeIcon icon={faCaretDown} size="sm" />
|
||||
{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
|
||||
|
||||
@@ -40,14 +40,16 @@ export const KmsLayout = () => {
|
||||
>
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}>KMIP</MenuItem>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/$projectId/cert-manager/settings"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
|
||||
</Link>
|
||||
{
|
||||
// <Link
|
||||
// to="/projects/$projectId/kms/settings"
|
||||
// params={{
|
||||
// projectId: currentWorkspace.id
|
||||
// }}
|
||||
// >
|
||||
// {({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
|
||||
// </Link>
|
||||
}
|
||||
</Menu>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -73,14 +73,16 @@ export const PkiManagerLayout = () => {
|
||||
>
|
||||
{({ 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>
|
||||
{
|
||||
// <Link
|
||||
// to="/projects/$projectId/cert-manager/settings"
|
||||
// params={{
|
||||
// projectId: currentWorkspace.id
|
||||
// }}
|
||||
// >
|
||||
// {({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
|
||||
// </Link>
|
||||
}
|
||||
</Menu>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ProjectGeneralLayout = () => {
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
|
||||
Project General
|
||||
Project Overview
|
||||
</div>
|
||||
<div className="mt-2 flex-grow">
|
||||
<Menu>
|
||||
|
||||
@@ -51,7 +51,7 @@ export const SecretManagerLayout = () => {
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
|
||||
Project Overview
|
||||
Secret Manager
|
||||
</div>
|
||||
<div className="mt-2 flex-grow">
|
||||
<Menu>
|
||||
@@ -110,9 +110,7 @@ export const SecretManagerLayout = () => {
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>{t("nav.menu.project-settings")}</MenuItem>
|
||||
)}
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
|
||||
</Link>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const SecretScanningLayout = () => {
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}>Findings</MenuItem>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/$projectId/cert-manager/settings"
|
||||
to="/projects/$projectId/secret-scanning/settings"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
|
||||
@@ -46,40 +46,14 @@ export const SshLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}> Certificate Templates</MenuItem>
|
||||
<MenuItem isSelected={isActive}> Certificate Authorities</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</ProjectPermissionCan>
|
||||
<Link
|
||||
to="/projects/$projectId/cert-manager/certificates"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}> Certificates</MenuItem>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/$projectId/cert-manager/certificate-authorities"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}> Certificates Authorities</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/$projectId/cert-manager/alerting"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}>Alerting</MenuItem>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/$projectId/cert-manager/settings"
|
||||
to="/projects/$projectId/ssh/settings"
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
|
||||
@@ -18,6 +18,9 @@ export const Route = createFileRoute(
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: params.caName
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -5,5 +5,15 @@ import { CertificatesPage } from "./CertificatesPage";
|
||||
export const Route = createFileRoute(
|
||||
"/_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
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Details"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -18,6 +18,9 @@ export const Route = createFileRoute(
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: params.subscriberName
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -5,5 +5,15 @@ import { PkiSubscribersPage } from "./PkiSubscribersPage";
|
||||
export const Route = createFileRoute(
|
||||
"/_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(
|
||||
"/_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();
|
||||
|
||||
@@ -5,10 +5,5 @@ import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout";
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout"
|
||||
)({
|
||||
component: PkiManagerLayout,
|
||||
beforeLoad: async () => {
|
||||
return {
|
||||
breadcrumbs: []
|
||||
};
|
||||
}
|
||||
component: PkiManagerLayout
|
||||
});
|
||||
|
||||
@@ -5,5 +5,15 @@ import { KmipPage } from "./KmipPage";
|
||||
export const Route = createFileRoute(
|
||||
"/_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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,5 +5,15 @@ import { OverviewPage } from "./OverviewPage";
|
||||
export const Route = createFileRoute(
|
||||
"/_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();
|
||||
|
||||
@@ -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";
|
||||
@@ -5,10 +5,5 @@ import { KmsLayout } from "@app/layouts/KmsLayout";
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout"
|
||||
)({
|
||||
component: KmsLayout,
|
||||
beforeLoad: async () => {
|
||||
return {
|
||||
breadcrumbs: []
|
||||
};
|
||||
}
|
||||
component: KmsLayout
|
||||
});
|
||||
|
||||
@@ -21,7 +21,12 @@ export const Route = createFileRoute(
|
||||
},
|
||||
beforeLoad: ({ context }) => {
|
||||
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";
|
||||
|
||||
@@ -6,9 +6,23 @@ export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/groups/$groupId"
|
||||
)({
|
||||
component: GroupDetailsByIDPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
beforeLoad: ({ context, params }) => {
|
||||
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";
|
||||
|
||||
@@ -6,9 +6,23 @@ export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/identities/$identityId"
|
||||
)({
|
||||
component: IdentityDetailsByIDPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
beforeLoad: ({ context, params }) => {
|
||||
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";
|
||||
|
||||
@@ -6,9 +6,23 @@ export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/members/$membershipId"
|
||||
)({
|
||||
component: MemberDetailsByIDPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
beforeLoad: ({ context, params }) => {
|
||||
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";
|
||||
|
||||
@@ -6,9 +6,23 @@ export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/roles/$roleSlug"
|
||||
)({
|
||||
component: RoleDetailsBySlugPage,
|
||||
beforeLoad: ({}) => {
|
||||
beforeLoad: ({ context, params }) => {
|
||||
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 = () => {
|
||||
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(
|
||||
"/_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,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs
|
||||
// {
|
||||
// label: "Access Control",
|
||||
// link: linkOptions({
|
||||
// to: "/ssh/$projectId/access-management",
|
||||
// params: {
|
||||
// projectId: params.projectId
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: 'Settings',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 { z } from "zod";
|
||||
|
||||
@@ -16,5 +16,18 @@ export const Route = createFileRoute(
|
||||
validateSearch: zodValidator(SecretOverviewPageQuerySchema),
|
||||
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 {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Secrets",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/overview",
|
||||
params
|
||||
})
|
||||
},
|
||||
{
|
||||
type: BreadcrumbTypes.Dropdown,
|
||||
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 { 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 { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
@@ -18,22 +18,17 @@ export const SettingsPage = () => {
|
||||
{
|
||||
name: "Encryption",
|
||||
key: "tab-project-encryption",
|
||||
isHidden:
|
||||
currentWorkspace?.version !== ProjectVersion.V3 ||
|
||||
currentWorkspace?.type !== ProjectType.SecretManager,
|
||||
isHidden: currentWorkspace?.version !== ProjectVersion.V3,
|
||||
Component: EncryptionTab
|
||||
},
|
||||
{
|
||||
name: "Workflow Integrations",
|
||||
key: "tab-workflow-integrations",
|
||||
isHidden: currentWorkspace?.type !== ProjectType.SecretManager,
|
||||
Component: WorkflowIntegrationTab
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
key: "tab-project-webhooks",
|
||||
isHidden: currentWorkspace?.type !== ProjectType.SecretManager,
|
||||
|
||||
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 { 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 { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
|
||||
import { DeleteProjectProtection } from "../DeleteProjectProtection";
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { EnvironmentSection } from "../EnvironmentSection";
|
||||
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
||||
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
||||
@@ -16,24 +13,18 @@ import { SecretTagsSection } from "../SecretTagsSection";
|
||||
|
||||
export const ProjectGeneralTab = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
{isSecretManager && <EnvironmentSection />}
|
||||
{isSecretManager && <SecretTagsSection />}
|
||||
{isSecretManager && <AutoCapitalizationSection />}
|
||||
{isSecretManager && <SecretSharingSection />}
|
||||
{isSecretManager && <SecretSnapshotsLegacySection />}
|
||||
{isSecretManager && <PointInTimeVersionLimitSection />}
|
||||
<AuditLogsRetentionSection />
|
||||
{isSecretManager && <BackfillSecretReferenceSecretion />}
|
||||
{currentWorkspace?.version !== ProjectVersion.V3 && isSecretManager && (
|
||||
<RebuildSecretIndicesSection />
|
||||
)}
|
||||
<EnvironmentSection />
|
||||
<SecretTagsSection />
|
||||
<AutoCapitalizationSection />
|
||||
<SecretSharingSection />
|
||||
<SecretSnapshotsLegacySection />
|
||||
<PointInTimeVersionLimitSection />
|
||||
<BackfillSecretReferenceSecretion />
|
||||
{currentWorkspace?.version !== ProjectVersion.V3 && <RebuildSecretIndicesSection />}
|
||||
<DeleteProjectProtection />
|
||||
<DeleteProjectSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
|
||||
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { EnvironmentSection } from "./EnvironmentSection";
|
||||
export { SecretTagsSection } from "./SecretTagsSection";
|
||||
|
||||
@@ -13,12 +13,12 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Data Sources",
|
||||
link: linkOptions({
|
||||
to: "/secret-scanning/$projectId/data-sources",
|
||||
to: "/projects/$projectId/secret-scanning/data-sources",
|
||||
params
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Data Source"
|
||||
label: "Details"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -5,15 +5,15 @@ import { SecretScanningDataSourcesPage } from "./SecretScanningDataSourcesPage";
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/data-sources/"
|
||||
)({
|
||||
component: SecretScanningDataSourcesPage
|
||||
// beforeLoad: ({ context }) => {
|
||||
// return {
|
||||
// breadcrumbs: [
|
||||
// ...context.breadcrumbs,
|
||||
// {
|
||||
// label: "Data Sources"
|
||||
// }
|
||||
// ]
|
||||
// };
|
||||
// }
|
||||
component: SecretScanningDataSourcesPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Data Sources"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab";
|
||||
|
||||
export const SettingsPage = () => {
|
||||
@@ -22,30 +21,16 @@ export const SettingsPage = () => {
|
||||
<Tabs defaultValue="tab-project-general">
|
||||
<TabList>
|
||||
<Tab value="tab-project-general">General</Tab>
|
||||
</TabList>
|
||||
<TabPanel value="tab-project-general">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningConfigActions.Read}
|
||||
a={ProjectPermissionSub.SecretScanningConfigs}
|
||||
renderGuardBanner
|
||||
>
|
||||
{(isAllowed) =>
|
||||
isAllowed && <Tab value="tab-project-scanning-config">Scanning Configuration</Tab>
|
||||
}
|
||||
<ProjectScanningConfigTab />
|
||||
</ProjectPermissionCan>
|
||||
</TabList>
|
||||
<TabPanel value="tab-project-general">
|
||||
<ProjectGeneralTab />
|
||||
</TabPanel>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningConfigActions.Read}
|
||||
a={ProjectPermissionSub.SecretScanningConfigs}
|
||||
>
|
||||
{(isAllowed) =>
|
||||
isAllowed && (
|
||||
<TabPanel value="tab-project-scanning-config">
|
||||
<ProjectScanningConfigTab />
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
</ProjectPermissionCan>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,5 @@ import { SecretScanningLayout } from "@app/layouts/SecretScanningLayout";
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout"
|
||||
)({
|
||||
component: SecretScanningLayout,
|
||||
beforeLoad: async () => {
|
||||
return {
|
||||
breadcrumbs: []
|
||||
};
|
||||
}
|
||||
component: SecretScanningLayout
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
|
||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
import { ProjectSshTab } from "./components/ProjectSshTab";
|
||||
|
||||
export const SettingsPage = () => {
|
||||
@@ -21,25 +20,16 @@ export const SettingsPage = () => {
|
||||
<Tabs defaultValue="tab-project-general">
|
||||
<TabList>
|
||||
<Tab value="tab-project-general">General</Tab>
|
||||
</TabList>
|
||||
<TabPanel value="tab-project-general">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
renderGuardBanner
|
||||
>
|
||||
{(isAllowed) => isAllowed && <Tab value="tab-project-ssh">SSH Settings</Tab>}
|
||||
<ProjectSshTab />
|
||||
</ProjectPermissionCan>
|
||||
</TabList>
|
||||
<TabPanel value="tab-project-general">
|
||||
<ProjectGeneralTab />
|
||||
</TabPanel>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
||||
{(isAllowed) =>
|
||||
isAllowed && (
|
||||
<TabPanel value="tab-project-ssh">
|
||||
<ProjectSshTab />
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
</ProjectPermissionCan>
|
||||
</Tabs>
|
||||
</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: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "SSH Certificate Authorities",
|
||||
label: "Certificate Authorities",
|
||||
link: linkOptions({
|
||||
to: "/ssh/$projectId/overview",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
to: "/projects/$projectId/ssh/cas",
|
||||
params
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Details"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { SshCasPage } from "./SshCasPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_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";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,5 +5,15 @@ import { SshHostsPage } from "./SshHostsPage";
|
||||
export const Route = createFileRoute(
|
||||
"/_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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,10 +5,5 @@ import { SshLayout } from "@app/layouts/SshLayout";
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout"
|
||||
)({
|
||||
component: SshLayout,
|
||||
beforeLoad: async () => {
|
||||
return {
|
||||
breadcrumbs: []
|
||||
};
|
||||
}
|
||||
component: SshLayout
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user