Merge branch 'ent-3992-anadir-el-modo-de-edicion-a-la-vista-de-la-consola-visual' into 'develop'

Añadir el modo de edición a la vista de la consola visual

Closes pandora_enterprise#3981

See merge request artica/pandorafms!2482
This commit is contained in:
Daniel Rodriguez 2019-06-17 15:07:12 +02:00
commit a583da3de3
34 changed files with 376 additions and 103 deletions

View File

@ -67,6 +67,13 @@ function createVisualConsole(
? JSON.parse(data.items)
: data.items;
// Add the datetime when the item was received.
var receivedAt = new Date();
items.map(function(item) {
item["receivedAt"] = receivedAt;
return item;
});
var prevProps = visualConsole.props;
// Update the data structure.
visualConsole.props = props;

View File

@ -813,6 +813,15 @@ input.datos {
* - VISUAL MAPS -
* ---------------------------------------------------------------------
*/
.visual-console-edit-controls {
display: flex;
justify-content: flex-end;
}
.visual-console-edit-controls > span {
margin: 4px;
}
input.vs_button_ghost {
background-color: transparent !important;
border: 1px solid #82b92e;

View File

@ -25,6 +25,12 @@
user-select: text;
}
.visual-console-item.is-editing {
border: 2px dashed #33ccff;
-webkit-transform: translateX(-2px) translateY(-2px);
transform: translateX(-2px) translateY(-2px);
}
@font-face {
font-family: Alarm Clock;
src: url(alarm-clock.ttf);

View File

@ -1 +1 @@
{"version":3,"sources":["webpack:///main.css","webpack:///styles.css"],"names":[],"mappings":"AAAA;EACE,gBAAgB;EAChB,kBAAkB;EAClB,4BAA4B;EAC5B,0BAA0B;EAC1B,2BAA2B;AAC7B;;AAEA;EACE,kBAAkB;EAClB,oBAAa;EAAb,oBAAa;EAAb,aAAa;EACb,2BAAuB;EAAvB,8BAAuB;MAAvB,2BAAuB;UAAvB,uBAAuB;EACvB,qBAAqB;EACrB,yBAAmB;MAAnB,sBAAmB;UAAnB,mBAAmB;EACnB,yBAAiB;KAAjB,sBAAiB;MAAjB,qBAAiB;UAAjB,iBAAiB;AACnB;;ACfA;EACE,wBAAwB;EACxB,0BAA2B;AAC7B;;AAEA,kBAAkB;;AAElB;EACE,oBAAa;EAAb,oBAAa;EAAb,aAAa;EACb,4BAAsB;EAAtB,6BAAsB;MAAtB,0BAAsB;UAAtB,sBAAsB;EACtB,wBAAuB;MAAvB,qBAAuB;UAAvB,uBAAuB;EACvB,qBAAqB;EACrB,0BAAqB;MAArB,qBAAqB;EACrB,yBAAmB;MAAnB,sBAAmB;UAAnB,mBAAmB;AACrB;;AAEA;EACE,6DAA6D;EAC7D,eAAe;;EAEf,0BAA0B;EAC1B,mCAAmC;EACnC,kCAAkC;EAClC,kCAAkC;EAClC,wCAAwC;AAC1C;;AAEA;EACE,eAAe;AACjB;;AAEA;EACE,eAAe;AACjB;;AAEA,iBAAiB;;AAEjB;EACE,kBAAkB;AACpB;;AAEA;EACE,qDAA6C;UAA7C,6CAA6C;AAC/C;;AAEA;EACE,sDAA8C;UAA9C,8CAA8C;AAChD;;AAEA;EACE,oDAA4C;UAA5C,4CAA4C;AAC9C","file":"vc.main.css","sourcesContent":["#visual-console-container {\n margin: 0px auto;\n position: relative;\n background-repeat: no-repeat;\n background-size: 100% 100%;\n background-position: center;\n}\n\n.visual-console-item {\n position: absolute;\n display: flex;\n flex-direction: initial;\n justify-items: center;\n align-items: center;\n user-select: text;\n}\n","@font-face {\n font-family: Alarm Clock;\n src: url(./alarm-clock.ttf);\n}\n\n/* Digital clock */\n\n.visual-console-item .digital-clock {\n display: flex;\n flex-direction: column;\n justify-content: center;\n justify-items: center;\n align-content: center;\n align-items: center;\n}\n\n.visual-console-item .digital-clock > span {\n font-family: \"Alarm Clock\", \"Courier New\", Courier, monospace;\n font-size: 50px;\n\n /* To improve legibility */\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px;\n}\n\n.visual-console-item .digital-clock > span.date {\n font-size: 25px;\n}\n\n.visual-console-item .digital-clock > span.timezone {\n font-size: 25px;\n}\n\n/* Analog clock */\n\n.visual-console-item .analogic-clock {\n text-align: center;\n}\n\n.visual-console-item .analogic-clock .hour-hand {\n animation: rotate-hour 43200s infinite linear;\n}\n\n.visual-console-item .analogic-clock .minute-hand {\n animation: rotate-minute 3600s infinite linear;\n}\n\n.visual-console-item .analogic-clock .second-hand {\n animation: rotate-second 60s infinite linear;\n}\n"],"sourceRoot":""}
{"version":3,"sources":["webpack:///main.css","webpack:///styles.css"],"names":[],"mappings":"AAAA;EACE,gBAAgB;EAChB,kBAAkB;EAClB,4BAA4B;EAC5B,0BAA0B;EAC1B,2BAA2B;AAC7B;;AAEA;EACE,kBAAkB;EAClB,oBAAa;EAAb,oBAAa;EAAb,aAAa;EACb,2BAAuB;EAAvB,8BAAuB;MAAvB,2BAAuB;UAAvB,uBAAuB;EACvB,qBAAqB;EACrB,yBAAmB;MAAnB,sBAAmB;UAAnB,mBAAmB;EACnB,yBAAiB;KAAjB,sBAAiB;MAAjB,qBAAiB;UAAjB,iBAAiB;AACnB;;AAEA;EACE,0BAA0B;EAC1B,oDAA4C;UAA5C,4CAA4C;AAC9C;;ACpBA;EACE,wBAAwB;EACxB,0BAA2B;AAC7B;;AAEA,kBAAkB;;AAElB;EACE,oBAAa;EAAb,oBAAa;EAAb,aAAa;EACb,4BAAsB;EAAtB,6BAAsB;MAAtB,0BAAsB;UAAtB,sBAAsB;EACtB,wBAAuB;MAAvB,qBAAuB;UAAvB,uBAAuB;EACvB,qBAAqB;EACrB,0BAAqB;MAArB,qBAAqB;EACrB,yBAAmB;MAAnB,sBAAmB;UAAnB,mBAAmB;AACrB;;AAEA;EACE,6DAA6D;EAC7D,eAAe;;EAEf,0BAA0B;EAC1B,mCAAmC;EACnC,kCAAkC;EAClC,kCAAkC;EAClC,wCAAwC;AAC1C;;AAEA;EACE,eAAe;AACjB;;AAEA;EACE,eAAe;AACjB;;AAEA,iBAAiB;;AAEjB;EACE,kBAAkB;AACpB;;AAEA;EACE,qDAA6C;UAA7C,6CAA6C;AAC/C;;AAEA;EACE,sDAA8C;UAA9C,8CAA8C;AAChD;;AAEA;EACE,oDAA4C;UAA5C,4CAA4C;AAC9C","file":"vc.main.css","sourcesContent":["#visual-console-container {\n margin: 0px auto;\n position: relative;\n background-repeat: no-repeat;\n background-size: 100% 100%;\n background-position: center;\n}\n\n.visual-console-item {\n position: absolute;\n display: flex;\n flex-direction: initial;\n justify-items: center;\n align-items: center;\n user-select: text;\n}\n\n.visual-console-item.is-editing {\n border: 2px dashed #33ccff;\n transform: translateX(-2px) translateY(-2px);\n}\n","@font-face {\n font-family: Alarm Clock;\n src: url(./alarm-clock.ttf);\n}\n\n/* Digital clock */\n\n.visual-console-item .digital-clock {\n display: flex;\n flex-direction: column;\n justify-content: center;\n justify-items: center;\n align-content: center;\n align-items: center;\n}\n\n.visual-console-item .digital-clock > span {\n font-family: \"Alarm Clock\", \"Courier New\", Courier, monospace;\n font-size: 50px;\n\n /* To improve legibility */\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px;\n}\n\n.visual-console-item .digital-clock > span.date {\n font-size: 25px;\n}\n\n.visual-console-item .digital-clock > span.timezone {\n font-size: 25px;\n}\n\n/* Analog clock */\n\n.visual-console-item .analogic-clock {\n text-align: center;\n}\n\n.visual-console-item .analogic-clock .hour-hand {\n animation: rotate-hour 43200s infinite linear;\n}\n\n.visual-console-item .analogic-clock .minute-hand {\n animation: rotate-minute 3600s infinite linear;\n}\n\n.visual-console-item .analogic-clock .second-hand {\n animation: rotate-second 60s infinite linear;\n}\n"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -198,6 +198,14 @@ $visualConsoleItems = VisualConsole::getItemsFromDB(
}
}
}
// Add the datetime when the item was received.
var receivedAt = new Date();
items.map(function(item) {
item["receivedAt"] = receivedAt;
return item;
});
var visualConsoleManager = createVisualConsole(
container,
props,

View File

@ -155,6 +155,16 @@ if (!is_metaconsole()) {
html_print_input_hidden('metaconsole', 1);
}
if ($pure === false) {
echo '<div class="visual-console-edit-controls">';
echo '<span>'.__('Move and resize mode').'</span>';
echo '<span>';
echo html_print_checkbox_switch('edit-mode', 1, false, true);
echo '</span>';
echo '</div>';
echo '<br />';
}
echo '<div id="visual-console-container"></div>';
if ($pure === true) {
@ -306,6 +316,14 @@ $visualConsoleItems = VisualConsole::getItemsFromDB(
}
}
}
// Add the datetime when the item was received.
var receivedAt = new Date();
items.map(function(item) {
item["receivedAt"] = receivedAt;
return item;
});
var visualConsoleManager = createVisualConsole(
container,
props,
@ -315,6 +333,17 @@ $visualConsoleItems = VisualConsole::getItemsFromDB(
handleUpdate
);
// Enable/disable the edition mode.
$('input[name=edit-mode]').change(function(event) {
if ($(this).prop('checked')) {
visualConsoleManager.visualConsole.enableEditMode();
visualConsoleManager.changeUpdateInterval(0);
} else {
visualConsoleManager.visualConsole.disableEditMode();
visualConsoleManager.changeUpdateInterval(<?php echo ($refr * 1000); ?>); // To ms.
}
});
// Update the data fetch interval.
$('select#vc-refr').change(function(event) {
var refr = Number.parseInt(event.target.value);

View File

@ -1,4 +1,10 @@
import { Position, Size, UnknownObject, WithModuleProps } from "./types";
import {
Position,
Size,
AnyObject,
WithModuleProps,
ItemMeta
} from "./lib/types";
import {
sizePropsDecoder,
positionPropsDecoder,
@ -9,7 +15,7 @@ import {
humanDate,
humanTime
} from "./lib";
import TypedEvent, { Listener, Disposable } from "./TypedEvent";
import TypedEvent, { Listener, Disposable } from "./lib/TypedEvent";
// Enum: https://www.typescriptlang.org/docs/handbook/enums.html.
export const enum ItemType {
@ -52,14 +58,14 @@ export interface ItemProps extends Position, Size {
// FIXME: Fix type compatibility.
export interface ItemClickEvent<Props extends ItemProps> {
// data: Props;
data: UnknownObject;
data: AnyObject;
nativeEvent: Event;
}
// FIXME: Fix type compatibility.
export interface ItemRemoveEvent<Props extends ItemProps> {
// data: Props;
data: UnknownObject;
data: AnyObject;
}
/**
@ -89,7 +95,7 @@ const parseLabelPosition = (
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function itemBasePropsDecoder(data: UnknownObject): ItemProps | never {
export function itemBasePropsDecoder(data: AnyObject): ItemProps | never {
if (data.id == null || isNaN(parseInt(data.id))) {
throw new TypeError("invalid id.");
}
@ -118,6 +124,8 @@ export function itemBasePropsDecoder(data: UnknownObject): ItemProps | never {
abstract class VisualConsoleItem<Props extends ItemProps> {
// Properties of the item.
private itemProps: Props;
// Metadata of the item.
private _metadata: ItemMeta;
// Reference to the DOM element which will contain the item.
public elementRef: HTMLElement;
public readonly labelElementRef: HTMLElement;
@ -138,8 +146,9 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
*/
protected abstract createDomElement(): HTMLElement;
public constructor(props: Props) {
public constructor(props: Props, metadata: ItemMeta) {
this.itemProps = props;
this._metadata = metadata;
/*
* Get a HTMLElement which represents the container box
@ -185,8 +194,14 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
box.style.zIndex = this.props.isOnTop ? "2" : "1";
box.style.left = `${this.props.x}px`;
box.style.top = `${this.props.y}px`;
box.onclick = e =>
this.clickEventManager.emit({ data: this.props, nativeEvent: e });
box.addEventListener("click", e => {
if (this.meta.editMode) {
e.preventDefault();
e.stopPropagation();
} else {
this.clickEventManager.emit({ data: this.props, nativeEvent: e });
}
});
return box;
}
@ -310,7 +325,34 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
// From this point, things which rely on this.props can access to the changes.
// Check if we should re-render.
if (this.shouldBeUpdated(prevProps, newProps)) this.render(prevProps);
if (this.shouldBeUpdated(prevProps, newProps))
this.render(prevProps, this._metadata);
}
/**
* Public accessor of the `meta` property.
* @return Properties.
*/
public get meta(): ItemMeta {
return { ...this._metadata }; // Return a copy.
}
/**
* Public setter of the `meta` property.
* If the new meta are different enough than the
* stored meta, a render would be fired.
* @param newProps
*/
public set meta(newMetadata: ItemMeta) {
const prevMetadata = this._metadata;
// Update the internal meta.
this._metadata = newMetadata;
// From this point, things which rely on this.props can access to the changes.
// Check if we should re-render.
// if (this.shouldBeUpdated(prevMetadata, newMetadata))
this.render(this.itemProps, prevMetadata);
}
/**
@ -333,7 +375,10 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
* To recreate or update the HTMLElement which represents the item into the DOM.
* @param prevProps If exists it will be used to only perform DOM updates instead of a full replace.
*/
public render(prevProps: Props | null = null): void {
public render(
prevProps: Props | null = null,
prevMeta: ItemMeta | null = null
): void {
this.updateDomElement(this.childElementRef);
// Move box.
@ -378,6 +423,29 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
// Changed the reference to the main element. It's ugly, but needed.
this.elementRef = container;
}
// Change metadata related things.
if (!prevMeta || prevMeta.editMode !== this.meta.editMode) {
if (this.meta.editMode) {
this.elementRef.classList.add("is-editing");
} else {
this.elementRef.classList.remove("is-editing");
}
}
if (!prevMeta || prevMeta.isFetching !== this.meta.isFetching) {
if (this.meta.isFetching) {
this.elementRef.classList.add("is-fetching");
} else {
this.elementRef.classList.remove("is-fetching");
}
}
if (!prevMeta || prevMeta.isUpdating !== this.meta.isUpdating) {
if (this.meta.isUpdating) {
this.elementRef.classList.add("is-updating");
} else {
this.elementRef.classList.remove("is-updating");
}
}
}
/**

View File

@ -1,9 +1,10 @@
import { UnknownObject, Size } from "./types";
import { AnyObject, Size } from "./lib/types";
import {
parseBoolean,
sizePropsDecoder,
parseIntOr,
notEmptyStringOr
notEmptyStringOr,
itemMetaDecoder
} from "./lib";
import Item, {
ItemType,
@ -24,7 +25,7 @@ import EventsHistory, {
eventsHistoryPropsDecoder
} from "./items/EventsHistory";
import Percentile, { percentilePropsDecoder } from "./items/Percentile";
import TypedEvent, { Disposable, Listener } from "./TypedEvent";
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";
@ -32,47 +33,49 @@ import Service, { servicePropsDecoder } from "./items/Service";
// TODO: Document.
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function itemInstanceFrom(data: UnknownObject) {
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));
return new StaticGraph(staticGraphPropsDecoder(data), meta);
case ItemType.MODULE_GRAPH:
return new ModuleGraph(moduleGraphPropsDecoder(data));
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));
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));
return new Percentile(percentilePropsDecoder(data), meta);
case ItemType.LABEL:
return new Label(labelPropsDecoder(data));
return new Label(labelPropsDecoder(data), meta);
case ItemType.ICON:
return new Icon(iconPropsDecoder(data));
return new Icon(iconPropsDecoder(data), meta);
case ItemType.SERVICE:
return new Service(servicePropsDecoder(data));
return new Service(servicePropsDecoder(data), meta);
case ItemType.GROUP_ITEM:
return new Group(groupPropsDecoder(data));
return new Group(groupPropsDecoder(data), meta);
case ItemType.BOX_ITEM:
return new Box(boxPropsDecoder(data));
return new Box(boxPropsDecoder(data), meta);
case ItemType.LINE_ITEM:
return new Line(linePropsDecoder(data));
return new Line(linePropsDecoder(data), meta);
case ItemType.AUTO_SLA_GRAPH:
return new EventsHistory(eventsHistoryPropsDecoder(data));
return new EventsHistory(eventsHistoryPropsDecoder(data), meta);
case ItemType.DONUT_GRAPH:
return new DonutGraph(donutGraphPropsDecoder(data));
return new DonutGraph(donutGraphPropsDecoder(data), meta);
case ItemType.BARS_GRAPH:
return new BarsGraph(barsGraphPropsDecoder(data));
return new BarsGraph(barsGraphPropsDecoder(data), meta);
case ItemType.CLOCK:
return new Clock(clockPropsDecoder(data));
return new Clock(clockPropsDecoder(data), meta);
case ItemType.COLOR_CLOUD:
return new ColorCloud(colorCloudPropsDecoder(data));
return new ColorCloud(colorCloudPropsDecoder(data), meta);
default:
throw new TypeError("item not found");
}
@ -80,7 +83,7 @@ function itemInstanceFrom(data: UnknownObject) {
// TODO: Document.
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function decodeProps(data: UnknownObject) {
function decodeProps(data: AnyObject) {
const type = parseIntOr(data.type, null);
if (type == null) throw new TypeError("missing item type.");
@ -147,7 +150,7 @@ export interface VisualConsoleProps extends Size {
* is missing from the raw object or have an invalid type.
*/
export function visualConsolePropsDecoder(
data: UnknownObject
data: AnyObject
): VisualConsoleProps | never {
// Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation
const {
@ -226,8 +229,8 @@ export default class VisualConsole {
public constructor(
container: HTMLElement,
props: UnknownObject,
items: UnknownObject[]
props: AnyObject,
items: AnyObject[]
) {
this.containerRef = container;
this._props = visualConsolePropsDecoder(props);
@ -288,13 +291,13 @@ export default class VisualConsole {
* Public setter of the `elements` property.
* @param items.
*/
public updateElements(items: UnknownObject[]): void {
const itemIds = items.map(item => item.id || null).filter(id => id != null);
itemIds as number[]; // Tell the type system to rely on us.
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: number[] = this.elementIds.filter(
id => itemIds.indexOf(id) < 0
);
const deletedIds = this.elementIds.filter(id => itemIds.indexOf(id) < 0);
// Delete the elements.
deletedIds.forEach(id => {
if (this.elementsById[id] != null) {
@ -530,6 +533,9 @@ export default class VisualConsole {
height: 0,
lineWidth: this.props.relationLineWidth,
color: "#CCCCCC"
}),
itemMetaDecoder({
receivedAt: new Date()
})
);
// Save a reference to the line item.
@ -557,4 +563,22 @@ export default class VisualConsole {
return disposable;
}
/**
* Enable the edition mode.
*/
public enableEditMode(): void {
this.elements.forEach(item => {
item.meta = { ...item.meta, editMode: true };
});
}
/**
* Disable the edition mode.
*/
public disableEditMode(): void {
this.elements.forEach(item => {
item.meta = { ...item.meta, editMode: false };
});
}
}

View File

@ -1,4 +1,4 @@
import { UnknownObject, WithModuleProps } from "../types";
import { AnyObject, WithModuleProps } from "../lib/types";
import { modulePropsDecoder, decodeBase64, stringIsEmpty } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -17,9 +17,7 @@ export type BarsGraphProps = {
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function barsGraphPropsDecoder(
data: UnknownObject
): BarsGraphProps | never {
export function barsGraphPropsDecoder(data: AnyObject): BarsGraphProps | never {
if (stringIsEmpty(data.html) && stringIsEmpty(data.encodedHtml)) {
throw new TypeError("missing html content.");
}

View File

@ -1,4 +1,4 @@
import { UnknownObject } from "../types";
import { AnyObject } from "../lib/types";
import { parseIntOr, notEmptyStringOr } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -24,7 +24,7 @@ interface BoxProps extends ItemProps {
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function boxPropsDecoder(data: UnknownObject): BoxProps | never {
export function boxPropsDecoder(data: AnyObject): BoxProps | never {
return {
...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects.
type: ItemType.BOX_ITEM,

View File

@ -1,6 +1,11 @@
import "./styles.css";
import { LinkedVisualConsoleProps, UnknownObject, Size } from "../../types";
import {
LinkedVisualConsoleProps,
AnyObject,
Size,
ItemMeta
} from "../../lib/types";
import {
linkedVCPropsDecoder,
parseIntOr,
@ -60,7 +65,7 @@ const parseClockFormat = (clockFormat: unknown): ClockProps["clockFormat"] => {
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function clockPropsDecoder(data: UnknownObject): ClockProps | never {
export function clockPropsDecoder(data: AnyObject): ClockProps | never {
if (
typeof data.clockTimezone !== "string" ||
data.clockTimezone.length === 0
@ -85,9 +90,9 @@ export default class Clock extends Item<ClockProps> {
public static readonly TICK_INTERVAL = 1000; // In ms.
private intervalRef: number | null = null;
public constructor(props: ClockProps) {
public constructor(props: ClockProps, meta: ItemMeta) {
// Call the superclass constructor.
super(props);
super(props, meta);
/* The item is already loaded and inserted into the DOM.
* The class properties are now initialized.

View File

@ -1,4 +1,5 @@
import Clock, { clockPropsDecoder } from ".";
import { itemMetaDecoder } from "../../lib";
const genericRawProps = {
id: 1,
@ -46,6 +47,9 @@ describe("Clock item", () => {
...sizeRawProps,
...linkedModuleProps,
...digitalClockProps
}),
itemMetaDecoder({
receivedAt: new Date(1)
})
);

View File

@ -1,4 +1,5 @@
import ColorCloud, { colorCloudPropsDecoder } from "./ColorCloud";
import { itemMetaDecoder } from "../lib";
const genericRawProps = {
id: 1,
@ -41,6 +42,9 @@ describe("Color cloud item", () => {
...sizeRawProps,
...linkedModuleProps,
...colorCloudProps
}),
itemMetaDecoder({
receivedAt: new Date(1)
})
);

View File

@ -1,8 +1,8 @@
import {
WithModuleProps,
LinkedVisualConsoleProps,
UnknownObject
} from "../types";
AnyObject
} from "../lib/types";
import { modulePropsDecoder, linkedVCPropsDecoder } from "../lib";
import Item, { itemBasePropsDecoder, ItemType, ItemProps } from "../Item";
@ -24,7 +24,7 @@ export type ColorCloudProps = {
* is missing from the raw object or have an invalid type.
*/
export function colorCloudPropsDecoder(
data: UnknownObject
data: AnyObject
): ColorCloudProps | never {
// TODO: Validate the color.
if (typeof data.color !== "string" || data.color.length === 0) {

View File

@ -1,8 +1,8 @@
import {
LinkedVisualConsoleProps,
UnknownObject,
AnyObject,
WithModuleProps
} from "../types";
} from "../lib/types";
import {
linkedVCPropsDecoder,
modulePropsDecoder,
@ -28,7 +28,7 @@ export type DonutGraphProps = {
* is missing from the raw object or have an invalid type.
*/
export function donutGraphPropsDecoder(
data: UnknownObject
data: AnyObject
): DonutGraphProps | never {
if (stringIsEmpty(data.html) && stringIsEmpty(data.encodedHtml)) {
throw new TypeError("missing html content.");

View File

@ -1,4 +1,4 @@
import { UnknownObject, WithModuleProps } from "../types";
import { AnyObject, WithModuleProps } from "../lib/types";
import {
modulePropsDecoder,
parseIntOr,
@ -24,7 +24,7 @@ export type EventsHistoryProps = {
* is missing from the raw object or have an invalid type.
*/
export function eventsHistoryPropsDecoder(
data: UnknownObject
data: AnyObject
): EventsHistoryProps | never {
if (stringIsEmpty(data.html) && stringIsEmpty(data.encodedHtml)) {
throw new TypeError("missing html content.");

View File

@ -1,4 +1,5 @@
import Group, { groupPropsDecoder } from "./Group";
import { itemMetaDecoder } from "../lib";
const genericRawProps = {
id: 1,
@ -33,6 +34,9 @@ describe("Group item", () => {
...positionRawProps,
...sizeRawProps,
...groupRawProps
}),
itemMetaDecoder({
receivedAt: new Date(1)
})
);

View File

@ -1,4 +1,4 @@
import { LinkedVisualConsoleProps, UnknownObject } from "../types";
import { LinkedVisualConsoleProps, AnyObject } from "../lib/types";
import {
linkedVCPropsDecoder,
parseIntOr,
@ -19,7 +19,7 @@ export type GroupProps = {
} & ItemProps &
LinkedVisualConsoleProps;
function extractHtml(data: UnknownObject): string | null {
function extractHtml(data: AnyObject): string | null {
if (!stringIsEmpty(data.html)) return data.html;
if (!stringIsEmpty(data.encodedHtml)) return decodeBase64(data.encodedHtml);
return null;
@ -34,7 +34,7 @@ function extractHtml(data: UnknownObject): string | null {
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function groupPropsDecoder(data: UnknownObject): GroupProps | never {
export function groupPropsDecoder(data: AnyObject): GroupProps | never {
if (
(typeof data.imageSrc !== "string" || data.imageSrc.length === 0) &&
data.encodedHtml === null

View File

@ -1,4 +1,4 @@
import { LinkedVisualConsoleProps, UnknownObject } from "../types";
import { LinkedVisualConsoleProps, AnyObject } from "../lib/types";
import { linkedVCPropsDecoder } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -17,7 +17,7 @@ export type IconProps = {
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function iconPropsDecoder(data: UnknownObject): IconProps | never {
export function iconPropsDecoder(data: AnyObject): IconProps | never {
if (typeof data.imageSrc !== "string" || data.imageSrc.length === 0) {
throw new TypeError("invalid image src.");
}

View File

@ -1,4 +1,4 @@
import { LinkedVisualConsoleProps, UnknownObject } from "../types";
import { LinkedVisualConsoleProps, AnyObject } from "../lib/types";
import { linkedVCPropsDecoder } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -16,7 +16,7 @@ export type LabelProps = {
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function labelPropsDecoder(data: UnknownObject): LabelProps | never {
export function labelPropsDecoder(data: AnyObject): LabelProps | never {
return {
...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects.
type: ItemType.LABEL,

View File

@ -1,4 +1,4 @@
import { UnknownObject, Position, Size } from "../types";
import { AnyObject, Position, Size, ItemMeta } from "../lib/types";
import { parseIntOr, notEmptyStringOr } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -25,7 +25,7 @@ interface LineProps extends ItemProps {
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function linePropsDecoder(data: UnknownObject): LineProps | never {
export function linePropsDecoder(data: AnyObject): LineProps | never {
const props: LineProps = {
...itemBasePropsDecoder({ ...data, width: 1, height: 1 }), // Object spread. It will merge the properties of the two objects.
type: ItemType.LINE_ITEM,
@ -71,17 +71,20 @@ export default class Line extends Item<LineProps> {
/**
* @override
*/
public constructor(props: LineProps) {
public constructor(props: LineProps, meta: ItemMeta) {
/*
* We need to override the constructor cause we need to obtain
* the
* box size and position from the start and finish points
* of the line.
*/
super({
...props,
...Line.extractBoxSizeAndPosition(props)
});
super(
{
...props,
...Line.extractBoxSizeAndPosition(props)
},
meta
);
}
/**

View File

@ -1,8 +1,8 @@
import {
LinkedVisualConsoleProps,
UnknownObject,
AnyObject,
WithModuleProps
} from "../types";
} from "../lib/types";
import {
linkedVCPropsDecoder,
modulePropsDecoder,
@ -28,7 +28,7 @@ export type ModuleGraphProps = {
* is missing from the raw object or have an invalid type.
*/
export function moduleGraphPropsDecoder(
data: UnknownObject
data: AnyObject
): ModuleGraphProps | never {
if (stringIsEmpty(data.html) && stringIsEmpty(data.encodedHtml)) {
throw new TypeError("missing html content.");

View File

@ -2,9 +2,9 @@ import { arc as arcFactory } from "d3-shape";
import {
LinkedVisualConsoleProps,
UnknownObject,
AnyObject,
WithModuleProps
} from "../types";
} from "../lib/types";
import {
linkedVCPropsDecoder,
modulePropsDecoder,
@ -81,7 +81,7 @@ function extractValueType(valueType: unknown): PercentileProps["valueType"] {
* is missing from the raw object or have an invalid type.
*/
export function percentilePropsDecoder(
data: UnknownObject
data: AnyObject
): PercentileProps | never {
return {
...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects.

View File

@ -1,4 +1,4 @@
import { UnknownObject } from "../types";
import { AnyObject } from "../lib/types";
import {
stringIsEmpty,
notEmptyStringOr,
@ -24,7 +24,7 @@ export type ServiceProps = {
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function servicePropsDecoder(data: UnknownObject): ServiceProps | never {
export function servicePropsDecoder(data: AnyObject): ServiceProps | never {
if (data.imageSrc !== null) {
if (
typeof data.statusImageSrc !== "string" ||

View File

@ -1,8 +1,8 @@
import {
LinkedVisualConsoleProps,
UnknownObject,
AnyObject,
WithModuleProps
} from "../types";
} from "../lib/types";
import {
linkedVCPropsDecoder,
parseIntOr,
@ -69,7 +69,7 @@ const parseProcessValue = (
* is missing from the raw object or have an invalid type.
*/
export function simpleValuePropsDecoder(
data: UnknownObject
data: AnyObject
): SimpleValueProps | never {
if (typeof data.value !== "string" || data.value.length === 0) {
throw new TypeError("invalid value");

View File

@ -1,8 +1,8 @@
import {
WithModuleProps,
LinkedVisualConsoleProps,
UnknownObject
} from "../types";
AnyObject
} from "../lib/types";
import {
modulePropsDecoder,
@ -47,7 +47,7 @@ const parseShowLastValueTooltip = (
* is missing from the raw object or have an invalid type.
*/
export function staticGraphPropsDecoder(
data: UnknownObject
data: AnyObject
): StaticGraphProps | never {
if (typeof data.imageSrc !== "string" || data.imageSrc.length === 0) {
throw new TypeError("invalid image src.");

View File

@ -1,4 +1,4 @@
import TypedEvent, { Disposable, Listener } from "../TypedEvent";
import TypedEvent, { Disposable, Listener } from "./TypedEvent";
interface Cancellable {
cancel(): void;

View File

@ -1,12 +1,14 @@
import {
UnknownObject,
AnyObject,
Position,
Size,
WithAgentProps,
WithModuleProps,
LinkedVisualConsoleProps,
LinkedVisualConsolePropsStatus
} from "../types";
LinkedVisualConsolePropsStatus,
UnknownObject,
ItemMeta
} from "./types";
/**
* Return a number or a default value from a raw value.
@ -72,6 +74,23 @@ export function parseBoolean(value: unknown): boolean {
else return false;
}
/**
* Return a valid date or a default value from a raw value.
* @param value Raw value from which we will try to extract a valid date.
* @param defaultValue Default value to use if we cannot extract a valid date.
* @return A valid date or the default value.
*/
export function parseDateOr<T>(value: unknown, defaultValue: T): Date | T {
if (value instanceof Date) return value;
else if (typeof value === "number") return new Date(value * 1000);
else if (
typeof value === "string" &&
!Number.isNaN(new Date(value).getTime())
)
return new Date(value);
else return defaultValue;
}
/**
* Pad the current string with another string (multiple times, if needed)
* until the resulting string reaches the given length.
@ -113,7 +132,7 @@ export function leftPad(
* @param data Raw object.
* @return An object representing the position.
*/
export function positionPropsDecoder(data: UnknownObject): Position {
export function positionPropsDecoder(data: AnyObject): Position {
return {
x: parseIntOr(data.x, 0),
y: parseIntOr(data.y, 0)
@ -126,7 +145,7 @@ export function positionPropsDecoder(data: UnknownObject): Position {
* @return An object representing the size.
* @throws Will throw a TypeError if the width and height are not valid numbers.
*/
export function sizePropsDecoder(data: UnknownObject): Size | never {
export function sizePropsDecoder(data: AnyObject): Size | never {
if (
data.width == null ||
isNaN(parseInt(data.width)) ||
@ -147,7 +166,7 @@ export function sizePropsDecoder(data: UnknownObject): Size | never {
* @param data Raw object.
* @return An object representing the agent properties.
*/
export function agentPropsDecoder(data: UnknownObject): WithAgentProps {
export function agentPropsDecoder(data: AnyObject): WithAgentProps {
const agentProps: WithAgentProps = {
agentId: parseIntOr(data.agent, null),
agentName: notEmptyStringOr(data.agentName, null),
@ -169,7 +188,7 @@ export function agentPropsDecoder(data: UnknownObject): WithAgentProps {
* @param data Raw object.
* @return An object representing the module and agent properties.
*/
export function modulePropsDecoder(data: UnknownObject): WithModuleProps {
export function modulePropsDecoder(data: AnyObject): WithModuleProps {
return {
moduleId: parseIntOr(data.moduleId, null),
moduleName: notEmptyStringOr(data.moduleName, null),
@ -185,7 +204,7 @@ export function modulePropsDecoder(data: UnknownObject): WithModuleProps {
* @throws Will throw a TypeError if the status calculation properties are invalid.
*/
export function linkedVCPropsDecoder(
data: UnknownObject
data: AnyObject
): LinkedVisualConsoleProps | never {
// Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation
const {
@ -246,6 +265,29 @@ export function linkedVCPropsDecoder(
: linkedLayoutBaseProps;
}
/**
* Build a valid typed object from a raw object.
* @param data Raw object.
* @return An object representing the item's meta properties.
*/
export function itemMetaDecoder(data: UnknownObject): ItemMeta | never {
const receivedAt = parseDateOr(data.receivedAt, null);
if (receivedAt === null) throw new TypeError("invalid meta structure");
let error = null;
if (data.error instanceof Error) error = data.error;
else if (typeof data.error === "string") error = new Error(data.error);
return {
receivedAt,
error,
editMode: parseBoolean(data.editMode),
isFromCache: parseBoolean(data.isFromCache),
isFetching: false,
isUpdating: false
};
}
/**
* To get a CSS rule with the most used prefixes.
* @param ruleName Name of the CSS rule.

View File

@ -7,7 +7,8 @@ import {
decodeBase64,
humanDate,
humanTime,
replaceMacros
replaceMacros,
itemMetaDecoder
} from ".";
describe("function parseIntOr", () => {
@ -72,14 +73,14 @@ describe("function prefixedCssRules", () => {
describe("function decodeBase64", () => {
it("should decode the base64 without errors", () => {
expect(decodeBase64("SGkgSSdtIGRlY29kZWQ=")).toEqual("Hi I'm decoded");
expect(decodeBase64("Rk9PQkFSQkFa")).toEqual("FOOBARBAZ");
expect(decodeBase64("eyJpZCI6MSwibmFtZSI6ImZvbyJ9")).toEqual(
expect(decodeBase64("SGkgSSdtIGRlY29kZWQ=")).toBe("Hi I'm decoded");
expect(decodeBase64("Rk9PQkFSQkFa")).toBe("FOOBARBAZ");
expect(decodeBase64("eyJpZCI6MSwibmFtZSI6ImZvbyJ9")).toBe(
'{"id":1,"name":"foo"}'
);
expect(
decodeBase64("PGRpdj5Cb3ggPHA+UGFyYWdyYXBoPC9wPjxociAvPjwvZGl2Pg==")
).toEqual("<div>Box <p>Paragraph</p><hr /></div>");
).toBe("<div>Box <p>Paragraph</p><hr /></div>");
});
});
@ -118,3 +119,46 @@ describe("replaceMacros function", () => {
expect(replaceMacros(macros, text)).toBe("Lorem foo Ipsum baz");
});
});
describe("itemMetaDecoder function", () => {
it("should extract a default meta object", () => {
expect(
itemMetaDecoder({
receivedAt: 1
})
).toEqual({
receivedAt: new Date(1000),
error: null,
isFromCache: false,
isFetching: false,
isUpdating: false,
editMode: false
});
});
it("should extract a valid meta object", () => {
expect(
itemMetaDecoder({
receivedAt: new Date(1000),
error: new Error("foo"),
editMode: 1
})
).toEqual({
receivedAt: new Date(1000),
error: new Error("foo"),
isFromCache: false,
isFetching: false,
isUpdating: false,
editMode: true
});
});
it("should fail when a invalid structure is used", () => {
expect(() => itemMetaDecoder({})).toThrowError(TypeError);
expect(() =>
itemMetaDecoder({
receivedAt: "foo"
})
).toThrowError(TypeError);
});
});

View File

@ -1,7 +1,11 @@
export interface UnknownObject {
export interface AnyObject {
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface UnknownObject {
[key: string]: unknown;
}
export interface Position {
x: number;
y: number;
@ -45,3 +49,12 @@ export type LinkedVisualConsoleProps = {
linkedLayoutId: number | null;
linkedLayoutAgentId: number | null;
} & LinkedVisualConsolePropsStatus;
export interface ItemMeta {
receivedAt: Date;
error: Error | null;
isFromCache: boolean;
isFetching: boolean;
isUpdating: boolean;
editMode: boolean;
}

View File

@ -14,3 +14,8 @@
align-items: center;
user-select: text;
}
.visual-console-item.is-editing {
border: 2px dashed #33ccff;
transform: translateX(-2px) translateY(-2px);
}