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) ? JSON.parse(data.items)
: 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; var prevProps = visualConsole.props;
// Update the data structure. // Update the data structure.
visualConsole.props = props; visualConsole.props = props;

View File

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

View File

@ -25,6 +25,12 @@
user-select: text; 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-face {
font-family: Alarm Clock; font-family: Alarm Clock;
src: url(alarm-clock.ttf); 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( var visualConsoleManager = createVisualConsole(
container, container,
props, props,

View File

@ -155,6 +155,16 @@ if (!is_metaconsole()) {
html_print_input_hidden('metaconsole', 1); 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>'; echo '<div id="visual-console-container"></div>';
if ($pure === true) { 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( var visualConsoleManager = createVisualConsole(
container, container,
props, props,
@ -315,6 +333,17 @@ $visualConsoleItems = VisualConsole::getItemsFromDB(
handleUpdate 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. // Update the data fetch interval.
$('select#vc-refr').change(function(event) { $('select#vc-refr').change(function(event) {
var refr = Number.parseInt(event.target.value); 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 { import {
sizePropsDecoder, sizePropsDecoder,
positionPropsDecoder, positionPropsDecoder,
@ -9,7 +15,7 @@ import {
humanDate, humanDate,
humanTime humanTime
} from "./lib"; } from "./lib";
import TypedEvent, { Listener, Disposable } from "./TypedEvent"; import TypedEvent, { Listener, Disposable } from "./lib/TypedEvent";
// Enum: https://www.typescriptlang.org/docs/handbook/enums.html. // Enum: https://www.typescriptlang.org/docs/handbook/enums.html.
export const enum ItemType { export const enum ItemType {
@ -52,14 +58,14 @@ export interface ItemProps extends Position, Size {
// FIXME: Fix type compatibility. // FIXME: Fix type compatibility.
export interface ItemClickEvent<Props extends ItemProps> { export interface ItemClickEvent<Props extends ItemProps> {
// data: Props; // data: Props;
data: UnknownObject; data: AnyObject;
nativeEvent: Event; nativeEvent: Event;
} }
// FIXME: Fix type compatibility. // FIXME: Fix type compatibility.
export interface ItemRemoveEvent<Props extends ItemProps> { export interface ItemRemoveEvent<Props extends ItemProps> {
// data: Props; // data: Props;
data: UnknownObject; data: AnyObject;
} }
/** /**
@ -89,7 +95,7 @@ const parseLabelPosition = (
* @throws Will throw a TypeError if some property * @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type. * 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))) { if (data.id == null || isNaN(parseInt(data.id))) {
throw new TypeError("invalid id."); throw new TypeError("invalid id.");
} }
@ -118,6 +124,8 @@ export function itemBasePropsDecoder(data: UnknownObject): ItemProps | never {
abstract class VisualConsoleItem<Props extends ItemProps> { abstract class VisualConsoleItem<Props extends ItemProps> {
// Properties of the item. // Properties of the item.
private itemProps: Props; private itemProps: Props;
// Metadata of the item.
private _metadata: ItemMeta;
// Reference to the DOM element which will contain the item. // Reference to the DOM element which will contain the item.
public elementRef: HTMLElement; public elementRef: HTMLElement;
public readonly labelElementRef: HTMLElement; public readonly labelElementRef: HTMLElement;
@ -138,8 +146,9 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
*/ */
protected abstract createDomElement(): HTMLElement; protected abstract createDomElement(): HTMLElement;
public constructor(props: Props) { public constructor(props: Props, metadata: ItemMeta) {
this.itemProps = props; this.itemProps = props;
this._metadata = metadata;
/* /*
* Get a HTMLElement which represents the container box * 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.zIndex = this.props.isOnTop ? "2" : "1";
box.style.left = `${this.props.x}px`; box.style.left = `${this.props.x}px`;
box.style.top = `${this.props.y}px`; box.style.top = `${this.props.y}px`;
box.onclick = e => box.addEventListener("click", e => {
if (this.meta.editMode) {
e.preventDefault();
e.stopPropagation();
} else {
this.clickEventManager.emit({ data: this.props, nativeEvent: e }); this.clickEventManager.emit({ data: this.props, nativeEvent: e });
}
});
return box; 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. // From this point, things which rely on this.props can access to the changes.
// Check if we should re-render. // 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. * 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. * @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); this.updateDomElement(this.childElementRef);
// Move box. // Move box.
@ -378,6 +423,29 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
// Changed the reference to the main element. It's ugly, but needed. // Changed the reference to the main element. It's ugly, but needed.
this.elementRef = container; 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 { import {
parseBoolean, parseBoolean,
sizePropsDecoder, sizePropsDecoder,
parseIntOr, parseIntOr,
notEmptyStringOr notEmptyStringOr,
itemMetaDecoder
} from "./lib"; } from "./lib";
import Item, { import Item, {
ItemType, ItemType,
@ -24,7 +25,7 @@ import EventsHistory, {
eventsHistoryPropsDecoder eventsHistoryPropsDecoder
} from "./items/EventsHistory"; } from "./items/EventsHistory";
import Percentile, { percentilePropsDecoder } from "./items/Percentile"; 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 DonutGraph, { donutGraphPropsDecoder } from "./items/DonutGraph";
import BarsGraph, { barsGraphPropsDecoder } from "./items/BarsGraph"; import BarsGraph, { barsGraphPropsDecoder } from "./items/BarsGraph";
import ModuleGraph, { moduleGraphPropsDecoder } from "./items/ModuleGraph"; import ModuleGraph, { moduleGraphPropsDecoder } from "./items/ModuleGraph";
@ -32,47 +33,49 @@ import Service, { servicePropsDecoder } from "./items/Service";
// TODO: Document. // TODO: Document.
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function itemInstanceFrom(data: UnknownObject) { function itemInstanceFrom(data: AnyObject) {
const type = parseIntOr(data.type, null); const type = parseIntOr(data.type, null);
if (type == null) throw new TypeError("missing item type."); if (type == null) throw new TypeError("missing item type.");
const meta = itemMetaDecoder(data);
switch (type as ItemType) { switch (type as ItemType) {
case ItemType.STATIC_GRAPH: case ItemType.STATIC_GRAPH:
return new StaticGraph(staticGraphPropsDecoder(data)); return new StaticGraph(staticGraphPropsDecoder(data), meta);
case ItemType.MODULE_GRAPH: case ItemType.MODULE_GRAPH:
return new ModuleGraph(moduleGraphPropsDecoder(data)); return new ModuleGraph(moduleGraphPropsDecoder(data), meta);
case ItemType.SIMPLE_VALUE: case ItemType.SIMPLE_VALUE:
case ItemType.SIMPLE_VALUE_MAX: case ItemType.SIMPLE_VALUE_MAX:
case ItemType.SIMPLE_VALUE_MIN: case ItemType.SIMPLE_VALUE_MIN:
case ItemType.SIMPLE_VALUE_AVG: case ItemType.SIMPLE_VALUE_AVG:
return new SimpleValue(simpleValuePropsDecoder(data)); return new SimpleValue(simpleValuePropsDecoder(data), meta);
case ItemType.PERCENTILE_BAR: case ItemType.PERCENTILE_BAR:
case ItemType.PERCENTILE_BUBBLE: case ItemType.PERCENTILE_BUBBLE:
case ItemType.CIRCULAR_PROGRESS_BAR: case ItemType.CIRCULAR_PROGRESS_BAR:
case ItemType.CIRCULAR_INTERIOR_PROGRESS_BAR: case ItemType.CIRCULAR_INTERIOR_PROGRESS_BAR:
return new Percentile(percentilePropsDecoder(data)); return new Percentile(percentilePropsDecoder(data), meta);
case ItemType.LABEL: case ItemType.LABEL:
return new Label(labelPropsDecoder(data)); return new Label(labelPropsDecoder(data), meta);
case ItemType.ICON: case ItemType.ICON:
return new Icon(iconPropsDecoder(data)); return new Icon(iconPropsDecoder(data), meta);
case ItemType.SERVICE: case ItemType.SERVICE:
return new Service(servicePropsDecoder(data)); return new Service(servicePropsDecoder(data), meta);
case ItemType.GROUP_ITEM: case ItemType.GROUP_ITEM:
return new Group(groupPropsDecoder(data)); return new Group(groupPropsDecoder(data), meta);
case ItemType.BOX_ITEM: case ItemType.BOX_ITEM:
return new Box(boxPropsDecoder(data)); return new Box(boxPropsDecoder(data), meta);
case ItemType.LINE_ITEM: case ItemType.LINE_ITEM:
return new Line(linePropsDecoder(data)); return new Line(linePropsDecoder(data), meta);
case ItemType.AUTO_SLA_GRAPH: case ItemType.AUTO_SLA_GRAPH:
return new EventsHistory(eventsHistoryPropsDecoder(data)); return new EventsHistory(eventsHistoryPropsDecoder(data), meta);
case ItemType.DONUT_GRAPH: case ItemType.DONUT_GRAPH:
return new DonutGraph(donutGraphPropsDecoder(data)); return new DonutGraph(donutGraphPropsDecoder(data), meta);
case ItemType.BARS_GRAPH: case ItemType.BARS_GRAPH:
return new BarsGraph(barsGraphPropsDecoder(data)); return new BarsGraph(barsGraphPropsDecoder(data), meta);
case ItemType.CLOCK: case ItemType.CLOCK:
return new Clock(clockPropsDecoder(data)); return new Clock(clockPropsDecoder(data), meta);
case ItemType.COLOR_CLOUD: case ItemType.COLOR_CLOUD:
return new ColorCloud(colorCloudPropsDecoder(data)); return new ColorCloud(colorCloudPropsDecoder(data), meta);
default: default:
throw new TypeError("item not found"); throw new TypeError("item not found");
} }
@ -80,7 +83,7 @@ function itemInstanceFrom(data: UnknownObject) {
// TODO: Document. // TODO: Document.
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function decodeProps(data: UnknownObject) { function decodeProps(data: AnyObject) {
const type = parseIntOr(data.type, null); const type = parseIntOr(data.type, null);
if (type == null) throw new TypeError("missing item type."); 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. * is missing from the raw object or have an invalid type.
*/ */
export function visualConsolePropsDecoder( export function visualConsolePropsDecoder(
data: UnknownObject data: AnyObject
): VisualConsoleProps | never { ): VisualConsoleProps | never {
// Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation // Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation
const { const {
@ -226,8 +229,8 @@ export default class VisualConsole {
public constructor( public constructor(
container: HTMLElement, container: HTMLElement,
props: UnknownObject, props: AnyObject,
items: UnknownObject[] items: AnyObject[]
) { ) {
this.containerRef = container; this.containerRef = container;
this._props = visualConsolePropsDecoder(props); this._props = visualConsolePropsDecoder(props);
@ -288,13 +291,13 @@ export default class VisualConsole {
* Public setter of the `elements` property. * Public setter of the `elements` property.
* @param items. * @param items.
*/ */
public updateElements(items: UnknownObject[]): void { public updateElements(items: AnyObject[]): void {
const itemIds = items.map(item => item.id || null).filter(id => id != null); // Ensure the type cause Typescript doesn't know the filter removes null items.
itemIds as number[]; // Tell the type system to rely on us. const itemIds = items
.map(item => item.id || null)
.filter(id => id != null) as number[];
// Get the elements we should delete. // Get the elements we should delete.
const deletedIds: number[] = this.elementIds.filter( const deletedIds = this.elementIds.filter(id => itemIds.indexOf(id) < 0);
id => itemIds.indexOf(id) < 0
);
// Delete the elements. // Delete the elements.
deletedIds.forEach(id => { deletedIds.forEach(id => {
if (this.elementsById[id] != null) { if (this.elementsById[id] != null) {
@ -530,6 +533,9 @@ export default class VisualConsole {
height: 0, height: 0,
lineWidth: this.props.relationLineWidth, lineWidth: this.props.relationLineWidth,
color: "#CCCCCC" color: "#CCCCCC"
}),
itemMetaDecoder({
receivedAt: new Date()
}) })
); );
// Save a reference to the line item. // Save a reference to the line item.
@ -557,4 +563,22 @@ export default class VisualConsole {
return disposable; 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 { modulePropsDecoder, decodeBase64, stringIsEmpty } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item"; import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -17,9 +17,7 @@ export type BarsGraphProps = {
* @throws Will throw a TypeError if some property * @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type. * is missing from the raw object or have an invalid type.
*/ */
export function barsGraphPropsDecoder( export function barsGraphPropsDecoder(data: AnyObject): BarsGraphProps | never {
data: UnknownObject
): BarsGraphProps | never {
if (stringIsEmpty(data.html) && stringIsEmpty(data.encodedHtml)) { if (stringIsEmpty(data.html) && stringIsEmpty(data.encodedHtml)) {
throw new TypeError("missing html content."); 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 { parseIntOr, notEmptyStringOr } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item"; import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -24,7 +24,7 @@ interface BoxProps extends ItemProps {
* @throws Will throw a TypeError if some property * @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type. * 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 { return {
...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects. ...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects.
type: ItemType.BOX_ITEM, type: ItemType.BOX_ITEM,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { LinkedVisualConsoleProps, UnknownObject } from "../types"; import { LinkedVisualConsoleProps, AnyObject } from "../lib/types";
import { import {
linkedVCPropsDecoder, linkedVCPropsDecoder,
parseIntOr, parseIntOr,
@ -19,7 +19,7 @@ export type GroupProps = {
} & ItemProps & } & ItemProps &
LinkedVisualConsoleProps; LinkedVisualConsoleProps;
function extractHtml(data: UnknownObject): string | null { function extractHtml(data: AnyObject): string | null {
if (!stringIsEmpty(data.html)) return data.html; if (!stringIsEmpty(data.html)) return data.html;
if (!stringIsEmpty(data.encodedHtml)) return decodeBase64(data.encodedHtml); if (!stringIsEmpty(data.encodedHtml)) return decodeBase64(data.encodedHtml);
return null; return null;
@ -34,7 +34,7 @@ function extractHtml(data: UnknownObject): string | null {
* @throws Will throw a TypeError if some property * @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type. * 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 ( if (
(typeof data.imageSrc !== "string" || data.imageSrc.length === 0) && (typeof data.imageSrc !== "string" || data.imageSrc.length === 0) &&
data.encodedHtml === null 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 { linkedVCPropsDecoder } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item"; import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -17,7 +17,7 @@ export type IconProps = {
* @throws Will throw a TypeError if some property * @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type. * 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) { if (typeof data.imageSrc !== "string" || data.imageSrc.length === 0) {
throw new TypeError("invalid image src."); 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 { linkedVCPropsDecoder } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item"; import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -16,7 +16,7 @@ export type LabelProps = {
* @throws Will throw a TypeError if some property * @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type. * 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 { return {
...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects. ...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects.
type: ItemType.LABEL, 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 { parseIntOr, notEmptyStringOr } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item"; import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
@ -25,7 +25,7 @@ interface LineProps extends ItemProps {
* @throws Will throw a TypeError if some property * @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type. * 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 = { const props: LineProps = {
...itemBasePropsDecoder({ ...data, width: 1, height: 1 }), // Object spread. It will merge the properties of the two objects. ...itemBasePropsDecoder({ ...data, width: 1, height: 1 }), // Object spread. It will merge the properties of the two objects.
type: ItemType.LINE_ITEM, type: ItemType.LINE_ITEM,
@ -71,17 +71,20 @@ export default class Line extends Item<LineProps> {
/** /**
* @override * @override
*/ */
public constructor(props: LineProps) { public constructor(props: LineProps, meta: ItemMeta) {
/* /*
* We need to override the constructor cause we need to obtain * We need to override the constructor cause we need to obtain
* the * the
* box size and position from the start and finish points * box size and position from the start and finish points
* of the line. * of the line.
*/ */
super({ super(
{
...props, ...props,
...Line.extractBoxSizeAndPosition(props) ...Line.extractBoxSizeAndPosition(props)
}); },
meta
);
} }
/** /**

View File

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

View File

@ -2,9 +2,9 @@ import { arc as arcFactory } from "d3-shape";
import { import {
LinkedVisualConsoleProps, LinkedVisualConsoleProps,
UnknownObject, AnyObject,
WithModuleProps WithModuleProps
} from "../types"; } from "../lib/types";
import { import {
linkedVCPropsDecoder, linkedVCPropsDecoder,
modulePropsDecoder, modulePropsDecoder,
@ -81,7 +81,7 @@ function extractValueType(valueType: unknown): PercentileProps["valueType"] {
* is missing from the raw object or have an invalid type. * is missing from the raw object or have an invalid type.
*/ */
export function percentilePropsDecoder( export function percentilePropsDecoder(
data: UnknownObject data: AnyObject
): PercentileProps | never { ): PercentileProps | never {
return { return {
...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects. ...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 { import {
stringIsEmpty, stringIsEmpty,
notEmptyStringOr, notEmptyStringOr,
@ -24,7 +24,7 @@ export type ServiceProps = {
* @throws Will throw a TypeError if some property * @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type. * 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 (data.imageSrc !== null) {
if ( if (
typeof data.statusImageSrc !== "string" || typeof data.statusImageSrc !== "string" ||

View File

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

View File

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

View File

@ -1,12 +1,14 @@
import { import {
UnknownObject, AnyObject,
Position, Position,
Size, Size,
WithAgentProps, WithAgentProps,
WithModuleProps, WithModuleProps,
LinkedVisualConsoleProps, LinkedVisualConsoleProps,
LinkedVisualConsolePropsStatus LinkedVisualConsolePropsStatus,
} from "../types"; UnknownObject,
ItemMeta
} from "./types";
/** /**
* Return a number or a default value from a raw value. * Return a number or a default value from a raw value.
@ -72,6 +74,23 @@ export function parseBoolean(value: unknown): boolean {
else return false; 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) * Pad the current string with another string (multiple times, if needed)
* until the resulting string reaches the given length. * until the resulting string reaches the given length.
@ -113,7 +132,7 @@ export function leftPad(
* @param data Raw object. * @param data Raw object.
* @return An object representing the position. * @return An object representing the position.
*/ */
export function positionPropsDecoder(data: UnknownObject): Position { export function positionPropsDecoder(data: AnyObject): Position {
return { return {
x: parseIntOr(data.x, 0), x: parseIntOr(data.x, 0),
y: parseIntOr(data.y, 0) y: parseIntOr(data.y, 0)
@ -126,7 +145,7 @@ export function positionPropsDecoder(data: UnknownObject): Position {
* @return An object representing the size. * @return An object representing the size.
* @throws Will throw a TypeError if the width and height are not valid numbers. * @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 ( if (
data.width == null || data.width == null ||
isNaN(parseInt(data.width)) || isNaN(parseInt(data.width)) ||
@ -147,7 +166,7 @@ export function sizePropsDecoder(data: UnknownObject): Size | never {
* @param data Raw object. * @param data Raw object.
* @return An object representing the agent properties. * @return An object representing the agent properties.
*/ */
export function agentPropsDecoder(data: UnknownObject): WithAgentProps { export function agentPropsDecoder(data: AnyObject): WithAgentProps {
const agentProps: WithAgentProps = { const agentProps: WithAgentProps = {
agentId: parseIntOr(data.agent, null), agentId: parseIntOr(data.agent, null),
agentName: notEmptyStringOr(data.agentName, null), agentName: notEmptyStringOr(data.agentName, null),
@ -169,7 +188,7 @@ export function agentPropsDecoder(data: UnknownObject): WithAgentProps {
* @param data Raw object. * @param data Raw object.
* @return An object representing the module and agent properties. * @return An object representing the module and agent properties.
*/ */
export function modulePropsDecoder(data: UnknownObject): WithModuleProps { export function modulePropsDecoder(data: AnyObject): WithModuleProps {
return { return {
moduleId: parseIntOr(data.moduleId, null), moduleId: parseIntOr(data.moduleId, null),
moduleName: notEmptyStringOr(data.moduleName, 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. * @throws Will throw a TypeError if the status calculation properties are invalid.
*/ */
export function linkedVCPropsDecoder( export function linkedVCPropsDecoder(
data: UnknownObject data: AnyObject
): LinkedVisualConsoleProps | never { ): LinkedVisualConsoleProps | never {
// Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation // Object destructuring: http://es6-features.org/#ObjectMatchingShorthandNotation
const { const {
@ -246,6 +265,29 @@ export function linkedVCPropsDecoder(
: linkedLayoutBaseProps; : 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. * To get a CSS rule with the most used prefixes.
* @param ruleName Name of the CSS rule. * @param ruleName Name of the CSS rule.

View File

@ -7,7 +7,8 @@ import {
decodeBase64, decodeBase64,
humanDate, humanDate,
humanTime, humanTime,
replaceMacros replaceMacros,
itemMetaDecoder
} from "."; } from ".";
describe("function parseIntOr", () => { describe("function parseIntOr", () => {
@ -72,14 +73,14 @@ describe("function prefixedCssRules", () => {
describe("function decodeBase64", () => { describe("function decodeBase64", () => {
it("should decode the base64 without errors", () => { it("should decode the base64 without errors", () => {
expect(decodeBase64("SGkgSSdtIGRlY29kZWQ=")).toEqual("Hi I'm decoded"); expect(decodeBase64("SGkgSSdtIGRlY29kZWQ=")).toBe("Hi I'm decoded");
expect(decodeBase64("Rk9PQkFSQkFa")).toEqual("FOOBARBAZ"); expect(decodeBase64("Rk9PQkFSQkFa")).toBe("FOOBARBAZ");
expect(decodeBase64("eyJpZCI6MSwibmFtZSI6ImZvbyJ9")).toEqual( expect(decodeBase64("eyJpZCI6MSwibmFtZSI6ImZvbyJ9")).toBe(
'{"id":1,"name":"foo"}' '{"id":1,"name":"foo"}'
); );
expect( expect(
decodeBase64("PGRpdj5Cb3ggPHA+UGFyYWdyYXBoPC9wPjxociAvPjwvZGl2Pg==") 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"); 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 [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
} }
export interface UnknownObject {
[key: string]: unknown;
}
export interface Position { export interface Position {
x: number; x: number;
y: number; y: number;
@ -45,3 +49,12 @@ export type LinkedVisualConsoleProps = {
linkedLayoutId: number | null; linkedLayoutId: number | null;
linkedLayoutAgentId: number | null; linkedLayoutAgentId: number | null;
} & LinkedVisualConsolePropsStatus; } & 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; align-items: center;
user-select: text; user-select: text;
} }
.visual-console-item.is-editing {
border: 2px dashed #33ccff;
transform: translateX(-2px) translateY(-2px);
}