feat: enhance container dashboard with new features

- Added a "Ports" column to display container port information with sorting functionality.
- Implemented a dropdown menu item to copy the Container ID to the clipboard with a success toast notification.
- Introduced a state filter dropdown to filter containers by their current state, with options for all defined states.
- Added a refresh button to allow manual refreshing of the container list.
This commit is contained in:
Mauricio Siu
2026-07-09 02:47:12 -06:00
parent 93b7942f7d
commit 1c4414165d
2 changed files with 102 additions and 9 deletions
@@ -1,10 +1,13 @@
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import copy from "copy-to-clipboard";
import { ArrowUpDown, MoreHorizontal } from "lucide-react"; import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
@@ -37,6 +40,7 @@ export const columns: ColumnDef<Container>[] = [
}, },
{ {
accessorKey: "state", accessorKey: "state",
filterFn: "equals",
header: ({ column }) => { header: ({ column }) => {
return ( return (
<Button <Button
@@ -56,7 +60,7 @@ export const columns: ColumnDef<Container>[] = [
variant={ variant={
value === "running" value === "running"
? "default" ? "default"
: value === "failed" : value === "exited" || value === "dead"
? "destructive" ? "destructive"
: "secondary" : "secondary"
} }
@@ -99,6 +103,28 @@ export const columns: ColumnDef<Container>[] = [
}, },
cell: ({ row }) => <div className="lowercase">{row.getValue("image")}</div>, cell: ({ row }) => <div className="lowercase">{row.getValue("image")}</div>,
}, },
{
accessorKey: "ports",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Ports
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const value = row.getValue("ports") as string;
return (
<div className="max-w-[16rem] truncate lowercase" title={value}>
{value}
</div>
);
},
},
{ {
id: "actions", id: "actions",
enableHiding: false, enableHiding: false,
@@ -115,6 +141,14 @@ export const columns: ColumnDef<Container>[] = [
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
copy(container.containerId);
toast.success("Container ID copied to clipboard");
}}
>
Copy Container ID
</DropdownMenuItem>
<ShowDockerModalLogs <ShowDockerModalLogs
containerId={container.containerId} containerId={container.containerId}
serverId={container.serverId} serverId={container.serverId}
@@ -9,7 +9,7 @@ import {
useReactTable, useReactTable,
type VisibilityState, type VisibilityState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { ChevronDown, Container } from "lucide-react"; import { ChevronDown, Container, RefreshCw } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -26,6 +26,13 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { import {
Table, Table,
TableBody, TableBody,
@@ -44,10 +51,26 @@ interface Props {
serverId?: string; serverId?: string;
} }
const CONTAINER_STATES = [
"running",
"exited",
"paused",
"restarting",
"created",
"removing",
"dead",
];
export const ShowContainers = ({ serverId }: Props) => { export const ShowContainers = ({ serverId }: Props) => {
const { data, isPending } = api.docker.getContainers.useQuery({ const { data, isPending, refetch, isRefetching } =
serverId, api.docker.getContainers.useQuery(
}); {
serverId,
},
{
refetchInterval: 10_000,
},
);
const [sorting, setSorting] = React.useState<SortingState>([]); const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
@@ -106,12 +129,48 @@ export const ShowContainers = ({ serverId }: Props) => {
} }
className="md:max-w-sm" className="md:max-w-sm"
/> />
<Select
value={
(table.getColumn("state")?.getFilterValue() as string) ??
"all"
}
onValueChange={(value) =>
table
.getColumn("state")
?.setFilterValue(value === "all" ? undefined : value)
}
>
<SelectTrigger className="w-40 max-sm:w-full capitalize">
<SelectValue placeholder="Filter by state" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All states</SelectItem>
{CONTAINER_STATES.map((state) => (
<SelectItem
key={state}
value={state}
className="capitalize"
>
{state}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
className="shrink-0 sm:ml-auto"
onClick={() => refetch()}
disabled={isRefetching}
>
<RefreshCw
className={`h-4 w-4 ${isRefetching ? "animate-spin" : ""}`}
/>
<span className="sr-only">Refresh</span>
</Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button variant="outline" className="max-sm:w-full">
variant="outline"
className="sm:ml-auto max-sm:w-full"
>
Columns <ChevronDown className="ml-2 h-4 w-4" /> Columns <ChevronDown className="ml-2 h-4 w-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>