Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 2 additions & 19 deletions app/(protected)/tunnels/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { InteractionContextType, useInteractionContext } from '@lib/contexts/interaction';
import { TunnelsContextType, useTunnelsContext } from '@lib/contexts/tunnels';
import { routePath } from '@lib/routes/route-paths';
import { tunnelStatusTagVariantByStatus } from '@lib/tunnel-status';
import ActionButton from '@shared/ActionButton';
import { CompactCustomCard } from '@shared/cards/CompactCustomCard';
import { CopyableValue } from '@shared/CopyableValue';
Expand Down Expand Up @@ -266,24 +267,6 @@ export default function TunnelPage() {
}
};

const getStatusTagVariant = () => {
if (!tunnel) {
return 'slate';
}

switch (tunnel.status) {
case 'healthy':
return 'green';
case 'degraded':
return 'yellow';
case 'down':
return 'red';

default:
return 'slate';
}
};

if (!tunnel) {
return (
<div className="col mx-auto w-full max-w-[620px] gap-6">
Expand Down Expand Up @@ -313,7 +296,7 @@ export default function TunnelPage() {

<div className="row gap-3">
<div className="text-2xl font-bold">{tunnel.alias}</div>
<SmallTag variant={getStatusTagVariant()} isLarge>
<SmallTag variant={tunnelStatusTagVariantByStatus[tunnel.status]} isLarge>
<div className="capitalize">{tunnel.status}</div>
</SmallTag>
</div>
Expand Down
57 changes: 50 additions & 7 deletions src/components/create-job/sections/AppParametersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SelectItem } from '@heroui/select';
import { BOOLEAN_TYPES } from '@data/booleanTypes';
import { getTunnels } from '@lib/api/tunnels';
import { TunnelsContextType, useTunnelsContext } from '@lib/contexts/tunnels';
import { compareTunnelStatusAndAlias, tunnelStatusDotColorClassByStatus, type TunnelStatus } from '@lib/tunnel-status';
import InputWithLabel from '@shared/InputWithLabel';
import Label from '@shared/Label';
import DeeployInfoTag from '@shared/jobs/DeeployInfoTag';
Expand All @@ -25,12 +26,20 @@ type ExistingTunnelOption = {
alias: string;
token: string;
url: string;
status: TunnelStatus;
isCustom?: false;
};

type TunnelSelectOption = ExistingTunnelOption & {
isCustom?: boolean;
type CustomTunnelOption = {
id: string;
alias: string;
token: string;
url: string;
isCustom: true;
};

type TunnelSelectOption = ExistingTunnelOption | CustomTunnelOption;

const CUSTOM_TUNNEL_OPTION = 'custom';

export default function AppParametersSection({
Expand Down Expand Up @@ -102,8 +111,9 @@ export default function AppParametersSection({
alias: (tunnel.metadata.alias || tunnel.metadata.dns_name) as string,
token: tunnel.metadata.tunnel_token as string,
url: tunnel.metadata.dns_name as string,
status: tunnel.status as TunnelStatus,
}))
.sort((a, b) => a.alias.localeCompare(b.alias));
.sort(compareTunnelStatusAndAlias);

setExistingTunnels(tunnels);
return tunnels;
Expand Down Expand Up @@ -240,6 +250,17 @@ export default function AppParametersSection({
}}
placeholder={isFetchingTunnels ? 'Loading tunnels...' : 'Select an existing tunnel'}
isDisabled={isFetchingTunnels}
renderValue={(items) => {
return items.map((item) => {
const tunnel = item.data as TunnelSelectOption | undefined;

if (!tunnel) {
return <div key={item.key}>{item.textValue}</div>;
}

return <TunnelSelectOptionContent key={item.key} tunnel={tunnel} />;
});
}}
>
{(option: object) => {
const tunnel = option as TunnelSelectOption;
Expand All @@ -249,10 +270,7 @@ export default function AppParametersSection({
key={tunnel.id}
textValue={tunnel.isCustom ? tunnel.alias : `${tunnel.alias} | ${tunnel.url}`}
>
<div className="row items-center gap-2 py-1">
<div className="font-medium">{tunnel.alias}</div>
<div className="font-roboto-mono text-xs text-slate-500">{tunnel.url}</div>
</div>
<TunnelSelectOptionContent tunnel={tunnel} />
</SelectItem>
);
}}
Expand Down Expand Up @@ -313,3 +331,28 @@ export default function AppParametersSection({
</div>
);
}

function TunnelSelectOptionContent({ tunnel }: { tunnel: TunnelSelectOption }) {
if (tunnel.isCustom) {
return (
<div className="row min-w-0 items-center gap-2 py-1">
<div className="max-w-[12rem] shrink-0 truncate font-medium sm:max-w-[14rem]">{tunnel.alias}</div>
<div className="font-roboto-mono min-w-0 flex-1 truncate text-xs text-slate-500">{tunnel.url}</div>
</div>
);
}

return (
<div className="grid grid-cols-[minmax(0,1fr)_6.75rem] items-center gap-2 py-1">
<div className="row min-w-0 items-center gap-2">
<div className="max-w-[12rem] shrink-0 truncate font-medium sm:max-w-[14rem]">{tunnel.alias}</div>
<div className="font-roboto-mono min-w-0 flex-1 truncate text-xs text-slate-500">{tunnel.url}</div>
</div>

<div className="row min-w-0 items-center justify-end gap-1.5 text-right text-xs">
<div className={`h-2 w-2 rounded-full ${tunnelStatusDotColorClassByStatus[tunnel.status]}`}></div>
<div className="capitalize">{tunnel.status}</div>
</div>
</div>
);
}
11 changes: 2 additions & 9 deletions src/components/tunnels/TunnelCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { deleteTunnel } from '@lib/api/tunnels';
import { InteractionContextType, useInteractionContext } from '@lib/contexts/interaction';
import { TunnelsContextType, useTunnelsContext } from '@lib/contexts/tunnels';
import { routePath } from '@lib/routes/route-paths';
import { tunnelStatusDotColorClassByStatus } from '@lib/tunnel-status';
import { BorderedCard } from '@shared/cards/BorderedCard';
import ContextMenuWithTrigger from '@shared/ContextMenuWithTrigger';
import { CopyableValue } from '@shared/CopyableValue';
import { SmallTag } from '@shared/SmallTag';
import { Tunnel } from '@typedefs/tunnels';
import clsx from 'clsx';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import toast from 'react-hot-toast';
Expand Down Expand Up @@ -56,14 +56,7 @@ export default function TunnelCard({ tunnel, fetchTunnels }: { tunnel: Tunnel; f
<BorderedCard isHoverable>
<div className="row justify-between gap-3 bg-white lg:gap-6">
<div className="row gap-2.5">
<div
className={clsx('h-9 w-1 rounded-full', {
'bg-emerald-500': tunnel.status === 'healthy',
'bg-red-500': tunnel.status === 'down',
'bg-gray-500': tunnel.status === 'inactive',
'bg-yellow-500': tunnel.status === 'degraded',
})}
></div>
<div className={`h-9 w-1 rounded-full ${tunnelStatusDotColorClassByStatus[tunnel.status]}`}></div>

<div className="col">
<div className="text-[15px] font-medium">{tunnel.alias}</div>
Expand Down
43 changes: 43 additions & 0 deletions src/lib/tunnel-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Tunnel } from '@typedefs/tunnels';

type TunnelStatus = Tunnel['status'];
type TunnelStatusTagVariant = 'slate' | 'green' | 'yellow' | 'red';

const tunnelStatusDotColorClassByStatus: Record<TunnelStatus, string> = {
healthy: 'bg-emerald-500',
degraded: 'bg-yellow-500',
down: 'bg-red-500',
inactive: 'bg-gray-500',
};

const tunnelStatusTagVariantByStatus: Record<TunnelStatus, TunnelStatusTagVariant> = {
healthy: 'green',
degraded: 'yellow',
down: 'red',
inactive: 'slate',
Comment on lines +4 to +17
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TunnelStatusTagVariant duplicates the existing ColorVariant union exported by @shared/SmallTag. Consider typing tunnelStatusTagVariantByStatus as Record<TunnelStatus, ColorVariant> (or Extract<ColorVariant, 'slate' | 'green' | 'yellow' | 'red'>) to keep the mapping type aligned with the component’s accepted variants and avoid future drift.

Copilot uses AI. Check for mistakes.
};

const tunnelStatusSortPriority: Record<TunnelStatus, number> = {
inactive: 0,
down: 1,
degraded: 2,
healthy: 3,
};

function compareTunnelStatusAndAlias<T extends { status: TunnelStatus; alias: string }>(a: T, b: T): number {
const statusPriorityDiff = tunnelStatusSortPriority[a.status] - tunnelStatusSortPriority[b.status];

if (statusPriorityDiff !== 0) {
return statusPriorityDiff;
}

return a.alias.localeCompare(b.alias);
}

export {
compareTunnelStatusAndAlias,
tunnelStatusDotColorClassByStatus,
tunnelStatusTagVariantByStatus,
tunnelStatusSortPriority,
type TunnelStatus,
};
Loading