Post UX Change
This commit is contained in:
57
docker-compose.yml
Normal file
57
docker-compose.yml
Normal file
@@ -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
|
||||||
@@ -6,13 +6,16 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
"build": "tsc && vite build",
|
"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": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.5",
|
"@emotion/styled": "^11.11.5",
|
||||||
"@mui/icons-material": "^5.15.20",
|
"@mui/icons-material": "^5.15.20",
|
||||||
"@mui/material": "^5.15.20",
|
"@mui/material": "^5.15.20",
|
||||||
|
"express": "^4.22.2",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
BIN
public/assets/datacenter-command-bg.png
Normal file
BIN
public/assets/datacenter-command-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -1,8 +1,28 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
<rect width="64" height="64" rx="12" fill="#0f1720"/>
|
<defs>
|
||||||
<rect x="15" y="10" width="34" height="44" rx="4" fill="#1e293b" stroke="#7dd3fc" stroke-width="3"/>
|
<linearGradient id="bg" x1="8" y1="4" x2="56" y2="60" gradientUnits="userSpaceOnUse">
|
||||||
<path d="M21 18h22M21 26h22M21 34h22M21 42h22" stroke="#f59e0b" stroke-width="3" stroke-linecap="round"/>
|
<stop stop-color="#06100e"/>
|
||||||
<circle cx="25" cy="50" r="2" fill="#22c55e"/>
|
<stop offset=".58" stop-color="#0b201d"/>
|
||||||
<circle cx="32" cy="50" r="2" fill="#7dd3fc"/>
|
<stop offset="1" stop-color="#211a0c"/>
|
||||||
<circle cx="39" cy="50" r="2" fill="#ef4444"/>
|
</linearGradient>
|
||||||
|
<linearGradient id="edge" x1="13" y1="10" x2="51" y2="54" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#61ffcf"/>
|
||||||
|
<stop offset=".55" stop-color="#8be9ff"/>
|
||||||
|
<stop offset="1" stop-color="#f0b84d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-40%" y="-40%" width="180%" height="180%">
|
||||||
|
<feGaussianBlur stdDeviation="2.2" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<path d="M8 4h43l9 9v43l-4 4H13L4 51V8l4-4z" fill="url(#bg)"/>
|
||||||
|
<path d="M14 11h33l6 6v36H18l-7-7V14l3-3z" fill="#07110f" stroke="url(#edge)" stroke-width="2.8" filter="url(#glow)"/>
|
||||||
|
<path d="M22 19h20M22 27h20M22 35h20M22 43h20" stroke="#f0b84d" stroke-width="2.8" stroke-linecap="square"/>
|
||||||
|
<path d="M17 18v28M47 18v28" stroke="#61ffcf" stroke-width="2" stroke-linecap="square" opacity=".9"/>
|
||||||
|
<circle cx="24" cy="50" r="2" fill="#5cff8d"/>
|
||||||
|
<circle cx="32" cy="50" r="2" fill="#61ffcf"/>
|
||||||
|
<circle cx="40" cy="50" r="2" fill="#ff5f6d"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 482 B After Width: | Height: | Size: 1.3 KiB |
9
server/api.js
Normal file
9
server/api.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
23
server/index.js
Normal file
23
server/index.js
Normal file
@@ -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}`);
|
||||||
|
});
|
||||||
696
src/App.tsx
696
src/App.tsx
@@ -33,11 +33,13 @@ import type { SelectChangeEvent } from '@mui/material';
|
|||||||
import {
|
import {
|
||||||
Add,
|
Add,
|
||||||
Cable,
|
Cable,
|
||||||
|
Close,
|
||||||
DarkMode,
|
DarkMode,
|
||||||
Delete,
|
Delete,
|
||||||
Dns,
|
Dns,
|
||||||
Domain,
|
Domain,
|
||||||
Download,
|
Download,
|
||||||
|
DragIndicator,
|
||||||
FileOpen,
|
FileOpen,
|
||||||
Image,
|
Image,
|
||||||
Inventory2,
|
Inventory2,
|
||||||
@@ -45,9 +47,12 @@ import {
|
|||||||
OpenInNew,
|
OpenInNew,
|
||||||
PictureAsPdf,
|
PictureAsPdf,
|
||||||
Power,
|
Power,
|
||||||
|
PushPin,
|
||||||
|
PushPinOutlined,
|
||||||
Router,
|
Router,
|
||||||
Save,
|
Save,
|
||||||
Security,
|
Security,
|
||||||
|
Settings,
|
||||||
SettingsInputComponent,
|
SettingsInputComponent,
|
||||||
Storage,
|
Storage,
|
||||||
Terminal,
|
Terminal,
|
||||||
@@ -85,6 +90,20 @@ const PORT_MAPPED_COMPONENTS = new Set<ComponentType>([
|
|||||||
'switch',
|
'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<ComponentType, JSX.Element> = {
|
const icons: Record<ComponentType, JSX.Element> = {
|
||||||
rack: <Dns fontSize="small" />,
|
rack: <Dns fontSize="small" />,
|
||||||
cabinet: <Domain fontSize="small" />,
|
cabinet: <Domain fontSize="small" />,
|
||||||
@@ -161,6 +180,11 @@ export default function App() {
|
|||||||
const [pendingSize, setPendingSize] = useState(42);
|
const [pendingSize, setPendingSize] = useState(42);
|
||||||
const [welcomeOpen, setWelcomeOpen] = useState(true);
|
const [welcomeOpen, setWelcomeOpen] = useState(true);
|
||||||
const [donationAmount, setDonationAmount] = useState(5);
|
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 [notice, setNotice] = useState('');
|
||||||
const workspaceRef = useRef<HTMLDivElement>(null);
|
const workspaceRef = useRef<HTMLDivElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -170,17 +194,230 @@ export default function App() {
|
|||||||
createTheme({
|
createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: themeMode,
|
mode: themeMode,
|
||||||
primary: { main: themeMode === 'dark' ? '#7dd3fc' : '#006c9c' },
|
primary: { main: themeMode === 'dark' ? '#61ffcf' : '#006c5f' },
|
||||||
secondary: { main: '#f59e0b' },
|
secondary: { main: '#f0b84d' },
|
||||||
|
success: { main: '#5cff8d' },
|
||||||
|
warning: { main: '#f0b84d' },
|
||||||
|
error: { main: '#ff5f6d' },
|
||||||
background:
|
background:
|
||||||
themeMode === 'dark'
|
themeMode === 'dark'
|
||||||
? { default: '#0f1720', paper: '#16212c' }
|
? { default: '#050807', paper: 'rgba(9, 18, 20, 0.76)' }
|
||||||
: { default: '#eef3f7', paper: '#ffffff' },
|
: { 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: {
|
typography: {
|
||||||
fontFamily: 'Roboto, Arial, sans-serif',
|
fontFamily: '"Inter", "Roboto", "Helvetica", Arial, sans-serif',
|
||||||
button: { textTransform: 'none', fontWeight: 700 },
|
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],
|
[themeMode],
|
||||||
@@ -202,6 +439,28 @@ export default function App() {
|
|||||||
localStorage.setItem('datacenter-modeler.theme', themeMode);
|
localStorage.setItem('datacenter-modeler.theme', themeMode);
|
||||||
}, [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) => {
|
const commitDiagram = (updater: (current: DiagramData) => DiagramData) => {
|
||||||
setDiagram((current) => ({ ...updater(current), updatedAt: new Date().toISOString() }));
|
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);
|
const nextRack = createRack(pendingContainer, diagram.racks.length + 1, pendingSize);
|
||||||
commitDiagram((current) => ({ ...current, racks: [...current.racks, nextRack] }));
|
commitDiagram((current) => ({ ...current, racks: [...current.racks, nextRack] }));
|
||||||
setSelection({ kind: 'rack', rackId: nextRack.id });
|
setSelection({ kind: 'rack', rackId: nextRack.id });
|
||||||
|
setPropertiesOpen(true);
|
||||||
setPendingContainer(null);
|
setPendingContainer(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -565,16 +825,84 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column', bgcolor: 'background.default' }}>
|
<Box
|
||||||
<AppBar position="static" color="default" elevation={0} sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
sx={{
|
||||||
<Toolbar sx={{ minHeight: 64, gap: 1.5 }}>
|
height: '100vh',
|
||||||
<Dns color="primary" />
|
display: 'flex',
|
||||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
flexDirection: 'column',
|
||||||
<Typography variant="h6" sx={{ fontWeight: 800, lineHeight: 1.1 }}>
|
bgcolor: 'transparent',
|
||||||
|
color: 'text.primary',
|
||||||
|
p: { xs: 1, md: 1.5 },
|
||||||
|
gap: { xs: 1, md: 1.5 },
|
||||||
|
overflow: 'hidden',
|
||||||
|
'&::before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
background:
|
||||||
|
'linear-gradient(rgba(97, 255, 207, 0.025) 50%, transparent 50%), radial-gradient(circle at 50% 100%, rgba(240, 184, 77, 0.08), transparent 34%)',
|
||||||
|
backgroundSize: '100% 4px, 100% 100%',
|
||||||
|
mixBlendMode: themeMode === 'dark' ? 'screen' : 'multiply',
|
||||||
|
opacity: themeMode === 'dark' ? 0.72 : 0.18,
|
||||||
|
zIndex: 0,
|
||||||
|
},
|
||||||
|
'& > *': { position: 'relative', zIndex: 1 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AppBar
|
||||||
|
position="static"
|
||||||
|
color="default"
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
...commandPanelSx(themeMode),
|
||||||
|
flex: '0 0 auto',
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar
|
||||||
|
sx={{
|
||||||
|
minHeight: { xs: 118, sm: 66 },
|
||||||
|
gap: 1,
|
||||||
|
rowGap: 0.75,
|
||||||
|
px: { xs: 1, md: 2 },
|
||||||
|
flexWrap: { xs: 'wrap', sm: 'nowrap' },
|
||||||
|
alignContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 38,
|
||||||
|
height: 38,
|
||||||
|
flex: '0 0 auto',
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
color: 'primary.main',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
background: 'linear-gradient(135deg, rgba(97, 255, 207, 0.16), rgba(240, 184, 77, 0.1))',
|
||||||
|
boxShadow: '0 0 28px rgba(97, 255, 207, 0.18), inset 0 0 18px rgba(97, 255, 207, 0.08)',
|
||||||
|
clipPath: 'polygon(10% 0, 100% 0, 100% 76%, 76% 100%, 0 100%, 0 10%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dns fontSize="small" />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ minWidth: 0, flex: { xs: '1 1 220px', sm: 1 } }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 950, lineHeight: 1.05 }}>
|
||||||
Datacenter Modeler
|
Datacenter Modeler
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'uppercase' }}>
|
||||||
{diagram.racks.length}/{MAX_CONTAINERS} racks and cabinets
|
{diagram.racks.length}/{MAX_CONTAINERS}
|
||||||
|
<Box component="span" sx={{ display: { xs: 'none', sm: 'inline' } }}>
|
||||||
|
{' '}
|
||||||
|
racks and cabinets
|
||||||
|
</Box>
|
||||||
|
<Box component="span" sx={{ display: { xs: 'inline', sm: 'none' } }}>
|
||||||
|
{' '}
|
||||||
|
racks
|
||||||
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -582,7 +910,21 @@ export default function App() {
|
|||||||
onChange={(event) => updateDiagramName(event.target.value)}
|
onChange={(event) => updateDiagramName(event.target.value)}
|
||||||
size="small"
|
size="small"
|
||||||
label="Diagram"
|
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',
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Tooltip title="Save JSON">
|
<Tooltip title="Save JSON">
|
||||||
<IconButton color="primary" onClick={saveJson}>
|
<IconButton color="primary" onClick={saveJson}>
|
||||||
@@ -594,8 +936,24 @@ export default function App() {
|
|||||||
<FileOpen />
|
<FileOpen />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button variant="contained" endIcon={<Download />} onClick={(event) => setExportAnchor(event.currentTarget)}>
|
<Tooltip title={propertiesOpen ? 'Hide properties' : 'Show properties'}>
|
||||||
Export
|
<IconButton
|
||||||
|
color={propertiesOpen ? 'secondary' : 'primary'}
|
||||||
|
onClick={() => setPropertiesOpen((open) => !open)}
|
||||||
|
aria-label={propertiesOpen ? 'Hide properties' : 'Show properties'}
|
||||||
|
>
|
||||||
|
<Settings />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
endIcon={<Download />}
|
||||||
|
onClick={(event) => setExportAnchor(event.currentTarget)}
|
||||||
|
sx={{ minWidth: { xs: 48, sm: 96 }, px: { xs: 1.25, sm: 2 } }}
|
||||||
|
>
|
||||||
|
<Box component="span" sx={{ display: { xs: 'none', sm: 'inline' } }}>
|
||||||
|
Export
|
||||||
|
</Box>
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip title={themeMode === 'dark' ? 'Light theme' : 'Dark theme'}>
|
<Tooltip title={themeMode === 'dark' ? 'Light theme' : 'Dark theme'}>
|
||||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||||
@@ -620,8 +978,16 @@ export default function App() {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<Box sx={{ flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: { xs: '1fr', lg: '280px minmax(0, 1fr) 340px' } }}>
|
<Box
|
||||||
<LibraryPanel />
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: '1fr', lg: '280px minmax(0, 1fr)' },
|
||||||
|
gap: { xs: 1, md: 1.5 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LibraryPanel themeMode={themeMode} />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
onDrop={handleCanvasDrop}
|
onDrop={handleCanvasDrop}
|
||||||
@@ -629,30 +995,57 @@ export default function App() {
|
|||||||
sx={{
|
sx={{
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
p: 2,
|
p: { xs: 1, md: 1.5 },
|
||||||
borderLeft: { lg: 1 },
|
borderRadius: 2,
|
||||||
borderRight: { lg: 1 },
|
...commandPanelSx(themeMode),
|
||||||
borderColor: 'divider',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
ref={workspaceRef}
|
ref={workspaceRef}
|
||||||
sx={{
|
sx={{
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
minWidth: 620,
|
minWidth: { xs: '100%', sm: 620 },
|
||||||
|
width: diagram.racks.length ? 'max-content' : '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: 2,
|
gap: 2.25,
|
||||||
p: 2,
|
p: { xs: 1.5, md: 2 },
|
||||||
border: 1,
|
border: '1px solid',
|
||||||
borderColor: 'divider',
|
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.55 : 0.86),
|
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 ? (
|
{diagram.racks.length === 0 ? (
|
||||||
<Box sx={{ m: 'auto', textAlign: 'center', color: 'text.secondary' }}>
|
<Box
|
||||||
<Dns sx={{ fontSize: 52, mb: 1, opacity: 0.45 }} />
|
sx={{
|
||||||
<Typography variant="h6">No racks in diagram</Typography>
|
m: 'auto',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'text.secondary',
|
||||||
|
border: '1px dashed',
|
||||||
|
borderColor: 'divider',
|
||||||
|
px: 4,
|
||||||
|
py: 3,
|
||||||
|
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.16)' : 'rgba(255, 255, 255, 0.62)'),
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dns sx={{ fontSize: 52, mb: 1, opacity: 0.62, color: 'primary.main' }} />
|
||||||
|
<Typography variant="h6">Awaiting rack drop</Typography>
|
||||||
|
<Typography variant="caption">Drag a container from the component rail.</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
diagram.racks.map((rack) => (
|
diagram.racks.map((rack) => (
|
||||||
@@ -660,21 +1053,47 @@ export default function App() {
|
|||||||
key={rack.id}
|
key={rack.id}
|
||||||
rack={rack}
|
rack={rack}
|
||||||
selected={selection}
|
selected={selection}
|
||||||
onSelectRack={() => setSelection({ kind: 'rack', rackId: rack.id })}
|
onSelectRack={() => {
|
||||||
onSelectEquipment={(equipmentId) => setSelection({ kind: 'equipment', rackId: rack.id, equipmentId })}
|
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)}
|
onDrop={(event) => handleRackDrop(event, rack.id)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{propertiesOpen && (
|
||||||
<PropertiesPanel
|
<PropertiesPanel
|
||||||
diagram={diagram}
|
diagram={diagram}
|
||||||
selectedRack={selectedRack || null}
|
selectedRack={selectedRack || null}
|
||||||
selectedEquipment={selectedEquipment || null}
|
selectedEquipment={selectedEquipment || null}
|
||||||
selectedEquipmentRack={selectedEquipmentRack}
|
selectedEquipmentRack={selectedEquipmentRack}
|
||||||
selection={selection}
|
selection={selection}
|
||||||
|
docked={propertiesDocked}
|
||||||
|
position={propertiesPosition}
|
||||||
|
dragging={draggingProperties}
|
||||||
|
themeMode={themeMode}
|
||||||
|
onStartDrag={(event) => {
|
||||||
|
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}
|
onDiagramNameChange={updateDiagramName}
|
||||||
onRackChange={updateRack}
|
onRackChange={updateRack}
|
||||||
onRackSizeChange={updateRackSize}
|
onRackSizeChange={updateRackSize}
|
||||||
@@ -682,9 +1101,20 @@ export default function App() {
|
|||||||
onEquipmentPlacementChange={updateEquipmentPlacement}
|
onEquipmentPlacementChange={updateEquipmentPlacement}
|
||||||
onDelete={deleteSelection}
|
onDelete={deleteSelection}
|
||||||
/>
|
/>
|
||||||
</Box>
|
)}
|
||||||
|
|
||||||
<Dialog open={Boolean(pendingContainer)} onClose={() => setPendingContainer(null)} maxWidth="xs" fullWidth>
|
<Dialog
|
||||||
|
open={Boolean(pendingContainer)}
|
||||||
|
onClose={() => 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)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogTitle>Add {pendingContainer === 'cabinet' ? 'Cabinet' : 'Rack'}</DialogTitle>
|
<DialogTitle>Add {pendingContainer === 'cabinet' ? 'Cabinet' : 'Rack'}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Stack spacing={2} sx={{ pt: 1 }}>
|
<Stack spacing={2} sx={{ pt: 1 }}>
|
||||||
@@ -707,7 +1137,18 @@ export default function App() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={welcomeOpen} onClose={() => setWelcomeOpen(false)} maxWidth="sm" fullWidth>
|
<Dialog
|
||||||
|
open={welcomeOpen}
|
||||||
|
onClose={() => 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)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Dns color="primary" />
|
<Dns color="primary" />
|
||||||
Welcome to Datacenter Modeler
|
Welcome to Datacenter Modeler
|
||||||
@@ -729,7 +1170,9 @@ export default function App() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
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 (<a href="https://www.theblindengineer.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
theblindengineer.com
|
||||||
|
</a>). A donation is appreciated if this tool helps you,
|
||||||
but it is absolutely not required.
|
but it is absolutely not required.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
@@ -771,12 +1214,22 @@ export default function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LibraryPanel() {
|
function LibraryPanel({ themeMode }: { themeMode: ThemeMode }) {
|
||||||
const containers = componentLibrary.filter((item) => item.category === 'container');
|
const containers = componentLibrary.filter((item) => item.category === 'container');
|
||||||
const equipment = componentLibrary.filter((item) => item.category === 'equipment');
|
const equipment = componentLibrary.filter((item) => item.category === 'equipment');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper square elevation={0} sx={{ minHeight: 0, overflow: 'auto', p: 2, display: { xs: 'none', lg: 'block' } }}>
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
...commandPanelSx(themeMode),
|
||||||
|
minHeight: 0,
|
||||||
|
overflow: 'auto',
|
||||||
|
p: 1.5,
|
||||||
|
display: { xs: 'none', lg: 'block' },
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="overline" color="text.secondary" sx={{ fontWeight: 800 }}>
|
<Typography variant="overline" color="text.secondary" sx={{ fontWeight: 800 }}>
|
||||||
@@ -817,8 +1270,21 @@ function LibraryTile({ item }: { item: (typeof componentLibrary)[number] }) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
borderLeft: 4,
|
border: '1px solid',
|
||||||
|
borderColor: alpha(item.color, 0.35),
|
||||||
|
borderLeft: 3,
|
||||||
borderLeftColor: item.color,
|
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' },
|
'&:active': { cursor: 'grabbing' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -857,10 +1323,37 @@ function RackView({
|
|||||||
width: 270,
|
width: 270,
|
||||||
flex: '0 0 270px',
|
flex: '0 0 270px',
|
||||||
overflow: 'hidden',
|
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))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ p: 1.25, bgcolor: 'background.paper', borderBottom: 1, borderColor: 'divider' }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 1.25,
|
||||||
|
bgcolor: (theme) => (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))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
|
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
|
||||||
<Box sx={{ minWidth: 0 }}>
|
<Box sx={{ minWidth: 0 }}>
|
||||||
<Typography variant="subtitle2" noWrap sx={{ fontWeight: 800 }}>
|
<Typography variant="subtitle2" noWrap sx={{ fontWeight: 800 }}>
|
||||||
@@ -881,7 +1374,7 @@ function RackView({
|
|||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
height: rackHeight,
|
height: rackHeight,
|
||||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#0b1118' : '#edf2f7'),
|
bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#030706' : '#edf2e6'),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -890,13 +1383,14 @@ function RackView({
|
|||||||
inset: 0,
|
inset: 0,
|
||||||
left: 34,
|
left: 34,
|
||||||
right: 34,
|
right: 34,
|
||||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#101820' : '#f8fafc'),
|
bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#081210' : '#f8faf2'),
|
||||||
backgroundImage: (theme) =>
|
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`,
|
backgroundSize: `100% ${RACK_ROW_HEIGHT}px`,
|
||||||
borderLeft: 10,
|
borderLeft: 10,
|
||||||
borderRight: 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) => (
|
{Array.from({ length: rack.sizeU }, (_, index) => rack.sizeU - index).map((u) => (
|
||||||
@@ -942,9 +1436,9 @@ function RackView({
|
|||||||
height: item.sizeU * RACK_ROW_HEIGHT,
|
height: item.sizeU * RACK_ROW_HEIGHT,
|
||||||
minHeight: RACK_ROW_HEIGHT,
|
minHeight: RACK_ROW_HEIGHT,
|
||||||
border: 2,
|
border: 2,
|
||||||
borderColor: selectedItem ? 'secondary.main' : alpha('#000000', 0.16),
|
borderColor: selectedItem ? 'secondary.main' : alpha(item.color, 0.48),
|
||||||
bgcolor: item.color,
|
bgcolor: alpha(item.color, 0.9),
|
||||||
color: '#071018',
|
color: '#04100d',
|
||||||
px: 0.75,
|
px: 0.75,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -952,7 +1446,9 @@ function RackView({
|
|||||||
gap: 1,
|
gap: 1,
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
overflow: 'hidden',
|
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)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="caption" noWrap sx={{ fontWeight: 900, minWidth: 0 }}>
|
<Typography variant="caption" noWrap sx={{ fontWeight: 900, minWidth: 0 }}>
|
||||||
@@ -982,21 +1478,22 @@ function RackView({
|
|||||||
onSelectEquipment(item.id);
|
onSelectEquipment(item.id);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 6,
|
top: 6,
|
||||||
bottom: 6,
|
bottom: 6,
|
||||||
width,
|
width,
|
||||||
left: item.pduSide === 'left' ? 3 : 'auto',
|
left: item.pduSide === 'left' ? 3 : 'auto',
|
||||||
right: item.pduSide === 'right' ? 3 : 'auto',
|
right: item.pduSide === 'right' ? 3 : 'auto',
|
||||||
border: 2,
|
border: 2,
|
||||||
borderColor: selectedItem ? 'secondary.main' : alpha('#000000', 0.24),
|
borderColor: selectedItem ? 'secondary.main' : alpha(item.color, 0.48),
|
||||||
bgcolor: item.color,
|
bgcolor: alpha(item.color, 0.9),
|
||||||
color: '#111827',
|
color: '#04100d',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
cursor: 'grab',
|
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',
|
overflow: 'hidden',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
@@ -1027,6 +1524,13 @@ function PropertiesPanel({
|
|||||||
selectedEquipment,
|
selectedEquipment,
|
||||||
selectedEquipmentRack,
|
selectedEquipmentRack,
|
||||||
selection,
|
selection,
|
||||||
|
docked,
|
||||||
|
position,
|
||||||
|
dragging,
|
||||||
|
themeMode,
|
||||||
|
onStartDrag,
|
||||||
|
onClose,
|
||||||
|
onToggleDock,
|
||||||
onDiagramNameChange,
|
onDiagramNameChange,
|
||||||
onRackChange,
|
onRackChange,
|
||||||
onRackSizeChange,
|
onRackSizeChange,
|
||||||
@@ -1039,6 +1543,13 @@ function PropertiesPanel({
|
|||||||
selectedEquipment: Equipment | null;
|
selectedEquipment: Equipment | null;
|
||||||
selectedEquipmentRack: RackContainer | null;
|
selectedEquipmentRack: RackContainer | null;
|
||||||
selection: Selection;
|
selection: Selection;
|
||||||
|
docked: boolean;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
dragging: boolean;
|
||||||
|
themeMode: ThemeMode;
|
||||||
|
onStartDrag: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onToggleDock: () => void;
|
||||||
onDiagramNameChange: (name: string) => void;
|
onDiagramNameChange: (name: string) => void;
|
||||||
onRackChange: (rackId: string, patch: Partial<RackContainer>) => void;
|
onRackChange: (rackId: string, patch: Partial<RackContainer>) => void;
|
||||||
onRackSizeChange: (rackId: string, sizeU: number) => void;
|
onRackSizeChange: (rackId: string, sizeU: number) => void;
|
||||||
@@ -1047,26 +1558,80 @@ function PropertiesPanel({
|
|||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Paper square elevation={0} sx={{ minHeight: 0, overflow: 'auto', p: 2, display: { xs: 'none', lg: 'block' } }}>
|
<Paper
|
||||||
<Stack spacing={2}>
|
data-properties-panel="true"
|
||||||
<Box>
|
elevation={0}
|
||||||
<Typography variant="overline" color="text.secondary" sx={{ fontWeight: 800 }}>
|
sx={{
|
||||||
|
...commandPanelSx(themeMode),
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: 20,
|
||||||
|
top: { xs: 8, lg: docked ? 96 : position.y },
|
||||||
|
right: { xs: 8, lg: docked ? 18 : 'auto' },
|
||||||
|
bottom: { xs: 8, lg: docked ? 18 : 'auto' },
|
||||||
|
left: { xs: 8, lg: docked ? 'auto' : position.x },
|
||||||
|
width: { xs: 'auto', lg: 390 },
|
||||||
|
maxWidth: { xs: 'none', lg: 'calc(100vw - 36px)' },
|
||||||
|
height: { xs: 'auto', lg: docked ? 'auto' : 'min(760px, calc(100vh - 36px))' },
|
||||||
|
maxHeight: { xs: 'calc(100vh - 16px)', lg: docked ? 'calc(100vh - 114px)' : 'calc(100vh - 36px)' },
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
transform: dragging ? 'scale(1.006)' : 'none',
|
||||||
|
transition: dragging ? 'none' : 'box-shadow 160ms ease, transform 160ms ease',
|
||||||
|
borderRadius: 2,
|
||||||
|
resize: { xs: 'none', lg: docked ? 'none' : 'both' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
onMouseDown={onStartDrag}
|
||||||
|
sx={{
|
||||||
|
cursor: docked ? 'default' : dragging ? 'grabbing' : 'grab',
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.9,
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'auto minmax(0, 1fr) auto',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(97, 255, 207, 0.05)' : 'rgba(0, 108, 95, 0.08)'),
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DragIndicator fontSize="small" sx={{ color: docked ? 'text.disabled' : 'primary.main' }} />
|
||||||
|
<Box sx={{ minWidth: 0 }}>
|
||||||
|
<Typography variant="overline" color="text.secondary" sx={{ display: 'block', lineHeight: 1.1, fontWeight: 800 }}>
|
||||||
Properties
|
Properties
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 800 }}>
|
<Typography variant="subtitle1" noWrap sx={{ fontWeight: 900, lineHeight: 1.25 }}>
|
||||||
{selectedEquipment ? selectedEquipment.name : selectedRack ? selectedRack.name : 'Diagram'}
|
{selectedEquipment ? selectedEquipment.name : selectedRack ? selectedRack.name : 'Diagram'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Stack direction="row" spacing={0.75} onMouseDown={(event) => event.stopPropagation()}>
|
||||||
|
<Tooltip title={docked ? 'Undock properties' : 'Dock right'}>
|
||||||
|
<IconButton size="small" color={docked ? 'secondary' : 'primary'} onClick={onToggleDock} aria-label={docked ? 'Undock properties' : 'Dock properties'}>
|
||||||
|
{docked ? <PushPin fontSize="small" /> : <PushPinOutlined fontSize="small" />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Close properties">
|
||||||
|
<IconButton size="small" color="primary" onClick={onClose} aria-label="Close properties">
|
||||||
|
<Close fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ minHeight: 0, overflow: 'auto', p: 1.25 }}>
|
||||||
|
<Stack spacing={1.25}>
|
||||||
{!selection && (
|
{!selection && (
|
||||||
<Stack spacing={2}>
|
<Stack spacing={1.25}>
|
||||||
<TextField label="Diagram name" value={diagram.name} onChange={(event) => onDiagramNameChange(event.target.value)} fullWidth />
|
<TextField label="Diagram name" value={diagram.name} onChange={(event) => onDiagramNameChange(event.target.value)} fullWidth />
|
||||||
<MetadataSummary diagram={diagram} />
|
<MetadataSummary diagram={diagram} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedRack && selection?.kind === 'rack' && (
|
{selectedRack && selection?.kind === 'rack' && (
|
||||||
<Stack spacing={2}>
|
<Stack spacing={1.25}>
|
||||||
<TextField label="Name" value={selectedRack.name} onChange={(event) => onRackChange(selectedRack.id, { name: event.target.value })} fullWidth />
|
<TextField label="Name" value={selectedRack.name} onChange={(event) => onRackChange(selectedRack.id, { name: event.target.value })} fullWidth />
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel>Type</InputLabel>
|
<InputLabel>Type</InputLabel>
|
||||||
@@ -1098,7 +1663,7 @@ function PropertiesPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedEquipment && selectedEquipmentRack && selection?.kind === 'equipment' && (
|
{selectedEquipment && selectedEquipmentRack && selection?.kind === 'equipment' && (
|
||||||
<Stack spacing={2}>
|
<Stack spacing={1.25}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Name"
|
label="Name"
|
||||||
value={selectedEquipment.name}
|
value={selectedEquipment.name}
|
||||||
@@ -1697,6 +2262,7 @@ function PropertiesPanel({
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import type { LibraryItem } from '../types';
|
import type { LibraryItem } from '../types';
|
||||||
|
|
||||||
export const componentLibrary: LibraryItem[] = [
|
export const componentLibrary: LibraryItem[] = [
|
||||||
{ type: 'rack', category: 'container', label: 'Rack', defaultU: 42, minU: 2, maxU: 42, color: '#607d8b' },
|
{ 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: '#546e7a' },
|
{ 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: '#00acc1' },
|
{ 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: '#26a69a' },
|
{ 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: '#ef5350' },
|
{ 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: '#5c6bc0' },
|
{ 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: '#7e57c2' },
|
{ 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: '#ffb300' },
|
{ 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: '#42a5f5' },
|
{ 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: '#8d6e63' },
|
{ 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: '#f97316' },
|
{ 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: '#78909c' },
|
{ 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: '#66bb6a' },
|
{ 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: '#ec407a' },
|
{ type: 'kvm-console', category: 'equipment', label: 'KVM (Console)', defaultU: 1, minU: 1, maxU: 24, color: '#ff7ac8' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getLibraryItem = (type: string | undefined) =>
|
export const getLibraryItem = (type: string | undefined) =>
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Roboto, Arial, sans-serif;
|
min-width: 360px;
|
||||||
|
font-family: Inter, Roboto, Arial, sans-serif;
|
||||||
|
font-variant-ligatures: contextual;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
@@ -18,3 +20,14 @@ input,
|
|||||||
textarea {
|
textarea {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration-color: rgba(97, 255, 207, 0.55);
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user