WIP Visual Console Client

Former-commit-id: a638705269ea6ea35c00f1f4429c273bf861cfed
This commit is contained in:
Alejandro Gallardo Escobar 2019-02-18 17:23:57 +01:00
parent a08db7ce00
commit 589c5c97bf
6 changed files with 416 additions and 54 deletions

View File

@ -1,3 +1,256 @@
class VisualConsole {}
import { UnknownObject, Size } from "./types";
import { parseBoolean, sizePropsDecoder, parseIntOr } from "./lib";
import VisualConsoleItem, {
VisualConsoleItemProps,
VisualConsoleItemType
} from "./VisualConsoleItem";
import StaticGraph, { staticGraphPropsDecoder } from "./items/StaticGraph";
import Icon, { iconPropsDecoder } from "./items/Icon";
import ColorCloud, { colorCloudPropsDecoder } from "./items/ColorCloud";
import Group, { groupPropsDecoder } from "./items/Group";
export default VisualConsole;
// Base properties.
export interface VisualConsoleProps extends Size {
readonly id: number;
name: string;
groupId: number;
backgroundURL: string | null; // URL?
backgroundColor: string | null;
isFavorite: boolean;
}
/**
* 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: UnknownObject
): VisualConsoleProps | never {
// Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation
const {
id,
name,
groupId,
backgroundURL,
backgroundColor,
isFavorite
} = 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:
typeof backgroundURL === "string" && backgroundURL.length > 0
? backgroundURL
: null,
backgroundColor:
typeof backgroundColor === "string" && backgroundColor.length > 0
? backgroundColor
: null,
isFavorite: parseBoolean(isFavorite),
...sizePropsDecoder(data)
};
}
// TODO: Document.
function itemInstanceFrom(data: UnknownObject) {
const type = parseIntOr(data.type, null);
if (type == null) throw new TypeError("missing item type.");
switch (<VisualConsoleItemType>type) {
case VisualConsoleItemType.STATIC_GRAPH:
return new StaticGraph(staticGraphPropsDecoder(data));
case VisualConsoleItemType.MODULE_GRAPH:
throw new TypeError("item not found");
case VisualConsoleItemType.SIMPLE_VALUE:
throw new TypeError("item not found");
case VisualConsoleItemType.PERCENTILE_BAR:
throw new TypeError("item not found");
case VisualConsoleItemType.LABEL:
throw new TypeError("item not found");
case VisualConsoleItemType.ICON:
return new Icon(iconPropsDecoder(data));
case VisualConsoleItemType.SIMPLE_VALUE_MAX:
throw new TypeError("item not found");
case VisualConsoleItemType.SIMPLE_VALUE_MIN:
throw new TypeError("item not found");
case VisualConsoleItemType.SIMPLE_VALUE_AVG:
throw new TypeError("item not found");
case VisualConsoleItemType.PERCENTILE_BUBBLE:
throw new TypeError("item not found");
case VisualConsoleItemType.SERVICE:
throw new TypeError("item not found");
case VisualConsoleItemType.GROUP_ITEM:
return new Group(groupPropsDecoder(data));
case VisualConsoleItemType.BOX_ITEM:
throw new TypeError("item not found");
case VisualConsoleItemType.LINE_ITEM:
throw new TypeError("item not found");
case VisualConsoleItemType.AUTO_SLA_GRAPH:
throw new TypeError("item not found");
case VisualConsoleItemType.CIRCULAR_PROGRESS_BAR:
throw new TypeError("item not found");
case VisualConsoleItemType.CIRCULAR_INTERIOR_PROGRESS_BAR:
throw new TypeError("item not found");
case VisualConsoleItemType.DONUT_GRAPH:
throw new TypeError("item not found");
case VisualConsoleItemType.BARS_GRAPH:
throw new TypeError("item not found");
case VisualConsoleItemType.CLOCK:
throw new TypeError("item not found");
case VisualConsoleItemType.COLOR_CLOUD:
return new ColorCloud(colorCloudPropsDecoder(data));
default:
throw new TypeError("item not found");
}
}
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.
private elements: VisualConsoleItem<VisualConsoleItemProps>[] = [];
constructor(
container: HTMLElement,
props: VisualConsoleProps,
items: UnknownObject[]
) {
this.containerRef = container;
this._props = props;
// Force the first render.
this.render();
// TODO: Document.
items.forEach(item => {
try {
const itemInstance = itemInstanceFrom(item);
this.elements.push(itemInstance);
itemInstance.onClick(e =>
console.log(`Clicked element #${e.data.id}`, e)
);
this.containerRef.append(itemInstance.elementRef);
} catch (error) {
console.log("Error creating a new element:", error.message);
}
});
// Sort by isOnTop, id ASC
this.elements.sort(function(a, b) {
if (a.props.isOnTop && !b.props.isOnTop) return 1;
else if (!a.props.isOnTop && b.props.isOnTop) return -1;
else if (a.props.id < b.props.id) return 1;
else return -1;
});
}
/**
* Public accessor of the `props` property.
* @return Properties.
*/
get props(): VisualConsoleProps {
return this._props;
}
/**
* Public setter of the `props` property.
* If the new props are different enough than the
* stored props, a render would be fired.
* @param newProps
*/
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.
*/
render(prevProps: VisualConsoleProps | null = null): void {
if (prevProps) {
if (prevProps.backgroundURL !== this.props.backgroundURL) {
this.containerRef.style.backgroundImage = this.props.backgroundURL;
}
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 {
this.containerRef.style.backgroundImage = this.props.backgroundURL;
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.
*/
sizeChanged(prevSize: Size, newSize: Size): boolean {
return (
prevSize.width !== newSize.width || prevSize.height !== newSize.height
);
}
/**
* Resize the DOM container.
* @param width
* @param height
*/
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
*/
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.
*/
remove(): void {
this.elements.forEach(e => e.remove()); // Arrow function.
this.elements = [];
}
}

View File

@ -7,19 +7,63 @@ import {
} from "./lib";
import TypedEvent, { Listener, Disposable } from "./TypedEvent";
// Enum: https://www.typescriptlang.org/docs/handbook/enums.html.
export const enum VisualConsoleItemType {
STATIC_GRAPH = 0,
MODULE_GRAPH = 1,
SIMPLE_VALUE = 2,
PERCENTILE_BAR = 3,
LABEL = 4,
ICON = 5,
SIMPLE_VALUE_MAX = 6,
SIMPLE_VALUE_MIN = 7,
SIMPLE_VALUE_AVG = 8,
PERCENTILE_BUBBLE = 9,
SERVICE = 10,
GROUP_ITEM = 11,
BOX_ITEM = 12,
LINE_ITEM = 13,
AUTO_SLA_GRAPH = 14,
CIRCULAR_PROGRESS_BAR = 15,
CIRCULAR_INTERIOR_PROGRESS_BAR = 16,
DONUT_GRAPH = 17,
BARS_GRAPH = 18,
CLOCK = 19,
COLOR_CLOUD = 20
}
// Base item properties. This interface should be extended by the item implementations.
export interface VisualConsoleItemProps extends Position, Size {
readonly id: number;
readonly type: number;
readonly type: VisualConsoleItemType;
label: string | null;
labelPosition: "up" | "right" | "down" | "left";
isLinkEnabled: boolean;
isOnTop: boolean;
parentId: number | null;
aclGroupId: number | null;
}
// FIXME: Fix type compatibility.
export type ItemClickEvent<ItemProps extends VisualConsoleItemProps> = {
data: ItemProps;
// data: ItemProps;
data: UnknownObject;
};
/**
* Extract a valid enum value from a raw label position value.
* @param labelPosition Raw value.
*/
const parseLabelPosition = (labelPosition: any) => {
switch (labelPosition) {
case "up":
case "right":
case "down":
case "left":
return labelPosition;
default:
return "down";
}
};
/**
@ -31,13 +75,13 @@ export type ItemClickEvent<ItemProps extends VisualConsoleItemProps> = {
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function itemPropsDecoder(
export function itemBasePropsDecoder(
data: UnknownObject
): VisualConsoleItemProps | never {
if (data.id == null || isNaN(parseInt(data.id))) {
throw new TypeError("invalid id.");
}
// TODO: Check valid types.
// TODO: Check for valid types.
if (data.type == null || isNaN(parseInt(data.type))) {
throw new TypeError("invalid type.");
}
@ -49,6 +93,7 @@ export function itemPropsDecoder(
typeof data.label === "string" && data.label.length > 0
? data.label
: null,
labelPosition: parseLabelPosition(data.labelPosition),
isLinkEnabled: parseBoolean(data.isLinkEnabled),
isOnTop: parseBoolean(data.isOnTop),
parentId: parseIntOr(data.parentId, null),
@ -61,12 +106,10 @@ export function itemPropsDecoder(
abstract class VisualConsoleItem<ItemProps extends VisualConsoleItemProps> {
// Properties of the item.
private itemProps: ItemProps;
// Reference of the DOM element which contain all the items.
private readonly containerRef: HTMLElement;
// Reference of the DOM element which contain the item box.
private readonly itemBoxRef: HTMLElement;
// Reference of the DOM element which contain the view of the item which extends this class.
protected readonly elementRef: HTMLElement;
// Reference to the DOM element which will contain the item.
public readonly elementRef: HTMLElement;
// Reference to the DOM element which will contain the view of the item which extends this class.
protected readonly childElementRef: HTMLElement;
// Event manager for click events.
private readonly clickEventManager = new TypedEvent<
ItemClickEvent<ItemProps>
@ -80,8 +123,7 @@ abstract class VisualConsoleItem<ItemProps extends VisualConsoleItemProps> {
*/
abstract createDomElement(): HTMLElement;
constructor(container: HTMLElement, props: ItemProps) {
this.containerRef = container;
constructor(props: ItemProps) {
this.itemProps = props;
/*
@ -90,30 +132,31 @@ abstract class VisualConsoleItem<ItemProps extends VisualConsoleItemProps> {
* all the common things like click events, show a border
* when hovered, etc.
*/
this.itemBoxRef = this.createItemBoxDomElement();
this.elementRef = this.createContainerDomElement();
/*
* Get a HTMLElement which represents the custom view
* of the Visual Console item. This element will be
* different depending on the item implementation.
*/
this.elementRef = this.createDomElement();
this.childElementRef = this.createDomElement();
// Insert the elements into their parents.
// Visual Console Container > Generic Item Box > Custom Item View.
this.itemBoxRef.append(this.elementRef);
this.containerRef.append(this.itemBoxRef);
// Insert the elements into the container.
// Visual Console Item Container > Custom Item View.
this.elementRef.append(this.childElementRef);
}
/**
* To create a new box for the visual console item.
* @return Item box.
*/
private createItemBoxDomElement(): HTMLElement {
private createContainerDomElement(): HTMLElement {
const box: HTMLDivElement = document.createElement("div");
box.className = "visual-console-item";
box.style.width = `${this.props.width}px`;
box.style.height = `${this.props.height}px`;
box.style.left = `${this.props.x}px`;
box.style.top = `${this.props.y}px`;
box.onclick = () => this.clickEventManager.emit({ data: this.props });
// TODO: Add label.
return box;
@ -161,26 +204,25 @@ abstract class VisualConsoleItem<ItemProps extends VisualConsoleItemProps> {
/**
* To recreate or update the HTMLElement which represents the item into the DOM.
* @param prevProps If exists it will be used to only perform
* perform DOM updates instead of a full replace.
* @param prevProps If exists it will be used to only perform DOM updates instead of a full replace.
*/
render(prevProps: ItemProps | null): void {
render(prevProps: ItemProps | null = null): void {
// Move box.
if (!prevProps || prevProps.x !== this.props.x) {
this.itemBoxRef.style.left = `${this.props.x}px`;
this.elementRef.style.left = `${this.props.x}px`;
}
if (!prevProps || prevProps.y !== this.props.y) {
this.itemBoxRef.style.top = `${this.props.y}px`;
this.elementRef.style.top = `${this.props.y}px`;
}
// Resize box.
if (!prevProps || prevProps.width !== this.props.width) {
this.itemBoxRef.style.width = `${this.props.width}px`;
this.elementRef.style.width = `${this.props.width}px`;
}
if (!prevProps || prevProps.height !== this.props.height) {
this.itemBoxRef.style.height = `${this.props.height}px`;
this.elementRef.style.height = `${this.props.height}px`;
}
this.elementRef.replaceWith(this.createDomElement());
this.childElementRef.replaceWith(this.createDomElement());
}
/**
@ -190,9 +232,9 @@ abstract class VisualConsoleItem<ItemProps extends VisualConsoleItemProps> {
// Event listeners.
this.disposables.forEach(_ => _.dispose());
// VisualConsoleItem extension DOM element.
this.elementRef.remove();
this.childElementRef.remove();
// VisualConsoleItem DOM element.
this.itemBoxRef.remove();
this.elementRef.remove();
}
/**
@ -207,8 +249,8 @@ abstract class VisualConsoleItem<ItemProps extends VisualConsoleItemProps> {
this.itemProps.x = x;
this.itemProps.y = y;
// Move element.
this.itemBoxRef.style.left = `${x}px`;
this.itemBoxRef.style.top = `${y}px`;
this.elementRef.style.left = `${x}px`;
this.elementRef.style.top = `${y}px`;
}
/**
@ -223,8 +265,8 @@ abstract class VisualConsoleItem<ItemProps extends VisualConsoleItemProps> {
this.itemProps.width = width;
this.itemProps.height = height;
// Resize element.
this.itemBoxRef.style.width = `${width}px`;
this.itemBoxRef.style.height = `${height}px`;
this.elementRef.style.width = `${width}px`;
this.elementRef.style.height = `${height}px`;
}
/**

View File

@ -1,5 +1,14 @@
// import VisualConsole from "./VisualConsole";
import StaticGraphItem from "./items/StaticGraph";
/*
* Useful resources.
* http://es6-features.org/
* http://exploringjs.com/es6
* https://www.typescriptlang.org/
*/
import VisualConsole, {
visualConsolePropsDecoder,
VisualConsoleProps
} from "./VisualConsole";
// declare global {
// interface Window {
@ -12,21 +21,32 @@ import StaticGraphItem from "./items/StaticGraph";
const container = document.getElementById("visual-console-container");
if (container != null) {
const item = new StaticGraphItem(container, {
const rawProps = {
id: 1,
groupId: 0,
name: "Test Visual Console",
width: 800,
height: 300,
backgroundURL: null,
backgroundColor: "#000000",
isFavorite: false
};
const staticGraphRawProps = {
// Generic props.
id: 1,
type: 1,
type: 0, // Static graph = 0
label: null,
isLinkEnabled: false,
isOnTop: false,
parentId: null,
aclGroupId: null,
// Position props.
x: 0,
y: 0,
x: 100,
y: 50,
// Size props.
width: 50,
height: 50,
width: 100,
height: 100,
// Agent props.
agentId: null,
agentName: null,
@ -37,5 +57,42 @@ if (container != null) {
imageSrc:
"https://brutus.artica.lan:8081/uploads/-/system/project/avatar/1/1.png",
showLastValueTooltip: "default"
});
};
const colorCloudRawProps = {
// Generic props.
id: 2,
type: 20, // Static graph = 0
label: null,
labelText: "CLOUD",
isLinkEnabled: false,
isOnTop: false,
parentId: null,
aclGroupId: null,
// Position props.
x: 300,
y: 50,
// Size props.
width: 150,
height: 150,
// Agent props.
agentId: null,
agentName: null,
// Module props.
moduleId: null,
moduleName: null,
// Custom props.
color: "rgb(100, 50, 245)"
};
try {
const visualConsole = new VisualConsole(
container,
visualConsolePropsDecoder(rawProps),
[staticGraphRawProps, colorCloudRawProps]
);
console.log(visualConsole);
} catch (error) {
console.log("ERROR", error.message);
}
}

View File

@ -4,14 +4,16 @@ import {
UnknownObject
} from "../types";
import { modulePropsDecoder } from "../lib";
import { modulePropsDecoder, linkedVCPropsDecoder } from "../lib";
import VisualConsoleItem, {
VisualConsoleItemProps,
itemPropsDecoder
itemBasePropsDecoder,
VisualConsoleItemType
} from "../VisualConsoleItem";
export type StaticGraphProps = {
type: VisualConsoleItemType.STATIC_GRAPH;
imageSrc: string; // URL?
showLastValueTooltip: "default" | "enabled" | "disabled";
} & VisualConsoleItemProps &
@ -49,10 +51,12 @@ export function staticGraphPropsDecoder(
}
return {
...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects.
type: VisualConsoleItemType.STATIC_GRAPH,
imageSrc: data.imageSrc,
showLastValueTooltip: parseShowLastValueTooltip(data.showLastValueTooltip),
...itemPropsDecoder(data), // Object spread. It will merge the properties of the two objects.
...modulePropsDecoder(data) // Object spread. It will merge the properties of the two objects.
...modulePropsDecoder(data), // Object spread. It will merge the properties of the two objects.
...linkedVCPropsDecoder(data) // Object spread. It will merge the properties of the two objects.
};
}
@ -62,6 +66,8 @@ export default class StaticGraph extends VisualConsoleItem<StaticGraphProps> {
img.className = "static-graph";
img.src = this.props.imageSrc;
// TODO: Show last value in a tooltip.
return img;
}
}

View File

@ -73,7 +73,7 @@ export function sizePropsDecoder(data: UnknownObject): Size | never {
* @return An object representing the agent properties.
*/
export function agentPropsDecoder(data: UnknownObject): WithAgentProps {
// Object destructuring: http://exploringjs.com/es6/ch_destructuring.html
// Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation
const { metaconsoleId, agentId: id, agentName: name } = data;
const agentProps: WithAgentProps = {
@ -84,7 +84,7 @@ export function agentPropsDecoder(data: UnknownObject): WithAgentProps {
return metaconsoleId != null
? {
metaconsoleId,
...agentProps // Object spread: http://exploringjs.com/es6/ch_parameter-handling.html#sec_spread-operator
...agentProps // Object spread: http://es6-features.org/#SpreadOperator
}
: agentProps;
}
@ -95,7 +95,7 @@ export function agentPropsDecoder(data: UnknownObject): WithAgentProps {
* @return An object representing the module and agent properties.
*/
export function modulePropsDecoder(data: UnknownObject): WithModuleProps {
// Object destructuring: http://exploringjs.com/es6/ch_destructuring.html
// Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation
const { moduleId: id, moduleName: name } = data;
return {
@ -114,7 +114,7 @@ export function modulePropsDecoder(data: UnknownObject): WithModuleProps {
export function linkedVCPropsDecoder(
data: UnknownObject
): LinkedVisualConsoleProps | never {
// Object destructuring: http://exploringjs.com/es6/ch_destructuring.html
// Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation
const {
metaconsoleId,
linkedLayoutId: id,
@ -160,13 +160,13 @@ export function linkedVCPropsDecoder(
const linkedLayoutBaseProps = {
linkedLayoutId: parseIntOr(id, null),
linkedLayoutAgentId: parseIntOr(agentId, null),
...linkedLayoutStatusProps // Object spread: http://exploringjs.com/es6/ch_parameter-handling.html#sec_spread-operator
...linkedLayoutStatusProps // Object spread: http://es6-features.org/#SpreadOperator
};
return metaconsoleId != null
? {
metaconsoleId,
...linkedLayoutBaseProps // Object spread: http://exploringjs.com/es6/ch_parameter-handling.html#sec_spread-operator
...linkedLayoutBaseProps // Object spread: http://es6-features.org/#SpreadOperator
}
: linkedLayoutBaseProps;
}

View File

@ -1,3 +1,7 @@
.visual-console-item {
position: absolute;
}
.visual-console-item > * {
width: inherit;
height: inherit;