Skip to content

Commit d139748

Browse files
committed
Show containers with no public ports
This commit adds containers that don't expose public ports to our container view, to help users troubleshoot why they can't access those containers.
1 parent a407e30 commit d139748

5 files changed

Lines changed: 104 additions & 51 deletions

File tree

ui/src/components/dropdown-menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ function DropdownMenuItem(props: DropdownMenuItemProps) {
103103
<MenuPrimitive.Item
104104
className={cx(className, menuItemClasses, menuItemInteractiveClasses, {
105105
"text-red-400": intent === "danger",
106-
"text-gray-400 bg-white cursor-default": disabled,
106+
"text-gray-400 cursor-default": disabled,
107107
})}
108108
disabled={disabled}
109109
{...rest}

ui/src/components/icon.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ const icons: Record<string, React.FC<IconProps>> = {
109109
<line x1="12" y1="8" x2="12.01" y2="8"></line>
110110
</svg>
111111
),
112+
error: (props: IconProps) => (
113+
<svg
114+
xmlns="http://www.w3.org/2000/svg"
115+
{...props}
116+
viewBox="0 0 24 24"
117+
fill="none"
118+
stroke="currentColor"
119+
strokeWidth="2"
120+
strokeLinecap="round"
121+
strokeLinejoin="round"
122+
>
123+
<circle cx="12" cy="12" r="10"></circle>
124+
<line x1="12" y1="8" x2="12" y2="12"></line>
125+
<line x1="12" y1="16" x2="12.01" y2="16"></line>
126+
</svg>
127+
),
112128
check: (props: IconProps) => (
113129
<svg
114130
xmlns="http://www.w3.org/2000/svg"

ui/src/styles.css

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
*/
4040
background-color: var(--dd-color-background);
4141
color: var(--dd-text-color-primary);
42-
padding: var(--dd-page-padding-top, 1.5rem) var(--dd-page-padding-right, 1.5rem) var(--dd-page-padding-bottom, 1.5rem) var(--dd-page-padding-left, 1.5rem);
42+
padding: var(--dd-page-padding-top, 1.5rem)
43+
var(--dd-page-padding-right, 1.5rem) var(--dd-page-padding-bottom, 1.5rem)
44+
var(--dd-page-padding-left, 1.5rem);
4345
}
4446
}
4547

@@ -165,6 +167,14 @@
165167
opacity: 0.2;
166168
}
167169
}
170+
171+
.help-text {
172+
@apply text-gray-500 dark:text-gray-400;
173+
}
174+
175+
.code {
176+
@apply font-mono bg-gray-800/5 dark:bg-white/5 px-1.5 py-0.5 rounded-lg;
177+
}
168178
}
169179

