Merge branch 'dev' into alerting-rules

This commit is contained in:
Owen
2026-04-16 15:53:32 -07:00
11 changed files with 326 additions and 57 deletions

View File

@@ -477,7 +477,7 @@ export default function BillingPage() {
};
const handleContactUs = () => {
window.open("https://pangolin.net/talk-to-us", "_blank");
window.open("https://pangolin.net/contact", "_blank");
};
// Get current plan ID from tier
@@ -558,6 +558,14 @@ export default function BillingPage() {
// Get button label and action for each plan
const getPlanAction = (plan: PlanOption) => {
if (plan.id === "enterprise") {
if (plan.id === currentPlanId) {
return {
label: "Manage Current Plan",
action: handleModifySubscription,
variant: "default" as const,
disabled: false
};
}
return {
label: "Contact Us",
action: handleContactUs,

View File

@@ -161,16 +161,13 @@ export default function Page() {
description: t("siteNewtTunnelDescription"),
disabled: true
},
...(env.flags.disableBasicWireguardSites
...(env.flags.disableBasicWireguardSites || build == "saas"
? []
: [
{
id: "wireguard" as SiteType,
title: t("siteWg"),
description:
build == "saas"
? t("siteWgDescriptionSaas")
: t("siteWgDescription"),
description: t("siteWgDescription"),
disabled: true
}
]),
@@ -426,9 +423,22 @@ export default function Page() {
}));
setRemoteExitNodeOptions(exitNodeOptions);
if (exitNodeOptions.length === 0) {
// No remote exit nodes available — remove local option and default to newt
setTunnelTypes((prev: any) =>
prev.filter((item: any) => item.id !== "local")
);
form.setValue("method", "newt");
}
}
} catch (error) {
console.error("Failed to fetch remote exit nodes:", error);
// If fetch fails, no remote exit nodes available — remove local option and default to newt
setTunnelTypes((prev: any) =>
prev.filter((item: any) => item.id !== "local")
);
form.setValue("method", "newt");
}
}

View File

@@ -66,18 +66,23 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
© {new Date().getFullYear()} Fossorial, Inc.
</span>
</a>
<Separator orientation="vertical" />
<a
href="https://pangolin.net"
target="_blank"
rel="noopener noreferrer"
aria-label="Built by Fossorial"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>
{process.env.BRANDING_APP_NAME || "Pangolin"}
</span>
</a>
{build !== "saas" && (
<>
<Separator orientation="vertical" />
<a
href="https://pangolin.net"
target="_blank"
rel="noopener noreferrer"
aria-label="Built by Fossorial"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>
{process.env.BRANDING_APP_NAME ||
"Pangolin"}
</span>
</a>
</>
)}
<Separator orientation="vertical" />
<span>
{build === "oss"

View File

@@ -4,7 +4,7 @@ import { useTranslations } from "next-intl";
import { ColumnDef } from "@tanstack/react-table";
import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { Button } from "./ui/button";
import { ArrowUpDown } from "lucide-react";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import CopyToClipboard from "./CopyToClipboard";
import { Badge } from "./ui/badge";
import moment from "moment";
@@ -16,6 +16,12 @@ import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import NewPricingLicenseForm from "./NewPricingLicenseForm";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu";
type GnerateLicenseKeysTableProps = {
licenseKeys: GeneratedLicenseKey[];
@@ -44,6 +50,7 @@ export default function GenerateLicenseKeysTable({
const [isRefreshing, setIsRefreshing] = useState(false);
const [showGenerateForm, setShowGenerateForm] = useState(false);
const [isClearingInstanceName, setIsClearingInstanceName] = useState(false);
useEffect(() => {
if (searchParams.get(GENERATE_QUERY) !== null) {
@@ -63,6 +70,28 @@ export default function GenerateLicenseKeysTable({
refreshData();
};
const clearInstanceName = async (licenseKey: string) => {
setIsClearingInstanceName(true);
try {
await api.post(
`/org/${orgId}/license/${encodeURIComponent(licenseKey)}/clear-instance-name`
);
toast({
title: t("success"),
description: "Instance name cleared successfully"
});
await refreshData();
} catch (error) {
toast({
title: t("error"),
description: formatAxiosError(error, "Failed to clear instance name"),
variant: "destructive"
});
} finally {
setIsClearingInstanceName(false);
}
};
const refreshData = async () => {
console.log("Data refreshed");
setIsRefreshing(true);
@@ -236,6 +265,39 @@ export default function GenerateLicenseKeysTable({
const termianteAt = row.original.expiresAt;
return moment(termianteAt).format("lll");
}
},
{
id: "actions",
enableHiding: false,
header: () => <span className="p-3"></span>,
cell: ({ row }) => {
const key = row.original;
return (
<div className="flex items-center gap-2 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={
!key.instanceName ||
isClearingInstanceName
}
onClick={() =>
clearInstanceName(key.licenseKey)
}
>
Clear Instance Name
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
}
];
@@ -254,6 +316,7 @@ export default function GenerateLicenseKeysTable({
onAdd={() => {
setShowGenerateForm(true);
}}
stickyRightColumn="actions"
/>
<NewPricingLicenseForm