diff --git a/README.md b/README.md
index bc3c1b1..9fcfe26 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,129 @@
-# T.O.P. (Topology Orchestration Platform)
+# TOPHAT
+
+## Installation
+
+### STEP 1 - Interconnects
+
+### Supported Platforms
+Interconnect devices must meet the following requirements to ensure compatibility with TOPHAT:
+
+**Operating System (OS)**
+
+ - Cisco IOSv
+ - Cisco IOS
+ - Cisco IOS-XE
+
+**Port Density**
+
+TOPHAT can support up to 2 Interconnect devices, each with varying port densities:
+
+ - **1x Interconnects**
+ - Must be 48 ports
+ - **2x Interconnects**
+ - Interconnect 1: 48 ports
+ - Interconnect 2: 24-48 ports
+
+---
+
+#### Initial Configuration
+
+Interconnect devices must be **remotely accessible** via **SSH** from the **out-of-band (OOB) management interface** to the TOPHAT application host.
+
+It is recommended to use spanning-tree mode MST, and static assign OOB IP addresses for the Interconnects.
+
+---
+
+##### Authentication
+
+- **SSH Access**: Required for secure remote administration.
+- **User Authentication**: Devices must support **username/password authentication**.
+- **Privilege Escalation**: An **enable secret password** must be configured for administrative access.
+
+An example basic configuration is provided below:
+
+```
+hostname Interconnect
+ip domain-name interconnect.lab
+username admin privilege 15 secret 0 cisco
+enable secret cisco
+line vty 0 15
+ login local
+ transport input ssh
+crypto key generate rsa modulus 2048
+ip ssh version 2
+```
+
+---
+
+##### Lab Device Ports
+
+All device interfaces (**excluding the last four ports**) must be configured for **dot1q tunneling (QinQ)** to encapsulate Layer 2 protocol frames. These ports serve as direct connections to lab devices.
+
+```
+interface range GigabitEthernet1/0/1-44
+ shutdown
+ no switchport access vlan
+ switchport mode dot1q-tunnel
+ negotiation auto
+ mtu 9000
+ mtu 8978
+ l2protocol-tunnel cdp
+ l2protocol-tunnel lldp
+ l2protocol-tunnel stp
+ l2protocol-tunnel vtp
+ l2protocol-tunnel point-to-point pagp
+ l2protocol-tunnel point-to-point lacp
+ l2protocol-tunnel point-to-point udld
+ no cdp enable
+```
+
+---
+
+##### Transport Ports
+
+The last four interfaces (**45-48** or **21-24**, depending on the platform) are dedicated to **transporting traffic between Interconnects**.
+
+*If you are only using one Interconnect, shut these ports.*
+
+```
+port-channel load-balance src-dst-mac
+!
+interface GigabitEthernet1/0/45-48
+ channel-protocol lacp
+ channel-group 1 mode active
+ no shutdown
+!
+interface Port-channel1
+ switchport mode trunk
+ switchport trunk allowed vlan all
+ switchport nonegotiate
+ mtu 9000
+ mtu 8978
+ no cdp enable
+ no shutdown
+```
+
+### STEP 2 - Application
+
+#### Environment Setup
+
+Once the Interconnects are configured, proceed with the installation of the TOPHAT application.
+
+Navigate to the [TOPHAT GitHub](https://github.com/breyr/TOPHAT/blob/main/compose.prod.yaml) repository, and make a copy of the `compose.prod.yaml` file.
+
+Save the file as `docker-compose.yml` in your desired directory.
+
+#### Running the Application
+
+Run the following command to start the application:
+
+```sh
+docker-compose -f docker-compose.yml up -d
+```
+
+TOPHAT will now be running at [0.0.0.0:80](0.0.0.0:80).
+
+To expose TOPHAT outside of your LAN, we recommend using [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) to securely expose the UI externally with ZeroTrust.
## Developing
diff --git a/backend/src/controllers/DeviceController.ts b/backend/src/controllers/DeviceController.ts
index ca51445..e572438 100644
--- a/backend/src/controllers/DeviceController.ts
+++ b/backend/src/controllers/DeviceController.ts
@@ -162,7 +162,7 @@ export class DeviceController {
try {
const { id } = req.params;
// will have a payload user id otherwise this request is not authenticated through middleware
- const device = await this.deviceService.unbookDevice(parseInt(id), req.jwt_payload?.id!);
+ const device = await this.deviceService.unbookDevice(parseInt(id), req.jwt_payload?.id!, req.jwt_payload?.accountType!);
// we successfully unbooked the device
if (device) {
diff --git a/backend/src/repositories/PrismaDeviceRepository.ts b/backend/src/repositories/PrismaDeviceRepository.ts
index a119bac..d96e198 100644
--- a/backend/src/repositories/PrismaDeviceRepository.ts
+++ b/backend/src/repositories/PrismaDeviceRepository.ts
@@ -1,4 +1,4 @@
-import { DeviceType, IconType, PrismaClient, type Device } from "@prisma/client";
+import { AccountType, DeviceType, IconType, PrismaClient, type Device } from "@prisma/client";
import bcrypt from 'bcryptjs';
import { IDeviceRepository } from "../types/classInterfaces";
@@ -108,7 +108,7 @@ export class PrismaDeviceRepository implements IDeviceRepository {
});
}
- async unbookDevice(deviceId: number, userId: number): Promise {
+ async unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise {
return await this.prisma.$transaction(async (tx) => {
// check if device is already booked
const current = await tx.device.findUnique({
@@ -116,8 +116,8 @@ export class PrismaDeviceRepository implements IDeviceRepository {
select: { userId: true }
});
- // only allow unbooking of device if userIds match
- if (current?.userId !== userId) {
+ // only allow unbooking of device if userIds match AND account type is not admin or owner
+ if (accountType !== 'ADMIN' && accountType !== 'OWNER' && current?.userId !== userId) {
throw new Error("UNAUTHORIZED");
}
diff --git a/backend/src/services/DeviceService.ts b/backend/src/services/DeviceService.ts
index e6be06b..83248e7 100644
--- a/backend/src/services/DeviceService.ts
+++ b/backend/src/services/DeviceService.ts
@@ -1,4 +1,4 @@
-import type { Device, DeviceType, IconType } from "@prisma/client";
+import type { AccountType, Device, DeviceType, IconType } from "@prisma/client";
import { IDeviceRepository, IDeviceService } from "../types/classInterfaces";
export class DeviceService implements IDeviceService {
@@ -58,7 +58,7 @@ export class DeviceService implements IDeviceService {
return this.deviceRepository.bookDevice(deviceId, userId);
}
- async unbookDevice(deviceId: number, userId: number): Promise {
- return this.deviceRepository.unbookDevice(deviceId, userId);
+ async unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise {
+ return this.deviceRepository.unbookDevice(deviceId, userId, accountType);
}
}
\ No newline at end of file
diff --git a/backend/src/types/classInterfaces.ts b/backend/src/types/classInterfaces.ts
index 3ef05ef..de4b6a4 100644
--- a/backend/src/types/classInterfaces.ts
+++ b/backend/src/types/classInterfaces.ts
@@ -1,5 +1,5 @@
// holds interfaces similar to C#
-import { AppConfig, AppUser, Connection, Device, DeviceType, IconType, Topology } from "@prisma/client";
+import { AccountType, AppConfig, AppUser, Connection, Device, DeviceType, IconType, Topology } from "@prisma/client";
import type { CreateConnectionRequestPayload, CreateTopologyRequestPayload, RegisterUserRequestPayload } from "common";
import { UpdateTopologyDTO } from "./types";
@@ -55,7 +55,7 @@ export interface IDeviceRepository {
findByType(deviceType: DeviceType): Promise;
findByIcon(deviceIcon: IconType): Promise;
bookDevice(deviceId: number, userId: number): Promise;
- unbookDevice(deviceId: number, userId: number): Promise;
+ unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise;
}
export interface IDeviceService {
@@ -69,7 +69,7 @@ export interface IDeviceService {
getDevicesByType(deviceType: DeviceType): Promise;
getDevicesByIcon(deviceIcon: IconType): Promise;
bookDevice(deviceId: number, userId: number): Promise;
- unbookDevice(deviceId: number, userId: number): Promise;
+ unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise;
}
export interface IConnectionRepository {
diff --git a/compose.prod.yaml b/compose.prod.yaml
index 7a69e07..59b9aeb 100644
--- a/compose.prod.yaml
+++ b/compose.prod.yaml
@@ -14,7 +14,7 @@ services:
- postgres_data:/var/lib/postgresql/data
backend:
- image: breyr/top-backend:1.0.0
+ image: breyr/top-backend:1.0.1
container_name: backend
environment:
DATABASE_URL: postgres://demo:demo@postgres:5432/demo
@@ -25,7 +25,7 @@ services:
- postgres
frontend:
- image: breyr/top-frontend:1.0.0
+ image: breyr/top-frontend:1.0.1
container_name: frontend
ports:
- "80:80"
@@ -33,7 +33,7 @@ services:
- backend
interconnect-api:
- image: breyr/top-interconnectapi:1.0.0
+ image: breyr/top-interconnectapi:1.0.1
container_name: interconnect-api
environment:
SECRET_KEY: your_secret
diff --git a/frontend/package.json b/frontend/package.json
index b4046f9..2fcc379 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -40,7 +40,7 @@
"tailwindcss": "^3.4.14",
"typescript": "^5.5.3",
"typescript-eslint": "^8.7.0",
- "vite": "^6.1.1",
+ "vite": "^6.2.4",
"vite-plugin-svgr": "^4.3.0"
}
}
diff --git a/frontend/src/components/TopologyCard.tsx b/frontend/src/components/TopologyCard.tsx
index aa941c9..943b543 100644
--- a/frontend/src/components/TopologyCard.tsx
+++ b/frontend/src/components/TopologyCard.tsx
@@ -1,5 +1,5 @@
import { Topology } from 'common';
-import { Image, Trash } from "lucide-react";
+import { Archive, Image, Trash } from "lucide-react";
import React, { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth.ts";
@@ -21,7 +21,6 @@ const TopologyCard: React.FC = ({
onDelete,
onArchive,
readOnly,
- userId
}) => {
const { menuOpen, hideMenu } = useContextMenu();
const { user } = useAuth();
@@ -29,7 +28,6 @@ const TopologyCard: React.FC = ({
const [thumbnailSrc, setThumbnailSrc] = useState(null);
const [archived, setArchived] = useState(initialArchived);
const [isModalOpen, setIsModalOpen] = useState(false);
- const ownsTopology = user?.id === userId;
const handleClick = (event: React.MouseEvent) => {
if (archived) return;
@@ -87,9 +85,9 @@ const TopologyCard: React.FC = ({
return (
= ({
{archived ? "Archived" : "Active"}
diff --git a/frontend/src/components/TopologyDetailCard.tsx b/frontend/src/components/TopologyDetailCard.tsx
index 80be032..68e5802 100644
--- a/frontend/src/components/TopologyDetailCard.tsx
+++ b/frontend/src/components/TopologyDetailCard.tsx
@@ -1,7 +1,6 @@
import { Topology } from 'common';
-import { Trash } from "lucide-react";
-import React, { useState } from 'react';
-import { useAuth } from "../hooks/useAuth.ts";
+import { Image, Trash } from "lucide-react";
+import React, { useEffect, useState } from 'react';
import DeletionModal from "./DeletionModal.tsx";
interface TopologyProps extends Topology {
@@ -13,28 +12,18 @@ interface TopologyProps extends Topology {
const TopologyDetailCard: React.FC = ({
id,
name,
- archived: initialArchived,
+ archived,
updatedAt,
onDelete,
- onArchive,
- userId,
+ thumbnail,
reactFlowState
}) => {
- const { user } = useAuth();
- const [archived, setArchived] = useState(initialArchived);
+ const [thumbnailSrc, setThumbnailSrc] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
- const ownsTopology = user?.id === userId;
// get the devices on the topology
const devicesUsedInTopology = reactFlowState?.nodes.map(n => n.id);
- // toggle the archive state
- const toggleArchived = (event: React.MouseEvent) => {
- event.stopPropagation();
- setArchived(!archived);
- onArchive(id);
- };
-
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation();
setIsModalOpen(true);
@@ -45,6 +34,30 @@ const TopologyDetailCard: React.FC = ({
setIsModalOpen(false);
};
+ // handle conversion of thumbnail
+ useEffect(() => {
+ if (!thumbnail) return;
+ try {
+ // convert the object to a Uint8Array
+ const byteArray = new Uint8Array(Object.values(thumbnail));
+
+ // convert Uint8Array to binary string
+ const binaryString = Array.from(byteArray)
+ .map(byte => String.fromCharCode(byte))
+ .join('');
+
+ // convert binary string to base64
+ const base64String = btoa(binaryString);
+ // check if empty
+ if (base64String !== "AA==") {
+ const thumbnailSourceString = `data:image/jpg;base64,${base64String}`;
+ setThumbnailSrc(thumbnailSourceString);
+ }
+ } catch (error) {
+ console.error('Error converting to base64:', error);
+ }
+ }, [thumbnail]);
+
return (
diff --git a/frontend/src/components/reactflow/TopologyCanvas.tsx b/frontend/src/components/reactflow/TopologyCanvas.tsx
index 7425ec6..68c3e1b 100644
--- a/frontend/src/components/reactflow/TopologyCanvas.tsx
+++ b/frontend/src/components/reactflow/TopologyCanvas.tsx
@@ -329,6 +329,7 @@ const TopologyCanvas = () => {
selectionMode={SelectionMode.Partial}
onPaneClick={onPaneClick}
onNodeContextMenu={onNodeContextMenu}
+ snapToGrid={true}
>
{menu && (
diff --git a/frontend/src/components/reactflow/overlayui/ContextMenu.tsx b/frontend/src/components/reactflow/overlayui/ContextMenu.tsx
index 731eb62..71c72a2 100644
--- a/frontend/src/components/reactflow/overlayui/ContextMenu.tsx
+++ b/frontend/src/components/reactflow/overlayui/ContextMenu.tsx
@@ -84,11 +84,17 @@ export default function ContextMenu({
onClick(); // close the context menu
setDisableDelete(true);
// Check if the device has any edges
- const edgesForDevice = edges.filter(e => e.source === node?.data.deviceData?.name || e.target === node?.data.deviceData?.name);
- console.log(edgesForDevice);
+ const edgesForDevice = edges.filter(e => e.source === node?.data.deviceData?.name || e.target === node?.data.deviceData?.name).map(e => ({
+ value: e.id,
+ label: `(${e.source}) ${e.data?.sourcePort ?? ''} -> (${e.target}) ${e.data?.targetPort ?? ''}`,
+ firstLabDevice: e.source,
+ firstLabDevicePort: e.data?.sourcePort ?? '',
+ secondLabDevice: e.target,
+ secondLabDevicePort: e.data?.targetPort ?? '',
+ }));
if (edgesForDevice.length > 0) {
// attempt to delete all links
- const numFailures = await deleteLinkBulk(new Set(currentEdges));
+ const numFailures = await deleteLinkBulk(new Set(edgesForDevice));
// only remove the node if all links were successfully deleted
if (numFailures === 0) {
diff --git a/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx b/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx
index 7de1fbe..ac92688 100644
--- a/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx
+++ b/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx
@@ -16,37 +16,24 @@ interface CreateLinkModalProps {
export default function CreateLinkModal({ deviceData, currentDevicePorts, labDevices, onClose }: CreateLinkModalProps) {
const { user } = useAuth();
- const { getEdges, getNodes } = useReactFlow, Edge>();
+ const { getEdges } = useReactFlow, Edge>();
const { createLink } = useLinkOperations();
const [selectedFirstDevice, setSelectedFirstDevice] = useState(deviceData?.name ?? "");
const [selectedFirstDevicePort, setSelectedFirstDevicePort] = useState("");
const [selectedSecondDevice, setSelectedSecondDevice] = useState("");
const [selectedSecondDevicePort, setSelectedSecondDevicePort] = useState("");
const [availablePorts, setAvailablePorts] = useState([]);
- const [filteredLabDevices] = useState(labDevices);
const [firstDeviceOccupiedPorts, setFirstDeviceOccupiedPorts] = useState([]);
- const [secondDeviceOccupiedPorts, setSecondDeviceOccupiedPorts] = useState([]);
+ const [secondDeviceOccupiedPorts, setSecondDeviceOccupiedPorts] = useState>({});
useEscapeKey(onClose);
- // get a list of the current nodes in the topology to create a link
- const nodes = getNodes();
- const currentNodesInTopology = new Set(nodes.map(n => n.data?.deviceData?.name));
-
- // get occupied ports when selectedFirstDevice changes
useEffect(() => {
const edges = getEdges();
const ports = edges.filter((edge: Edge) => edge.source === selectedFirstDevice || edge.target === selectedFirstDevice).map((edge: Edge) => edge.id.split('-').filter((port: string) => port !== 'edge'));
setFirstDeviceOccupiedPorts(ports.flat());
}, [selectedFirstDevice, getEdges]);
- // get occupied ports when selectedSecondDevice changes
- useEffect(() => {
- const edges = getEdges();
- const ports = edges.filter((edge: Edge) => edge.source === selectedSecondDevice || edge.target === selectedSecondDevice).map((edge: Edge) => edge.id.split('-').filter((port: string) => port !== 'edge'));
- setSecondDeviceOccupiedPorts(ports.flat());
- }, [selectedSecondDevice, getEdges]);
-
useEffect(() => {
if (deviceData?.name) {
setSelectedFirstDevice(deviceData.name);
@@ -89,6 +76,31 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev
});
}
+ useEffect(() => {
+ const edges = getEdges();
+ if (selectedSecondDevice) {
+ const ports = edges
+ .filter((edge: Edge) => edge.source === selectedSecondDevice || edge.target === selectedSecondDevice)
+ .map((edge: Edge) => edge.id.split('-').filter((port: string) => port !== 'edge'));
+ setSecondDeviceOccupiedPorts((prev) => ({
+ ...prev,
+ [selectedSecondDevice]: ports.flat(),
+ }));
+ } else {
+ // Initialize occupied ports for all devices on mount
+ const allOccupiedPorts = labDevices.reduce((acc, device) => {
+ const devicePorts = edges
+ .filter((edge: Edge) => edge.source === device.name || edge.target === device.name)
+ .map((edge: Edge) => edge.id.split('-').filter((port: string) => port !== 'edge'))
+ .flat();
+ acc[device.name] = devicePorts;
+ return acc;
+ }, {} as Record);
+
+ setSecondDeviceOccupiedPorts(allOccupiedPorts);
+ }
+ }, [selectedSecondDevice, getEdges, labDevices]);
+
return (
@@ -109,11 +121,16 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev
disabled={!!deviceData?.name}
>
- {labDevices.filter((device) => device.userId == null || device.userId == user?.id).map((device) => (
-
- ))}
+ {labDevices.filter((device) => device.userId == null || device.userId == user?.id).map((device) => {
+ const portsArray = device.ports.split(',');
+ const generatedPorts = portsArray.flatMap(portDef => generatePorts(portDef));
+ const hasAvailablePorts = generatedPorts.some(port => !firstDeviceOccupiedPorts.includes(port));
+ return (
+
+ );
+ })}
@@ -122,9 +139,10 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev
value={selectedFirstDevicePort}
onChange={(e) => setSelectedFirstDevicePort(e.target.value)}
className="block w-full mt-1 rounded-md bg-[#ffffff] focus:outline-none"
+
>
- {(deviceData ? currentDevicePorts : filteredLabDevices.map(device => device.ports.split(',').flatMap(portDef => generatePorts(portDef)))).flat().map((port, index) => (
+ {(deviceData ? currentDevicePorts : labDevices.map(device => device.ports.split(',').flatMap(portDef => generatePorts(portDef)))).flat().map((port, index) => (
@@ -139,11 +157,21 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev
className="block w-full mt-1 rounded-md bg-[#ffffff] focus:outline-none"
>
- {filteredLabDevices.filter((device) => (device.userId == null || device.userId == user?.id) && currentNodesInTopology.has(device.name)).map((device) => (
-
- ))}
+ {labDevices.filter((d) => d.name !== deviceData?.name).map((device) => {
+ const portsArray = device.ports.split(',');
+ const generatedPorts = portsArray.flatMap(portDef => generatePorts(portDef));
+ const occupiedPorts = secondDeviceOccupiedPorts[device.name] || [];
+ const hasAvailablePorts = generatedPorts.some(port => !occupiedPorts.includes(port));
+ return (
+
+ );
+ })}
@@ -151,14 +179,18 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev
diff --git a/frontend/src/hooks/useLinkOperations.ts b/frontend/src/hooks/useLinkOperations.ts
index 58711b1..211450e 100644
--- a/frontend/src/hooks/useLinkOperations.ts
+++ b/frontend/src/hooks/useLinkOperations.ts
@@ -32,6 +32,7 @@ export function useLinkOperationsBase() {
}
const interconnectDevices = await authenticatedApiClient.getDevicesByType('INTERCONNECT');
+ // only two devices here so find is okay
return interconnectDevices.data?.find(d => d.name === connectionInfo.interconnectDeviceName);
};
@@ -57,11 +58,15 @@ export function useLinkOperationsBase() {
return true;
};
- const removePortNumber = (port: string) => port.replace(/\/\d+$/, '/');
+ const removePortNumber = (port?: string) => port?.split('|')[0];
const calculateOffsetPort = (port: string, deviceNumber: number) => {
// splits off interface number and calculates offset
- return Number(port.split('/').pop()) * deviceNumber;
+ let devicePort = Number(port.split('/').pop())
+ if (deviceNumber === 2) {
+ devicePort += 44 // For IDs being fed to the VLAN mapping algorithm.
+ }
+ return devicePort;
};
// API operations without ReactFlow dependencies
@@ -99,29 +104,41 @@ export function useLinkOperationsBase() {
}
// Prepare link payload
- const interconnect1Prefix = removePortNumber(firstConnectionInfo.interconnectDevicePort);
- const interconnect2Prefix = removePortNumber(secondConnectionInfo.interconnectDevicePort);
+ // Get the correct interconnect information based on the device number for the interconnect
+ const [interconnect1, interconnect2] = firstInterconnectInfo?.deviceNumber === 1
+ ? [firstInterconnectInfo, secondInterconnectInfo]
+ : [secondInterconnectInfo, firstInterconnectInfo];
+
+ const interconnect1Prefix = removePortNumber(interconnect1?.ports);
+ const interconnect2Prefix = removePortNumber(interconnect2?.ports);
- const offsetPort1 = calculateOffsetPort(
- firstConnectionInfo.interconnectDevicePort,
- firstInterconnectInfo!.deviceNumber!
- );
+ // if ports is undefined
+ if (!interconnect1Prefix || !interconnect2Prefix) {
+ return false;
+ }
- const offsetPort2 = calculateOffsetPort(
- secondConnectionInfo.interconnectDevicePort,
- secondInterconnectInfo!.deviceNumber!
- );
+ const [offsetPort1, offsetPort2] = firstInterconnectInfo?.deviceNumber === 1
+ ?
+ [
+ calculateOffsetPort(firstConnectionInfo.interconnectDevicePort, firstInterconnectInfo!.deviceNumber!),
+ calculateOffsetPort(secondConnectionInfo.interconnectDevicePort, secondInterconnectInfo!.deviceNumber!)
+ ]
+ :
+ [
+ calculateOffsetPort(secondConnectionInfo.interconnectDevicePort, secondInterconnectInfo!.deviceNumber!),
+ calculateOffsetPort(firstConnectionInfo.interconnectDevicePort, firstInterconnectInfo!.deviceNumber!)
+ ]
const linkPayload: LinkRequest = {
- interconnect1IP: firstInterconnectInfo!.ipAddress,
+ interconnect1IP: interconnect1!.ipAddress,
interconnect1Prefix,
- interconnect2IP: secondInterconnectInfo!.ipAddress,
+ interconnect2IP: interconnect2!.ipAddress,
interconnect2Prefix,
interconnectPortID1: offsetPort1,
interconnectPortID2: offsetPort2,
- username: firstInterconnectInfo!.username!,
- password: firstInterconnectInfo!.password!,
- secret: firstInterconnectInfo!.secretPassword!
+ username: interconnect1!.username!,
+ password: interconnect1!.password!,
+ secret: interconnect1!.secretPassword!
};
// Perform the requested operation
@@ -301,6 +318,7 @@ export function useLinkOperations() {
// Override the base methods to include ReactFlow operations
const createLink = async (params: LinkOperationParams, createToastPerLink: boolean = true) => {
+ console.log(params);
const edgeId = `edge-${params.firstDeviceName}-${params.firstDevicePort}-${params.secondDeviceName}-${params.secondDevicePort}`;
createEdge(params);
const result = await baseOperations.createLink(params, createToastPerLink);
diff --git a/frontend/src/lib/helpers.ts b/frontend/src/lib/helpers.ts
index 62df851..b0439c1 100644
--- a/frontend/src/lib/helpers.ts
+++ b/frontend/src/lib/helpers.ts
@@ -68,12 +68,18 @@ export const getCurvedPath = (
};
export function substringFromFirstNumber(port: string) {
- const idx = port.search(/\d/);
- if (idx !== -1) {
- return port.substring(0, 3) + port.substring(idx);
+ // match up to the first 3 alphabetic characters followed by any numbers
+ const match = port.match(/^([a-zA-Z]{1,3})\D*(\d.*)$/);
+
+ if (match) {
+ const prefix = match[1]; // up to the first 3 alphabetic characters
+ const numbers = match[2]; // all numbers after the prefix
+ return prefix + numbers;
}
+
+ // if no match, return an empty string
return "";
-};
+}
// validate email format
export function validateEmail(email: string) {
diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx
index 953fdb8..3b479ab 100644
--- a/frontend/src/pages/Index.tsx
+++ b/frontend/src/pages/Index.tsx
@@ -55,14 +55,14 @@ export default function IndexPage() {
-
+
-
+
diff --git a/package-lock.json b/package-lock.json
index 9fbc8c8..65ac900 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -833,7 +833,7 @@
"tailwindcss": "^3.4.14",
"typescript": "^5.5.3",
"typescript-eslint": "^8.7.0",
- "vite": "^6.1.1",
+ "vite": "^6.2.4",
"vite-plugin-svgr": "^4.3.0"
}
},
@@ -3562,9 +3562,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
- "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
+ "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"cpu": [
"ppc64"
],
@@ -3578,9 +3578,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
- "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
+ "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"cpu": [
"arm"
],
@@ -3594,9 +3594,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
- "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
+ "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
"cpu": [
"arm64"
],
@@ -3610,9 +3610,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
- "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
+ "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"cpu": [
"x64"
],
@@ -3626,9 +3626,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
- "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
+ "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
"cpu": [
"arm64"
],
@@ -3642,9 +3642,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
- "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
+ "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"cpu": [
"x64"
],
@@ -3658,9 +3658,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
- "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
+ "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
"cpu": [
"arm64"
],
@@ -3674,9 +3674,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
- "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
+ "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"cpu": [
"x64"
],
@@ -3690,9 +3690,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
- "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
+ "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"cpu": [
"arm"
],
@@ -3706,9 +3706,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
- "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
+ "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"cpu": [
"arm64"
],
@@ -3722,9 +3722,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
- "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
+ "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
"cpu": [
"ia32"
],
@@ -3738,9 +3738,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
- "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
+ "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"cpu": [
"loong64"
],
@@ -3754,9 +3754,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
- "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
+ "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
"cpu": [
"mips64el"
],
@@ -3770,9 +3770,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
- "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
+ "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"cpu": [
"ppc64"
],
@@ -3786,9 +3786,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
- "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
+ "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"cpu": [
"riscv64"
],
@@ -3802,9 +3802,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
- "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
+ "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
"cpu": [
"s390x"
],
@@ -3818,9 +3818,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
- "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
+ "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"cpu": [
"x64"
],
@@ -3834,9 +3834,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
- "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
+ "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
"cpu": [
"arm64"
],
@@ -3850,9 +3850,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
- "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
+ "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"cpu": [
"x64"
],
@@ -3866,9 +3866,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
- "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
+ "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
"cpu": [
"arm64"
],
@@ -3882,9 +3882,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
- "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
+ "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"cpu": [
"x64"
],
@@ -3898,9 +3898,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
- "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
+ "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
"cpu": [
"x64"
],
@@ -3914,9 +3914,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
- "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
+ "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"cpu": [
"arm64"
],
@@ -3930,9 +3930,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
- "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
+ "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"cpu": [
"ia32"
],
@@ -3946,9 +3946,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
- "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
+ "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
"cpu": [
"x64"
],
@@ -5301,9 +5301,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
- "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+ "version": "0.25.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
+ "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
@@ -5313,31 +5313,31 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.24.2",
- "@esbuild/android-arm": "0.24.2",
- "@esbuild/android-arm64": "0.24.2",
- "@esbuild/android-x64": "0.24.2",
- "@esbuild/darwin-arm64": "0.24.2",
- "@esbuild/darwin-x64": "0.24.2",
- "@esbuild/freebsd-arm64": "0.24.2",
- "@esbuild/freebsd-x64": "0.24.2",
- "@esbuild/linux-arm": "0.24.2",
- "@esbuild/linux-arm64": "0.24.2",
- "@esbuild/linux-ia32": "0.24.2",
- "@esbuild/linux-loong64": "0.24.2",
- "@esbuild/linux-mips64el": "0.24.2",
- "@esbuild/linux-ppc64": "0.24.2",
- "@esbuild/linux-riscv64": "0.24.2",
- "@esbuild/linux-s390x": "0.24.2",
- "@esbuild/linux-x64": "0.24.2",
- "@esbuild/netbsd-arm64": "0.24.2",
- "@esbuild/netbsd-x64": "0.24.2",
- "@esbuild/openbsd-arm64": "0.24.2",
- "@esbuild/openbsd-x64": "0.24.2",
- "@esbuild/sunos-x64": "0.24.2",
- "@esbuild/win32-arm64": "0.24.2",
- "@esbuild/win32-ia32": "0.24.2",
- "@esbuild/win32-x64": "0.24.2"
+ "@esbuild/aix-ppc64": "0.25.2",
+ "@esbuild/android-arm": "0.25.2",
+ "@esbuild/android-arm64": "0.25.2",
+ "@esbuild/android-x64": "0.25.2",
+ "@esbuild/darwin-arm64": "0.25.2",
+ "@esbuild/darwin-x64": "0.25.2",
+ "@esbuild/freebsd-arm64": "0.25.2",
+ "@esbuild/freebsd-x64": "0.25.2",
+ "@esbuild/linux-arm": "0.25.2",
+ "@esbuild/linux-arm64": "0.25.2",
+ "@esbuild/linux-ia32": "0.25.2",
+ "@esbuild/linux-loong64": "0.25.2",
+ "@esbuild/linux-mips64el": "0.25.2",
+ "@esbuild/linux-ppc64": "0.25.2",
+ "@esbuild/linux-riscv64": "0.25.2",
+ "@esbuild/linux-s390x": "0.25.2",
+ "@esbuild/linux-x64": "0.25.2",
+ "@esbuild/netbsd-arm64": "0.25.2",
+ "@esbuild/netbsd-x64": "0.25.2",
+ "@esbuild/openbsd-arm64": "0.25.2",
+ "@esbuild/openbsd-x64": "0.25.2",
+ "@esbuild/sunos-x64": "0.25.2",
+ "@esbuild/win32-arm64": "0.25.2",
+ "@esbuild/win32-ia32": "0.25.2",
+ "@esbuild/win32-x64": "0.25.2"
}
},
"node_modules/escalade": {
@@ -6694,13 +6694,13 @@
}
},
"node_modules/vite": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz",
- "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==",
+ "version": "6.2.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
+ "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
"dev": true,
"dependencies": {
- "esbuild": "^0.24.2",
- "postcss": "^8.5.2",
+ "esbuild": "^0.25.0",
+ "postcss": "^8.5.3",
"rollup": "^4.30.1"
},
"bin": {