import { AnyObject, Size, Position, WithModuleProps } from "./lib/types"; import { parseBoolean, sizePropsDecoder, parseIntOr, notEmptyStringOr, itemMetaDecoder, t, ellipsize, debounce } from "./lib"; import Item, { ItemType, ItemProps, ItemClickEvent, ItemRemoveEvent, ItemMovedEvent, ItemResizedEvent, ItemSelectionChangedEvent } from "./Item"; import StaticGraph, { staticGraphPropsDecoder } from "./items/StaticGraph"; import Icon, { iconPropsDecoder } from "./items/Icon"; import ColorCloud, { colorCloudPropsDecoder } from "./items/ColorCloud"; import NetworkLink, { networkLinkPropsDecoder } from "./items/NetworkLink"; import Group, { groupPropsDecoder } from "./items/Group"; import Clock, { clockPropsDecoder } from "./items/Clock"; import Box, { boxPropsDecoder } from "./items/Box"; import Line, { linePropsDecoder, LineMovedEvent } from "./items/Line"; import Label, { labelPropsDecoder } from "./items/Label"; import SimpleValue, { simpleValuePropsDecoder } from "./items/SimpleValue"; import EventsHistory, { eventsHistoryPropsDecoder } from "./items/EventsHistory"; import Percentile, { percentilePropsDecoder } from "./items/Percentile"; import TypedEvent, { Disposable, Listener } from "./lib/TypedEvent"; import DonutGraph, { donutGraphPropsDecoder } from "./items/DonutGraph"; import BarsGraph, { barsGraphPropsDecoder } from "./items/BarsGraph"; import ModuleGraph, { moduleGraphPropsDecoder } from "./items/ModuleGraph"; import Service, { servicePropsDecoder } from "./items/Service"; import Odometer, { odometerPropsDecoder } from "./items/Odometer"; import BasicChart, { basicChartPropsDecoder } from "./items/BasicChart"; // TODO: Document. // eslint-disable-next-line @typescript-eslint/explicit-function-return-type function itemInstanceFrom(data: AnyObject) { const type = parseIntOr(data.type, null); if (type == null) throw new TypeError("missing item type."); const meta = itemMetaDecoder(data); switch (type as ItemType) { case ItemType.STATIC_GRAPH: return new StaticGraph(staticGraphPropsDecoder(data), meta); case ItemType.MODULE_GRAPH: return new ModuleGraph(moduleGraphPropsDecoder(data), meta); case ItemType.SIMPLE_VALUE: case ItemType.SIMPLE_VALUE_MAX: case ItemType.SIMPLE_VALUE_MIN: case ItemType.SIMPLE_VALUE_AVG: return new SimpleValue(simpleValuePropsDecoder(data), meta); case ItemType.PERCENTILE_BAR: case ItemType.PERCENTILE_BUBBLE: case ItemType.CIRCULAR_PROGRESS_BAR: case ItemType.CIRCULAR_INTERIOR_PROGRESS_BAR: return new Percentile(percentilePropsDecoder(data), meta); case ItemType.LABEL: return new Label(labelPropsDecoder(data), meta); case ItemType.ICON: return new Icon(iconPropsDecoder(data), meta); case ItemType.SERVICE: return new Service(servicePropsDecoder(data), meta); case ItemType.GROUP_ITEM: return new Group(groupPropsDecoder(data), meta); case ItemType.BOX_ITEM: return new Box(boxPropsDecoder(data), meta); case ItemType.LINE_ITEM: return new Line(linePropsDecoder(data), meta); case ItemType.AUTO_SLA_GRAPH: return new EventsHistory(eventsHistoryPropsDecoder(data), meta); case ItemType.DONUT_GRAPH: return new DonutGraph(donutGraphPropsDecoder(data), meta); case ItemType.BARS_GRAPH: return new BarsGraph(barsGraphPropsDecoder(data), meta); case ItemType.CLOCK: return new Clock(clockPropsDecoder(data), meta); case ItemType.COLOR_CLOUD: return new ColorCloud(colorCloudPropsDecoder(data), meta); case ItemType.NETWORK_LINK: return new NetworkLink(networkLinkPropsDecoder(data), meta); case ItemType.ODOMETER: return new Odometer(odometerPropsDecoder(data), meta); case ItemType.BASIC_CHART: return new BasicChart(basicChartPropsDecoder(data), meta); default: throw new TypeError("item not found"); } } // TODO: Document. // eslint-disable-next-line @typescript-eslint/explicit-function-return-type function decodeProps(data: AnyObject) { const type = parseIntOr(data.type, null); if (type == null) throw new TypeError("missing item type."); switch (type as ItemType) { case ItemType.STATIC_GRAPH: return staticGraphPropsDecoder(data); case ItemType.MODULE_GRAPH: return moduleGraphPropsDecoder(data); case ItemType.SIMPLE_VALUE: case ItemType.SIMPLE_VALUE_MAX: case ItemType.SIMPLE_VALUE_MIN: case ItemType.SIMPLE_VALUE_AVG: return simpleValuePropsDecoder(data); case ItemType.PERCENTILE_BAR: case ItemType.PERCENTILE_BUBBLE: case ItemType.CIRCULAR_PROGRESS_BAR: case ItemType.CIRCULAR_INTERIOR_PROGRESS_BAR: return percentilePropsDecoder(data); case ItemType.LABEL: return labelPropsDecoder(data); case ItemType.ICON: return iconPropsDecoder(data); case ItemType.SERVICE: return servicePropsDecoder(data); case ItemType.GROUP_ITEM: return groupPropsDecoder(data); case ItemType.BOX_ITEM: return boxPropsDecoder(data); case ItemType.LINE_ITEM: return linePropsDecoder(data); case ItemType.AUTO_SLA_GRAPH: return eventsHistoryPropsDecoder(data); case ItemType.DONUT_GRAPH: return donutGraphPropsDecoder(data); case ItemType.BARS_GRAPH: return barsGraphPropsDecoder(data); case ItemType.CLOCK: return clockPropsDecoder(data); case ItemType.COLOR_CLOUD: return colorCloudPropsDecoder(data); case ItemType.NETWORK_LINK: return networkLinkPropsDecoder(data); case ItemType.ODOMETER: return odometerPropsDecoder(data); case ItemType.BASIC_CHART: return basicChartPropsDecoder(data); default: throw new TypeError("decoder not found"); } } // Base properties. export interface VisualConsoleProps extends Size { readonly id: number; name: string; groupId: number; backgroundURL: string | null; // URL? backgroundColor: string | null; isFavorite: boolean; relationLineWidth: number; maintenanceMode: MaintenanceModeInterface | null; } export interface MaintenanceModeInterface { user: string; timestamp: number; } /** * Build a valid typed object from a raw object. * This will allow us to ensure the type safety. * * @param data Raw object. * @return An object representing the Visual Console props. * @throws Will throw a TypeError if some property * is missing from the raw object or have an invalid type. */ export function visualConsolePropsDecoder( data: AnyObject ): VisualConsoleProps | never { // Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation const { id, name, groupId, backgroundURL, backgroundColor, isFavorite, relationLineWidth, maintenanceMode } = data; if (id == null || isNaN(parseInt(id))) { throw new TypeError("invalid Id."); } if (typeof name !== "string" || name.length === 0) { throw new TypeError("invalid name."); } if (groupId == null || isNaN(parseInt(groupId))) { throw new TypeError("invalid group Id."); } return { id: parseInt(id), name, groupId: parseInt(groupId), backgroundURL: notEmptyStringOr(backgroundURL, null), backgroundColor: notEmptyStringOr(backgroundColor, null), isFavorite: parseBoolean(isFavorite), relationLineWidth: parseIntOr(relationLineWidth, 0), maintenanceMode: maintenanceMode, ...sizePropsDecoder(data) }; } export default class VisualConsole { // Reference to the DOM element which will contain the items. private readonly containerRef: HTMLElement; // Properties. private _props: VisualConsoleProps; // Visual Console Item instances by their Id. private elementsById: { [key: number]: Item; } = {}; // Visual Console Item Ids. private elementIds: ItemProps["id"][] = []; // Dictionary which store the created lines. private relations: { [key: string]: Line; } = {}; // Dictionary which store the related items (by ID). private lineLinks: { [key: number]: { [key: number]: { [key: string]: number } }; } = {}; private lines: { [key: number]: { [key: string]: number }; } = {}; // Event manager for click events. private readonly clickEventManager = new TypedEvent(); // Event manager for double click events. private readonly dblClickEventManager = new TypedEvent(); // Event manager for move events. private readonly movedEventManager = new TypedEvent(); // Event manager for line move events. private readonly lineMovedEventManager = new TypedEvent(); // Event manager for resize events. private readonly resizedEventManager = new TypedEvent(); // Event manager for remove events. private readonly selectionChangedEventManager = new TypedEvent< ItemSelectionChangedEvent >(); // List of references to clean the event listeners. private readonly disposables: Disposable[] = []; /** * React to a click on an element. * @param e Event object. */ private handleElementClick: (e: ItemClickEvent) => void = e => { this.clickEventManager.emit(e); // console.log(`Clicked element #${e.data.id}`, e); }; /** * React to a double click on an element. * @param e Event object. */ private handleElementDblClick: (e: ItemClickEvent) => void = e => { this.dblClickEventManager.emit(e); // console.log(`Double clicked element #${e.data.id}`, e); }; /** * React to a movement on an element. * @param e Event object. */ private handleElementMovement: (e: ItemMovedEvent) => void = e => { // Move their relation lines. const itemId = e.item.props.id; const relations = this.getItemRelations(itemId); relations.forEach(relation => { if (relation.parentId === itemId) { // Move the line start. relation.line.props = { ...relation.line.props, startPosition: this.getVisualCenter(e.newPosition, e.item) }; } else if (relation.childId === itemId) { // Move the line end. relation.line.props = { ...relation.line.props, endPosition: this.getVisualCenter(e.newPosition, e.item) }; } }); // Move lines conneted with this item. this.updateLinesConnected(e.item.props, e.newPosition, false); // console.log(`Moved element #${e.item.props.id}`, e); }; /** * React to a movement finished on an element. * @param e Event object. */ private handleElementMovementFinished: (e: ItemMovedEvent) => void = e => { this.movedEventManager.emit(e); // Move lines conneted with this item. this.updateLinesConnected(e.item.props, e.newPosition, true); // console.log(`Movement finished for element #${e.item.props.id}`, e); }; /** * Verifies if x,y are inside item coordinates. * @param x Coordinate X * @param y Coordinate Y * @param item ItemProps instance. */ private coordinatesInItem(x: number, y: number, props: ItemProps) { if ( props.type == ItemType.LINE_ITEM || props.type == ItemType.NETWORK_LINK ) { return false; } if ( x > props.x && x < props.x + props.width && y > props.y && y < props.y + props.height ) { return true; } return false; } /** * React to a line movement. * @param e Event object. */ private handleLineElementMovementFinished: ( e: LineMovedEvent ) => void = e => { // Update links. this.refreshLink(e.item); // Build line relationships between items and lines. this.lineMovedEventManager.emit(e); // console.log(`Movement finished for element #${e.item.props.id}`, e); }; /** * React to a resizement on an element. * @param e Event object. */ private handleElementResizement: (e: ItemResizedEvent) => void = e => { // Move their relation lines. const item = e.item; const props = item.props; const itemId = props.id; const relations = this.getItemRelations(itemId); const position = { x: props.x, y: props.y }; const meta = this.elementsById[itemId].meta; this.elementsById[itemId].meta = { ...meta, isUpdating: true }; relations.forEach(relation => { if (relation.parentId === itemId) { // Move the line start. relation.line.props = { ...relation.line.props, startPosition: this.getVisualCenter(position, item) }; } else if (relation.childId === itemId) { // Move the line end. relation.line.props = { ...relation.line.props, endPosition: this.getVisualCenter(position, item) }; } }); // console.log(`Resized element #${e.item.props.id}`, e); }; /** * React to a finished resizement on an element. * @param e Event object. */ private handleElementResizementFinished: ( e: ItemResizedEvent ) => void = e => { this.resizedEventManager.emit(e); // console.log(`Resize fonished for element #${e.item.props.id}`, e); }; /** * Clear some element references. * @param e Event object. */ private handleElementRemove: (e: ItemRemoveEvent) => void = e => { // Remove the element from the list and its relations. this.elementIds = this.elementIds.filter(id => id !== e.item.props.id); delete this.elementsById[e.item.props.id]; this.clearRelations(e.item.props.id); }; /** * React to element selection change * @param e Event object. */ private handleElementSelectionChanged: ( e: ItemSelectionChangedEvent ) => void = e => { if (this.elements.filter(item => item.meta.isSelected == true).length > 0) { e.selected = true; } else { e.selected = false; } this.selectionChangedEventManager.emit(e); }; // TODO: Document private handleContainerClick: (e: MouseEvent) => void = () => { this.unSelectItems(); }; /** * Refresh link for given line. * * @param line Line. */ protected refreshLink(l: Line) { let line: number = l.props.id; let itemAtStart = 0; let itemAtEnd = 0; try { for (let i in this.elementsById) { if ( this.coordinatesInItem( l.props.startPosition.x, l.props.startPosition.y, this.elementsById[i].props ) ) { // Start position at element i. itemAtStart = parseInt(i); } if ( this.coordinatesInItem( l.props.endPosition.x, l.props.endPosition.y, this.elementsById[i].props ) ) { // Start position at element i. itemAtEnd = parseInt(i); } } if (this.lineLinks == null) { this.lineLinks = {}; } if (this.lines == null) { this.lines = {}; } if (itemAtStart == line) { itemAtStart = 0; } if (itemAtEnd == line) { itemAtEnd = 0; } // Initialize line if not registered. if (this.lines[line] == null) { this.lines[line] = { start: itemAtStart, end: itemAtEnd }; } // Register 'start' side of the line. if (itemAtStart > 0) { // Initialize. if (this.lineLinks[itemAtStart] == null) { this.lineLinks[itemAtStart] = {}; } // Assign. this.lineLinks[itemAtStart][line] = { start: itemAtStart, end: itemAtEnd }; // Register line if not exists prviously. } else { // Clean previous line relationship. if (this.lines[line]["start"] > 0) { this.lineLinks[this.lines[line]["start"]][line]["start"] = 0; this.lines[line]["start"] = 0; } } if (itemAtEnd > 0) { if (this.lineLinks[itemAtEnd] == null) { this.lineLinks[itemAtEnd] = {}; } this.lineLinks[itemAtEnd][line] = { start: itemAtStart, end: itemAtEnd }; } else { // Clean previous line relationship. if (this.lines[line]["end"] > 0) { this.lineLinks[this.lines[line]["end"]][line]["end"] = 0; this.lines[line]["end"] = 0; } } this.lines[line] = { start: itemAtStart, end: itemAtEnd }; // Cleanup. for (let i in this.lineLinks) { if (this.lineLinks[i][line]) { if ( this.lineLinks[i][line].start == 0 && this.lineLinks[i][line].end == 0 ) { // Object not connected to a line. delete this.lineLinks[i][line]; if (Object.keys(this.lineLinks[i]).length === 0) { delete this.lineLinks[i]; } } if ( (this.lineLinks[i][line].start != itemAtStart && this.lineLinks[i][line].end == itemAtEnd) || (this.lineLinks[i][line].start == itemAtStart && this.lineLinks[i][line].end != itemAtEnd) ) { // Object not connected to a line. delete this.lineLinks[i][line]; if (Object.keys(this.lineLinks[i]).length === 0) { delete this.lineLinks[i]; } } } } } catch (error) { console.error(error); } } /** * Updates lines connected to this item. * * @param item Item moved. * @param newPosition New location for item. * @param oldPosition Old location for item. * @param save Save to ajax or not. */ protected updateLinesConnected(item: ItemProps, to: Position, save: boolean) { if (this.lineLinks[item.id] == null) { return; } Object.keys(this.lineLinks[item.id]).forEach(i => { let lineId = parseInt(i); const found = this.elementIds.indexOf(lineId); if (found === -1) { return; } let line = this.elementsById[lineId] as Line; if (line.props) { let startX = line.props.startPosition.x; let startY = line.props.startPosition.y; let endX = line.props.endPosition.x; let endY = line.props.endPosition.y; if (item.id == this.lineLinks[item.id][lineId]["start"]) { startX = to.x + item.width / 2; startY = to.y + item.height / 2; } if (item.id == this.lineLinks[item.id][lineId]["end"]) { endX = to.x + item.width / 2; endY = to.y + item.height / 2; } // Update line movement. this.updateElement({ ...line.props, startX: startX, startY: startY, endX: endX, endY: endY }); if (save) { let debouncedLinePositionSave = debounce( 500, (options: AnyObject) => { this.lineMovedEventManager.emit({ item: options.line, startPosition: { x: options.startX, y: options.startY }, endPosition: { x: options.endX, y: options.endY } }); } ); // Save line positon. debouncedLinePositionSave({ line: line, startX: startX, startY: startY, endX: endX, endY: endY }); } } }); // Update parents... this.buildRelations(item.id, to.x + item.width / 2, to.y + item.height / 2); } public constructor( container: HTMLElement, props: AnyObject, items: AnyObject[] ) { this.containerRef = container; this._props = visualConsolePropsDecoder(props); // Force the first render. this.render(); // Sort by id ASC items = items.sort(function(a, b) { if (a.id == null || b.id == null) return 0; else if (a.id > b.id) return 1; else return -1; }); // Initialize the items. items.forEach(item => this.addElement(item, this)); // Create lines. this.buildRelations(); // Re-attach all connected lines if any. this.elements.forEach(item => { if (item instanceof Line) { this.refreshLink(item); } }); this.containerRef.addEventListener("click", this.handleContainerClick); } /** * Public accessor of the `elements` property. * @return Properties. */ public get elements(): Item[] { // Ensure the type cause Typescript doesn't know the filter removes null items. return this.elementIds .map(id => this.elementsById[id]) .filter(_ => _ != null) as Item[]; } /** * To create a new element add it to the DOM. * @param item. Raw representation of the item's data. */ public addElement(item: AnyObject, context: this = this) { try { const itemInstance = itemInstanceFrom(item); // Add the item to the list. context.elementsById[itemInstance.props.id] = itemInstance; context.elementIds.push(itemInstance.props.id); // Item event handlers. itemInstance.onRemove(context.handleElementRemove); itemInstance.onSelectionChanged(context.handleElementSelectionChanged); itemInstance.onClick(context.handleElementClick); itemInstance.onDblClick(context.handleElementDblClick); // TODO:Continue if (itemInstance instanceof Line) { itemInstance.onLineMovementFinished( context.handleLineElementMovementFinished ); this.refreshLink(itemInstance); } else { itemInstance.onMoved(context.handleElementMovement); itemInstance.onMovementFinished(context.handleElementMovementFinished); itemInstance.onResized(context.handleElementResizement); itemInstance.onResizeFinished(context.handleElementResizementFinished); } // Add the item to the DOM. context.containerRef.append(itemInstance.elementRef); return itemInstance; } catch (error) { console.error("Error creating a new element:", (error as Error).message); } return; } /** * Public setter of the `elements` property. * @param items. */ public updateElements(items: AnyObject[]): void { // Ensure the type cause Typescript doesn't know the filter removes null items. const itemIds = items .map(item => item.id || null) .filter(id => id != null) as number[]; // Get the elements we should delete. const deletedIds = this.elementIds.filter(id => itemIds.indexOf(id) < 0); // Delete the elements. deletedIds.forEach(id => { if (this.elementsById[id] != null) { this.elementsById[id].remove(); delete this.elementsById[id]; } }); // Replace the element ids. this.elementIds = itemIds; // Initialize the items. items.forEach(item => { if (item.id) { if (this.elementsById[item.id] == null) { // New item. this.addElement(item); } else { // Update item. try { this.elementsById[item.id].props = decodeProps(item); } catch (error) { console.error( "Error updating an element:", (error as Error).message ); } } } }); // Re-build relations. this.buildRelations(); } /** * Public setter of the `element` property. * @param item. */ public updateElement(item: AnyObject): void { // Update item. try { this.elementsById[item.id].props = { ...decodeProps(item) }; } catch (error) { console.error("Error updating element:", (error as Error).message); } // Re-build relations. this.buildRelations(); } /** * Public accessor of the `props` property. * @return Properties. */ public get props(): VisualConsoleProps { return { ...this._props }; // Return a copy. } /** * Public setter of the `props` property. * If the new props are different enough than the * stored props, a render would be fired. * @param newProps */ public set props(newProps: VisualConsoleProps) { const prevProps = this.props; // Update the internal props. this._props = newProps; // From this point, things which rely on this.props can access to the changes. // Re-render. this.render(prevProps); } /** * Recreate or update the HTMLElement which represents the Visual Console into the DOM. * @param prevProps If exists it will be used to only DOM updates instead of a full replace. */ public render(prevProps: VisualConsoleProps | null = null): void { if (prevProps) { if (prevProps.backgroundURL !== this.props.backgroundURL) { this.containerRef.style.backgroundImage = this.props.backgroundURL !== null ? `url(${this.props.backgroundURL})` : ""; } if (this.props.backgroundColor != null) if (prevProps.backgroundColor !== this.props.backgroundColor) { this.containerRef.style.backgroundColor = this.props.backgroundColor; } if (this.sizeChanged(prevProps, this.props)) { this.resizeElement(this.props.width, this.props.height); } } else { if (this.props.backgroundURL) this.containerRef.style.backgroundImage = this.props.backgroundURL !== null ? `url(${this.props.backgroundURL})` : ""; if (this.props.backgroundColor) this.containerRef.style.backgroundColor = this.props.backgroundColor; this.resizeElement(this.props.width, this.props.height); } } /** * Compare the previous and the new size and return * a boolean value in case the size changed. * @param prevSize * @param newSize * @return Whether the size changed or not. */ public sizeChanged(prevSize: Size, newSize: Size): boolean { return ( prevSize.width !== newSize.width || prevSize.height !== newSize.height ); } /** * Resize the DOM container. * @param width * @param height */ public resizeElement(width: number, height: number): void { this.containerRef.style.width = `${width}px`; this.containerRef.style.height = `${height}px`; } /** * Update the size into the properties and resize the DOM container. * @param width * @param height */ public resize(width: number, height: number): void { this.props = { ...this.props, // Object spread: http://es6-features.org/#SpreadOperator width, height }; } /** * To remove the event listeners and the elements from the DOM. */ public remove(): void { this.disposables.forEach(d => d.dispose()); // Arrow function. this.elements.forEach(e => e.remove()); // Arrow function. this.elementsById = {}; this.elementIds = []; // Clear relations. this.clearRelations(); // Remove the click event listener. this.containerRef.removeEventListener("click", this.handleContainerClick); // Clean container. this.containerRef.innerHTML = ""; } /** * Create line elements which connect the elements with their parents. * * When itemId is being moved, overwrite position of the 'parent' or 'child' * endpoints of the line, using X and Y values. */ public buildRelations(itemId?: number, x?: number, y?: number): void { // Clear relations. this.clearRelations(); // Add relations. this.elements.forEach(item => { if (item.props.parentId !== null) { const parent = this.elementsById[item.props.parentId]; const child = this.elementsById[item.props.id]; if (parent && child) { if (itemId != undefined) { if (item.props.parentId == itemId) { // Update parent line position. this.addRelationLine(parent, child, x, y); } else if (item.props.id == itemId) { // Update child line position. this.addRelationLine(parent, child, undefined, undefined, x, y); } else { this.addRelationLine(parent, child); } } else { // No movements default behaviour. this.addRelationLine(parent, child); } } } }); } /** * @param itemId Optional identifier of a parent or child item. * Remove the line elements which connect the elements with their parents. */ private clearRelations(itemId?: number): void { if (itemId != null) { for (let key in this.relations) { const ids = key.split("|"); const parentId = Number.parseInt(ids[0]); const childId = Number.parseInt(ids[1]); if (itemId === parentId || itemId === childId) { this.relations[key].remove(); delete this.relations[key]; } } } else { for (let key in this.relations) { this.relations[key].remove(); delete this.relations[key]; } } } /** * Retrieve the line element which represent the relation between items. * @param parentId Identifier of the parent item. * @param childId Itentifier of the child item. * @return The line element or nothing. */ private getRelationLine(parentId: number, childId: number): Line | null { const identifier = `${parentId}|${childId}`; return this.relations[identifier] || null; } // TODO: Document. private getItemRelations( itemId: number ): { parentId: number; childId: number; line: Line; }[] { const itemRelations = []; for (let key in this.relations) { const ids = key.split("|"); const parentId = Number.parseInt(ids[0]); const childId = Number.parseInt(ids[1]); if (itemId === parentId || itemId === childId) { itemRelations.push({ parentId, childId, line: this.relations[key] }); } } return itemRelations; } /** * Retrieve the visual center of the item. It's ussually the center of the * content, like the label doesn't exist. * @param position Initial position. * @param element Element we want to use. */ private getVisualCenter( position: Position, element: Item ): Position { let x = position.x + element.elementRef.clientWidth / 2; let y = position.y + element.elementRef.clientHeight / 2; if ( typeof element.props.label !== "undefined" || element.props.label !== "" || element.props.label !== null ) { switch (element.props.labelPosition) { case "up": y = position.y + (element.elementRef.clientHeight + element.labelElementRef.clientHeight) / 2; break; case "down": y = position.y + (element.elementRef.clientHeight - element.labelElementRef.clientHeight) / 2; break; case "right": x = position.x + (element.elementRef.clientWidth - element.labelElementRef.clientWidth) / 2; break; case "left": x = position.x + (element.elementRef.clientWidth + element.labelElementRef.clientWidth) / 2; break; } } return { x, y }; } /** * Add a new line item to represent a relation between the items. * @param parent Parent item. * @param child Child item. * @return Whether the line was added or not. */ private addRelationLine( parent: Item, child: Item, parentX?: number, parentY?: number, childX?: number, childY?: number ): Line { const identifier = `${parent.props.id}|${child.props.id}`; if (this.relations[identifier] != null) { this.relations[identifier].remove(); } // Get the items center. let { x: startX, y: startY } = this.getVisualCenter(parent.props, parent); let { x: endX, y: endY } = this.getVisualCenter(child.props, child); // Overwrite positions if needed (while moving it!). if (parentX != null) { startX = parentX; } if (parentY != null) { startY = parentY; } if (childX != null) { endX = childX; } if (childY != null) { endY = childY; } // Line inherits child element status. const line = new Line( linePropsDecoder({ id: 0, type: ItemType.LINE_ITEM, startX, startY, endX, endY, width: 0, height: 0, lineWidth: this.props.relationLineWidth, color: notEmptyStringOr(child.props.colorStatus, "#CCC") }), itemMetaDecoder({ receivedAt: new Date() }) ); // Save a reference to the line item. this.relations[identifier] = line; // Add the line to the DOM. line.elementRef.style.zIndex = "0"; this.containerRef.append(line.elementRef); return line; } /** * Add an event handler to the click of the linked visual console elements. * @param listener Function which is going to be executed when a linked console is clicked. */ public onItemClick(listener: Listener): Disposable { /* * The '.on' function returns a function which will clean the event * listener when executed. We store all the 'dispose' functions to * call them when the item should be cleared. */ const disposable = this.clickEventManager.on(listener); this.disposables.push(disposable); return disposable; } /** * Add an event handler to the double click of the linked visual console elements. * @param listener Function which is going to be executed when a linked console is double clicked. */ public onItemDblClick(listener: Listener): Disposable { /* * The '.on' function returns a function which will clean the event * listener when executed. We store all the 'dispose' functions to * call them when the item should be cleared. */ const disposable = this.dblClickEventManager.on(listener); this.disposables.push(disposable); return disposable; } /** * Add an event handler to the movement of the visual console elements. * @param listener Function which is going to be executed when a linked console is moved. */ public onItemMoved(listener: Listener): Disposable { /* * The '.on' function returns a function which will clean the event * listener when executed. We store all the 'dispose' functions to * call them when the item should be cleared. */ const disposable = this.movedEventManager.on(listener); this.disposables.push(disposable); return disposable; } /** * Add an event handler to the movement of the visual console line elements. * @param listener Function which is going to be executed when a linked console is moved. */ public onLineMoved(listener: Listener): Disposable { /* * The '.on' function returns a function which will clean the event * listener when executed. We store all the 'dispose' functions to * call them when the item should be cleared. */ const disposable = this.lineMovedEventManager.on(listener); this.disposables.push(disposable); return disposable; } /** * Add an event handler to the resizement of the visual console elements. * @param listener Function which is going to be executed when a linked console is moved. */ public onItemResized(listener: Listener): Disposable { /* * The '.on' function returns a function which will clean the event * listener when executed. We store all the 'dispose' functions to * call them when the item should be cleared. */ const disposable = this.resizedEventManager.on(listener); this.disposables.push(disposable); return disposable; } /** * Add an event handler to the elements selection change of the visual console . * @param listener Function which is going to be executed when a linked console is moved. */ public onItemSelectionChanged( listener: Listener ): Disposable { /* * The '.on' function returns a function which will clean the event * listener when executed. We store all the 'dispose' functions to * call them when the item should be cleared. */ const disposable = this.selectionChangedEventManager.on(listener); this.disposables.push(disposable); return disposable; } /** * Enable the edition mode. */ public enableEditMode(): void { this.elements.forEach(item => { item.meta = { ...item.meta, editMode: true }; }); this.containerRef.classList.add("is-editing"); } /** * Disable the edition mode. */ public disableEditMode(): void { this.elements.forEach(item => { item.meta = { ...item.meta, editMode: false }; }); this.containerRef.classList.remove("is-editing"); } /** * Enable the maintenance mode. */ public enableMaintenanceMode(): void { this.elements.forEach(item => { item.meta = { ...item.meta, maintenanceMode: true }; }); this.containerRef.classList.add("is-maintenance"); this.containerRef.classList.remove("is-editing"); } /** * Disable the maintenance mode. */ public disableMaintenanceMode(): void { this.elements.forEach(item => { item.meta = { ...item.meta, maintenanceMode: false }; }); this.containerRef.classList.remove("is-maintenance"); this.containerRef.classList.add("is-editing"); } /** * Select an item. * @param itemId Item Id. * @param unique To remove the selection of other items or not. */ public selectItem(itemId: number, unique: boolean = false): void { if (unique) { this.elementIds.forEach(currentItemId => { const meta = this.elementsById[currentItemId].meta; if (currentItemId !== itemId && meta.isSelected) { this.elementsById[currentItemId].unSelectItem(); } else if (currentItemId === itemId && !meta.isSelected) { this.elementsById[currentItemId].selectItem(); } }); } else if (this.elementsById[itemId]) { this.elementsById[itemId].selectItem(); } } /** * Unselect an item. * @param itemId Item Id. */ public unSelectItem(itemId: number): void { if (this.elementsById[itemId]) { const meta = this.elementsById[itemId].meta; if (meta.isSelected) { this.elementsById[itemId].unSelectItem(); } } } /** * Unselect all items. */ public unSelectItems(): void { this.elementIds.forEach(itemId => { if (this.elementsById[itemId]) { this.elementsById[itemId].unSelectItem(); } }); } // TODO: Document. public static items = { [ItemType.STATIC_GRAPH]: StaticGraph, [ItemType.MODULE_GRAPH]: ModuleGraph, [ItemType.SIMPLE_VALUE]: SimpleValue, [ItemType.SIMPLE_VALUE_MAX]: SimpleValue, [ItemType.SIMPLE_VALUE_MIN]: SimpleValue, [ItemType.SIMPLE_VALUE_AVG]: SimpleValue, [ItemType.PERCENTILE_BAR]: Percentile, [ItemType.PERCENTILE_BUBBLE]: Percentile, [ItemType.CIRCULAR_PROGRESS_BAR]: Percentile, [ItemType.CIRCULAR_INTERIOR_PROGRESS_BAR]: Percentile, [ItemType.LABEL]: Label, [ItemType.ICON]: Icon, [ItemType.SERVICE]: Service, [ItemType.GROUP_ITEM]: Group, [ItemType.BOX_ITEM]: Box, [ItemType.LINE_ITEM]: Line, [ItemType.AUTO_SLA_GRAPH]: EventsHistory, [ItemType.DONUT_GRAPH]: DonutGraph, [ItemType.BARS_GRAPH]: BarsGraph, [ItemType.CLOCK]: Clock, [ItemType.COLOR_CLOUD]: ColorCloud, [ItemType.NETWORK_LINK]: NetworkLink, [ItemType.ODOMETER]: Odometer, [ItemType.BASIC_CHART]: BasicChart }; /** * Relying type item and srcimg and agent and module * name convert name item representative. * * @param item Instance item from extract name. * * @return Name item. */ public static itemDescriptiveName(item: Item): string { let text: string; switch (item.props.type) { case ItemType.STATIC_GRAPH: text = `${t("Static graph")} - ${(item as StaticGraph).props.imageSrc}`; break; case ItemType.MODULE_GRAPH: text = t("Module graph"); break; case ItemType.CLOCK: text = t("Clock"); break; case ItemType.BARS_GRAPH: text = t("Bars graph"); break; case ItemType.AUTO_SLA_GRAPH: text = t("Event history graph"); break; case ItemType.PERCENTILE_BAR: text = t("Percentile bar"); break; case ItemType.CIRCULAR_PROGRESS_BAR: text = t("Circular progress bar"); break; case ItemType.CIRCULAR_INTERIOR_PROGRESS_BAR: text = t("Circular progress bar (interior)"); break; case ItemType.SIMPLE_VALUE: text = t("Simple Value"); break; case ItemType.LABEL: text = t("Label"); break; case ItemType.GROUP_ITEM: text = t("Group"); break; case ItemType.COLOR_CLOUD: text = t("Color cloud"); break; case ItemType.ICON: text = `${t("Icon")} - ${(item as Icon).props.imageSrc}`; break; case ItemType.ODOMETER: text = t("Odometer"); break; case ItemType.BASIC_CHART: text = t("BasicChart"); break; default: text = t("Item"); break; } const linkedAgentAndModuleProps = item.props as Partial; if ( linkedAgentAndModuleProps.agentAlias != null && linkedAgentAndModuleProps.moduleName != null ) { text += ` (${ellipsize( linkedAgentAndModuleProps.agentAlias, 18 )} - ${ellipsize(linkedAgentAndModuleProps.moduleName, 25)})`; } else if (linkedAgentAndModuleProps.agentAlias != null) { text += ` (${ellipsize(linkedAgentAndModuleProps.agentAlias, 25)})`; } return text; } }