updated
This commit is contained in:
@@ -103,6 +103,7 @@ async function listContainers() {
|
||||
traefikUrl: traefikUrl,
|
||||
preferredLocalUrl: preferredLocalUrl,
|
||||
accessibleUrls: accessibleUrls,
|
||||
labels: container.Labels,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
16
frontend/components.json
Normal file
16
frontend/components.json
Normal 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
10
frontend/craco.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
style: {
|
||||
postcss: {
|
||||
plugins: () => [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
10
frontend/jsconfig.json
Normal file
10
frontend/jsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
1228
frontend/package-lock.json
generated
1228
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,21 +3,31 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"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/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.13.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-scripts": "5.0.1",
|
||||
"serve": "^14.2.6",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
@@ -37,5 +47,10 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.27",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^3.4.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ body {
|
||||
.service-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
/* Removed justify-content: center; */
|
||||
padding: 40px; /* Adjusted padding */
|
||||
gap: 25px; /* Increased gap */
|
||||
max-width: none; /* Allow full width */
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@@ -49,3 +50,28 @@ body {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import './App.css';
|
||||
import { getServices } from './api';
|
||||
import ServiceCard from './components/ServiceCard';
|
||||
import SearchBox from './components/SearchBox'; // Import SearchBox
|
||||
import FilterBox from './components/FilterBox'; // Import FilterBox
|
||||
|
||||
function App() {
|
||||
const [services, setServices] = useState([]);
|
||||
const [searchTerm, setSearchTerm] = useState(''); // Add state for search term
|
||||
const [selectedLabels, setSelectedLabels] = useState([]);
|
||||
|
||||
async function fetchServices() {
|
||||
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 (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<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>
|
||||
<main className="service-grid">
|
||||
{services.map(service => (
|
||||
{filteredServices.map(service => (
|
||||
<ServiceCard key={service.id} service={service} />
|
||||
))}
|
||||
</main>
|
||||
|
||||
50
frontend/src/components/Dropdown.css
Normal file
50
frontend/src/components/Dropdown.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
39
frontend/src/components/Dropdown.js
Normal file
39
frontend/src/components/Dropdown.js
Normal 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;
|
||||
35
frontend/src/components/FilterBox.js
Normal file
35
frontend/src/components/FilterBox.js
Normal 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;
|
||||
|
||||
16
frontend/src/components/SearchBox.js
Normal file
16
frontend/src/components/SearchBox.js
Normal 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;
|
||||
@@ -5,7 +5,8 @@
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* More pronounced shadow */
|
||||
padding: 25px; /* Increased padding */
|
||||
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 */
|
||||
}
|
||||
|
||||
|
||||
47
frontend/src/components/ui/button.jsx
Normal file
47
frontend/src/components/ui/button.jsx
Normal 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 }
|
||||
24
frontend/src/components/ui/checkbox.jsx
Normal file
24
frontend/src/components/ui/checkbox.jsx
Normal 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 }
|
||||
155
frontend/src/components/ui/dropdown-menu.jsx
Normal file
155
frontend/src/components/ui/dropdown-menu.jsx
Normal 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,
|
||||
}
|
||||
120
frontend/src/components/ui/select.jsx
Normal file
120
frontend/src/components/ui/select.jsx
Normal 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,
|
||||
}
|
||||
@@ -1,3 +1,75 @@
|
||||
@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',
|
||||
@@ -11,3 +83,18 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
6
frontend/src/lib/utils.js
Normal file
6
frontend/src/lib/utils.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
10
frontend/tailwind.config.js
Normal file
10
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-27
|
||||
36
openspec/changes/add-service-search-and-filter/design.md
Normal file
36
openspec/changes/add-service-search-and-filter/design.md
Normal 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.
|
||||
27
openspec/changes/add-service-search-and-filter/proposal.md
Normal file
27
openspec/changes/add-service-search-and-filter/proposal.md
Normal 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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
29
openspec/changes/add-service-search-and-filter/tasks.md
Normal file
29
openspec/changes/add-service-search-and-filter/tasks.md
Normal 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.
|
||||
2
openspec/changes/improve-ui-and-filter/.openspec.yaml
Normal file
2
openspec/changes/improve-ui-and-filter/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-27
|
||||
37
openspec/changes/improve-ui-and-filter/design.md
Normal file
37
openspec/changes/improve-ui-and-filter/design.md
Normal 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.
|
||||
24
openspec/changes/improve-ui-and-filter/proposal.md
Normal file
24
openspec/changes/improve-ui-and-filter/proposal.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
30
openspec/changes/improve-ui-and-filter/tasks.md
Normal file
30
openspec/changes/improve-ui-and-filter/tasks.md
Normal 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.
|
||||
2
openspec/changes/integrate-shadcn-library/.openspec.yaml
Normal file
2
openspec/changes/integrate-shadcn-library/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-28
|
||||
25
openspec/changes/integrate-shadcn-library/design.md
Normal file
25
openspec/changes/integrate-shadcn-library/design.md
Normal 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.
|
||||
23
openspec/changes/integrate-shadcn-library/proposal.md
Normal file
23
openspec/changes/integrate-shadcn-library/proposal.md
Normal 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.
|
||||
@@ -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.
|
||||
14
openspec/changes/integrate-shadcn-library/tasks.md
Normal file
14
openspec/changes/integrate-shadcn-library/tasks.md
Normal 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.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-28
|
||||
24
openspec/changes/refactor-dropdown-with-checkboxes/design.md
Normal file
24
openspec/changes/refactor-dropdown-with-checkboxes/design.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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`).
|
||||
@@ -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").
|
||||
19
openspec/changes/refactor-dropdown-with-checkboxes/tasks.md
Normal file
19
openspec/changes/refactor-dropdown-with-checkboxes/tasks.md
Normal 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.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-28
|
||||
24
openspec/changes/update-dropdown-style-for-shadcn/design.md
Normal file
24
openspec/changes/update-dropdown-style-for-shadcn/design.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
22
openspec/changes/update-dropdown-style-for-shadcn/tasks.md
Normal file
22
openspec/changes/update-dropdown-style-for-shadcn/tasks.md
Normal 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.
|
||||
Reference in New Issue
Block a user