fix(rack): drop the cable type filter, and dress the visibility select like the header
Two native <select> elements sat in a header made of ghost buttons, and they looked it: their own border and background, an OS chevron, an unthemed popup. The type filter goes entirely — state, action and the CableLayer branch that read it, plus CABLE_TYPE_LABELS which had no other consumer. Filtering the cabling down to copper or fibre answered a question nobody asks of a rack they are looking at; the type still follows the port a patch starts from, which is where it matters. The visibility control becomes the shared ui/select, with a trigger sized and coloured like the buttons beside it — transparent until hovered — and an icon per option: pointer for on-hover, eye for always, crossed eye for hidden. Tests cover the trigger reflecting the active visibility, a pick writing it to the store, and CableLayer drawing ethernet and fibre alike. base-ui commits a selection on a real pointer sequence rather than a bare click, so that one goes through userEvent. ha-relevant: no
This commit is contained in:
committed by
Pouzor - Rémy Jardient
parent
a70fc248bf
commit
b1186bd7dd
@@ -6,22 +6,29 @@
|
||||
* much of the cabling is on screen.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import { Cable, Link2, Plus, Trash2, X } from 'lucide-react'
|
||||
import { Cable, Eye, EyeOff, Link2, MousePointer2, Plus, Trash2, X } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { useRackStore } from '@/rack/store'
|
||||
import { useRackPalette } from '@/rack/rackTheme'
|
||||
import { loadNetworkLinks } from '@/rack/networkLinks'
|
||||
import { CABLE_TYPE_LABELS } from '@/rack/rackDefaults'
|
||||
import { useDesignStore } from '@/stores/designStore'
|
||||
import type { CableType, CableVisibility } from '@/types'
|
||||
import type { CableVisibility } from '@/types'
|
||||
|
||||
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
||||
|
||||
const ghost =
|
||||
'gap-1.5 text-muted-foreground hover:text-foreground cursor-pointer hover:bg-[#21262d]'
|
||||
const selectClass =
|
||||
'rounded border border-border bg-[#0d1117] px-2 py-1 text-xs text-foreground outline-none focus:border-[#00d4ff] cursor-pointer'
|
||||
/** Reads as one of the ghost buttons around it: no box until you hover it. */
|
||||
const triggerClass =
|
||||
'gap-1.5 border-transparent bg-transparent px-2 text-xs font-medium text-muted-foreground hover:bg-[#21262d] hover:text-foreground dark:bg-transparent dark:hover:bg-[#21262d] cursor-pointer'
|
||||
|
||||
const VISIBILITY_OPTIONS: { value: CableVisibility; label: string; icon: typeof Eye }[] = [
|
||||
{ value: 'hover', label: 'Cables on hover', icon: MousePointer2 },
|
||||
{ value: 'always', label: 'Cables always', icon: Eye },
|
||||
{ value: 'hidden', label: 'Cables hidden', icon: EyeOff },
|
||||
]
|
||||
|
||||
export function RackToolbarActions() {
|
||||
const palette = useRackPalette()
|
||||
@@ -30,8 +37,6 @@ export function RackToolbarActions() {
|
||||
const toggleCableMode = useRackStore((s) => s.toggleCableMode)
|
||||
const cableVisibility = useRackStore((s) => s.cableVisibility)
|
||||
const setCableVisibility = useRackStore((s) => s.setCableVisibility)
|
||||
const cableTypeFilter = useRackStore((s) => s.cableTypeFilter)
|
||||
const setCableTypeFilter = useRackStore((s) => s.setCableTypeFilter)
|
||||
const cableDraft = useRackStore((s) => s.cableDraft)
|
||||
const cancelCableDraft = useRackStore((s) => s.cancelCableDraft)
|
||||
const selectedCableId = useRackStore((s) => s.selectedCableId)
|
||||
@@ -95,30 +100,23 @@ export function RackToolbarActions() {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<select
|
||||
className={selectClass}
|
||||
aria-label="Cable visibility"
|
||||
value={cableVisibility}
|
||||
onChange={(e) => setCableVisibility(e.target.value as CableVisibility)}
|
||||
>
|
||||
<option value="hover">Cables on hover</option>
|
||||
<option value="always">Cables always</option>
|
||||
<option value="hidden">Cables hidden</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
className={selectClass}
|
||||
aria-label="Cable type filter"
|
||||
value={cableTypeFilter}
|
||||
onChange={(e) => setCableTypeFilter(e.target.value as CableType | 'all')}
|
||||
>
|
||||
<option value="all">All types</option>
|
||||
{(Object.keys(CABLE_TYPE_LABELS) as CableType[]).map((t) => (
|
||||
<option key={t} value={t}>
|
||||
{CABLE_TYPE_LABELS[t]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select value={cableVisibility} onValueChange={(v) => setCableVisibility(v as CableVisibility)}>
|
||||
<SelectTrigger size="sm" className={triggerClass} aria-label="Cable visibility">
|
||||
<SelectValue>
|
||||
{(() => {
|
||||
const { icon: Icon, label } = VISIBILITY_OPTIONS.find((o) => o.value === cableVisibility)!
|
||||
return <><Icon size={14} /> {label}</>
|
||||
})()}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#21262d] border-[#30363d]">
|
||||
{VISIBILITY_OPTIONS.map(({ value, icon: Icon, label }) => (
|
||||
<SelectItem key={value} value={value} className="text-xs">
|
||||
<Icon size={14} /> {label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{!STANDALONE && (
|
||||
<Button
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { RackToolbarActions } from '../RackToolbarActions'
|
||||
import { useRackStore } from '@/rack/store'
|
||||
|
||||
@@ -15,6 +16,32 @@ beforeEach(() => {
|
||||
|
||||
const unplug = () => screen.queryByRole('button', { name: /unplug/i })
|
||||
|
||||
describe('RackToolbarActions cable visibility', () => {
|
||||
it('shows the active visibility on the trigger', () => {
|
||||
useRackStore.getState().setCableVisibility('always')
|
||||
render(<RackToolbarActions />)
|
||||
expect(screen.getByLabelText('Cable visibility')).toHaveTextContent('Cables always')
|
||||
})
|
||||
|
||||
it('applies the picked visibility to the store', async () => {
|
||||
// base-ui commits its selection on real pointer sequences, not a bare click.
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 })
|
||||
render(<RackToolbarActions />)
|
||||
|
||||
await user.click(screen.getByLabelText('Cable visibility'))
|
||||
await user.click(await screen.findByRole('option', { name: 'Cables hidden' }))
|
||||
|
||||
expect(useRackStore.getState().cableVisibility).toBe('hidden')
|
||||
})
|
||||
|
||||
it('carries no cable type filter — copper and fibre always draw together', () => {
|
||||
render(<RackToolbarActions />)
|
||||
expect(screen.queryByLabelText('Cable type filter')).toBeNull()
|
||||
expect(screen.queryByText('All types')).toBeNull()
|
||||
expect('cableTypeFilter' in useRackStore.getState()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('RackToolbarActions cable selection', () => {
|
||||
it('hides Unplug outside patch mode', () => {
|
||||
useRackStore.getState().selectCable(useRackStore.getState().cables[0].id)
|
||||
|
||||
@@ -60,7 +60,7 @@ types in `@/types/rack`, narrowing every enum on the way in.
|
||||
|
||||
## Panel behaviour in rack mode
|
||||
|
||||
- **Header**: undo/redo, auto layout, YAML import/export, MD and live View are hidden. Add Rack, Patch, cable visibility, cable type filter and Import links replace them. PNG export and Save stay.
|
||||
- **Header**: undo/redo, auto layout, YAML import/export, MD and live View are hidden. Add Rack, Patch, cable visibility and Import links replace them. PNG export and Save stay.
|
||||
- **Left rail**: the design switcher, Device Inventory, Scan History, Settings and Logout stay. The node/zone/text/scan/import block becomes the accessory tray plus **+ Device**. Devices themselves are not listed there — they live in the Device Inventory modal. The footer counts racks, mounts, cables and free U instead of online/offline nodes.
|
||||
- **Right rail**: none. A mount is edited in `RackDeviceModal`, a rack in `RackSettingsModal`, so the canvas keeps the full width.
|
||||
|
||||
|
||||
@@ -81,6 +81,15 @@ describe('CableLayer selection', () => {
|
||||
expect(band.querySelector('path')!.getAttribute('stroke-dasharray')).toBe('5 4')
|
||||
})
|
||||
|
||||
it('draws copper and fibre alike — there is no type filter any more', () => {
|
||||
useRackStore.getState().toggleCableMode()
|
||||
const { cables } = useRackStore.getState()
|
||||
expect(new Set(cables.map((c) => c.type))).toEqual(new Set(['ethernet', 'fiber']))
|
||||
|
||||
const { container } = render(<CableLayer />)
|
||||
expect(hitPaths(container)).toHaveLength(cables.length)
|
||||
})
|
||||
|
||||
it('draws the selected cable with a highlight halo', () => {
|
||||
useRackStore.getState().toggleCableMode()
|
||||
const { container, rerender } = render(<CableLayer />)
|
||||
|
||||
@@ -29,7 +29,6 @@ export function CableLayer() {
|
||||
const devices = useRackStore((s) => s.devices)
|
||||
const cables = useRackStore((s) => s.cables)
|
||||
const visibility = useRackStore((s) => s.cableVisibility)
|
||||
const typeFilter = useRackStore((s) => s.cableTypeFilter)
|
||||
const hoveredDeviceId = useRackStore((s) => s.hoveredDeviceId)
|
||||
const selectedDeviceId = useRackStore((s) => s.selectedDeviceId)
|
||||
const cableMode = useRackStore((s) => s.cableMode)
|
||||
@@ -57,7 +56,6 @@ export function CableLayer() {
|
||||
|
||||
const visible: Resolved[] = []
|
||||
for (const cable of cables) {
|
||||
if (typeFilter !== 'all' && cable.type !== typeFilter) continue
|
||||
if (!showAll) {
|
||||
if (!focus) continue
|
||||
if (cable.from.deviceId !== focus && cable.to.deviceId !== focus) continue
|
||||
|
||||
@@ -14,11 +14,6 @@ export const CABLE_COLORS: Record<CableType, string> = {
|
||||
fiber: '#f0a500',
|
||||
}
|
||||
|
||||
export const CABLE_TYPE_LABELS: Record<CableType, string> = {
|
||||
ethernet: 'Ethernet',
|
||||
fiber: 'Fiber',
|
||||
}
|
||||
|
||||
/** Cable type implied by the port a patch starts from. */
|
||||
export const PORT_CABLE_TYPE: Record<PortType, CableType> = {
|
||||
rj45: 'ethernet',
|
||||
|
||||
@@ -112,7 +112,6 @@ interface RackState {
|
||||
* `moved` separates a drag from a plain click on the same port.
|
||||
*/
|
||||
cableDrag: { pointer: { x: number; y: number } | null; moved: boolean } | null
|
||||
cableTypeFilter: CableType | 'all'
|
||||
/**
|
||||
* Open editor dialogs. Device and rack settings live in modals, so any
|
||||
* component (sidebar, canvas, header) can open them without prop drilling.
|
||||
@@ -189,7 +188,6 @@ interface RackState {
|
||||
removeSelectedCable: () => void
|
||||
hoverDevice: (id: string | null) => void
|
||||
setCableVisibility: (v: CableVisibility) => void
|
||||
setCableTypeFilter: (t: CableType | 'all') => void
|
||||
toggleCableMode: () => void
|
||||
pickPort: (deviceId: string, portId: string) => void
|
||||
/**
|
||||
@@ -235,7 +233,6 @@ function emptyState() {
|
||||
cableMode: false,
|
||||
cableDraft: null,
|
||||
cableDrag: null,
|
||||
cableTypeFilter: 'all' as const,
|
||||
networkImportDone: false,
|
||||
deviceEditor: null,
|
||||
rackEditorId: null,
|
||||
@@ -695,7 +692,6 @@ export const useRackStore = create<RackState>((set, get) => {
|
||||
},
|
||||
hoverDevice: (id) => set({ hoveredDeviceId: id }),
|
||||
setCableVisibility: (v) => set({ cableVisibility: v, cableVisibilityBeforePatch: null }),
|
||||
setCableTypeFilter: (t) => set({ cableTypeFilter: t }),
|
||||
|
||||
toggleCableMode: () =>
|
||||
set((s) => {
|
||||
|
||||
Reference in New Issue
Block a user