From 967e5d0328d459234e1c592fea616a96880f248c Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Thu, 12 Feb 2026 11:12:31 -0500 Subject: [PATCH 1/3] feat: sort lists by name and simplify status bar command display Sort all list views alphabetically by first column (name/repository). Refactor status bar to show highlighted shortcut letters within command names using yellow underline. Apply same highlighting to global actions on the right side. Show 'hjkl:nav' as plain dim text. Change stop shortcut from 'x' to 'o' (stop). Rename 'n:run' to 'c:create'. Completed GitHub issue #14 Co-Authored-By: Claude --- progress.txt | 6 ++ src/__tests__/sorting.test.tsx | 103 ++++++++++++++++++++++++++++++ src/__tests__/status-bar.test.tsx | 97 +++++++++++++++++++++++++++- src/components/App.tsx | 23 ++++--- src/components/ContainersView.tsx | 16 +++-- src/components/HelpOverlay.tsx | 4 +- src/components/ImagesView.tsx | 14 ++-- src/components/NetworksView.tsx | 14 ++-- src/components/StatusBar.tsx | 91 ++++++++++++++++++++++---- src/components/VolumesView.tsx | 14 ++-- src/hooks/useKeyboard.ts | 5 -- 11 files changed, 330 insertions(+), 57 deletions(-) create mode 100644 src/__tests__/sorting.test.tsx diff --git a/progress.txt b/progress.txt index 6266dc8..b9ab81f 100644 --- a/progress.txt +++ b/progress.txt @@ -40,3 +40,9 @@ Each entry documents: date, feature, decisions, files changed, tests, and concer - Changes: Added `runContainer` CLI method with `buildRunArgs` helper, multi-field RunContainerDialog (image, name, ports, env), `n` shortcut on Containers/Images tabs with image pre-fill from Images tab - Decisions: Exported `buildRunArgs` as standalone function for testability; used Tab key to cycle between fields in dialog; comma-separated ports/env input - Issues: None + +[2026-02-12] #14 feat: Sort lists by name and simplify status bar command display +- Files: src/components/ContainersView.tsx, ImagesView.tsx, NetworksView.tsx, VolumesView.tsx, StatusBar.tsx, HelpOverlay.tsx, src/hooks/useKeyboard.ts, src/components/App.tsx, src/__tests__/sorting.test.tsx, src/__tests__/status-bar.test.tsx +- Changes: Added alphabetical sorting to all list views, refactored status bar to use highlighted shortcut format (inverse text), renamed `n:run` to `c:create` across keyboard handler, action handler, and help overlay +- Decisions: Used `` for shortcut highlighting; merged "run" and "create" actions under single "create" action routed by active tab; label "exit" for stop action to embed "x" shortcut +- Issues: None diff --git a/src/__tests__/sorting.test.tsx b/src/__tests__/sorting.test.tsx new file mode 100644 index 0000000..e10dd42 --- /dev/null +++ b/src/__tests__/sorting.test.tsx @@ -0,0 +1,103 @@ +import { render } from "ink-testing-library"; +import { ContainersView } from "../components/ContainersView.js"; +import { ImagesView } from "../components/ImagesView.js"; +import { NetworksView } from "../components/NetworksView.js"; +import { VolumesView } from "../components/VolumesView.js"; +import type { Container, Image, Network, Volume } from "../types/index.js"; + +function makeContainer(name: string): Container { + return { + id: name, + name, + image: "nginx:latest", + status: "running", + state: "running", + ports: [], + created: "2024-01-01", + }; +} + +function makeImage(repository: string): Image { + return { + id: repository, + repository, + tag: "latest", + size: "100MB", + created: "2024-01-01", + reference: `${repository}:latest`, + }; +} + +function makeNetwork(name: string): Network { + return { id: name, name, driver: "bridge", scope: "local" }; +} + +function makeVolume(name: string): Volume { + return { name, driver: "local", mountpoint: `/var/${name}`, scope: "local" }; +} + +describe("List sorting", () => { + it("sorts containers alphabetically by name", () => { + const containers = [makeContainer("zebra"), makeContainer("alpha"), makeContainer("mango")]; + const { lastFrame } = render( + + ); + const frame = lastFrame() ?? ""; + const alphaIdx = frame.indexOf("alpha"); + const mangoIdx = frame.indexOf("mango"); + const zebraIdx = frame.indexOf("zebra"); + expect(alphaIdx).toBeLessThan(mangoIdx); + expect(mangoIdx).toBeLessThan(zebraIdx); + }); + + it("sorts images alphabetically by repository", () => { + const images = [makeImage("zookeeper"), makeImage("alpine"), makeImage("nginx")]; + const { lastFrame } = render( + + ); + const frame = lastFrame() ?? ""; + const alpineIdx = frame.indexOf("alpine"); + const nginxIdx = frame.indexOf("nginx"); + const zookeeperIdx = frame.indexOf("zookeeper"); + expect(alpineIdx).toBeLessThan(nginxIdx); + expect(nginxIdx).toBeLessThan(zookeeperIdx); + }); + + it("sorts networks alphabetically by name", () => { + const networks = [makeNetwork("zoo-net"), makeNetwork("app-net"), makeNetwork("db-net")]; + const { lastFrame } = render( + + ); + const frame = lastFrame() ?? ""; + const appIdx = frame.indexOf("app-net"); + const dbIdx = frame.indexOf("db-net"); + const zooIdx = frame.indexOf("zoo-net"); + expect(appIdx).toBeLessThan(dbIdx); + expect(dbIdx).toBeLessThan(zooIdx); + }); + + it("sorts volumes alphabetically by name", () => { + const volumes = [makeVolume("z-vol"), makeVolume("a-vol"), makeVolume("m-vol")]; + const { lastFrame } = render( + + ); + const frame = lastFrame() ?? ""; + const aIdx = frame.indexOf("a-vol"); + const mIdx = frame.indexOf("m-vol"); + const zIdx = frame.indexOf("z-vol"); + expect(aIdx).toBeLessThan(mIdx); + expect(mIdx).toBeLessThan(zIdx); + }); + + it("sorts containers after filtering", () => { + const containers = [makeContainer("zebra-app"), makeContainer("alpha-app"), makeContainer("no-match")]; + const { lastFrame } = render( + + ); + const frame = lastFrame() ?? ""; + expect(frame).not.toContain("no-match"); + const alphaIdx = frame.indexOf("alpha-app"); + const zebraIdx = frame.indexOf("zebra-app"); + expect(alphaIdx).toBeLessThan(zebraIdx); + }); +}); diff --git a/src/__tests__/status-bar.test.tsx b/src/__tests__/status-bar.test.tsx index 77640f6..39ba3c7 100644 --- a/src/__tests__/status-bar.test.tsx +++ b/src/__tests__/status-bar.test.tsx @@ -1,5 +1,5 @@ import { render } from "ink-testing-library"; -import { StatusBar } from "../components/StatusBar.js"; +import { StatusBar, renderAction } from "../components/StatusBar.js"; import type { ReleaseCheckState } from "../hooks/useReleaseCheck.js"; describe("StatusBar", () => { @@ -30,6 +30,99 @@ describe("StatusBar", () => { /> ); - expect(lastFrame()).toContain("h/l:tabs"); + const frame = lastFrame() ?? ""; + expect(frame).toContain("hjkl:nav"); + expect(frame).toContain("search"); + expect(frame).toContain("quit"); + }); + + it("displays action labels with highlighted shortcut for containers tab", () => { + const { lastFrame } = render( + + ); + + const frame = lastFrame() ?? ""; + expect(frame).toContain("create"); + expect(frame).toContain("edit"); + expect(frame).toContain("start"); + expect(frame).toContain("stop"); + expect(frame).toContain("Restart"); + expect(frame).toContain("delete"); + expect(frame).toContain("Logs"); + expect(frame).toContain("inspect"); + // Should NOT contain old format + expect(frame).not.toContain("n:run"); + expect(frame).not.toContain("s:start"); + }); + + it("displays action labels for images tab", () => { + const { lastFrame } = render( + + ); + + const frame = lastFrame() ?? ""; + expect(frame).toContain("create"); + expect(frame).toContain("pull"); + expect(frame).toContain("delete"); + expect(frame).toContain("inspect"); + }); + + it("displays action labels for networks tab", () => { + const { lastFrame } = render( + + ); + + const frame = lastFrame() ?? ""; + expect(frame).toContain("create"); + expect(frame).toContain("delete"); + expect(frame).toContain("inspect"); + }); + + it("displays action labels for volumes tab", () => { + const { lastFrame } = render( + + ); + + const frame = lastFrame() ?? ""; + expect(frame).toContain("create"); + expect(frame).toContain("delete"); + expect(frame).toContain("inspect"); + }); +}); + +describe("renderAction", () => { + it("highlights the shortcut character in a label", () => { + const { lastFrame } = render( + renderAction({ key: "c", label: "create" }) + ); + // The rendered output should contain "create" with 'c' highlighted (yellow underline) + expect(lastFrame()).toContain("reate"); + }); + + it("handles mid-word shortcut character", () => { + const { lastFrame } = render( + renderAction({ key: "x", label: "exit" }) + ); + expect(lastFrame()).toContain("e"); + expect(lastFrame()).toContain("it"); + }); + + it("handles uppercase shortcut character", () => { + const { lastFrame } = render( + renderAction({ key: "R", label: "Restart" }) + ); + expect(lastFrame()).toContain("estart"); }); }); diff --git a/src/components/App.tsx b/src/components/App.tsx index 067abbc..e9aa659 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -251,18 +251,17 @@ export function App(): React.ReactElement { return; } - if (action === "create" && (activeTab === "networks" || activeTab === "volumes")) { - setDialog({ - type: "create", - createType: activeTab === "networks" ? "network" : "volume", - }); - return; - } - - if (action === "run" && (activeTab === "containers" || activeTab === "images")) { - const image = - activeTab === "images" ? images[selectedIndex]?.reference : undefined; - setDialog({ type: "run", initialValues: image ? { image } : undefined }); + if (action === "create") { + if (activeTab === "networks" || activeTab === "volumes") { + setDialog({ + type: "create", + createType: activeTab === "networks" ? "network" : "volume", + }); + } else if (activeTab === "containers" || activeTab === "images") { + const image = + activeTab === "images" ? images[selectedIndex]?.reference : undefined; + setDialog({ type: "run", initialValues: image ? { image } : undefined }); + } return; } diff --git a/src/components/ContainersView.tsx b/src/components/ContainersView.tsx index d1c004c..1bb8cd5 100644 --- a/src/components/ContainersView.tsx +++ b/src/components/ContainersView.tsx @@ -34,13 +34,15 @@ export function ContainersView({ selectedIndex, searchQuery, }: ContainersViewProps): React.ReactElement { - const filteredContainers = containers.filter( - (c) => - !searchQuery || - c.name.toLowerCase().includes(searchQuery.toLowerCase()) || - c.image.toLowerCase().includes(searchQuery.toLowerCase()) || - c.status.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const filteredContainers = containers + .filter( + (c) => + !searchQuery || + c.name.toLowerCase().includes(searchQuery.toLowerCase()) || + c.image.toLowerCase().includes(searchQuery.toLowerCase()) || + c.status.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => a.name.localeCompare(b.name)); const columns = [ { key: "name", header: "NAME", width: 25 }, diff --git a/src/components/HelpOverlay.tsx b/src/components/HelpOverlay.tsx index 8ffe478..b4cea2a 100644 --- a/src/components/HelpOverlay.tsx +++ b/src/components/HelpOverlay.tsx @@ -19,7 +19,7 @@ const HELP_SECTIONS = [ { title: "Container Actions", items: [ - { key: "n", desc: "Run new container" }, + { key: "c", desc: "Create new container" }, { key: "e", desc: "Edit container" }, { key: "s", desc: "Start container" }, { key: "x", desc: "Stop container" }, @@ -32,7 +32,7 @@ const HELP_SECTIONS = [ { title: "Image Actions", items: [ - { key: "n", desc: "Run container from image" }, + { key: "c", desc: "Create container from image" }, { key: "p", desc: "Pull image" }, { key: "d", desc: "Delete image" }, { key: "i", desc: "Inspect image" }, diff --git a/src/components/ImagesView.tsx b/src/components/ImagesView.tsx index 64be0b8..3af8100 100644 --- a/src/components/ImagesView.tsx +++ b/src/components/ImagesView.tsx @@ -14,12 +14,14 @@ export function ImagesView({ selectedIndex, searchQuery, }: ImagesViewProps): React.ReactElement { - const filteredImages = images.filter( - (img) => - !searchQuery || - img.repository.toLowerCase().includes(searchQuery.toLowerCase()) || - img.tag.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const filteredImages = images + .filter( + (img) => + !searchQuery || + img.repository.toLowerCase().includes(searchQuery.toLowerCase()) || + img.tag.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => a.repository.localeCompare(b.repository)); const columns = [ { key: "repository", header: "REPOSITORY", width: 35 }, diff --git a/src/components/NetworksView.tsx b/src/components/NetworksView.tsx index 91c30a8..cfd27fd 100644 --- a/src/components/NetworksView.tsx +++ b/src/components/NetworksView.tsx @@ -14,12 +14,14 @@ export function NetworksView({ selectedIndex, searchQuery, }: NetworksViewProps): React.ReactElement { - const filteredNetworks = networks.filter( - (net) => - !searchQuery || - net.name.toLowerCase().includes(searchQuery.toLowerCase()) || - net.driver.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const filteredNetworks = networks + .filter( + (net) => + !searchQuery || + net.name.toLowerCase().includes(searchQuery.toLowerCase()) || + net.driver.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => a.name.localeCompare(b.name)); const columns = [ { key: "id", header: "NETWORK ID", width: 15 }, diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index fdaa81e..324d9d3 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -12,13 +12,86 @@ interface StatusBarProps { releaseStatus?: ReleaseCheckState | null; } -const TAB_ACTIONS: Record = { - containers: "n:run e:edit s:start x:stop R:restart d:delete L:logs i:inspect", - images: "n:run p:pull d:delete i:inspect", - networks: "c:create d:delete i:inspect", - volumes: "c:create d:delete i:inspect", +interface Action { + key: string; + label: string; +} + +const TAB_ACTIONS: Record = { + containers: [ + { key: "c", label: "create" }, + { key: "e", label: "edit" }, + { key: "s", label: "start" }, + { key: "o", label: "stop" }, + { key: "R", label: "Restart" }, + { key: "d", label: "delete" }, + { key: "L", label: "Logs" }, + { key: "i", label: "inspect" }, + ], + images: [ + { key: "c", label: "create" }, + { key: "p", label: "pull" }, + { key: "d", label: "delete" }, + { key: "i", label: "inspect" }, + ], + networks: [ + { key: "c", label: "create" }, + { key: "d", label: "delete" }, + { key: "i", label: "inspect" }, + ], + volumes: [ + { key: "c", label: "create" }, + { key: "d", label: "delete" }, + { key: "i", label: "inspect" }, + ], }; +export function renderAction({ key, label }: Action, dim = false): React.ReactElement { + const idx = label.indexOf(key); + if (idx === -1) { + return {key}:{label}; + } + const before = label.slice(0, idx); + const after = label.slice(idx + 1); + return ( + + {before}{label[idx]}{after} + + ); +} + +function renderActions(actions: Action[]): React.ReactElement { + return ( + + {actions.map((action, i) => ( + + {i > 0 ? " " : ""}{renderAction(action)} + + ))} + + ); +} + +const GLOBAL_ACTIONS: Action[] = [ + { key: "/", label: "/search" }, + { key: "r", label: "refresh" }, + { key: "?", label: "?help" }, + { key: "q", label: "quit" }, +]; + +function renderGlobalActions(): React.ReactElement { + return ( + + hjkl:nav + {GLOBAL_ACTIONS.map((action) => ( + + {" "}{renderAction(action, true)} + + ))} + + ); +} + function renderReleaseStatus(releaseStatus?: ReleaseCheckState | null): React.ReactNode { if (!releaseStatus) return null; @@ -64,13 +137,9 @@ export function StatusBar({ return ( - {itemCount} {activeTab} | {TAB_ACTIONS[activeTab]} + {itemCount} {activeTab} | {renderActions(TAB_ACTIONS[activeTab])} - {releaseContent ?? ( - - h/l:tabs j/k:navigate /:search r:refresh ?:help q:quit - - )} + {releaseContent ?? renderGlobalActions()} ); } diff --git a/src/components/VolumesView.tsx b/src/components/VolumesView.tsx index 74a6bf0..0ba102c 100644 --- a/src/components/VolumesView.tsx +++ b/src/components/VolumesView.tsx @@ -14,12 +14,14 @@ export function VolumesView({ selectedIndex, searchQuery, }: VolumesViewProps): React.ReactElement { - const filteredVolumes = volumes.filter( - (vol) => - !searchQuery || - vol.name.toLowerCase().includes(searchQuery.toLowerCase()) || - vol.driver.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const filteredVolumes = volumes + .filter( + (vol) => + !searchQuery || + vol.name.toLowerCase().includes(searchQuery.toLowerCase()) || + vol.driver.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => a.name.localeCompare(b.name)); const columns = [ { key: "name", header: "VOLUME NAME", width: 35 }, diff --git a/src/hooks/useKeyboard.ts b/src/hooks/useKeyboard.ts index 99c66ab..5948a18 100644 --- a/src/hooks/useKeyboard.ts +++ b/src/hooks/useKeyboard.ts @@ -160,11 +160,6 @@ export function useKeyboard(options: UseKeyboardOptions): void { return; } - if (input === "n") { - onAction("run"); - return; - } - if (input === "e") { onAction("edit"); return; From b35dd945482fbcc50fb74e982fd59429db88519e Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Thu, 12 Feb 2026 11:21:25 -0500 Subject: [PATCH 2/3] feat: rebind stop container shortcut from 'x' to 'o' Co-Authored-By: Claude --- src/components/HelpOverlay.tsx | 2 +- src/hooks/useKeyboard.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/HelpOverlay.tsx b/src/components/HelpOverlay.tsx index b4cea2a..c11ff5f 100644 --- a/src/components/HelpOverlay.tsx +++ b/src/components/HelpOverlay.tsx @@ -22,7 +22,7 @@ const HELP_SECTIONS = [ { key: "c", desc: "Create new container" }, { key: "e", desc: "Edit container" }, { key: "s", desc: "Start container" }, - { key: "x", desc: "Stop container" }, + { key: "o", desc: "Stop container" }, { key: "R", desc: "Restart container" }, { key: "d", desc: "Delete (remove)" }, { key: "L", desc: "View logs" }, diff --git a/src/hooks/useKeyboard.ts b/src/hooks/useKeyboard.ts index 5948a18..f4c8369 100644 --- a/src/hooks/useKeyboard.ts +++ b/src/hooks/useKeyboard.ts @@ -111,7 +111,7 @@ export function useKeyboard(options: UseKeyboardOptions): void { return; } - if (input === "x") { + if (input === "o") { onAction("stop"); return; } From dbb858a4ccc6719789d0bf79a531d768179c6d6b Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Thu, 12 Feb 2026 11:27:22 -0500 Subject: [PATCH 3/3] fix: lift sorting and filtering to parent so selected index matches display Sorting was done inside each view component for display, but the parent used unsorted source arrays for action lookups (inspect, edit, delete, etc.), causing actions to target the wrong item. Now the parent sorts and filters with useMemo before passing data to views and using it for all index lookups. Completed GitHub issue #14 Co-Authored-By: Claude --- src/__tests__/sorting.test.tsx | 24 ++++---- src/components/App.tsx | 99 ++++++++++++++++++++++++------- src/components/ContainersView.tsx | 12 +--- src/components/ImagesView.tsx | 11 +--- src/components/NetworksView.tsx | 11 +--- src/components/VolumesView.tsx | 11 +--- 6 files changed, 94 insertions(+), 74 deletions(-) diff --git a/src/__tests__/sorting.test.tsx b/src/__tests__/sorting.test.tsx index e10dd42..969cd04 100644 --- a/src/__tests__/sorting.test.tsx +++ b/src/__tests__/sorting.test.tsx @@ -36,9 +36,12 @@ function makeVolume(name: string): Volume { return { name, driver: "local", mountpoint: `/var/${name}`, scope: "local" }; } +// Sorting and filtering now happens in App.tsx before data reaches view components. +// These tests verify that pre-sorted data renders in the correct order. + describe("List sorting", () => { - it("sorts containers alphabetically by name", () => { - const containers = [makeContainer("zebra"), makeContainer("alpha"), makeContainer("mango")]; + it("renders containers in the order provided (sorted by App)", () => { + const containers = [makeContainer("alpha"), makeContainer("mango"), makeContainer("zebra")]; const { lastFrame } = render( ); @@ -50,8 +53,8 @@ describe("List sorting", () => { expect(mangoIdx).toBeLessThan(zebraIdx); }); - it("sorts images alphabetically by repository", () => { - const images = [makeImage("zookeeper"), makeImage("alpine"), makeImage("nginx")]; + it("renders images in the order provided (sorted by App)", () => { + const images = [makeImage("alpine"), makeImage("nginx"), makeImage("zookeeper")]; const { lastFrame } = render( ); @@ -63,8 +66,8 @@ describe("List sorting", () => { expect(nginxIdx).toBeLessThan(zookeeperIdx); }); - it("sorts networks alphabetically by name", () => { - const networks = [makeNetwork("zoo-net"), makeNetwork("app-net"), makeNetwork("db-net")]; + it("renders networks in the order provided (sorted by App)", () => { + const networks = [makeNetwork("app-net"), makeNetwork("db-net"), makeNetwork("zoo-net")]; const { lastFrame } = render( ); @@ -76,8 +79,8 @@ describe("List sorting", () => { expect(dbIdx).toBeLessThan(zooIdx); }); - it("sorts volumes alphabetically by name", () => { - const volumes = [makeVolume("z-vol"), makeVolume("a-vol"), makeVolume("m-vol")]; + it("renders volumes in the order provided (sorted by App)", () => { + const volumes = [makeVolume("a-vol"), makeVolume("m-vol"), makeVolume("z-vol")]; const { lastFrame } = render( ); @@ -89,8 +92,9 @@ describe("List sorting", () => { expect(mIdx).toBeLessThan(zIdx); }); - it("sorts containers after filtering", () => { - const containers = [makeContainer("zebra-app"), makeContainer("alpha-app"), makeContainer("no-match")]; + it("renders pre-filtered containers without excluded items", () => { + // App.tsx filters and sorts before passing to view + const containers = [makeContainer("alpha-app"), makeContainer("zebra-app")]; const { lastFrame } = render( ); diff --git a/src/components/App.tsx b/src/components/App.tsx index e9aa659..8f7ba85 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, useMemo } from "react"; import { Box, Text, useApp } from "ink"; import Spinner from "ink-spinner"; import { TabBar } from "./TabBar.js"; @@ -80,20 +80,73 @@ export function App(): React.ReactElement { const [actionError, setActionError] = useState(null); const [actionInProgress, setActionInProgress] = useState(null); + const sortedContainers = useMemo( + () => + containers + .filter( + (c) => + !searchQuery || + c.name.toLowerCase().includes(searchQuery.toLowerCase()) || + c.image.toLowerCase().includes(searchQuery.toLowerCase()) || + c.status.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => a.name.localeCompare(b.name)), + [containers, searchQuery] + ); + + const sortedImages = useMemo( + () => + images + .filter( + (img) => + !searchQuery || + img.repository.toLowerCase().includes(searchQuery.toLowerCase()) || + img.tag.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => a.repository.localeCompare(b.repository)), + [images, searchQuery] + ); + + const sortedNetworks = useMemo( + () => + networks + .filter( + (net) => + !searchQuery || + net.name.toLowerCase().includes(searchQuery.toLowerCase()) || + net.driver.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => a.name.localeCompare(b.name)), + [networks, searchQuery] + ); + + const sortedVolumes = useMemo( + () => + volumes + .filter( + (vol) => + !searchQuery || + vol.name.toLowerCase().includes(searchQuery.toLowerCase()) || + vol.driver.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => a.name.localeCompare(b.name)), + [volumes, searchQuery] + ); + const getItemCount = useCallback((): number => { switch (activeTab) { case "containers": - return containers.length; + return sortedContainers.length; case "images": - return images.length; + return sortedImages.length; case "networks": - return networks.length; + return sortedNetworks.length; case "volumes": - return volumes.length; + return sortedVolumes.length; default: return 0; } - }, [activeTab, containers.length, images.length, networks.length, volumes.length]); + }, [activeTab, sortedContainers.length, sortedImages.length, sortedNetworks.length, sortedVolumes.length]); const handleQuit = useCallback(() => { exit(); @@ -130,7 +183,7 @@ export function App(): React.ReactElement { switch (activeTab) { case "containers": { - const container = containers[selectedIndex]; + const container = sortedContainers[selectedIndex]; if (!container) return; const details = await containerCli.inspectContainer(container.id); const curated: Record = { @@ -153,13 +206,13 @@ export function App(): React.ReactElement { break; } case "images": { - const image = images[selectedIndex]; + const image = sortedImages[selectedIndex]; if (!image) return; inspectData = await containerCli.inspectImage(image.reference); break; } case "networks": { - const network = networks[selectedIndex]; + const network = sortedNetworks[selectedIndex]; if (!network) return; inspectData = (await containerCli.inspectNetwork( network.id @@ -167,7 +220,7 @@ export function App(): React.ReactElement { break; } case "volumes": { - const volume = volumes[selectedIndex]; + const volume = sortedVolumes[selectedIndex]; if (!volume) return; inspectData = (await containerCli.inspectVolume( volume.name @@ -183,7 +236,7 @@ export function App(): React.ReactElement { } catch (err) { setActionError(err instanceof Error ? err.message : "Failed to inspect"); } - }, [activeTab, selectedIndex, containers, images, networks, volumes]); + }, [activeTab, selectedIndex, sortedContainers, sortedImages, sortedNetworks, sortedVolumes]); const handleBack = useCallback(() => { setActionError(null); @@ -222,25 +275,25 @@ export function App(): React.ReactElement { const getItemIdentifiers = useCallback(() => { switch (activeTab) { case "containers": { - const c = containers[selectedIndex]; + const c = sortedContainers[selectedIndex]; return c ? { id: c.id, name: c.name } : null; } case "images": { - const i = images[selectedIndex]; + const i = sortedImages[selectedIndex]; return i ? { id: i.reference, name: `${i.repository}:${i.tag}` } : null; } case "networks": { - const n = networks[selectedIndex]; + const n = sortedNetworks[selectedIndex]; return n ? { id: n.id, name: n.name } : null; } case "volumes": { - const v = volumes[selectedIndex]; + const v = sortedVolumes[selectedIndex]; return v ? { id: v.name, name: v.name } : null; } default: return null; } - }, [activeTab, selectedIndex, containers, images, networks, volumes]); + }, [activeTab, selectedIndex, sortedContainers, sortedImages, sortedNetworks, sortedVolumes]); const handleAction = useCallback( async (action: string) => { @@ -259,14 +312,14 @@ export function App(): React.ReactElement { }); } else if (activeTab === "containers" || activeTab === "images") { const image = - activeTab === "images" ? images[selectedIndex]?.reference : undefined; + activeTab === "images" ? sortedImages[selectedIndex]?.reference : undefined; setDialog({ type: "run", initialValues: image ? { image } : undefined }); } return; } if (action === "edit" && activeTab === "containers") { - const container = containers[selectedIndex]; + const container = sortedContainers[selectedIndex]; if (!container) return; try { setActionInProgress("Loading container settings..."); @@ -374,7 +427,7 @@ export function App(): React.ReactElement { setActionError(err instanceof Error ? err.message : "Action failed"); } }, - [getItemIdentifiers, activeTab, refresh, handleSelect, images, selectedIndex, containers] + [getItemIdentifiers, activeTab, refresh, handleSelect, sortedImages, selectedIndex, sortedContainers] ); const handleConfirm = useCallback(async () => { @@ -556,28 +609,28 @@ export function App(): React.ReactElement { {activeTab === "containers" && ( )} {activeTab === "images" && ( )} {activeTab === "networks" && ( )} {activeTab === "volumes" && ( diff --git a/src/components/ContainersView.tsx b/src/components/ContainersView.tsx index 1bb8cd5..28dd365 100644 --- a/src/components/ContainersView.tsx +++ b/src/components/ContainersView.tsx @@ -34,16 +34,6 @@ export function ContainersView({ selectedIndex, searchQuery, }: ContainersViewProps): React.ReactElement { - const filteredContainers = containers - .filter( - (c) => - !searchQuery || - c.name.toLowerCase().includes(searchQuery.toLowerCase()) || - c.image.toLowerCase().includes(searchQuery.toLowerCase()) || - c.status.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .sort((a, b) => a.name.localeCompare(b.name)); - const columns = [ { key: "name", header: "NAME", width: 25 }, { key: "image", header: "IMAGE", width: 30 }, @@ -71,7 +61,7 @@ export function ContainersView({ )} diff --git a/src/components/ImagesView.tsx b/src/components/ImagesView.tsx index 3af8100..fb6f5f2 100644 --- a/src/components/ImagesView.tsx +++ b/src/components/ImagesView.tsx @@ -14,15 +14,6 @@ export function ImagesView({ selectedIndex, searchQuery, }: ImagesViewProps): React.ReactElement { - const filteredImages = images - .filter( - (img) => - !searchQuery || - img.repository.toLowerCase().includes(searchQuery.toLowerCase()) || - img.tag.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .sort((a, b) => a.repository.localeCompare(b.repository)); - const columns = [ { key: "repository", header: "REPOSITORY", width: 35 }, { key: "tag", header: "TAG", width: 20 }, @@ -40,7 +31,7 @@ export function ImagesView({ )}
diff --git a/src/components/NetworksView.tsx b/src/components/NetworksView.tsx index cfd27fd..94d2c09 100644 --- a/src/components/NetworksView.tsx +++ b/src/components/NetworksView.tsx @@ -14,15 +14,6 @@ export function NetworksView({ selectedIndex, searchQuery, }: NetworksViewProps): React.ReactElement { - const filteredNetworks = networks - .filter( - (net) => - !searchQuery || - net.name.toLowerCase().includes(searchQuery.toLowerCase()) || - net.driver.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .sort((a, b) => a.name.localeCompare(b.name)); - const columns = [ { key: "id", header: "NETWORK ID", width: 15 }, { key: "name", header: "NAME", width: 30 }, @@ -39,7 +30,7 @@ export function NetworksView({ )}
diff --git a/src/components/VolumesView.tsx b/src/components/VolumesView.tsx index 0ba102c..1527bc9 100644 --- a/src/components/VolumesView.tsx +++ b/src/components/VolumesView.tsx @@ -14,15 +14,6 @@ export function VolumesView({ selectedIndex, searchQuery, }: VolumesViewProps): React.ReactElement { - const filteredVolumes = volumes - .filter( - (vol) => - !searchQuery || - vol.name.toLowerCase().includes(searchQuery.toLowerCase()) || - vol.driver.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .sort((a, b) => a.name.localeCompare(b.name)); - const columns = [ { key: "name", header: "VOLUME NAME", width: 35 }, { key: "driver", header: "DRIVER", width: 15 }, @@ -39,7 +30,7 @@ export function VolumesView({ )}