Added a new metadata structure to the visual console client items

This commit is contained in:
Alejandro Gallardo Escobar 2019-06-04 13:17:19 +02:00
parent 5872a3ed56
commit 75718a85a4
25 changed files with 287 additions and 100 deletions

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,10 @@ 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 => {
this.clickEventManager.emit({ data: this.props, nativeEvent: e }); if (!this.meta.editMode)
this.clickEventManager.emit({ data: this.props, nativeEvent: e });
});
return box; return box;
} }
@ -310,7 +321,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 +371,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 +419,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.

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, {
...Line.extractBoxSizeAndPosition(props) ...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;
}