From 125893252f5f32f1d2a6d9c9ec287f9f66db8a9d Mon Sep 17 00:00:00 2001 From: Alejandro Gallardo Escobar Date: Mon, 5 Aug 2019 12:57:58 +0200 Subject: [PATCH 1/3] Added a period selector to the visual console item library --- visual_console_client/src/lib/index.ts | 196 ++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 4 deletions(-) diff --git a/visual_console_client/src/lib/index.ts b/visual_console_client/src/lib/index.ts index c04ba476c5..d42d4580cf 100644 --- a/visual_console_client/src/lib/index.ts +++ b/visual_console_client/src/lib/index.ts @@ -11,6 +11,8 @@ import { } from "./types"; import helpTipIcon from "./help-tip.png"; +import fontAwesomeIcon from "./FontAwesomeIcon"; +import { faPencilAlt, faListAlt } from "@fortawesome/free-solid-svg-icons"; /** * Return a number or a default value from a raw value. @@ -752,13 +754,199 @@ export function helpTip(text: string): HTMLElement { return container; } +interface PeriodSelectorOption { + value: number; + text: string; +} +export function periodSelector( + selectedValue: PeriodSelectorOption["value"] | null, + emptyOption: PeriodSelectorOption | null, + options: PeriodSelectorOption[], + onChange: (value: PeriodSelectorOption["value"]) => void +): HTMLElement { + if (selectedValue === null) selectedValue = 0; + const initialValue = emptyOption ? emptyOption.value : 0; + let currentValue: number = + selectedValue != null ? selectedValue : initialValue; + // Main container. + const container = document.createElement("div"); + // Container for the period selector. + const periodsContainer = document.createElement("div"); + const selectPeriods = document.createElement("select"); + const useManualPeriodsBtn = document.createElement("a"); + // Container for the custom period input. + const manualPeriodsContainer = document.createElement("div"); + const inputTimeValue = document.createElement("input"); + const unitsSelect = document.createElement("select"); + const usePeriodsBtn = document.createElement("a"); + // Units to multiply the custom period input. + const unitOptions: { value: string; text: string }[] = [ + { value: "1", text: t("Seconds").toLowerCase() }, + { value: "60", text: t("Minutes").toLowerCase() }, + { value: "3600", text: t("Hours").toLowerCase() }, + { value: "86400", text: t("Days").toLowerCase() }, + { value: "604800", text: t("Weeks").toLowerCase() }, + { value: `${86400 * 30}`, text: t("Months").toLowerCase() }, + { value: `${86400 * 30 * 12}`, text: t("Years").toLowerCase() } + ]; + + // Will be executed every time the value changes. + const handleChange = (value: number) => { + currentValue = value; + onChange(currentValue); + }; + // Will return the first period option smaller than the value. + const findPeriodsOption = (value: number) => + options + .sort((a, b) => (a.value < b.value ? 1 : -1)) + .find(optionVal => value >= optionVal.value); + // Will return the first multiple of the value using the custom input multipliers. + const findManualPeriodsOptionValue = (value: number) => + unitOptions + .map(unitOption => Number.parseInt(unitOption.value)) + .sort((a, b) => (a < b ? 1 : -1)) + .find(optionVal => value % optionVal === 0); + // Will find and set a valid option for the period selector. + const setPeriodsValue = (value: number) => { + let option = findPeriodsOption(value); + selectPeriods.value = `${option ? option.value : initialValue}`; + }; + // Will transform the value to show the perfect fit for the custom input period. + const setManualPeriodsValue = (value: number) => { + const optionVal = findManualPeriodsOptionValue(value); + if (optionVal) { + inputTimeValue.value = `${value / optionVal}`; + unitsSelect.value = `${optionVal}`; + } else { + inputTimeValue.value = `${value}`; + unitsSelect.value = "1"; + } + }; + + // Will modify the value to show the perfect fit for this element and show its container. + const showPeriods = () => { + let option = findPeriodsOption(currentValue); + const newValue = option ? option.value : initialValue; + selectPeriods.value = `${newValue}`; + + if (newValue !== currentValue) handleChange(newValue); + + container.replaceChild(periodsContainer, manualPeriodsContainer); + }; + // Will modify the value to show the perfect fit for this element and show its container. + const showManualPeriods = () => { + const optionVal = findManualPeriodsOptionValue(currentValue); + + if (optionVal) { + inputTimeValue.value = `${currentValue / optionVal}`; + unitsSelect.value = `${optionVal}`; + } else { + inputTimeValue.value = `${currentValue}`; + unitsSelect.value = "1"; + } + + container.replaceChild(manualPeriodsContainer, periodsContainer); + }; + + // Append the elements + + periodsContainer.appendChild(selectPeriods); + periodsContainer.appendChild(useManualPeriodsBtn); + + manualPeriodsContainer.appendChild(inputTimeValue); + manualPeriodsContainer.appendChild(unitsSelect); + manualPeriodsContainer.appendChild(usePeriodsBtn); + + if ( + options.find(option => option.value === selectedValue) || + (emptyOption && emptyOption.value === selectedValue) + ) { + // Start with the custom periods select. + container.appendChild(periodsContainer); + } else { + // Start with the manual time input + container.appendChild(manualPeriodsContainer); + } + + // Set and fill the elements. + + // Periods selector. + + selectPeriods.addEventListener("change", (e: Event) => + handleChange( + parseIntOr((e.target as HTMLSelectElement).value, initialValue) + ) + ); + if (emptyOption) { + const optionElem = document.createElement("option"); + optionElem.value = `${emptyOption.value}`; + optionElem.text = emptyOption.text; + selectPeriods.appendChild(optionElem); + } + options.forEach(option => { + const optionElem = document.createElement("option"); + optionElem.value = `${option.value}`; + optionElem.text = option.text; + selectPeriods.appendChild(optionElem); + }); + + setPeriodsValue(selectedValue); + + useManualPeriodsBtn.appendChild( + fontAwesomeIcon(faPencilAlt, t("Show manual period input"), { + size: "small" + }) + ); + useManualPeriodsBtn.addEventListener("click", e => { + e.preventDefault(); + showManualPeriods(); + }); + + // Manual periods input. + + inputTimeValue.type = "number"; + inputTimeValue.min = "0"; + inputTimeValue.required = true; + inputTimeValue.addEventListener("change", (e: Event) => + handleChange( + parseIntOr((e.target as HTMLSelectElement).value, 0) * + parseIntOr(unitsSelect.value, 1) + ) + ); + // Select for time units. + unitsSelect.addEventListener("change", (e: Event) => + handleChange( + parseIntOr(inputTimeValue.value, 0) * + parseIntOr((e.target as HTMLSelectElement).value, 1) + ) + ); + unitOptions.forEach(option => { + const optionElem = document.createElement("option"); + optionElem.value = `${option.value}`; + optionElem.text = option.text; + unitsSelect.appendChild(optionElem); + }); + + setManualPeriodsValue(selectedValue); + + usePeriodsBtn.appendChild( + fontAwesomeIcon(faListAlt, t("Show periods selector"), { size: "small" }) + ); + usePeriodsBtn.addEventListener("click", e => { + e.preventDefault(); + showPeriods(); + }); + + return container; +} + /** * Cuts the text if their length is greater than the selected max length * and applies the selected ellipse to the result text. - * @param {string} str Text to cut - * @param {number} max Maximum length after cutting the text - * @param {string} ellipse String to be added to the cutted text - * @returns {string} Full text or text cutted with the ellipse + * @param str Text to cut + * @param max Maximum length after cutting the text + * @param ellipse String to be added to the cutted text + * @returns Full text or text cutted with the ellipse */ export function ellipsize( str: string, From b30baffb5dd103dd715f5ec73c1dab18fca8989d Mon Sep 17 00:00:00 2001 From: Alejandro Gallardo Escobar Date: Mon, 5 Aug 2019 12:58:05 +0200 Subject: [PATCH 2/3] Bugfix --- visual_console_client/src/lib/FontAwesomeIcon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visual_console_client/src/lib/FontAwesomeIcon.ts b/visual_console_client/src/lib/FontAwesomeIcon.ts index 7db0445bf3..7b0534afc1 100644 --- a/visual_console_client/src/lib/FontAwesomeIcon.ts +++ b/visual_console_client/src/lib/FontAwesomeIcon.ts @@ -13,7 +13,7 @@ interface ExtraProps { const fontAwesomeIcon = ( iconDefinition: IconDefinition, title: string, - { size, color, spin, pulse }: ExtraProps + { size, color, spin, pulse }: ExtraProps = {} ): HTMLElement => { const container = document.createElement("figure"); container.title = title; From 3333cb2b59b18e721155bd8c5954a38793ef178d Mon Sep 17 00:00:00 2001 From: Alejandro Gallardo Escobar Date: Mon, 5 Aug 2019 12:58:25 +0200 Subject: [PATCH 3/3] Added the cache expiration to the new visual console --- .../rest-api/models/VisualConsole/Item.php | 65 ++++++++++++------- visual_console_client/src/Item.ts | 58 +++++++++++++++-- 2 files changed, 94 insertions(+), 29 deletions(-) diff --git a/pandora_console/include/rest-api/models/VisualConsole/Item.php b/pandora_console/include/rest-api/models/VisualConsole/Item.php index 742a12d308..cb325b8a5d 100644 --- a/pandora_console/include/rest-api/models/VisualConsole/Item.php +++ b/pandora_console/include/rest-api/models/VisualConsole/Item.php @@ -187,18 +187,19 @@ class Item extends CachedModel protected function decode(array $data): array { $decodedData = [ - 'id' => (int) $data['id'], - 'type' => (int) $data['type'], - 'label' => static::extractLabel($data), - 'labelPosition' => static::extractLabelPosition($data), - 'isLinkEnabled' => static::extractIsLinkEnabled($data), - 'isOnTop' => static::extractIsOnTop($data), - 'parentId' => static::extractParentId($data), - 'aclGroupId' => static::extractAclGroupId($data), - 'width' => (int) $data['width'], - 'height' => (int) $data['height'], - 'x' => static::extractX($data), - 'y' => static::extractY($data), + 'id' => (int) $data['id'], + 'type' => (int) $data['type'], + 'label' => static::extractLabel($data), + 'labelPosition' => static::extractLabelPosition($data), + 'isLinkEnabled' => static::extractIsLinkEnabled($data), + 'isOnTop' => static::extractIsOnTop($data), + 'parentId' => static::extractParentId($data), + 'aclGroupId' => static::extractAclGroupId($data), + 'width' => (int) $data['width'], + 'height' => (int) $data['height'], + 'x' => static::extractX($data), + 'y' => static::extractY($data), + 'cacheExpiration' => static::extractCacheExpiration($data), ]; if (static::$useLinkedModule === true) { @@ -434,6 +435,28 @@ class Item extends CachedModel } + /** + * Extract the cache expiration value. + * + * @param array $data Unknown input data structure. + * + * @return integer Cache expiration time. + */ + private static function extractCacheExpiration(array $data) + { + return static::parseIntOr( + static::issetInArray( + $data, + [ + 'cacheExpiration', + 'cache_expiration', + ] + ), + null + ); + } + + /** * Extract an module Id value. * @@ -1254,6 +1277,9 @@ class Item extends CachedModel /** + * TODO: CRITICAL. This function contains values which belong to its + * subclasses. This function should be overrided there to add them. + * * Return a valid representation of a record in database. * * @param array $data Input data. @@ -1568,18 +1594,9 @@ class Item extends CachedModel $result['show_last_value'] = $show_last_value; } - $cache_expiration = static::parseIntOr( - static::issetInArray( - $data, - [ - 'cache_expiration', - 'cacheExpiration', - ] - ), - null - ); - if ($cache_expiration !== null) { - $result['cache_expiration'] = $cache_expiration; + $cacheExpiration = static::extractCacheExpiration($data); + if ($cacheExpiration !== null) { + $result['cache_expiration'] = $cacheExpiration; } return $result; diff --git a/visual_console_client/src/Item.ts b/visual_console_client/src/Item.ts index 78319a6fcf..0a6cbdbef3 100644 --- a/visual_console_client/src/Item.ts +++ b/visual_console_client/src/Item.ts @@ -19,7 +19,8 @@ import { debounce, addResizementListener, t, - helpTip + helpTip, + periodSelector } from "./lib"; import TypedEvent, { Listener, Disposable } from "./lib/TypedEvent"; import { FormContainer, InputGroup } from "./Form"; @@ -66,6 +67,7 @@ export interface ItemProps extends Position, Size { isOnTop: boolean; parentId: number | null; aclGroupId: number | null; + cacheExpiration: number | null; } export interface ItemClickEvent { @@ -351,6 +353,33 @@ class AclGroupInputGroup extends InputGroup> { } } +// TODO: Document +class CacheExpirationInputGroup extends InputGroup> { + protected createContent(): HTMLElement | HTMLElement[] { + const periodLabel = document.createElement("label"); + periodLabel.textContent = t("Cache expiration"); + + const periodControl = periodSelector( + this.currentData.cacheExpiration || this.initialData.cacheExpiration || 0, + { text: t("No cache"), value: 0 }, + [ + { text: t("10 seconds"), value: 10 }, + { text: t("30 seconds"), value: 30 }, + { text: t("60 seconds"), value: 60 }, + { text: t("5 minutes"), value: 300 }, + { text: t("15 minutes"), value: 900 }, + { text: t("30 minutes"), value: 1800 }, + { text: t("1 hour"), value: 3600 } + ], + value => this.updateData({ cacheExpiration: value }) + ); + + periodLabel.appendChild(periodControl); + + return periodLabel; + } +} + /** * Class to add item to the general items form * This item consists of a label and a color type select. @@ -800,6 +829,7 @@ export function itemBasePropsDecoder(data: AnyObject): ItemProps | never { isOnTop: parseBoolean(data.isOnTop), parentId: parseIntOr(data.parentId, null), aclGroupId: parseIntOr(data.aclGroupId, null), + cacheExpiration: parseIntOr(data.cacheExpiration, null), ...sizePropsDecoder(data), // Object spread. It will merge the properties of the two objects. ...positionPropsDecoder(data) // Object spread. It will merge the properties of the two objects. }; @@ -1656,9 +1686,18 @@ abstract class VisualConsoleItem { new LinkInputGroup("link", this.props), new OnTopInputGroup("show-on-top", this.props), new ParentInputGroup("parent", this.props), - new AclGroupInputGroup("acl-group", this.props) + new AclGroupInputGroup("acl-group", this.props), + new CacheExpirationInputGroup("cache-expiration", this.props) ], - ["position", "size", "link", "show-on-top", "parent", "acl-group"] + [ + "position", + "size", + "link", + "show-on-top", + "parent", + "acl-group", + "cache-expiration" + ] ); //return VisualConsoleItem.getFormContainer(this.props); @@ -1674,9 +1713,18 @@ abstract class VisualConsoleItem { new LinkInputGroup("link", props), new OnTopInputGroup("show-on-top", props), new ParentInputGroup("parent", props), - new AclGroupInputGroup("acl-group", props) + new AclGroupInputGroup("acl-group", props), + new CacheExpirationInputGroup("cache-expiration", props) ], - ["position", "size", "link", "show-on-top", "parent", "acl-group"] + [ + "position", + "size", + "link", + "show-on-top", + "parent", + "acl-group", + "cache-expiration" + ] ); } }