aligned services and path styling
This commit is contained in:
@@ -2,6 +2,7 @@ import { createElement, useState } from 'react'
|
||||
import { X, Edit, Trash2, ExternalLink, Plus, Pencil, Layers, Ungroup, Eye, EyeOff } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { useCanvasStore } from '@/stores/canvasStore'
|
||||
import { NODE_TYPE_LABELS, STATUS_COLORS, type ServiceInfo, type NodeData, type NodeProperty } from '@/types'
|
||||
import { getServiceUrl } from '@/utils/serviceUrl'
|
||||
@@ -663,123 +664,85 @@ function ServiceBadge({ svc, host, onEdit, onRemove }: { svc: ServiceInfo; host?
|
||||
const color = CATEGORY_COLORS[svc.category ?? ''] ?? '#8b949e';
|
||||
const hasPort = svc.port != null;
|
||||
const portLabel = hasPort ? String(svc.port) : '';
|
||||
const pathLabel = svc.path?.trim() ? svc.path.trim() : '';
|
||||
|
||||
const maxTotalWidth = 220;
|
||||
const nameCharWidth = 7.2;
|
||||
const nameWidth = Math.min(svc.service_name.length * nameCharWidth, maxTotalWidth - 60);
|
||||
const pathMaxWidth = Math.max(0, maxTotalWidth - nameWidth - 60);
|
||||
const showPath = pathLabel && pathMaxWidth > 30;
|
||||
|
||||
const [tooltip, setTooltip] = useState<{ visible: boolean; x: number; y: number; text: string }>({ visible: false, x: 0, y: 0, text: '' });
|
||||
const pathLabel = svc.path?.trim() ? svc.path.trim() : ''
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-center border rounded-md text-xs transition-colors px-2 py-1.5 min-w-0"
|
||||
style={{ background: '#21262d', borderColor: '#30363d', position: 'relative' }}
|
||||
className="group flex items-center gap-1 border rounded-md text-xs transition-colors px-2 py-1.5 min-w-0"
|
||||
style={{ background: '#21262d', borderColor: '#30363d' }}
|
||||
>
|
||||
<span className="shrink-0 w-1.5 h-1.5 rounded-full mr-1" style={{ backgroundColor: color }} />
|
||||
{url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium truncate flex-grow min-w-0"
|
||||
style={{ color, marginRight: 12, maxWidth: '100%' }}
|
||||
title={svc.service_name}
|
||||
tabIndex={0}
|
||||
aria-label={svc.service_name}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{svc.service_name}
|
||||
</a>
|
||||
) : (
|
||||
<span
|
||||
className="font-medium truncate flex-grow min-w-0"
|
||||
style={{ color, marginRight: 12, maxWidth: '100%' }}
|
||||
title={svc.service_name}
|
||||
tabIndex={0}
|
||||
aria-label={svc.service_name}
|
||||
>
|
||||
{svc.service_name}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-1 shrink-0" style={{ maxWidth: 180, minWidth: 0 }}>
|
||||
{showPath && (
|
||||
<span
|
||||
className="truncate text-[#8b949e] text-right"
|
||||
style={{ minWidth: 0, maxWidth: pathMaxWidth, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}
|
||||
onMouseEnter={e => {
|
||||
if (e.currentTarget.offsetWidth < e.currentTarget.scrollWidth) {
|
||||
setTooltip({ visible: true, x: e.clientX, y: e.clientY, text: pathLabel });
|
||||
}
|
||||
}}
|
||||
onMouseMove={e => {
|
||||
setTooltip(t => t.visible ? { ...t, x: e.clientX, y: e.clientY } : t);
|
||||
}}
|
||||
onMouseLeave={() => setTooltip(t => ({ ...t, visible: false }))}
|
||||
tabIndex={0}
|
||||
aria-label={pathLabel}
|
||||
>
|
||||
{pathLabel}
|
||||
</span>
|
||||
<span className="shrink-0 w-1.5 h-1.5 rounded-full" style={{ backgroundColor: color }} />
|
||||
{url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium truncate min-w-0 flex-1"
|
||||
style={{ color }}
|
||||
title={svc.service_name}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{svc.service_name}
|
||||
</a>
|
||||
) : (
|
||||
<span
|
||||
className="font-medium truncate min-w-0 flex-1"
|
||||
style={{ color }}
|
||||
title={svc.service_name}
|
||||
>
|
||||
{svc.service_name}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{pathLabel && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="truncate text-[#8b949e] max-w-[80px]"
|
||||
tabIndex={0}
|
||||
aria-label={pathLabel}
|
||||
>
|
||||
{pathLabel}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{pathLabel}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{hasPort && (
|
||||
<span className="font-mono text-[#8b949e]" style={{ whiteSpace: 'nowrap', flexShrink: 0 }}>{portLabel}/{svc.protocol}</span>
|
||||
<span className="font-mono text-[#8b949e] shrink-0">{portLabel}/{svc.protocol}</span>
|
||||
)}
|
||||
{url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex w-2.5 h-2.5 items-center justify-center shrink-0"
|
||||
aria-label="Open service link"
|
||||
style={{ color: 'inherit' }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink size={10} className="text-muted-foreground" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="w-2.5 shrink-0" />
|
||||
)}
|
||||
{url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex w-2.5 h-2.5 items-center justify-center shrink-0"
|
||||
tabIndex={0}
|
||||
aria-label="Open service link"
|
||||
style={{ color: 'inherit' }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink size={10} className="text-muted-foreground" />
|
||||
</a>
|
||||
) : (
|
||||
<span style={{ width: 10, display: 'inline-block' }} />
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onEdit(); }}
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onEdit() }}
|
||||
className="opacity-100 transition-opacity text-[#8b949e] hover:text-[#00d4ff] ml-0.5"
|
||||
title="Edit service"
|
||||
>
|
||||
<Pencil size={10} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onRemove(); }}
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onRemove() }}
|
||||
className="opacity-100 transition-opacity text-[#8b949e] hover:text-[#f85149] ml-0.5"
|
||||
title="Remove service"
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{tooltip.visible && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: tooltip.x - 50,
|
||||
top: tooltip.y + 10,
|
||||
background: '#222',
|
||||
color: '#fff',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none',
|
||||
boxShadow: '0 2px 8px #0008',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'visible'
|
||||
}}
|
||||
>
|
||||
{tooltip.text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,64 +1,30 @@
|
||||
"use client"
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
|
||||
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delay = 0,
|
||||
...props
|
||||
}: TooltipPrimitive.Provider.Props) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delay={delay}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
side = "top",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: TooltipPrimitive.Popup.Props &
|
||||
Pick<
|
||||
TooltipPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset"
|
||||
>) {
|
||||
}: React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
<TooltipPrimitive.Content
|
||||
sideOffset={sideOffset}
|
||||
className="isolate z-50"
|
||||
className={[
|
||||
'z-50 overflow-hidden rounded-md border border-[#30363d] bg-[#161b22] px-2 py-1 text-xs text-[#e6edf3] shadow-md',
|
||||
'animate-in fade-in-0 zoom-in-95',
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
{...props}
|
||||
>
|
||||
<TooltipPrimitive.Popup
|
||||
data-slot="tooltip-content"
|
||||
className={cn(
|
||||
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
||||
</TooltipPrimitive.Popup>
|
||||
</TooltipPrimitive.Positioner>
|
||||
{children}
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user