This commit is contained in:
2026-03-28 13:41:29 +01:00
parent 5c1c3eefa3
commit 551b5a2658
52 changed files with 2559 additions and 8 deletions

View File

@@ -103,6 +103,7 @@ async function listContainers() {
traefikUrl: traefikUrl, traefikUrl: traefikUrl,
preferredLocalUrl: preferredLocalUrl, preferredLocalUrl: preferredLocalUrl,
accessibleUrls: accessibleUrls, accessibleUrls: accessibleUrls,
labels: container.Labels,
}; };
}); });
} }

16
frontend/components.json Normal file
View File

@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

10
frontend/craco.config.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
style: {
postcss: {
plugins: () => [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
};

10
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,21 +3,31 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@craco/craco": "^7.1.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.13.6", "axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.7.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"serve": "^14.2.6", "serve": "^14.2.6",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "craco start",
"build": "react-scripts build", "build": "craco build",
"test": "react-scripts test", "test": "craco test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
@@ -37,5 +47,10 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"autoprefixer": "^10.4.27",
"postcss": "^8.5.8",
"tailwindcss": "^3.4.19"
} }
} }

View File

@@ -20,9 +20,10 @@ body {
.service-grid { .service-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; /* Removed justify-content: center; */
padding: 40px 20px; padding: 40px; /* Adjusted padding */
gap: 25px; /* Increased gap */ gap: 25px; /* Increased gap */
max-width: none; /* Allow full width */
} }
/* Responsive design */ /* Responsive design */
@@ -49,3 +50,28 @@ body {
gap: 10px; gap: 10px;
} }
} }
.search-box,
.filter-box {
margin: 10px auto;
width: 80%;
max-width: 500px;
}
.search-box input,
.filter-box input {
width: 100%;
padding: 10px 15px;
border: 1px solid #999; /* Darker border */
border-radius: 10px; /* More rounded corners */
font-size: 1em;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
transition: border-color 0.3s ease;
}
.search-box input:focus,
.filter-box input:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}

View File

