diff options
Diffstat (limited to 'src/scripts/controllers/modes')
| -rw-r--r-- | src/scripts/controllers/modes/building.ts | 114 | ||||
| -rw-r--r-- | src/scripts/controllers/modes/node.ts | 297 | ||||
| -rw-r--r-- | src/scripts/controllers/modes/object.ts | 297 | ||||
| -rw-r--r-- | src/scripts/controllers/modes/room.ts | 382 |
4 files changed, 1090 insertions, 0 deletions
diff --git a/src/scripts/controllers/modes/building.ts b/src/scripts/controllers/modes/building.ts new file mode 100644 index 00000000..4d82f090 --- /dev/null +++ b/src/scripts/controllers/modes/building.ts @@ -0,0 +1,114 @@ +import {InteractionMode, MapController} from "../mapcontroller"; +import {MapView} from "../../views/mapview"; +import * as $ from "jquery"; + + +/** + * Class responsible for handling building mode interactions. + */ +export class BuildingModeController { + public newRoomId: number; + + private mapController: MapController; + private mapView: MapView; + + + constructor(mapController: MapController) { + this.mapController = mapController; + this.mapView = this.mapController.mapView; + } + + /** + * Connects all DOM event listeners to their respective element targets. + */ + public setupEventListeners() { + let resetConstructionButtons = () => { + this.mapController.interactionMode = InteractionMode.DEFAULT; + this.mapView.hoverLayer.setHoverTileVisibility(false); + $("#room-construction").text("Construct new room"); + $("#room-construction-cancel").slideToggle(300); + }; + + // Room construction button + $("#room-construction").on("click", (event: JQueryEventObject) => { + if (this.mapController.interactionMode === InteractionMode.DEFAULT) { + this.mapController.interactionMode = InteractionMode.SELECT_ROOM; + this.mapView.hoverLayer.setHoverTileVisibility(true); + this.mapController.api.addRoomToDatacenter(this.mapView.simulation.id, + this.mapView.currentDatacenter.id).then((room: IRoom) => { + this.newRoomId = room.id; + }); + $(event.target).text("Finalize room"); + $("#room-construction-cancel").slideToggle(300); + } else if (this.mapController.interactionMode === InteractionMode.SELECT_ROOM) { + resetConstructionButtons(); + this.finalizeRoom(); + } + }); + + // Cancel button for room construction + $("#room-construction-cancel").on("click", () => { + resetConstructionButtons(); + this.cancelRoomConstruction(); + }); + } + + /** + * Cancels room construction and deletes the temporary room created previously. + */ + public cancelRoomConstruction() { + this.mapController.api.deleteRoom(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.newRoomId).then(() => { + this.mapView.roomLayer.cancelRoomConstruction(); + }); + } + + /** + * Finalizes room construction by triggering a redraw of the room layer with the new room added. + */ + public finalizeRoom() { + this.mapController.api.getRoom(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.newRoomId).then((room: IRoom) => { + this.mapView.roomLayer.finalizeRoom(room); + }); + } + + /** + * Adds a newly selected tile to the list of selected tiles. + * + * @param position The new tile position to be added + */ + public addSelectedTile(position: IGridPosition): void { + let tile = { + id: -1, + roomId: this.newRoomId, + position: {x: position.x, y: position.y} + }; + this.mapController.api.addTileToRoom(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.newRoomId, tile).then((tile: ITile) => { + this.mapView.roomLayer.addSelectedTile(tile); + }); + } + + /** + * Removes a previously selected tile. + * + * @param position The position of the tile to be removed + */ + public removeSelectedTile(position: IGridPosition): void { + let tile; + let objectIndex = -1; + + for (let i = 0; i < this.mapView.roomLayer.selectedTileObjects.length; i++) { + tile = this.mapView.roomLayer.selectedTileObjects[i]; + if (tile.position.x === position.x && tile.position.y === position.y) { + objectIndex = i; + } + } + this.mapController.api.deleteTile(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.newRoomId, + this.mapView.roomLayer.selectedTileObjects[objectIndex].tileObject.id).then(() => { + this.mapView.roomLayer.removeSelectedTile(position, objectIndex); + }); + } +}
\ No newline at end of file diff --git a/src/scripts/controllers/modes/node.ts b/src/scripts/controllers/modes/node.ts new file mode 100644 index 00000000..3b3f8a32 --- /dev/null +++ b/src/scripts/controllers/modes/node.ts @@ -0,0 +1,297 @@ +import {MapController, AppMode, InteractionLevel} from "../mapcontroller"; +import {MapView} from "../../views/mapview"; +import * as $ from "jquery"; + + +/** + * Class responsible for rendering node mode and handling UI interactions within it. + */ +export class NodeModeController { + public currentMachine: IMachine; + + private mapController: MapController; + private mapView: MapView; + + + constructor(mapController: MapController) { + this.mapController = mapController; + this.mapView = this.mapController.mapView; + + this.loadAddDropdowns(); + } + + /** + * Moves the UI model into node mode. + * + * @param machine The machine that was selected in rack mode + */ + public enterMode(machine: IMachine): void { + this.currentMachine = machine; + this.populateUnitLists(); + $("#node-menu").removeClass("hidden"); + + if (this.mapController.appMode === AppMode.SIMULATION) { + this.mapController.simulationController.transitionFromRackToNode(); + } + } + + /** + * Performs cleanup and closing actions before allowing transferal to rack mode. + */ + public goToObjectMode(): void { + $("#node-menu").addClass("hidden"); + $(".node-element-overlay").addClass("hidden"); + this.currentMachine = undefined; + this.mapController.interactionLevel = InteractionLevel.OBJECT; + + if (this.mapController.appMode === AppMode.SIMULATION) { + this.mapController.simulationController.transitionFromNodeToRack(); + } + } + + /** + * Connects all DOM event listeners to their respective element targets. + */ + public setupEventListeners(): void { + let nodeMenu = $("#node-menu"); + + nodeMenu.find(".panel-group").on("click", ".remove-unit", (event: JQueryEventObject) => { + MapController.showConfirmDeleteDialog("unit", () => { + let index = $(event.target).closest(".panel").index(); + + if (index === -1) { + return; + } + + let closestTabPane = $(event.target).closest(".panel-group"); + + let objectList, idList; + if (closestTabPane.is("#cpu-accordion")) { + objectList = this.currentMachine.cpus; + idList = this.currentMachine.cpuIds; + } else if (closestTabPane.is("#gpu-accordion")) { + objectList = this.currentMachine.gpus; + idList = this.currentMachine.gpuIds; + } else if (closestTabPane.is("#memory-accordion")) { + objectList = this.currentMachine.memories; + idList = this.currentMachine.memoryIds; + } else if (closestTabPane.is("#storage-accordion")) { + objectList = this.currentMachine.storages; + idList = this.currentMachine.storageIds; + } + + idList.splice(idList.indexOf(objectList[index]).id, 1); + objectList.splice(index, 1); + + this.mapController.api.updateMachine(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id, + this.mapController.objectModeController.currentObjectTile.id, this.currentMachine).then( + () => { + this.populateUnitLists(); + this.mapController.objectModeController.updateNodeComponentOverlays(); + }); + }); + }); + + nodeMenu.find(".add-unit").on("click", (event: JQueryEventObject) => { + let dropdown = $(event.target).closest(".input-group-btn").siblings("select").first(); + + let closestTabPane = $(event.target).closest(".input-group").siblings(".panel-group").first(); + let objectList, idList, typePlural; + if (closestTabPane.is("#cpu-accordion")) { + objectList = this.currentMachine.cpus; + idList = this.currentMachine.cpuIds; + typePlural = "cpus"; + } else if (closestTabPane.is("#gpu-accordion")) { + objectList = this.currentMachine.gpus; + idList = this.currentMachine.gpuIds; + typePlural = "gpus"; + } else if (closestTabPane.is("#memory-accordion")) { + objectList = this.currentMachine.memories; + idList = this.currentMachine.memoryIds; + typePlural = "memories"; + } else if (closestTabPane.is("#storage-accordion")) { + objectList = this.currentMachine.storages; + idList = this.currentMachine.storageIds; + typePlural = "storages"; + } + + if (idList.length + 1 > 4) { + this.mapController.showInfoBalloon("Machine has only 4 slots", "warning"); + return; + } + + let id = parseInt(dropdown.val(), 10); + idList.push(id); + this.mapController.api.getSpecificationOfType(typePlural, id).then((spec: INodeUnit) => { + objectList.push(spec); + + this.mapController.api.updateMachine(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id, + this.mapController.objectModeController.currentObjectTile.id, this.currentMachine).then( + () => { + this.populateUnitLists(); + this.mapController.objectModeController.updateNodeComponentOverlays(); + }); + }); + }); + } + + /** + * Populates the "add" dropdowns with all available unit options. + */ + private loadAddDropdowns(): void { + let unitTypes = [ + "cpus", "gpus", "memories", "storages" + ]; + let dropdowns = [ + $("#add-cpu-form").find("select"), + $("#add-gpu-form").find("select"), + $("#add-memory-form").find("select"), + $("#add-storage-form").find("select"), + ]; + + unitTypes.forEach((type: string, index: number) => { + this.mapController.api.getAllSpecificationsOfType(type).then((data: any) => { + data.forEach((option: INodeUnit) => { + dropdowns[index].append($("<option>").val(option.id).text(option.manufacturer + " " + option.family + + " " + option.model + " (" + option.generation + ")")); + }); + }); + }); + } + + /** + * Generates and inserts dynamically HTML code concerning all units of a machine. + */ + private populateUnitLists(): void { + // Contains the skeleton of a unit element and inserts the given data into it + let generatePanel = (type: string, index: number, list: any, specSection: string): string => { + return '<div class="panel panel-default">' + + ' <div class="panel-heading">' + + ' <h4 class="panel-title">' + + ' <a class="glyphicon glyphicon-remove remove-unit" href="javascript:void(0)"></a>' + + ' <a class="accordion-toggle collapsed" data-toggle="collapse" data-parent="#' + type + '-accordion"' + + ' href="#' + type + '-' + index + '">' + + list[index].manufacturer + ' ' + list[index].family + ' ' + list[index].model + + ' </a>' + + ' </h4>' + + ' </div>' + + ' <div id="' + type + '-' + index + '" class="panel-collapse collapse">' + + ' <table class="spec-table">' + + ' <tbody>' + + specSection + + ' </tbody>' + + ' </table>' + + ' </div>' + + '</div>'; + }; + + // Generates the structure of the specification list of a processing unit + let generateProcessingUnitHtml = (element: IProcessingUnit) => { + return ' <tr>' + + ' <td class="glyphicon glyphicon-tasks"></td>' + + ' <td>Number of Cores</td>' + + ' <td>' + element.numberOfCores + '</td>' + + ' </tr>' + + ' <tr>' + + ' <td class="glyphicon glyphicon-dashboard"></td>' + + ' <td>Clockspeed (MHz)</td>' + + ' <td>' + element.clockRateMhz + '</td>' + + ' </tr>' + + ' <tr>' + + ' <td class="glyphicon glyphicon-flash"></td>' + + ' <td>Energy Consumption (W)</td>' + + ' <td>' + element.energyConsumptionW + '</td>' + + ' </tr>' + + ' <tr>' + + ' <td class="glyphicon glyphicon-alert"></td>' + + ' <td>Failure Rate (%)</td>' + + ' <td>' + element.failureModel.rate + '</td>' + + ' </tr>'; + }; + + // Generates the structure of the spec list of a storage unit + let generateStorageUnitHtml = (element: IStorageUnit) => { + return ' <tr>' + + ' <td class="glyphicon glyphicon-floppy-disk"></td>' + + ' <td>Size (Mb)</td>' + + ' <td>' + element.sizeMb + '</td>' + + ' </tr>' + + ' <tr>' + + ' <td class="glyphicon glyphicon-dashboard"></td>' + + ' <td>Speed (Mb/s)</td>' + + ' <td>' + element.speedMbPerS + '</td>' + + ' </tr>' + + ' <tr>' + + ' <td class="glyphicon glyphicon-flash"></td>' + + ' <td>Energy Consumption (W)</td>' + + ' <td>' + element.energyConsumptionW + '</td>' + + ' </tr>' + + ' <tr>' + + ' <td class="glyphicon glyphicon-alert"></td>' + + ' <td>Failure Rate (%)</td>' + + ' <td>' + element.failureModel.rate + '</td>' + + ' </tr>'; + }; + + // Inserts a "No units" message into the container of the given unit type + let addNoUnitsMessage = (type: string) => { + $("#" + type + "-accordion").append("<p>There are currently no units present here. " + + "<em>Add some with the dropdown below!</em></p>"); + }; + + let container = $("#cpu-accordion"); + container.children().remove(".panel"); + container.children().remove("p"); + + if (this.currentMachine.cpus.length === 0) { + addNoUnitsMessage("cpu"); + } else { + this.currentMachine.cpus.forEach((element: ICPU, i: number) => { + let specSection = generateProcessingUnitHtml(element); + let content = generatePanel("cpu", i, this.currentMachine.cpus, specSection); + container.append(content); + }); + } + + container = $("#gpu-accordion"); + container.children().remove(".panel"); + container.children().remove("p"); + if (this.currentMachine.gpus.length === 0) { + addNoUnitsMessage("gpu"); + } else { + this.currentMachine.gpus.forEach((element: IGPU, i: number) => { + let specSection = generateProcessingUnitHtml(element); + let content = generatePanel("gpu", i, this.currentMachine.gpus, specSection); + container.append(content); + }); + } + + container = $("#memory-accordion"); + container.children().remove(".panel"); + container.children().remove("p"); + if (this.currentMachine.memories.length === 0) { + addNoUnitsMessage("memory"); + } else { + this.currentMachine.memories.forEach((element: IMemory, i: number) => { + let specSection = generateStorageUnitHtml(element); + let content = generatePanel("memory", i, this.currentMachine.memories, specSection); + container.append(content); + }); + } + + container = $("#storage-accordion"); + container.children().remove(".panel"); + container.children().remove("p"); + if (this.currentMachine.storages.length === 0) { + addNoUnitsMessage("storage"); + } else { + this.currentMachine.storages.forEach((element: IMemory, i: number) => { + let specSection = generateStorageUnitHtml(element); + let content = generatePanel("storage", i, this.currentMachine.storages, specSection); + container.append(content); + }); + } + } +}
\ No newline at end of file diff --git a/src/scripts/controllers/modes/object.ts b/src/scripts/controllers/modes/object.ts new file mode 100644 index 00000000..e922433e --- /dev/null +++ b/src/scripts/controllers/modes/object.ts @@ -0,0 +1,297 @@ +import {AppMode, MapController, InteractionLevel} from "../mapcontroller"; +import {MapView} from "../../views/mapview"; +import * as $ from "jquery"; + + +/** + * Class responsible for rendering object mode and handling its UI interactions. + */ +export class ObjectModeController { + public currentObject: IDCObject; + public objectType: string; + public currentRack: IRack; + public currentPSU: IPSU; + public currentCoolingItem: ICoolingItem; + public currentObjectTile: ITile; + + private mapController: MapController; + private mapView: MapView; + + + constructor(mapController: MapController) { + this.mapController = mapController; + this.mapView = this.mapController.mapView; + } + + /** + * Performs the necessary setup actions and enters object mode. + * + * @param tile A reference to the tile containing the rack that was selected. + */ + public enterMode(tile: ITile) { + this.currentObjectTile = tile; + this.mapView.grayLayer.currentObjectTile = tile; + this.currentObject = tile.object; + this.objectType = tile.objectType; + + // Show the corresponding sub-menu of object mode + $(".object-sub-menu").hide(); + + switch (this.objectType) { + case "RACK": + $("#rack-sub-menu").show(); + this.currentRack = <IRack>this.currentObject; + $("#rack-name-input").val(this.currentRack.name); + this.populateNodeList(); + + break; + + case "PSU": + $("#psu-sub-menu").show(); + this.currentPSU = <IPSU>this.currentObject; + + break; + + case "COOLING_ITEM": + $("#cooling-item-sub-menu").show(); + this.currentCoolingItem = <ICoolingItem>this.currentObject; + + break; + } + + this.mapView.grayLayer.drawRackLevel(); + MapController.hideAndShowMenus("#object-menu"); + this.scrollToBottom(); + + if (this.mapController.appMode === AppMode.SIMULATION) { + this.mapController.simulationController.transitionFromRoomToRack(); + } + } + + /** + * Leaves object mode and transfers to room mode. + */ + public goToRoomMode() { + this.mapController.interactionLevel = InteractionLevel.ROOM; + this.mapView.grayLayer.hideRackLevel(); + MapController.hideAndShowMenus("#room-menu"); + this.mapController.roomModeController.enterMode(this.mapController.roomModeController.currentRoom); + + if (this.mapController.appMode === AppMode.SIMULATION) { + this.mapController.simulationController.transitionFromRackToRoom(); + } + } + + /** + * Connects all DOM event listeners to their respective element targets. + */ + public setupEventListeners() { + // Handler for saving a new rack name + $("#rack-name-save").on("click", () => { + this.currentRack.name = $("#rack-name-input").val(); + this.mapController.api.updateRack(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id, + this.mapController.objectModeController.currentObjectTile.id, this.currentRack).then( + () => { + this.mapController.showInfoBalloon("Rack name saved", "info"); + }); + }); + + let nodeListContainer = $(".node-list-container"); + + // Handler for the 'add' button of each machine slot of the rack + nodeListContainer.on("click", ".add-node", (event: JQueryEventObject) => { + // Convert the DOM element index to a JS array index + let index = this.currentRack.machines.length - $(event.target).closest(".node-element").index() - 1; + + // Insert an empty machine at the selected position + this.mapController.api.addMachineToRack(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id, + this.mapController.objectModeController.currentObjectTile.id, { + id: -1, + rackId: this.currentRack.id, + position: index, + tags: [], + cpuIds: [], + gpuIds: [], + memoryIds: [], + storageIds: [] + }).then((data: IMachine) => { + this.currentRack.machines[index] = data; + this.populateNodeList(); + this.mapView.dcObjectLayer.draw(); + }); + + event.stopPropagation(); + }); + + // Handler for the 'remove' button of each machine slot of the rack + nodeListContainer.on("click", ".remove-node", (event: JQueryEventObject) => { + let target = $(event.target); + MapController.showConfirmDeleteDialog("machine", () => { + // Convert the DOM element index to a JS array index + let index = this.currentRack.machines.length - target.closest(".node-element").index() - 1; + + this.mapController.api.deleteMachine(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id, + this.mapController.objectModeController.currentObjectTile.id, + index).then(() => { + this.currentRack.machines[index] = null; + this.populateNodeList(); + this.mapView.dcObjectLayer.draw(); + }); + }); + event.stopPropagation(); + }); + + // Handler for every node element, triggering node mode + nodeListContainer.on("click", ".node-element", (event: JQueryEventObject) => { + let domIndex = $(event.target).closest(".node-element").index(); + let index = this.currentRack.machines.length - domIndex - 1; + let machine = this.currentRack.machines[index]; + + if (machine != null) { + this.mapController.interactionLevel = InteractionLevel.NODE; + + // Gray out the other nodes + $(event.target).closest(".node-list-container").children(".node-element").each((nodeIndex: number, element: Element) => { + if (nodeIndex !== domIndex) { + $(element).children(".node-element-overlay").removeClass("hidden"); + } else { + $(element).children(".node-element-overlay").addClass("hidden"); + } + }); + + this.mapController.nodeModeController.enterMode(machine); + } + }); + + // Handler for rack deletion button + $("#rack-deletion").on("click", () => { + MapController.showConfirmDeleteDialog("rack", () => { + this.mapController.api.deleteRack(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id, + this.mapController.objectModeController.currentObjectTile.id).then(() => { + this.currentObjectTile.object = undefined; + this.currentObjectTile.objectType = undefined; + this.currentObjectTile.objectId = undefined; + this.mapView.redrawMap(); + this.goToRoomMode(); + }); + }); + }); + + // Handler for PSU deletion button + $("#psu-deletion").on("click", () => { + MapController.showConfirmDeleteDialog("PSU", () => { + this.mapController.api.deletePSU(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id, + this.mapController.objectModeController.currentObjectTile.id).then(() => { + this.mapView.redrawMap(); + this.goToRoomMode(); + }); + this.currentObjectTile.object = undefined; + this.currentObjectTile.objectType = undefined; + this.currentObjectTile.objectId = undefined; + }); + }); + + // Handler for Cooling Item deletion button + $("#cooling-item-deletion").on("click", () => { + MapController.showConfirmDeleteDialog("cooling item", () => { + this.mapController.api.deleteCoolingItem(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id, + this.mapController.objectModeController.currentObjectTile.id).then(() => { + this.mapView.redrawMap(); + this.goToRoomMode(); + }); + this.currentObjectTile.object = undefined; + this.currentObjectTile.objectType = undefined; + this.currentObjectTile.objectId = undefined; + }); + }); + } + + public updateNodeComponentOverlays(): void { + if (this.currentRack === undefined || this.currentRack.machines === undefined) { + return; + } + + for (let i = 0; i < this.currentRack.machines.length; i++) { + if (this.currentRack.machines[i] === null) { + continue; + } + + let container = this.mapController.appMode === AppMode.CONSTRUCTION ? ".construction" : ".simulation"; + let element = $(container + " .node-element").eq(this.currentRack.machines.length - i - 1); + if (this.currentRack.machines[i].cpus.length !== 0) { + element.find(".overlay-cpu").addClass("hidden"); + } else { + element.find(".overlay-cpu").removeClass("hidden"); + } + if (this.currentRack.machines[i].gpus.length !== 0) { + element.find(".overlay-gpu").addClass("hidden"); + } else { + element.find(".overlay-gpu").removeClass("hidden"); + } + if (this.currentRack.machines[i].memories.length !== 0) { + element.find(".overlay-memory").addClass("hidden"); + } else { + element.find(".overlay-memory").removeClass("hidden"); + } + if (this.currentRack.machines[i].storages.length !== 0) { + element.find(".overlay-storage").addClass("hidden"); + } else { + element.find(".overlay-storage").removeClass("hidden"); + } + } + } + + /** + * Dynamically generates and inserts HTML code for every node in the current rack. + */ + private populateNodeList(): void { + let type, content; + let container = $(".node-list-container"); + + // Remove any previously present node elements + container.children().remove(".node-element"); + + for (let i = 0; i < this.currentRack.machines.length; i++) { + // Depending on whether the current machine slot is filled, allow removing or adding a new machine by adding + // the appropriate button next to the machine slot + type = (this.currentRack.machines[i] == null ? "glyphicon-plus add-node" : "glyphicon-remove remove-node"); + content = + '<div class="node-element" data-id="' + (this.currentRack.machines[i] === null ? + "" : this.currentRack.machines[i].id) + '">' + + ' <div class="node-element-overlay hidden"></div>' + + ' <a class="node-element-btn glyphicon ' + type + '" href="javascript:void(0)"></a>' + + ' <div class="node-element-number">' + (i + 1) + '</div>'; + if (this.currentRack.machines[i] !== null) { + content += + '<div class="node-element-content">' + + ' <img src="img/app/node-cpu.png">' + + ' <img src="img/app/node-gpu.png">' + + ' <img src="img/app/node-memory.png">' + + ' <img src="img/app/node-storage.png">' + + ' <img src="img/app/node-network.png">' + + ' <div class="icon-overlay overlay-cpu hidden"></div>' + + ' <div class="icon-overlay overlay-gpu hidden"></div>' + + ' <div class="icon-overlay overlay-memory hidden"></div>' + + ' <div class="icon-overlay overlay-storage hidden"></div>' + + ' <div class="icon-overlay overlay-network"></div>' + + '</div>'; + } + content += '</div>'; + // Insert the generated machine slot into the DOM + container.prepend(content); + } + + this.updateNodeComponentOverlays(); + } + + private scrollToBottom(): void { + let scrollContainer = $('.node-list-container'); + scrollContainer.scrollTop(scrollContainer[0].scrollHeight); + } +}
\ No newline at end of file diff --git a/src/scripts/controllers/modes/room.ts b/src/scripts/controllers/modes/room.ts new file mode 100644 index 00000000..a858af5a --- /dev/null +++ b/src/scripts/controllers/modes/room.ts @@ -0,0 +1,382 @@ +import {Util} from "../../util"; +import {InteractionLevel, MapController, AppMode} from "../mapcontroller"; +import {MapView} from "../../views/mapview"; +import * as $ from "jquery"; + + +export enum RoomInteractionMode { + DEFAULT, + ADD_RACK, + ADD_PSU, + ADD_COOLING_ITEM +} + + +export class RoomModeController { + public currentRoom: IRoom; + public roomInteractionMode: RoomInteractionMode; + + private mapController: MapController; + private mapView: MapView; + private roomTypes: string[]; + private roomTypeMap: IRoomTypeMap; + private availablePSUs: IPSU[]; + private availableCoolingItems: ICoolingItem[]; + + + constructor(mapController: MapController) { + this.mapController = mapController; + this.mapView = this.mapController.mapView; + + this.mapController.api.getAllRoomTypes().then((roomTypes: string[]) => { + this.roomTypes = roomTypes; + this.roomTypeMap = {}; + + this.roomTypes.forEach((type: string) => { + this.mapController.api.getAllowedObjectsByRoomType(type).then((objects: string[]) => { + this.roomTypeMap[type] = objects; + }); + }); + + this.populateRoomTypeDropdown(); + }); + + // this.mapController.api.getAllPSUSpecs().then((specs: IPSU[]) => { + // this.availablePSUs = specs; + // }); + // + // this.mapController.api.getAllCoolingItemSpecs().then((specs: ICoolingItem[]) => { + // this.availableCoolingItems = specs; + // }); + + this.roomInteractionMode = RoomInteractionMode.DEFAULT; + } + + public enterMode(room: IRoom) { + this.currentRoom = room; + this.roomInteractionMode = RoomInteractionMode.DEFAULT; + + this.mapView.roomTextLayer.setVisibility(false); + + this.mapView.zoomInOnRoom(this.currentRoom); + $("#room-name-input").val(this.currentRoom.name); + MapController.hideAndShowMenus("#room-menu"); + + // Pre-select the type of the current room in the dropdown + let roomTypeDropdown = $("#roomtype-select"); + roomTypeDropdown.find('option').prop("selected", "false"); + let roomTypeIndex = this.roomTypes.indexOf(this.currentRoom.roomType); + if (roomTypeIndex !== -1) { + roomTypeDropdown.find('option[value="' + roomTypeIndex + '"]').prop("selected", "true"); + } else { + roomTypeDropdown.val([]); + } + + this.populateAllowedObjectTypes(); + + this.mapView.roomLayer.setClickable(false); + + if (this.mapController.appMode === AppMode.SIMULATION) { + this.mapController.simulationController.transitionFromBuildingToRoom(); + } + } + + public goToBuildingMode() { + this.mapController.interactionLevel = InteractionLevel.BUILDING; + + if (this.roomInteractionMode !== RoomInteractionMode.DEFAULT) { + this.roomInteractionMode = RoomInteractionMode.DEFAULT; + this.mapView.hoverLayer.setHoverItemVisibility(false); + $("#add-rack-btn").attr("data-active", "false"); + $("#add-psu-btn").attr("data-active", "false"); + $("#add-cooling-item-btn").attr("data-active", "false"); + } + + this.mapView.roomTextLayer.setVisibility(true); + + this.mapView.zoomOutOnDC(); + MapController.hideAndShowMenus("#building-menu"); + + this.mapView.roomLayer.setClickable(true); + + if (this.mapController.appMode === AppMode.SIMULATION) { + this.mapController.simulationController.transitionFromRoomToBuilding(); + } + } + + public setupEventListeners(): void { + // Component buttons + let addRackBtn = $("#add-rack-btn"); + let addPSUBtn = $("#add-psu-btn"); + let addCoolingItemBtn = $("#add-cooling-item-btn"); + + let roomTypeDropdown = $("#roomtype-select"); + + addRackBtn.on("click", () => { + this.handleItemClick("RACK"); + }); + addPSUBtn.on("click", () => { + this.handleItemClick("PSU"); + }); + addCoolingItemBtn.on("click", () => { + this.handleItemClick("COOLING_ITEM"); + }); + + // Handler for saving a new room name + $("#room-name-save").on("click", () => { + this.currentRoom.name = $("#room-name-input").val(); + this.mapController.api.updateRoom(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.currentRoom).then(() => { + this.mapView.roomTextLayer.draw(); + this.mapController.showInfoBalloon("Room name saved", "info"); + }); + }); + + // Handler for room deletion button + $("#room-deletion").on("click", () => { + MapController.showConfirmDeleteDialog("room", () => { + this.mapController.api.deleteRoom(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.currentRoom.id).then(() => { + let roomIndex = this.mapView.currentDatacenter.rooms.indexOf(this.currentRoom); + this.mapView.currentDatacenter.rooms.splice(roomIndex, 1); + + this.mapView.redrawMap(); + this.goToBuildingMode(); + }); + }); + }); + + // Handler for the room type dropdown component + roomTypeDropdown.on("change", () => { + let newRoomType = this.roomTypes[roomTypeDropdown.val()]; + if (!this.checkRoomTypeLegality(newRoomType)) { + roomTypeDropdown.val(this.roomTypes.indexOf(this.currentRoom.roomType)); + this.mapController.showInfoBalloon("Room type couldn't be changed, illegal objects", "warning"); + return; + } + + this.currentRoom.roomType = newRoomType; + this.mapController.api.updateRoom(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.currentRoom).then(() => { + this.populateAllowedObjectTypes(); + this.mapView.roomTextLayer.draw(); + this.mapController.showInfoBalloon("Room type changed", "info"); + }); + }); + } + + public handleCanvasMouseClick(gridPos: IGridPosition): void { + if (this.roomInteractionMode === RoomInteractionMode.DEFAULT) { + let tileIndex = Util.tileListPositionIndexOf(this.currentRoom.tiles, gridPos); + + if (tileIndex !== -1) { + let tile = this.currentRoom.tiles[tileIndex]; + + if (tile.object !== undefined) { + this.mapController.interactionLevel = InteractionLevel.OBJECT; + this.mapController.objectModeController.enterMode(tile); + } + } else { + this.goToBuildingMode(); + } + } else if (this.roomInteractionMode === RoomInteractionMode.ADD_RACK) { + this.addObject(this.mapView.hoverLayer.hoverTilePosition, "RACK"); + + } else if (this.roomInteractionMode === RoomInteractionMode.ADD_PSU) { + this.addObject(this.mapView.hoverLayer.hoverTilePosition, "PSU"); + + } else if (this.roomInteractionMode === RoomInteractionMode.ADD_COOLING_ITEM) { + this.addObject(this.mapView.hoverLayer.hoverTilePosition, "COOLING_ITEM"); + + } + } + + private handleItemClick(type: string): void { + let addRackBtn = $("#add-rack-btn"); + let addPSUBtn = $("#add-psu-btn"); + let addCoolingItemBtn = $("#add-cooling-item-btn"); + let allObjectContainers = $(".dc-component-container"); + let objectTypes = [ + { + type: "RACK", + mode: RoomInteractionMode.ADD_RACK, + btn: addRackBtn + }, + { + type: "PSU", + mode: RoomInteractionMode.ADD_PSU, + btn: addPSUBtn + }, + { + type: "COOLING_ITEM", + mode: RoomInteractionMode.ADD_COOLING_ITEM, + btn: addCoolingItemBtn + } + ]; + + allObjectContainers.attr("data-active", "false"); + + if (this.roomInteractionMode === RoomInteractionMode.DEFAULT) { + this.mapView.hoverLayer.setHoverItemVisibility(true, type); + + if (type === "RACK") { + this.roomInteractionMode = RoomInteractionMode.ADD_RACK; + addRackBtn.attr("data-active", "true"); + } else if (type === "PSU") { + this.roomInteractionMode = RoomInteractionMode.ADD_PSU; + addPSUBtn.attr("data-active", "true"); + } else if (type === "COOLING_ITEM") { + this.roomInteractionMode = RoomInteractionMode.ADD_COOLING_ITEM; + addCoolingItemBtn.attr("data-active", "true"); + } + + return; + } + + let changed = false; + objectTypes.forEach((objectType: any, index: number) => { + if (this.roomInteractionMode === objectType.mode) { + if (changed) { + return; + } + if (type === objectType.type) { + this.roomInteractionMode = RoomInteractionMode.DEFAULT; + this.mapView.hoverLayer.setHoverItemVisibility(false); + objectType.btn.attr("data-active", "false"); + } else { + objectTypes.forEach((otherObjectType, otherIndex: number) => { + if (index !== otherIndex) { + if (type === otherObjectType.type) { + this.mapView.hoverLayer.setHoverItemVisibility(true, type); + otherObjectType.btn.attr("data-active", "true"); + this.roomInteractionMode = otherObjectType.mode; + } + } + }); + } + changed = true; + } + }); + } + + private addObject(position: IGridPosition, type: string): void { + if (!this.mapView.roomLayer.checkHoverTileValidity(position)) { + return; + } + + let tileList = this.mapView.mapController.roomModeController.currentRoom.tiles; + + for (let i = 0; i < tileList.length; i++) { + if (tileList[i].position.x === position.x && tileList[i].position.y === position.y) { + if (type === "RACK") { + this.mapController.api.addRack(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.currentRoom.id, tileList[i].id, { + id: -1, + objectType: "RACK", + name: "", + capacity: 42, + powerCapacityW: 5000 + }).then((rack: IRack) => { + tileList[i].object = rack; + tileList[i].objectId = rack.id; + tileList[i].objectType = type; + this.mapView.dcObjectLayer.populateObjectList(); + this.mapView.dcObjectLayer.draw(); + + this.mapView.updateScene = true; + }); + } else if (type === "PSU") { + this.mapController.api.addPSU(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.currentRoom.id, tileList[i].id, this.availablePSUs[0]) + .then((psu: IPSU) => { + tileList[i].object = psu; + tileList[i].objectId = psu.id; + tileList[i].objectType = type; + this.mapView.dcObjectLayer.populateObjectList(); + this.mapView.dcObjectLayer.draw(); + + this.mapView.updateScene = true; + }); + } else if (type === "COOLING_ITEM") { + this.mapController.api.addCoolingItem(this.mapView.simulation.id, + this.mapView.currentDatacenter.id, this.currentRoom.id, tileList[i].id, + this.availableCoolingItems[0]).then((coolingItem: ICoolingItem) => { + tileList[i].object = coolingItem; + tileList[i].objectId = coolingItem.id; + tileList[i].objectType = type; + this.mapView.dcObjectLayer.populateObjectList(); + this.mapView.dcObjectLayer.draw(); + + this.mapView.updateScene = true; + }); + } + + break; + } + } + } + + /** + * Populates the room-type dropdown element with all available room types + */ + private populateRoomTypeDropdown(): void { + let dropdown = $("#roomtype-select"); + + this.roomTypes.forEach((type: string, index: number) => { + dropdown.append($('<option>').text(Util.toSentenceCase(type)).val(index)); + }); + } + + /** + * Loads all object types that are allowed in the current room into the menu. + */ + private populateAllowedObjectTypes(): void { + let addObjectsLabel = $("#add-objects-label"); + let noObjectsInfo = $("#no-objects-info"); + let allowedObjectTypes = this.roomTypeMap[this.currentRoom.roomType]; + + $(".dc-component-container").addClass("hidden"); + + if (allowedObjectTypes === undefined || allowedObjectTypes === null || allowedObjectTypes.length === 0) { + addObjectsLabel.addClass("hidden"); + noObjectsInfo.removeClass("hidden"); + + return; + } + + addObjectsLabel.removeClass("hidden"); + noObjectsInfo.addClass("hidden"); + allowedObjectTypes.forEach((type: string) => { + switch (type) { + case "RACK": + $("#add-rack-btn").removeClass("hidden"); + break; + case "PSU": + $("#add-psu-btn").removeClass("hidden"); + break; + case "COOLING_ITEM": + $("#add-cooling-item-btn").removeClass("hidden"); + break; + } + }); + } + + /** + * Checks whether a given room type can be assigned to the current room based on units already present. + * + * @param newRoomType The new room type to be validated + * @returns {boolean} Whether it is allowed to change the room's type to the new type + */ + private checkRoomTypeLegality(newRoomType: string): boolean { + let legality = true; + + this.currentRoom.tiles.forEach((tile: ITile) => { + if (tile.objectType !== undefined && tile.objectType !== null && tile.objectType !== "" && + this.roomTypeMap[newRoomType].indexOf(tile.objectType) === -1) { + legality = false; + } + }); + + return legality; + } +}
\ No newline at end of file |
