Merge pull request #3872 from Infisical/feat/team-autonomy-product-migration

feat: project ui v3
This commit is contained in:
Maidul Islam
2025-07-01 21:09:43 -04:00
committed by GitHub
540 changed files with 8386 additions and 10521 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ export const Route = createFileRoute("/_authenticate/_inject-org-details/admin/_
beforeLoad: ({ context }) => {
if (!context.user.superAdmin) {
throw redirect({
to: "/organization/secret-manager/overview"
to: "/organization/projects"
});
}
}
@@ -4,7 +4,6 @@ import { useServerConfig } from "@app/context";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { queryClient } from "@app/hooks/api/reactQuery";
import { userKeys } from "@app/hooks/api/users";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: string) => {
const userOrgs = await fetchOrganizations();
@@ -13,7 +12,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
if (organizationId) {
localStorage.setItem("orgData.id", organizationId);
navigate({ to: "/organization/secret-manager/overview" });
navigate({ to: "/organization/projects" });
return;
}
@@ -21,7 +20,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
// user is part of at least 1 non-auth enforced org
const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id;
localStorage.setItem("orgData.id", userOrg);
navigate({ to: `/organization/${ProjectType.SecretManager}/overview` as const });
navigate({ to: "/organization/projects" });
} else {
// user is not part of any non-auth enforced orgs
localStorage.removeItem("orgData.id");
@@ -28,7 +28,6 @@ import {
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { isLoggedIn } from "@app/hooks/api/reactQuery";
import { ProjectType } from "@app/hooks/api/workspace/types";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
@@ -204,7 +203,7 @@ export const SignupInvitePage = () => {
localStorage.setItem("orgData.id", orgId);
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
to: "/organization/projects"
});
};
@@ -13,7 +13,6 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
import { useServerConfig } from "@app/context";
import { useVerifySignupEmailVerificationCode } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const SignUpPage = () => {
const [email, setEmail] = useState("");
@@ -73,7 +72,7 @@ export const SignUpPage = () => {
if (!serverDetails?.emailConfigured && step === 4) {
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
to: "/organization/projects"
});
}
})();
@@ -18,7 +18,6 @@ import { useToggle } from "@app/hooks";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { ProjectType } from "@app/hooks/api/workspace/types";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
@@ -207,7 +206,7 @@ export const UserInfoSSOStep = ({
localStorage.setItem("orgData.id", orgId);
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
to: "/organization/projects"
});
} catch (error) {
setIsLoading(false);
@@ -12,7 +12,6 @@ import {
useGetPkiCollectionById,
useUpdatePkiCollection
} from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
@@ -83,7 +82,7 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
});
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/pki-collections/$collectionId` as const,
to: "/projects/$projectId/cert-manager/pki-collections/$collectionId",
params: {
projectId,
collectionId
@@ -61,7 +61,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
key={`pki-collection-${pkiCollection.id}`}
onClick={() =>
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/pki-collections/$collectionId` as const,
to: `/projects/$projectId/${ProjectType.CertificateManager}/pki-collections/$collectionId` as const,
params: {
projectId,
collectionId: pkiCollection.id
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { AlertingPage } from "./AlertingPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/alerting"
)({
component: AlertingPage,
beforeLoad: ({ context }) => {
@@ -71,7 +71,7 @@ const Page = () => {
handlePopUpClose("deleteCa");
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/certificate-authorities` as const,
to: `/projects/$projectId/${ProjectType.CertificateManager}/certificate-authorities` as const,
params: {
projectId
}
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/ca/$caName"
)({
component: CertAuthDetailsByIDPage,
beforeLoad: ({ context, params }) => {
@@ -13,11 +13,14 @@ export const Route = createFileRoute(
{
label: "Certificate Authorities",
link: linkOptions({
to: "/cert-manager/$projectId/certificate-authorities",
to: "/projects/$projectId/cert-manager/certificate-authorities",
params: {
projectId: params.projectId
}
})
},
{
label: params.caName
}
]
};
@@ -79,7 +79,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
key={`ca-${ca.id}`}
onClick={() =>
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/ca/$caName` as const,
to: `/projects/$projectId/${ProjectType.CertificateManager}/ca/$caName` as const,
params: {
projectId: currentWorkspace.id,
caName: ca.name
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { CertificateAuthoritiesPage } from "./CertificateAuthoritiesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-authorities"
)({
component: CertificateAuthoritiesPage,
beforeLoad: ({ context }) => {
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { CertificatesPage } from "./CertificatesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificates"
)({
component: CertificatesPage
component: CertificatesPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificates"
}
]
};
}
});
@@ -57,7 +57,7 @@ export const PkiCollectionPage = () => {
});
handlePopUpClose("deletePkiCollection");
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const,
to: `/projects/$projectId/${ProjectType.CertificateManager}/certificates` as const,
params: {
projectId
}
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/pki-collections/$collectionId"
)({
component: PkiCollectionDetailsByIDPage,
beforeLoad: ({ context, params }) => {
@@ -13,11 +13,14 @@ export const Route = createFileRoute(
{
label: "Certificate Collections",
link: linkOptions({
to: "/cert-manager/$projectId/certificates",
to: "/projects/$projectId/cert-manager/certificates",
params: {
projectId: params.projectId
}
})
},
{
label: "Details"
}
]
};
@@ -61,7 +61,7 @@ const Page = () => {
handlePopUpClose("deletePkiSubscriber");
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/subscribers` as const,
to: `/projects/$projectId/${ProjectType.CertificateManager}/subscribers` as const,
params: {
projectId
}
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/$subscriberName"
)({
component: PkiSubscriberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
@@ -13,11 +13,14 @@ export const Route = createFileRoute(
{
label: "Subscribers",
link: linkOptions({
to: "/cert-manager/$projectId/subscribers",
to: "/projects/$projectId/cert-manager/subscribers",
params: {
projectId: params.projectId
}
})
},
{
label: params.subscriberName
}
]
};
@@ -76,7 +76,7 @@ export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => {
key={`pki-subscriber-${subscriber.id}`}
onClick={() =>
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/subscribers/$subscriberName` as const,
to: `/projects/$projectId/${ProjectType.CertificateManager}/subscribers/$subscriberName` as const,
params: {
projectId: currentWorkspace.id,
subscriberName: subscriber.name
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { PkiSubscribersPage } from "./PkiSubscribersPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/"
)({
component: PkiSubscribersPage
component: PkiSubscribersPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Subscribers"
}
]
};
}
});
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { PkiTemplateListPage } from "./PkiTemplateListPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-templates/"
)({
component: PkiTemplateListPage
component: PkiTemplateListPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificate Templates"
}
]
};
}
});
@@ -3,9 +3,13 @@ import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }];
const tabs = [
{
name: "General",
key: "tab-project-general",
Component: () => <div className="text-white">Coming soon</div>
}
];
export const SettingsPage = () => {
const { t } = useTranslation();
@@ -1,14 +0,0 @@
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
export const ProjectGeneralTab = () => {
return (
<div>
<ProjectOverviewChangeSection showSlugField />
<AuditLogsRetentionSection />
<DeleteProjectSection />
</div>
);
};
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/settings"
)({
component: SettingsPage,
beforeLoad: ({ context }) => {
+4 -36
View File
@@ -1,41 +1,9 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout";
import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout"
)({
component: ProjectLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
return {
breadcrumbs: [
{
label: "Cert Managers",
link: linkOptions({ to: "/organization/cert-manager/overview" })
},
{
label: project.name,
link: linkOptions({
to: "/cert-manager/$projectId/subscribers",
params: { projectId: project.id }
})
}
]
};
}
component: PkiManagerLayout
});
+12 -2
View File
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { KmipPage } from "./KmipPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/kmip"
)({
component: KmipPage
component: KmipPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "KMIP"
}
]
};
}
});
+12 -2
View File
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/overview"
)({
component: OverviewPage
component: OverviewPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Overview"
}
]
};
}
});
@@ -3,9 +3,13 @@ import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }];
const tabs = [
{
name: "General",
key: "tab-project-general",
Component: () => <div className="text-white">Coming soon...</div>
}
];
export const SettingsPage = () => {
const { t } = useTranslation();
@@ -16,7 +20,7 @@ export const SettingsPage = () => {
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader title={t("settings.project.title")} />
<PageHeader title="Settings" />
<Tabs defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (
@@ -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 +0,0 @@
export { ProjectGeneralTab } from "./ProjectGeneralTab";
@@ -1 +0,0 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/settings"
)({
component: SettingsPage,
beforeLoad: ({ context }) => {
+4 -36
View File
@@ -1,41 +1,9 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout";
import { KmsLayout } from "@app/layouts/KmsLayout";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout"
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout"
)({
component: ProjectLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
return {
breadcrumbs: [
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
},
{
label: project.name,
link: linkOptions({
to: "/kms/$projectId/overview",
params: { projectId: project.id }
})
}
]
};
}
component: KmsLayout
});
@@ -12,7 +12,6 @@ import { SessionStorageKeys } from "@app/const";
import { useServerConfig } from "@app/context";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { setAuthToken } from "@app/hooks/api/reactQuery";
import { ProjectType } from "@app/hooks/api/workspace/types";
const QueryParamsSchema = z.object({
callback_port: z.coerce.number().optional().catch(undefined),
@@ -112,7 +111,7 @@ export const Route = createFileRoute("/_restrict-login-signup")({
throw redirect({ to: "/login/select-organization" });
}
throw redirect({
to: `/organization/${ProjectType.SecretManager}/overview` as const
to: "/organization/projects"
});
},
component: AuthConsentWrapper
@@ -1,6 +1,4 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -24,11 +22,6 @@ export const Route = createFileRoute(
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Access Control"
}
@@ -29,6 +29,7 @@ import {
OrgPermissionAdminConsoleAction,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { getProjectHomePage } from "@app/helpers/project";
import { withPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks";
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
@@ -58,26 +59,8 @@ export const OrgAdminProjects = withPermission(
await orgAdminAccessProject.mutateAsync({
projectId
});
if (type === ProjectType.CertificateManager) {
await navigate({
to: "/cert-manager/$projectId/subscribers" as const,
params: {
projectId
}
});
return;
}
if (type === ProjectType.SecretScanning) {
await navigate({
to: "/secret-scanning/$projectId/data-sources" as const,
params: {
projectId
}
});
return;
}
await navigate({
to: `/${type}/$projectId/overview` as const,
to: getProjectHomePage(type),
params: {
projectId
}
@@ -122,7 +105,7 @@ export const OrgAdminProjects = withPermission(
<TBody>
{isProjectsLoading && <TableSkeleton columns={4} innerKey="projects" />}
{!isProjectsLoading &&
projects?.map(({ name, slug, createdAt, type, id }) => (
projects?.map(({ name, slug, createdAt, id, defaultProduct }) => (
<Tr key={`project-${id}`} className="group w-full">
<Td>{name}</Td>
<Td>{slug}</Td>
@@ -143,7 +126,7 @@ export const OrgAdminProjects = withPermission(
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleAccessProject(type, id);
handleAccessProject(defaultProduct, id);
}}
icon={<FontAwesomeIcon icon={faSignIn} />}
disabled={
@@ -1,6 +1,4 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { AdminPage } from "./AdminPage";
@@ -10,11 +8,6 @@ export const Route = createFileRoute(
component: AdminPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Admin Console"
}
@@ -1,6 +1,4 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { AppConnectionsPage } from "./AppConnectionsPage";
@@ -10,11 +8,6 @@ export const Route = createFileRoute(
component: AppConnectionsPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "App Connections"
}
@@ -418,10 +418,10 @@ export const LogsFilter = ({
onChange(e);
}}
placeholder="All projects"
options={workspacesInOrg.map(({ name, id, type }) => ({
options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({
name,
id,
type
type: defaultProduct
}))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
@@ -1,6 +1,4 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage";
@@ -10,11 +8,6 @@ export const Route = createFileRoute(
component: AuditLogsPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Audit Logs"
}
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { OrgPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { OrgPermissionBillingActions, OrgPermissionSubjects } from "@app/context";
import { BillingTabGroup } from "./components";
@@ -18,10 +19,11 @@ export const BillingPage = () => {
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6">
<div className="my-6">
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
<div />
</div>
<PageHeader
title={t("billing.title")}
description="View your billing plan, next billing cycle."
/>
<OrgPermissionCan
passThrough={false}
I={OrgPermissionBillingActions.Read}
@@ -5,5 +5,14 @@ import { BillingPage } from "./BillingPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/billing"
)({
component: BillingPage
component: BillingPage,
beforeLoad: () => {
return {
breadcrumbs: [
{
label: "Billing"
}
]
};
}
});
@@ -1,7 +0,0 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProductOverviewPage } from "../SecretManagerOverviewPage/SecretManagerOverviewPage";
export const CertManagerOverviewPage = () => (
<ProductOverviewPage type={ProjectType.CertificateManager} />
);
@@ -1,23 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertManagerOverviewPage } from "./CertManagerOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview"
)({
component: CertManagerOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Cert Management",
link: linkOptions({ to: "/organization/cert-manager/overview" })
}
]
})
});
@@ -1,20 +0,0 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const CertManagerSettingsPage = () => {
return (
<>
<Helmet>
<title>Cert Management Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="Cert Management Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};
@@ -1,26 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertManagerSettingsPage } from "./CertManagerSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings"
)({
component: CertManagerSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Cert Management",
link: linkOptions({ to: "/organization/cert-manager/overview" })
},
{
label: "Settings"
}
]
})
});
@@ -1,6 +1,4 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { GatewayListPage } from "./GatewayListPage";
@@ -10,11 +8,6 @@ export const Route = createFileRoute(
component: GatewayListPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Gateways"
}
@@ -1,5 +1,3 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
@@ -10,11 +8,6 @@ export const Route = createFileRoute(
component: GroupDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
@@ -44,7 +44,7 @@ type Props = {
const Content = ({ identityId, handlePopUpToggle }: Omit<Props, "popUp">) => {
const { currentOrg } = useOrganization();
const { data: workspaces = [] } = useGetUserWorkspaces({ type: "all" });
const { data: workspaces = [] } = useGetUserWorkspaces();
const { mutateAsync: addIdentityToWorkspace } = useAddIdentityToWorkspace();
const {
@@ -5,7 +5,7 @@ import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { IdentityMembership } from "@app/hooks/api/identities/types";
@@ -50,7 +50,7 @@ export const IdentityProjectRow = ({
onClick={() => {
if (isAccessible) {
navigate({
to: `/${project?.type}/$projectId/access-management` as const,
to: "/projects/$projectId/access-management",
params: {
projectId: project.id
},
@@ -68,9 +68,6 @@ export const IdentityProjectRow = ({
}}
>
<Td className="max-w-0 truncate">{project.name}</Td>
<Td>
<Tag size="xs">{project.type}</Tag>
</Td>
<Td>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
roles.length > 1 ? ` (+${roles.length - 1})` : ""
}`}</Td>
@@ -118,7 +118,6 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) =>
</div>
</Th>
<Th>Type</Th>
<Th>Role</Th>
<Th>Added On</Th>
<Th className="w-5" />
@@ -1,5 +1,3 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
@@ -10,11 +8,6 @@ export const Route = createFileRoute(
component: IdentityDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
@@ -1,5 +0,0 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProductOverviewPage } from "../SecretManagerOverviewPage/SecretManagerOverviewPage";
export const KmsOverviewPage = () => <ProductOverviewPage type={ProjectType.KMS} />;
@@ -1,23 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { KmsOverviewPage } from "./KmsOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/kms/overview"
)({
component: KmsOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
}
]
})
});
@@ -1,20 +0,0 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const KmsSettingsPage = () => {
return (
<>
<Helmet>
<title>KMS Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="KMS Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};
@@ -1,26 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { KmsSettingsPage } from "./KmsSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/kms/settings"
)({
component: KmsSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
},
{
label: "Settings"
}
]
})
});
@@ -10,31 +10,25 @@ import { NewProjectModal } from "@app/components/projects";
import { PageHeader } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { AllProjectView } from "./components/AllProjectView";
import { MyProjectView } from "./components/MyProjectView";
import { ProjectListToggle, ProjectListView } from "./components/ProjectListToggle";
const formatDescription = (type: ProjectType) => {
if (type === ProjectType.SecretManager)
return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.";
if (type === ProjectType.CertificateManager)
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
if (type === ProjectType.KMS)
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
if (type === ProjectType.SecretScanning)
return "Connect and monitor data sources to prevent secret leaks.";
return "Infisical SSH lets you issue SSH credentials to users for short-lived, secure SSH access to infrastructure.";
};
// const formatDescription = (type: ProjectType) => {
// if (type === ProjectType.SecretManager)
// return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.";
// if (type === ProjectType.CertificateManager)
// return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
// if (type === ProjectType.KMS)
// return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
// if (type === ProjectType.SecretScanning)
// return "Connect and monitor data sources to prevent secret leaks.";
// return "Infisical SSH lets you issue SSH credentials to users for short-lived, secure SSH access to infrastructure.";
// };
type Props = {
type: ProjectType;
};
// #TODO: Update all the workspaceIds
export const ProductOverviewPage = ({ type }: Props) => {
export const ProjectsPage = () => {
const { t } = useTranslation();
const [projectListView, setProjectListView] = useState(ProjectListView.MyProjects);
@@ -87,19 +81,17 @@ export const ProductOverviewPage = ({ type }: Props) => {
<ProjectListToggle value={projectListView} onChange={setProjectListView} />
</div>
}
description={formatDescription(type)}
description="Your team's complete security toolkit - organized and ready when you need them."
/>
</div>
{projectListView === ProjectListView.MyProjects ? (
<MyProjectView
type={type}
onAddNewProject={() => handlePopUpOpen("addNewWs")}
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
isAddingProjectsAllowed={isAddingProjectsAllowed}
/>
) : (
<AllProjectView
type={type}
onAddNewProject={() => handlePopUpOpen("addNewWs")}
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
isAddingProjectsAllowed={isAddingProjectsAllowed}
@@ -108,7 +100,6 @@ export const ProductOverviewPage = ({ type }: Props) => {
<NewProjectModal
isOpen={popUp.addNewWs.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
projectType={type}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
@@ -118,7 +109,3 @@ export const ProductOverviewPage = ({ type }: Props) => {
</div>
);
};
export const SecretManagerOverviewPage = () => (
<ProductOverviewPage type={ProjectType.SecretManager} />
);
@@ -35,10 +35,9 @@ import {
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useRequestProjectAccess, useSearchProjects } from "@app/hooks/api";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
import { Workspace } from "@app/hooks/api/workspace/types";
type Props = {
type: ProjectType;
onAddNewProject: () => void;
onUpgradePlan: () => void;
isAddingProjectsAllowed: boolean;
@@ -92,7 +91,6 @@ const RequestAccessModal = ({ projectId, onPopUpToggle }: RequestAccessModalProp
};
export const AllProjectView = ({
type,
onAddNewProject,
onUpgradePlan,
isAddingProjectsAllowed
@@ -125,7 +123,6 @@ export const AllProjectView = ({
const { data: searchedProjects, isPending: isProjectLoading } = useSearchProjects({
limit,
offset,
type,
name: debouncedSearch || undefined,
orderDirection
});
@@ -225,7 +222,7 @@ export const AllProjectView = ({
onKeyDown={(evt) => {
if (evt.key === "Enter" && workspace.isMember) {
navigate({
to: getProjectHomePage(workspace),
to: getProjectHomePage(workspace.defaultProduct),
params: {
projectId: workspace.id
}
@@ -235,7 +232,7 @@ export const AllProjectView = ({
onClick={() => {
if (workspace.isMember) {
navigate({
to: getProjectHomePage(workspace),
to: getProjectHomePage(workspace.defaultProduct),
params: {
projectId: workspace.id
}
@@ -29,10 +29,9 @@ import { useGetUserWorkspaces } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
import { Workspace } from "@app/hooks/api/workspace/types";
type Props = {
type: ProjectType;
onAddNewProject: () => void;
onUpgradePlan: () => void;
isAddingProjectsAllowed: boolean;
@@ -48,7 +47,6 @@ enum ProjectsViewMode {
}
export const MyProjectView = ({
type,
onAddNewProject,
onUpgradePlan,
isAddingProjectsAllowed
@@ -56,9 +54,7 @@ export const MyProjectView = ({
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserWorkspaces({
type
});
const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserWorkspaces();
const {
setPage,
perPage,
@@ -157,7 +153,7 @@ export const MyProjectView = ({
<div
onClick={() => {
navigate({
to: getProjectHomePage(workspace),
to: getProjectHomePage(workspace.defaultProduct),
params: {
projectId: workspace.id
}
@@ -202,11 +198,6 @@ export const MyProjectView = ({
</div>
<div className="flex w-full flex-row items-end justify-between place-self-end">
{type === ProjectType.SecretManager && (
<div className="mt-0 text-xs text-mineshaft-400">
{workspace.environments?.length || 0} environments
</div>
)}
<button type="button">
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 px-4 py-2 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
Explore{" "}
@@ -224,7 +215,7 @@ export const MyProjectView = ({
<div
onClick={() => {
navigate({
to: getProjectHomePage(workspace),
to: getProjectHomePage(workspace.defaultProduct),
params: {
projectId: workspace.id
}
@@ -239,11 +230,6 @@ export const MyProjectView = ({
<div className="truncate text-sm text-mineshaft-100">{workspace.name}</div>
</div>
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
{type === ProjectType.SecretManager && (
<div className="text-center text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
)}
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
@@ -0,0 +1,16 @@
import { createFileRoute } from "@tanstack/react-router";
import { ProjectsPage } from "./ProjectsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/projects"
)({
component: ProjectsPage,
context: () => ({
breadcrumbs: [
{
label: "Projects"
}
]
})
});
@@ -1,5 +1,3 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleByIDPage } from "./RoleByIDPage";
@@ -10,11 +8,6 @@ export const Route = createFileRoute(
component: RoleByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
@@ -1,22 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute } from "@tanstack/react-router";
import { SecretManagerOverviewPage } from "./SecretManagerOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview"
)({
component: SecretManagerOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Management"
}
]
})
});
@@ -1,20 +0,0 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const SecretManagerSettingsPage = () => {
return (
<>
<Helmet>
<title>Secret Management Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="Secret Management Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};
@@ -1,26 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretManagerSettingsPage } from "./SecretManagerSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings"
)({
component: SecretManagerSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Management",
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Settings"
}
]
})
});
@@ -1,7 +0,0 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProductOverviewPage } from "../SecretManagerOverviewPage/SecretManagerOverviewPage";
export const SecretScanningOverviewPage = () => (
<ProductOverviewPage type={ProjectType.SecretScanning} />
);
@@ -1,23 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretScanningOverviewPage } from "./SecretScanningOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning/overview"
)({
component: SecretScanningOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Scanning",
link: linkOptions({ to: "/organization/secret-scanning/overview" })
}
]
})
});
@@ -1,20 +0,0 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const SecretScanningSettingsPage = () => {
return (
<>
<Helmet>
<title>Secret Scanning Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="Secret Scanning Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};
@@ -1,26 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretScanningSettingsPage } from "./SecretScanningSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning/settings"
)({
component: SecretScanningSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Scanning",
link: linkOptions({ to: "/organization/secret-scanning/overview" })
},
{
label: "Settings"
}
]
})
});
@@ -1,6 +1,4 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -21,11 +19,6 @@ export const Route = createFileRoute(
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Secret Sharing"
}
@@ -1,5 +1,3 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -20,11 +18,6 @@ export const Route = createFileRoute(
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Secret Sharing",
link: linkOptions({ to: "/organization/secret-sharing" })
@@ -3,18 +3,14 @@ import { useOrgPermission } from "@app/context";
import { OrgDeleteSection } from "../OrgDeleteSection";
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
import { OrgNameChangeSection } from "../OrgNameChangeSection";
import { OrgProductSelectSection } from "../OrgProductSelectSection";
export const OrgGeneralTab = () => {
const { membership } = useOrgPermission();
return (
<>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgNameChangeSection />
<OrgIncidentContactsSection />
{membership && membership.role === "admin" && <OrgDeleteSection />}
</div>
{membership && membership.role === "admin" && <OrgProductSelectSection />}
</>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgNameChangeSection />
<OrgIncidentContactsSection />
{membership && membership.role === "admin" && <OrgDeleteSection />}
</div>
);
};
@@ -1,11 +1,8 @@
import { useState } from "react";
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
import { Link, useSearch } from "@tanstack/react-router";
import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { ImportTab } from "../ImportTab";
@@ -14,6 +11,7 @@ import { OrgEncryptionTab } from "../OrgEncryptionTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
import { OrgSecurityTab } from "../OrgSecurityTab";
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
export const OrgTabGroup = () => {
const search = useSearch({
@@ -33,60 +31,7 @@ export const OrgTabGroup = () => {
{
name: "Project Templates",
key: "project-templates",
// scott: temporary, remove once users have adjusted
// eslint-disable-next-line react/no-unstable-nested-components
component: () => (
<div>
<NoticeBannerV2
className="mx-auto"
titleClassName="text-base"
title="Project Templates Relocated"
>
<p className="mt-1 text-mineshaft-300">
Project templates have been relocated and are now product specific:
</p>
<ul className="mb-1 flex gap-x-4 text-mineshaft-200">
{[
{
type: ProjectType.SecretManager,
label: "Secret Management",
icon: "sliding-carousel"
},
{
type: ProjectType.CertificateManager,
label: "Certificate Management",
icon: "note"
},
{
type: ProjectType.KMS,
label: "KMS",
icon: "unlock"
},
{
type: ProjectType.SSH,
label: "SSH",
icon: "verified"
}
].map(({ label, type, icon }, index) => (
<li key={`project-template-${type}`}>
<Link
to={`/organization/${type}/settings`}
className="mt-1 flex items-center gap-x-2 hover:text-mineshaft-100"
>
{index !== 0 && <span className="text-mineshaft-300">•</span>}
<DotLottieReact
src={`/lotties/${icon}.json`}
loop
className="mt-0.5 h-5 w-5"
/>{" "}
<span className="underline underline-offset-2">{label}</span>
</Link>
</li>
))}
</ul>
</NoticeBannerV2>
</div>
)
component: ProjectTemplatesTab
},
{ name: "KMIP", key: "kmip", component: KmipTab }
];
@@ -0,0 +1,9 @@
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { ProjectTemplatesSection } from "./components";
export const ProjectTemplatesTab = withPermission(() => <ProjectTemplatesSection />, {
action: OrgPermissionActions.Read,
subject: OrgPermissionSubjects.ProjectTemplates
});
@@ -0,0 +1,49 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates";
type Props = {
template?: TProjectTemplate;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const DeleteProjectTemplateModal = ({ isOpen, onOpenChange, template }: Props) => {
const deleteTemplate = useDeleteProjectTemplate();
if (!template) return null;
const { id: templateId, name } = template;
const handleDeleteProjectTemplate = async () => {
try {
await deleteTemplate.mutateAsync({
templateId
});
createNotification({
text: "Successfully removed project template",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed remove project template",
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure you want to delete ${name}?`}
deleteKey="confirm"
onDeleteApproved={handleDeleteProjectTemplate}
/>
);
};
@@ -0,0 +1,55 @@
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, EmptyState, Spinner } from "@app/components/v2";
import {
InfisicalProjectTemplate,
TProjectTemplate,
useGetProjectTemplateById
} from "@app/hooks/api/projectTemplates";
import { EditProjectTemplate } from "./components";
type Props = {
template: TProjectTemplate;
onBack: () => void;
};
export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
const isInfisicalTemplate = Object.values(InfisicalProjectTemplate).includes(
template.name as InfisicalProjectTemplate
);
const { data: projectTemplate, isPending } = useGetProjectTemplateById(template.id, {
initialData: template,
enabled: !isInfisicalTemplate
});
return (
<div>
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={onBack}
className="mb-4"
>
Back to Templates
</Button>
{/* eslint-disable-next-line no-nested-ternary */}
{isPending ? (
<div className="flex h-[60vh] w-full items-center justify-center p-24">
<Spinner />
</div>
) : projectTemplate ? (
<EditProjectTemplate
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
onBack={onBack}
/>
) : (
<EmptyState title="Error: Unable to find project template." className="py-12" />
)}
</div>
);
};
@@ -0,0 +1,119 @@
import { faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates";
import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal";
import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm";
import { ProjectTemplateRolesSection } from "./ProjectTemplateRolesSection";
type Props = {
projectTemplate: TProjectTemplate;
onBack: () => void;
isInfisicalTemplate: boolean;
};
export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBack }: Props) => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeTemplate",
"editDetails"
] as const);
const { id: templateId, name, description } = projectTemplate;
const deleteProjectTemplate = useDeleteProjectTemplate();
const handleRemoveTemplate = async () => {
try {
await deleteProjectTemplate.mutateAsync({
templateId
});
createNotification({
text: "Successfully removed project template",
type: "success"
});
onBack();
} catch (error) {
console.error(error);
createNotification({
text: "Failed to remove project template",
type: "error"
});
}
handlePopUpClose("removeTemplate");
};
return (
<>
<div className="mb-4 flex items-start justify-between border-b border-bunker-400 pb-4">
<div className="flex-col">
<h3 className="text-xl font-semibold">{name}</h3>
<h2 className="text-sm text-mineshaft-400">{description || "Project Template"}</h2>
</div>
{!isInfisicalTemplate && (
<div className="flex gap-2">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPencil} />}
size="xs"
colorSchema="secondary"
onClick={() => handlePopUpOpen("editDetails")}
>
Edit Details
</Button>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("removeTemplate");
}}
leftIcon={<FontAwesomeIcon icon={faTrash} />}
size="xs"
colorSchema="danger"
>
Delete Template
</Button>
)}
</OrgPermissionCan>
</div>
)}
</div>
<ProjectTemplateEnvironmentsForm
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
/>
<ProjectTemplateRolesSection
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
/>
<ProjectTemplateDetailsModal
isOpen={popUp.editDetails.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
projectTemplate={projectTemplate}
/>
<DeleteActionModal
isOpen={popUp.removeTemplate.isOpen}
title={`Are you sure you want to delete ${projectTemplate.name}?`}
deleteKey="confirm"
onChange={(isOpen) => handlePopUpToggle("removeTemplate", isOpen)}
onDeleteApproved={handleRemoveTemplate}
/>
</>
);
};
@@ -0,0 +1,193 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { faChevronLeft, faSave } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { isCustomProjectRole } from "@app/helpers/roles";
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
import { slugSchema } from "@app/lib/schemas";
import { AddPoliciesButton } from "@app/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import {
formRolePermission2API,
PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema,
rolePermission2Form
} from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils";
import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection";
type Props = {
projectTemplate: TProjectTemplate;
role?: TProjectTemplate["roles"][number];
onGoBack: () => void;
isDisabled?: boolean;
};
const formSchema = z.object({
slug: slugSchema(),
name: z.string().trim().min(1),
permissions: projectRoleFormSchema.shape.permissions
});
type TFormSchema = z.infer<typeof formSchema>;
export const ProjectTemplateEditRoleForm = ({
onGoBack,
projectTemplate,
role,
isDisabled
}: Props) => {
const formMethods = useForm<TFormSchema>({
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
resolver: zodResolver(formSchema)
});
const {
handleSubmit,
control,
formState: { isDirty, isSubmitting }
} = formMethods;
const updateProjectTemplate = useUpdateProjectTemplate();
const onSubmit = async (form: TFormSchema) => {
try {
await updateProjectTemplate.mutateAsync({
templateId: projectTemplate.id,
roles: [
...projectTemplate.roles.filter(
(r) => r.slug !== role?.slug && isCustomProjectRole(r.slug) // filter out default roles as well
),
{
...form,
permissions: formRolePermission2API(form.permissions)
}
]
});
onGoBack();
createNotification({
text: "Template roles successfully updated",
type: "success"
});
} catch (e: any) {
console.error(e);
createNotification({
text: "Failed to update template roles",
type: "error"
});
}
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<FormProvider {...formMethods}>
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<Button
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
className="text-base font-semibold text-mineshaft-200"
variant="link"
onClick={onGoBack}
>
{isDisabled ? "Back" : "Cancel"}
</Button>
{!isDisabled && (
<div className="flex items-center space-x-4">
{isDirty && (
<Button
className="mr-4 text-mineshaft-300"
variant="link"
isDisabled={isSubmitting}
isLoading={isSubmitting}
onClick={onGoBack}
>
Discard
</Button>
)}
<div className="flex items-center">
<Button
variant="outline_bg"
type="submit"
className={twMerge(
"mr-4 h-10 border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />}
>
Save
</Button>
<AddPoliciesButton isDisabled={isDisabled} />
</div>
</div>
)}
</div>
<div className="mt-2 border-b border-gray-800 p-4 pt-2 first:rounded-t-md last:rounded-b-md">
{isDisabled ? (
<div className="flex flex-col">
<span className="text-lg font-semibold">{role?.name}</span>
<span className="text-mineshaft-400">{role?.slug}</span>
</div>
) : (
<div className="flex w-full gap-2">
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Name"
className="mb-0 flex-1"
>
<Input {...field} autoFocus placeholder="Role name..." />
</FormControl>
)}
/>
<Controller
control={control}
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Slug"
className="mb-0 flex-1"
>
<Input {...field} placeholder="Role slug..." />
</FormControl>
)}
/>
</div>
)}
</div>
<div className="p-4">
<div className="mb-2 text-lg">Policies</div>
<PermissionEmptyState />
<div>
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
<GeneralPermissionPolicies
subject={subject}
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
title={PROJECT_PERMISSION_OBJECT[subject].title}
key={`project-permission-${subject}`}
isDisabled={isDisabled}
>
{renderConditionalComponents(subject, isDisabled)}
</GeneralPermissionPolicies>
))}
</div>
</div>
</FormProvider>
</form>
);
};
@@ -0,0 +1,289 @@
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faArrowDown, faArrowUp, faPlus, faSave, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
FormControl,
IconButton,
Input,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
import { slugSchema } from "@app/lib/schemas";
type Props = {
projectTemplate: TProjectTemplate;
isInfisicalTemplate: boolean;
};
const formSchema = z.object({
environments: z
.object({
name: z.string().trim().min(1),
slug: slugSchema({ min: 1, max: 64 })
})
.array()
.nullish()
});
type TFormSchema = z.infer<typeof formSchema>;
export const ProjectTemplateEnvironmentsForm = ({
projectTemplate,
isInfisicalTemplate
}: Props) => {
const {
control,
handleSubmit,
formState: { isDirty, errors },
reset
} = useForm<TFormSchema>({
defaultValues: {
environments: projectTemplate.environments
},
resolver: zodResolver(formSchema)
});
const { subscription } = useSubscription();
const {
fields: environments,
move,
remove,
append
} = useFieldArray({ control, name: "environments" });
const updateProjectTemplate = useUpdateProjectTemplate();
const onFormSubmit = async (form: TFormSchema) => {
try {
const { environments: updatedEnvs } = await updateProjectTemplate.mutateAsync({
environments: form.environments?.map((env, index) => ({
...env,
position: index + 1
})),
templateId: projectTemplate.id
});
reset({ environments: updatedEnvs });
createNotification({
text: "Project template updated successfully",
type: "success"
});
} catch (e: any) {
console.error(e);
createNotification({
text: e.message ?? "Failed to update project template",
type: "error"
});
}
};
const isEnvironmentLimitExceeded =
Boolean(subscription.environmentLimit) && environments.length >= subscription.environmentLimit;
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<div>
<h2 className="text-lg font-semibold">Project Environments</h2>
{!isInfisicalTemplate && (
<p className="text-sm text-mineshaft-400">
Add, rename, remove and reorder environments for this project template
</p>
)}
</div>
{!isInfisicalTemplate && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Button
type="submit"
colorSchema="primary"
className="ml-auto w-40"
variant={isDirty ? "solid" : "outline_bg"}
leftIcon={<FontAwesomeIcon icon={faSave} />}
isDisabled={!isAllowed || !isDirty}
>
{isDirty ? "Save" : "No"} Changes
</Button>
)}
</OrgPermissionCan>
)}
</div>
{errors.environments && (
<span className="my-4 text-sm text-red">{errors.environments.message}</span>
)}
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Friendly Name</Th>
<Th>Slug</Th>
{!isInfisicalTemplate && (
<Th>
<div className="flex w-full justify-end normal-case">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
renderTooltip={isEnvironmentLimitExceeded ? true : undefined}
allowedLabel={`Plan environment limit of ${subscription.environmentLimit} exceeded. Contact Infisical to increase limit.`}
>
{(isAllowed) => (
<Button
onClick={() => append({ name: "", slug: "" })}
colorSchema="secondary"
className="ml-auto"
variant="solid"
size="xs"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed || isEnvironmentLimitExceeded}
>
Add Environment
</Button>
)}
</OrgPermissionCan>
</div>
</Th>
)}
</Tr>
</THead>
<TBody>
{environments.map(({ id, name, slug }, pos) => (
<Tr key={id}>
<Td>
{isInfisicalTemplate ? (
name
) : (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Controller
control={control}
name={`environments.${pos}.name`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input isDisabled={!isAllowed} {...field} placeholder="Name..." />
</FormControl>
)}
/>
)}
</OrgPermissionCan>
)}
</Td>
<Td>
{isInfisicalTemplate ? (
slug
) : (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Controller
control={control}
name={`environments.${pos}.slug`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input isDisabled={!isAllowed} {...field} placeholder="Slug..." />
</FormControl>
)}
/>
)}
</OrgPermissionCan>
)}
</Td>
{!isInfisicalTemplate && (
<Td className="flex items-center justify-end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<IconButton
className={`mr-3 py-2 ${
pos === environments.length - 1 ? "pointer-events-none opacity-50" : ""
}`}
onClick={() => move(pos, pos + 1)}
colorSchema="primary"
variant="plain"
ariaLabel="Increase position"
isDisabled={pos === environments.length - 1 || !isAllowed}
>
<FontAwesomeIcon icon={faArrowDown} />
</IconButton>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<IconButton
className={`mr-3 py-2 ${
pos === 0 ? "pointer-events-none opacity-50" : ""
}`}
onClick={() => move(pos, pos - 1)}
colorSchema="primary"
variant="plain"
ariaLabel="Decrease position"
isDisabled={pos === 0 || !isAllowed}
>
<FontAwesomeIcon icon={faArrowUp} />
</IconButton>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<IconButton
onClick={() => remove(pos)}
colorSchema="danger"
variant="plain"
ariaLabel="Remove environment"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
</Td>
)}
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</form>
);
};
@@ -0,0 +1,223 @@
import { faPlus, faTrash, faUnlock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
IconButton,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { isCustomProjectRole } from "@app/helpers/roles";
import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { ProjectTemplateEditRoleForm } from "./ProjectTemplateEditRoleForm";
type Props = {
projectTemplate: TProjectTemplate;
isInfisicalTemplate: boolean;
};
export const ProjectTemplateRolesSection = ({ projectTemplate, isInfisicalTemplate }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"removeRole",
"editRole"
] as const);
const { permission } = useOrgPermission();
const { roles } = projectTemplate;
const updateProjectTemplate = useUpdateProjectTemplate();
const handleRemoveRole = async (slug: string) => {
try {
await updateProjectTemplate.mutateAsync({
templateId: projectTemplate.id,
roles: projectTemplate.roles.filter(
(role) => role.slug !== slug && isCustomProjectRole(role.slug) // filter out default roles as well
)
});
createNotification({
text: "Successfully removed role from template",
type: "success"
});
handlePopUpClose("removeRole");
} catch (e) {
console.error(e);
createNotification({
text: "Error removing role from template",
type: "error"
});
}
};
const editRole = popUp?.editRole?.data as TProjectRole;
const roleToDelete = popUp?.removeRole?.data as TProjectRole;
return (
<div className="relative">
<AnimatePresence>
{popUp?.editRole.isOpen ? (
<motion.div
key="edit-role"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
className="absolute min-h-[10rem] w-full"
>
<ProjectTemplateEditRoleForm
onGoBack={() => handlePopUpClose("editRole")}
projectTemplate={projectTemplate}
role={editRole}
isDisabled={
permission.cannot(
OrgPermissionActions.Edit,
OrgPermissionSubjects.ProjectTemplates
) ||
(editRole && !isCustomProjectRole(editRole.slug))
}
/>
<div className="h-4 w-full" />
</motion.div>
) : (
<motion.div
key="role-list"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: 0 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
className="absolute w-full"
>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<div>
<h2 className="text-lg font-semibold">Project Roles</h2>
<p className="text-sm text-mineshaft-400">
{isInfisicalTemplate
? "Click a role to view the associated permissions"
: "Add, edit and remove roles for this project template"}
</p>
</div>
{!isInfisicalTemplate && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Button
onClick={() => {
handlePopUpOpen("editRole");
}}
colorSchema="primary"
className="ml-auto"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
Add Role
</Button>
)}
</OrgPermissionCan>
)}
</div>
<div className="py-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{roles.length ? (
roles.map((role) => {
return (
<Tr
key={role.slug}
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
handlePopUpOpen("editRole", role);
}
}}
onClick={() => handlePopUpOpen("editRole", role)}
>
<Td>{role.name}</Td>
<Td>{role.slug}</Td>
<Td>
{isCustomProjectRole(role.slug) && (
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
renderTooltip
allowedLabel="Remove Role"
>
{(isAllowed) => (
<IconButton
colorSchema="danger"
ariaLabel="delete-icon"
variant="plain"
className="group relative"
isDisabled={!isAllowed}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handlePopUpOpen("removeRole", role);
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
</div>
)}
</Td>
</Tr>
);
})
) : (
<Tr>
<Td colSpan={2}>
<EmptyState title="No roles assigned to template" icon={faUnlock} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
<DeleteActionModal
isOpen={popUp.removeRole.isOpen}
deleteKey="remove"
title={`Are you sure you want to remove the role ${roleToDelete?.slug}?`}
onChange={(isOpen) => handlePopUpToggle("removeRole", isOpen)}
onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)}
/>
</div>
<div className="h-4 w-full" />
</motion.div>
)}
</AnimatePresence>
</div>
);
};
@@ -0,0 +1 @@
export * from "./EditProjectTemplateSection";
@@ -0,0 +1,144 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
TextArea
} from "@app/components/v2";
import {
TProjectTemplate,
useCreateProjectTemplate,
useUpdateProjectTemplate
} from "@app/hooks/api/projectTemplates";
import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({
name: slugSchema({ min: 1, max: 64, field: "Name" }),
description: z.string().max(500).optional()
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onComplete?: (template: TProjectTemplate) => void;
projectTemplate?: TProjectTemplate;
};
type FormProps = {
projectTemplate?: TProjectTemplate;
onComplete: (template: TProjectTemplate) => void;
};
const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
const createProjectTemplate = useCreateProjectTemplate();
const updateProjectTemplate = useUpdateProjectTemplate();
const {
handleSubmit,
register,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: projectTemplate?.name,
description: projectTemplate?.description
}
});
const onFormSubmit = async (data: FormData) => {
const mutation = projectTemplate
? updateProjectTemplate.mutateAsync({ templateId: projectTemplate.id, ...data })
: createProjectTemplate.mutateAsync({ ...data });
try {
const template = await mutation;
createNotification({
text: `Successfully ${
projectTemplate ? "updated template details" : "created project template"
}`,
type: "success"
});
onComplete(template);
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${
projectTemplate ? "update template details" : "create project template"
}`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<FormControl
helperText="Name must be slug-friendly"
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input autoFocus placeholder="my-project-template" {...register("name")} />
</FormControl>
<FormControl
label="Description (optional)"
errorText={errors.description?.message}
isError={Boolean(errors.description?.message)}
>
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
{...register("description")}
/>
</FormControl>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{projectTemplate ? "Update" : "Add"} Template
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
export const ProjectTemplateDetailsModal = ({
isOpen,
onOpenChange,
projectTemplate,
onComplete
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title={projectTemplate ? "Edit Project Template Details" : "Create Project Template"}
>
<ProjectTemplateForm
projectTemplate={projectTemplate}
onComplete={(template) => {
if (onComplete) onComplete(template);
onOpenChange(false);
}}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,117 @@
import { useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { TProjectTemplate } from "@app/hooks/api/projectTemplates";
import { usePopUp } from "@app/hooks/usePopUp";
import { EditProjectTemplateSection } from "./EditProjectTemplateSection";
import { ProjectTemplateDetailsModal } from "./ProjectTemplateDetailsModal";
import { ProjectTemplatesTable } from "./ProjectTemplatesTable";
export const ProjectTemplatesSection = () => {
const { subscription } = useSubscription();
const [editTemplate, setEditTemplate] = useState<TProjectTemplate | null>(null);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan",
"addTemplate"
] as const);
return (
<div className="relative">
<AnimatePresence>
{editTemplate ? (
<motion.div
key="edit-project-template"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
className="absolute min-h-[10rem] w-full"
>
<EditProjectTemplateSection
template={editTemplate}
onBack={() => setEditTemplate(null)}
/>
</motion.div>
) : (
<motion.div
key="project-templates-list"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
className="absolute min-h-[10rem] w-full"
>
<div>
<p className="mb-6 font-inter text-bunker-300">
Create and configure templates with predefined roles and environments to streamline
project setup
</p>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-start">
<p className="text-xl font-semibold text-mineshaft-100">Project Templates</p>
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/project-templates"
>
<div className="ml-2 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!subscription?.projectTemplates) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("addTemplate");
}}
isDisabled={!isAllowed}
className="ml-auto"
>
Add Template
</Button>
)}
</OrgPermissionCan>
</div>
<ProjectTemplatesTable onEdit={setEditTemplate} />
<ProjectTemplateDetailsModal
onComplete={(template) => setEditTemplate(template)}
isOpen={popUp.addTemplate.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addTemplate", isOpen)}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can create project templates if you switch to Infisical's Enterprise plan."
/>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
@@ -0,0 +1,193 @@
import { useMemo, useState } from "react";
import {
faCircleInfo,
faClone,
faMagnifyingGlass,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
Input,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
import { DeleteProjectTemplateModal } from "./DeleteProjectTemplateModal";
type Props = {
onEdit: (projectTemplate: TProjectTemplate) => void;
};
export const ProjectTemplatesTable = ({ onEdit }: Props) => {
const { subscription } = useSubscription();
const { isPending, data: projectTemplates = [] } = useListProjectTemplates({
enabled: subscription?.projectTemplates
});
const [search, setSearch] = useState("");
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteTemplate"] as const);
const filteredTemplates = useMemo(
() =>
projectTemplates?.filter((template) =>
template.name.toLowerCase().includes(search.toLowerCase().trim())
) ?? [],
[search, projectTemplates]
);
const colSpan = 4;
return (
<div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search templates..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Roles</Th>
<Th>Environments</Th>
<Th />
</Tr>
</THead>
<TBody>
{subscription?.projectTemplates && isPending && (
<TableSkeleton
innerKey="project-templates-table"
columns={colSpan}
key="project-templates"
/>
)}
{filteredTemplates.map((template) => {
const { id, name, roles, environments = [], description } = template;
return (
<Tr
onClick={() => onEdit(template)}
className="cursor-pointer hover:bg-mineshaft-700"
key={id}
>
<Td>
{name}
{description && (
<Tooltip content={description}>
<FontAwesomeIcon
size="sm"
className="ml-2 text-mineshaft-400"
icon={faCircleInfo}
/>
</Tooltip>
)}
</Td>
<Td className="pl-8">
{roles.length}
{roles.length > 0 && (
<Tooltip
content={
<ul className="ml-2 list-disc">
{roles.map((role) => (
<li key={role.name}>{role.name}</li>
))}
</ul>
}
>
<FontAwesomeIcon
size="sm"
className="ml-2 text-mineshaft-400"
icon={faCircleInfo}
/>
</Tooltip>
)}
</Td>
<Td className="pl-14">
{environments?.length || 0}
{environments?.length && (
<Tooltip
content={
<ul className="ml-2 list-disc">
{environments
?.sort((a, b) => (a.position > b.position ? 1 : -1))
.map((env) => <li key={env.slug}>{env.name}</li>)}
</ul>
}
>
<FontAwesomeIcon
size="sm"
className="ml-2 text-mineshaft-400"
icon={faCircleInfo}
/>
</Tooltip>
)}
</Td>
<Td className="w-5">
{name !== "default" && (
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<IconButton
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteTemplate", template);
}}
variant="plain"
colorSchema="danger"
ariaLabel="Delete template"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
)}
</Td>
</Tr>
);
})}
{(!subscription?.projectTemplates ||
(!isPending && filteredTemplates?.length === 0)) && (
<Tr>
<Td colSpan={colSpan}>
<EmptyState
title={
search.trim()
? "No project templates match search"
: "No project templates found"
}
icon={faClone}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<DeleteProjectTemplateModal
isOpen={popUp.deleteTemplate.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteTemplate", isOpen)}
template={popUp.deleteTemplate.data}
/>
</div>
);
};
@@ -0,0 +1 @@
export { ProjectTemplatesSection } from "./ProjectTemplatesSection";
@@ -0,0 +1 @@
export { ProjectTemplatesTab } from "./ProjectTemplatesTab";
@@ -1,6 +1,4 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -20,11 +18,6 @@ export const Route = createFileRoute(
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Settings"
}
@@ -1,5 +0,0 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProductOverviewPage } from "../SecretManagerOverviewPage/SecretManagerOverviewPage";
export const SshOverviewPage = () => <ProductOverviewPage type={ProjectType.SSH} />;
@@ -1,23 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SshOverviewPage } from "./SshOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview"
)({
component: SshOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "SSH",
link: linkOptions({ to: "/organization/ssh/overview" })
}
]
})
});
@@ -1,20 +0,0 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const SshSettingsPage = () => {
return (
<>
<Helmet>
<title>SSH Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="SSH Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};
@@ -1,26 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SshSettingsPage } from "./SshSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings"
)({
component: SshSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "SSH",
link: linkOptions({ to: "/organization/ssh/overview" })
},
{
label: "Settings"
}
]
})
});
@@ -1,6 +1,4 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -20,11 +18,6 @@ export const Route = createFileRoute(
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Single Sign-On (SSO)"
}
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { TWorkspaceUser } from "@app/hooks/api/types";
@@ -43,7 +43,7 @@ export const UserProjectRow = ({
onClick={() => {
if (isAccessible) {
navigate({
to: `/${project.type}/$projectId/access-management` as const,
to: "/projects/$projectId/access-management",
params: {
projectId: project.id
},
@@ -61,9 +61,6 @@ export const UserProjectRow = ({
}}
>
<Td className="max-w-0 truncate">{project.name}</Td>
<Td>
<Tag size="xs">{project.type}</Tag>
</Td>
<Td>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
roles.length > 1 ? ` (+${roles.length - 1})` : ""
}`}</Td>
@@ -123,7 +123,6 @@ export const UserProjectsTable = ({ membershipId, handlePopUpOpen }: Props) => {
</IconButton>
</div>
</Th>
<Th>Type</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
@@ -1,5 +1,3 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { UserDetailsByIDPage } from "./UserDetailsByIDPage";
@@ -10,11 +8,6 @@ export const Route = createFileRoute(
component: UserDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
@@ -4,7 +4,6 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectAccessControlTabs } from "@app/types/project";
import {
@@ -25,7 +24,7 @@ const Page = () => {
const updateSelectedTab = (tab: string) => {
navigate({
to: `/${currentWorkspace.type}/$projectId/access-management` as const,
to: "/projects/$projectId/access-management",
search: (prev) => ({ ...prev, selectedTab: tab }),
params: {
projectId: currentWorkspace.id
@@ -49,9 +48,7 @@ const Page = () => {
<p>Machine Identities</p>
</div>
</Tab>
{currentWorkspace?.type === ProjectType.SecretManager && (
<Tab value={ProjectAccessControlTabs.ServiceTokens}>Service Tokens</Tab>
)}
<Tab value={ProjectAccessControlTabs.ServiceTokens}>Service Tokens</Tab>
<Tab value={ProjectAccessControlTabs.Roles}>Project Roles</Tab>
</TabList>
<TabPanel value={ProjectAccessControlTabs.Member}>
@@ -63,11 +60,9 @@ const Page = () => {
<TabPanel value={ProjectAccessControlTabs.Identities}>
<IdentityTab />
</TabPanel>
{currentWorkspace?.type === ProjectType.SecretManager && (
<TabPanel value={ProjectAccessControlTabs.ServiceTokens}>
<ServiceTokenTab />
</TabPanel>
)}
<TabPanel value={ProjectAccessControlTabs.ServiceTokens}>
<ServiceTokenTab />
</TabPanel>
<TabPanel value={ProjectAccessControlTabs.Roles}>
<ProjectRoleListTab />
</TabPanel>
@@ -158,7 +158,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
onKeyDown={(evt) => {
if (evt.key === "Enter") {
navigate({
to: `/${currentWorkspace.type}/$projectId/groups/$groupId` as const,
to: "/projects/$projectId/groups/$groupId",
params: {
projectId: currentWorkspace.id,
groupId: id
@@ -168,7 +168,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
}}
onClick={() =>
navigate({
to: `/${currentWorkspace.type}/$projectId/groups/$groupId` as const,
to: "/projects/$projectId/groups/$groupId",
params: {
projectId: currentWorkspace.id,
groupId: id

Some files were not shown because too many files have changed in this diff Show More