Skip to content

feat: container port links and overview#457

Merged
kmendell merged 1 commit intomainfrom
feat/port-links
Sep 13, 2025
Merged

feat: container port links and overview#457
kmendell merged 1 commit intomainfrom
feat/port-links

Conversation

@kmendell
Copy link
Member

@kmendell kmendell commented Sep 13, 2025

Fixes: #379
Fixes: #382

Summary by CodeRabbit

  • New Features

    • Added clickable port badges to container details and the containers table, with links built from the server base URL.
    • Introduced a “Ports” column in the containers list.
  • Refactor

    • Unified port display across pages for consistency and clarity.
    • Improved page load performance by fetching containers, stats, and settings in parallel.
    • Removed the port-count badge from the Compose services list.
  • Localization

    • Simplified port labels by introducing “Ports” and removing older singular/plural keys.

@kmendell
Copy link
Member Author

@coderabbitait review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 13, 2025

@kmendell I'll review the changes in this PR for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 13, 2025

📝 Walkthrough

Walkthrough

Implements a new PortBadges Svelte component and integrates it into container list and detail pages, adds settings loading to provide a baseServerUrl, updates i18n keys for ports, and removes the old compose ports count badge.

Changes

Cohort / File(s) Summary of changes
i18n keys
frontend/messages/en.json
Removed compose_port_label, compose_ports_label_one, compose_ports_label_many; added ports = "Ports".
Port badges component
frontend/src/lib/components/port-badges.svelte
New component rendering published port badges from ports with links derived from baseServerUrl (or window origin). Normalizes, filters, dedupes, sorts ports; shows “containers_not_published” when none. Props: ports?, baseServerUrl?.
Containers data loading
frontend/src/routes/containers/+page.ts, frontend/src/routes/containers/[containerId]/+page.ts
Fetch settings via settingsAPI.getSettings() in parallel with existing data using Promise.all; return settings in load output.
Containers UI integration
frontend/src/routes/containers/+page.svelte, frontend/src/routes/containers/container-table.svelte, frontend/src/routes/containers/[containerId]/+page.svelte
Derive baseServerUrl from data.settings and pass to child components. Add ports column using PortBadges in container table. Replace per-port rendering in container detail with PortBadges. Expose baseServerUrl prop on container table.
Compose services list
frontend/src/routes/compose/[projectId]/+page.svelte
Removed ports count badge rendering for services.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Pre-merge checks

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The diff includes edits that appear unrelated to the linked issues' objectives: three translation keys were removed and replaced/added in frontend/messages/en.json and the port-count badge was removed from the compose service list (frontend/src/routes/compose/[projectId]/+page.svelte), changes which affect compose UI rather than container listing or clickable host-port links. In addition, exporting settingsAPI from $lib/services/api and switching load functions to Promise.all (which changes error-timing semantics) alter module surface and behavior beyond the feature scope. These unrelated edits increase review scope and risk unintended regressions. Isolate or justify these unrelated changes: revert the compose-related deletions or move them to a separate PR with an explanation, document and communicate the intentional public API change for settingsAPI, and add notes/tests about the Promise.all error behavior change so reviewers can assess risk and compatibility.
Linked Issues Check ❓ Inconclusive The PR implements a new PortBadges component and adds a "ports" column to the Containers overview (frontend/src/routes/containers/container-table.svelte) while passing a derived baseServerUrl from settings (containers/+page.svelte and +page.ts), which enables constructing links to host:port and surfaces ports in-list as requested by issues #379 and #382. However, the provided summaries indicate PortBadges filters to publicly published ports only and do not confirm that the anchor elements explicitly open a new browser tab (target="_blank" and appropriate rel attributes), and they do not confirm whether non-published/private-only mappings are presented when required. Because the linked issues explicitly require opening links in a new tab and at-a-glance visibility of mappings, I cannot conclusively verify full compliance from the summaries alone. Please confirm that PortBadges renders anchors with target="_blank" and rel="noopener noreferrer" (or equivalent) to satisfy the "open in a new tab" requirement, and clarify whether private/non-published port mappings should also be displayed or intentionally omitted so the component matches the exact expectations in #379 and #382.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: container port links and overview" is concise and accurately reflects the primary change set: adding an overview of container ports and making host ports linkable, and it uses the conventional "feat:" prefix without noisy file lists or vague terms. This gives a reviewer quick, correct context about the main intention of the PR.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (8)
frontend/src/routes/containers/+page.svelte (1)

27-30: Deduplicate baseServerUrl derivation via a helper.

This precedence chain is duplicated in multiple files. Centralize it (e.g., getBaseServerUrl(settings) in a small util) to avoid drift.

Example helper:

// $lib/utils/url.util.ts
export function getBaseServerUrl(s: any): string {
  return s?.serverBaseUrl ?? s?.baseServerUrl ?? s?.baseUrl ?? '';
}

