mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-23 14:29:05 +02:00
Display the quantities again
This commit is contained in:
@@ -590,6 +590,14 @@
|
||||
"licensePurchaseSites": "Purchase Additional Sites",
|
||||
"licenseSitesUsedMax": "{usedSites} of {maxSites} sites used",
|
||||
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} in system.",
|
||||
"licenseUsage": "License Usage",
|
||||
"licenseUsageDescription": "View the number of users and sites licensed for this host.",
|
||||
"licenseUsageSites": "Sites",
|
||||
"licenseUsageUsers": "Users",
|
||||
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed host.",
|
||||
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
|
||||
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}} in system.",
|
||||
"licenseUnlimited": "Unlimited",
|
||||
"licensePurchaseDescription": "Choose how many sites you want to {selectedMode, select, license {purchase a license for. You can always add more sites later.} other {add to your existing license.}}",
|
||||
"licenseFee": "License fee",
|
||||
"licensePriceSite": "Price per site",
|
||||
|
||||
+145
-65
@@ -41,6 +41,7 @@ import {
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionFooter
|
||||
} from "@app/components/Settings";
|
||||
import { Progress } from "@app/components/ui/progress";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import {
|
||||
ArrowRight,
|
||||
@@ -64,13 +65,6 @@ 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);
|
||||
const lastPart = key.substring(key.length - 4);
|
||||
return `${firstPart}••••••••••••••••••••${lastPart}`;
|
||||
}
|
||||
|
||||
export default function LicensePage() {
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [rows, setRows] = useState<LicenseKeyCache[]>([]);
|
||||
@@ -80,7 +74,6 @@ export default function LicensePage() {
|
||||
useState<LicenseKeyCache | null>(null);
|
||||
|
||||
const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext();
|
||||
const [hostLicense, setHostLicense] = useState<string | null>(null);
|
||||
const [isPurchaseModalOpen, setIsPurchaseModalOpen] = useState(false);
|
||||
const [purchaseMode, setPurchaseMode] = useState<"license">("license");
|
||||
|
||||
@@ -128,12 +121,6 @@ export default function LicensePage() {
|
||||
);
|
||||
const keys = response.data.data;
|
||||
setRows(keys);
|
||||
const hostKey = keys.find((key) => key.type === "host");
|
||||
if (hostKey) {
|
||||
setHostLicense(hostKey.licenseKey);
|
||||
} else {
|
||||
setHostLicense(null);
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("licenseErrorKeyLoad"),
|
||||
@@ -399,62 +386,155 @@ export default function LicensePage() {
|
||||
{/* </Alert> */}
|
||||
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SSTitle>{t("licenseHost")}</SSTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("licenseHostDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
{licenseStatus?.isLicenseValid ? (
|
||||
<div className="space-y-2 text-green-500">
|
||||
<div className="text-2xl flex items-center gap-2">
|
||||
<Check />
|
||||
{t("licensed") +
|
||||
`${licenseStatus?.tier === "personal" ? ` (${t("personalUseOnly")})` : ""}`}
|
||||
</div>
|
||||
<SettingsSectionGrid cols={2}>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SSTitle>{t("licenseUsage")}</SSTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("licenseUsageDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
{t("licenseUsageSites")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl">
|
||||
{t("unlicensed")}
|
||||
{t("licenseSitesUsed", {
|
||||
count: licenseStatus?.usedSites || 0
|
||||
})}
|
||||
</div>
|
||||
{licenseStatus?.maxSites ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{t("licenseSitesUsedMax", {
|
||||
usedSites:
|
||||
licenseStatus.usedSites ||
|
||||
0,
|
||||
maxSites:
|
||||
licenseStatus.maxSites
|
||||
})}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{Math.round(
|
||||
((licenseStatus.usedSites ||
|
||||
0) /
|
||||
licenseStatus.maxSites) *
|
||||
100
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={
|
||||
((licenseStatus.usedSites ||
|
||||
0) /
|
||||
licenseStatus.maxSites) *
|
||||
100
|
||||
}
|
||||
className="h-5"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("licenseNoSiteLimit")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
{t("licenseUsageUsers")}
|
||||
</div>
|
||||
<div className="text-2xl">
|
||||
{t("licenseUsersUsed", {
|
||||
count: licenseStatus?.usedUsers || 0
|
||||
})}
|
||||
</div>
|
||||
{licenseStatus?.maxUsers ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{t("licenseUsersUsedMax", {
|
||||
usedUsers:
|
||||
licenseStatus.usedUsers ||
|
||||
0,
|
||||
maxUsers:
|
||||
licenseStatus.maxUsers
|
||||
})}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{Math.round(
|
||||
((licenseStatus.usedUsers ||
|
||||
0) /
|
||||
licenseStatus.maxUsers) *
|
||||
100
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={
|
||||
((licenseStatus.usedUsers ||
|
||||
0) /
|
||||
licenseStatus.maxUsers) *
|
||||
100
|
||||
}
|
||||
className="h-5"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("licenseNoUserLimit")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SSTitle>{t("licenseHost")}</SSTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("licenseHostDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
{licenseStatus?.isLicenseValid ? (
|
||||
<div className="space-y-2 text-green-500">
|
||||
<div className="text-2xl flex items-center gap-2">
|
||||
<Check />
|
||||
{t("licensed") +
|
||||
`${licenseStatus?.tier === "personal" ? ` (${t("personalUseOnly")})` : ""}`}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl">
|
||||
{t("unlicensed")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{licenseStatus?.hostId && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
{t("hostId")}
|
||||
</div>
|
||||
<CopyTextBox text={licenseStatus.hostId} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{licenseStatus?.hostId && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
{t("hostId")}
|
||||
</div>
|
||||
<CopyTextBox text={licenseStatus.hostId} />
|
||||
</div>
|
||||
)}
|
||||
{hostLicense && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
{t("licenseKey")}
|
||||
</div>
|
||||
<CopyTextBox
|
||||
text={hostLicense}
|
||||
displayText={obfuscateLicenseKey(
|
||||
hostLicense
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={recheck}
|
||||
disabled={isRecheckingLicense}
|
||||
loading={isRecheckingLicense}
|
||||
>
|
||||
{t("licenseReckeckAll")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={recheck}
|
||||
disabled={isRecheckingLicense}
|
||||
loading={isRecheckingLicense}
|
||||
>
|
||||
{t("licenseReckeckAll")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsSectionGrid>
|
||||
<LicenseKeysDataTable
|
||||
licenseKeys={rows}
|
||||
onDelete={(key) => {
|
||||
|
||||
@@ -112,6 +112,54 @@ export function LicenseKeysDataTable({
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "quantity",
|
||||
friendlyName: t("users"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("users")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const quantity = row.original.quantity;
|
||||
if (quantity === undefined) {
|
||||
return "-";
|
||||
}
|
||||
return quantity < 0 ? t("licenseUnlimited") : quantity;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "quantity_2",
|
||||
friendlyName: t("sites"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("sites")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const quantity = row.original.quantity_2;
|
||||
if (quantity === undefined) {
|
||||
return "-";
|
||||
}
|
||||
return quantity < 0 ? t("licenseUnlimited") : quantity;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "terminateAt",
|
||||
friendlyName: t("licenseTableValidUntil"),
|
||||
|
||||
Reference in New Issue
Block a user