Skip to content

Commit 2e4b550

Browse files
Feat/alerts (#195)
This pull request introduces a new alert system to provide user feedback across various components. The changes include the addition of an `AlertManager` class, new alert messages, and the integration of alerts into existing functionalities. Key changes: ### Introduction of Alert System: * [`src/graphics/renderables/alert_manager.ts`](diffhunk://#diff-fd1a555a03e7967c9ee25646492276930d53e717b2afa03416a951f9b41a48a4R1-R60): Added `AlertManager` class and `AlertType` enum to manage and display alerts. * [`src/utils/constants/alert_constants.ts`](diffhunk://#diff-eb40c98ea3835dbb2ac2f0e377bdaf16ab581142e74f7d24ecb4e87608ba36b1R1-R16): Defined constant alert messages for various scenarios. * [`src/styles/alert.css`](diffhunk://#diff-dda8bc7a5a268e288d796962edb5124a791c788e8a2e6cb6d84d66946d9f2253R1-R63): Added CSS styles for alert messages. ![image](https://github.com/user-attachments/assets/ea8eeedf-a82d-4502-962c-2c95af5dac54) ![image](https://github.com/user-attachments/assets/4ee72474-c3f9-49a2-95e9-eacbc0d5f531) ![image](https://github.com/user-attachments/assets/3ef49af6-058f-4482-b2d8-6869977e6c12) --------- Co-authored-by: Tomás Grüner <[email protected]>
1 parent beefa2b commit 2e4b550

File tree

12 files changed

+206
-14
lines changed

12 files changed

+206
-14
lines changed

src/graphics/basic_components/parameter_editor.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { CSS_CLASSES } from "../../utils/constants/css_constants";
2+
import { ALERT_MESSAGES } from "../../utils/constants/alert_constants";
3+
import { showError, showSuccess } from "../renderables/alert_manager";
24
import { TooltipManager } from "../renderables/tooltip_manager";
35

46
export interface EditableParameter {
@@ -51,10 +53,14 @@ export class ParameterEditor {
5153
: (input.value as string);
5254

5355
if (input.value === "") {
56+
showError(ALERT_MESSAGES.EMPTY_INPUT);
57+
input.value = previousValue.toString();
58+
} else if (input.type === "number" && isNaN(newValue as number)) {
5459
input.value = previousValue.toString();
5560
} else {
5661
previousValue = newValue;
5762
onChange(newValue);
63+
showSuccess(ALERT_MESSAGES.PARAMETER_UPDATED);
5864
}
5965
});
6066

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
let alertTimeout: NodeJS.Timeout | null = null;
2+
3+
export enum AlertType {
4+
Success = "alert-success",
5+
Warning = "alert-warning",
6+
Error = "alert-error",
7+
}
8+
9+
function showAlert(message: string, type: AlertType, duration = 3000): void {
10+
// Clear any existing alert timeout
11+
if (alertTimeout) {
12+
clearTimeout(alertTimeout);
13+
alertTimeout = null;
14+
}
15+
16+
// Create the alert container if it doesn't exist
17+
let alertContainer = document.getElementById("global-alert");
18+
if (!alertContainer) {
19+
alertContainer = document.createElement("div");
20+
alertContainer.id = "global-alert";
21+
alertContainer.classList.add("alert-container");
22+
document.body.appendChild(alertContainer);
23+
}
24+
25+
// Set the alert content and type
26+
alertContainer.textContent = message;
27+
alertContainer.className = `alert-container ${type} show`;
28+
29+
// Automatically hide the alert after the specified duration
30+
alertTimeout = setTimeout(() => {
31+
hideAlert();
32+
}, duration);
33+
}
34+
35+
function hideAlert(): void {
36+
const alertContainer = document.getElementById("global-alert");
37+
if (alertContainer) {
38+
alertContainer.classList.remove("show");
39+
alertContainer.textContent = ""; // Clear the content
40+
}
41+
}
42+
43+
export function showSuccess(message: string, duration?: number): void {
44+
showAlert(message, AlertType.Success, duration);
45+
}
46+
47+
export function showError(message: string, duration?: number): void {
48+
showAlert(message, AlertType.Error, duration);
49+
}
50+
51+
export function showWarning(message: string, duration?: number): void {
52+
showAlert(message, AlertType.Warning, duration);
53+
}

src/graphics/renderables/program_runner_info.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { ProgramRunner, RunningProgram } from "../../programs";
22
import { CSS_CLASSES } from "../../utils/constants/css_constants";
3+
import { ALERT_MESSAGES } from "../../utils/constants/alert_constants";
34
import { TOOLTIP_KEYS } from "../../utils/constants/tooltips_constants";
45
import { Button } from "../basic_components/button";
56
import { Dropdown } from "../basic_components/dropdown";
67
import { Table } from "../basic_components/table";
8+
import { showError, showSuccess } from "./alert_manager";
79
import { Renderable } from "./base_info";
810
import { ProgramInfo } from "./device_info";
911
import { TooltipManager } from "./tooltip_manager";
@@ -31,13 +33,12 @@ export class ProgramRunnerInfo implements Renderable {
3133
this.inputFields.push(labelElement);
3234
}
3335
private addPrograms(programs: ProgramInfo[]) {
34-
let selectedProgram = programs[0];
36+
let selectedProgram: ProgramInfo = null;
3537

3638
const programOptions = programs.map(({ name }, i) => {
3739
return { value: i.toString(), text: name };
3840
});
3941
const programInputs = document.createElement("div");
40-
programInputs.replaceChildren(...selectedProgram.toHTML());
4142

4243
// Create the dropdown using the Dropdown class
4344
const selectProgramDropdown = new Dropdown({
@@ -54,13 +55,18 @@ export class ProgramRunnerInfo implements Renderable {
5455
const startProgramButton = new Button({
5556
text: TOOLTIP_KEYS.START_PROGRAM,
5657
onClick: () => {
58+
if (!selectedProgram) {
59+
showError(ALERT_MESSAGES.NO_PROGRAM_SELECTED);
60+
return;
61+
}
5762
const { name } = selectedProgram;
5863
const inputs = selectedProgram.getInputValues();
5964
if (inputs.some((input) => input === null || input === undefined)) {
60-
console.error("Some inputs are missing or invalid.");
65+
showError(ALERT_MESSAGES.START_PROGRAM_INVALID_INPUT);
6166
return;
6267
}
6368
this.runner.addRunningProgram(name, inputs);
69+
showSuccess(ALERT_MESSAGES.PROGRAM_STARTED);
6470
this.refreshTable();
6571
},
6672
classList: [

src/graphics/renderables/routing_table.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { DeviceId } from "../../types/graphs/datagraph";
66
import { TOOLTIP_KEYS } from "../../utils/constants/tooltips_constants";
77
import { CSS_CLASSES } from "../../utils/constants/css_constants";
88
import { ROUTER_CONSTANTS } from "../../utils/constants/router_constants";
9+
import { ALERT_MESSAGES } from "../../utils/constants/alert_constants";
10+
import { showError, showSuccess } from "./alert_manager";
911

1012
export interface RoutingTableProps {
1113
rows: string[][]; // Rows for the table
@@ -107,6 +109,8 @@ export class RoutingTable {
107109
]);
108110

109111
this.updateRows(newRows);
112+
113+
showSuccess(ALERT_MESSAGES.ROUTING_TABLE_REGENERATED);
110114
}
111115

112116
private setRoutingTableCallbacks(viewgraph: ViewGraph, deviceId: DeviceId) {
@@ -119,9 +123,9 @@ export class RoutingTable {
119123
isValid = isValidIP(newValue);
120124
else if (col === ROUTER_CONSTANTS.INTERFACE_COL_INDEX)
121125
isValid = isValidInterface(newValue);
122-
123126
if (isValid) {
124127
viewgraph.getDataGraph().saveManualChange(deviceId, row, col, newValue);
128+
showSuccess(ALERT_MESSAGES.ROUTING_TABLE_UPDATED);
125129
}
126130
return isValid;
127131
};
@@ -144,11 +148,19 @@ export class RoutingTable {
144148
function isValidIP(ip: string): boolean {
145149
const ipPattern =
146150
/^(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]?)$/;
147-
return ipPattern.test(ip);
151+
const result = ipPattern.test(ip);
152+
if (!result) {
153+
showError(ALERT_MESSAGES.INVALID_IP_MASK);
154+
}
155+
return result;
148156
}
149157

150158
// Function to validate Interface format (ethX where X is a number)
151159
function isValidInterface(interfaceStr: string): boolean {
152160
const interfacePattern = /^eth[0-9]+$/;
153-
return interfacePattern.test(interfaceStr);
161+
const result = interfacePattern.test(interfaceStr);
162+
if (!result) {
163+
showError(ALERT_MESSAGES.INVALID_IFACE);
164+
}
165+
return result;
154166
}

src/handlers/layerSelectorHandler.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
} from "../graphics/basic_components/dropdown";
99
import { TOOLTIP_KEYS } from "../utils/constants/tooltips_constants";
1010
import { CSS_CLASSES } from "../utils/constants/css_constants";
11+
import { showSuccess } from "../graphics/renderables/alert_manager";
12+
import { ALERT_MESSAGES } from "../utils/constants/alert_constants";
1113

1214
export class LayerHandler {
1315
private ctx: GlobalContext;
@@ -45,8 +47,9 @@ export class LayerHandler {
4547
});
4648

4749
// Initialize the dropdown with the current layer
48-
this.selectNewLayer(layerToName(this.ctx.getCurrentLayer()));
49-
this.layerDropdown.setValue(layerToName(this.ctx.getCurrentLayer()));
50+
const selectedLayer = layerToName(this.ctx.getCurrentLayer());
51+
this.applyLayerChange(selectedLayer, false); // No success message on initialization
52+
this.layerDropdown.setValue(selectedLayer);
5053
}
5154

5255
private getLayerOptions(): DropdownOption[] {
@@ -67,10 +70,22 @@ export class LayerHandler {
6770
private selectNewLayer(selectedLayer: string | null) {
6871
if (!selectedLayer) return;
6972

70-
console.debug(`Layer selected: ${selectedLayer}`);
73+
this.applyLayerChange(selectedLayer, true); // Show success message on layer change
74+
}
75+
76+
/**
77+
* Applies the layer change logic.
78+
* @param selectedLayer - The layer to change to.
79+
* @param showAlert - Whether to show a success message.
80+
*/
81+
private applyLayerChange(selectedLayer: string, showAlert: boolean) {
7182
this.ctx.changeLayer(selectedLayer);
7283
saveToLocalStorage(this.ctx);
7384
this.leftBar.setButtonsByLayer(selectedLayer);
7485
deselectElement();
86+
87+
if (showAlert) {
88+
showSuccess(ALERT_MESSAGES.LAYER_CHANGED, 7000);
89+
}
7590
}
7691
}

src/index.ejs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<div id="settingsModal"></div>
1818
<div id="global-tooltip" class="global-tooltip"></div>
19+
<div id="global-alert" class="alert-container"></div>
1920

2021
<div id="bottom-screen" class="row container">
2122
<div id="left-bar" class="left-bar"></div>

src/styles/alert.css

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* Alert container */
2+
.alert-container {
3+
display: none; /* Hidden by default */
4+
position: fixed; /* Fixed position on the screen */
5+
top: 80%; /* Center vertically */
6+
left: 40%; /* Center horizontally */
7+
transform: translate(-50%, 0); /* Adjust position to center horizontally */
8+
background: #ff4d4d; /* Default red background for errors */
9+
color: #ffffff; /* White text */
10+
padding: 15px 20px; /* Internal spacing */
11+
border-radius: 8px; /* Rounded corners */
12+
font-size: 1rem; /* Font size */
13+
text-align: left;
14+
z-index: 1000; /* Ensure it appears above other elements */
15+
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.3); /* Shadow effect */
16+
border: 1px solid rgba(255, 255, 255, 0.2); /* Subtle border */
17+
max-width: 400px; /* Maximum width */
18+
line-height: 1.5; /* Line spacing */
19+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; /* Font family */
20+
opacity: 0; /* Initially transparent */
21+
transition:
22+
opacity 0.3s ease-in-out,
23+
transform 0.3s ease-in-out; /* Smooth transitions */
24+
}
25+
26+
/* Show the alert */
27+
.alert-container.show {
28+
display: block; /* Make it visible */
29+
opacity: 1; /* Fully opaque */
30+
transform: translate(-50%, 20px); /* Slightly lower position for animation */
31+
}
32+
33+
/* Success alert */
34+
.alert-success {
35+
background: #4caf50; /* Green background for success */
36+
}
37+
38+
/* Warning alert */
39+
.alert-warning {
40+
background: #ff9800; /* Orange background for warnings */
41+
}
42+
43+
/* Error alert */
44+
.alert-error {
45+
background: #f44336; /* Red background for errors */
46+
}
47+
48+
@media (max-width: 768px) {
49+
.alert-container {
50+
top: 50%;
51+
left: 50%;
52+
transform: translate(-50%, -50%);
53+
max-width: 22.5rem;
54+
max-height: 80vh;
55+
font-size: 0.875rem;
56+
overflow-y: scroll; /* Enable scrolling */
57+
scrollbar-width: none; /* Hide scrollbar in Firefox */
58+
}
59+
60+
.alert-container::-webkit-scrollbar {
61+
display: none; /* Hide scrollbar in WebKit browsers */
62+
}
63+
}

src/styles/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ import "./slider.css";
2121
import "./divider.css";
2222
import "./progress-bar.css";
2323
import "./label.css";
24+
import "./alert.css";

src/types/graphs/datagraph.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
DataRouter,
1212
} from "../data-devices";
1313
import { GlobalContext } from "../../context";
14+
import { ALERT_MESSAGES } from "../../utils/constants/alert_constants";
15+
import { showWarning } from "../../graphics/renderables/alert_manager";
1416

1517
export type DeviceId = VertexId;
1618

@@ -244,8 +246,8 @@ export class DataGraph {
244246
const unavailableDevices =
245247
n1Iface === null && n2Iface === null
246248
? `devices ${n1Id} and ${n2Id}`
247-
: `device ${n1Id === null ? n1Id : n2Id}`;
248-
alert(`No free interfaces available for ${unavailableDevices}.`);
249+
: `device ${n1Iface === null ? n1Id : n2Id}`;
250+
showWarning(ALERT_MESSAGES.NO_FREE_INTERFACES(unavailableDevices));
249251
return null;
250252
}
251253
const edge = {

src/types/viewportManager.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
RemoveEdgeMove,
2020
} from "./undo-redo";
2121
import { SpeedMultiplier } from "./speedMultiplier";
22+
import { showError, showSuccess } from "../graphics/renderables/alert_manager";
23+
import { ALERT_MESSAGES } from "../utils/constants/alert_constants";
2224

2325
type Selectable = ViewDevice | Edge | Packet;
2426

@@ -175,9 +177,18 @@ export function loadFromFile(ctx: GlobalContext) {
175177
reader.onload = (readerEvent) => {
176178
const jsonData = readerEvent.target.result as string;
177179
const graphData: GraphData = JSON.parse(jsonData);
178-
ctx.load(DataGraph.fromData(graphData, ctx));
179-
console.log("Graph loaded successfully.");
180+
let dataGraph: DataGraph;
181+
try {
182+
dataGraph = DataGraph.fromData(graphData, ctx);
183+
} catch (error) {
184+
console.error("Failed to load graph data:", error);
185+
showError(ALERT_MESSAGES.FAILED_TO_LOAD_GRAPH);
186+
return;
187+
}
188+
ctx.load(dataGraph);
180189
ctx.centerView();
190+
191+
showSuccess(ALERT_MESSAGES.GRAPH_LOADED_SUCCESSFULLY);
181192
};
182193
};
183194

0 commit comments

Comments
 (0)