diff --git a/visual_console_client/src/Item.ts b/visual_console_client/src/Item.ts index 7ad0f779d6..e09017d6df 100644 --- a/visual_console_client/src/Item.ts +++ b/visual_console_client/src/Item.ts @@ -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 { // data: Props; - data: UnknownObject; + data: AnyObject; nativeEvent: Event; } // FIXME: Fix type compatibility. export interface ItemRemoveEvent { // 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 { // 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 { */ 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,10 @@ abstract class VisualConsoleItem { 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) + this.clickEventManager.emit({ data: this.props, nativeEvent: e }); + }); return box; } @@ -310,7 +321,34 @@ abstract class VisualConsoleItem { // 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 +371,10 @@ abstract class VisualConsoleItem { * 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 +419,29 @@ abstract class VisualConsoleItem { // 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"); + } + } } /** diff --git a/visual_console_client/src/VisualConsole.ts b/visual_console_client/src/VisualConsole.ts index 7b45ccc2f1..59971fdff7 100644 --- a/visual_console_client/src/VisualConsole.ts +++ b/visual_console_client/src/VisualConsole.ts @@ -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. diff --git a/visual_console_client/src/items/BarsGraph.ts b/visual_console_client/src/items/BarsGraph.ts index d1a6fe97a5..7fcde89177 100644 --- a/visual_console_client/src/items/BarsGraph.ts +++ b/visual_console_client/src/items/BarsGraph.ts @@ -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."); } diff --git a/visual_console_client/src/items/Box.ts b/visual_console_client/src/items/Box.ts index ca3078c0a6..042621c5a6 100644 --- a/visual_console_client/src/items/Box.ts +++ b/visual_console_client/src/items/Box.ts @@ -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, diff --git a/visual_console_client/src/items/Clock/index.ts b/visual_console_client/src/items/Clock/index.ts index 52e0e65775..e775c00102 100644 --- a/visual_console_client/src/items/Clock/index.ts +++ b/visual_console_client/src/items/Clock/index.ts @@ -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 { 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. diff --git a/visual_console_client/src/items/Clock/spec.ts b/visual_console_client/src/items/Clock/spec.ts index 7380f98468..acd4233ed1 100644 --- a/visual_console_client/src/items/Clock/spec.ts +++ b/visual_console_client/src/items/Clock/spec.ts @@ -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) }) ); diff --git a/visual_console_client/src/items/ColorCloud.spec.ts b/visual_console_client/src/items/ColorCloud.spec.ts index 103850b04b..fb873b8894 100644 --- a/visual_console_client/src/items/ColorCloud.spec.ts +++ b/visual_console_client/src/items/ColorCloud.spec.ts @@ -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) }) ); diff --git a/visual_console_client/src/items/ColorCloud.ts b/visual_console_client/src/items/ColorCloud.ts index 0b5dfe9948..ced87424d9 100644 --- a/visual_console_client/src/items/ColorCloud.ts +++ b/visual_console_client/src/items/ColorCloud.ts @@ -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) { diff --git a/visual_console_client/src/items/DonutGraph.ts b/visual_console_client/src/items/DonutGraph.ts index d60f268567..c6436583c4 100644 --- a/visual_console_client/src/items/DonutGraph.ts +++ b/visual_console_client/src/items/DonutGraph.ts @@ -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."); diff --git a/visual_console_client/src/items/EventsHistory.ts b/visual_console_client/src/items/EventsHistory.ts index 7e9db0ae9b..1d460e5b5b 100644 --- a/visual_console_client/src/items/EventsHistory.ts +++ b/visual_console_client/src/items/EventsHistory.ts @@ -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."); diff --git a/visual_console_client/src/items/Group.spec.ts b/visual_console_client/src/items/Group.spec.ts index e00a5cf327..c117a95b42 100644 --- a/visual_console_client/src/items/Group.spec.ts +++ b/visual_console_client/src/items/Group.spec.ts @@ -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) }) ); diff --git a/visual_console_client/src/items/Group.ts b/visual_console_client/src/items/Group.ts index fa97f69a62..98552a0f1b 100644 --- a/visual_console_client/src/items/Group.ts +++ b/visual_console_client/src/items/Group.ts @@ -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 diff --git a/visual_console_client/src/items/Icon.ts b/visual_console_client/src/items/Icon.ts index 12c1b035c7..d6e4a21fc6 100644 --- a/visual_console_client/src/items/Icon.ts +++ b/visual_console_client/src/items/Icon.ts @@ -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."); } diff --git a/visual_console_client/src/items/Label.ts b/visual_console_client/src/items/Label.ts index c8de572c15..4f6a382a08 100644 --- a/visual_console_client/src/items/Label.ts +++ b/visual_console_client/src/items/Label.ts @@ -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, diff --git a/visual_console_client/src/items/Line.ts b/visual_console_client/src/items/Line.ts index 20532e44ea..8735279a9b 100644 --- a/visual_console_client/src/items/Line.ts +++ b/visual_console_client/src/items/Line.ts @@ -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 { /** * @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 + ); } /** diff --git a/visual_console_client/src/items/ModuleGraph.ts b/visual_console_client/src/items/ModuleGraph.ts index 3440496d19..ce23785d36 100644 --- a/visual_console_client/src/items/ModuleGraph.ts +++ b/visual_console_client/src/items/ModuleGraph.ts @@ -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."); diff --git a/visual_console_client/src/items/Percentile.ts b/visual_console_client/src/items/Percentile.ts index 4c93b86b1f..0706f55ed9 100644 --- a/visual_console_client/src/items/Percentile.ts +++ b/visual_console_client/src/items/Percentile.ts @@ -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. diff --git a/visual_console_client/src/items/Service.ts b/visual_console_client/src/items/Service.ts index 9440503e80..09a8c26824 100644 --- a/visual_console_client/src/items/Service.ts +++ b/visual_console_client/src/items/Service.ts @@ -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" || diff --git a/visual_console_client/src/items/SimpleValue.ts b/visual_console_client/src/items/SimpleValue.ts index 7cc5e139af..10b8e4097a 100644 --- a/visual_console_client/src/items/SimpleValue.ts +++ b/visual_console_client/src/items/SimpleValue.ts @@ -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"); diff --git a/visual_console_client/src/items/StaticGraph.ts b/visual_console_client/src/items/StaticGraph.ts index 899d54ec70..39267e33f9 100644 --- a/visual_console_client/src/items/StaticGraph.ts +++ b/visual_console_client/src/items/StaticGraph.ts @@ -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."); diff --git a/visual_console_client/src/lib/AsyncTaskManager.ts b/visual_console_client/src/lib/AsyncTaskManager.ts index 5ad261194b..ef187f45ad 100644 --- a/visual_console_client/src/lib/AsyncTaskManager.ts +++ b/visual_console_client/src/lib/AsyncTaskManager.ts @@ -1,4 +1,4 @@ -import TypedEvent, { Disposable, Listener } from "../TypedEvent"; +import TypedEvent, { Disposable, Listener } from "./TypedEvent"; interface Cancellable { cancel(): void; diff --git a/visual_console_client/src/TypedEvent.ts b/visual_console_client/src/lib/TypedEvent.ts similarity index 100% rename from visual_console_client/src/TypedEvent.ts rename to visual_console_client/src/lib/TypedEvent.ts diff --git a/visual_console_client/src/lib/index.ts b/visual_console_client/src/lib/index.ts index 1a04ca74af..65f0a1ab86 100644 --- a/visual_console_client/src/lib/index.ts +++ b/visual_console_client/src/lib/index.ts @@ -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(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. diff --git a/visual_console_client/src/lib/spec.ts b/visual_console_client/src/lib/spec.ts index ca86e2f499..ae8ffb02cc 100644 --- a/visual_console_client/src/lib/spec.ts +++ b/visual_console_client/src/lib/spec.ts @@ -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("
Box

Paragraph


"); + ).toBe("
Box

Paragraph


"); }); }); @@ -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); + }); +}); diff --git a/visual_console_client/src/types.ts b/visual_console_client/src/lib/types.ts similarity index 83% rename from visual_console_client/src/types.ts rename to visual_console_client/src/lib/types.ts index 79dee56e74..97f6cb622d 100644 --- a/visual_console_client/src/types.ts +++ b/visual_console_client/src/lib/types.ts @@ -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; +}