diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d551e1f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +services: + dcm-site: + image: ubuntu:latest + restart: unless-stopped + + environment: + PORT: "3000" + NODE_ENV: production + CLIENT_ORIGIN: https://www.datacentermodeler.com + DEBIAN_FRONTEND: noninteractive + + command: + - bash + - -lc + - | + set -e + apt-get update + apt-get install -y --no-install-recommends \ + python3 \ + make \ + g++ \ + nodejs \ + nano \ + npm \ + git \ + ca-certificates + rm -rf /var/lib/apt/lists/* + if [ -f /apps/package.json ] && [ -d /apps/.git ]; then + cd /apps + git reset --hard + git pull origin main + else + mkdir -p /apps + cd /apps + git init + git remote remove origin 2>/dev/null || true + git remote add origin "https://addb4a1f69126e229fe50ad4a7ea5837b1a3a090@git.datacentermodeler.com/pucketm2016/DCM.git" + git fetch origin main + git checkout -B main origin/main + fi + npm install + npm run build + npm run start + + volumes: + - dcm_data:/apps/data + + networks: + - dcm_internal + +volumes: + dcm_data: + +networks: + dcm_internal: + driver: overlay + attachable: true \ No newline at end of file diff --git a/package.json b/package.json index ab0937b..f50ab24 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,16 @@ "scripts": { "dev": "vite --host 0.0.0.0", "build": "tsc && vite build", - "preview": "vite preview --host 0.0.0.0" + "preview": "vite preview --host 0.0.0.0", + "prestart": "npm run build", + "start": "node server/index.js" }, "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.20", + "express": "^4.22.2", "html2canvas": "^1.4.1", "jspdf": "^4.2.1", "react": "^18.2.0", diff --git a/public/assets/datacenter-command-bg.png b/public/assets/datacenter-command-bg.png new file mode 100644 index 0000000..35c01ec Binary files /dev/null and b/public/assets/datacenter-command-bg.png differ diff --git a/public/favicon.svg b/public/favicon.svg index 5128772..dd2633c 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,8 +1,28 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/api.js b/server/api.js new file mode 100644 index 0000000..69d8e1e --- /dev/null +++ b/server/api.js @@ -0,0 +1,9 @@ +import express from 'express'; + +const router = express.Router(); + +router.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +export default router; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..6aaab9d --- /dev/null +++ b/server/index.js @@ -0,0 +1,23 @@ +import express from 'express'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import apiRouter from './api.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const distDir = path.join(__dirname, '..', 'dist'); +const port = process.env.PORT || 3000; + +const app = express(); + +app.use(express.json()); +app.use('/api', apiRouter); + +app.use(express.static(distDir)); + +app.get('*', (req, res) => { + res.sendFile(path.join(distDir, 'index.html')); +}); + +app.listen(port, () => { + console.log(`Datacenter Modeler listening on http://localhost:${port}`); +}); diff --git a/src/App.tsx b/src/App.tsx index 3d90e14..3f42c3f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,11 +33,13 @@ import type { SelectChangeEvent } from '@mui/material'; import { Add, Cable, + Close, DarkMode, Delete, Dns, Domain, Download, + DragIndicator, FileOpen, Image, Inventory2, @@ -45,9 +47,12 @@ import { OpenInNew, PictureAsPdf, Power, + PushPin, + PushPinOutlined, Router, Save, Security, + Settings, SettingsInputComponent, Storage, Terminal, @@ -85,6 +90,20 @@ const PORT_MAPPED_COMPONENTS = new Set([ 'switch', ]); +const commandPanelSx = (mode: ThemeMode) => ({ + border: `1px solid ${mode === 'dark' ? 'rgba(97, 255, 207, 0.18)' : 'rgba(0, 108, 95, 0.22)'}`, + background: + mode === 'dark' + ? 'linear-gradient(145deg, rgba(9, 18, 20, 0.78), rgba(10, 22, 27, 0.58) 54%, rgba(50, 40, 20, 0.32))' + : 'linear-gradient(145deg, rgba(246, 250, 242, 0.94), rgba(226, 235, 225, 0.88) 54%, rgba(218, 231, 220, 0.82))', + backdropFilter: 'blur(22px) saturate(145%)', + color: 'text.primary', + boxShadow: + mode === 'dark' + ? '0 24px 80px rgba(0, 0, 0, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.06)' + : '0 24px 70px rgba(23, 38, 33, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.72)', +}); + const icons: Record = { rack: , cabinet: , @@ -161,6 +180,11 @@ export default function App() { const [pendingSize, setPendingSize] = useState(42); const [welcomeOpen, setWelcomeOpen] = useState(true); const [donationAmount, setDonationAmount] = useState(5); + const [propertiesOpen, setPropertiesOpen] = useState(true); + const [propertiesDocked, setPropertiesDocked] = useState(true); + const [propertiesPosition, setPropertiesPosition] = useState({ x: 920, y: 112 }); + const dragOffsetRef = useRef({ x: 0, y: 0 }); + const [draggingProperties, setDraggingProperties] = useState(false); const [notice, setNotice] = useState(''); const workspaceRef = useRef(null); const fileInputRef = useRef(null); @@ -170,17 +194,230 @@ export default function App() { createTheme({ palette: { mode: themeMode, - primary: { main: themeMode === 'dark' ? '#7dd3fc' : '#006c9c' }, - secondary: { main: '#f59e0b' }, + primary: { main: themeMode === 'dark' ? '#61ffcf' : '#006c5f' }, + secondary: { main: '#f0b84d' }, + success: { main: '#5cff8d' }, + warning: { main: '#f0b84d' }, + error: { main: '#ff5f6d' }, background: themeMode === 'dark' - ? { default: '#0f1720', paper: '#16212c' } - : { default: '#eef3f7', paper: '#ffffff' }, + ? { default: '#050807', paper: 'rgba(9, 18, 20, 0.76)' } + : { default: '#dfe7df', paper: 'rgba(246, 250, 242, 0.86)' }, + text: + themeMode === 'dark' + ? { primary: '#e6fff7', secondary: 'rgba(198, 229, 216, 0.72)', disabled: 'rgba(198, 229, 216, 0.38)' } + : { primary: '#16231f', secondary: 'rgba(22, 35, 31, 0.68)', disabled: 'rgba(22, 35, 31, 0.36)' }, + divider: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.16)' : 'rgba(0, 108, 95, 0.18)', }, - shape: { borderRadius: 8 }, + shape: { borderRadius: 6 }, typography: { - fontFamily: 'Roboto, Arial, sans-serif', - button: { textTransform: 'none', fontWeight: 700 }, + fontFamily: '"Inter", "Roboto", "Helvetica", Arial, sans-serif', + h6: { letterSpacing: 0, fontWeight: 900 }, + subtitle1: { letterSpacing: 0, fontWeight: 800 }, + subtitle2: { letterSpacing: 0, fontWeight: 800 }, + overline: { + fontFamily: '"Roboto Mono", "SFMono-Regular", Consolas, monospace', + letterSpacing: '0.08em', + fontWeight: 900, + }, + caption: { fontFamily: '"Roboto Mono", "SFMono-Regular", Consolas, monospace' }, + button: { + textTransform: 'none', + fontWeight: 850, + letterSpacing: 0, + fontFamily: '"Roboto Mono", "SFMono-Regular", Consolas, monospace', + }, + }, + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + backgroundColor: themeMode === 'dark' ? '#050807' : '#dfe7df', + backgroundImage: + themeMode === 'dark' + ? 'radial-gradient(circle at 12% 18%, rgba(97, 255, 207, 0.12), transparent 30%), radial-gradient(circle at 84% 12%, rgba(240, 184, 77, 0.1), transparent 28%), url("/assets/datacenter-command-bg.png")' + : 'radial-gradient(circle at 12% 18%, rgba(0, 108, 95, 0.1), transparent 30%), radial-gradient(circle at 84% 12%, rgba(156, 92, 12, 0.1), transparent 28%), url("/assets/datacenter-command-bg.png")', + backgroundAttachment: 'fixed', + backgroundPosition: 'center', + backgroundSize: 'cover', + }, + '*::selection': { + background: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.35)' : 'rgba(0, 108, 95, 0.24)', + }, + '*::-webkit-scrollbar': { width: 10, height: 10 }, + '*::-webkit-scrollbar-thumb': { + backgroundColor: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.28)' : 'rgba(0, 108, 95, 0.28)', + border: '2px solid transparent', + backgroundClip: 'padding-box', + }, + '*::-webkit-scrollbar-track': { backgroundColor: 'rgba(0, 0, 0, 0.16)' }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + color: themeMode === 'dark' ? '#e6fff7' : '#16231f', + background: + themeMode === 'dark' + ? 'linear-gradient(135deg, rgba(6, 14, 15, 0.82), rgba(18, 34, 33, 0.68))' + : 'linear-gradient(135deg, rgba(244, 249, 241, 0.9), rgba(222, 234, 222, 0.76))', + backdropFilter: 'blur(20px) saturate(145%)', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: 5, + minHeight: 36, + boxShadow: 'none', + }, + containedPrimary: { + color: '#04100d', + background: 'linear-gradient(135deg, #61ffcf, #8be9ff)', + boxShadow: '0 0 22px rgba(97, 255, 207, 0.24)', + }, + outlined: { + borderColor: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.28)' : 'rgba(0, 108, 95, 0.28)', + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + borderRadius: 5, + border: themeMode === 'dark' ? '1px solid rgba(97, 255, 207, 0.16)' : '1px solid rgba(0, 108, 95, 0.16)', + backgroundColor: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.06)' : 'rgba(255, 255, 255, 0.42)', + }, + }, + }, + MuiOutlinedInput: { + defaultProps: { + size: 'small', + }, + styleOverrides: { + root: { + borderRadius: 5, + backgroundColor: themeMode === 'dark' ? 'rgba(3, 10, 11, 0.46)' : 'rgba(255, 255, 255, 0.52)', + fontSize: '0.88rem', + '& fieldset': { + borderColor: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.2)' : 'rgba(0, 108, 95, 0.22)', + }, + '&:hover fieldset': { + borderColor: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.45)' : 'rgba(0, 108, 95, 0.42)', + }, + '&.Mui-focused': { + boxShadow: + themeMode === 'dark' + ? '0 0 0 1px rgba(97, 255, 207, 0.22), 0 0 18px rgba(97, 255, 207, 0.12)' + : '0 0 0 1px rgba(0, 108, 95, 0.18)', + }, + }, + input: { + paddingTop: 8, + paddingBottom: 8, + }, + multiline: { + paddingTop: 8, + paddingBottom: 8, + }, + }, + }, + MuiTextField: { + defaultProps: { + size: 'small', + }, + }, + MuiFormControl: { + defaultProps: { + size: 'small', + }, + styleOverrides: { + root: { + minWidth: 0, + }, + }, + }, + MuiInputLabel: { + styleOverrides: { + root: { + fontFamily: '"Roboto Mono", "SFMono-Regular", Consolas, monospace', + fontSize: '0.76rem', + fontWeight: 800, + textTransform: 'none', + }, + }, + }, + MuiSelect: { + defaultProps: { + size: 'small', + }, + styleOverrides: { + select: { + paddingTop: 8, + paddingBottom: 8, + fontSize: '0.88rem', + }, + }, + }, + MuiMenuItem: { + styleOverrides: { + root: { + minHeight: 34, + fontSize: '0.88rem', + fontFamily: '"Roboto Mono", "SFMono-Regular", Consolas, monospace', + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + borderRadius: 4, + fontFamily: '"Roboto Mono", "SFMono-Regular", Consolas, monospace', + fontWeight: 800, + }, + }, + }, + MuiDialog: { + styleOverrides: { + paper: { + ...commandPanelSx(themeMode), + borderRadius: 8, + }, + }, + }, + MuiMenu: { + styleOverrides: { + paper: { + ...commandPanelSx(themeMode), + borderRadius: 6, + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: { + borderColor: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.16)' : 'rgba(0, 108, 95, 0.16)', + }, + }, + }, + MuiLinearProgress: { + styleOverrides: { + root: { + backgroundColor: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.12)' : 'rgba(0, 108, 95, 0.14)', + }, + bar: { + background: 'linear-gradient(90deg, #61ffcf, #f0b84d)', + }, + }, + }, }, }), [themeMode], @@ -202,6 +439,28 @@ export default function App() { localStorage.setItem('datacenter-modeler.theme', themeMode); }, [themeMode]); + useEffect(() => { + if (!draggingProperties) { + return undefined; + } + + const handleMove = (event: MouseEvent) => { + const nextX = clampNumber(event.clientX - dragOffsetRef.current.x, 8, window.innerWidth - 360, propertiesPosition.x); + const nextY = clampNumber(event.clientY - dragOffsetRef.current.y, 8, window.innerHeight - 160, propertiesPosition.y); + setPropertiesPosition({ x: nextX, y: nextY }); + }; + + const handleUp = () => setDraggingProperties(false); + + window.addEventListener('mousemove', handleMove); + window.addEventListener('mouseup', handleUp); + + return () => { + window.removeEventListener('mousemove', handleMove); + window.removeEventListener('mouseup', handleUp); + }; + }, [draggingProperties, propertiesPosition.x, propertiesPosition.y]); + const commitDiagram = (updater: (current: DiagramData) => DiagramData) => { setDiagram((current) => ({ ...updater(current), updatedAt: new Date().toISOString() })); }; @@ -232,6 +491,7 @@ export default function App() { const nextRack = createRack(pendingContainer, diagram.racks.length + 1, pendingSize); commitDiagram((current) => ({ ...current, racks: [...current.racks, nextRack] })); setSelection({ kind: 'rack', rackId: nextRack.id }); + setPropertiesOpen(true); setPendingContainer(null); }; @@ -565,16 +825,84 @@ export default function App() { return ( - - - - - - + *': { position: 'relative', zIndex: 1 }, + }} + > + + + + + + + Datacenter Modeler - - {diagram.racks.length}/{MAX_CONTAINERS} racks and cabinets + + {diagram.racks.length}/{MAX_CONTAINERS} + + {' '} + racks and cabinets + + + {' '} + racks + updateDiagramName(event.target.value)} size="small" label="Diagram" - sx={{ width: { xs: 180, md: 320 }, display: { xs: 'none', sm: 'block' } }} + fullWidth + sx={{ + width: { xs: 180, sm: 340, md: 430, xl: 520 }, + maxWidth: { md: '34vw', xl: 520 }, + display: { xs: 'none', sm: 'block' }, + flex: { sm: '0 0 auto', md: 'none' }, + position: { md: 'absolute' }, + left: { md: '50%' }, + top: { md: '50%' }, + transform: { md: 'translate(-50%, -50%)' }, + zIndex: 2, + '& .MuiInputBase-input': { + textOverflow: 'clip', + }, + }} /> @@ -594,8 +936,24 @@ export default function App() { - @@ -620,8 +978,16 @@ export default function App() { - - + + alpha(muiTheme.palette.background.paper, themeMode === 'dark' ? 0.55 : 0.86), + gap: 2.25, + p: { xs: 1.5, md: 2 }, + border: '1px solid', + borderColor: themeMode === 'dark' ? 'rgba(97, 255, 207, 0.14)' : 'rgba(0, 108, 95, 0.16)', + bgcolor: (muiTheme) => alpha(muiTheme.palette.background.paper, themeMode === 'dark' ? 0.32 : 0.9), + backgroundImage: + themeMode === 'dark' + ? 'linear-gradient(rgba(97, 255, 207, 0.045) 1px, transparent 1px), linear-gradient(90deg, rgba(97, 255, 207, 0.035) 1px, transparent 1px)' + : 'linear-gradient(rgba(0, 108, 95, 0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 108, 95, 0.045) 1px, transparent 1px)', + backgroundSize: '44px 44px', + boxShadow: 'inset 0 0 80px rgba(0, 0, 0, 0.22)', + borderRadius: 1.5, + '&::after': diagram.racks.length + ? { + content: '""', + flex: '0 0 1px', + alignSelf: 'stretch', + } + : undefined, }} > {diagram.racks.length === 0 ? ( - - - No racks in diagram + (theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.16)' : 'rgba(255, 255, 255, 0.62)'), + borderRadius: 1, + }} + > + + Awaiting rack drop + Drag a container from the component rail. ) : ( diagram.racks.map((rack) => ( @@ -660,21 +1053,47 @@ export default function App() { key={rack.id} rack={rack} selected={selection} - onSelectRack={() => setSelection({ kind: 'rack', rackId: rack.id })} - onSelectEquipment={(equipmentId) => setSelection({ kind: 'equipment', rackId: rack.id, equipmentId })} + onSelectRack={() => { + setSelection({ kind: 'rack', rackId: rack.id }); + setPropertiesOpen(true); + }} + onSelectEquipment={(equipmentId) => { + setSelection({ kind: 'equipment', rackId: rack.id, equipmentId }); + setPropertiesOpen(true); + }} onDrop={(event) => handleRackDrop(event, rack.id)} /> )) )} + + {propertiesOpen && ( { + if (propertiesDocked) { + return; + } + const panel = event.currentTarget.closest('[data-properties-panel="true"]'); + const bounds = panel?.getBoundingClientRect(); + dragOffsetRef.current = { + x: event.clientX - (bounds?.left ?? propertiesPosition.x), + y: event.clientY - (bounds?.top ?? propertiesPosition.y), + }; + setDraggingProperties(true); + }} + onClose={() => setPropertiesOpen(false)} + onToggleDock={() => setPropertiesDocked((docked) => !docked)} onDiagramNameChange={updateDiagramName} onRackChange={updateRack} onRackSizeChange={updateRackSize} @@ -682,9 +1101,20 @@ export default function App() { onEquipmentPlacementChange={updateEquipmentPlacement} onDelete={deleteSelection} /> - + )} - setPendingContainer(null)} maxWidth="xs" fullWidth> + setPendingContainer(null)} + maxWidth="xs" + fullWidth + BackdropProps={{ + sx: { + backgroundColor: themeMode === 'dark' ? 'rgba(0, 0, 0, 0.28)' : 'rgba(235, 242, 233, 0.42)', + backdropFilter: 'blur(6px)', + }, + }} + > Add {pendingContainer === 'cabinet' ? 'Cabinet' : 'Rack'} @@ -707,7 +1137,18 @@ export default function App() { - setWelcomeOpen(false)} maxWidth="sm" fullWidth> + setWelcomeOpen(false)} + maxWidth="sm" + fullWidth + BackdropProps={{ + sx: { + backgroundColor: themeMode === 'dark' ? 'rgba(0, 0, 0, 0.28)' : 'rgba(235, 242, 233, 0.42)', + backdropFilter: 'blur(6px)', + }, + }} + > Welcome to Datacenter Modeler @@ -729,7 +1170,9 @@ export default function App() { - This application was made by a homebrew developer. A donation is appreciated if this tool helps you, + This application was made by a homebrew developer ( + theblindengineer.com + ). A donation is appreciated if this tool helps you, but it is absolutely not required. @@ -771,12 +1214,22 @@ export default function App() { ); } -function LibraryPanel() { +function LibraryPanel({ themeMode }: { themeMode: ThemeMode }) { const containers = componentLibrary.filter((item) => item.category === 'container'); const equipment = componentLibrary.filter((item) => item.category === 'equipment'); return ( - + @@ -817,8 +1270,21 @@ function LibraryTile({ item }: { item: (typeof componentLibrary)[number] }) { alignItems: 'center', gap: 1, cursor: 'grab', - borderLeft: 4, + border: '1px solid', + borderColor: alpha(item.color, 0.35), + borderLeft: 3, borderLeftColor: item.color, + color: 'text.primary', + bgcolor: (theme) => (theme.palette.mode === 'dark' ? alpha(item.color, 0.07) : alpha(item.color, 0.13)), + boxShadow: (theme) => `inset 0 1px 0 ${alpha('#ffffff', theme.palette.mode === 'dark' ? 0.05 : 0.55)}`, + transition: 'transform 150ms ease, border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease', + '&:hover': { + transform: 'translateY(-1px)', + borderColor: alpha(item.color, 0.72), + bgcolor: (theme) => (theme.palette.mode === 'dark' ? alpha(item.color, 0.12) : alpha(item.color, 0.2)), + boxShadow: (theme) => + `0 0 22px ${alpha(item.color, theme.palette.mode === 'dark' ? 0.16 : 0.12)}, inset 0 1px 0 ${alpha('#ffffff', theme.palette.mode === 'dark' ? 0.08 : 0.7)}`, + }, '&:active': { cursor: 'grabbing' }, }} > @@ -857,10 +1323,37 @@ function RackView({ width: 270, flex: '0 0 270px', overflow: 'hidden', - borderColor: selected?.kind === 'rack' && selected.rackId === rack.id ? 'primary.main' : 'divider', + borderColor: (theme) => + selected?.kind === 'rack' && selected.rackId === rack.id + ? 'primary.main' + : theme.palette.mode === 'dark' + ? 'rgba(97, 255, 207, 0.16)' + : 'rgba(0, 108, 95, 0.24)', + bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(3, 8, 9, 0.72)' : 'rgba(238, 244, 236, 0.96)'), + borderRadius: 1, + boxShadow: (theme) => + selected?.kind === 'rack' && selected.rackId === rack.id + ? theme.palette.mode === 'dark' + ? '0 0 34px rgba(97, 255, 207, 0.2), 0 18px 40px rgba(0, 0, 0, 0.32)' + : '0 0 0 1px rgba(0, 108, 95, 0.18), 0 16px 34px rgba(23, 38, 33, 0.18)' + : theme.palette.mode === 'dark' + ? '0 18px 40px rgba(0, 0, 0, 0.24)' + : '0 14px 28px rgba(23, 38, 33, 0.14)', + clipPath: 'polygon(0 0, 96% 0, 100% 12px, 100% 100%, 4% 100%, 0 calc(100% - 12px))', }} > - + (theme.palette.mode === 'dark' ? 'rgba(9, 20, 20, 0.78)' : 'rgba(218, 232, 220, 0.96)'), + borderBottom: 1, + borderColor: 'divider', + backgroundImage: (theme) => + theme.palette.mode === 'dark' + ? 'linear-gradient(90deg, rgba(97, 255, 207, 0.12), rgba(240, 184, 77, 0.04))' + : 'linear-gradient(90deg, rgba(0, 108, 95, 0.12), rgba(156, 92, 12, 0.07))', + }} + > @@ -881,7 +1374,7 @@ function RackView({ sx={{ position: 'relative', height: rackHeight, - bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#0b1118' : '#edf2f7'), + bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#030706' : '#edf2e6'), }} > (theme.palette.mode === 'dark' ? '#101820' : '#f8fafc'), + bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#081210' : '#f8faf2'), backgroundImage: (theme) => - `linear-gradient(${alpha(theme.palette.text.primary, 0.08)} 1px, transparent 1px)`, + `linear-gradient(${alpha(theme.palette.primary.main, 0.18)} 1px, transparent 1px), linear-gradient(90deg, ${alpha(theme.palette.primary.main, 0.05)} 1px, transparent 1px)`, backgroundSize: `100% ${RACK_ROW_HEIGHT}px`, borderLeft: 10, borderRight: 10, - borderColor: (theme) => (theme.palette.mode === 'dark' ? '#263442' : '#cbd5e1'), + borderColor: (theme) => (theme.palette.mode === 'dark' ? '#1b332e' : '#b4c7bb'), + boxShadow: 'inset 0 0 36px rgba(0, 0, 0, 0.32)', }} > {Array.from({ length: rack.sizeU }, (_, index) => rack.sizeU - index).map((u) => ( @@ -942,9 +1436,9 @@ function RackView({ height: item.sizeU * RACK_ROW_HEIGHT, minHeight: RACK_ROW_HEIGHT, border: 2, - borderColor: selectedItem ? 'secondary.main' : alpha('#000000', 0.16), - bgcolor: item.color, - color: '#071018', + borderColor: selectedItem ? 'secondary.main' : alpha(item.color, 0.48), + bgcolor: alpha(item.color, 0.9), + color: '#04100d', px: 0.75, display: 'flex', alignItems: 'center', @@ -952,7 +1446,9 @@ function RackView({ gap: 1, cursor: 'grab', overflow: 'hidden', - boxShadow: selectedItem ? 4 : 1, + boxShadow: selectedItem ? `0 0 20px ${alpha(item.color, 0.45)}` : `0 4px 14px ${alpha('#000000', 0.32)}`, + backgroundImage: + 'linear-gradient(180deg, rgba(255, 255, 255, 0.22), transparent 44%), repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.12) 0 1px, transparent 1px 12px)', }} > @@ -982,21 +1478,22 @@ function RackView({ onSelectEquipment(item.id); }} sx={{ - position: 'absolute', + position: 'absolute', top: 6, bottom: 6, width, left: item.pduSide === 'left' ? 3 : 'auto', right: item.pduSide === 'right' ? 3 : 'auto', border: 2, - borderColor: selectedItem ? 'secondary.main' : alpha('#000000', 0.24), - bgcolor: item.color, - color: '#111827', + borderColor: selectedItem ? 'secondary.main' : alpha(item.color, 0.48), + bgcolor: alpha(item.color, 0.9), + color: '#04100d', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'grab', - boxShadow: selectedItem ? 4 : 1, + boxShadow: selectedItem ? `0 0 20px ${alpha(item.color, 0.45)}` : `0 4px 14px ${alpha('#000000', 0.32)}`, + backgroundImage: 'linear-gradient(180deg, rgba(255, 255, 255, 0.22), transparent 44%)', overflow: 'hidden', zIndex: 2, }} @@ -1027,6 +1524,13 @@ function PropertiesPanel({ selectedEquipment, selectedEquipmentRack, selection, + docked, + position, + dragging, + themeMode, + onStartDrag, + onClose, + onToggleDock, onDiagramNameChange, onRackChange, onRackSizeChange, @@ -1039,6 +1543,13 @@ function PropertiesPanel({ selectedEquipment: Equipment | null; selectedEquipmentRack: RackContainer | null; selection: Selection; + docked: boolean; + position: { x: number; y: number }; + dragging: boolean; + themeMode: ThemeMode; + onStartDrag: (event: React.MouseEvent) => void; + onClose: () => void; + onToggleDock: () => void; onDiagramNameChange: (name: string) => void; onRackChange: (rackId: string, patch: Partial) => void; onRackSizeChange: (rackId: string, sizeU: number) => void; @@ -1047,26 +1558,80 @@ function PropertiesPanel({ onDelete: () => void; }) { return ( - - - - + + (theme.palette.mode === 'dark' ? 'rgba(97, 255, 207, 0.05)' : 'rgba(0, 108, 95, 0.08)'), + userSelect: 'none', + }} + > + + + Properties - + {selectedEquipment ? selectedEquipment.name : selectedRack ? selectedRack.name : 'Diagram'} + event.stopPropagation()}> + + + {docked ? : } + + + + + + + + + + + {!selection && ( - + onDiagramNameChange(event.target.value)} fullWidth /> )} {selectedRack && selection?.kind === 'rack' && ( - + onRackChange(selectedRack.id, { name: event.target.value })} fullWidth /> Type @@ -1098,7 +1663,7 @@ function PropertiesPanel({ )} {selectedEquipment && selectedEquipmentRack && selection?.kind === 'equipment' && ( - + )} + ); } diff --git a/src/data/componentLibrary.ts b/src/data/componentLibrary.ts index 660239a..453ce32 100644 --- a/src/data/componentLibrary.ts +++ b/src/data/componentLibrary.ts @@ -1,20 +1,20 @@ import type { LibraryItem } from '../types'; export const componentLibrary: LibraryItem[] = [ - { type: 'rack', category: 'container', label: 'Rack', defaultU: 42, minU: 2, maxU: 42, color: '#607d8b' }, - { type: 'cabinet', category: 'container', label: 'Cabinet', defaultU: 42, minU: 2, maxU: 42, color: '#546e7a' }, - { type: 'switch', category: 'equipment', label: 'Switch', defaultU: 1, minU: 1, maxU: 24, color: '#00acc1' }, - { type: 'router', category: 'equipment', label: 'Router', defaultU: 1, minU: 1, maxU: 24, color: '#26a69a' }, - { type: 'firewall', category: 'equipment', label: 'Firewall', defaultU: 1, minU: 1, maxU: 24, color: '#ef5350' }, - { type: 'server-rack', category: 'equipment', label: 'Server (Rack)', defaultU: 2, minU: 1, maxU: 24, color: '#5c6bc0' }, - { type: 'blade-chassis', category: 'equipment', label: 'Blade Chassis', defaultU: 10, minU: 1, maxU: 24, color: '#7e57c2' }, - { type: 'storage-array', category: 'equipment', label: 'Storage Array', defaultU: 4, minU: 1, maxU: 24, color: '#ffb300' }, - { type: 'storage-switch', category: 'equipment', label: 'Storage Switch', defaultU: 1, minU: 1, maxU: 24, color: '#42a5f5' }, - { type: 'patch-panel', category: 'equipment', label: 'Patch Panel', defaultU: 1, minU: 1, maxU: 24, color: '#8d6e63' }, - { type: 'pdu', category: 'equipment', label: 'PDU', defaultU: 1, minU: 1, maxU: 12, color: '#f97316' }, - { type: 'cable-management', category: 'equipment', label: 'Cable Management', defaultU: 1, minU: 1, maxU: 24, color: '#78909c' }, - { type: 'kvm', category: 'equipment', label: 'KVM', defaultU: 1, minU: 1, maxU: 24, color: '#66bb6a' }, - { type: 'kvm-console', category: 'equipment', label: 'KVM (Console)', defaultU: 1, minU: 1, maxU: 24, color: '#ec407a' }, + { type: 'rack', category: 'container', label: 'Rack', defaultU: 42, minU: 2, maxU: 42, color: '#6f8179' }, + { type: 'cabinet', category: 'container', label: 'Cabinet', defaultU: 42, minU: 2, maxU: 42, color: '#51675f' }, + { type: 'switch', category: 'equipment', label: 'Switch', defaultU: 1, minU: 1, maxU: 24, color: '#61ffcf' }, + { type: 'router', category: 'equipment', label: 'Router', defaultU: 1, minU: 1, maxU: 24, color: '#89ff7a' }, + { type: 'firewall', category: 'equipment', label: 'Firewall', defaultU: 1, minU: 1, maxU: 24, color: '#ff5f6d' }, + { type: 'server-rack', category: 'equipment', label: 'Server (Rack)', defaultU: 2, minU: 1, maxU: 24, color: '#8be9ff' }, + { type: 'blade-chassis', category: 'equipment', label: 'Blade Chassis', defaultU: 10, minU: 1, maxU: 24, color: '#b38cff' }, + { type: 'storage-array', category: 'equipment', label: 'Storage Array', defaultU: 4, minU: 1, maxU: 24, color: '#f0b84d' }, + { type: 'storage-switch', category: 'equipment', label: 'Storage Switch', defaultU: 1, minU: 1, maxU: 24, color: '#4dd7ff' }, + { type: 'patch-panel', category: 'equipment', label: 'Patch Panel', defaultU: 1, minU: 1, maxU: 24, color: '#d9a87b' }, + { type: 'pdu', category: 'equipment', label: 'PDU', defaultU: 1, minU: 1, maxU: 12, color: '#ff9b42' }, + { type: 'cable-management', category: 'equipment', label: 'Cable Management', defaultU: 1, minU: 1, maxU: 24, color: '#9cad9d' }, + { type: 'kvm', category: 'equipment', label: 'KVM', defaultU: 1, minU: 1, maxU: 24, color: '#5cff8d' }, + { type: 'kvm-console', category: 'equipment', label: 'KVM (Console)', defaultU: 1, minU: 1, maxU: 24, color: '#ff7ac8' }, ]; export const getLibraryItem = (type: string | undefined) => diff --git a/src/styles.css b/src/styles.css index 6a9e106..844d6de 100644 --- a/src/styles.css +++ b/src/styles.css @@ -10,7 +10,9 @@ body, } body { - font-family: Roboto, Arial, sans-serif; + min-width: 360px; + font-family: Inter, Roboto, Arial, sans-serif; + font-variant-ligatures: contextual; } button, @@ -18,3 +20,14 @@ input, textarea { font: inherit; } + +a { + color: inherit; + text-decoration-color: rgba(97, 255, 207, 0.55); + text-underline-offset: 3px; +} + +img, +svg { + max-width: 100%; +}