mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 07:36:40 +00:00
feat(app-store): add port conflict indicator to deploy sheet (#533)
Show a pulsating warning dot next to host ports that are already in use by a running container. Hovering over the dot reveals which Sencho-managed stack or external app occupies the port. Adds GET /api/ports/in-use endpoint that returns a map of bound host ports with ownership info, and a getPortsInUse method on DockerController that reuses the existing container-to-stack resolution logic.
This commit is contained in:
@@ -12,6 +12,7 @@ import { cn } from '@/lib/utils';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { useNodes } from '@/context/NodeContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { CursorProvider, CursorContainer, Cursor, CursorFollow } from '@/components/animate-ui/primitives/animate/cursor';
|
||||
|
||||
function isValidPort(value: string): boolean {
|
||||
if (!value) return true;
|
||||
@@ -47,6 +48,11 @@ interface Template {
|
||||
source?: string;
|
||||
}
|
||||
|
||||
interface PortInUseInfo {
|
||||
stack: string | null;
|
||||
container: string;
|
||||
}
|
||||
|
||||
interface AppStoreViewProps {
|
||||
onDeploySuccess: (stackName: string) => void;
|
||||
}
|
||||
@@ -70,6 +76,7 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
|
||||
const [volVars, setVolVars] = useState<Record<string, string>>({});
|
||||
const [customEnvs, setCustomEnvs] = useState<Array<{ key: string, value: string }>>([]);
|
||||
const [newEnvKey, setNewEnvKey] = useState('');
|
||||
const [portsInUse, setPortsInUse] = useState<Record<string, PortInUseInfo>>({});
|
||||
const [newEnvVal, setNewEnvVal] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -139,6 +146,10 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
|
||||
.replace(/^-|-$/g, '');
|
||||
setStackName(defaultName);
|
||||
|
||||
// Fetch currently bound host ports for conflict detection
|
||||
setPortsInUse({});
|
||||
apiFetch('/ports/in-use').then(r => r.ok ? r.json() : {}).then(setPortsInUse).catch(err => console.error('[AppStore] Failed to fetch ports in use:', err));
|
||||
|
||||
setIsSheetOpen(true);
|
||||
};
|
||||
|
||||
@@ -424,17 +435,41 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
|
||||
{selectedTemplate.ports.map((p, idx) => {
|
||||
const parts = p.split(':');
|
||||
if (parts.length < 2) return null;
|
||||
const hostPort = portVars[p] || '';
|
||||
const conflict = hostPort ? portsInUse[hostPort] : undefined;
|
||||
return (
|
||||
<div key={idx} className="flex items-center space-x-2">
|
||||
<Input
|
||||
value={portVars[p] || ''}
|
||||
value={hostPort}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value.replace(/[^0-9]/g, '');
|
||||
setPortVars(prev => ({ ...prev, [p]: val }));
|
||||
}}
|
||||
className={cn("w-24 text-center font-mono", portVars[p] && !isValidPort(portVars[p]) && "border-destructive")}
|
||||
className={cn("w-24 text-center font-mono", hostPort && !isValidPort(hostPort) && "border-destructive")}
|
||||
/>
|
||||
<span className="text-muted-foreground font-mono">: {parts[1]}</span>
|
||||
{conflict ? (
|
||||
<CursorProvider>
|
||||
<CursorContainer className="inline-flex items-center gap-2">
|
||||
<span className="text-muted-foreground font-mono">: {parts[1]}</span>
|
||||
<span className="w-2 h-2 rounded-full bg-warning animate-pulse" />
|
||||
</CursorContainer>
|
||||
<Cursor>
|
||||
<div className="h-2 w-2 rounded-full bg-brand" />
|
||||
</Cursor>
|
||||
<CursorFollow side="bottom" sideOffset={4} align="center" transition={{ stiffness: 400, damping: 40, bounce: 0 }}>
|
||||
<div className="rounded-md border border-card-border bg-popover/95 backdrop-blur-[10px] backdrop-saturate-[1.15] px-2.5 py-1.5 shadow-md">
|
||||
<span className="font-mono tabular-nums text-xs text-stat-value">
|
||||
{conflict.stack !== null
|
||||
? <>Port {hostPort} used by <span className="text-brand">{conflict.stack}</span></>
|
||||
: <>Port {hostPort} used by an external app</>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</CursorFollow>
|
||||
</CursorProvider>
|
||||
) : (
|
||||
<span className="text-muted-foreground font-mono">: {parts[1]}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user