Skip to content

Commit cd75128

Browse files
Feat/switching table UI (#240)
Switching tables have been added, allowing the modification of the port. Currently, the system operates using device IDs. In the future, once the logic supports interface-based management, this functionality will be updated accordingly. ![image](https://github.com/user-attachments/assets/25f0c884-3d1c-4b34-96b3-9a124f18159f) closes #214 --------- Co-authored-by: Tomás Grüner <[email protected]>
1 parent 0498e32 commit cd75128

File tree

9 files changed

+358
-2
lines changed

9 files changed

+358
-2
lines changed

src/graphics/renderables/device_info.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import { ProgressBar } from "../basic_components/progress_bar";
1616
import { LabeledProgressBar } from "../components/labeled_progress_bar";
1717
import { ArpTable } from "./arp_table";
1818
import { Layer } from "../../types/layer";
19-
import { DataNetworkDevice } from "../../types/data-devices";
19+
import { DataNetworkDevice, DataSwitch } from "../../types/data-devices";
20+
import { SwitchingTable } from "./switching_table";
2021

2122
export class DeviceInfo extends BaseInfo {
2223
readonly device: ViewDevice;
@@ -173,6 +174,30 @@ export class DeviceInfo extends BaseInfo {
173174
console.warn(`Device with ID ${deviceId} is not a DataNetworkDevice.`);
174175
}
175176
}
177+
178+
addSwitchingTable(viewgraph: ViewGraph, deviceId: number): void {
179+
const entries = viewgraph.getDataGraph().getSwitchingTable(deviceId);
180+
181+
const rows = entries.map((entry) => [entry.mac, entry.port.toString()]);
182+
183+
const switchingTable = new SwitchingTable({
184+
rows,
185+
viewgraph,
186+
deviceId,
187+
});
188+
189+
this.inputFields.push(switchingTable.toHTML());
190+
191+
const dataDevice = viewgraph.getDataGraph().getDevice(deviceId);
192+
193+
if (dataDevice instanceof DataSwitch) {
194+
dataDevice.setSwitchingTableChangeListener(() => {
195+
switchingTable.refreshTable();
196+
});
197+
} else {
198+
console.warn(`Device with ID ${deviceId} is not a DataNetworkDevice.`);
199+
}
200+
}
176201
}
177202

178203
function getTypeName(device: ViewDevice): string {
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { DeviceId } from "../../types/graphs/datagraph";
2+
import { ViewGraph } from "../../types/graphs/viewgraph";
3+
import { ALERT_MESSAGES } from "../../utils/constants/alert_constants";
4+
import { CSS_CLASSES } from "../../utils/constants/css_constants";
5+
import { TOOLTIP_KEYS } from "../../utils/constants/tooltips_constants";
6+
import { Button } from "../basic_components/button";
7+
import { Table } from "../basic_components/table";
8+
import { ToggleButton } from "../basic_components/toggle_button";
9+
import { showError, showSuccess } from "./alert_manager";
10+
11+
export interface SwitchingTableProps {
12+
rows: string[][]; // Rows for the table
13+
viewgraph: ViewGraph; // ViewGraph instance for callbacks
14+
deviceId: DeviceId; // Device ID for callbacks
15+
}
16+
17+
export class SwitchingTable {
18+
private container: HTMLElement;
19+
private table: Table;
20+
private toggleButton: ToggleButton;
21+
22+
constructor(private props: SwitchingTableProps) {
23+
this.container = document.createElement("div");
24+
25+
const { onEdit, onRegenerate, onDelete } =
26+
this.setSwitchingTableCallbacks();
27+
28+
// Create the regenerate button
29+
const regenerateButton = this.createRegenerateButton(onRegenerate);
30+
31+
const headers = {
32+
[TOOLTIP_KEYS.MAC_ADDRESS]: TOOLTIP_KEYS.MAC_ADDRESS,
33+
[TOOLTIP_KEYS.PORT]: TOOLTIP_KEYS.PORT,
34+
[TOOLTIP_KEYS.REGENERATE]: regenerateButton, // Add the regenerate button to the header
35+
};
36+
37+
this.table = new Table({
38+
headers: headers,
39+
fieldsPerRow: 2, // MAC and Port
40+
rows: props.rows,
41+
editableColumns: [false, true], // Make the Port column editable
42+
onEdit: onEdit,
43+
onDelete: onDelete,
44+
tableClasses: [CSS_CLASSES.TABLE, CSS_CLASSES.RIGHT_BAR_TABLE],
45+
});
46+
47+
this.toggleButton = new ToggleButton({
48+
text: TOOLTIP_KEYS.SWITCHING_TABLE,
49+
className: CSS_CLASSES.RIGHT_BAR_TOGGLE_BUTTON,
50+
onToggle: (isToggled) => {
51+
const tableElement = this.table.toHTML();
52+
tableElement.style.display = isToggled ? "block" : "none";
53+
},
54+
tooltip: TOOLTIP_KEYS.SWITCHING_TABLE,
55+
});
56+
57+
// Initially hide the table
58+
const tableElement = this.table.toHTML();
59+
tableElement.style.display = "none";
60+
61+
this.initialize();
62+
}
63+
64+
private initialize(): void {
65+
this.container.appendChild(this.toggleButton.toHTML());
66+
this.container.appendChild(this.table.toHTML());
67+
}
68+
69+
toHTML(): HTMLElement {
70+
return this.container;
71+
}
72+
73+
updateRows(newRows: string[][]): void {
74+
this.table.updateRows(newRows); // Update the table with new rows
75+
}
76+
77+
private createRegenerateButton(
78+
onRegenerateCallback: () => void,
79+
): HTMLButtonElement {
80+
const regenerateButton = new Button({
81+
text: "🔄",
82+
classList: [CSS_CLASSES.REGENERATE_BUTTON],
83+
onClick: onRegenerateCallback,
84+
});
85+
86+
return regenerateButton.toHTML();
87+
}
88+
89+
private OnRegenerate(): void {
90+
const dataGraph = this.props.viewgraph.getDataGraph();
91+
92+
// clear the current switching table
93+
dataGraph.clearSwitchingTable(this.props.deviceId);
94+
95+
this.updateRows([]);
96+
97+
showSuccess(ALERT_MESSAGES.SWITCHING_TABLE_CLEARED);
98+
}
99+
100+
private setSwitchingTableCallbacks() {
101+
const onDelete = (row: number) => {
102+
// Get the current switching table
103+
const switchingTable = this.props.viewgraph
104+
.getDataGraph()
105+
.getSwitchingTable(this.props.deviceId);
106+
107+
// Validate that the index is valid
108+
if (row < 0 || row >= switchingTable.length) {
109+
console.warn(`Invalid row index: ${row}`);
110+
return false;
111+
}
112+
113+
// Get the MAC corresponding to the row
114+
const mac = switchingTable[row].mac;
115+
116+
// Remove the entry from the switching table using the MAC
117+
this.props.viewgraph
118+
.getDataGraph()
119+
.removeSwitchingTableEntry(this.props.deviceId, mac);
120+
121+
return true;
122+
};
123+
124+
const onRegenerate = () => {
125+
console.log("Regenerating Switching Table...");
126+
this.OnRegenerate();
127+
};
128+
129+
const onEdit = (row: number, _col: number, newValue: string) => {
130+
const isValidPort = isValidPortNumber(newValue);
131+
132+
if (!isValidPort) {
133+
console.warn(`Invalid value: ${newValue}`);
134+
return false;
135+
}
136+
137+
// Get the current switching table
138+
const switchingTable = this.props.viewgraph
139+
.getDataGraph()
140+
.getSwitchingTable(this.props.deviceId);
141+
142+
// Validate that the index is valid
143+
if (row < 0 || row >= switchingTable.length) {
144+
console.warn(`Invalid row index: ${row}`);
145+
return false;
146+
}
147+
148+
// Get the MAC corresponding to the row
149+
const mac = switchingTable[row].mac;
150+
151+
// Update the Switching Table entry
152+
this.props.viewgraph
153+
.getDataGraph()
154+
.saveSwitchingTableManualChange(
155+
this.props.deviceId,
156+
mac,
157+
parseInt(newValue, 10),
158+
);
159+
160+
return true;
161+
};
162+
163+
return { onEdit, onRegenerate, onDelete };
164+
}
165+
166+
refreshTable(): void {
167+
const updatedEntries = this.props.viewgraph
168+
.getDataGraph()
169+
.getSwitchingTable(this.props.deviceId);
170+
171+
const updatedRows = updatedEntries.map((entry) => [
172+
entry.mac.toString(),
173+
entry.port.toString(),
174+
]);
175+
176+
this.updateRows(updatedRows);
177+
}
178+
}
179+
180+
function isValidPortNumber(port: string): boolean {
181+
// verify if the port is a number
182+
const portNumber = parseInt(port, 10);
183+
const isValid = !isNaN(portNumber) && portNumber > 0;
184+
185+
if (!isValid) {
186+
showError(ALERT_MESSAGES.INVALID_PORT);
187+
}
188+
189+
return isValid;
190+
}

src/types/data-devices/dSwitch.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DataGraph, DeviceId, SwitchDataNode } from "../graphs/datagraph";
66
export class DataSwitch extends DataDevice {
77
// would be interface
88
switchingTable: Map<string, DeviceId> = new Map<string, DeviceId>();
9+
private switchingTableChangeListener: (() => void) | null = null;
910

1011
constructor(graphData: SwitchDataNode, datagraph: DataGraph) {
1112
super(graphData, datagraph);
@@ -21,6 +22,13 @@ export class DataSwitch extends DataDevice {
2122
console.debug(`Adding ${mac.toString()} to the switching table`);
2223
this.switchingTable.set(mac.toString(), deviceId);
2324
}
25+
if (this.switchingTableChangeListener) {
26+
this.switchingTableChangeListener();
27+
}
28+
}
29+
30+
setSwitchingTableChangeListener(listener: () => void): void {
31+
this.switchingTableChangeListener = listener;
2432
}
2533

2634
getDataNode(): SwitchDataNode {

src/types/graphs/datagraph.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,4 +849,95 @@ export class DataGraph {
849849
// Notificar los cambios
850850
this.notifyChanges();
851851
}
852+
853+
/**
854+
* Retrieves the switching table of a switch.
855+
* @param deviceId - ID of the device (switch).
856+
* @returns An array of objects with the entries of the switching table.
857+
*/
858+
getSwitchingTable(deviceId: DeviceId): { mac: string; port: number }[] {
859+
const device = this.getDevice(deviceId);
860+
if (!device || !(device instanceof DataSwitch)) {
861+
console.warn(`Device with ID ${deviceId} is not a switch.`);
862+
return [];
863+
}
864+
865+
// Convert the Map to an array and map it to a readable format
866+
return Array.from(device.switchingTable.entries()).map(([mac, port]) => ({
867+
mac,
868+
port,
869+
}));
870+
}
871+
872+
/**
873+
* Clears the switching table of a switch.
874+
* @param deviceId - ID of the device (switch).
875+
*/
876+
clearSwitchingTable(deviceId: DeviceId): void {
877+
const device = this.getDevice(deviceId);
878+
if (!device || !(device instanceof DataSwitch)) {
879+
console.warn(`Device with ID ${deviceId} is not a switch.`);
880+
return;
881+
}
882+
883+
// Clear the switching table
884+
device.switchingTable.clear();
885+
console.log(`Switching table cleared for device ID ${deviceId}.`);
886+
887+
// Notify changes
888+
this.notifyChanges();
889+
}
890+
891+
/**
892+
* Removes a specific entry from the switching table.
893+
* @param deviceId - ID of the device (switch).
894+
* @param mac - MAC address of the entry to remove.
895+
*/
896+
removeSwitchingTableEntry(deviceId: DeviceId, mac: string): void {
897+
const device = this.getDevice(deviceId);
898+
if (!device || !(device instanceof DataSwitch)) {
899+
console.warn(`Device with ID ${deviceId} is not a switch.`);
900+
return;
901+
}
902+
903+
// Remove the entry from the Map
904+
if (device.switchingTable.has(mac)) {
905+
device.switchingTable.delete(mac);
906+
console.log(
907+
`Entry with MAC ${mac} removed from switching table of device ID ${deviceId}.`,
908+
);
909+
this.notifyChanges();
910+
} else {
911+
console.warn(
912+
`Entry with MAC ${mac} not found in switching table of device ID ${deviceId}.`,
913+
);
914+
}
915+
}
916+
917+
/**
918+
* Manually updates an entry in the switching table.
919+
* @param deviceId - ID of the device (switch).
920+
* @param mac - MAC address of the entry to update.
921+
* @param port - New port associated with the MAC address.
922+
*/
923+
saveSwitchingTableManualChange(
924+
deviceId: DeviceId,
925+
mac: string,
926+
port: number,
927+
): void {
928+
const device = this.getDevice(deviceId);
929+
if (!device || !(device instanceof DataSwitch)) {
930+
console.warn(`Device with ID ${deviceId} is not a switch.`);
931+
return;
932+
}
933+
934+
// Update or add the entry in the Map
935+
device.switchingTable.set(mac, port);
936+
console.log(
937+
`Updated/added entry in switching table for device ID ${deviceId}: MAC=${mac}, Port=${port}.`,
938+
);
939+
940+
// Notify changes
941+
this.notifyChanges();
942+
}
852943
}

src/types/packet.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
IcmpPacket,
3131
} from "../packets/icmp";
3232
import { PacketInfo } from "../graphics/renderables/packet_info";
33+
import { showWarning } from "../graphics/renderables/alert_manager";
34+
import { ALERT_MESSAGES } from "../utils/constants/alert_constants";
3335
import {
3436
hideTooltip,
3537
removeTooltip,
@@ -378,6 +380,27 @@ export function sendViewPacket(
378380
);
379381
});
380382
}
383+
384+
if (nextHopId) {
385+
const nextHopDevice = viewgraph.getDevice(nextHopId);
386+
387+
// Verify if the next hop is a valid device
388+
if (!nextHopDevice) {
389+
showWarning(ALERT_MESSAGES.INEXISTENT_PORT(nextHopId.toString()));
390+
return;
391+
}
392+
393+
// Verify if the next hop is a valid connection
394+
const isNeighbor = viewgraph
395+
.getConnections(srcId)
396+
.some((edge) => edge.otherEnd(srcId) === nextHopId);
397+
398+
if (!isNeighbor) {
399+
showWarning(ALERT_MESSAGES.NON_NEIGHBOR_PORT(nextHopId.toString()));
400+
return;
401+
}
402+
}
403+
381404
if (firstEdge === undefined && !nextHopId) {
382405
console.warn(
383406
"El dispositivo de origen no está conectado al destino, a un router o a un switch.",

0 commit comments

Comments
 (0)