Skip to content

Commit 6da8f97

Browse files
authored
Feature/editable routing tables (#103)
* Routing Table Editing: - Implemented functionality to allow manual edits on routing table entries. - Changes persist across system updates and refreshes. - Validation added to ensure correct IP, mask and interface formatting. * Persistence Improvements: - Routing tables are now stored and retrieved correctly when devices are added or removed. - Manually edited entries are preserved if the corresponding connection still exists. * Undo/Redo Enhancements: - Fixed the RemoveDevice undo operation to restore the routing tables correctly. - Fixed the RemoveEdge undo operation to restore the routing tables correctly - Improved the logic to prevent loss of manually modified entries during undo/redo actions. TODO: Evaluate alternative UI approaches for routing table editing to improve user experience. closes #73
1 parent 4b79c66 commit 6da8f97

File tree

12 files changed

+760
-121
lines changed

12 files changed

+760
-121
lines changed

src/graphics/renderables/device_info.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Device } from "../../types/devices";
22
import { DeviceType } from "../../types/devices/device";
3-
import { RoutingTableEntry } from "../../types/graphs/datagraph";
3+
import { ViewGraph } from "../../types/graphs/viewgraph";
44
import { RemoveDeviceMove } from "../../types/undo-redo";
55
import { urManager } from "../../types/viewportManager";
66
import {
@@ -46,7 +46,11 @@ export class DeviceInfo extends StyledInfo {
4646
"Delete device",
4747
() => {
4848
const deviceData = this.device.getCreateDevice();
49-
const move = new RemoveDeviceMove(deviceData);
49+
const move = new RemoveDeviceMove(
50+
deviceData,
51+
this.device.getConnections(),
52+
this.device.viewgraph,
53+
);
5054
this.device.delete();
5155
urManager.push(move);
5256
},
@@ -77,19 +81,21 @@ export class DeviceInfo extends StyledInfo {
7781
);
7882
}
7983

80-
addRoutingTable(entries: RoutingTableEntry[]) {
84+
addRoutingTable(viewgraph: ViewGraph, deviceId: number) {
85+
const entries = viewgraph.getRoutingTable(deviceId);
86+
8187
const rows = entries.map((entry) => [
8288
entry.ip,
8389
entry.mask,
8490
`eth${entry.iface}`,
8591
]);
8692

8793
const dynamicTable = createToggleTable(
88-
"Routing Table", // Title
89-
["IP Address", "Mask", "Interface"], // Headers
90-
rows, // Generated files
91-
"right-bar-toggle-button", // Button class
92-
"right-bar-table", // Table class
94+
"Routing Table",
95+
["IP", "Mask", "Interface"],
96+
rows,
97+
viewgraph,
98+
deviceId,
9399
);
94100

95101
this.inputFields.push(dynamicTable);

src/graphics/right_bar.ts

Lines changed: 219 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import {
2+
DeviceId,
3+
isRouter,
4+
RoutingTableEntry,
5+
} from "../types/graphs/datagraph";
6+
import { ViewGraph } from "../types/graphs/viewgraph";
7+
18
export { StyledInfo } from "./renderables/styled_info";
29
export { DeviceInfo } from "./renderables/device_info";
310

@@ -102,56 +109,240 @@ export class RightBar {
102109
}
103110
}
104111

105-
export function createToggleTable(
112+
// Function to create a toggle button
113+
function createToggleButton(
106114
title: string,
107-
headers: string[],
108-
rows: string[][],
109-
buttonClass = "right-bar-toggle-button",
110-
tableClass = "right-bar-table",
115+
buttonClass: string,
116+
table: HTMLTableElement,
111117
) {
112-
const container = document.createElement("div");
113-
container.classList.add("toggle-table-container");
114-
115-
// Create toggle button
116118
const button = document.createElement("button");
117119
button.classList.add(buttonClass);
118120
button.textContent = title;
119121

120-
// Create table
122+
button.onclick = () => {
123+
const isHidden = table.classList.contains("hidden");
124+
table.classList.toggle("hidden", !isHidden);
125+
button.classList.toggle("open", isHidden);
126+
};
127+
128+
return button;
129+
}
130+
131+
function updateRoutingTableUI(
132+
deviceId: DeviceId,
133+
newTableData: RoutingTableEntry[],
134+
viewgraph: ViewGraph,
135+
): void {
136+
const router = viewgraph.getDataGraph().getDevice(deviceId);
137+
if (!router || !isRouter(router)) {
138+
console.warn(`Device with ID ${deviceId} is not a valid router.`);
139+
return;
140+
}
141+
router.routingTable = newTableData;
142+
143+
const tableContainer = document.querySelector(".toggle-table-container");
144+
if (!tableContainer)
145+
return console.warn("Routing table container not found.");
146+
147+
const existingTable = tableContainer.querySelector("table");
148+
if (!existingTable)
149+
return console.warn("Existing table not found inside container.");
150+
151+
clearTableRows(existingTable);
152+
newTableData.forEach((entry) =>
153+
createTableRow(entry, existingTable, deviceId, viewgraph),
154+
);
155+
console.log(`Routing table for router ID ${deviceId} updated successfully.`);
156+
}
157+
158+
function createTable(
159+
headers: string[],
160+
rows: string[][],
161+
tableClass: string,
162+
viewgraph: ViewGraph,
163+
deviceId: DeviceId,
164+
): HTMLTableElement {
121165
const table = document.createElement("table");
122166
table.classList.add(tableClass, "hidden");
123167

124-
// Add headers
125168
const headerRow = document.createElement("tr");
126169
headers.forEach((header) => {
127170
const th = document.createElement("th");
128171
th.textContent = header;
129172
headerRow.appendChild(th);
130173
});
174+
175+
const actionsHeader = document.createElement("th");
176+
actionsHeader.appendChild(createRegenerateButton(deviceId, viewgraph));
177+
headerRow.appendChild(actionsHeader);
131178
table.appendChild(headerRow);
132179

133-
// Add rows
134180
rows.forEach((row) => {
135-
const rowElement = document.createElement("tr");
136-
row.forEach((cellData) => {
137-
const cell = document.createElement("td");
138-
cell.textContent = cellData;
139-
rowElement.appendChild(cell);
181+
createTableRow(
182+
{ ip: row[0], mask: row[1], iface: parseInt(row[2].replace("eth", "")) },
183+
table,
184+
deviceId,
185+
viewgraph,
186+
);
187+
});
188+
189+
return table;
190+
}
191+
192+
function clearTableRows(table: HTMLTableElement): void {
193+
const rows = Array.from(table.querySelectorAll("tr"));
194+
rows.slice(1).forEach((row) => row.remove()); // Mantener solo el encabezado
195+
}
196+
197+
function createTableRow(
198+
entry: RoutingTableEntry,
199+
table: HTMLTableElement,
200+
deviceId: DeviceId,
201+
viewgraph: ViewGraph,
202+
): void {
203+
const rowElement = document.createElement("tr");
204+
205+
[entry.ip, entry.mask, `eth${entry.iface}`].forEach((cellData, colIndex) => {
206+
const cell = document.createElement("td");
207+
cell.textContent = cellData;
208+
cell.classList.add("editable-cell");
209+
cell.contentEditable = "true";
210+
211+
cell.addEventListener("keydown", (event) => {
212+
if (event.key === "Delete" || event.key === "Backspace") {
213+
event.stopPropagation(); // Evita que el evento borre la fila en la tabla
214+
}
215+
});
216+
217+
cell.addEventListener("blur", () => {
218+
const updatedRowIndex =
219+
Array.from(table.querySelectorAll("tr")).indexOf(rowElement) - 1; // Ajuste dinámico del índice
220+
const newValue = cell.textContent?.trim() || "";
221+
222+
let isValid = false;
223+
if (colIndex === 0) isValid = isValidIP(newValue);
224+
else if (colIndex === 1) isValid = isValidIP(newValue);
225+
else if (colIndex === 2) isValid = isValidInterface(newValue);
226+
227+
if (!isValid) {
228+
console.warn(`Invalid input for column ${colIndex}: ${newValue}`);
229+
cell.textContent = cellData; // Revertir cambio si es inválido
230+
return;
231+
}
232+
233+
viewgraph
234+
.getDataGraph()
235+
.saveManualChange(deviceId, updatedRowIndex, colIndex, newValue);
236+
console.log(
237+
`Updated cell at row ${updatedRowIndex}, column ${colIndex} with value: ${newValue}`,
238+
);
140239
});
141-
table.appendChild(rowElement);
240+
241+
rowElement.appendChild(cell);
142242
});
143243

144-
// Toggle when clicking on button
145-
button.onclick = () => {
146-
const isHidden = table.classList.contains("hidden");
147-
table.classList.toggle("hidden", !isHidden);
148-
table.classList.toggle("open", isHidden);
149-
container.classList.toggle("hidden", !isHidden);
150-
container.classList.toggle("open", isHidden);
151-
button.classList.toggle("open", isHidden);
152-
};
244+
rowElement.appendChild(
245+
createDeleteButton(rowElement, table, deviceId, viewgraph),
246+
);
247+
table.appendChild(rowElement);
248+
}
249+
250+
function createRegenerateButton(
251+
deviceId: DeviceId,
252+
viewgraph: ViewGraph,
253+
): HTMLButtonElement {
254+
const regenerateAllButton = document.createElement("button");
255+
regenerateAllButton.innerHTML = "🔄";
256+
regenerateAllButton.style.border = "none";
257+
regenerateAllButton.style.background = "transparent";
258+
regenerateAllButton.style.cursor = "pointer";
259+
regenerateAllButton.style.fontSize = "1.2em";
260+
regenerateAllButton.title = "Regenerate Full Routing Table";
261+
262+
regenerateAllButton.addEventListener("click", () => {
263+
console.log(`Regenerating full routing table for device ${deviceId}`);
264+
const newTableData = viewgraph
265+
.getDataGraph()
266+
.regenerateRoutingTableClean(deviceId);
267+
if (!newTableData.length) {
268+
console.warn("Failed to regenerate routing table.");
269+
return;
270+
}
271+
updateRoutingTableUI(deviceId, newTableData, viewgraph);
272+
});
273+
274+
return regenerateAllButton;
275+
}
276+
277+
function createDeleteButton(
278+
rowElement: HTMLTableRowElement,
279+
table: HTMLTableElement,
280+
deviceId: DeviceId,
281+
viewgraph: ViewGraph,
282+
): HTMLTableCellElement {
283+
const deleteCell = document.createElement("td");
284+
const deleteButton = document.createElement("button");
285+
deleteButton.innerHTML = "🗑️";
286+
deleteButton.style.border = "none";
287+
deleteButton.style.background = "transparent";
288+
deleteButton.style.cursor = "pointer";
289+
deleteButton.title = "Delete row";
290+
291+
deleteButton.addEventListener("click", () =>
292+
handleDeleteRow(rowElement, table, deviceId, viewgraph),
293+
);
294+
295+
deleteCell.appendChild(deleteButton);
296+
return deleteCell;
297+
}
298+
299+
function handleDeleteRow(
300+
rowElement: HTMLTableRowElement,
301+
table: HTMLTableElement,
302+
deviceId: DeviceId,
303+
viewgraph: ViewGraph,
304+
): void {
305+
const updatedRowIndex =
306+
Array.from(table.querySelectorAll("tr")).indexOf(rowElement) - 1; // Ajuste dinámico del índice
307+
308+
if (updatedRowIndex < 0) {
309+
console.warn("Cannot delete header row");
310+
return;
311+
}
312+
313+
table.removeChild(rowElement);
314+
viewgraph.getDataGraph().removeRoutingTableRow(deviceId, updatedRowIndex);
315+
console.log(`Deleted row ${updatedRowIndex} from device ${deviceId}`);
316+
}
317+
318+
// Function to validate IP format
319+
function isValidIP(ip: string): boolean {
320+
const ipPattern =
321+
/^(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)$/;
322+
return ipPattern.test(ip);
323+
}
324+
325+
// Function to validate Interface format (ethX where X is a number)
326+
function isValidInterface(interfaceStr: string): boolean {
327+
const interfacePattern = /^eth[0-9]+$/;
328+
return interfacePattern.test(interfaceStr);
329+
}
330+
331+
export function createToggleTable(
332+
title: string,
333+
headers: string[],
334+
rows: string[][],
335+
viewgraph: ViewGraph,
336+
deviceId: number,
337+
buttonClass = "right-bar-toggle-button",
338+
tableClass = "right-bar-table",
339+
) {
340+
const container = document.createElement("div");
341+
container.classList.add("toggle-table-container");
342+
343+
const table = createTable(headers, rows, tableClass, viewgraph, deviceId);
344+
const button = createToggleButton(title, buttonClass, table);
153345

154-
// Add button and table to container
155346
container.appendChild(button);
156347
container.appendChild(table);
157348

src/styles/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ import "./right-bar-buttons.css";
88
import "./table.css";
99
import "./info.css";
1010
import "./buttons.css";
11+
import "./modal.css";

0 commit comments

Comments
 (0)