mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-15 04:39:54 +00:00
Merge branch 'dev' into private-http-ha
This commit is contained in:
@@ -491,6 +491,10 @@ export default function BillingPage() {
|
||||
|
||||
const currentPlanId = getCurrentPlanId();
|
||||
|
||||
const visiblePlanOptions = planOptions.filter(
|
||||
(plan) => plan.id !== "home" || currentPlanId === "home"
|
||||
);
|
||||
|
||||
// Check if subscription is in a problematic state that requires attention
|
||||
const hasProblematicSubscription = (): boolean => {
|
||||
if (!tierSubscription?.subscription) return false;
|
||||
@@ -803,8 +807,8 @@ export default function BillingPage() {
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
{/* Plan Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
{planOptions.map((plan) => {
|
||||
<div className={cn("grid grid-cols-1 gap-4", visiblePlanOptions.length === 5 ? "md:grid-cols-5" : "md:grid-cols-4")}>
|
||||
{visiblePlanOptions.map((plan) => {
|
||||
const isCurrentPlan = plan.id === currentPlanId;
|
||||
const planAction = getPlanAction(plan);
|
||||
|
||||
|
||||
@@ -133,8 +133,7 @@ export default function ResourceAuthenticationPage() {
|
||||
...orgQueries.identityProviders({
|
||||
orgId: org.org.orgId,
|
||||
useOrgOnlyIdp: env.app.identityProviderMode === "org"
|
||||
}),
|
||||
enabled: isPaidUser(tierMatrix.orgOidc)
|
||||
})
|
||||
});
|
||||
|
||||
const pageLoading =
|
||||
|
||||
@@ -95,7 +95,8 @@ export default async function ProxyResourcesPage(
|
||||
ip: target.ip,
|
||||
port: target.port,
|
||||
enabled: target.enabled,
|
||||
healthStatus: target.healthStatus
|
||||
healthStatus: target.healthStatus,
|
||||
siteName: target.siteName
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
@@ -42,7 +42,9 @@ import {
|
||||
SettingsSectionFooter
|
||||
} from "@app/components/Settings";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Check, Heart, InfoIcon } from "lucide-react";
|
||||
import { ArrowRight, Check, ExternalLink, Heart, InfoIcon, TicketCheck } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import DismissableBanner from "@app/components/DismissableBanner";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { SitePriceCalculator } from "@app/components/SitePriceCalculator";
|
||||
@@ -51,6 +53,10 @@ import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const ENTERPRISE_DOCS_URL =
|
||||
"https://docs.pangolin.net/self-host/enterprise-edition";
|
||||
const ENTERPRISE_PRICING_URL = "https://pangolin.net/pricing#Self-Hosted";
|
||||
|
||||
function obfuscateLicenseKey(key: string): string {
|
||||
if (key.length <= 8) return key;
|
||||
const firstPart = key.substring(0, 4);
|
||||
@@ -336,6 +342,47 @@ export default function LicensePage() {
|
||||
description={t("licenseTitleDescription")}
|
||||
/>
|
||||
|
||||
{!licenseStatus?.isLicenseValid && (
|
||||
<DismissableBanner
|
||||
storageKey="license-banner-dismissed"
|
||||
version={1}
|
||||
title={t("licenseBannerTitle")}
|
||||
titleIcon={
|
||||
<TicketCheck className="w-5 h-5 text-primary" />
|
||||
}
|
||||
description={t("licenseBannerDescription")}
|
||||
>
|
||||
<Link
|
||||
href={ENTERPRISE_PRICING_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
{t("licenseBannerGetLicense")}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href={ENTERPRISE_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
|
||||
>
|
||||
{t("licenseBannerViewDocs")}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</DismissableBanner>
|
||||
)}
|
||||
|
||||
{/* <Alert variant="neutral" className="mb-6"> */}
|
||||
{/* <InfoIcon className="h-4 w-4" /> */}
|
||||
{/* <AlertTitle className="font-semibold"> */}
|
||||
|
||||
@@ -49,6 +49,7 @@ import { usePaidStatus } from "@/hooks/usePaidStatus";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { toUnicode } from "punycode";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
|
||||
type AvailableOption = {
|
||||
domainNamespaceId: string;
|
||||
@@ -97,10 +98,16 @@ export default function DomainPicker({
|
||||
warnOnProvidedDomain = false
|
||||
}: DomainPickerProps) {
|
||||
const { env } = useEnvContext();
|
||||
const { user } = useUserContext();
|
||||
const api = createApiClient({ env });
|
||||
const t = useTranslations();
|
||||
const { hasSaasSubscription } = usePaidStatus();
|
||||
|
||||
const requiresPaywall =
|
||||
build === "saas" &&
|
||||
!hasSaasSubscription(tierMatrix[TierFeature.DomainNamespaces]) &&
|
||||
new Date(user.dateCreated) > new Date("2026-04-13");
|
||||
|
||||
const { data = [], isLoading: loadingDomains } = useQuery(
|
||||
orgQueries.domains({ orgId })
|
||||
);
|
||||
@@ -659,6 +666,7 @@ export default function DomainPicker({
|
||||
})
|
||||
}
|
||||
className="mx-2 rounded-md"
|
||||
disabled={requiresPaywall}
|
||||
>
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3">
|
||||
<Zap className="h-4 w-4 text-primary" />
|
||||
@@ -699,11 +707,7 @@ export default function DomainPicker({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{build === "saas" &&
|
||||
!hasSaasSubscription(
|
||||
tierMatrix[TierFeature.DomainNamespaces]
|
||||
) &&
|
||||
!hideFreeDomain && (
|
||||
{requiresPaywall && !hideFreeDomain && (
|
||||
<Card className="mt-3 border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden">
|
||||
<CardContent className="py-3 px-4">
|
||||
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
||||
|
||||
@@ -54,6 +54,7 @@ export type TargetHealth = {
|
||||
port: number;
|
||||
enabled: boolean;
|
||||
healthStatus: "healthy" | "unhealthy" | "unknown" | null;
|
||||
siteName: string | null;
|
||||
};
|
||||
|
||||
export type ResourceRow = {
|
||||
@@ -274,7 +275,9 @@ export default function ProxyResourcesTable({
|
||||
}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
{`${target.ip}:${target.port}`}
|
||||
{target.siteName
|
||||
? `${target.siteName} (${target.ip}:${target.port})`
|
||||
: `${target.ip}:${target.port}`}
|
||||
</div>
|
||||
<span
|
||||
className={`capitalize ${
|
||||
@@ -301,7 +304,9 @@ export default function ProxyResourcesTable({
|
||||
status="unknown"
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
{`${target.ip}:${target.port}`}
|
||||
{target.siteName
|
||||
? `${target.siteName} (${target.ip}:${target.port})`
|
||||
: `${target.ip}:${target.port}`}
|
||||
</div>
|
||||
<span className="text-muted-foreground">
|
||||
{!target.enabled
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Button } from "./ui/button";
|
||||
import { TicketCheck } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import Link from "next/link";
|
||||
|
||||
interface SidebarLicenseButtonProps {
|
||||
@@ -20,8 +21,11 @@ export default function SidebarLicenseButton({
|
||||
isCollapsed = false
|
||||
}: SidebarLicenseButtonProps) {
|
||||
const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext();
|
||||
const { user } = useUserContext();
|
||||
|
||||
const url = "https://docs.pangolin.net/self-host/enterprise-edition";
|
||||
const url = user?.serverAdmin
|
||||
? "/admin/license"
|
||||
: "https://docs.pangolin.net/self-host/enterprise-edition";
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ export default function UserDevicesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "online",
|
||||
friendlyName: t("online"),
|
||||
friendlyName: t("connected"),
|
||||
header: () => {
|
||||
return (
|
||||
<ColumnFilterButton
|
||||
@@ -410,7 +410,7 @@ export default function UserDevicesTable({
|
||||
}
|
||||
searchPlaceholder={t("searchPlaceholder")}
|
||||
emptyMessage={t("emptySearchOptions")}
|
||||
label={t("online")}
|
||||
label={t("connected")}
|
||||
className="p-3"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -237,7 +237,7 @@ function drawInteractiveCountries(
|
||||
return svg;
|
||||
}
|
||||
|
||||
type WorldJsonCountryData = { properties: { name: string; a3: string } };
|
||||
type WorldJsonCountryData = d3.ExtendedFeature<d3.GeoGeometryObjects | null, { name: string; a3: string }>;
|
||||
|
||||
function parseWorldTopoJsonToGeoJsonFeatures(): Array<WorldJsonCountryData> {
|
||||
const collection = topojson.feature(
|
||||
|
||||
Reference in New Issue
Block a user