Skip to content

Commit bd25698

Browse files
authored
refactor: move Device.showInfo logic to new class (#55)
This PR moves the formatting logic of the `Device` hierarchy's `showInfo` method to a new class `DeviceInfo`, which will allow for more customization in subclasses without repeating code or adding more logic to `Device`. It also simplifies a bit the codebase: - removes `AddRouter` and similar functions from `viewportManager` - simplifies `createDevice`'s arguments - extracts code into functions in `RightBar` and `Device` - makes `selectedDeviceId` a static member of `Device`
1 parent 1044945 commit bd25698

File tree

14 files changed

+359
-391
lines changed

14 files changed

+359
-391
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { Device } from "../../types/devices";
2+
import { DeviceType } from "../../types/devices/device";
3+
import { ViewGraph } from "../../types/graphs/viewgraph";
4+
import { sendPacket } from "../../types/packet";
5+
import { createDropdown, createRightBarButton } from "../right_bar";
6+
import { StyledInfo } from "./styled_info";
7+
8+
export class DeviceInfo extends StyledInfo {
9+
readonly device: Device;
10+
inputFields: Node[] = [];
11+
12+
constructor(device: Device) {
13+
super(getTypeName(device) + " Information");
14+
this.device = device;
15+
this.addCommonInfoFields();
16+
this.addCommonButtons();
17+
this.addSendPacketButton();
18+
}
19+
20+
private addCommonInfoFields() {
21+
const { id, connections } = this.device;
22+
super.addField("ID", id.toString());
23+
super.addListField("Connected Devices", Array.from(connections.values()));
24+
}
25+
26+
private addCommonButtons() {
27+
this.inputFields.push(
28+
createRightBarButton(
29+
"Connect device",
30+
() => this.device.selectToConnect(),
31+
"right-bar-connect-button",
32+
true,
33+
),
34+
createRightBarButton(
35+
"Delete device",
36+
() => this.device.delete(),
37+
"right-bar-delete-button",
38+
),
39+
);
40+
}
41+
42+
private addSendPacketButton() {
43+
const { id, viewgraph } = this.device;
44+
45+
const adjacentDevices = viewgraph
46+
.getDeviceIds()
47+
.filter((adjId) => adjId !== id)
48+
.map((id) => ({ value: id.toString(), text: `Device ${id}` }));
49+
this.inputFields.push(
50+
// Dropdown for selecting packet type
51+
createDropdown(
52+
"Packet Type",
53+
[
54+
{ value: "IP", text: "IP" },
55+
{ value: "ICMP", text: "ICMP" },
56+
],
57+
"packet-type",
58+
),
59+
// Dropdown for selecting destination
60+
createDropdown("Destination", adjacentDevices, "destination"),
61+
// Button to send a packet
62+
createRightBarButton("Send Packet", () =>
63+
sendSelectedPacket(viewgraph, id),
64+
),
65+
);
66+
}
67+
68+
toHTML(): Node[] {
69+
return super.toHTML().concat(this.inputFields);
70+
}
71+
}
72+
73+
function getTypeName(device: Device): string {
74+
switch (device.getType()) {
75+
case DeviceType.Router:
76+
return "Router";
77+
case DeviceType.Server:
78+
return "Server";
79+
case DeviceType.Pc:
80+
return "PC";
81+
}
82+
}
83+
84+
function sendSelectedPacket(viewgraph: ViewGraph, id: number): void {
85+
// Get the selected packet type and destination ID
86+
const packetType = (
87+
document.getElementById("packet-type") as HTMLSelectElement
88+
)?.value;
89+
const destinationId = Number(
90+
(document.getElementById("destination") as HTMLSelectElement)?.value,
91+
);
92+
93+
// Call the sendPacket method with the selected values
94+
if (packetType && !isNaN(destinationId)) {
95+
sendPacket(viewgraph, packetType, id, destinationId);
96+
} else {
97+
console.warn("Please select both a packet type and a destination.");
98+
}
99+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Renderable } from "../right_bar";
2+
3+
export interface Field {
4+
label: string;
5+
value: string;
6+
}
7+
8+
export class StyledInfo implements Renderable {
9+
title: string;
10+
info: Field[] = [];
11+
12+
constructor(title: string) {
13+
this.title = title;
14+
}
15+
16+
// Clears previous calls to addX methods
17+
clear() {
18+
this.info = [];
19+
return this;
20+
}
21+
22+
// Adds a new field to show on the info list
23+
addField(label: string, value: string) {
24+
this.info.push({ label, value });
25+
}
26+
27+
// Adds a new field to show on the info list, which has a list of values
28+
addListField(label: string, values: number[]) {
29+
const value = values.length !== 0 ? "[" + values.join(", ") + "]" : "None";
30+
this.info.push({ label, value });
31+
}
32+
33+
// Returns a list of HTML nodes with the info to show
34+
toHTML() {
35+
const childNodes: Node[] = [];
36+
const header = document.createElement("h3");
37+
header.textContent = this.title;
38+
childNodes.push(header);
39+
40+
this.info.forEach((item) => {
41+
const p = document.createElement("p");
42+
p.innerHTML = `<strong>${item.label}:</strong> ${item.value}`;
43+
childNodes.push(p);
44+
});
45+
return childNodes;
46+
}
47+
}

src/graphics/right_bar.ts

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
export { StyledInfo } from "./renderables/styled_info";
2+
export { DeviceInfo } from "./renderables/device_info";
3+
4+
export interface Renderable {
5+
toHTML(): Node[];
6+
}
7+
18
export class RightBar {
2-
private static instance: RightBar | null = null; // Unique instance
9+
private static instance: RightBar | null = null; // Singleton
310
private rightBar: HTMLElement;
411

512
private constructor(rightBar: HTMLElement) {
@@ -41,41 +48,28 @@ export class RightBar {
4148
}
4249

4350
// Shows specific information of an element in info-content
44-
renderInfo(title: string, info: { label: string; value: string }[]) {
45-
this.clearContent(); // Clears before adding new content
46-
51+
renderInfo(info: Renderable) {
4752
const infoContent = document.getElementById("info-content");
4853
if (infoContent) {
49-
const header = document.createElement("h3");
50-
header.textContent = title;
51-
infoContent.appendChild(header);
52-
53-
info.forEach((item) => {
54-
const p = document.createElement("p");
55-
p.innerHTML = `<strong>${item.label}:</strong> ${item.value}`;
56-
infoContent.appendChild(p);
57-
});
54+
infoContent.replaceChildren(...info.toHTML());
5855
}
5956
}
6057

6158
// Adds a standard button to the right-bar
6259
addButton(
6360
text: string,
6461
onClick: () => void,
65-
buttonClass = "right-bar-button",
62+
buttonClass = "",
6663
toggleSelected = false,
6764
) {
65+
const button = createRightBarButton(
66+
text,
67+
onClick,
68+
buttonClass,
69+
toggleSelected,
70+
);
6871
const infoContent = document.getElementById("info-content");
6972
if (infoContent) {
70-
const button = document.createElement("button");
71-
button.classList.add(...buttonClass.split(" "));
72-
button.textContent = text;
73-
button.onclick = () => {
74-
onClick();
75-
if (toggleSelected) {
76-
button.classList.toggle("selected-button"); // Changes color on click
77-
}
78-
};
7973
infoContent.appendChild(button);
8074
}
8175
}
@@ -86,32 +80,64 @@ export class RightBar {
8680
options: { value: string; text: string }[],
8781
selectId?: string,
8882
) {
83+
const container = createDropdown(label, options, selectId);
8984
const infoContent = document.getElementById("info-content");
90-
const container = document.createElement("div");
91-
container.classList.add("dropdown-container");
92-
93-
const labelElement = document.createElement("label");
94-
labelElement.textContent = label;
95-
labelElement.classList.add("right-bar-label");
96-
97-
const select = document.createElement("select");
98-
select.classList.add("right-bar-select");
99-
if (selectId) select.id = selectId;
100-
101-
options.forEach((optionData) => {
102-
const option = document.createElement("option");
103-
option.value = optionData.value;
104-
option.textContent = optionData.text;
105-
select.appendChild(option);
106-
});
107-
108-
// Default onchange behavior: logs the selected value
109-
select.onchange = () => {
110-
console.log(`Selected ${label}:`, select.value);
111-
};
112-
113-
container.appendChild(labelElement);
114-
container.appendChild(select);
115-
infoContent.appendChild(container);
85+
if (infoContent) {
86+
infoContent.appendChild(container);
87+
}
88+
}
89+
}
90+
91+
export function createRightBarButton(
92+
text: string,
93+
onClick: () => void,
94+
buttonClass = "",
95+
toggleSelected = false,
96+
) {
97+
const button = document.createElement("button");
98+
button.classList.add("right-bar-button");
99+
if (buttonClass) {
100+
button.classList.add(...buttonClass.split(" "));
116101
}
102+
button.textContent = text;
103+
button.onclick = () => {
104+
onClick();
105+
if (toggleSelected) {
106+
button.classList.toggle("selected-button"); // Changes color on click
107+
}
108+
};
109+
return button;
110+
}
111+
112+
export function createDropdown(
113+
label: string,
114+
options: { value: string; text: string }[],
115+
selectId?: string,
116+
) {
117+
const container = document.createElement("div");
118+
container.classList.add("dropdown-container");
119+
120+
const labelElement = document.createElement("label");
121+
labelElement.textContent = label;
122+
labelElement.classList.add("right-bar-label");
123+
124+
const select = document.createElement("select");
125+
select.classList.add("right-bar-select");
126+
if (selectId) select.id = selectId;
127+
128+
options.forEach((optionData) => {
129+
const option = document.createElement("option");
130+
option.value = optionData.value;
131+
option.textContent = optionData.text;
132+
select.appendChild(option);
133+
});
134+
135+
// Default onchange behavior: logs the selected value
136+
select.onchange = () => {
137+
console.log(`Selected ${label}:`, select.value);
138+
};
139+
140+
container.appendChild(labelElement);
141+
container.appendChild(select);
142+
return container;
117143
}

src/graphics/viewport.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Graphics, EventSystem } from "pixi.js";
22
import * as pixi_viewport from "pixi-viewport";
3-
import { selectElement } from "../types/viewportManager";
3+
import { deselectElement } from "../types/viewportManager";
44

55
const WORLD_WIDTH = 10000;
66
const WORLD_HEIGHT = 10000;
@@ -36,7 +36,7 @@ export class Viewport extends pixi_viewport.Viewport {
3636
this.on("click", (event) => {
3737
// If the click target is the viewport itself, deselect any selected element
3838
if (event.target === this) {
39-
selectElement(null);
39+
deselectElement();
4040
}
4141
});
4242
}

0 commit comments

Comments
 (0)