Usage here:

-const baseServerUrl = $derived(
-  (data.settings as any)?.serverBaseUrl ?? (data.settings as any)?.baseServerUrl ?? (data.settings as any)?.baseUrl ?? ''
-);
+import { getBaseServerUrl } from '$lib/utils/url.util';
+const baseServerUrl = $derived(getBaseServerUrl(data.settings as any));
frontend/src/routes/containers/container-table.svelte (1)

26-38: Make baseServerUrl optional with a safe default.

Avoid forcing all callers to pass it. Defaulting to '' keeps behavior (falls back to window origin in PortBadges).

-  requestOptions = $bindable(),
-  baseServerUrl = $bindable()
+  requestOptions = $bindable(),
+  baseServerUrl = $bindable('')
 }: {
   containers: Paginated<ContainerSummaryDto>;
   selectedIds: string[];
   requestOptions: SearchPaginationSortRequest;
-  baseServerUrl: string;
+  baseServerUrl?: string;
 } = $props();
frontend/src/routes/containers/[containerId]/+page.svelte (2)

269-271: Reuse a shared helper for baseServerUrl.

Mirror the containers list page suggestion to avoid duplicated precedence logic.

+import { getBaseServerUrl } from '$lib/utils/url.util';
-const baseServerUrl = $derived(
-  (data?.settings as any)?.serverBaseUrl ?? (data?.settings as any)?.baseServerUrl ?? (data?.settings as any)?.baseUrl ?? ''
-);
+const baseServerUrl = $derived(getBaseServerUrl(data?.settings as any));

368-431: Also surface ports in the Overview card to match the “at-a-glance” objective.

Add a Ports row under Overview so users don’t need to jump to Config.

         <div class="space-y-4">
@@
           <div class="flex items-center gap-3">
             <div class="rounded bg-amber-50 p-2 dark:bg-amber-950/20">
               <TerminalIcon class="size-4 text-amber-600" />
             </div>
             <div class="min-w-0 flex-1">
               <div class="text-muted-foreground text-sm">{m.containers_command()}</div>
               <div class="truncate font-medium" title={container.config?.cmd?.join(' ')}>
                 {container.config?.cmd?.join(' ') || m.common_na()}
               </div>
             </div>
           </div>
+          <div class="flex items-center gap-3">
+            <div class="rounded bg-sky-50 p-2 dark:bg-sky-950/20">
+              <NetworkIcon class="size-4 text-sky-600" />
+            </div>
+            <div class="min-w-0 flex-1">
+              <div class="text-muted-foreground text-sm">{m.ports()}</div>
+              <div class="font-medium">
+                <PortBadges ports={container.ports ?? []} {baseServerUrl} />
+              </div>
+            </div>
+          </div>
         </div>
frontend/src/lib/components/port-badges.svelte (4)

10-16: Normalize protocol to ‘tcp’ by default and avoid any leakage.

Default unknown protocol to tcp (Docker’s default) for consistent display/dedupe; narrow proto to string.

 type NormalizedPort = {
   hostPort: string;
   containerPort: string;
-  proto?: string;
+  proto: string;
   ip?: string | null;
 };
@@
-function getProto(p: PortDto): string | undefined {
-  return (p as any).type ?? (p as any).protocol ?? undefined;
-}
+function getProto(p: PortDto): string {
+  const v = (p as any).type ?? (p as any).protocol ?? 'tcp';
+  return String(v).toLowerCase();
+}
@@
   return {
     hostPort,
     containerPort: getPrivatePort(p),
-    proto: getProto(p),
+    proto: getProto(p),
     ip: (p as any).ip ?? null
   };

Also applies to: 25-27, 29-38


40-53: Fix sort for non-numeric ports and add protocol tiebreaker.

Current Number(...) can yield NaN, producing unstable ordering.

