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 {
sizePropsDecoder,
positionPropsDecoder,
@ -9,7 +15,7 @@ import {
humanDate,
humanTime
} from "./lib";
import TypedEvent, { Listener, Disposable } from "./TypedEvent";
import TypedEvent, { Listener, Disposable } from "./lib/TypedEvent";
// Enum: https://www.typescriptlang.org/docs/handbook/enums.html.
export const enum ItemType {
@ -52,14 +58,14 @@ export interface ItemProps extends Position, Size {
// FIXME: Fix type compatibility.
export interface ItemClickEvent<Props extends ItemProps> {
// data: Props;
data: UnknownObject;
data: AnyObject;
nativeEvent: Event;
}
// FIXME: Fix type compatibility.
export interface ItemRemoveEvent<Props extends ItemProps> {
// data: Props;
data: UnknownObject;
data: AnyObject;
}
/**
@ -89,7 +95,7 @@ const parseLabelPosition = (
* @throws Will throw a TypeError if some property
* is missing from the raw object or have an invalid type.
*/
export function itemBasePropsDecoder(data: UnknownObject): ItemProps | never {
export function itemBasePropsDecoder(data: AnyObject): ItemProps | never {
if (data.id == null || isNaN(parseInt(data.id))) {
throw new TypeError("invalid id.");
}
@ -118,6 +124,8 @@ export function itemBasePropsDecoder(data: UnknownObject): ItemProps | never {
abstract class VisualConsoleItem<Props extends ItemProps> {
// Properties of the item.
private itemProps: Props;
// Metadata of the item.
private _metadata: ItemMeta;
// Reference to the DOM element which will contain the item.
public elementRef: HTMLElement;
public readonly labelElementRef: HTMLElement;
@ -138,8 +146,9 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
*/
protected abstract createDomElement(): HTMLElement;
public constructor(props: Props) {
public constructor(props: Props, metadata: ItemMeta) {
this.itemProps = props;
this._metadata = metadata;
/*
* Get a HTMLElement which represents the container box
@ -185,8 +194,10 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
box.style.zIndex = this.props.isOnTop ? "2" : "1";
box.style.left = `${this.props.x}px`;
box.style.top = `${this.props.y}px`;
box.onclick = e =>
this.clickEventManager.emit({ data: this.props, nativeEvent: e });
box.addEventListener("click", e => {
if (!this.meta.editMode)
this.clickEventManager.emit({ data: this.props, nativeEvent: e });
});
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.
// 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.
* @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<Props extends ItemProps> {
// Changed the reference to the main element. It's ugly, but needed.
this.elementRef = container;
}
// Change metadata related things.
if (!prevMeta || prevMeta.editMode !== this.meta.editMode) {
if (this.meta.editMode) {
this.elementRef.classList.add("is-editing");
} else {
this.elementRef.classList.remove("is-editing");
}
}
if (!prevMeta || prevMeta.isFetching !== this.meta.isFetching) {
if (this.meta.isFetching) {
this.elementRef.classList.add("is-fetching");
} else {
this.elementRef.classList.remove("is-fetching");
}
}
if (!prevMeta || prevMeta.isUpdating !== this.meta.isUpdating) {
if (this.meta.isUpdating) {
this.elementRef.classList.add("is-updating");
} else {
this.elementRef.classList.remove("is-updating");
}
}
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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