feat: add Ngrok tunnel provider (#1415)
Adds Ngrok quick tunnel support Adds desktop tunnel docs across locales Improves tunnel settings provider and TTL labels
This commit is contained in:
committed by
GitHub
parent
25ed8e6abb
commit
00a7807168
@@ -149,16 +149,20 @@ const getProviderLabel = (provider: string): string => {
|
||||
if (provider === 'cloudflare') {
|
||||
return 'Cloudflare';
|
||||
}
|
||||
if (provider === 'ngrok') {
|
||||
return 'Ngrok';
|
||||
}
|
||||
return provider;
|
||||
};
|
||||
|
||||
const ProviderOptionLabel: React.FC<{ provider: string }> = ({ provider }) => {
|
||||
const label = getProviderLabel(provider);
|
||||
const isCloudflare = provider === 'cloudflare';
|
||||
const isNgrok = provider === 'ngrok';
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<Icon name="cloud" className={cn('size-4 shrink-0', isCloudflare ? 'text-[var(--status-warning)]' : 'text-muted-foreground')} />
|
||||
<Icon name="cloud" className={cn('size-4 shrink-0', isCloudflare || isNgrok ? 'text-[var(--status-warning)]' : 'text-muted-foreground')} />
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
@@ -182,6 +186,11 @@ const ttlOptionValue = (options: TtlOption[], ttlMs: number | null, fallback: st
|
||||
return matched?.value || fallback;
|
||||
};
|
||||
|
||||
const ttlOptionLabel = (options: TtlOption[], ttlMs: number | null, fallback: string) => {
|
||||
const value = ttlOptionValue(options, ttlMs, fallback);
|
||||
return options.find((entry) => entry.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const formatRemaining = (remainingMs: number): string => {
|
||||
const safeMs = Math.max(0, remainingMs);
|
||||
const totalSeconds = Math.floor(safeMs / 1000);
|
||||
@@ -361,6 +370,23 @@ export const TunnelSettings: React.FC = () => {
|
||||
}
|
||||
return null;
|
||||
}, [localPort]);
|
||||
const selectedProviderCapability = React.useMemo(() => {
|
||||
return providerCapabilities.find((capability) => capability.provider === tunnelProvider) ?? null;
|
||||
}, [providerCapabilities, tunnelProvider]);
|
||||
const tunnelModeOptions = React.useMemo(() => {
|
||||
const supportedModes = new Set(
|
||||
selectedProviderCapability?.modes
|
||||
?.map((mode) => mode.key)
|
||||
.filter((mode): mode is TunnelMode => mode === 'quick' || mode === 'managed-remote' || mode === 'managed-local')
|
||||
);
|
||||
if (supportedModes.size === 0) {
|
||||
return TUNNEL_MODE_OPTIONS;
|
||||
}
|
||||
return TUNNEL_MODE_OPTIONS.filter((option) => supportedModes.has(option.value));
|
||||
}, [selectedProviderCapability]);
|
||||
const installCommand = tunnelProvider === 'ngrok'
|
||||
? 'brew install ngrok'
|
||||
: 'brew install cloudflared';
|
||||
const openExternal = React.useCallback(async (url: string) => {
|
||||
await openExternalUrl(url);
|
||||
}, []);
|
||||
@@ -691,8 +717,12 @@ export const TunnelSettings: React.FC = () => {
|
||||
const handleProviderChange = React.useCallback(async (provider: string) => {
|
||||
setManagedRemoteValidationError(null);
|
||||
setErrorMessage(null);
|
||||
await saveTunnelSettings({ tunnelProvider: provider });
|
||||
}, [saveTunnelSettings]);
|
||||
const capability = providerCapabilities.find((entry) => entry.provider === provider);
|
||||
const defaultMode = capability?.modes?.some((mode) => mode.key === tunnelMode)
|
||||
? tunnelMode
|
||||
: toUiTunnelMode(capability?.modes?.[0]?.key);
|
||||
await saveTunnelSettings({ tunnelProvider: provider, tunnelMode: defaultMode });
|
||||
}, [providerCapabilities, saveTunnelSettings, tunnelMode]);
|
||||
|
||||
const handleBrowseManagedLocalConfig = React.useCallback(async () => {
|
||||
const result = await requestFileAccess({
|
||||
@@ -1146,17 +1176,17 @@ export const TunnelSettings: React.FC = () => {
|
||||
<div className="flex items-start gap-2 rounded-lg border border-[var(--status-warning)]/30 bg-[var(--status-warning)]/5 p-3">
|
||||
<Icon name="error-warning" className="mt-0.5 size-4 shrink-0 text-[var(--status-warning)]" />
|
||||
<div className="space-y-1">
|
||||
<p className="typography-meta font-medium text-foreground">{t('settings.openchamber.tunnel.notAvailable.cloudflaredNotFound')}</p>
|
||||
<p className="typography-meta font-medium text-foreground">{getProviderLabel(tunnelProvider)} tunnel dependency was not found.</p>
|
||||
<p className="typography-meta text-muted-foreground/70">{t('settings.openchamber.tunnel.notAvailable.installHint')}</p>
|
||||
<code className="typography-code block rounded bg-muted/50 px-2 py-1 text-xs text-foreground">
|
||||
brew install cloudflared
|
||||
{installCommand}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{state !== 'not-available' && (
|
||||
{(
|
||||
<section className="space-y-4 px-2 pb-2 pt-0">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
@@ -1193,7 +1223,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
<div className="space-y-1.5">
|
||||
<p className="typography-ui-label text-foreground">{t('settings.openchamber.tunnel.field.tunnelType')}</p>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{TUNNEL_MODE_OPTIONS.map((option) => (
|
||||
{tunnelModeOptions.map((option) => (
|
||||
<Tooltip key={option.value}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -1229,7 +1259,9 @@ export const TunnelSettings: React.FC = () => {
|
||||
disabled={isSavingTtl || isSavingMode || state === 'starting' || state === 'stopping'}
|
||||
>
|
||||
<SelectTrigger className="max-w-[11rem] min-w-0">
|
||||
<SelectValue className="truncate" />
|
||||
<SelectValue className="truncate">
|
||||
{ttlOptionLabel(BOOTSTRAP_TTL_OPTIONS, bootstrapTtlMs, '1800000')}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BOOTSTRAP_TTL_OPTIONS.map((option) => (
|
||||
@@ -1249,7 +1281,9 @@ export const TunnelSettings: React.FC = () => {
|
||||
disabled={isSavingTtl || isSavingMode || state === 'starting' || state === 'stopping'}
|
||||
>
|
||||
<SelectTrigger className="max-w-[11rem] min-w-0">
|
||||
<SelectValue className="truncate" />
|
||||
<SelectValue className="truncate">
|
||||
{ttlOptionLabel(SESSION_TTL_OPTIONS, sessionTtlMs, '28800000')}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SESSION_TTL_OPTIONS.map((option) => (
|
||||
|
||||
Reference in New Issue
Block a user