Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
daca53c
Update Docker workflow to support new branch naming and improve image…
breyr Mar 2, 2025
0a66c41
Refactor Docker workflow to use correct pull request number syntax fo…
breyr Mar 2, 2025
3926313
Update action (#54)
breyr Mar 2, 2025
d7f5fb4
Red 76 UI changes (#55)
Tylermui Mar 3, 2025
2d2d9ab
refactored react code (#56)
breyr Mar 4, 2025
b4d6da1
Red 72 topology send create and delete link requests (#57)
breyr Mar 4, 2025
4220c89
Auth and onboarding fixes (#58)
breyr Mar 7, 2025
7f754ec
Bug fixes (#59)
breyr Mar 20, 2025
a2b4030
DONE (#60)
Tylermui Mar 20, 2025
3366342
table (#61)
Tylermui Mar 20, 2025
6a7f60e
Implement table structure and layout adjustments (#62)
breyr Mar 21, 2025
acf79af
Connections table again (#63)
breyr Mar 21, 2025
a27088e
Refactor table layout for improved responsiveness (#64)
breyr Mar 21, 2025
e0adcac
Stretch features (#65)
breyr Mar 21, 2025
f9fea0f
Stretch features (#66)
breyr Mar 22, 2025
acad8f8
Stretch features (#67)
breyr Mar 22, 2025
8241fb7
Stretch features (#68)
breyr Mar 24, 2025
d790a3c
Stretch features (#69)
breyr Mar 25, 2025
361993b
Stretch features (#70)
breyr Mar 25, 2025
3a0fe9e
Stretch features (#71)
breyr Mar 25, 2025
b3f11b8
Stretch features (#72)
breyr Mar 25, 2025
067b917
Stretch features (#73)
breyr Mar 28, 2025
dc97442
Stretch features (#74)
breyr Mar 28, 2025
181ff1d
Merge branch 'main' into dev
breyr Mar 31, 2025
fcdfdd5
Enhance port calculation logic to adjust for device number in VLAN ma…
breyr Mar 31, 2025
cd5c521
Bug fixes (#77)
breyr Mar 31, 2025
8482cc3
Refactor link operations and enhance substring extraction for port fo…
breyr Mar 31, 2025
117654d
Bump vite version (#79)
breyr Mar 31, 2025
2c18564
Topology card updates (#81)
breyr Mar 31, 2025
96090a0
Enhance context menu and link creation modal to display available por…
breyr Mar 31, 2025
7cf332a
More fixes (#83)
breyr Apr 1, 2025
983fe90
Update compose.prod.yaml
breyr Apr 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/controllers/DeviceController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 4 additions & 4 deletions backend/src/repositories/PrismaDeviceRepository.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -108,16 +108,16 @@ export class PrismaDeviceRepository implements IDeviceRepository {
});
}

async unbookDevice(deviceId: number, userId: number): Promise<Device | null> {
async unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise<Device | null> {
return await this.prisma.$transaction(async (tx) => {
// check if device is already booked
const current = await tx.device.findUnique({
where: { id: deviceId },
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");
}

Expand Down
6 changes: 3 additions & 3 deletions backend/src/services/DeviceService.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -58,7 +58,7 @@ export class DeviceService implements IDeviceService {
return this.deviceRepository.bookDevice(deviceId, userId);
}

async unbookDevice(deviceId: number, userId: number): Promise<Device | null> {
return this.deviceRepository.unbookDevice(deviceId, userId);
async unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise<Device | null> {
return this.deviceRepository.unbookDevice(deviceId, userId, accountType);
}
}
6 changes: 3 additions & 3 deletions backend/src/types/classInterfaces.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -55,7 +55,7 @@ export interface IDeviceRepository {
findByType(deviceType: DeviceType): Promise<Device[]>;
findByIcon(deviceIcon: IconType): Promise<Device[]>;
bookDevice(deviceId: number, userId: number): Promise<Device | null>;
unbookDevice(deviceId: number, userId: number): Promise<Device | null>;
unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise<Device | null>;
}

export interface IDeviceService {
Expand All @@ -69,7 +69,7 @@ export interface IDeviceService {
getDevicesByType(deviceType: DeviceType): Promise<Device[]>;
getDevicesByIcon(deviceIcon: IconType): Promise<Device[]>;
bookDevice(deviceId: number, userId: number): Promise<Device | null>;
unbookDevice(deviceId: number, userId: number): Promise<Device | null>;
unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise<Device | null>;
}

export interface IConnectionRepository {
Expand Down
6 changes: 3 additions & 3 deletions compose.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,15 +25,15 @@ services:
- postgres

frontend:
image: breyr/top-frontend:1.0.0
image: breyr/top-frontend:1.0.1
container_name: frontend
ports:
- "80:80"
depends_on:
- 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
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
15 changes: 6 additions & 9 deletions frontend/src/components/TopologyCard.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -21,15 +21,13 @@ const TopologyCard: React.FC<TopologyProps> = ({
onDelete,
onArchive,
readOnly,
userId
}) => {
const { menuOpen, hideMenu } = useContextMenu();
const { user } = useAuth();
const navigateTo = useNavigate();
const [thumbnailSrc, setThumbnailSrc] = useState<string | null>(null);
const [archived, setArchived] = useState(initialArchived);
const [isModalOpen, setIsModalOpen] = useState(false);
const ownsTopology = user?.id === userId;

const handleClick = (event: React.MouseEvent) => {
if (archived) return;
Expand Down Expand Up @@ -87,9 +85,9 @@ const TopologyCard: React.FC<TopologyProps> = ({
return (
<div className="relative group">
<button
onClick={handleDeleteClick}
className="hidden group-hover:block absolute top-0 right-[-20px] m-1 p-2 shadow-md text-red-500 bg-gray-50 rounded-full hover:bg-gray-100 z-10">
<Trash size={16} />
onClick={(e) => archived ? handleDeleteClick(e) : toggleArchived(e)}
className="hidden group-hover:block absolute top-0 right-[-20px] m-1 p-2 shadow-md bg-gray-50 rounded-full hover:bg-gray-100 z-10">
{archived ? <Trash size={16} className='text-red-500' /> : <Archive size={16} className='text-amber-500' />}
</button>
<div
key={id}
Expand Down Expand Up @@ -119,10 +117,9 @@ const TopologyCard: React.FC<TopologyProps> = ({
</p>
</div>
<span
onClick={!ownsTopology ? undefined : toggleArchived}
className={`inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full ${archived
? `bg-red-100 text-red-700 ${!ownsTopology ? "cursor-default" : "hover:bg-red-300 cursor-pointer"}`
: `bg-green-100 text-green-700 ${!ownsTopology ? "cursor-default" : "hover:bg-green-300 cursor-pointer"}`
? `bg-red-100 text-red-700`
: `bg-green-100 text-green-700`
}`}
>
{archived ? "Archived" : "Active"}
Expand Down
80 changes: 55 additions & 25 deletions frontend/src/components/TopologyDetailCard.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,28 +12,18 @@ interface TopologyProps extends Topology {
const TopologyDetailCard: React.FC<TopologyProps> = ({
id,
name,
archived: initialArchived,
archived,
updatedAt,
onDelete,
onArchive,
userId,
thumbnail,
reactFlowState
}) => {
const { user } = useAuth();
const [archived, setArchived] = useState(initialArchived);
const [thumbnailSrc, setThumbnailSrc] = useState<string | null>(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);
Expand All @@ -45,6 +34,30 @@ const TopologyDetailCard: React.FC<TopologyProps> = ({
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 (
<div className="relative group">
<button
Expand All @@ -56,6 +69,19 @@ const TopologyDetailCard: React.FC<TopologyProps> = ({
key={id}
className={`my-5 rounded-lg border border-gray-200 bg-[#ffffff] shadow-sm transition-shadow duration-200 flex flex-col items-center text-gray-700`}
>
<div className="w-full">
{thumbnailSrc ? (
<img
src={thumbnailSrc}
alt="Topology Thumbnail"
className="w-full h-36 object-cover bg-gray-100 rounded-t-md"
/>
) : (
<div className="w-full h-36 flex items-center justify-center bg-gray-100 rounded-t-md">
<Image size={80} className="text-gray-400" />
</div>
)}
</div>
<div className="w-full flex-1 rounded-b-md p-5">
<p className="text-sm font-medium text-gray-900 mb-1">{name}</p>
<div className="flex justify-between w-full items-center">
Expand All @@ -66,22 +92,26 @@ const TopologyDetailCard: React.FC<TopologyProps> = ({
</p>
</div>
<span
onClick={!ownsTopology ? undefined : toggleArchived}
className={`inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full ${archived
? `bg-red-100 text-red-700 ${!ownsTopology ? "cursor-default" : "hover:bg-red-300 cursor-pointer"}`
: `bg-green-100 text-green-700 ${!ownsTopology ? "cursor-default" : "hover:bg-green-300 cursor-pointer"}`
? `bg-red-100 text-red-700`
: `bg-green-100 text-green-700`
}`}
>
{archived ? "Archived" : "Active"}
</span>
</div>
<div className='mt-2'>
<p className='text-md text-gray-500'>Devices in Use</p>
<div className='flex flex-col'>
{devicesUsedInTopology?.map(d => (
<span key={d} className='text-xs'>{d}</span>
))}
</div>
{
!archived &&
<>
<p className='text-md text-gray-500'>Devices in Use</p>
<div className='flex flex-col'>
{devicesUsedInTopology?.map(d => (
<span key={d} className='text-xs'>{d}</span>
))}
</div>
</>
}
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/reactflow/TopologyCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ const TopologyCanvas = () => {
selectionMode={SelectionMode.Partial}
onPaneClick={onPaneClick}
onNodeContextMenu={onNodeContextMenu}
snapToGrid={true}
>
<Background color="rgb(247, 247, 247)" variant={BackgroundVariant.Dots} size={3} />
{menu && (
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/components/reactflow/overlayui/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading