Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
a9d2a164b3 Bump golang.org/x/term
Bumps the minor-updates group with 1 update in the /install directory: [golang.org/x/term](https://github.com/golang/term).


Updates `golang.org/x/term` from 0.43.0 to 0.44.0
- [Commits](https://github.com/golang/term/compare/v0.43.0...v0.44.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-11 01:33:20 +00:00
20 changed files with 99 additions and 199 deletions

View File

@@ -5,7 +5,7 @@ go 1.25.0
require (
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/term v0.43.0
golang.org/x/term v0.44.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -33,6 +33,6 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.23.0 // indirect
)

View File

@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@@ -214,7 +214,6 @@
"resourceErrorDelte": "Error deleting resource",
"resourcePoliciesBannerTitle": "Re-use Authentication and Access Rules",
"resourcePoliciesBannerDescription": "Shared resource policies let you define authentication methods and access rules once, then attach them to multiple public resources. When you update a policy, every linked resource inherits the change automatically.",
"resourcePoliciesBannerButtonText": "Learn More",
"resourcePoliciesTitle": "Manage Public Resource Policies",
"resourcePoliciesAttachedResourcesColumnTitle": "Resources",
"resourcePoliciesAttachedResources": "{count} resource(s)",

View File

@@ -327,6 +327,27 @@ export async function listSites(
);
}
let accessibleSites;
if (req.user) {
accessibleSites = await db
.select({
siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`
})
.from(userSites)
.fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId))
.where(
or(
eq(userSites.userId, req.user!.userId),
inArray(roleSites.roleId, req.userOrgRoleIds!)
)
);
} else {
accessibleSites = await db
.select({ siteId: sites.siteId })
.from(sites)
.where(eq(sites.orgId, orgId));
}
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
orgId,
tierMatrix.labels
@@ -343,38 +364,14 @@ export async function listSites(
labels: labelFilter
} = parsedQuery.data;
const conditions = [eq(sites.orgId, orgId)];
const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
if (req.user) {
const userAccessConditions = [
inArray(
sites.siteId,
db
.select({ siteId: userSites.siteId })
.from(userSites)
.where(eq(userSites.userId, req.user.userId))
)
];
const roleIds = req.userOrgRoleIds ?? [];
if (roleIds.length > 0) {
userAccessConditions.push(
inArray(
sites.siteId,
db
.select({ siteId: roleSites.siteId })
.from(roleSites)
.where(inArray(roleSites.roleId, roleIds))
)
);
}
conditions.push(
userAccessConditions.length === 1
? userAccessConditions[0]
: or(...userAccessConditions)!
);
}
const conditions = [
and(
inArray(sites.siteId, accessibleSiteIds),
eq(sites.orgId, orgId)
)
];
if (typeof online !== "undefined") {
conditions.push(eq(sites.online, online));
@@ -421,15 +418,17 @@ export async function listSites(
)
);
}
conditions.push(or(...queryList)!);
conditions.push(or(...queryList));
}
const baseQuery = querySitesBase().where(and(...conditions));
const countQuery = db
.select({ count: sql<number>`count(*)` })
.from(sites)
.where(and(...conditions));
// we need to add `as` so that drizzle filters the result as a subquery
const countQuery = db.$count(
querySitesBase()
.where(and(...conditions))
.as("filtered_sites")
);
const siteListQuery = baseQuery
.limit(pageSize)
@@ -442,13 +441,11 @@ export async function listSites(
: asc(sites.name)
);
const [countRows, rows] = await Promise.all([
const [totalCount, rows] = await Promise.all([
countQuery,
siteListQuery
]);
const totalCount = Number(countRows[0]?.count ?? 0);
// Get latest version asynchronously without blocking the response
const latestNewtVersionPromise = getLatestNewtVersion();

View File

@@ -282,7 +282,7 @@ function GeneralSectionForm({ org }: SectionFormProps) {
<FormDescription>
{t("newtAutoUpdateDescription")}{" "}
<a
href="https://docs.pangolin.net/manage/sites/auto-update"
href="https://docs.pangolin.net/manage/sites/configure-site#auto-update"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"

View File

@@ -229,7 +229,7 @@ function RdpServerForm({
sitesField="selectedSites"
destinationField="destination"
destinationPortField="destinationPort"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp#site-and-host-configuration"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp"
defaultPort={3389}
/>
</SettingsSectionForm>

View File

@@ -467,7 +467,7 @@ function SshServerForm({
<p className="text-sm text-muted-foreground">
{t("sshDaemonDisclaimer")}{" "}
<a
href="https://docs.pangolin.net/manage/ssh"
href="https://docs.pangolin.net/manage/resources/public/ssh"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"
@@ -589,7 +589,7 @@ function SshServerForm({
sitesField="selectedSites"
destinationField="destination"
destinationPortField="destinationPort"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh#site-and-host-configuration"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
defaultPort={22}
/>
</SettingsFormCell>
@@ -602,7 +602,7 @@ function SshServerForm({
siteField="selectedSite"
destinationField="destination"
destinationPortField="destinationPort"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh#site-and-host-configuration"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
defaultPort={22}
/>
</SettingsFormCell>

View File

@@ -80,6 +80,7 @@ import { toASCII } from "punycode";
import {
useMemo,
useState,
useTransition,
useEffect
} from "react";
import { useForm, type Resolver } from "react-hook-form";
@@ -228,7 +229,7 @@ export default function Page() {
>([]);
const [loadingExitNodes, setLoadingExitNodes] = useState(build === "saas");
const [createLoading, setCreateLoading] = useState(false);
const [createLoading, startTransition] = useTransition();
const [showSnippets, setShowSnippets] = useState(false);
const [niceId, setNiceId] = useState<string>("");
@@ -460,7 +461,6 @@ export default function Page() {
};
async function onSubmit() {
setCreateLoading(true);
const baseData = baseForm.getValues();
try {
@@ -707,8 +707,6 @@ export default function Page() {
t("resourceErrorCreateMessageDescription")
)
});
} finally {
setCreateLoading(false);
}
}
@@ -764,7 +762,7 @@ export default function Page() {
ssh: "SSH",
rdp: "RDP",
vnc: "VNC",
};
}
}
const typeOptions: OptionSelectOption<NewResourceType>[] =
@@ -1099,7 +1097,7 @@ export default function Page() {
"sshDaemonDisclaimer"
)}{" "}
<a
href="https://docs.pangolin.net/manage/ssh"
href="https://docs.pangolin.net/manage/resources/public/ssh"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"
@@ -1237,7 +1235,7 @@ export default function Page() {
sitesField="selectedSites"
destinationField="destination"
destinationPortField="destinationPort"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh#site-and-host-configuration"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
defaultPort={22}
/>
</Form>
@@ -1258,7 +1256,7 @@ export default function Page() {
siteField="selectedSite"
destinationField="destination"
destinationPortField="destinationPort"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh#site-and-host-configuration"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
defaultPort={22}
/>
</Form>
@@ -1308,7 +1306,7 @@ export default function Page() {
sitesField="selectedSites"
destinationField="destination"
destinationPortField="destinationPort"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp#site-and-host-configuration"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp"
defaultPort={3389}
/>
</Form>
@@ -1355,7 +1353,7 @@ export default function Page() {
sitesField="selectedSites"
destinationField="destination"
destinationPortField="destinationPort"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/vnc#site-and-host-configuration"
learnMoreHref="https://docs.pangolin.net/manage/resources/public/vnc"
defaultPort={5900}
/>
</Form>
@@ -1429,7 +1427,7 @@ export default function Page() {
}
}}
loading={createLoading}
disabled={!areAllTargetsValid() || browserGatewayDisabled || createLoading}
disabled={!areAllTargetsValid() || browserGatewayDisabled}
>
{t("resourceCreate")}
</Button>

View File

@@ -317,7 +317,7 @@ export default function GeneralPage() {
"siteAutoUpdateDescription"
)}{" "}
<a
href="https://docs.pangolin.net/manage/sites/auto-update"
href="https://docs.pangolin.net/manage/sites/configure-site#auto-update"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"

View File

@@ -596,7 +596,7 @@ export default function SshClient({
<p className="text-sm text-muted-foreground">
{t("sshPrivateKeyDisclaimer")}{" "}
<Link
href="https://docs.pangolin.net/manage/ssh#authentication-method"
href="https://docs.pangolin.net/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"

View File

@@ -124,21 +124,21 @@ export default function VncClient({
authToken: target.authToken
});
// try {
// const checkParams = new URLSearchParams(params);
// checkParams.set("checkOnly", "1");
// const response = await fetch(`${base}?${checkParams.toString()}`);
// if (!response.ok) {
// const detail = (await response.text()).trim();
// setConnectError(detail || t("sshErrorConnectionClosed"));
// setConnecting(false);
// return;
// }
// } catch {
// setConnectError(t("sshErrorWebSocket"));
// setConnecting(false);
// return;
// }
try {
const checkParams = new URLSearchParams(params);
checkParams.set("checkOnly", "1");
const response = await fetch(`${base}?${checkParams.toString()}`);
if (!response.ok) {
const detail = (await response.text()).trim();
setConnectError(detail || t("sshErrorConnectionClosed"));
setConnecting(false);
return;
}
} catch {
setConnectError(t("sshErrorWebSocket"));
setConnecting(false);
return;
}
let RFB: new (
target: HTMLElement,

View File

@@ -222,7 +222,7 @@ export function BrowserGatewayTargetForm<T extends FieldValues>(
<a
href={
props.learnMoreHref ??
"https://docs.pangolin.net/manage/resources/private/multi-site-routing"
"https://docs.pangolin.net/manage/resources/public/ssh"
}
target="_blank"
rel="noopener noreferrer"

View File

@@ -1,10 +1,8 @@
"use client";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
import type { AxiosResponse } from "axios";
import { useTranslations } from "next-intl";
@@ -20,7 +18,6 @@ import {
CredenzaTitle
} from "./Credenza";
import { OrgLabelForm } from "./OrgLabelForm";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { Button } from "./ui/button";
export type CreateOrgLabelDialogProps = {
@@ -38,8 +35,6 @@ export function CreateOrgLabelDialog({
}: CreateOrgLabelDialogProps) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
const { isPaidUser } = usePaidStatus();
const canManageLabels = isPaidUser(tierMatrix.labels);
const [isSubmitting, startTransition] = useTransition();
async function createOrgLabel(data: { name: string; color: string }) {
@@ -84,11 +79,8 @@ export function CreateOrgLabelDialog({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<PaidFeaturesAlert tiers={tierMatrix.labels} />
<OrgLabelForm
disabled={!canManageLabels}
onSubmit={(data) => {
if (!canManageLabels) return;
startTransition(async () => createOrgLabel(data));
}}
/>
@@ -106,7 +98,7 @@ export function CreateOrgLabelDialog({
<Button
type="submit"
form="org-label-form"
disabled={isSubmitting || !canManageLabels}
disabled={isSubmitting}
loading={isSubmitting}
>
{t("labelCreate")}

View File

@@ -1,10 +1,8 @@
"use client";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
import type { AxiosResponse } from "axios";
import { useTranslations } from "next-intl";
@@ -20,7 +18,6 @@ import {
CredenzaTitle
} from "./Credenza";
import { OrgLabelForm } from "./OrgLabelForm";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { Button } from "./ui/button";
export type EditOrgLabelDialogProps = {
@@ -44,8 +41,6 @@ export function EditOrgLabelDialog({
}: EditOrgLabelDialogProps) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
const { isPaidUser } = usePaidStatus();
const canManageLabels = isPaidUser(tierMatrix.labels);
const [isSubmitting, startTransition] = useTransition();
async function editOrgLabel(data: { name: string; color: string }) {
@@ -90,12 +85,9 @@ export function EditOrgLabelDialog({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<PaidFeaturesAlert tiers={tierMatrix.labels} />
<OrgLabelForm
disabled={!canManageLabels}
defaultValue={label}
onSubmit={(data) => {
if (!canManageLabels) return;
startTransition(async () => editOrgLabel(data));
}}
/>
@@ -113,7 +105,7 @@ export function EditOrgLabelDialog({
<Button
type="submit"
form="org-label-form"
disabled={isSubmitting || !canManageLabels}
disabled={isSubmitting}
loading={isSubmitting}
>
{t("labelEdit")}

View File

@@ -35,14 +35,9 @@ export type LabelFormData = z.infer<typeof labelFormSchema>;
export type OrgLabelFormProps = {
onSubmit: (data: LabelFormData) => void;
defaultValue?: LabelFormData;
disabled?: boolean;
};
export function OrgLabelForm({
onSubmit,
defaultValue,
disabled = false
}: OrgLabelFormProps) {
export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
const t = useTranslations();
const colorValues = Object.values(LABEL_COLORS);
@@ -75,7 +70,9 @@ export function OrgLabelForm({
<FormItem>
<FormLabel>{t("labelNameField")}</FormLabel>
<FormControl>
<Input {...field} disabled={disabled} />
<Input
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -91,7 +88,6 @@ export function OrgLabelForm({
<Select
onValueChange={field.onChange}
value={field.value}
disabled={disabled}
>
<SelectTrigger className="w-full">
<SelectValue
@@ -114,9 +110,7 @@ export function OrgLabelForm({
}}
/>
<span data-name>
{color
.charAt(0)
.toUpperCase() +
{color.charAt(0).toUpperCase() +
color.slice(1)}
</span>
</SelectItem>

View File

@@ -2079,7 +2079,7 @@ export function PrivateResourceForm({
<p className="text-sm text-muted-foreground">
{t("sshDaemonDisclaimer")}{" "}
<a
href="https://docs.pangolin.net/manage/ssh"
href="https://docs.pangolin.net/manage/resources/private/ssh"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"

View File

@@ -767,7 +767,7 @@ function TargetStatusCell({
if (!targets || targets.length === 0) {
return (
<div className="flex items-center gap-2 px-0">
<div className="flex items-center gap-2 px-2">
<StatusIcon status="unknown" />
<span className="text-sm">{t("resourcesTableNoTargets")}</span>
</div>

View File

@@ -1,9 +1,7 @@
"use client";
import { Button } from "@app/components/ui/button";
import { Shield, ArrowRight } from "lucide-react";
import { Shield } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import DismissableBanner from "./DismissableBanner";
export const ResourcePoliciesBanner = () => {
@@ -16,22 +14,7 @@ export const ResourcePoliciesBanner = () => {
title={t("resourcePoliciesBannerTitle")}
titleIcon={<Shield className="w-5 h-5 text-primary" />}
description={t("resourcePoliciesBannerDescription")}
>
<Link
href="https://docs.pangolin.net/manage/resources/public/resource-policies"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant="outline"
size="sm"
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
>
{t("resourcePoliciesBannerButtonText")}
<ArrowRight className="w-4 h-4" />
</Button>
</Link>
</DismissableBanner>
/>
);
};

View File

@@ -354,7 +354,7 @@ WantedBy=default.target`
{t.rich("siteInstallAdvantechDocsDescription", {
docsLink: (chunks) => (
<a
href="https://docs.pangolin.net/manage/sites/install-site"
href="https://docs.pangolin.net/manage/sites/install-advantech"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"

View File

@@ -8,14 +8,13 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { orgQueries } from "@app/lib/queries";
import { build } from "@server/build";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { EditPolicyNameSectionForm } from "./EditPolicyNameSectionForm";
import { PolicyAuthStackSection } from "./PolicyAuthStackSection";
import { PolicyAccessRulesSection } from "./PolicyAccessRulesSection";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
export type EditPolicyFormSection = "general" | "authentication" | "rules";
@@ -72,17 +71,13 @@ export function EditPolicyForm({
return <></>;
}
const policyTiers = tierMatrix[TierFeature.ResourcePolicies];
const isDisabled = !isPaidUser(policyTiers);
const effectiveReadonly = readonly || isDisabled;
const authSection = (
<PolicyAuthStackSection
mode="edit"
orgId={org.org.orgId}
allIdps={allIdps}
emailEnabled={env.email.emailEnabled}
readonly={effectiveReadonly}
readonly={readonly}
resourceId={resourceId}
/>
);
@@ -92,82 +87,32 @@ export function EditPolicyForm({
mode="edit"
isMaxmindAvailable={isMaxmindAvailable}
isMaxmindAsnAvailable={isMaxmindASNAvailable}
readonly={effectiveReadonly}
readonly={readonly}
resourceId={resourceId}
/>
);
if (section === "general") {
return (
<>
<PaidFeaturesAlert tiers={policyTiers} />
<div
className={
isDisabled
? "pointer-events-none opacity-50"
: undefined
}
>
<EditPolicyNameSectionForm readonly={effectiveReadonly} />
</div>
</>
);
return <EditPolicyNameSectionForm readonly={readonly} />;
}
if (section === "authentication") {
return (
<>
<PaidFeaturesAlert tiers={policyTiers} />
<div
className={
isDisabled
? "pointer-events-none opacity-50"
: undefined
}
>
{authSection}
</div>
</>
);
return authSection;
}
if (section === "rules") {
return (
<>
<PaidFeaturesAlert tiers={policyTiers} />
<div
className={
isDisabled
? "pointer-events-none opacity-50"
: undefined
}
>
{rulesSection}
</div>
</>
);
return rulesSection;
}
return (
<>
<PaidFeaturesAlert tiers={policyTiers} />
<div
className={
isDisabled ? "pointer-events-none opacity-50" : undefined
}
>
<SettingsContainer>
{!hidePolicyNameForm && !isOverlay && (
<EditPolicyNameSectionForm
readonly={effectiveReadonly}
/>
)}
<SettingsContainer>
{!hidePolicyNameForm && !isOverlay && (
<EditPolicyNameSectionForm readonly={readonly} />
)}
{authSection}
{authSection}
{rulesSection}
</SettingsContainer>
</div>
</>
{rulesSection}
</SettingsContainer>
);
}