-function uniquePublished(list: PortDto[]): NormalizedPort[] {
+function uniquePublished(list: PortDto[]): NormalizedPort[] {
   const map = new Map<string, NormalizedPort>();
   for (const p of list) {
     const n = normalize(p);
     if (!n) continue;
-    const key = `${n.hostPort}:${n.containerPort}/${n.proto ?? ''}`;
+    const key = `${n.hostPort}:${n.containerPort}/${n.proto}`;
     if (!map.has(key)) map.set(key, n);
   }
-  return Array.from(map.values()).sort((a, b) => {
-    const hp = Number(a.hostPort) - Number(b.hostPort);
-    if (hp !== 0) return hp;
-    return Number(a.containerPort) - Number(b.containerPort);
-  });
+  const isNumeric = (s: string) => /^\d+$/.test(s);
+  return Array.from(map.values()).sort((a, b) => {
+    const hp = isNumeric(a.hostPort) && isNumeric(b.hostPort)
+      ? Number(a.hostPort) - Number(b.hostPort)
+      : a.hostPort.localeCompare(b.hostPort, undefined, { numeric: true });
+    if (hp !== 0) return hp;
+    const cp = isNumeric(a.containerPort) && isNumeric(b.containerPort)
+      ? Number(a.containerPort) - Number(b.containerPort)
+      : a.containerPort.localeCompare(b.containerPort, undefined, { numeric: true });
+    if (cp !== 0) return cp;
+    return a.proto.localeCompare(b.proto);
+  });
 }

55-64: Preserve origin scheme but guard against base with paths.

Optional: strip path/query from baseServerUrl to avoid unexpected /foo paths in links.

-const url = new URL(base.startsWith('http') ? base : `http://${base}`);
+const url = new URL(base.startsWith('http') ? base : `http://${base}`);
+url.pathname = '/';
+url.search = '';
+url.hash = '';

69-87: Don’t hyperlink non-TCP ports; visually indicate disabled.

Clicking UDP/SCTP yields meaningless HTTP requests. Keep badge, disable link.

-      <a
-        class="ring-offset-background focus-visible:ring-ring bg-background/70 inline-flex items-center gap-1 rounded-lg border border-sky-700/20 px-2 py-1 text-[11px] shadow-sm transition-colors transition-shadow hover:border-sky-700/40 hover:bg-sky-500/10 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
-        href={toHref(p.hostPort)}
-        target="_blank"
-        rel="noopener noreferrer"
-        title={`${p.ip ?? '0.0.0.0'}:${p.hostPort} → ${p.containerPort}${p.proto ? `/${p.proto}` : ''}`}
-      >
+      <a
+        class="ring-offset-background focus-visible:ring-ring bg-background/70 inline-flex items-center gap-1 rounded-lg border border-sky-700/20 px-2 py-1 text-[11px] shadow-sm transition-colors transition-shadow hover:border-sky-700/40 hover:bg-sky-500/10 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 {p.proto !== 'tcp' ? 'pointer-events-none opacity-60' : ''}"
+        href={p.proto === 'tcp' ? toHref(p.hostPort) : '#'}
+        aria-disabled={p.proto !== 'tcp'}
+        target="_blank"
+        rel="noopener noreferrer"
+        title={`${p.ip ?? '0.0.0.0'}:${p.hostPort} → ${p.containerPort}${p.proto ? `/${p.proto}` : ''}`}
+      >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0b43b6 and 91e6507.

📒 Files selected for processing (8)
  • frontend/messages/en.json (1 hunks)
  • frontend/src/lib/components/port-badges.svelte (1 hunks)
  • frontend/src/routes/compose/[projectId]/+page.svelte (0 hunks)
  • frontend/src/routes/containers/+page.svelte (2 hunks)
  • frontend/src/routes/containers/+page.ts (1 hunks)
  • frontend/src/routes/containers/[containerId]/+page.svelte (3 hunks)
  • frontend/src/routes/containers/[containerId]/+page.ts (1 hunks)
  • frontend/src/routes/containers/container-table.svelte (2 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/routes/compose/[projectId]/+page.svelte
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-09T21:42:07.222Z
Learnt from: CR
PR: ofkm/arcane#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-09T21:42:07.222Z
Learning: Applies to frontend/src/routes/**/+page.svelte : Implement page components using +page.svelte within routes

Applied to files:

  • frontend/src/routes/containers/[containerId]/+page.svelte
  • frontend/src/routes/containers/+page.svelte
🔇 Additional comments (8)
frontend/src/routes/containers/+page.svelte (1)

112-112: Propagate baseServerUrl to the table — LGTM.

frontend/src/routes/containers/container-table.svelte (2)

134-136: New “Ports” column — LGTM.

Good placement and non-sortable as expected.


139-142: Ports cell integration — LGTM.

Delegating to PortBadges keeps the table lean.

frontend/src/routes/containers/[containerId]/+page.ts (1)

20-23: Return shape extension — LGTM.

Downstream code now reading data.settings is consistent with this.

frontend/src/routes/containers/[containerId]/+page.svelte (2)

29-29: Import PortBadges — LGTM.


713-713: Config section PortBadges — LGTM.

frontend/src/routes/containers/+page.ts (1)

7-9: Pagination/sort defaults — LGTM.

frontend/src/lib/components/port-badges.svelte (1)

66-67: Derived store usage — LGTM.

Simple and efficient computation of published ports.

@kmendell kmendell merged commit 154107d into main Sep 13, 2025
4 checks passed
@kmendell kmendell deleted the feat/port-links branch September 13, 2025 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⚡️ Feature: Show container ports in the Container List ⚡️ Feature: Port Overview and click port for a redirect to that port

1 participant