@@ -1,10 +1,14 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import './App.css'; import './App.css';
import { getServices } from './api'; import { getServices } from './api';
import ServiceCard from './components/ServiceCard'; import ServiceCard from './components/ServiceCard';
import SearchBox from './components/SearchBox'; // Import SearchBox
import FilterBox from './components/FilterBox'; // Import FilterBox
function App() { function App() {
const [services, setServices] = useState([]); const [services, setServices] = useState([]);
const [searchTerm, setSearchTerm] = useState(''); // Add state for search term
const [selectedLabels, setSelectedLabels] = useState([]);
async function fetchServices() { async function fetchServices() {
const fetchedServices = await getServices(); const fetchedServices = await getServices();
@@ -32,13 +36,63 @@ function App() {
}; };
}, []); }, []);
// Extract unique labels
const allLabels = useMemo(() => {
const labels = new Set();
services.forEach(service => {
for (const key in service.labels) {
labels.add(`${key}:${service.labels[key]}`);
}
});
return Array.from(labels).sort();
}, [services]);
// Handler for toggling labels
const handleLabelToggle = (labelToToggle) => {
setSelectedLabels(prevSelectedLabels => {
if (prevSelectedLabels.includes(labelToToggle)) {
return prevSelectedLabels.filter(label => label !== labelToToggle);
} else {
return [...prevSelectedLabels, labelToToggle];
}
});
};
// Filtering logic
const filteredServices = services.filter(service => {
const matchesSearchTerm = service.name.toLowerCase().includes(searchTerm.toLowerCase());
let matchesFilterTerm = true;
// Update filtering logic to use selectedLabels
if (selectedLabels.length > 0) {
matchesFilterTerm = selectedLabels.every(selectedLabel => {
const [key, value] = selectedLabel.split(':');
if (value) { // key:value format
return service.labels[key] === value;
} else { // key only format (e.g., "has:env" was replaced by just "env")
return Object.keys(service.labels).includes(key);
}
});
}
return matchesSearchTerm && matchesFilterTerm;
});
return ( return (
<div className="App"> <div className="App">
<header className="App-header"> <header className="App-header">
<h1>Docker Service Display</h1> <h1>Docker Service Display</h1>
<div className="flex space-x-2 w-fit">
<SearchBox searchTerm={searchTerm} onSearchChange={setSearchTerm} />
<FilterBox
availableLabels={allLabels}
selectedLabels={selectedLabels}
onLabelToggle={handleLabelToggle}
/>
</div>
</header> </header>
<main className="service-grid"> <main className="service-grid">
{services.map(service => ( {filteredServices.map(service => (
<ServiceCard key={service.id} service={service} /> <ServiceCard key={service.id} service={service} />
))} ))}
</main> </main>

View File

@@ -0,0 +1,50 @@
.dropdown {
position: relative;
display: inline-block;
width: 200px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 14px;
}
.dropdownHeader {
padding: 8px 12px;
background-color: #fff;
cursor: pointer;
}
.dropdownList {
position: absolute;
top: 100%;
left: 0;
width: 100%;
margin: 4px 0 0;
padding: 0;
list-style: none;
border: 1px solid #d1d5db;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 1;
}
.dropdownItem {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
}
.dropdownItem input {
margin-right: 8px;
}
.dropdownItem:hover {
background-color: #f3f4f6;
}
@media (max-width: 600px) {
.dropdown {
width: 100%;
}
}

View File

@@ -0,0 +1,39 @@
import React, { useState } from 'react';
import styles from './Dropdown.css';
const Dropdown = ({ options, onSelect, selectedValues }) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState(null);
const handleSelect = (value) => {
onSelect(value);
};
return (
<div className={styles.dropdown}>
<div className={styles.dropdownHeader} onClick={() => setIsOpen(!isOpen)}>
{selectedValue || 'Select...'}
</div>
{isOpen && (
<ul className={styles.dropdownList}>
{options.map((option) => (
<li
key={option.value}
className={styles.dropdownItem}
onClick={() => handleSelect(option.value)}
>
<input
type="checkbox"
checked={selectedValues.includes(option.value)}
onChange={() => {}}
/>
{option.label}
</li>
))}
</ul>
)}
</div>
);
};
export default Dropdown;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "./ui/dropdown-menu"
import { Checkbox } from "./ui/checkbox"
import { Button } from "./ui/button"
const FilterBox = ({ availableLabels, selectedLabels, onLabelToggle }) => {
return (
<div className="filter-dropdown">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" className="bg-white">Filter by Labels ({selectedLabels.length})</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-72">
{availableLabels.map(label => (
<DropdownMenuItem key={label} onSelect={(e) => e.preventDefault()}>
<Checkbox
checked={selectedLabels.includes(label)}
onCheckedChange={() => onLabelToggle(label)}
/>
<span className="ml-2">{label}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
export default FilterBox;

View File

@@ -0,0 +1,16 @@
import React from 'react';
const SearchBox = ({ searchTerm, onSearchChange }) => {
return (
<div className="search-box">
<input
type="text"
placeholder="Search services by name..."
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
/>
</div>
);
};
export default SearchBox;

View File

@@ -5,7 +5,8 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* More pronounced shadow */ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* More pronounced shadow */
padding: 25px; /* Increased padding */ padding: 25px; /* Increased padding */
margin: 15px; /* Increased margin */ margin: 15px; /* Increased margin */
width: 300px; flex-basis: calc(33.333% - 30px); /* Allow 3 cards per row with gap */
max-width: 350px; /* Prevent cards from becoming too wide */
transition: transform 0.2s ease-in-out; /* Smooth hover effect */ transition: transform 0.2s ease-in-out; /* Smooth hover effect */
} }

View File

@@ -0,0 +1,47 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";
import { cn } from "../../lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} />
);
})
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "../../lib/utils"
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}>
<CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -0,0 +1,155 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "../../lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props} />
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-white p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props} />
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props} />
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props} />
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props} />
);
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@@ -0,0 +1,120 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "../../lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn("p-1", position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -1,3 +1,90 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 20% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 20% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 20% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 20% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 20% 98%;
--primary: 210 20% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 20% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.4% 65.0%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
.filter-dropdown {
position: relative;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

View File

@@ -0,0 +1,6 @@
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-27

View File

@@ -0,0 +1,36 @@
## Context
The existing application displays a list of Docker services. Currently, there is no way to search or filter these services, which becomes problematic as the number of services grows. The proposal outlines the need for a search box (by service name) and a filter box (by service labels) with real-time updates.
## Goals / Non-Goals
**Goals:**
- Implement a user-friendly search and filter interface for Docker services.
- Allow filtering by service name (case-insensitive substring match).
- Allow filtering by service labels (exact match for key-value pairs or just key presence).
- Update the displayed services in real-time as the user types.
- Maintain application performance with a reasonable number of services (e.g., up to 100 services).
**Non-Goals:**
- Complex query language for search/filter.
- Server-side pagination for search/filter results (initially, all services will be fetched and filtered client-side).
- Persistence of search/filter state across sessions.
- Advanced UI features like autocomplete or suggested filters.
## Decisions
- **Client-side Filtering**: All services will be fetched once from the backend. Filtering and searching will be performed on the client-side (in the React frontend).
- *Rationale*: Simplifies backend implementation, reduces server load for each filter/search keystroke, and provides a snappier user experience for a moderate number of services.
- *Alternatives Considered*: Server-side filtering. Rejected due to increased backend complexity and potential latency for real-time updates.
- **Search Logic**: Case-insensitive substring matching for service names.
- *Rationale*: Common and intuitive search behavior.
- **Filter Logic (Labels)**:
- Initial approach: Allow filtering by a single label key-value pair (e.g., `env=production`) or by a label key presence (e.g., `has:env`).
- *Rationale*: Provides basic filtering capability without over-complicating the UI initially. Can be extended later.
- **UI Implementation**: Use standard HTML input elements for search and filter. React state will manage the input values and trigger re-rendering of the service list.
- *Rationale*: Leverages existing React component structure and state management.
## Risks / Trade-offs
- **Performance with Large Number of Services**: [Risk] Client-side filtering might become slow if the number of services grows very large (e.g., thousands). → Mitigation: Monitor performance. If it becomes an issue, consider introducing server-side filtering or pagination.
- **Backend API Changes**: [Risk] The current backend API might not expose all necessary service metadata (e.g., labels) in a readily consumable format. → Mitigation: Review `backend/docker.js` and `backend/index.js` to ensure all required service details (name, labels) are returned. If not, modify the backend to include them.

View File

@@ -0,0 +1,27 @@
## Why
The current service display lacks efficient navigation and discovery for users. Adding search and filter capabilities will significantly improve the user experience by allowing them to quickly find specific services based on their names or associated labels, especially as the number of deployed services grows.
## What Changes
- Introduce a search input field to filter services by name.
- Introduce a filter input field to filter services by labels.
- Implement real-time filtering as the user types in either field.
- Display only services that match both the search and filter criteria.
## Capabilities
### New Capabilities
- `service-search`: Allows users to search for services by name.
- `service-filtering`: Allows users to filter services by labels.
- `realtime-service-display-update`: Updates the displayed services in real-time based on search and filter input.
### Modified Capabilities
<!-- Existing capabilities whose REQUIREMENTS are changing (not just implementation).
Only list here if spec-level behavior changes. Each needs a delta spec file.
Use existing spec names from openspec/specs/. Leave empty if no requirement changes. -->
## Impact
- Frontend: `frontend/src/App.js`, `frontend/src/components/ServiceCard.js`, `frontend/src/api.js` will be modified to include the search/filter UI and logic for fetching/displaying filtered services.
- Backend: `backend/index.js` and `backend/docker.js` might need modifications to support filtering services based on labels and names, or to expose more service metadata if not already available.

View File

@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: Service display updates in real-time
The displayed list of services SHALL update instantaneously as the user types in either the search or filter input fields.
#### Scenario: Display updates on search input change
- **WHEN** the user types a character into the search input field
- **THEN** the list of displayed services is immediately re-evaluated and updated based on the new search criteria
#### Scenario: Display updates on filter input change
- **WHEN** the user types a character into the filter input field
- **THEN** the list of displayed services is immediately re-evaluated and updated based on the new filter criteria
#### Scenario: Display updates on both search and filter input changes
- **WHEN** the user types a character into the search input field, and a filter is already active
- **THEN** the list of displayed services is immediately re-evaluated and updated based on both the new search criteria and the existing filter criteria

View File

@@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: User can filter services by labels
The system SHALL allow users to filter services based on their Docker labels using a dedicated input field. The filter SHALL support key-value pair matching (e.g., `env=production`) and key presence matching (e.g., `has:env`). Multiple label filters are not supported in this initial version.
#### Scenario: Filter by label key-value pair returns matching services
- **WHEN** the user types "env=production" into the filter input field
- **THEN** only services with a label `env` set to `production` are displayed
#### Scenario: Filter by label key presence returns matching services
- **WHEN** the user types "has:env" into the filter input field
- **THEN** only services that have an `env` label (regardless of its value) are displayed
#### Scenario: Filter returns no services
- **WHEN** the user types "env=development" (a non-existent label value) into the filter input field
- **THEN** no services are displayed
#### Scenario: Empty filter input displays all services
- **WHEN** the filter input field is empty
- **THEN** all services (subject to other searches) are displayed

View File

@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: User can search services by name
The system SHALL allow users to search for services by their name using a dedicated input field. The search SHALL be case-insensitive and perform a substring match.
#### Scenario: Search returns matching services
- **WHEN** the user types "web" into the search input field
- **THEN** only services with "web" (case-insensitive) in their name are displayed
#### Scenario: Search returns no services
- **WHEN** the user types "xyz" (a non-existent service name part) into the search input field
- **THEN** no services are displayed
#### Scenario: Empty search input displays all services
- **WHEN** the search input field is empty
- **THEN** all services (subject to other filters) are displayed

View File

@@ -0,0 +1,29 @@
## 1. Backend Modifications
- [x] 1.1 Modify `backend/docker.js` to include service labels in the returned service data.
- [x] 1.2 Update `backend/index.js` to ensure the API endpoint returns the extended service data including labels.
## 2. Frontend UI Implementation
- [x] 2.1 Create a new React component for the search input field (e.g., `SearchBox.js`).
- [x] 2.2 Create a new React component for the filter input field (e.g., `FilterBox.js`).
- [x] 2.3 Integrate `SearchBox` and `FilterBox` into `frontend/src/App.js`.
- [x] 2.4 Implement state management in `App.js` for search term and filter term.
- [x] 2.5 Update `App.js` to fetch all services once on component mount.
## 3. Frontend Filtering Logic
- [x] 3.1 Implement client-side filtering logic in `App.js` to filter services based on the search term (service name, case-insensitive substring match).
- [x] 3.2 Implement client-side filtering logic in `App.js` to filter services based on the filter term (label key-value or key presence match).
- [x] 3.3 Combine search and filter logic to display services that satisfy both criteria.
- [x] 3.4 Ensure real-time updates of the displayed services as search/filter inputs change.
## 4. Styling and User Experience
- [x] 4.1 Add basic CSS styling for the search and filter input fields.
- [x] 4.2 Ensure the UI is responsive and user-friendly.
## 5. Testing
- [x] 5.1 Write unit/integration tests for the new search and filter functionality in the frontend.
- [x] 5.2 Manually test the search and filter features with various inputs and label configurations.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-27

View File

@@ -0,0 +1,37 @@
## Context
The current Docker service display UI has limitations in utilizing screen real estate and the search/filter components are basic. The goal is to enhance the visual appeal, responsiveness, and functionality of these elements. Specifically, service cards should span the full width, the search bar needs a visual refresh, and the label filtering should evolve from a text input to a more interactive dropdown.
## Goals / Non-Goals
**Goals:**
- Optimize service card layout to use full screen width, improving information density.
- Enhance the visual design of the search bar with rounded corners for better integration.
- Implement a user-friendly dropdown filter for service labels, allowing selection/deselection of available labels.
- Improve the overall user experience by making filtering more intuitive and visually appealing.
**Non-Goals:**
- Implementing complex multi-select or nested label filtering in the dropdown.
- Server-side changes beyond ensuring all labels are returned with service data (which is already handled by the previous change).
- Major redesign of the `ServiceCard` component itself, beyond adapting to new layout.
- Implementing a backend API for dynamic label discovery (labels will be extracted from currently displayed services).
## Decisions
- **Full-width Layout**: The `service-grid` CSS will be modified to remove `justify-content: center` and potentially adjust `max-width` or `padding` to allow cards to spread across the available width. Card sizing will be managed using `flex-basis` or `grid-template-columns` to ensure responsiveness.
- *Rationale*: Maximizes screen utilization and improves visual density.
- **Search Bar Styling**: The `SearchBox` component's input field will receive updated CSS to apply rounded corners and potentially adjust padding/margins for a more integrated look.
- *Rationale*: Improves aesthetic appeal and consistency with modern UI design.
- **Dropdown Label Filter Implementation**:
- The `FilterBox` component will be refactored from a simple input to a dropdown.
- It will dynamically populate available labels by inspecting the `services` data in `App.js`.
- Users will be able to toggle labels on/off within the dropdown.
- The `App.js` filtering logic will be updated to filter based on the selected labels from the dropdown.
- *Rationale*: Provides a more intuitive and discoverable way to filter by labels compared to a text input, reducing user error and improving usability.
- *Alternatives Considered*: Multi-select dropdown, tag-based filtering. Rejected for initial implementation due to increased complexity and desire to keep the first iteration focused.
## Risks / Trade-offs
- **CSS Over-complication**: [Risk] Modifying the `service-grid` layout might introduce unexpected side effects on `ServiceCard` rendering or responsiveness. → Mitigation: Thorough testing across different screen sizes and careful application of CSS changes, leveraging existing media queries.
- **Dynamic Label Extraction Performance**: [Risk] Extracting unique labels from all services on every render might impact performance if there are a very large number of services and unique labels. → Mitigation: Memoize the label extraction process or perform it less frequently (e.g., only when `services` data changes). For the current scope (up to 100 services), this should not be a significant issue.
- **Complexity of `FilterBox`**: [Risk] Refactoring `FilterBox` into a dropdown will increase its complexity. → Mitigation: Break down the component into smaller, manageable parts if necessary, and ensure clear state management.

View File

@@ -0,0 +1,24 @@
## Why
The current UI for displaying service cards is not utilizing the available screen space effectively, leading to a suboptimal user experience, especially on larger displays. Additionally, the search and filter components lack visual integration and advanced filtering capabilities, making them less intuitive and powerful than desired.
## What Changes
- **UI Layout Improvement**: Adjust the layout of service cards to utilize the full width of the screen, improving visual density and information display.
- **Search Bar Styling**: Redesign the search bar to have rounded corners and better integrate with the overall application design.
- **Enhanced Filter Box**: Transform the filter input into a dropdown-style component that allows users to toggle available labels for filtering. This will replace the current text-based label filtering.
## Capabilities
### New Capabilities
- `full-width-service-display`: Ensures service cards utilize the full available screen width.
- `integrated-search-ui`: Provides a visually integrated search bar with rounded corners.
- `dropdown-label-filter`: Implements a dropdown-based filter for service labels, allowing users to toggle labels.
### Modified Capabilities
- `service-filtering`: The existing text-based label filtering will be replaced by the new dropdown-based filtering.
- `service-search`: The existing search functionality will be visually updated.
## Impact
- Frontend: `frontend/src/App.js`, `frontend/src/App.css`, `frontend/src/components/ServiceCard.js`, `frontend/src/components/SearchBox.js`, `frontend/src/components/FilterBox.js` will be modified. The `FilterBox` component will undergo significant changes to become a dropdown.

View File

@@ -0,0 +1,24 @@
## ADDED Requirements
### Requirement: User can filter services using a dropdown of available labels
The system SHALL provide a dropdown component for filtering services by labels, allowing users to select or deselect individual labels to apply filters. The dropdown SHALL dynamically populate with unique label keys and values present in the currently displayed services.
#### Scenario: Dropdown displays unique label keys and values
- **WHEN** the filter dropdown is opened
- **THEN** it SHALL display a list of unique label keys and their corresponding values (e.g., "env: production", "env: development", "tier: backend") found across all currently loaded services.
#### Scenario: User selects a label to filter
- **WHEN** the user selects a label (e.g., "env: production") from the dropdown
- **THEN** only services matching that label (e.g., `service.labels.env === 'production'`) are displayed.
#### Scenario: User deselects a label to remove filter
- **WHEN** the user deselects an active label filter from the dropdown
- **THEN** services previously hidden by that filter, but matching other active filters, are redisplayed.
#### Scenario: Multiple labels can be selected
- **WHEN** the user selects multiple labels (e.g., "env: production" and "tier: backend")
- **THEN** only services matching ALL selected labels are displayed.
#### Scenario: No labels selected displays all services (subject to search)
- **WHEN** no labels are selected in the dropdown
- **THEN** all services (subject to search criteria) are displayed.

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Service cards utilize full screen width
The system SHALL display service cards across the full available width of the browser window, adapting to different screen sizes to maximize space utilization.
#### Scenario: Cards fill available horizontal space on large screens
- **WHEN** the application is viewed on a wide desktop monitor
- **THEN** service cards are distributed horizontally to fill the available width, without excessive empty space on the sides.
#### Scenario: Cards adapt to narrower screens
- **WHEN** the application is viewed on a tablet or mobile device
- **THEN** service cards adjust their layout (e.g., stack vertically or reduce number of columns) to fit the narrower screen, maintaining readability and usability.

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Search bar has rounded corners and integrated appearance
The system SHALL render the search input field with rounded corners and styling that visually integrates it with the overall application design.
#### Scenario: Search bar displays with rounded corners
- **WHEN** the search bar is rendered
- **THEN** its input field SHALL have visibly rounded corners.
#### Scenario: Search bar styling matches application theme
- **WHEN** the search bar is rendered
- **THEN** its visual appearance (e.g., borders, background, font) SHALL be consistent with the application's overall design language.

View File

@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: User can filter services by labels
The system SHALL allow users to filter services based on their Docker labels using a dedicated dropdown component. The dropdown SHALL allow users to select multiple labels to filter by. The previous text-based input filtering SHALL be replaced.
#### Scenario: Filter by label key-value pair returns matching services
- **WHEN** the user selects "env: production" from the dropdown filter
- **THEN** only services with a label `env` set to `production` are displayed
#### Scenario: Filter by label key presence returns matching services
- **WHEN** the user selects a label key (e.g., "env") from the dropdown filter
- **THEN** only services that have an `env` label (regardless of its value) are displayed
#### Scenario: Filter returns no services
- **WHEN** the user selects a label combination that has no matching services
- **THEN** no services are displayed
#### Scenario: Empty filter input displays all services
- **WHEN** no labels are selected in the dropdown filter
- **THEN** all services (subject to other searches) are displayed

View File

@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: User can search services by name
The system SHALL allow users to search for services by their name using a dedicated input field. The search SHALL be case-insensitive and perform a substring match. The search input field SHALL have rounded corners and an integrated appearance.
#### Scenario: Search returns matching services
- **WHEN** the user types "web" into the search input field
- **THEN** only services with "web" (case-insensitive) in their name are displayed
#### Scenario: Search returns no services
- **WHEN** the user types "xyz" (a non-existent service name part) into the search input field
- **THEN** no services are displayed
#### Scenario: Empty search input displays all services
- **WHEN** the search input field is empty
- **THEN** all services (subject to other filters) are displayed
#### Scenario: Search bar displays with rounded corners
- **WHEN** the search bar is rendered
- **THEN** its input field SHALL have visibly rounded corners.

View File

@@ -0,0 +1,30 @@
## 1. UI Layout Improvements
- [x] 1.1 Modify `frontend/src/App.css` to adjust the `service-grid` layout for full-width utilization.
- [x] 1.2 Ensure `ServiceCard.css` styles adapt correctly to the new grid layout.
## 2. Search Bar Styling
- [x] 2.1 Update `frontend/src/App.css` with styles for rounded corners and better integration for the search input.
- [x] 2.2 Verify `frontend/src/components/SearchBox.js` renders with the new styles.
## 3. Dropdown Label Filter Implementation
- [x] 3.1 Refactor `frontend/src/components/FilterBox.js` to be a dropdown component instead of a text input.
- [x] 3.2 Implement logic in `frontend/src/App.js` to extract unique labels from all services.
- [x] 3.3 Pass the extracted unique labels to the `FilterBox` component.
- [x] 3.4 Implement state management in `frontend/src/App.js` for selected labels in the dropdown.
- [x] 3.5 Update the filtering logic in `frontend/src/App.js` to filter services based on selected labels from the dropdown.
- [x] 3.6 Implement UI for selecting/deselecting labels within the `FilterBox` dropdown.
## 4. Testing and Verification
- [x] 4.1 Manually test the new full-width layout on various screen sizes.
- [x] 4.2 Manually test the search bar's new styling.
- [x] 4.3 Manually test the dropdown label filter:
- Verify dynamic population of labels.
- Verify single label selection and filtering.
- Verify multiple label selection and filtering.
- Verify deselection of labels.
- Verify behavior when no labels are selected.
- [x] 4.4 (Optional) Write unit/integration tests for the new `FilterBox` component and updated filtering logic.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-28

View File

@@ -0,0 +1,25 @@
## Context
The frontend is currently built with custom React components and CSS. This has led to a fragmented and inconsistent user interface. The goal is to standardize the UI by adopting `shadcn/ui`.
## Goals / Non-Goals
**Goals:**
- Successfully integrate `shadcn/ui` into the Create React App project.
- Replace the `Dropdown` and `FilterBox` components with `shadcn/ui` components.
- Establish a pattern for using `shadcn/ui` for all future components.
**Non-Goals:**
- We will not be ejecting from Create React App. We will work within its constraints.
- We will not build a complete design system, but rather use `shadcn/ui` as our component foundation.
## Decisions
- **Installation:** We will follow the official `shadcn/ui` documentation for installation, which involves using `npx`.
- **Component Replacement:** We will replace components one by one, starting with `Dropdown` and `FilterBox`.
- **Styling:** `shadcn/ui` uses Tailwind CSS. We will need to install and configure Tailwind CSS in our project.
## Risks / Trade-offs
- **[Risk]** Integrating Tailwind CSS into a Create React App project can be complex. → **Mitigation:** We will follow the official guides for both Tailwind CSS and Create React App.
- **[Trade-off]** `shadcn/ui` is a relatively new library. → **Mitigation:** It has a strong and growing community, and it is backed by Vercel.

View File

@@ -0,0 +1,23 @@
## Why
The current frontend components lack a consistent and modern design. Manually styling components is time-consuming and leads to inconsistencies. Adopting a component library like `shadcn/ui` will provide a professional look and feel, and speed up development.
## What Changes
- Integrate the `shadcn/ui` library into the frontend application.
- Replace existing custom components (like `Dropdown` and `FilterBox`) with their `shadcn/ui` equivalents.
- All new frontend components will be built using `shadcn/ui`.
## Capabilities
### New Capabilities
- `shadcn-integration`: Integrate the `shadcn/ui` library and replace existing components.
### Modified Capabilities
- None
## Impact
- This will be a significant change to the frontend codebase.
- All existing components will be refactored or replaced.
- This will introduce a new dependency on the `shadcn/ui` library.

View File

@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: Shadcn/ui Integration
The system SHALL be integrated with the `shadcn/ui` component library.
#### Scenario: Component Replacement
- **WHEN** the application is rendered
- **THEN** the `Dropdown` and `FilterBox` components SHALL be replaced with their `shadcn/ui` equivalents.
- **THEN** the visual appearance of the components SHALL be consistent with the `shadcn/ui` design system.
### Requirement: Consistent UI
All new frontend components SHALL be built using `shadcn/ui`.
#### Scenario: New Component Development
- **WHEN** a new component is developed
- **THEN** it SHALL be built using components from the `shadcn/ui` library.

View File

@@ -0,0 +1,14 @@
## 1. Setup Environment
- [x] 1.1 Install Tailwind CSS and its dependencies.
- [x] 1.2 Configure Tailwind CSS in the project.
- [x] 1.3 Initialize `shadcn/ui` in the project.
## 2. Replace Components
- [x] 2.1 Replace the `Dropdown` component with the `shadcn/ui` `Select` component.
- [x] 2.2 Replace the `FilterBox` component with a combination of `shadcn/ui` components (e.g., `DropdownMenu` with `Checkbox`).
## 3. Verification
- [x] 3.1 Manually test the new components in the browser.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-28

View File

@@ -0,0 +1,24 @@
## Context
The existing `Dropdown` component is a simple, single-select dropdown. The styling is not well-defined, leading to visual inconsistencies. The user wants to be able to select multiple items from the dropdown and have a more polished UI.
## Goals / Non-Goals
**Goals:**
- Refactor the `Dropdown` component to support multi-selection with checkboxes.
- Fix the styling issues, including text size and adding a clear boundary to the dropdown.
- The refactored component should be reusable.
**Non-Goals:**
- This refactor will not introduce any new third-party libraries. We will use existing CSS and React capabilities.
- We will not be adding complex features like search or filtering within the dropdown itself.
## Decisions
- **State Management:** The `Dropdown` component will manage its own open/closed state. The selected values will be managed by the parent component (`FilterBox`) to allow for greater flexibility.
- **Styling:** We will use CSS modules to ensure the styles are scoped to the component. We will define clear classes for the container, header, list, items, and checkboxes.
- **Component API:** The `Dropdown` component will accept an `options` array, a `selectedValues` array, and an `onSelect` function as props.
## Risks / Trade-offs
- **[Risk]** A complete refactor could introduce new bugs. → **Mitigation:** We will build the new component in isolation first, and then integrate it into `FilterBox`. The user has also requested to skip tests for now, which increases the risk. We will rely on manual testing.

View File

@@ -0,0 +1,23 @@
## Why
The current dropdown component does not meet the desired style and functionality. The text is too large, it lacks a proper boundary, and it doesn't support multiple selections easily.
## What Changes
- Refactor the `Dropdown` component to improve its styling and layout.
- Incorporate checkboxes within the dropdown to allow for multiple selections.
- The component's text size and boundaries will be fixed.
## Capabilities
### New Capabilities
- `multi-select-dropdown`: Implement a dropdown component that supports multiple selections using checkboxes.
- `dropdown-styling-fix`: Correct the styling issues of the dropdown component.
### Modified Capabilities
- None
## Impact
- This will affect the `Dropdown` component and its parent component, `FilterBox`.
- The changes will be contained within the `frontend` directory.

View File

@@ -0,0 +1,27 @@
## ADDED Requirements
### Requirement: Dropdown Styling
The system SHALL ensure the dropdown component has a professional and consistent visual appearance.
#### Scenario: Dropdown Container Styling
- **WHEN** the dropdown is rendered
- **THEN** it SHALL be enclosed in a clearly defined boundary (e.g., a border).
- **THEN** the text within the dropdown SHALL be a reasonable size and not overflow its container.
- **THEN** the dropdown content SHALL have a white background.
- **THEN** the dropdown content SHALL have a width of `w-72`.
#### Scenario: Dropdown Item Styling
- **WHEN** the dropdown is open
- **THEN** the dropdown list SHALL have a background color that distinguishes it from the page content.
- **THEN** hovering over a dropdown item SHALL change its background color to indicate that it is interactive.
### Requirement: Filter Button Styling and Positioning
The system SHALL ensure the filter button is styled correctly and positioned next to the search box.
#### Scenario: Filter Button Styling
- **WHEN** the filter button is rendered
- **THEN** it SHALL have a white background.
#### Scenario: Filter Button Positioning
- **WHEN** the filter button and search box are rendered
- **THEN** they SHALL be positioned next to each other with a small spacing (`space-x-2`).

View File

@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: Multi-Select Dropdown
The system SHALL provide a dropdown component that allows users to select multiple options using checkboxes.
#### Scenario: Select Multiple Options
- **WHEN** a user clicks on the dropdown to open it
- **THEN** each option in the dropdown list SHALL be preceded by a checkbox.
- **WHEN** the user clicks on a checkbox
- **THEN** the state of that checkbox SHALL be toggled (checked/unchecked).
- **WHEN** the user clicks on another checkbox
- **THEN** the state of that checkbox SHALL also be toggled, without affecting the selection of other checkboxes.
#### Scenario: Display Selected Options
- **WHEN** the dropdown is closed
- **THEN** the dropdown header SHALL display a summary of the selected options (e.g., "2 selected").

View File

@@ -0,0 +1,19 @@
## 1. Refactor Dropdown Component
- [x] 1.1 Modify the `Dropdown.js` component to accept `selectedValues` as a prop.
- [x] 1.2 Implement the logic to render checkboxes for each option.
- [x] 1.3 Handle the `onSelect` event to toggle the selection of an option.
## 2. Update Styling
- [x] 2.1 Update `Dropdown.css` to include styles for the checkboxes.
- [x] 2.2 Adjust the styling of the dropdown container and items to fix the boundary and text size issues.
## 3. Update FilterBox Component
- [x] 3.1 Modify `FilterBox.js` to manage the `selectedLabels` state.
- [x] 3.2 Pass the `selectedLabels` and the `onLabelToggle` function to the `Dropdown` component.
## 4. Verification
- [x] 4.1 Manually test the multi-select dropdown in the browser.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-28

View File

@@ -0,0 +1,24 @@
## Context
The current dropdown in `FilterBox.js` is a basic HTML `<select>` element. To align with the shadcn/ui aesthetic, we need a more flexible and styleable component. The shadcn/ui Dropdown Menu is built on top of Radix UI's Dropdown Menu primitive, which provides accessibility and interaction features.
## Goals / Non-Goals
**Goals:**
- Replace the native `<select>` dropdown with a custom component that visually and functionally mimics the shadcn/ui Dropdown Menu.
- Ensure the new component is accessible and reusable.
**Non-Goals:**
- Implement every single feature of the shadcn/ui Dropdown Menu. We will only implement the features currently required by the `FilterBox`.
- Create a full component library. The focus is solely on the dropdown.
## Decisions
- **Component Library:** We will not install the entire `shadcn-ui` library, as that would be overkill. Instead, we will create a new React component that replicates the required styles.
- **Styling:** We will use CSS modules to encapsulate the styles for the new dropdown component, inspired by the styling of shadcn/ui. This avoids conflicts with existing styles.
- **Implementation:** A new component, `Dropdown.js`, will be created. It will manage its own state (e.g., open/closed, selected value). `FilterBox.js` will be updated to use this new `Dropdown` component.
## Risks / Trade-offs
- **[Risk]** Creating a custom component takes more effort than using a pre-built library. → **Mitigation:** The required dropdown functionality is simple, and a custom component gives us full control over the implementation and avoids unnecessary dependencies.
- **[Trade-off]** We are not using the exact shadcn/ui library, so we will have to manually update the component if the shadcn/ui design evolves. → **Mitigation:** The design is for a simple dropdown, which is unlikely to change significantly.

View File

@@ -0,0 +1,21 @@
## Why
The current dropdown component has an outdated and inconsistent style. Adopting a modern, standardized design from a proven design system like shadcn/ui will improve user experience and visual consistency.
## What Changes
- The existing dropdown component will be restyled to match the appearance and behavior of the shadcn/ui Dropdown Menu.
- This may involve replacing the underlying implementation or applying new CSS styles.
## Capabilities
### New Capabilities
- `shadcn-dropdown-style`: Implement a dropdown component styled according to the shadcn/ui design system.
### Modified Capabilities
- None
## Impact
- This will affect all parts of the frontend that use the dropdown component.
- The `frontend/src/components/FilterBox.js` is a likely candidate to be affected.

View File

@@ -0,0 +1,14 @@
## ADDED Requirements
### Requirement: Shadcn-Styled Dropdown Component
The system SHALL provide a dropdown component with a visual style that mimics the shadcn/ui Dropdown Menu.
#### Scenario: Dropdown Rendering
- **WHEN** the dropdown component is rendered
- **THEN** it SHALL display with a style consistent with the shadcn/ui Dropdown Menu, including fonts, colors, and spacing.
#### Scenario: Dropdown Interaction
- **WHEN** the user clicks on the dropdown trigger
- **THEN** a menu of options SHALL appear.
- **WHEN** the user selects an option from the menu
- **THEN** the menu SHALL close and the selected option SHALL be displayed in the trigger.

View File

@@ -0,0 +1,22 @@
## 1. Create the Dropdown Component
- [x] 1.1 Create a new file `frontend/src/components/Dropdown.js`.
- [x] 1.2 Create a corresponding CSS module `frontend/src/components/Dropdown.css`.
- [x] 1.3 Implement the basic structure of the `Dropdown` component in `Dropdown.js`.
- [x] 1.4 Add state management for open/closed state and selected value.
## 2. Style the Dropdown Component
- [x] 2.1 Add styles to `Dropdown.css` to mimic the shadcn/ui Dropdown Menu.
- [x] 2.2 Ensure the styles are responsive and work across different screen sizes.
## 3. Integrate the Dropdown Component
- [x] 3.1 Modify `frontend/src/components/FilterBox.js` to import and use the new `Dropdown` component.
- [x] 3.2 Replace the existing `<select>` element with the `Dropdown` component.
- [x] 3.3 Pass the necessary props (options, onSelect) to the `Dropdown` component.
## 4. Verification
- [x] 4.1 Manually test the dropdown in the browser to ensure it functions as expected.
- [ ] 4.2 Run any existing frontend tests to ensure no regressions have been introduced.