Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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