diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c2755a9..aa6af52 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,95 +1,95 @@ name: Publish Docker Image on: - workflow_dispatch: - inputs: - force_republish: - description: 'Force republish existing version (overwrite Docker tag)' - required: false - type: boolean - default: false + workflow_dispatch: + inputs: + force_republish: + description: 'Force republish existing version (overwrite Docker tag)' + required: false + type: boolean + default: false permissions: - contents: write - actions: write + contents: write + actions: write jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Configure Git - run: | - git config user.name "GitHub Actions" - git config user.email "actions@github.com" + - name: Configure Git + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" - - name: Determine Version - id: determine_version - run: | - FORCE="${{ inputs.force_republish }}" - CURRENT_VERSION=$(node -p "require('./package.json').version") - - if [ "$FORCE" = "true" ]; then - echo "Force republish request. Using current version v$CURRENT_VERSION without bumping." - echo "VERSION=$CURRENT_VERSION" >> $GITHUB_ENV - elif git rev-parse "v$CURRENT_VERSION" >/dev/null 2>&1; then - echo "Tag v$CURRENT_VERSION already exists. Bumping patch version..." - - # Bump version and push - npm version patch -m "chore: bump version to %s" - git push --follow-tags - - # Get new version - NEW_VERSION=$(node -p "require('./package.json').version") - echo "New version is $NEW_VERSION" - echo "VERSION=$NEW_VERSION" >> $GITHUB_ENV - else - echo "Tag v$CURRENT_VERSION does not exist. Using current version..." - echo "VERSION=$CURRENT_VERSION" >> $GITHUB_ENV - fi + - name: Determine Version + id: determine_version + run: | + FORCE="${{ inputs.force_republish }}" + CURRENT_VERSION=$(node -p "require('./package.json').version") - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + if [ "$FORCE" = "true" ]; then + echo "Force republish request. Using current version v$CURRENT_VERSION without bumping." + echo "VERSION=$CURRENT_VERSION" >> $GITHUB_ENV + elif git rev-parse "v$CURRENT_VERSION" >/dev/null 2>&1; then + echo "Tag v$CURRENT_VERSION already exists. Bumping patch version..." + + # Bump version and push + npm version patch -m "chore: bump version to %s" + git push --follow-tags + + # Get new version + NEW_VERSION=$(node -p "require('./package.json').version") + echo "New version is $NEW_VERSION" + echo "VERSION=$NEW_VERSION" >> $GITHUB_ENV + else + echo "Tag v$CURRENT_VERSION does not exist. Using current version..." + echo "VERSION=$CURRENT_VERSION" >> $GITHUB_ENV + fi - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - name: Build and Push Docker Image - uses: docker/build-push-action@v5 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: | - ${{ secrets.DOCKER_USERNAME }}/rack-planner:${{ env.VERSION }} - ${{ secrets.DOCKER_USERNAME }}/rack-planner:latest + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - tag_name: v${{ env.VERSION }} - name: Release v${{ env.VERSION }} - body: "Release for version ${{ env.VERSION }}" - draft: false - prerelease: false - allowUpdates: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and Push Docker Image + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: | + ${{ secrets.DOCKER_USERNAME }}/rack-planner:${{ env.VERSION }} + ${{ secrets.DOCKER_USERNAME }}/rack-planner:latest - - name: Trigger Deployment - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "Waiting for git propagation..." - sleep 5 - gh workflow run deploy.yml --ref main + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ env.VERSION }} + name: Release v${{ env.VERSION }} + body: 'Release for version ${{ env.VERSION }}' + draft: false + prerelease: false + allowUpdates: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Trigger Deployment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "Waiting for git propagation..." + sleep 5 + gh workflow run deploy.yml --ref main diff --git a/README.md b/README.md index 75586b1..f6fcfc0 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ If you just want to run the application without downloading the source code, you ```bash docker run -d -p 1019:80 brankko/rack-planner:latest ``` - *(You can change `1019` to any port you prefer, e.g., `-p 8080:80`)* + + _(You can change `1019` to any port you prefer, e.g., `-p 8080:80`)_ 3. Access the application at [http://localhost:1019](http://localhost:1019). diff --git a/docker-compose.yml b/docker-compose.yml index 0cc76c7..fcda9f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: - rack-planner: - build: . - ports: - - "1019:80" - restart: always + rack-planner: + build: . + ports: + - '1019:80' + restart: always diff --git a/index.html b/index.html index 039d76a..108b407 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,14 @@ + + + + + Rack Planner + - - - - - Rack Planner - - - -
- - - - \ No newline at end of file + +
+ + + diff --git a/src/App.tsx b/src/App.tsx index 77b817c..9a6c97b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,8 +23,6 @@ const WIDTH_19_INCH = Math.round(19 * PIXELS_PER_INCH); const WIDTH_10_INCH = Math.round(10 * PIXELS_PER_INCH); const RAIL_WIDTH = Math.round(0.625 * PIXELS_PER_INCH); - - export default function App() { const [rackSettings, setRackSettings] = useState({ heightU: 10, @@ -48,7 +46,10 @@ export default function App() { return false; }); const [areAnimationsEnabled, setAreAnimationsEnabled] = useState(true); - const [draggedItem, setDraggedItem] = useState<{ module: RackModule; originalIndex?: number } | null>(null); + const [draggedItem, setDraggedItem] = useState<{ + module: RackModule; + originalIndex?: number; + } | null>(null); const [dragOverIndex, setDragOverIndex] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [libraryTab, setLibraryTab] = useState<'catalog' | 'custom'>('catalog'); @@ -126,23 +127,23 @@ export default function App() { // Old format: length === initialHeight // New format: length === initialHeight * 2 if (parsedSlots.length === initialHeight) { - console.log("Migrating rack slots to 0.5U resolution..."); + console.log('Migrating rack slots to 0.5U resolution...'); const newSlots: RackSlot[] = []; - parsedSlots.forEach(oldSlot => { + parsedSlots.forEach((oldSlot) => { // Create 2 new slots for each old slot // 1. Top Half (Original U position) newSlots.push({ uPosition: oldSlot.uPosition, moduleId: oldSlot.moduleId, - module: oldSlot.module + module: oldSlot.module, }); // 2. Bottom Half (U position - 0.5) newSlots.push({ uPosition: oldSlot.uPosition - 0.5, moduleId: oldSlot.moduleId, - module: oldSlot.module + module: oldSlot.module, }); }); @@ -182,7 +183,7 @@ export default function App() { // i=0 -> 10 // i=1 -> 9.5 // i=2 -> 9 - uPosition: size - (i * 0.5), + uPosition: size - i * 0.5, moduleId: null, })); setRackSlots(slots); @@ -205,7 +206,7 @@ export default function App() { // Generate new slots starting from newHeight down to (oldTop + 0.5) const newSlots: RackSlot[] = Array.from({ length: slotsToAdd }, (_, i) => ({ - uPosition: newHeight - (i * 0.5), + uPosition: newHeight - i * 0.5, moduleId: null, })); updatedSlots = [...newSlots, ...rackSlots]; @@ -217,7 +218,11 @@ export default function App() { // Safety check const hasModules = slotsToRemove.some((s) => s.moduleId !== null); if (hasModules) { - if (!confirm(`Reducing size will remove modules in the top ${slotsToRemoveCount / 2}U. Continue?`)) { + if ( + !confirm( + `Reducing size will remove modules in the top ${slotsToRemoveCount / 2}U. Continue?` + ) + ) { return; // Abort } } @@ -289,11 +294,7 @@ export default function App() { // If over a merged empty 1U slot (Even index, next is empty), check sub-position if (index % 2 === 0) { const nextSlot = rackSlots[index + 1]; - if ( - nextSlot && - rackSlots[index].moduleId === null && - nextSlot.moduleId === null - ) { + if (nextSlot && rackSlots[index].moduleId === null && nextSlot.moduleId === null) { // This is a merged empty 1U slot. Check cursor position. const rect = e.currentTarget.getBoundingClientRect(); const offsetY = e.clientY - rect.top; @@ -505,8 +506,6 @@ export default function App() { ); } - - return (
@@ -518,7 +517,9 @@ export default function App() {

RackPlanner{' '} - v{__APP_VERSION__} + + v{__APP_VERSION__} +

@@ -599,9 +600,7 @@ export default function App() { {/* Rails Container */} -
+
{/* Static Background Rails - Fixed Visuals */}
{Array.from({ length: rackSettings.heightU }).map((_, i) => { @@ -667,10 +666,12 @@ export default function App() { {rackSlots.map((slot, index) => { const isOccupied = slot.moduleId !== null; const prevSlot = index > 0 ? rackSlots[index - 1] : null; - const nextSlot = index < rackSlots.length - 1 ? rackSlots[index + 1] : null; + const nextSlot = + index < rackSlots.length - 1 ? rackSlots[index + 1] : null; // Module Logic - const isModuleStart = isOccupied && prevSlot?.moduleId !== slot.moduleId; + const isModuleStart = + isOccupied && prevSlot?.moduleId !== slot.moduleId; // 0.5U / Split Logic const isEvenIndex = index % 2 === 0; // Top of a U @@ -725,14 +726,21 @@ export default function App() { // With 0.5U support, we need precise drag targeting. // If dragging 0.5U, we respect the exact dragOverIndex. // If dragging 1U+, we might be snapping effectiveDragIndex to even. - // BUT, checkCanDrop enforces Even for 1U+. + // BUT, checkCanDrop enforces Even for 1U+. // So dragOverIndex might be Odd, but we want to visualize the snap to Even? // Or does the UI expect user to mouse over Even? // Let's stick to accurate feedback: If user hovers Odd with 1U, it's invalid (Red). // So we don't snap effectiveDragIndex for validity, but maybe for visual alignment if we wanted to be helpful. // For now, strict feedback: - const isValid = dragOverIndex !== null ? checkCanDrop(dragOverIndex, uSize, draggedItem?.originalIndex) : true; + const isValid = + dragOverIndex !== null + ? checkCanDrop( + dragOverIndex, + uSize, + draggedItem?.originalIndex + ) + : true; // Specificity check: // If dragging 0.5U (slotsNeeded=1). // If dragOverIndex = Even (0). Range [0, 1). Match 0. @@ -750,35 +758,56 @@ export default function App() { if (dragOverIndex === index) { showHighlight = true; // If 0.5U, only top half. If 1U+, full inset. - highlightStyle = uSize === 0.5 - ? { top: 0, height: '50%', left: 0, right: 0 } - : { inset: 0 }; - } else if (uSize === 0.5 && dragOverIndex === index + 1) { + highlightStyle = + uSize === 0.5 + ? { + top: 0, + height: '50%', + left: 0, + right: 0, + } + : { inset: 0 }; + } else if ( + uSize === 0.5 && + dragOverIndex === index + 1 + ) { // 0.5U dragging to bottom half showHighlight = true; - highlightStyle = { bottom: 0, height: '50%', left: 0, right: 0 }; + highlightStyle = { + bottom: 0, + height: '50%', + left: 0, + right: 0, + }; } // 2. Multi-U Overlap Logic // If a module starts ABOVE this slot, does it extend into/over this slot? // Range: [dragOverIndex, dragOverIndex + slotsNeeded) // Current Slot Range: [index, index + 2) (Includes index and index+1) // If dragOverIndex < index AND (dragOverIndex + slotsNeeded) > index - else if (dragOverIndex < index && (dragOverIndex + slotsNeeded) > index) { + else if ( + dragOverIndex < index && + dragOverIndex + slotsNeeded > index + ) { showHighlight = true; highlightStyle = { inset: 0 }; } - } else { // Standard Slot Logic // If this slot is within the drag target range - if (index >= dragOverIndex && index < dragOverIndex + slotsNeeded) { + if ( + index >= dragOverIndex && + index < dragOverIndex + slotsNeeded + ) { showHighlight = true; highlightStyle = { inset: 0 }; } } } - const isMainDragTarget = dragOverIndex === index || (isMergedEmpty && dragOverIndex === index + 1); + const isMainDragTarget = + dragOverIndex === index || + (isMergedEmpty && dragOverIndex === index + 1); return (
handleDragOver(e, index)} onDrop={(e) => handleDrop(e, index)} className="relative flex w-full transition-colors" - // Use renderedHeight. + // Use renderedHeight. // NOTE: Module Start slot calculates full height. - style={{ height: renderedHeight, minHeight: renderedHeight }} + style={{ + height: renderedHeight, + minHeight: renderedHeight, + }} > {/* (Left Rail removed - rendered in background) */} @@ -796,15 +828,10 @@ export default function App() {
{/* Empty Slot Placeholder */} {!isOccupied && ( -
- - - - +
- Empty {slot.uPosition} {isMergedEmpty ? 'U' : ''} + Empty {slot.uPosition}{' '} + {isMergedEmpty ? 'U' : ''}
)} @@ -859,7 +886,7 @@ export default function App() { ) { newSlots[i] = { ...newSlots[ - i + i ], moduleId: null, @@ -887,8 +914,12 @@ export default function App() { style={highlightStyle} > {isMainDragTarget && ( - - {isValid ? `Mount Here (${uSize}U)` : 'Cannot Mount Here'} + + {isValid + ? `Mount Here (${uSize}U)` + : 'Cannot Mount Here'} )}
@@ -1007,31 +1038,31 @@ export default function App() { {module.id.startsWith( 'custom-' ) && ( -
- - -
- )} +
+ + +
+ )}
))} @@ -1043,10 +1074,10 @@ export default function App() { {Object.values(getGroupedModules()).every( (g) => g.length === 0 ) && ( -
- No items found -
- )} +
+ No items found +
+ )}
)} @@ -1073,7 +1104,12 @@ export default function App() { onChange={(e) => setCustomShowName(e.target.checked)} className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" /> - +
diff --git a/src/components/ModuleFace.tsx b/src/components/ModuleFace.tsx index b6050e6..02db921 100644 --- a/src/components/ModuleFace.tsx +++ b/src/components/ModuleFace.tsx @@ -27,10 +27,10 @@ export const ModuleFace = ({ style={ hasImage ? { - backgroundImage: `url(${module.image})`, - backgroundSize: 'cover', - backgroundPosition: 'center', - } + backgroundImage: `url(${module.image})`, + backgroundSize: 'cover', + backgroundPosition: 'center', + } : {} } > @@ -47,13 +47,21 @@ export const ModuleFace = ({
{/* Networking: Ports */} {module.type === 'network' && ( - + )} {/* Server: Indicators or Drive Bays */} {module.type === 'server' && (
- +
)} diff --git a/src/components/module-faces/accessory/RPIMountSlot.tsx b/src/components/module-faces/accessory/RPIMountSlot.tsx index 780aef4..685cee2 100644 --- a/src/components/module-faces/accessory/RPIMountSlot.tsx +++ b/src/components/module-faces/accessory/RPIMountSlot.tsx @@ -1,8 +1,9 @@ export const RPIMountSlot = ({ vertical = false }: { vertical?: boolean }) => { return (
diff --git a/src/components/module-faces/network/Port.tsx b/src/components/module-faces/network/Port.tsx index 3ef7dd1..67bd590 100644 --- a/src/components/module-faces/network/Port.tsx +++ b/src/components/module-faces/network/Port.tsx @@ -75,18 +75,20 @@ export const Port = ({
{/* Green LED: On when online or active */}
{/* Amber LED: Blinking when active, Off otherwise */}
diff --git a/src/components/module-faces/server/Server1U.tsx b/src/components/module-faces/server/Server1U.tsx index f408333..2649da8 100644 --- a/src/components/module-faces/server/Server1U.tsx +++ b/src/components/module-faces/server/Server1U.tsx @@ -14,10 +14,14 @@ export const Server1U = ({ isPowered, rackWidth = '19inch' }: Server1UProps) => {/* Top Left: Power Button & USBs */}
{/* Power Button */} -
-
+
@@ -32,7 +36,10 @@ export const Server1U = ({ isPowered, rackWidth = '19inch' }: Server1UProps) => {/* Right Side: Fan Grilles */}
{fans.map((i) => ( -
+
))} diff --git a/src/components/module-faces/server/Server2U.tsx b/src/components/module-faces/server/Server2U.tsx index 5461327..ec1a2ec 100644 --- a/src/components/module-faces/server/Server2U.tsx +++ b/src/components/module-faces/server/Server2U.tsx @@ -14,10 +14,14 @@ export const Server2U = ({ isPowered, rackWidth = '19inch' }: Server2UProps) => {/* Top Left: Power Button & USBs */}
{/* Power Button */} -
-
+
@@ -32,7 +36,10 @@ export const Server2U = ({ isPowered, rackWidth = '19inch' }: Server2UProps) => {/* Right Side: Larger Fan Grilles */}
{fans.map((i) => ( -
+
))} diff --git a/src/components/module-faces/server/Server3U.tsx b/src/components/module-faces/server/Server3U.tsx index c3c064c..8e5e47d 100644 --- a/src/components/module-faces/server/Server3U.tsx +++ b/src/components/module-faces/server/Server3U.tsx @@ -14,10 +14,14 @@ export const Server3U = ({ isPowered, rackWidth = '19inch' }: Server3UProps) => {/* Top Left: Power Button & USBs */}
{/* Power Button */} -
-
+
@@ -32,12 +36,14 @@ export const Server3U = ({ isPowered, rackWidth = '19inch' }: Server3UProps) => {/* Right Side: Even Larger Fan Grilles */}
{fans.map((i) => ( -
+
))}
-
); }; diff --git a/src/components/module-faces/server/Server4U.tsx b/src/components/module-faces/server/Server4U.tsx index 73a981b..b6d622e 100644 --- a/src/components/module-faces/server/Server4U.tsx +++ b/src/components/module-faces/server/Server4U.tsx @@ -14,10 +14,14 @@ export const Server4U = ({ isPowered, rackWidth = '19inch' }: Server4UProps) => {/* Top Left: Power Button & USBs */}
{/* Power Button */} -
-
+
@@ -35,7 +39,10 @@ export const Server4U = ({ isPowered, rackWidth = '19inch' }: Server4UProps) => {/* Top Row */}
{fans.map((i) => ( -
+
))} @@ -43,7 +50,10 @@ export const Server4U = ({ isPowered, rackWidth = '19inch' }: Server4UProps) => {/* Bottom Row */}
{fans.map((i) => ( -
+
))} diff --git a/src/components/module-faces/storage/DriveBay35.tsx b/src/components/module-faces/storage/DriveBay35.tsx index 30b08c7..c7f4ea2 100644 --- a/src/components/module-faces/storage/DriveBay35.tsx +++ b/src/components/module-faces/storage/DriveBay35.tsx @@ -18,18 +18,20 @@ export const DriveBay35 = ({
{/* Green LED (Power/Status) */}
{/* Amber LED (Activity) */}
diff --git a/src/data/modules.ts b/src/data/modules.ts index 58b1bb6..62ff624 100644 --- a/src/data/modules.ts +++ b/src/data/modules.ts @@ -15,20 +15,90 @@ export const PREDEFINED_MODULES: RackModule[] = [ { id: 'server-4u', name: 'Server 4U', uSize: 4, type: 'server', color: 'bg-indigo-950' }, // Storage - { id: 'nas-1u-35', name: '1U 3.5" HDD Bay', uSize: 1, type: 'storage', color: 'bg-gray-800', showName: false }, - { id: 'nas-2u-35', name: '2U 3.5" HDD Bay', uSize: 2, type: 'storage', color: 'bg-gray-800', showName: false }, - { id: 'nas-2u-25', name: '2U 2.5" SSD Bay', uSize: 2, type: 'storage', color: 'bg-gray-800', showName: false }, + { + id: 'nas-1u-35', + name: '1U 3.5" HDD Bay', + uSize: 1, + type: 'storage', + color: 'bg-gray-800', + showName: false, + }, + { + id: 'nas-2u-35', + name: '2U 3.5" HDD Bay', + uSize: 2, + type: 'storage', + color: 'bg-gray-800', + showName: false, + }, + { + id: 'nas-2u-25', + name: '2U 2.5" SSD Bay', + uSize: 2, + type: 'storage', + color: 'bg-gray-800', + showName: false, + }, // Networking - { id: 'switch-48', name: '48-Port Switch', uSize: 1, type: 'network', color: 'bg-cyan-950', showName: false }, - { id: 'switch-24', name: '24-Port Switch', uSize: 1, type: 'network', color: 'bg-cyan-950', showName: false }, - { id: 'switch-16', name: '16-Port Switch', uSize: 1, type: 'network', color: 'bg-cyan-950', showName: false }, - { id: 'switch-8', name: '8-Port Switch', uSize: 1, type: 'network', color: 'bg-cyan-950', showName: false }, - { id: 'switch-5', name: '5-Port Switch', uSize: 1, type: 'network', color: 'bg-cyan-950', showName: false }, + { + id: 'switch-48', + name: '48-Port Switch', + uSize: 1, + type: 'network', + color: 'bg-cyan-950', + showName: false, + }, + { + id: 'switch-24', + name: '24-Port Switch', + uSize: 1, + type: 'network', + color: 'bg-cyan-950', + showName: false, + }, + { + id: 'switch-16', + name: '16-Port Switch', + uSize: 1, + type: 'network', + color: 'bg-cyan-950', + showName: false, + }, + { + id: 'switch-8', + name: '8-Port Switch', + uSize: 1, + type: 'network', + color: 'bg-cyan-950', + showName: false, + }, + { + id: 'switch-5', + name: '5-Port Switch', + uSize: 1, + type: 'network', + color: 'bg-cyan-950', + showName: false, + }, // Power - { id: 'pdu-1u', name: 'PDU 1U', uSize: 1, type: 'power', color: 'bg-gray-800', showName: false }, - { id: 'ups-1u', name: 'UPS 1U', uSize: 1, type: 'power', color: 'bg-gray-800', showName: false }, + { + id: 'pdu-1u', + name: 'PDU 1U', + uSize: 1, + type: 'power', + color: 'bg-gray-800', + showName: false, + }, + { + id: 'ups-1u', + name: 'UPS 1U', + uSize: 1, + type: 'power', + color: 'bg-gray-800', + showName: false, + }, // Accessories { @@ -76,5 +146,4 @@ export const PREDEFINED_MODULES: RackModule[] = [ color: 'bg-slate-900', showName: false, }, - ];