File size: 4,435 Bytes
bf4f454
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<script lang="ts">
	import { onDestroy, getContext } from 'svelte';
	import type { ListeningPort } from '$lib/apis/terminal';
	import { getListeningPorts, getPortProxyUrl } from '$lib/apis/terminal';
	import Tooltip from '$lib/components/common/Tooltip.svelte';

	const i18n = getContext('i18n');

	export let baseUrl: string;
	export let apiKey: string;

	let ports: ListeningPort[] = [];
	let expanded = true;
	let loading = false;
	let pollTimer: ReturnType<typeof setInterval> | null = null;

	const loadPorts = async () => {
		loading = true;
		ports = await getListeningPorts(baseUrl, apiKey);
		loading = false;
	};

	const startPolling = () => {
		stopPolling();
		loadPorts();
		pollTimer = setInterval(loadPorts, 5000);
	};

	const stopPolling = () => {
		if (pollTimer) {
			clearInterval(pollTimer);
			pollTimer = null;
		}
	};

	const openPort = (port: number) => {
		const url = getPortProxyUrl(baseUrl, port);
		window.open(url, '_blank', 'noopener,noreferrer');
	};

	// Start polling when baseUrl is available
	$: if (baseUrl) {
		startPolling();
	}

	onDestroy(() => {
		stopPolling();
	});
</script>

<div class="px-2 py-1">
	<button
		class="flex items-center gap-1 w-full text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition"
		on:click={() => (expanded = !expanded)}
	>
		<svg
			xmlns="http://www.w3.org/2000/svg"
			viewBox="0 0 20 20"
			fill="currentColor"
			class="size-3 transition-transform {expanded ? '' : '-rotate-90'}"
		>
			<path
				fill-rule="evenodd"
				d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
				clip-rule="evenodd"
			/>
		</svg>
		{$i18n.t('Ports')}
		<span class="ml-auto flex items-center gap-1">
		{#if ports.length > 0}
			<span
				class="text-[10px] px-1.5 py-0.5 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400"
			>
				{ports.length}
			</span>
		{/if}
		<Tooltip content={$i18n.t('Refresh')}>
			<button
				class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
				on:click|stopPropagation={loadPorts}
				aria-label={$i18n.t('Refresh')}
			>
				<svg
					xmlns="http://www.w3.org/2000/svg"
					viewBox="0 0 20 20"
					fill="currentColor"
					class="size-3 {loading ? 'animate-spin' : ''}"
				>
					<path
						fill-rule="evenodd"
						d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.451a.75.75 0 0 0 0-1.5H4.5a.75.75 0 0 0-.75.75v3.75a.75.75 0 0 0 1.5 0v-2.127l.13.13a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.624-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.75a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 .75-.75V3.42a.75.75 0 0 0-1.5 0v2.126l-.13-.129A7 7 0 0 0 3.239 8.555a.75.75 0 0 0 1.449.39Z"
						clip-rule="evenodd"
					/>
				</svg>
			</button>
		</Tooltip>
		</span>
	</button>

	{#if expanded}
		<div class="mt-1 space-y-0.5 max-h-[150px] overflow-y-auto">
			{#if ports.length === 0}
				<div class="text-xs text-gray-400 dark:text-gray-500 px-1 py-1">
					{$i18n.t('No servers detected')}
				</div>
			{:else}
				{#each ports as port}
					<button
						class="flex items-center w-full gap-2 px-1.5 py-1 text-xs rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition group"
						on:click={() => openPort(port.port)}
					>
						<span class="font-mono text-blue-500 dark:text-blue-400 shrink-0">
							:{port.port}
						</span>
						<span
							class="text-gray-500 dark:text-gray-400 truncate flex-1 text-left"
						>
							{port.process ?? ''}
						</span>
						<span
							class="text-gray-400 dark:text-gray-500 opacity-0 group-hover:opacity-100 transition shrink-0"
						>
							<svg
								xmlns="http://www.w3.org/2000/svg"
								viewBox="0 0 20 20"
								fill="currentColor"
								class="size-3"
							>
								<path
									fill-rule="evenodd"
									d="M4.25 5.5a.75.75 0 0 0-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 0 0 .75-.75v-4a.75.75 0 0 1 1.5 0v4A2.25 2.25 0 0 1 12.75 17h-8.5A2.25 2.25 0 0 1 2 14.75v-8.5A2.25 2.25 0 0 1 4.25 4h5a.75.75 0 0 1 0 1.5h-5Zm7.5-3.5a.75.75 0 0 0 0 1.5h2.69l-4.72 4.72a.75.75 0 0 0 1.06 1.06l4.72-4.72v2.69a.75.75 0 0 0 1.5 0v-5.25a.75.75 0 0 0-.75-.75h-5.25Z"
									clip-rule="evenodd"
								/>
							</svg>
						</span>
					</button>
				{/each}
			{/if}
		</div>
	{/if}
</div>