mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-14 16:30:15 +02:00
improve resource type picker
This commit is contained in:
+16
-2
@@ -289,6 +289,20 @@
|
||||
"siteSelectionDescription": "This site will provide connectivity to the target.",
|
||||
"resourceType": "Resource Type",
|
||||
"resourceTypeDescription": "This controls the resource protocol and how it will be rendered in the browser. This can’t be changed later.",
|
||||
"resourceTypeSearch": "Search resource types...",
|
||||
"resourceTypeNotFound": "No resource type found",
|
||||
"resourceTypeHttpDescription": "Proxy web traffic over HTTPS using a domain name",
|
||||
"resourceTypeInferenceDescription": "Route AI inference requests through attached providers",
|
||||
"resourceTypeSshDescription": "Access an SSH server from the browser",
|
||||
"resourceTypeRdpDescription": "Access a remote desktop from the browser",
|
||||
"resourceTypeVncDescription": "Access a VNC desktop from the browser",
|
||||
"resourceTypeTcpDescription": "Proxy raw TCP traffic using a port number",
|
||||
"resourceTypeUdpDescription": "Proxy raw UDP traffic using a port number",
|
||||
"privateResourceTypeDescription": "This controls how clients reach the resource. This can’t be changed later.",
|
||||
"privateResourceTypeHostDescription": "Expose a single host on the site network to connected clients",
|
||||
"privateResourceTypeCidrDescription": "Expose a CIDR range on the site network to connected clients",
|
||||
"privateResourceTypeHttpDescription": "Access an HTTP or HTTPS service through a domain",
|
||||
"privateResourceTypeSshDescription": "Access an SSH server from connected clients",
|
||||
"resourceDomainDescription": "The resource will be served at this fully qualified domain name.",
|
||||
"resourceHTTPSSettings": "HTTPS Settings",
|
||||
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
|
||||
@@ -3522,8 +3536,8 @@
|
||||
"sourceAddress": "Source Address",
|
||||
"destinationAddress": "Destination Address",
|
||||
"duration": "Duration",
|
||||
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
|
||||
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
|
||||
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or POC</bookADemoLink>",
|
||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or POC</bookADemoLink>",
|
||||
"certResolver": "Certificate Resolver",
|
||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||
"selectCertResolver": "Select Certificate Resolver",
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
} from "@app/components/Settings";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import {
|
||||
OptionSelect,
|
||||
type OptionSelectOption
|
||||
} from "@app/components/OptionSelect";
|
||||
DescribedSelect,
|
||||
type DescribedSelectOption
|
||||
} from "@app/components/DescribedSelect";
|
||||
import DomainPicker from "@app/components/DomainPicker";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
@@ -147,24 +147,35 @@ export default function CreatePrivateResourcePage() {
|
||||
const authDaemonMode = form.watch("authDaemonMode");
|
||||
const isNativeSsh = mode === "ssh" && authDaemonMode === "native";
|
||||
|
||||
const modeOptions: OptionSelectOption<PrivateResourceMode>[] = [
|
||||
{ value: "host", label: t("createInternalResourceDialogModeHost") },
|
||||
{ value: "cidr", label: t("createInternalResourceDialogModeCidr") },
|
||||
const modeOptions: DescribedSelectOption<PrivateResourceMode>[] = [
|
||||
{
|
||||
value: "host",
|
||||
title: t("createInternalResourceDialogModeHost"),
|
||||
description: t("privateResourceTypeHostDescription")
|
||||
},
|
||||
{
|
||||
value: "cidr",
|
||||
title: t("createInternalResourceDialogModeCidr"),
|
||||
description: t("privateResourceTypeCidrDescription")
|
||||
},
|
||||
...(!disableEnterpriseFeatures
|
||||
? [
|
||||
{
|
||||
value: "http" as const,
|
||||
label: t("createInternalResourceDialogModeHttp")
|
||||
title: t("createInternalResourceDialogModeHttp"),
|
||||
description: t("privateResourceTypeHttpDescription")
|
||||
},
|
||||
{
|
||||
value: "ssh" as const,
|
||||
label: t("createInternalResourceDialogModeSsh")
|
||||
title: t("createInternalResourceDialogModeSsh"),
|
||||
description: t("privateResourceTypeSshDescription")
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
value: "inference" as const,
|
||||
label: t("createInternalResourceDialogModeInference")
|
||||
title: t("createInternalResourceDialogModeInference"),
|
||||
description: t("resourceTypeInferenceDescription")
|
||||
}
|
||||
];
|
||||
|
||||
@@ -260,6 +271,110 @@ export default function CreatePrivateResourcePage() {
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("type")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DescribedSelect<PrivateResourceMode>
|
||||
options={
|
||||
modeOptions
|
||||
}
|
||||
value={field.value}
|
||||
onChange={(
|
||||
newMode
|
||||
) => {
|
||||
field.onChange(
|
||||
newMode
|
||||
);
|
||||
if (
|
||||
newMode ===
|
||||
"ssh"
|
||||
) {
|
||||
form.setValue(
|
||||
"authDaemonMode",
|
||||
"native"
|
||||
);
|
||||
form.setValue(
|
||||
"standardDaemonLocation",
|
||||
"site"
|
||||
);
|
||||
form.setValue(
|
||||
"destination",
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"destinationPort",
|
||||
null
|
||||
);
|
||||
} else if (
|
||||
newMode ===
|
||||
"http"
|
||||
) {
|
||||
form.setValue(
|
||||
"destinationPort",
|
||||
443
|
||||
);
|
||||
} else if (
|
||||
newMode ===
|
||||
"inference"
|
||||
) {
|
||||
form.setValue(
|
||||
"siteIds",
|
||||
[]
|
||||
);
|
||||
setSelectedSites(
|
||||
[]
|
||||
);
|
||||
form.setValue(
|
||||
"destination",
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"destinationPort",
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"providerIds",
|
||||
[]
|
||||
);
|
||||
setSelectedProviders(
|
||||
[]
|
||||
);
|
||||
} else {
|
||||
form.setValue(
|
||||
"destinationPort",
|
||||
null
|
||||
);
|
||||
}
|
||||
}}
|
||||
searchPlaceholder={t(
|
||||
"resourceTypeSearch"
|
||||
)}
|
||||
emptyMessage={t(
|
||||
"resourceTypeNotFound"
|
||||
)}
|
||||
placeholder={t(
|
||||
"noneSelected"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"privateResourceTypeDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -283,91 +398,6 @@ export default function CreatePrivateResourcePage() {
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("type")}
|
||||
</FormLabel>
|
||||
<OptionSelect<PrivateResourceMode>
|
||||
options={modeOptions}
|
||||
value={field.value}
|
||||
onChange={(newMode) => {
|
||||
field.onChange(
|
||||
newMode
|
||||
);
|
||||
if (
|
||||
newMode ===
|
||||
"ssh"
|
||||
) {
|
||||
form.setValue(
|
||||
"authDaemonMode",
|
||||
"native"
|
||||
);
|
||||
form.setValue(
|
||||
"standardDaemonLocation",
|
||||
"site"
|
||||
);
|
||||
form.setValue(
|
||||
"destination",
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"destinationPort",
|
||||
null
|
||||
);
|
||||
} else if (
|
||||
newMode ===
|
||||
"http"
|
||||
) {
|
||||
form.setValue(
|
||||
"destinationPort",
|
||||
443
|
||||
);
|
||||
} else if (
|
||||
newMode ===
|
||||
"inference"
|
||||
) {
|
||||
form.setValue(
|
||||
"siteIds",
|
||||
[]
|
||||
);
|
||||
setSelectedSites(
|
||||
[]
|
||||
);
|
||||
form.setValue(
|
||||
"destination",
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"destinationPort",
|
||||
null
|
||||
);
|
||||
form.setValue(
|
||||
"providerIds",
|
||||
[]
|
||||
);
|
||||
setSelectedProviders(
|
||||
[]
|
||||
);
|
||||
} else {
|
||||
form.setValue(
|
||||
"destinationPort",
|
||||
null
|
||||
);
|
||||
}
|
||||
}}
|
||||
cols={4}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
{(mode === "http" ||
|
||||
mode === "inference") && (
|
||||
<SettingsFormCell span="full">
|
||||
|
||||
@@ -18,9 +18,9 @@ import {
|
||||
} from "@app/components/Settings";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import {
|
||||
OptionSelect,
|
||||
type OptionSelectOption
|
||||
} from "@app/components/OptionSelect";
|
||||
DescribedSelect,
|
||||
type DescribedSelectOption
|
||||
} from "@app/components/DescribedSelect";
|
||||
import {
|
||||
StrategySelect,
|
||||
type StrategyOption
|
||||
@@ -775,26 +775,45 @@ export default function Page() {
|
||||
}
|
||||
];
|
||||
|
||||
let typeLabels: Partial<Record<NewResourceType, string>> = {
|
||||
http: "HTTP",
|
||||
inference: t("createInternalResourceDialogModeInference"),
|
||||
tcp: "TCP",
|
||||
udp: "UDP"
|
||||
const typeMeta: Record<
|
||||
NewResourceType,
|
||||
{ title: string; description: string }
|
||||
> = {
|
||||
http: {
|
||||
title: t("createInternalResourceDialogModeHttp"),
|
||||
description: t("resourceTypeHttpDescription")
|
||||
},
|
||||
inference: {
|
||||
title: t("createInternalResourceDialogModeInference"),
|
||||
description: t("resourceTypeInferenceDescription")
|
||||
},
|
||||
ssh: {
|
||||
title: t("createInternalResourceDialogModeSsh"),
|
||||
description: t("resourceTypeSshDescription")
|
||||
},
|
||||
rdp: {
|
||||
title: t("rdpTitle"),
|
||||
description: t("resourceTypeRdpDescription")
|
||||
},
|
||||
vnc: {
|
||||
title: t("vncTitle"),
|
||||
description: t("resourceTypeVncDescription")
|
||||
},
|
||||
tcp: {
|
||||
title: t("createInternalResourceDialogTcp"),
|
||||
description: t("resourceTypeTcpDescription")
|
||||
},
|
||||
udp: {
|
||||
title: t("createInternalResourceDialogUdp"),
|
||||
description: t("resourceTypeUdpDescription")
|
||||
}
|
||||
};
|
||||
|
||||
if (enterpriseModesAllowed) {
|
||||
typeLabels = {
|
||||
...typeLabels,
|
||||
ssh: "SSH",
|
||||
rdp: "RDP",
|
||||
vnc: "VNC"
|
||||
};
|
||||
}
|
||||
|
||||
const typeOptions: OptionSelectOption<NewResourceType>[] =
|
||||
const typeOptions: DescribedSelectOption<NewResourceType>[] =
|
||||
availableTypes.map((type) => ({
|
||||
value: type,
|
||||
label: typeLabels[type] ?? type.toUpperCase()
|
||||
title: typeMeta[type].title,
|
||||
description: typeMeta[type].description
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -831,6 +850,35 @@ export default function Page() {
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="half">
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
{t("type")}
|
||||
</Label>
|
||||
<DescribedSelect<NewResourceType>
|
||||
options={typeOptions}
|
||||
value={resourceType}
|
||||
onChange={
|
||||
setResourceType
|
||||
}
|
||||
searchPlaceholder={t(
|
||||
"resourceTypeSearch"
|
||||
)}
|
||||
emptyMessage={t(
|
||||
"resourceTypeNotFound"
|
||||
)}
|
||||
placeholder={t(
|
||||
"noneSelected"
|
||||
)}
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(
|
||||
"resourceTypeDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
|
||||
<SettingsFormCell span="half">
|
||||
<Form {...baseForm}>
|
||||
<form
|
||||
@@ -876,27 +924,6 @@ export default function Page() {
|
||||
</Form>
|
||||
</SettingsFormCell>
|
||||
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">
|
||||
{t("type")}
|
||||
</p>
|
||||
<OptionSelect<NewResourceType>
|
||||
options={typeOptions}
|
||||
value={resourceType}
|
||||
onChange={
|
||||
setResourceType
|
||||
}
|
||||
cols={6}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"resourceTypeDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
|
||||
{isHttpResource && (
|
||||
<SettingsFormCell span="full">
|
||||
<Form {...httpForm}>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from "@app/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@app/components/ui/popover";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export type DescribedSelectOption<TValue extends string> = {
|
||||
value: TValue;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type DescribedSelectProps<TValue extends string> = {
|
||||
options: ReadonlyArray<DescribedSelectOption<TValue>>;
|
||||
value: TValue;
|
||||
onChange: (value: TValue) => void;
|
||||
searchPlaceholder: string;
|
||||
emptyMessage: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function DescribedSelect<TValue extends string>({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
searchPlaceholder,
|
||||
emptyMessage,
|
||||
placeholder,
|
||||
disabled,
|
||||
className
|
||||
}: DescribedSelectProps<TValue>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selected = options.find((option) => option.value === value);
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"h-9 w-full justify-between font-normal",
|
||||
!selected && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate text-left">
|
||||
{selected?.title ?? placeholder}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder={searchPlaceholder} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={`${option.value} ${option.title} ${option.description}`}
|
||||
onSelect={() => {
|
||||
onChange(option.value);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4 shrink-0",
|
||||
option.value === value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="truncate">
|
||||
{option.title}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs leading-snug">
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -516,7 +516,9 @@ export default function DomainPicker({
|
||||
return (
|
||||
<Alert>
|
||||
<Globe className="h-4 w-4" />
|
||||
<AlertTitle>{t("domainPickerNoDomainsAvailableTitle")}</AlertTitle>
|
||||
<AlertTitle>
|
||||
{t("domainPickerNoDomainsAvailableTitle")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="space-y-3">
|
||||
<p>{t("domainPickerNoDomainsAvailableDescription")}</p>
|
||||
<Button asChild size="sm" variant="outline">
|
||||
@@ -592,7 +594,6 @@ export default function DomainPicker({
|
||||
)}
|
||||
</p>
|
||||
<PaidFeaturesAlert
|
||||
showBookADemo={false}
|
||||
tiers={
|
||||
tierMatrix[
|
||||
TierFeature.WildcardSubdomain
|
||||
|
||||
@@ -45,11 +45,11 @@ const bannerClassName =
|
||||
"mb-6 border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden";
|
||||
const bannerContentClassName = "py-3 px-4";
|
||||
const bannerRowClassName =
|
||||
"flex items-center gap-2.5 text-sm text-muted-foreground";
|
||||
"flex items-center gap-2.5 text-sm text-muted-foreground whitespace-nowrap";
|
||||
const bannerIconClassName = "size-4 shrink-0 text-black-500";
|
||||
const bannerTextClassName = "whitespace-nowrap shrink-0";
|
||||
const docsLinkClassName =
|
||||
"inline-flex items-center gap-1 font-medium text-black-600 underline";
|
||||
const PANGOLIN_CLOUD_SIGNUP_URL = "https://app.pangolin.net/auth/signup/";
|
||||
const ENTERPRISE_DOCS_URL =
|
||||
"https://docs.pangolin.net/self-host/enterprise-edition";
|
||||
const BOOK_A_DEMO_URL = "https://click.fossorial.io/ep922";
|
||||
@@ -64,34 +64,21 @@ function getTierLinkRenderer(billingHref: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function getPangolinCloudLinkRenderer() {
|
||||
return function pangolinCloudLinkRenderer(chunks: React.ReactNode) {
|
||||
return (
|
||||
<Link
|
||||
href={PANGOLIN_CLOUD_SIGNUP_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={docsLinkClassName}
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getBookADemoLinkRenderer() {
|
||||
return function bookADemoLinkRenderer(chunks: React.ReactNode) {
|
||||
return (
|
||||
<Link
|
||||
href={BOOK_A_DEMO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={docsLinkClassName}
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</Link>
|
||||
<span className="whitespace-nowrap">
|
||||
<Link
|
||||
href={BOOK_A_DEMO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={docsLinkClassName}
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -114,10 +101,9 @@ function getDocsLinkRenderer(href: string) {
|
||||
|
||||
type Props = {
|
||||
tiers: Tier[];
|
||||
showBookADemo?: boolean;
|
||||
};
|
||||
|
||||
export function PaidFeaturesAlert({ tiers, showBookADemo = true }: Props) {
|
||||
export function PaidFeaturesAlert({ tiers }: Props) {
|
||||
const t = useTranslations();
|
||||
const params = useParams();
|
||||
const orgId = params?.orgId as string | undefined;
|
||||
@@ -133,11 +119,8 @@ export function PaidFeaturesAlert({ tiers, showBookADemo = true }: Props) {
|
||||
? `/${orgId}/settings/billing`
|
||||
: "https://pangolin.net/pricing";
|
||||
const tierLinkRenderer = getTierLinkRenderer(billingHref);
|
||||
const pangolinCloudLinkRenderer = getPangolinCloudLinkRenderer();
|
||||
const enterpriseDocsLinkRenderer = getDocsLinkRenderer(ENTERPRISE_DOCS_URL);
|
||||
const bookADemoLinkRenderer = showBookADemo
|
||||
? getBookADemoLinkRenderer()
|
||||
: () => null;
|
||||
const bookADemoLinkRenderer = getBookADemoLinkRenderer();
|
||||
|
||||
if (env.flags.disableEnterpriseFeatures) {
|
||||
return null;
|
||||
@@ -150,17 +133,12 @@ export function PaidFeaturesAlert({ tiers, showBookADemo = true }: Props) {
|
||||
<CardContent className={bannerContentClassName}>
|
||||
<div className={bannerRowClassName}>
|
||||
<KeyRound className={bannerIconClassName} />
|
||||
<span>
|
||||
<span className={bannerTextClassName}>
|
||||
{requiredTiersLabel
|
||||
? isActive
|
||||
? t.rich("upgradeToTierToUse", {
|
||||
tier: requiredTiersLabel,
|
||||
tierLink: tierLinkRenderer
|
||||
})
|
||||
: t.rich("upgradeToTierToUse", {
|
||||
tier: requiredTiersLabel,
|
||||
tierLink: tierLinkRenderer
|
||||
})
|
||||
? t.rich("upgradeToTierToUse", {
|
||||
tier: requiredTiersLabel,
|
||||
tierLink: tierLinkRenderer
|
||||
})
|
||||
: isActive
|
||||
? t("mustUpgradeToUse")
|
||||
: t("subscriptionRequiredToUse")}
|
||||
@@ -170,34 +148,16 @@ export function PaidFeaturesAlert({ tiers, showBookADemo = true }: Props) {
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{build === "enterprise" && !hasEnterpriseLicense ? (
|
||||
{(build === "enterprise" || build === "oss") &&
|
||||
!hasEnterpriseLicense ? (
|
||||
<Card className={bannerClassName}>
|
||||
<CardContent className={bannerContentClassName}>
|
||||
<div className={bannerRowClassName}>
|
||||
<KeyRound className={bannerIconClassName} />
|
||||
<span>
|
||||
{t.rich("licenseRequiredToUse", {
|
||||
enterpriseLicenseLink:
|
||||
enterpriseDocsLinkRenderer,
|
||||
pangolinCloudLink: pangolinCloudLinkRenderer,
|
||||
bookADemoLink: bookADemoLinkRenderer
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{build === "oss" && !hasEnterpriseLicense ? (
|
||||
<Card className={bannerClassName}>
|
||||
<CardContent className={bannerContentClassName}>
|
||||
<div className={bannerRowClassName}>
|
||||
<KeyRound className={bannerIconClassName} />
|
||||
<span>
|
||||
<span className={bannerTextClassName}>
|
||||
{t.rich("ossEnterpriseEditionRequired", {
|
||||
enterpriseEditionLink:
|
||||
enterpriseDocsLinkRenderer,
|
||||
pangolinCloudLink: pangolinCloudLinkRenderer,
|
||||
bookADemoLink: bookADemoLinkRenderer
|
||||
})}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user