Skip to content
Open
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
73 changes: 11 additions & 62 deletions app/dash/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"use client";
import Navbar from "@/components/Navbar";
import ErrorToast from "@/components/Error";
import { Edit, Plus, Trash, Dice5, Copy, ScanSearch, ChevronRight} from "lucide-react";
import { Edit, Plus, Trash, Dice5, ScanSearch, ChevronRight} from "lucide-react";
import { useEffect, useState, useMemo } from "react";
import axios from "axios";
import Fuse from "fuse.js";
import Footer from "@/components/Footer"
import Cookies from "js-cookie";
import RandomModal from "./components/RandomModal";
import { SortType, Server, Port } from "@/app/types";
import { compareIp } from "@/app/utils";

Expand Down Expand Up @@ -41,7 +42,7 @@ export default function Dashboard() {
const [portPort, setPortPort] = useState<number | null>(null);
const [editItem, setEditItem] = useState<Server | Port | null>(null);

const [randomPort, setRandomPort] = useState<number | null>(null);

const [showRandomModal, setShowRandomModal] = useState(false);

const [isScanning, setIsScanning] = useState(false);
Expand Down Expand Up @@ -251,29 +252,6 @@ const usedPorts = useMemo(() => {
return ports;
}, [servers]);

const generateRandomPort = () => {
let port;
let attempts = 0;

do {
port = Math.floor(Math.random() * (65535 - 1024) + 1024);
attempts++;
} while (usedPorts.has(port) && attempts < 1000);

if (attempts >= 1000) {
handleError("Could not find free port after 1000 attempts");
return;
}

setRandomPort(port);
setShowRandomModal(true);
};

const copyToClipboard = () => {
if (randomPort !== null) {
navigator.clipboard.writeText(randomPort.toString());
}
};

const sortedPorts = (ports: Port[]) =>
[...ports].sort((a, b) => a.port - b.port);
Expand Down Expand Up @@ -377,48 +355,19 @@ const generateRandomPort = () => {

<button
className="btn btn-square"
onClick={generateRandomPort}
onClick={() => setShowRandomModal(true)}
title="Generate random port"
aria-label="Generate random port"
>
<Dice5/>
</button>
{showRandomModal && randomPort !== null && (
<dialog open className="modal" aria-label="Random port generated">
<div className="modal-box max-w-xs space-y-4" role="dialog" aria-labelledby="random-port-title">
<div className="text-center">
<h3 className="font-bold text-xl mb-1" id="random-port-title">Random Port Generator</h3>
<p className="text-sm opacity-75">Your allocated port number</p>
</div>

<div className="bg-base-200 rounded-box p-4 w-full text-center shadow-inner">
<span className="text-4xl font-mono font-bold tracking-wider">
{randomPort}
</span>
</div>

<div className="flex flex-col w-full gap-2">
<button
className="btn btn-block gap-2"
onClick={copyToClipboard}
title="Copy port"
aria-label="Copy port number to clipboard"
>
<Copy size={18} className="mr-1"/>
Copy Port
</button>

<button
className="btn btn-ghost btn-sm btn-circle absolute top-2 right-2"
onClick={() => setShowRandomModal(false)}
title="Close"
aria-label="Close random port dialog"
>
</button>
</div>
</div>
</dialog>
{showRandomModal && (
<RandomModal
setShowRandomModal={setShowRandomModal}
globalErrorHandler={handleError}
availableServers={servers}
usedPorts={usedPorts}
/>
)}

<button
Expand Down
159 changes: 159 additions & 0 deletions app/dash/components/RandomModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"use client";
import { Server } from "@/app/types";
import {
generateRandomPortWithServerContext,
generateRandomPortWithUsedPortsContext,
} from "@/app/utils";
import { Copy, RefreshCcw } from "lucide-react";
import { useEffect, useState } from "react";

type RandomModalProps = {
setShowRandomModal: (show: boolean) => void;
globalErrorHandler: (message: string) => void;
availableServers: Server[];
usedPorts: Set<number>;
};

const copyPortToClipboard = (randomPort: number | null) => {
if (randomPort !== null) {
navigator.clipboard.writeText(randomPort.toString());
}
};

const RandomModal = (props: RandomModalProps) => {
const {
setShowRandomModal,
availableServers,
usedPorts,
globalErrorHandler,
} = props;
const generationRetryAttempts = 1000;
const [randomPort, setRandomPort] = useState<number | null>(
generateRandomPortWithUsedPortsContext(usedPorts, generationRetryAttempts)
);
const [selectedServerId, setselectedServerId] = useState<number | undefined>(
undefined
);

const handleServerContextChange = (serverId: number) => {
if (!serverId) {
setRandomPort(
generateRandomPortWithUsedPortsContext(usedPorts, generationRetryAttempts)
);
setselectedServerId(undefined);
return
}
setselectedServerId(serverId);
const selectedServer = availableServers.find(
(server) => server.id === serverId
);
setRandomPort(
generateRandomPortWithServerContext(
selectedServer,
generationRetryAttempts
)
);
};

const handleRegeneratePort = () => {
if (selectedServerId) {
handleServerContextChange(selectedServerId);
} else {
setRandomPort(
generateRandomPortWithUsedPortsContext(
usedPorts,
generationRetryAttempts
)
);
}
};

useEffect(() => {
if (!randomPort) {
globalErrorHandler(
"Could not find free port after " + generationRetryAttempts + " attempts"
);
setShowRandomModal(false);
}
}, [randomPort, globalErrorHandler]);

return (
<>
<dialog open className="modal" aria-label="Random port generated">
<div
className="modal-box max-w-xs space-y-4"
role="dialog"
aria-labelledby="random-port-title"
>
<div className="text-center">
<h3 className="font-bold text-xl mb-1" id="random-port-title">
Random Port Generator
</h3>
</div>
{availableServers.length != 0 && (
<select
className="select w-full"
value={selectedServerId || ""}
onChange={(e) => {
handleServerContextChange(Number(e.target.value));
}}
required
>
<option value={undefined}>Select server for context</option>
{availableServers.map((server) => (
<option key={server.id} value={server.id}>
{server.name}
</option>
))}
</select>
)}
<div className="text-center">
<p className="text-sm opacity-75">Your allocated port number</p>
</div>
<div className="bg-base-200 rounded-box p-4 w-full text-center shadow-inner">
<span className="text-4xl font-mono font-bold tracking-wider">
{randomPort}
</span>
{!selectedServerId && availableServers.length != 0 && (
<p className="text-sm opacity-75">
Port generated without specific server context in mind
</p>
)}
</div>

<div className="flex flex-col w-full gap-2">
<button
className="btn btn-block gap-2"
onClick={handleRegeneratePort}
title="Regenerate"
aria-label="Regenerate Port"
>
<RefreshCcw size={18} className="mr-1" />
Regenerate Port
</button>
<button
className="btn btn-block gap-2"
onClick={() => copyPortToClipboard(randomPort)}
title="Copy port"
aria-label="Copy port number to clipboard"
>
<Copy size={18} className="mr-1" />
Copy Port
</button>

<button
className="btn btn-ghost btn-sm btn-circle absolute top-2 right-2"
onClick={() => setShowRandomModal(false)}
title="Close"
aria-label="Close random port dialog"
>
</button>
</div>
</div>
</dialog>
</>
);
};

export default RandomModal;
32 changes: 31 additions & 1 deletion app/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Server } from "@/app/types";

export const compareIp = (a: string, b: string) => {
const pa = a.split('.').map(n => parseInt(n, 10));
const pb = b.split('.').map(n => parseInt(n, 10));
Expand All @@ -6,4 +8,32 @@ export const compareIp = (a: string, b: string) => {
if (diff !== 0) return diff;
}
return 0;
};
};

const generateRandomPort = (): number => {
return Math.floor(Math.random() * (65535 - 1024) + 1024);
};

export const findAvailablePort = (usedPorts: Set<number>, maxRetries: number): number | null => {
let port;
let attempts = 0;

do {
port = generateRandomPort();
attempts++;
} while (usedPorts.has(port) && attempts < maxRetries);

return attempts < maxRetries ? port : null;
};

export const generateRandomPortWithUsedPortsContext = (usedPorts: Set<number>, maxRetries: number): number | null => {
return findAvailablePort(usedPorts, maxRetries);
};

export const generateRandomPortWithServerContext = (serverContext: Server | undefined, maxRetries: number): number | null => {
if (!serverContext) {
return null;
}
const serverPorts = new Set(serverContext.ports.map(e => e.port)); // Use a Set for faster lookup
return findAvailablePort(serverPorts, maxRetries);
};