mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-08 01:09:51 +00:00
Paywall resource policies
This commit is contained in:
@@ -24,7 +24,8 @@ export enum TierFeature {
|
||||
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
|
||||
StandaloneHealthChecks = "standaloneHealthChecks",
|
||||
AlertingRules = "alertingRules",
|
||||
WildcardSubdomain = "wildcardSubdomain"
|
||||
WildcardSubdomain = "wildcardSubdomain",
|
||||
ResourcePolicies = "resourcePolicies"
|
||||
}
|
||||
|
||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
@@ -66,5 +67,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
||||
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
||||
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"]
|
||||
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"]
|
||||
};
|
||||
|
||||
@@ -389,7 +389,7 @@ authenticated.delete(
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
verifyResourcePolicyAccess,
|
||||
verifyValidLicense,
|
||||
// verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ?
|
||||
verifyValidSubscription(tierMatrix.resourcePolicies),
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.deleteResourcePolicy),
|
||||
logActionAudit(ActionsEnum.deleteResourcePolicy),
|
||||
@@ -399,7 +399,7 @@ authenticated.delete(
|
||||
authenticated.get(
|
||||
"/org/:orgId/resource-policies",
|
||||
verifyValidLicense,
|
||||
// verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ?
|
||||
verifyValidSubscription(tierMatrix.resourcePolicies),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.listResourcePolicies),
|
||||
@@ -410,7 +410,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/org/:orgId/resource-policy",
|
||||
verifyValidLicense,
|
||||
// verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ?
|
||||
verifyValidSubscription(tierMatrix.resourcePolicies),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createResourcePolicy),
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import {
|
||||
db,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
|
||||
@@ -25,7 +25,10 @@ import {
|
||||
import { registry } from "@server/openApi";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
|
||||
import {
|
||||
validateAndConstructDomain,
|
||||
checkWildcardDomainConflict
|
||||
} from "@server/lib/domainUtils";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -304,11 +307,30 @@ async function updateHttpResource(
|
||||
|
||||
const updateData = parsedBody.data;
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(
|
||||
resource.orgId,
|
||||
tierMatrix.wildcardSubdomain
|
||||
);
|
||||
|
||||
if (updateData.resourcePolicyId != null) {
|
||||
if (!isLicensed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Resource policies are not supported on your current plan. Please upgrade to access this feature."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [existingPolicy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, updateData.resourcePolicyId))
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
updateData.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existingPolicy) {
|
||||
@@ -346,10 +368,6 @@ async function updateHttpResource(
|
||||
|
||||
// Wildcard subdomains are a paid feature
|
||||
if (updateData.subdomain && updateData.subdomain.includes("*")) {
|
||||
const isLicensed = await isLicensedOrSubscribed(
|
||||
resource.orgId,
|
||||
tierMatrix.wildcardSubdomain
|
||||
);
|
||||
if (!isLicensed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -494,10 +512,6 @@ async function updateHttpResource(
|
||||
headers = null;
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(
|
||||
resource.orgId,
|
||||
tierMatrix.maintencePage
|
||||
);
|
||||
if (!isLicensed) {
|
||||
updateData.maintenanceModeEnabled = undefined;
|
||||
updateData.maintenanceModeType = undefined;
|
||||
@@ -560,7 +574,12 @@ async function updateRawResource(
|
||||
const [existingPolicy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, updateData.resourcePolicyId))
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
updateData.resourcePolicyId
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existingPolicy) {
|
||||
|
||||
@@ -41,7 +41,7 @@ import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowRightIcon, CheckIcon, ShieldAlertIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -70,6 +70,7 @@ export default function ResourceAuthenticationPage() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { env } = useEnvContext();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const api = createApiClient({ env });
|
||||
const router = useRouter();
|
||||
@@ -188,111 +189,118 @@ export default function ResourceAuthenticationPage() {
|
||||
return (
|
||||
<>
|
||||
<SettingsContainer>
|
||||
{build !== "oss" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourcePolicySelectTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourcePolicySelectDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<StrategySelect
|
||||
options={resourcePolicyTypes}
|
||||
value={selectedResourceType}
|
||||
onChange={(value) => {
|
||||
form.setValue("type", value);
|
||||
}}
|
||||
cols={2}
|
||||
/>
|
||||
{selectedResourceType === "shared" && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={
|
||||
"w-full md:w-1/2 justify-between"
|
||||
}
|
||||
>
|
||||
<span className="truncate max-w-37.5">
|
||||
{selectedPolicy
|
||||
? selectedPolicy.name
|
||||
: t("resourcePolicySelect")}
|
||||
</span>
|
||||
<CaretSortIcon className="ml-2h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-45">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={t("siteSearch")}
|
||||
value={
|
||||
resourcePolicysearchQuery
|
||||
{build !== "oss" &&
|
||||
isPaidUser(tierMatrix[TierFeature.ResourcePolicies]) && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourcePolicySelectTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourcePolicySelectDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<StrategySelect
|
||||
options={resourcePolicyTypes}
|
||||
value={selectedResourceType}
|
||||
onChange={(value) => {
|
||||
form.setValue("type", value);
|
||||
}}
|
||||
cols={2}
|
||||
/>
|
||||
{selectedResourceType === "shared" && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={
|
||||
"w-full md:w-1/2 justify-between"
|
||||
}
|
||||
onValueChange={
|
||||
setResourcePolicySearchQuery
|
||||
}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t(
|
||||
"resourcePolicyNotFound"
|
||||
>
|
||||
<span className="truncate max-w-37.5">
|
||||
{selectedPolicy
|
||||
? selectedPolicy.name
|
||||
: t(
|
||||
"resourcePolicySelect"
|
||||
)}
|
||||
</span>
|
||||
<CaretSortIcon className="ml-2h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-45">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={t(
|
||||
"siteSearch"
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{policiesList.map(
|
||||
(policy) => (
|
||||
<CommandItem
|
||||
key={
|
||||
policy.resourcePolicyId
|
||||
}
|
||||
value={policy.resourcePolicyId.toString()}
|
||||
onSelect={() =>
|
||||
setSelectedPolicy(
|
||||
{
|
||||
id: policy.resourcePolicyId,
|
||||
name: policy.name
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
policy.resourcePolicyId ===
|
||||
selectedPolicy?.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{policy.name}
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter className="justify-start">
|
||||
<Button
|
||||
onClick={() =>
|
||||
startTransition(
|
||||
handleSaveResourcePolicyType
|
||||
)
|
||||
}
|
||||
loading={isUpdatingResource}
|
||||
>
|
||||
{t("resourcePolicyTypeSave")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
)}
|
||||
value={
|
||||
resourcePolicysearchQuery
|
||||
}
|
||||
onValueChange={
|
||||
setResourcePolicySearchQuery
|
||||
}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t(
|
||||
"resourcePolicyNotFound"
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{policiesList.map(
|
||||
(policy) => (
|
||||
<CommandItem
|
||||
key={
|
||||
policy.resourcePolicyId
|
||||
}
|
||||
value={policy.resourcePolicyId.toString()}
|
||||
onSelect={() =>
|
||||
setSelectedPolicy(
|
||||
{
|
||||
id: policy.resourcePolicyId,
|
||||
name: policy.name
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
policy.resourcePolicyId ===
|
||||
selectedPolicy?.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{
|
||||
policy.name
|
||||
}
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter className="justify-start">
|
||||
<Button
|
||||
onClick={() =>
|
||||
startTransition(
|
||||
handleSaveResourcePolicyType
|
||||
)
|
||||
}
|
||||
loading={isUpdatingResource}
|
||||
>
|
||||
{t("resourcePolicyTypeSave")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{selectedResourceType === "inline" ? (
|
||||
<ResourcePolicyProvider policy={policies.defaultPolicy}>
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from "./ui/dropdown-menu";
|
||||
import ConfirmDeleteDialog from "./ConfirmDeleteDialog";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number];
|
||||
|
||||
@@ -253,6 +255,9 @@ export function ResourcePoliciesTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix[TierFeature.ResourcePolicies]}
|
||||
/>
|
||||
{selectedResourcePolicy && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
|
||||
@@ -9,29 +9,22 @@ import {
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { orgQueries } from "@app/lib/queries";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import z from "zod";
|
||||
|
||||
import { type PolicyFormValues, createPolicySchema } from ".";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { orgs, type ResourcePolicy } from "@server/db";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
@@ -42,13 +35,14 @@ import {
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
|
||||
import { useMemo, useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { CreatePolicyUsersRolesSectionForm } from "./CreatePolicyUserRolesSectionForm";
|
||||
import { CreatePolicyAuthMethodsSectionForm } from "./CreatePolicyAuthMethodsSectionForm";
|
||||
import { CreatePolicyOtpEmailSectionForm } from "./CreatePolicyOtpEmailSectionForm";
|
||||
import { CreatePolicyRulesSectionForm } from "./CreatePolicyRulesSectionForm";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
// ─── CreatePolicyForm ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -200,71 +194,87 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<SettingsContainer>
|
||||
{/* Name */}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourcePolicyName")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourcePolicyNameDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"resourcePolicyNamePlaceholder"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
const policyTiers = tierMatrix[TierFeature.ResourcePolicies];
|
||||
const isDisabled = !isPaidUser(policyTiers);
|
||||
|
||||
<CreatePolicyUsersRolesSectionForm
|
||||
form={form}
|
||||
allRoles={allRoles}
|
||||
allUsers={allUsers}
|
||||
allIdps={allIdps}
|
||||
/>
|
||||
<CreatePolicyAuthMethodsSectionForm form={form} />
|
||||
<CreatePolicyOtpEmailSectionForm
|
||||
form={form}
|
||||
emailEnabled={env.email.emailEnabled}
|
||||
/>
|
||||
<CreatePolicyRulesSectionForm
|
||||
form={form}
|
||||
isMaxmindAvailable={isMaxmindAvailable}
|
||||
isMaxmindAsnAvailable={isMaxmindAsnAvailable}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
return (
|
||||
<>
|
||||
<PaidFeaturesAlert tiers={policyTiers} />
|
||||
<Form {...form}>
|
||||
<div
|
||||
className={
|
||||
isDisabled
|
||||
? "pointer-events-none opacity-50"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<SettingsContainer>
|
||||
{/* Name */}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourcePolicyName")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourcePolicyNameDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"resourcePolicyNamePlaceholder"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<CreatePolicyUsersRolesSectionForm
|
||||
form={form}
|
||||
allRoles={allRoles}
|
||||
allUsers={allUsers}
|
||||
allIdps={allIdps}
|
||||
/>
|
||||
<CreatePolicyAuthMethodsSectionForm form={form} />
|
||||
<CreatePolicyOtpEmailSectionForm
|
||||
form={form}
|
||||
emailEnabled={env.email.emailEnabled}
|
||||
/>
|
||||
<CreatePolicyRulesSectionForm
|
||||
form={form}
|
||||
isMaxmindAvailable={isMaxmindAvailable}
|
||||
isMaxmindAsnAvailable={isMaxmindAsnAvailable}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</div>
|
||||
|
||||
<div className="flex py-6 justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => startTransition(onSubmit)}
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
disabled={isSubmitting || isDisabled}
|
||||
>
|
||||
{t("resourcePoliciesCreate")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user