170180
@layer utilities {

ui/src/tailscale.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export type State = {
3030
*/
3131
initialized: boolean
3232
backendState: BackendState
33+
/**
34+
* containers is the list of Docker containers currently running. This list
35+
* filters out all extension containers, but includes all containers that
36+
* are running, including those that don't expose public ports. Be sure to
37+
* filter down to containers with public ports before offering URLs to users.
38+
*/
3339
containers: Container[]
3440
hostname: string
3541
hostStatus: HostStatus
@@ -174,8 +180,6 @@ const useTailscale = create<State>((set, get) => ({
174180
(await ddClient.docker.listContainers()) as Container[]
175181
set((prev) => {
176182
const containers = allContainers
177-
// only show containers that expose a public port
178-
.filter((c) => c.Ports.some((p) => p.PublicPort))
179183
// only show non-extension containers
180184
.filter((c) => c.Labels["com.docker.desktop.plugin"] === undefined)
181185
if (shallow(prev.containers, containers)) {

ui/src/views/container-view.tsx

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -188,31 +188,31 @@ function ContainerTable() {
188188
<>
189189
<HostWarning />
190190
{containers.length > 0 ? (
191-
<table className="w-full text-left">
192-
<thead>
193-
<tr>
194-
<th className={cx(tableHeaderClass, "w-1/3")}>Container</th>
195-
<th className={cx(tableHeaderClass, "w-1/2")}>Tailscale URL</th>
196-
<th className={tableHeaderClass} />
197-
</tr>
198-
</thead>
199-
<tbody>
200-
{containers.map((c) => (
201-
<ContainerRow
202-
key={c.Id}
203-
container={c}
204-
hostIP={hostIP}
205-
host={host}
206-
/>
207-
))}
208-
</tbody>
209-
</table>
191+
<>
192+
<table className="w-full text-left">
193+
<thead>
194+
<tr>
195+
<th className={cx(tableHeaderClass, "w-1/3")}>Container</th>
196+
<th className={cx(tableHeaderClass, "w-1/2")}>Tailscale URL</th>
197+
<th className={tableHeaderClass} />
198+
</tr>
199+
</thead>
200+
<tbody>
201+
{containers.map((c) => (
202+
<ContainerRow
203+
key={c.Id}
204+
container={c}
205+
hostIP={hostIP}
206+
host={host}
207+
/>
208+
))}
209+
</tbody>
210+
</table>
211+
</>
210212
) : (
211213
<div className="text-center py-12">
212214
<p className="text-xl font-medium mb-1">No containers are running.</p>
213-
<p className="text-gray-500 dark:text-gray-400">
214-
Go to the Containers tab to get started.
215-
</p>
215+
<p className="help-text">Go to the Containers tab to get started.</p>
216216
</div>
217217
)}
218218
</>
@@ -233,6 +233,9 @@ function ContainerRow(props: {
233233
const tailscaleURL = `http://${tailscaleIPPort}/`
234234
const tailscaleIPUrl = `http://${hostIP}:${publicPort?.PublicPort}/`.trim()
235235

236+
const hasPublicPorts = container.Ports.some((p) => p.PublicPort)
237+
const online = container.State === "running" && hasPublicPorts
238+
236239
const handleCopyClick = useCallback(() => {
237240
copyToClipboard(tailscaleURL)
238241
setShowTooltip(true)
@@ -256,9 +259,8 @@ function ContainerRow(props: {
256259
<td className={cx(tableCellClass, "flex items-center")}>
257260
<Icon
258261
className={cx("mr-3", {
259-
"text-emerald-400 dark:text-green-300":
260-
container.State === "running",
261-
"text-gray-600": container.State !== "running",
262+
"text-emerald-400 dark:text-green-300": online,
263+
"text-gray-400 dark:text-gray-600": !online,
262264
})}
263265
name="container"
264266
size="24"
@@ -268,31 +270,49 @@ function ContainerRow(props: {
268270
</span>
269271
</td>
270272
<td className={cx(tableCellClass, "min-w-0")}>
271-
<Tooltip
272-
asChild
273-
content={
274-
copied || persistTooltipCopy ? "Copied!" : "Copy URL to clipboard"
275-
}
276-
closeOnClick={false}
277-
open={showTooltip || copied}
278-
onOpenChange={setShowTooltip}
279-
>
280-
<button
281-
className={cx(tableButtonClass, "flex items-center min-w-0")}
282-
onClick={handleCopyClick}
273+
{hasPublicPorts ? (
274+
<Tooltip
275+
asChild
276+
content={
277+
copied || persistTooltipCopy ? "Copied!" : "Copy URL to clipboard"
278+
}
279+
closeOnClick={false}
280+
open={showTooltip || copied}
281+
onOpenChange={setShowTooltip}
283282
>
284-
<span className="truncate">{tailscaleIPPort}</span>
285-
<Icon
286-
className="ml-1.5 text-gray-500 dark:text-gray-400"
287-
name={copied ? "check" : "clipboard"}
288-
size="14"
289-
/>
290-
</button>
291-
</Tooltip>
283+
<button
284+
className={cx(tableButtonClass, "flex items-center min-w-0")}
285+
onClick={handleCopyClick}
286+
>
287+
<span className="truncate">{tailscaleIPPort}</span>
288+
<Icon
289+
className="ml-1.5 text-gray-500 dark:text-gray-400"
290+
name={copied ? "check" : "clipboard"}
291+
size="14"
292+
/>
293+
</button>
294+
</Tooltip>
295+
) : (
296+
<Tooltip
297+
content={
298+
<>
299+
This container has no public ports. Expose ports using{" "}
300+
<code className="code">docker run</code> with the{" "}
301+
<code className="code">-p</code> flag.
302+
</>
303+
}
304+
>
305+
<div className="flex items-center min-w-0 help-text">
306+
<Icon name="error" size="14" />
307+
<span className="truncate ml-1.5">No public ports</span>
308+
</div>
309+
</Tooltip>
310+
)}
292311
</td>
293312
<td className={cx("space-x-3 text-right", tableButtonCellClass)}>
294313
<Tooltip asChild content="Open URL in browser">
295314
<button
315+
disabled={!online}
296316
className={cx(tableIconButtonClass)}
297317
onClick={() => openBrowser(tailscaleURL)}
298318
>
@@ -307,7 +327,10 @@ function ContainerRow(props: {
307327
</button>
308328
}
309329
>
310-
<DropdownMenu.Item onSelect={() => copyToClipboard(tailscaleIPUrl)}>
330+
<DropdownMenu.Item
331+
disabled={!online}
332+
onSelect={() => copyToClipboard(tailscaleIPUrl)}
333+
>
311334
Copy IP address
312335
</DropdownMenu.Item>
313336
<DropdownMenu.Separator />
@@ -332,7 +355,7 @@ const tableHeaderClass = cx(
332355
const tableCellClass = cx(tablePadding, borderColor)
333356
const tableButtonCellClass = cx("px-2", borderColor)
334357
const tableIconButtonClass =
335-
"text-gray-600 dark:text-gray-300 focus:outline-none hover:bg-[rgba(31,41,55,0.05)] dark:hover:bg-[rgba(255,255,255,0.05)] focus-visible:bg-[rgba(31,41,55,0.05)] dark:focus-visible:bg-[rgba(255,255,255,0.05)] px-2 py-2 rounded"
358+
"text-gray-600 dark:text-gray-300 focus:outline-none enabled:hover:bg-[rgba(31,41,55,0.05)] enabled:dark:hover:bg-[rgba(255,255,255,0.05)] focus-visible:bg-[rgba(31,41,55,0.05)] dark:focus-visible:bg-[rgba(255,255,255,0.05)] px-2 py-2 rounded disabled:opacity-50"
336359
const tableButtonClass = "focus:outline-none focus-visible:ring"
337360

338361
const hostWarningSelector = (state: State) => ({

0 commit comments

Comments
 (0)