Merge remote-tracking branch 'origin/ent-3997-anadir-posicionamiento-a-los-elementos-de-la-consola-visual' into ent-3994-Añadir-actualización-de-ciertos-valores-a-la-api-web-de-la-consola-visual

This commit is contained in:
Daniel Maya 2019-06-12 12:23:41 +02:00
commit e332c4ffba
73 changed files with 1116 additions and 171 deletions

View File

@ -1,5 +1,5 @@
package: pandorafms-agent-unix package: pandorafms-agent-unix
Version: 7.0NG.735-190610 Version: 7.0NG.735-190612
Architecture: all Architecture: all
Priority: optional Priority: optional
Section: admin Section: admin

View File

@ -14,7 +14,7 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
pandora_version="7.0NG.735-190610" pandora_version="7.0NG.735-190612"
echo "Test if you has the tools for to make the packages." echo "Test if you has the tools for to make the packages."
whereis dpkg-deb | cut -d":" -f2 | grep dpkg-deb > /dev/null whereis dpkg-deb | cut -d":" -f2 | grep dpkg-deb > /dev/null

View File

@ -42,7 +42,7 @@ my $Sem = undef;
my $ThreadSem = undef; my $ThreadSem = undef;
use constant AGENT_VERSION => '7.0NG.735'; use constant AGENT_VERSION => '7.0NG.735';
use constant AGENT_BUILD => '190610'; use constant AGENT_BUILD => '190612';
# Agent log default file size maximum and instances # Agent log default file size maximum and instances
use constant DEFAULT_MAX_LOG_SIZE => 600000; use constant DEFAULT_MAX_LOG_SIZE => 600000;

View File

@ -3,7 +3,7 @@
# #
%define name pandorafms_agent_unix %define name pandorafms_agent_unix
%define version 7.0NG.735 %define version 7.0NG.735
%define release 190610 %define release 190612
Summary: Pandora FMS Linux agent, PERL version Summary: Pandora FMS Linux agent, PERL version
Name: %{name} Name: %{name}

View File

@ -3,7 +3,7 @@
# #
%define name pandorafms_agent_unix %define name pandorafms_agent_unix
%define version 7.0NG.735 %define version 7.0NG.735
%define release 190610 %define release 190612
Summary: Pandora FMS Linux agent, PERL version Summary: Pandora FMS Linux agent, PERL version
Name: %{name} Name: %{name}

View File

@ -10,7 +10,7 @@
# ********************************************************************** # **********************************************************************
PI_VERSION="7.0NG.735" PI_VERSION="7.0NG.735"
PI_BUILD="190610" PI_BUILD="190612"
OS_NAME=`uname -s` OS_NAME=`uname -s`
FORCE=0 FORCE=0

View File

@ -6,17 +6,17 @@
# #
# grep_log Perl script to search log files for a matching pattern. The last # grep_log Perl script to search log files for a matching pattern. The last
# searched position is saved in an index file so that consecutive # searched position is saved in an index file so that consecutive
# runs do not return the same results. The log file inode number is # runs do not return the same results. The log file inode number is
# also saved to detect log rotation. # also saved to detect log rotation.
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License. # the Free Software Foundation; version 2 of the License.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
############################################################################### ###############################################################################
use strict; use strict;
@ -30,7 +30,7 @@ my $Output = 'module';
my $Verbose = 0; my $Verbose = 0;
# Index file storage directory, with a trailing '/' # Index file storage directory, with a trailing '/'
my $Idx_dir=($^O =~ /win/i)?'.\\':'/tmp/'; my $Idx_dir=($^O =~ /win/i)?"$ENV{'TMP'}\\":"/tmp/";
# Log file # Log file
my $Log_file = ''; my $Log_file = '';
@ -64,7 +64,7 @@ if ( (defined ($ENV{GREP_LOG_TMP})) && (-d $ENV{GREP_LOG_TMP}) ) {
} }
######################################################################################## ########################################################################################
# Erase blank spaces before and after the string # Erase blank spaces before and after the string
######################################################################################## ########################################################################################
sub trim($){ sub trim($){
my $string = shift; my $string = shift;
@ -226,7 +226,7 @@ sub parse_log (;$$) {
open(LOGFILE, $Log_file) || error_msg("Error opening file $Log_file: " . open(LOGFILE, $Log_file) || error_msg("Error opening file $Log_file: " .
$!); $!);
# Go to starting position. # Go to starting position.
seek(LOGFILE, $Idx_pos, 0); seek(LOGFILE, $Idx_pos, 0);
# Parse log file # Parse log file
@ -318,7 +318,7 @@ sub print_log ($) {
print_summary() if ($summary_flag == 1); print_summary() if ($summary_flag == 1);
return; return;
} }
# Log module # Log module
if ($Output eq 'log_module') { if ($Output eq 'log_module') {
my $output = "<log_module>\n"; my $output = "<log_module>\n";

View File

@ -186,7 +186,7 @@ UpgradeApplicationID
{} {}
Version Version
{190610} {190612}
ViewReadme ViewReadme
{Yes} {Yes}

View File

@ -30,7 +30,7 @@ using namespace Pandora;
using namespace Pandora_Strutils; using namespace Pandora_Strutils;
#define PATH_SIZE _MAX_PATH+1 #define PATH_SIZE _MAX_PATH+1
#define PANDORA_VERSION ("7.0NG.735(Build 190610)") #define PANDORA_VERSION ("7.0NG.735(Build 190612)")
string pandora_path; string pandora_path;
string pandora_dir; string pandora_dir;

View File

@ -11,7 +11,7 @@ BEGIN
VALUE "LegalCopyright", "Artica ST" VALUE "LegalCopyright", "Artica ST"
VALUE "OriginalFilename", "PandoraAgent.exe" VALUE "OriginalFilename", "PandoraAgent.exe"
VALUE "ProductName", "Pandora FMS Windows Agent" VALUE "ProductName", "Pandora FMS Windows Agent"
VALUE "ProductVersion", "(7.0NG.735(Build 190610))" VALUE "ProductVersion", "(7.0NG.735(Build 190612))"
VALUE "FileVersion", "1.0.0.0" VALUE "FileVersion", "1.0.0.0"
END END
END END

View File

@ -1,5 +1,5 @@
package: pandorafms-console package: pandorafms-console
Version: 7.0NG.735-190610 Version: 7.0NG.735-190612
Architecture: all Architecture: all
Priority: optional Priority: optional
Section: admin Section: admin

View File

@ -14,7 +14,7 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
pandora_version="7.0NG.735-190610" pandora_version="7.0NG.735-190612"
package_pear=0 package_pear=0
package_pandora=1 package_pandora=1

View File

@ -139,8 +139,9 @@ function get_logs_size($file)
function get_status_logs($path) function get_status_logs($path)
{ {
$status_server_log = ''; $status_server_log = '';
$size_server_log = get_logs_size($path); $size_server_log = number_format(get_logs_size($path));
if ($size_server_log <= 1048576) { $size_server_log = (0 + str_replace(',', '', $size_server_log));
if ($size_server_log <= 10485760) {
$status_server_log = "<a style ='color: green;text-decoration: none;'>Normal Status</a><a style ='text-decoration: none;'>&nbsp&nbsp You have less than 10 MB of logs</a>"; $status_server_log = "<a style ='color: green;text-decoration: none;'>Normal Status</a><a style ='text-decoration: none;'>&nbsp&nbsp You have less than 10 MB of logs</a>";
} else { } else {
$status_server_log = "<a class= 'content' style= 'color: red;text-decoration: none;'>Warning Status</a><a style ='text-decoration: none;'>&nbsp&nbsp You have more than 10 MB of logs</a>"; $status_server_log = "<a class= 'content' style= 'color: red;text-decoration: none;'>Warning Status</a><a style ='text-decoration: none;'>&nbsp&nbsp You have more than 10 MB of logs</a>";

View File

@ -357,8 +357,8 @@ function addConnectionMap() {
for (var index in connectionMaps) { for (var index in connectionMaps) {
if (isInt(index)) { if (isInt(index)) {
if (connectionMaps[index] == idConnectionMap) { if (connectionMaps[index] == idConnectionMap) {
alert("<?php echo __('The connection'); ?> "' + connectionMapName + '" <?php echo __('just added previously.'); ?>"); alert("<?php echo __('The connection'); ?> " + connectionMapName + " <?php echo __('just added previously.'); ?>");
return; return;
} }
} }

View File

@ -45,6 +45,9 @@
.parent() .parent()
.addClass('checkselected'); .addClass('checkselected');
$(".check_delete").prop("checked", true); $(".check_delete").prop("checked", true);
$('.check_delete').each(function(){
$('#hidden-id_report_'+$(this).val()).prop("disabled", false);
});
} }
else{ else{
$('[id^=checkbox-massive_report_check]') $('[id^=checkbox-massive_report_check]')

View File

@ -299,6 +299,7 @@ if (check_login()) {
'status' => "<div id='reload_status_agent_".$values['id_tmetaconsole_setup'].'_'.$values['id_tagente']."'>".$image_status.'</div>', 'status' => "<div id='reload_status_agent_".$values['id_tmetaconsole_setup'].'_'.$values['id_tagente']."'>".$image_status.'</div>',
'id_agent' => $values['id_tagente'], 'id_agent' => $values['id_tagente'],
'id_server' => $values['id_tmetaconsole_setup'], 'id_server' => $values['id_tmetaconsole_setup'],
'status_value' => $values['status'],
]; ];
} }

View File

@ -1472,7 +1472,10 @@ function local_ldap_search($ldap_host, $ldap_port=389, $ldap_version=3, $dn, $ac
$tls = ' -ZZ '; $tls = ' -ZZ ';
} }
if (stripos($ldap_host, 'ldap') !== false) { if (stripos($ldap_host, 'ldap://') !== false
|| stripos($ldap_host, 'ldaps://') !== false
|| stripos($ldap_host, 'ldapi://') !== false
) {
$ldap_host = ' -H '.$ldap_host.':'.$ldap_port; $ldap_host = ' -H '.$ldap_host.':'.$ldap_port;
} else { } else {
$ldap_host = ' -h '.$ldap_host.' -p '.$ldap_port; $ldap_host = ' -h '.$ldap_host.' -p '.$ldap_port;

View File

@ -20,7 +20,7 @@
/** /**
* Pandora build version and version * Pandora build version and version
*/ */
$build_version = 'PC190610'; $build_version = 'PC190612';
$pandora_version = 'v7.0NG.735'; $pandora_version = 'v7.0NG.735';
// Do not overwrite default timezone set if defined. // Do not overwrite default timezone set if defined.

View File

@ -2167,9 +2167,9 @@ function config_process_config()
if (!isset($config['ad_adv_perms'])) { if (!isset($config['ad_adv_perms'])) {
config_update_value('ad_adv_perms', ''); config_update_value('ad_adv_perms', '');
} else { } else {
$temp_ad_adv_perms = [];
if (!json_decode(io_safe_output($config['ad_adv_perms']))) { if (!json_decode(io_safe_output($config['ad_adv_perms']))) {
$temp_ad_adv_perms = []; if ($config['ad_adv_perms'] != '') {
if (!isset($config['ad_adv_perms']) && $config['ad_adv_perms'] != '') {
$perms = explode(';', io_safe_output($config['ad_adv_perms'])); $perms = explode(';', io_safe_output($config['ad_adv_perms']));
foreach ($perms as $ad_adv_perm) { foreach ($perms as $ad_adv_perm) {
if (preg_match('/[\[\]]/', $ad_adv_perm)) { if (preg_match('/[\[\]]/', $ad_adv_perm)) {
@ -2232,22 +2232,26 @@ function config_process_config()
if (!empty($new_ad_adv_perms)) { if (!empty($new_ad_adv_perms)) {
$temp_ad_adv_perms = json_encode($new_ad_adv_perms); $temp_ad_adv_perms = json_encode($new_ad_adv_perms);
} }
} else {
$temp_ad_adv_perms = '';
} }
} else {
config_update_value('ad_adv_perms', $temp_ad_adv_perms); $temp_ad_adv_perms = $config['ad_adv_perms'];
} }
config_update_value('ad_adv_perms', $temp_ad_adv_perms);
} }
if (!isset($config['ldap_adv_perms'])) { if (!isset($config['ldap_adv_perms'])) {
config_update_value('ldap_adv_perms', ''); config_update_value('ldap_adv_perms', '');
} else { } else {
$temp_ldap_adv_perms = [];
if (!json_decode(io_safe_output($config['ldap_adv_perms']))) { if (!json_decode(io_safe_output($config['ldap_adv_perms']))) {
$temp_ldap_adv_perms = []; if ($config['ldap_adv_perms'] != '') {
if (!isset($config['ad_adv_perms']) && $config['ldap_adv_perms'] != '') {
$perms = explode(';', io_safe_output($config['ldap_adv_perms'])); $perms = explode(';', io_safe_output($config['ldap_adv_perms']));
foreach ($perms as $ad_adv_perm) { foreach ($perms as $ldap_adv_perm) {
if (preg_match('/[\[\]]/', $ad_adv_perm)) { if (preg_match('/[\[\]]/', $ldap_adv_perm)) {
$all_data = explode(',', io_safe_output($ad_adv_perm)); $all_data = explode(',', io_safe_output($ldap_adv_perm));
$profile = $all_data[0]; $profile = $all_data[0];
$group_pnd = $all_data[1]; $group_pnd = $all_data[1];
$groups_ad = str_replace(['[', ']'], '', $all_data[2]); $groups_ad = str_replace(['[', ']'], '', $all_data[2]);
@ -2277,7 +2281,7 @@ function config_process_config()
'groups_ldap' => $groups_ldap, 'groups_ldap' => $groups_ldap,
]; ];
} else { } else {
$all_data = explode(',', io_safe_output($ad_adv_perm)); $all_data = explode(',', io_safe_output($ldap_adv_perm));
$profile = $all_data[0]; $profile = $all_data[0];
$group_pnd = $all_data[1]; $group_pnd = $all_data[1];
$groups_ad = $all_data[2]; $groups_ad = $all_data[2];
@ -2306,10 +2310,14 @@ function config_process_config()
if (!empty($new_ldap_adv_perms)) { if (!empty($new_ldap_adv_perms)) {
$temp_ldap_adv_perms = json_encode($new_ldap_adv_perms); $temp_ldap_adv_perms = json_encode($new_ldap_adv_perms);
} }
} else {
$temp_ldap_adv_perms = '';
} }
} else {
config_update_value('ldap_adv_perms', $temp_ldap_adv_perms); $temp_ldap_adv_perms = $config['ldap_adv_perms'];
} }
config_update_value('ldap_adv_perms', $temp_ldap_adv_perms);
} }
if (!isset($config['rpandora_server'])) { if (!isset($config['rpandora_server'])) {

View File

@ -188,10 +188,16 @@ function get_custom_fields_data($custom_field_name)
} }
$array_result = []; $array_result = [];
if (isset($result_meta) && is_array($result_meta)) { if (isset($result_meta) === true
&& is_array($result_meta) === true
) {
foreach ($result_meta as $result) { foreach ($result_meta as $result) {
foreach ($result as $k => $v) { if (isset($result) === true
$array_result[$v['description']] = $v['description']; && is_array($result) === true
) {
foreach ($result as $k => $v) {
$array_result[$v['description']] = $v['description'];
}
} }
} }
} }
@ -385,9 +391,13 @@ function agent_counters_custom_fields($filters)
// Filter custom data. // Filter custom data.
$custom_data_and = ''; $custom_data_and = '';
if (!in_array(-1, $filters['id_custom_fields_data'])) { if (isset($filters['id_custom_fields_data']) === true
$custom_data_array = implode("', '", $filters['id_custom_fields_data']); && is_array($filters['id_custom_fields_data']) === true
$custom_data_and = "AND tcd.description IN ('".$custom_data_array."')"; ) {
if (!in_array(-1, $filters['id_custom_fields_data'])) {
$custom_data_array = implode("', '", $filters['id_custom_fields_data']);
$custom_data_and = "AND tcd.description IN ('".$custom_data_array."')";
}
} }
// Filter custom name. // Filter custom name.
@ -693,3 +703,123 @@ function print_counters_cfv(
$html_result .= '</form>'; $html_result .= '</form>';
return $html_result; return $html_result;
} }
/**
* Function for export a csv file from Custom Fields View
*
* @param array $filters Status counters for agents and modules.
* @param array $id_status Agent status.
* @param array $module_status Module status.
*
* @return array Returns the data that will be saved in the csv file
*/
function export_custom_fields_csv($filters, $id_status, $module_status)
{
$data = agent_counters_custom_fields($filters);
$indexed_descriptions = $data['indexed_descriptions'];
// Table temporary for save array in table
// by order and search custom_field data.
$table_temporary = 'CREATE TEMPORARY TABLE temp_custom_fields (
id_server int(10),
id_agent int(10),
name_custom_fields varchar(2048),
critical_count int,
warning_count int,
unknown_count int,
notinit_count int,
normal_count int,
total_count int,
`status` int(2),
KEY `data_index_temp_1` (`id_server`, `id_agent`)
)';
db_process_sql($table_temporary);
// Insert values array in table temporary.
$values_insert = [];
foreach ($indexed_descriptions as $key => $value) {
$values_insert[] = '('.$value['id_server'].', '.$value['id_agente'].", '".$value['description']."', '".$value['critical_count']."', '".$value['warning_count']."', '".$value['unknown_count']."', '".$value['notinit_count']."', '".$value['normal_count']."', '".$value['total_count']."', ".$value['status'].')';
}
$values_insert_implode = implode(',', $values_insert);
$query_insert = 'INSERT INTO temp_custom_fields VALUES '.$values_insert_implode;
db_process_sql($query_insert);
// Search for status module.
$status_agent_search = '';
if (isset($id_status) === true && is_array($id_status) === true) {
if (in_array(-1, $id_status) === false) {
if (in_array(AGENT_MODULE_STATUS_NOT_NORMAL, $id_status) === false) {
$status_agent_search = ' AND temp.status IN ('.implode(',', $id_status).')';
} else {
// Not normal statuses.
$status_agent_search = ' AND temp.status IN (1,2,3,4,5)';
}
}
}
// Search for status module.
$status_module_search = '';
if (isset($module_status) === true && is_array($module_status) === true) {
if (in_array(-1, $module_status) === false) {
if (in_array(AGENT_MODULE_STATUS_NOT_NORMAL, $module_status) === false) {
if (count($module_status) > 0) {
$status_module_search = ' AND ( ';
foreach ($module_status as $key => $value) {
$status_module_search .= ($key != 0) ? ' OR (' : ' (';
switch ($value) {
default:
case AGENT_STATUS_NORMAL:
$status_module_search .= ' temp.normal_count > 0) ';
break;
case AGENT_STATUS_CRITICAL:
$status_module_search .= ' temp.critical_count > 0) ';
break;
case AGENT_STATUS_WARNING:
$status_module_search .= ' temp.warning_count > 0) ';
break;
case AGENT_STATUS_UNKNOWN:
$status_module_search .= ' temp.unknown_count > 0) ';
break;
case AGENT_STATUS_NOT_INIT:
$status_module_search .= ' temp.notinit_count > 0) ';
break;
}
}
$status_module_search .= ' ) ';
}
} else {
// Not normal.
$status_module_search = ' AND ( temp.critical_count > 0 OR temp.warning_count > 0 OR temp.unknown_count > 0 AND temp.notinit_count > 0 )';
}
}
}
// Query all fields result.
$query = sprintf(
'SELECT
temp.name_custom_fields,
tma.alias,
tma.direccion,
tma.server_name,
temp.status
FROM tmetaconsole_agent tma
INNER JOIN temp_custom_fields temp
ON temp.id_agent = tma.id_tagente
AND temp.id_server = tma.id_tmetaconsole_setup
WHERE tma.disabled = 0
%s
%s
',
$status_agent_search,
$status_module_search
);
$result = db_get_all_rows_sql($query);
return $result;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,45 @@
/*!
Buttons for DataTables 1.5.6
©2016-2019 SpryMedia Ltd - datatables.net/license
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(d,q,n){d instanceof String&&(d=String(d));for(var l=d.length,u=0;u<l;u++){var p=d[u];if(q.call(n,p,u,d))return{i:u,v:p}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(d,q,n){d!=Array.prototype&&d!=Object.prototype&&(d[q]=n.value)};$jscomp.getGlobal=function(d){return"undefined"!=typeof window&&window===d?d:"undefined"!=typeof global&&null!=global?global:d};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.polyfill=function(d,q,n,l){if(q){n=$jscomp.global;d=d.split(".");for(l=0;l<d.length-1;l++){var u=d[l];u in n||(n[u]={});n=n[u]}d=d[d.length-1];l=n[d];q=q(l);q!=l&&null!=q&&$jscomp.defineProperty(n,d,{configurable:!0,writable:!0,value:q})}};$jscomp.polyfill("Array.prototype.find",function(d){return d?d:function(d,n){return $jscomp.findInternal(this,d,n).v}},"es6","es3");
(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(q){return d(q,window,document)}):"object"===typeof exports?module.exports=function(q,n){q||(q=window);n&&n.fn.dataTable||(n=require("datatables.net")(q,n).$);return d(n,q,q.document)}:d(jQuery,window,document)})(function(d,q,n,l){function u(a){a=new p.Api(a);var b=a.init().buttons||p.defaults.buttons;return(new t(a,b)).container()}var p=d.fn.dataTable,B=0,C=0,r=p.ext.buttons,t=function(a,b){if(!(this instanceof
t))return function(b){return(new t(b,a)).container()};"undefined"===typeof b&&(b={});!0===b&&(b={});d.isArray(b)&&(b={buttons:b});this.c=d.extend(!0,{},t.defaults,b);b.buttons&&(this.c.buttons=b.buttons);this.s={dt:new p.Api(a),buttons:[],listenKeys:"",namespace:"dtb"+B++};this.dom={container:d("<"+this.c.dom.container.tag+"/>").addClass(this.c.dom.container.className)};this._constructor()};d.extend(t.prototype,{action:function(a,b){a=this._nodeToButton(a);if(b===l)return a.conf.action;a.conf.action=
b;return this},active:function(a,b){var c=this._nodeToButton(a);a=this.c.dom.button.active;c=d(c.node);if(b===l)return c.hasClass(a);c.toggleClass(a,b===l?!0:b);return this},add:function(a,b){var c=this.s.buttons;if("string"===typeof b){b=b.split("-");c=this.s;for(var d=0,f=b.length-1;d<f;d++)c=c.buttons[1*b[d]];c=c.buttons;b=1*b[b.length-1]}this._expandButton(c,a,!1,b);this._draw();return this},container:function(){return this.dom.container},disable:function(a){a=this._nodeToButton(a);d(a.node).addClass(this.c.dom.button.disabled);
return this},destroy:function(){d("body").off("keyup."+this.s.namespace);var a=this.s.buttons.slice(),b;var c=0;for(b=a.length;c<b;c++)this.remove(a[c].node);this.dom.container.remove();a=this.s.dt.settings()[0];c=0;for(b=a.length;c<b;c++)if(a.inst===this){a.splice(c,1);break}return this},enable:function(a,b){if(!1===b)return this.disable(a);a=this._nodeToButton(a);d(a.node).removeClass(this.c.dom.button.disabled);return this},name:function(){return this.c.name},node:function(a){if(!a)return this.dom.container;
a=this._nodeToButton(a);return d(a.node)},processing:function(a,b){a=this._nodeToButton(a);if(b===l)return d(a.node).hasClass("processing");d(a.node).toggleClass("processing",b);return this},remove:function(a){var b=this._nodeToButton(a),c=this._nodeToHost(a),e=this.s.dt;if(b.buttons.length)for(var f=b.buttons.length-1;0<=f;f--)this.remove(b.buttons[f].node);b.conf.destroy&&b.conf.destroy.call(e.button(a),e,d(a),b.conf);this._removeKey(b.conf);d(b.node).remove();a=d.inArray(b,c);c.splice(a,1);return this},
text:function(a,b){var c=this._nodeToButton(a);a=this.c.dom.collection.buttonLiner;a=c.inCollection&&a&&a.tag?a.tag:this.c.dom.buttonLiner.tag;var e=this.s.dt,f=d(c.node),g=function(a){return"function"===typeof a?a(e,f,c.conf):a};if(b===l)return g(c.conf.text);c.conf.text=b;a?f.children(a).html(g(b)):f.html(g(b));return this},_constructor:function(){var a=this,b=this.s.dt,c=b.settings()[0],e=this.c.buttons;c._buttons||(c._buttons=[]);c._buttons.push({inst:this,name:this.c.name});for(var f=0,g=e.length;f<
g;f++)this.add(e[f]);b.on("destroy",function(b,d){d===c&&a.destroy()});d("body").on("keyup."+this.s.namespace,function(b){if(!n.activeElement||n.activeElement===n.body){var c=String.fromCharCode(b.keyCode).toLowerCase();-1!==a.s.listenKeys.toLowerCase().indexOf(c)&&a._keypress(c,b)}})},_addKey:function(a){a.key&&(this.s.listenKeys+=d.isPlainObject(a.key)?a.key.key:a.key)},_draw:function(a,b){a||(a=this.dom.container,b=this.s.buttons);a.children().detach();for(var c=0,d=b.length;c<d;c++)a.append(b[c].inserter),
a.append(" "),b[c].buttons&&b[c].buttons.length&&this._draw(b[c].collection,b[c].buttons)},_expandButton:function(a,b,c,e){var f=this.s.dt,g=0;b=d.isArray(b)?b:[b];for(var h=0,k=b.length;h<k;h++){var v=this._resolveExtends(b[h]);if(v)if(d.isArray(v))this._expandButton(a,v,c,e);else{var m=this._buildButton(v,c);if(m){e!==l?(a.splice(e,0,m),e++):a.push(m);if(m.conf.buttons){var y=this.c.dom.collection;m.collection=d("<"+y.tag+"/>").addClass(y.className).attr("role","menu");m.conf._collection=m.collection;
this._expandButton(m.buttons,m.conf.buttons,!0,e)}v.init&&v.init.call(f.button(m.node),f,d(m.node),v);g++}}}},_buildButton:function(a,b){var c=this.c.dom.button,e=this.c.dom.buttonLiner,f=this.c.dom.collection,g=this.s.dt,h=function(b){return"function"===typeof b?b(g,m,a):b};b&&f.button&&(c=f.button);b&&f.buttonLiner&&(e=f.buttonLiner);if(a.available&&!a.available(g,a))return!1;var k=function(a,b,c,e){e.action.call(b.button(c),a,b,c,e);d(b.table().node()).triggerHandler("buttons-action.dt",[b.button(c),
b,c,e])};f=a.tag||c.tag;var v=a.clickBlurs===l?!0:a.clickBlurs,m=d("<"+f+"/>").addClass(c.className).attr("tabindex",this.s.dt.settings()[0].iTabIndex).attr("aria-controls",this.s.dt.table().node().id).on("click.dtb",function(b){b.preventDefault();!m.hasClass(c.disabled)&&a.action&&k(b,g,m,a);v&&m.blur()}).on("keyup.dtb",function(b){13===b.keyCode&&!m.hasClass(c.disabled)&&a.action&&k(b,g,m,a)});"a"===f.toLowerCase()&&m.attr("href","#");"button"===f.toLowerCase()&&m.attr("type","button");e.tag?(f=
d("<"+e.tag+"/>").html(h(a.text)).addClass(e.className),"a"===e.tag.toLowerCase()&&f.attr("href","#"),m.append(f)):m.html(h(a.text));!1===a.enabled&&m.addClass(c.disabled);a.className&&m.addClass(a.className);a.titleAttr&&m.attr("title",h(a.titleAttr));a.attr&&m.attr(a.attr);a.namespace||(a.namespace=".dt-button-"+C++);e=(e=this.c.dom.buttonContainer)&&e.tag?d("<"+e.tag+"/>").addClass(e.className).append(m):m;this._addKey(a);this.c.buttonCreated&&(e=this.c.buttonCreated(a,e));return{conf:a,node:m.get(0),
inserter:e,buttons:[],inCollection:b,collection:null}},_nodeToButton:function(a,b){b||(b=this.s.buttons);for(var c=0,d=b.length;c<d;c++){if(b[c].node===a)return b[c];if(b[c].buttons.length){var f=this._nodeToButton(a,b[c].buttons);if(f)return f}}},_nodeToHost:function(a,b){b||(b=this.s.buttons);for(var c=0,d=b.length;c<d;c++){if(b[c].node===a)return b;if(b[c].buttons.length){var f=this._nodeToHost(a,b[c].buttons);if(f)return f}}},_keypress:function(a,b){if(!b._buttonsHandled){var c=function(e){for(var f=
0,g=e.length;f<g;f++){var h=e[f].conf,k=e[f].node;h.key&&(h.key===a?(b._buttonsHandled=!0,d(k).click()):!d.isPlainObject(h.key)||h.key.key!==a||h.key.shiftKey&&!b.shiftKey||h.key.altKey&&!b.altKey||h.key.ctrlKey&&!b.ctrlKey||h.key.metaKey&&!b.metaKey||(b._buttonsHandled=!0,d(k).click()));e[f].buttons.length&&c(e[f].buttons)}};c(this.s.buttons)}},_removeKey:function(a){if(a.key){var b=d.isPlainObject(a.key)?a.key.key:a.key;a=this.s.listenKeys.split("");b=d.inArray(b,a);a.splice(b,1);this.s.listenKeys=
a.join("")}},_resolveExtends:function(a){var b=this.s.dt,c,e=function(c){for(var e=0;!d.isPlainObject(c)&&!d.isArray(c);){if(c===l)return;if("function"===typeof c){if(c=c(b,a),!c)return!1}else if("string"===typeof c){if(!r[c])throw"Unknown button type: "+c;c=r[c]}e++;if(30<e)throw"Buttons: Too many iterations";}return d.isArray(c)?c:d.extend({},c)};for(a=e(a);a&&a.extend;){if(!r[a.extend])throw"Cannot extend unknown button type: "+a.extend;var f=e(r[a.extend]);if(d.isArray(f))return f;if(!f)return!1;
var g=f.className;a=d.extend({},f,a);g&&a.className!==g&&(a.className=g+" "+a.className);var h=a.postfixButtons;if(h){a.buttons||(a.buttons=[]);g=0;for(c=h.length;g<c;g++)a.buttons.push(h[g]);a.postfixButtons=null}if(h=a.prefixButtons){a.buttons||(a.buttons=[]);g=0;for(c=h.length;g<c;g++)a.buttons.splice(g,0,h[g]);a.prefixButtons=null}a.extend=f.extend}return a}});t.background=function(a,b,c,e){c===l&&(c=400);e||(e=n.body);a?d("<div/>").addClass(b).css("display","none").insertAfter(e).stop().fadeIn(c):
d("div."+b).stop().fadeOut(c,function(){d(this).removeClass(b).remove()})};t.instanceSelector=function(a,b){if(!a)return d.map(b,function(a){return a.inst});var c=[],e=d.map(b,function(a){return a.name}),f=function(a){if(d.isArray(a))for(var g=0,k=a.length;g<k;g++)f(a[g]);else"string"===typeof a?-1!==a.indexOf(",")?f(a.split(",")):(a=d.inArray(d.trim(a),e),-1!==a&&c.push(b[a].inst)):"number"===typeof a&&c.push(b[a].inst)};f(a);return c};t.buttonSelector=function(a,b){for(var c=[],e=function(a,b,c){for(var d,
f,g=0,k=b.length;g<k;g++)if(d=b[g])f=c!==l?c+g:g+"",a.push({node:d.node,name:d.conf.name,idx:f}),d.buttons&&e(a,d.buttons,f+"-")},f=function(a,b){var g,h=[];e(h,b.s.buttons);var k=d.map(h,function(a){return a.node});if(d.isArray(a)||a instanceof d)for(k=0,g=a.length;k<g;k++)f(a[k],b);else if(null===a||a===l||"*"===a)for(k=0,g=h.length;k<g;k++)c.push({inst:b,node:h[k].node});else if("number"===typeof a)c.push({inst:b,node:b.s.buttons[a].node});else if("string"===typeof a)if(-1!==a.indexOf(","))for(h=
a.split(","),k=0,g=h.length;k<g;k++)f(d.trim(h[k]),b);else if(a.match(/^\d+(\-\d+)*$/))k=d.map(h,function(a){return a.idx}),c.push({inst:b,node:h[d.inArray(a,k)].node});else if(-1!==a.indexOf(":name"))for(a=a.replace(":name",""),k=0,g=h.length;k<g;k++)h[k].name===a&&c.push({inst:b,node:h[k].node});else d(k).filter(a).each(function(){c.push({inst:b,node:this})});else"object"===typeof a&&a.nodeName&&(h=d.inArray(a,k),-1!==h&&c.push({inst:b,node:k[h]}))},g=0,h=a.length;g<h;g++)f(b,a[g]);return c};t.defaults=
{buttons:["copy","excel","csv","pdf","print"],name:"main",tabIndex:0,dom:{container:{tag:"div",className:"dt-buttons"},collection:{tag:"div",className:"dt-button-collection"},button:{tag:"ActiveXObject"in q?"a":"button",className:"dt-button",active:"active",disabled:"disabled"},buttonLiner:{tag:"span",className:""}}};t.version="1.5.6";d.extend(r,{collection:{text:function(a){return a.i18n("buttons.collection","Collection")},className:"buttons-collection",init:function(a,b,c){b.attr("aria-expanded",
!1)},action:function(a,b,c,e){var f=function(){b.buttons('[aria-haspopup="true"][aria-expanded="true"]').nodes().each(function(){var a=d(this).siblings(".dt-button-collection");a.length&&a.stop().fadeOut(e.fade,function(){a.detach()});d(this).attr("aria-expanded","false")});d("div.dt-button-background").off("click.dtb-collection");t.background(!1,e.backgroundClassName,e.fade,l);d("body").off(".dtb-collection");b.off("buttons-action.b-internal")};a="true"===c.attr("aria-expanded");f();if(!a){var g=
d(c).parents("div.dt-button-collection");a=c.position();var h=d(b.table().container()),k=!1,l=c;c.attr("aria-expanded","true");g.length&&(k=d(".dt-button-collection").position(),l=g,d("body").trigger("click.dtb-collection"));l.parents("body")[0]!==n.body&&(l=n.body.lastChild);e._collection.find(".dt-button-collection-title").remove();e._collection.prepend('<div class="dt-button-collection-title">'+e.collectionTitle+"</div>");e._collection.addClass(e.collectionLayout).css("display","none").insertAfter(l).stop().fadeIn(e.fade);
g=e._collection.css("position");if(k&&"absolute"===g)e._collection.css({top:k.top,left:k.left});else if("absolute"===g){e._collection.css({top:a.top+c.outerHeight(),left:a.left});k=h.offset().top+h.height();k=a.top+c.outerHeight()+e._collection.outerHeight()-k;g=a.top-e._collection.outerHeight();var m=h.offset().top;(k>m-g||e.dropup)&&e._collection.css("top",a.top-e._collection.outerHeight()-5);e._collection.hasClass(e.rightAlignClassName)&&e._collection.css("left",a.left+c.outerWidth()-e._collection.outerWidth());
k=a.left+e._collection.outerWidth();h=h.offset().left+h.width();k>h&&e._collection.css("left",a.left-(k-h));c=c.offset().left+e._collection.outerWidth();c>d(q).width()&&e._collection.css("left",a.left-(c-d(q).width()))}else c=e._collection.height()/2,c>d(q).height()/2&&(c=d(q).height()/2),e._collection.css("marginTop",-1*c);e.background&&t.background(!0,e.backgroundClassName,e.fade,l);setTimeout(function(){d("div.dt-button-background").on("click.dtb-collection",function(){});d("body").on("click.dtb-collection",
function(a){var b=d.fn.addBack?"addBack":"andSelf";d(a.target).parents()[b]().filter(e._collection).length||f()}).on("keyup.dtb-collection",function(a){27===a.keyCode&&f()});if(e.autoClose)b.on("buttons-action.b-internal",function(){f()})},10)}},background:!0,collectionLayout:"",collectionTitle:"",backgroundClassName:"dt-button-background",rightAlignClassName:"dt-button-right",autoClose:!1,fade:400,attr:{"aria-haspopup":!0}},copy:function(a,b){if(r.copyHtml5)return"copyHtml5";if(r.copyFlash&&r.copyFlash.available(a,
b))return"copyFlash"},csv:function(a,b){if(r.csvHtml5&&r.csvHtml5.available(a,b))return"csvHtml5";if(r.csvFlash&&r.csvFlash.available(a,b))return"csvFlash"},excel:function(a,b){if(r.excelHtml5&&r.excelHtml5.available(a,b))return"excelHtml5";if(r.excelFlash&&r.excelFlash.available(a,b))return"excelFlash"},pdf:function(a,b){if(r.pdfHtml5&&r.pdfHtml5.available(a,b))return"pdfHtml5";if(r.pdfFlash&&r.pdfFlash.available(a,b))return"pdfFlash"},pageLength:function(a){a=a.settings()[0].aLengthMenu;var b=d.isArray(a[0])?
a[0]:a,c=d.isArray(a[0])?a[1]:a;return{extend:"collection",text:function(a){return a.i18n("buttons.pageLength",{"-1":"Show all rows",_:"Show %d rows"},a.page.len())},className:"buttons-page-length",autoClose:!0,buttons:d.map(b,function(a,b){return{text:c[b],className:"button-page-length",action:function(b,c){c.page.len(a).draw()},init:function(b,c,d){var e=this;c=function(){e.active(b.page.len()===a)};b.on("length.dt"+d.namespace,c);c()},destroy:function(a,b,c){a.off("length.dt"+c.namespace)}}}),
init:function(a,b,c){var d=this;a.on("length.dt"+c.namespace,function(){d.text(c.text)})},destroy:function(a,b,c){a.off("length.dt"+c.namespace)}}}});p.Api.register("buttons()",function(a,b){b===l&&(b=a,a=l);this.selector.buttonGroup=a;var c=this.iterator(!0,"table",function(c){if(c._buttons)return t.buttonSelector(t.instanceSelector(a,c._buttons),b)},!0);c._groupSelector=a;return c});p.Api.register("button()",function(a,b){a=this.buttons(a,b);1<a.length&&a.splice(1,a.length);return a});p.Api.registerPlural("buttons().active()",
"button().active()",function(a){return a===l?this.map(function(a){return a.inst.active(a.node)}):this.each(function(b){b.inst.active(b.node,a)})});p.Api.registerPlural("buttons().action()","button().action()",function(a){return a===l?this.map(function(a){return a.inst.action(a.node)}):this.each(function(b){b.inst.action(b.node,a)})});p.Api.register(["buttons().enable()","button().enable()"],function(a){return this.each(function(b){b.inst.enable(b.node,a)})});p.Api.register(["buttons().disable()",
"button().disable()"],function(){return this.each(function(a){a.inst.disable(a.node)})});p.Api.registerPlural("buttons().nodes()","button().node()",function(){var a=d();d(this.each(function(b){a=a.add(b.inst.node(b.node))}));return a});p.Api.registerPlural("buttons().processing()","button().processing()",function(a){return a===l?this.map(function(a){return a.inst.processing(a.node)}):this.each(function(b){b.inst.processing(b.node,a)})});p.Api.registerPlural("buttons().text()","button().text()",function(a){return a===
l?this.map(function(a){return a.inst.text(a.node)}):this.each(function(b){b.inst.text(b.node,a)})});p.Api.registerPlural("buttons().trigger()","button().trigger()",function(){return this.each(function(a){a.inst.node(a.node).trigger("click")})});p.Api.registerPlural("buttons().containers()","buttons().container()",function(){var a=d(),b=this._groupSelector;this.iterator(!0,"table",function(c){if(c._buttons){c=t.instanceSelector(b,c._buttons);for(var d=0,f=c.length;d<f;d++)a=a.add(c[d].container())}});
return a});p.Api.register("button().add()",function(a,b){var c=this.context;c.length&&(c=t.instanceSelector(this._groupSelector,c[0]._buttons),c.length&&c[0].add(b,a));return this.button(this._groupSelector,a)});p.Api.register("buttons().destroy()",function(){this.pluck("inst").unique().each(function(a){a.destroy()});return this});p.Api.registerPlural("buttons().remove()","buttons().remove()",function(){this.each(function(a){a.inst.remove(a.node)});return this});var w;p.Api.register("buttons.info()",
function(a,b,c){var e=this;if(!1===a)return d("#datatables_buttons_info").fadeOut(function(){d(this).remove()}),clearTimeout(w),w=null,this;w&&clearTimeout(w);d("#datatables_buttons_info").length&&d("#datatables_buttons_info").remove();a=a?"<h2>"+a+"</h2>":"";d('<div id="datatables_buttons_info" class="dt-button-info"/>').html(a).append(d("<div/>")["string"===typeof b?"html":"append"](b)).css("display","none").appendTo("body").fadeIn();c!==l&&0!==c&&(w=setTimeout(function(){e.buttons.info(!1)},c));
return this});p.Api.register("buttons.exportData()",function(a){if(this.context.length)return D(new p.Api(this.context[0]),a)});p.Api.register("buttons.exportInfo()",function(a){a||(a={});var b=a;var c="*"===b.filename&&"*"!==b.title&&b.title!==l&&null!==b.title&&""!==b.title?b.title:b.filename;"function"===typeof c&&(c=c());c===l||null===c?c=null:(-1!==c.indexOf("*")&&(c=d.trim(c.replace("*",d("head > title").text()))),c=c.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g,""),(b=x(b.extension))||
(b=""),c+=b);b=x(a.title);b=null===b?null:-1!==b.indexOf("*")?b.replace("*",d("head > title").text()||"Exported data"):b;return{filename:c,title:b,messageTop:z(this,a.message||a.messageTop,"top"),messageBottom:z(this,a.messageBottom,"bottom")}});var x=function(a){return null===a||a===l?null:"function"===typeof a?a():a},z=function(a,b,c){b=x(b);if(null===b)return null;a=d("caption",a.table().container()).eq(0);return"*"===b?a.css("caption-side")!==c?null:a.length?a.text():"":b},A=d("<textarea/>")[0],
D=function(a,b){var c=d.extend(!0,{},{rows:null,columns:"",modifier:{search:"applied",order:"applied"},orthogonal:"display",stripHtml:!0,stripNewlines:!0,decodeEntities:!0,trim:!0,format:{header:function(a){return e(a)},footer:function(a){return e(a)},body:function(a){return e(a)}},customizeData:null},b),e=function(a){if("string"!==typeof a)return a;a=a.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,"");a=a.replace(/<!\-\-.*?\-\->/g,"");c.stripHtml&&(a=a.replace(/<[^>]*>/g,""));c.trim&&
(a=a.replace(/^\s+|\s+$/g,""));c.stripNewlines&&(a=a.replace(/\n/g," "));c.decodeEntities&&(A.innerHTML=a,a=A.value);return a};b=a.columns(c.columns).indexes().map(function(b){var d=a.column(b).header();return c.format.header(d.innerHTML,b,d)}).toArray();var f=a.table().footer()?a.columns(c.columns).indexes().map(function(b){var d=a.column(b).footer();return c.format.footer(d?d.innerHTML:"",b,d)}).toArray():null,g=d.extend({},c.modifier);a.select&&"function"===typeof a.select.info&&g.selected===l&&
a.rows(c.rows,d.extend({selected:!0},g)).any()&&d.extend(g,{selected:!0});g=a.rows(c.rows,g).indexes().toArray();var h=a.cells(g,c.columns);g=h.render(c.orthogonal).toArray();h=h.nodes().toArray();for(var k=b.length,p=[],m=0,n=0,q=0<k?g.length/k:0;n<q;n++){for(var t=[k],r=0;r<k;r++)t[r]=c.format.body(g[m],n,r,h[m]),m++;p[n]=t}b={header:b,footer:f,body:p};c.customizeData&&c.customizeData(b);return b};d.fn.dataTable.Buttons=t;d.fn.DataTable.Buttons=t;d(n).on("init.dt plugin-init.dt",function(a,b){"dt"===
a.namespace&&(a=b.oInit.buttons||p.defaults.buttons)&&!b._buttons&&(new t(b,a)).container()});p.ext.feature.push({fnInit:u,cFeature:"B"});p.ext.features&&p.ext.features.register("buttons",u);return t});

View File

@ -67,6 +67,13 @@ function createVisualConsole(
? JSON.parse(data.items) ? JSON.parse(data.items)
: data.items; : data.items;
// Add the datetime when the item was received.
var receivedAt = new Date();
items.map(function(item) {
item["receivedAt"] = receivedAt;
return item;
});
var prevProps = visualConsole.props; var prevProps = visualConsole.props;
// Update the data structure. // Update the data structure.
visualConsole.props = props; visualConsole.props = props;
@ -116,7 +123,7 @@ function createVisualConsole(
try { try {
visualConsole = new VisualConsole(container, props, items); visualConsole = new VisualConsole(container, props, items);
// VC Item clicked. // VC Item clicked.
visualConsole.onClick(function(e) { visualConsole.onItemClick(function(e) {
// Override the link to another VC if it isn't on remote console. // Override the link to another VC if it isn't on remote console.
if ( if (
e.data && e.data &&
@ -132,6 +139,48 @@ function createVisualConsole(
updateVisualConsole(e.data.linkedLayoutId, updateInterval); updateVisualConsole(e.data.linkedLayoutId, updateInterval);
} }
}); });
// VC Item moved.
visualConsole.onItemMoved(function(e) {
var id = e.item.props.id;
var data = {
x: e.newPosition.x,
y: e.newPosition.y
};
var taskId = "visual-console-item-move-" + id;
asyncTaskManager
.add(taskId, function(done) {
var abortable = updateVisualConsoleItem(
baseUrl,
visualConsole.props.id,
id,
data,
function(error, data) {
if (!error && !data) return;
if (error) {
console.log(
"[ERROR]",
"[VISUAL-CONSOLE-CLIENT]",
"[API]",
error ? error.message : "Invalid response"
);
// Move the element to its initial position.
e.item.move(e.prevPosition.x, e.prevPosition.y);
}
done();
}
);
return {
cancel: function() {
abortable.abort();
}
};
})
.init();
});
if (updateInterval != null && updateInterval > 0) { if (updateInterval != null && updateInterval > 0) {
// Start an interval to update the Visual Console. // Start an interval to update the Visual Console.
@ -259,6 +308,75 @@ function loadVisualConsoleData(baseUrl, vcId, callback) {
}; };
} }
/**
* Fetch a Visual Console's structure and its items.
* @param {string} baseUrl Base URL to build the API path.
* @param {number} vcId Identifier of the Visual Console.
* @param {number} vcItemId Identifier of the Visual Console's item.
* @param {Object} data Data we want to save.
* @param {function} callback Function to be executed on request success or fail.
* @return {Object} Cancellable. Object which include and .abort([statusText]) function.
*/
// eslint-disable-next-line no-unused-vars
function updateVisualConsoleItem(baseUrl, vcId, vcItemId, data, callback) {
// var apiPath = baseUrl + "/include/rest-api";
var apiPath = baseUrl + "/ajax.php";
var jqXHR = null;
// Cancel the ajax requests.
var abort = function(textStatus) {
if (textStatus == null) textStatus = "abort";
// -- XMLHttpRequest.readyState --
// Value State Description
// 0 UNSENT Client has been created. open() not called yet.
// 4 DONE The operation is complete.
if (jqXHR.readyState !== 0 && jqXHR.readyState !== 4)
jqXHR.abort(textStatus);
};
// Failed request handler.
var handleFail = function(jqXHR, textStatus, errorThrown) {
abort();
// Manually aborted or not.
if (textStatus === "abort") {
callback();
} else {
var error = new Error(errorThrown);
error.request = jqXHR;
callback(error);
}
};
// Function which handle success case.
var handleSuccess = function(data) {
callback(null, data);
};
// Visual Console container request.
jqXHR = jQuery
// .get(apiPath + "/visual-consoles/" + vcId, null, "json")
.get(
apiPath,
{
page: "include/rest-api/index",
updateVisualConsoleItem: 1,
visualConsoleId: vcId,
visualConsoleItemId: vcItemId,
data: data
},
"json"
)
.done(handleSuccess)
.fail(handleFail);
// Abortable.
return {
abort: abort
};
}
// TODO: Delete the functions below when you can. // TODO: Delete the functions below when you can.
/************************************** /**************************************
These functions require jQuery library These functions require jQuery library

View File

@ -207,11 +207,7 @@ final class ModuleGraph extends Item
// Custom graph. // Custom graph.
if (empty($customGraphId) === false) { if (empty($customGraphId) === false) {
$customGraph = \db_get_row_filter( $customGraph = \db_get_row('tgraph', 'id_graph', $customGraphId);
'tgraph',
'id_graph',
$customGraphId
);
$params = [ $params = [
'period' => $period, 'period' => $period,

File diff suppressed because one or more lines are too long

View File

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

View File

@ -25,6 +25,12 @@
user-select: text; user-select: text;
} }
.visual-console-item.is-editing {
border: 2px dashed #33ccff;
-webkit-transform: translateX(-2px) translateY(-2px);
transform: translateX(-2px) translateY(-2px);
}
@font-face { @font-face {
font-family: Alarm Clock; font-family: Alarm Clock;
src: url(alarm-clock.ttf); src: url(alarm-clock.ttf);

View File

@ -1 +1 @@
{"version":3,"sources":["webpack:///main.css","webpack:///styles.css"],"names":[],"mappings":"AAAA;EACE,gBAAgB;EAChB,kBAAkB;EAClB,4BAA4B;EAC5B,0BAA0B;EAC1B,2BAA2B;AAC7B;;AAEA;EACE,kBAAkB;EAClB,oBAAa;EAAb,oBAAa;EAAb,aAAa;EACb,2BAAuB;EAAvB,8BAAuB;MAAvB,2BAAuB;UAAvB,uBAAuB;EACvB,qBAAqB;EACrB,yBAAmB;MAAnB,sBAAmB;UAAnB,mBAAmB;EACnB,yBAAiB;KAAjB,sBAAiB;MAAjB,qBAAiB;UAAjB,iBAAiB;AACnB;;ACfA;EACE,wBAAwB;EACxB,0BAA2B;AAC7B;;AAEA,kBAAkB;;AAElB;EACE,oBAAa;EAAb,oBAAa;EAAb,aAAa;EACb,4BAAsB;EAAtB,6BAAsB;MAAtB,0BAAsB;UAAtB,sBAAsB;EACtB,wBAAuB;MAAvB,qBAAuB;UAAvB,uBAAuB;EACvB,qBAAqB;EACrB,0BAAqB;MAArB,qBAAqB;EACrB,yBAAmB;MAAnB,sBAAmB;UAAnB,mBAAmB;AACrB;;AAEA;EACE,6DAA6D;EAC7D,eAAe;;EAEf,0BAA0B;EAC1B,mCAAmC;EACnC,kCAAkC;EAClC,kCAAkC;EAClC,wCAAwC;AAC1C;;AAEA;EACE,eAAe;AACjB;;AAEA;EACE,eAAe;AACjB;;AAEA,iBAAiB;;AAEjB;EACE,kBAAkB;AACpB;;AAEA;EACE,qDAA6C;UAA7C,6CAA6C;AAC/C;;AAEA;EACE,sDAA8C;UAA9C,8CAA8C;AAChD;;AAEA;EACE,oDAA4C;UAA5C,4CAA4C;AAC9C","file":"vc.main.css","sourcesContent":["#visual-console-container {\n margin: 0px auto;\n position: relative;\n background-repeat: no-repeat;\n background-size: 100% 100%;\n background-position: center;\n}\n\n.visual-console-item {\n position: absolute;\n display: flex;\n flex-direction: initial;\n justify-items: center;\n align-items: center;\n user-select: text;\n}\n","@font-face {\n font-family: Alarm Clock;\n src: url(./alarm-clock.ttf);\n}\n\n/* Digital clock */\n\n.visual-console-item .digital-clock {\n display: flex;\n flex-direction: column;\n justify-content: center;\n justify-items: center;\n align-content: center;\n align-items: center;\n}\n\n.visual-console-item .digital-clock > span {\n font-family: \"Alarm Clock\", \"Courier New\", Courier, monospace;\n font-size: 50px;\n\n /* To improve legibility */\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px;\n}\n\n.visual-console-item .digital-clock > span.date {\n font-size: 25px;\n}\n\n.visual-console-item .digital-clock > span.timezone {\n font-size: 25px;\n}\n\n/* Analog clock */\n\n.visual-console-item .analogic-clock {\n text-align: center;\n}\n\n.visual-console-item .analogic-clock .hour-hand {\n animation: rotate-hour 43200s infinite linear;\n}\n\n.visual-console-item .analogic-clock .minute-hand {\n animation: rotate-minute 3600s infinite linear;\n}\n\n.visual-console-item .analogic-clock .second-hand {\n animation: rotate-second 60s infinite linear;\n}\n"],"sourceRoot":""} {"version":3,"sources":["webpack:///main.css","webpack:///styles.css"],"names":[],"mappings":"AAAA;EACE,gBAAgB;EAChB,kBAAkB;EAClB,4BAA4B;EAC5B,0BAA0B;EAC1B,2BAA2B;AAC7B;;AAEA;EACE,kBAAkB;EAClB,oBAAa;EAAb,oBAAa;EAAb,aAAa;EACb,2BAAuB;EAAvB,8BAAuB;MAAvB,2BAAuB;UAAvB,uBAAuB;EACvB,qBAAqB;EACrB,yBAAmB;MAAnB,sBAAmB;UAAnB,mBAAmB;EACnB,yBAAiB;KAAjB,sBAAiB;MAAjB,qBAAiB;UAAjB,iBAAiB;AACnB;;AAEA;EACE,0BAA0B;EAC1B,oDAA4C;UAA5C,4CAA4C;AAC9C;;ACpBA;EACE,wBAAwB;EACxB,0BAA2B;AAC7B;;AAEA,kBAAkB;;AAElB;EACE,oBAAa;EAAb,oBAAa;EAAb,aAAa;EACb,4BAAsB;EAAtB,6BAAsB;MAAtB,0BAAsB;UAAtB,sBAAsB;EACtB,wBAAuB;MAAvB,qBAAuB;UAAvB,uBAAuB;EACvB,qBAAqB;EACrB,0BAAqB;MAArB,qBAAqB;EACrB,yBAAmB;MAAnB,sBAAmB;UAAnB,mBAAmB;AACrB;;AAEA;EACE,6DAA6D;EAC7D,eAAe;;EAEf,0BAA0B;EAC1B,mCAAmC;EACnC,kCAAkC;EAClC,kCAAkC;EAClC,wCAAwC;AAC1C;;AAEA;EACE,eAAe;AACjB;;AAEA;EACE,eAAe;AACjB;;AAEA,iBAAiB;;AAEjB;EACE,kBAAkB;AACpB;;AAEA;EACE,qDAA6C;UAA7C,6CAA6C;AAC/C;;AAEA;EACE,sDAA8C;UAA9C,8CAA8C;AAChD;;AAEA;EACE,oDAA4C;UAA5C,4CAA4C;AAC9C","file":"vc.main.css","sourcesContent":["#visual-console-container {\n margin: 0px auto;\n position: relative;\n background-repeat: no-repeat;\n background-size: 100% 100%;\n background-position: center;\n}\n\n.visual-console-item {\n position: absolute;\n display: flex;\n flex-direction: initial;\n justify-items: center;\n align-items: center;\n user-select: text;\n}\n\n.visual-console-item.is-editing {\n border: 2px dashed #33ccff;\n transform: translateX(-2px) translateY(-2px);\n}\n","@font-face {\n font-family: Alarm Clock;\n src: url(./alarm-clock.ttf);\n}\n\n/* Digital clock */\n\n.visual-console-item .digital-clock {\n display: flex;\n flex-direction: column;\n justify-content: center;\n justify-items: center;\n align-content: center;\n align-items: center;\n}\n\n.visual-console-item .digital-clock > span {\n font-family: \"Alarm Clock\", \"Courier New\", Courier, monospace;\n font-size: 50px;\n\n /* To improve legibility */\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px;\n}\n\n.visual-console-item .digital-clock > span.date {\n font-size: 25px;\n}\n\n.visual-console-item .digital-clock > span.timezone {\n font-size: 25px;\n}\n\n/* Analog clock */\n\n.visual-console-item .analogic-clock {\n text-align: center;\n}\n\n.visual-console-item .analogic-clock .hour-hand {\n animation: rotate-hour 43200s infinite linear;\n}\n\n.visual-console-item .analogic-clock .minute-hand {\n animation: rotate-minute 3600s infinite linear;\n}\n\n.visual-console-item .analogic-clock .second-hand {\n animation: rotate-second 60s infinite linear;\n}\n"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -129,7 +129,7 @@
<div style='height: 10px'> <div style='height: 10px'>
<?php <?php
$version = '7.0NG.735'; $version = '7.0NG.735';
$build = '190610'; $build = '190612';
$banner = "v$version Build $build"; $banner = "v$version Build $build";
error_reporting(0); error_reporting(0);

View File

@ -1389,6 +1389,34 @@ if (!empty($result)) {
true true
); );
} }
} else if ($row['estado'] == 3) {
if (is_numeric($row['datos'])) {
$data[6] = ui_print_status_image(
STATUS_MODULE_UNKNOWN,
__('UNKNOWN').': '.remove_right_zeros(number_format($row['datos'], $config['graph_precision'])),
true
);
} else {
$data[6] = ui_print_status_image(
STATUS_MODULE_UNKNOWN,
__('UNKNOWN').': '.$row['datos'],
true
);
}
} else if ($row['estado'] == 4) {
if (is_numeric($row['datos'])) {
$data[6] = ui_print_status_image(
STATUS_MODULE_NO_DATA,
__('NO DATA').': '.remove_right_zeros(number_format($row['datos'], $config['graph_precision'])),
true
);
} else {
$data[6] = ui_print_status_image(
STATUS_MODULE_NO_DATA,
__('NO DATA').': '.$row['datos'],
true
);
}
} else { } else {
$last_status = modules_get_agentmodule_last_status( $last_status = modules_get_agentmodule_last_status(
$row['id_agente_modulo'] $row['id_agente_modulo']

View File

@ -198,6 +198,14 @@ $visualConsoleItems = VisualConsole::getItemsFromDB(
} }
} }
} }
// Add the datetime when the item was received.
var receivedAt = new Date();
items.map(function(item) {
item["receivedAt"] = receivedAt;
return item;
});
var visualConsoleManager = createVisualConsole( var visualConsoleManager = createVisualConsole(
container, container,
props, props,

View File

@ -155,6 +155,16 @@ if (!is_metaconsole()) {
html_print_input_hidden('metaconsole', 1); html_print_input_hidden('metaconsole', 1);
} }
if ($pure === false) {
echo '<div class="visual-console-edit-controls">';
echo '<span>'.__('Move and resize mode').'</span>';
echo '<span>';
echo html_print_checkbox_switch('edit-mode', 1, false, true);
echo '</span>';
echo '</div>';
echo '<br />';
}
echo '<div id="visual-console-container"></div>'; echo '<div id="visual-console-container"></div>';
if ($pure === true) { if ($pure === true) {
@ -306,6 +316,14 @@ $visualConsoleItems = VisualConsole::getItemsFromDB(
} }
} }
} }
// Add the datetime when the item was received.
var receivedAt = new Date();
items.map(function(item) {
item["receivedAt"] = receivedAt;
return item;
});
var visualConsoleManager = createVisualConsole( var visualConsoleManager = createVisualConsole(
container, container,
props, props,
@ -315,6 +333,17 @@ $visualConsoleItems = VisualConsole::getItemsFromDB(
handleUpdate handleUpdate
); );
// Enable/disable the edition mode.
$('input[name=edit-mode]').change(function(event) {
if ($(this).prop('checked')) {
visualConsoleManager.visualConsole.enableEditMode();
visualConsoleManager.changeUpdateInterval(0);
} else {
visualConsoleManager.visualConsole.disableEditMode();
visualConsoleManager.changeUpdateInterval(<?php echo ($refr * 1000); ?>); // To ms.
}
});
// Update the data fetch interval. // Update the data fetch interval.
$('select#vc-refr').change(function(event) { $('select#vc-refr').change(function(event) {
var refr = Number.parseInt(event.target.value); var refr = Number.parseInt(event.target.value);

View File

@ -3,7 +3,7 @@
# #
%define name pandorafms_console %define name pandorafms_console
%define version 7.0NG.735 %define version 7.0NG.735
%define release 190610 %define release 190612
# User and Group under which Apache is running # User and Group under which Apache is running
%define httpd_name httpd %define httpd_name httpd

View File

@ -3,7 +3,7 @@
# #
%define name pandorafms_console %define name pandorafms_console
%define version 7.0NG.735 %define version 7.0NG.735
%define release 190610 %define release 190612
# User and Group under which Apache is running # User and Group under which Apache is running
%define httpd_name httpd %define httpd_name httpd

View File

@ -3,7 +3,7 @@
# #
%define name pandorafms_console %define name pandorafms_console
%define version 7.0NG.735 %define version 7.0NG.735
%define release 190610 %define release 190612
%define httpd_name httpd %define httpd_name httpd
# User and Group under which Apache is running # User and Group under which Apache is running
%define httpd_name apache2 %define httpd_name apache2

View File

@ -1,5 +1,5 @@
package: pandorafms-server package: pandorafms-server
Version: 7.0NG.735-190610 Version: 7.0NG.735-190612
Architecture: all Architecture: all
Priority: optional Priority: optional
Section: admin Section: admin

View File

@ -14,7 +14,7 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
pandora_version="7.0NG.735-190610" pandora_version="7.0NG.735-190612"
package_cpan=0 package_cpan=0
package_pandora=1 package_pandora=1

View File

@ -45,7 +45,7 @@ our @EXPORT = qw(
# version: Defines actual version of Pandora Server for this module only # version: Defines actual version of Pandora Server for this module only
my $pandora_version = "7.0NG.735"; my $pandora_version = "7.0NG.735";
my $pandora_build = "190610"; my $pandora_build = "190612";
our $VERSION = $pandora_version." ".$pandora_build; our $VERSION = $pandora_version." ".$pandora_build;
# Setup hash # Setup hash

View File

@ -32,7 +32,7 @@ our @ISA = qw(Exporter);
# version: Defines actual version of Pandora Server for this module only # version: Defines actual version of Pandora Server for this module only
my $pandora_version = "7.0NG.735"; my $pandora_version = "7.0NG.735";
my $pandora_build = "190610"; my $pandora_build = "190612";
our $VERSION = $pandora_version." ".$pandora_build; our $VERSION = $pandora_version." ".$pandora_build;
our %EXPORT_TAGS = ( 'all' => [ qw() ] ); our %EXPORT_TAGS = ( 'all' => [ qw() ] );

View File

@ -3,7 +3,7 @@
# #
%define name pandorafms_server %define name pandorafms_server
%define version 7.0NG.735 %define version 7.0NG.735
%define release 190610 %define release 190612
Summary: Pandora FMS Server Summary: Pandora FMS Server
Name: %{name} Name: %{name}

View File

@ -3,7 +3,7 @@
# #
%define name pandorafms_server %define name pandorafms_server
%define version 7.0NG.735 %define version 7.0NG.735
%define release 190610 %define release 190612
Summary: Pandora FMS Server Summary: Pandora FMS Server
Name: %{name} Name: %{name}

View File

@ -9,7 +9,7 @@
# ********************************************************************** # **********************************************************************
PI_VERSION="7.0NG.735" PI_VERSION="7.0NG.735"
PI_BUILD="190610" PI_BUILD="190612"
MODE=$1 MODE=$1
if [ $# -gt 1 ]; then if [ $# -gt 1 ]; then

View File

@ -34,7 +34,7 @@ use PandoraFMS::Config;
use PandoraFMS::DB; use PandoraFMS::DB;
# version: define current version # version: define current version
my $version = "7.0NG.735 PS190610"; my $version = "7.0NG.735 PS190612";
# Pandora server configuration # Pandora server configuration
my %conf; my %conf;

View File

@ -36,7 +36,7 @@ use Encode::Locale;
Encode::Locale::decode_argv; Encode::Locale::decode_argv;
# version: define current version # version: define current version
my $version = "7.0NG.735 PS190610"; my $version = "7.0NG.735 PS190612";
# save program name for logging # save program name for logging
my $progname = basename($0); my $progname = basename($0);

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,
@ -7,9 +13,11 @@ import {
notEmptyStringOr, notEmptyStringOr,
replaceMacros, replaceMacros,
humanDate, humanDate,
humanTime humanTime,
addMovementListener,
debounce
} 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 +60,20 @@ 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;
}
export interface ItemMovedEvent {
item: VisualConsoleItem<ItemProps>;
prevPosition: Position;
newPosition: Position;
} }
/** /**
@ -89,7 +103,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 +132,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;
@ -125,6 +141,8 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
protected readonly childElementRef: HTMLElement; protected readonly childElementRef: HTMLElement;
// Event manager for click events. // Event manager for click events.
private readonly clickEventManager = new TypedEvent<ItemClickEvent<Props>>(); private readonly clickEventManager = new TypedEvent<ItemClickEvent<Props>>();
// Event manager for moved events.
private readonly movedEventManager = new TypedEvent<ItemMovedEvent>();
// Event manager for remove events. // Event manager for remove events.
private readonly removeEventManager = new TypedEvent< private readonly removeEventManager = new TypedEvent<
ItemRemoveEvent<Props> ItemRemoveEvent<Props>
@ -132,14 +150,67 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
// List of references to clean the event listeners. // List of references to clean the event listeners.
private readonly disposables: Disposable[] = []; private readonly disposables: Disposable[] = [];
// This function will only run the 2nd arg function after the time
// of the first arg have passed after its last execution.
private debouncedMovementSave = debounce(
500, // ms.
(x: Position["x"], y: Position["y"]) => {
const prevPosition = {
x: this.props.x,
y: this.props.y
};
const newPosition = {
x: x,
y: y
};
// Save the new position to the props.
this.move(x, y);
// Emit the movement event.
this.movedEventManager.emit({
item: this,
prevPosition: prevPosition,
newPosition: newPosition
});
}
);
// This property will store the function
// to clean the movement listener.
private removeMovement: Function | null = null;
/**
* Start the movement funtionality.
* @param element Element to move inside its container.
*/
private initMovementListener(element: HTMLElement): void {
this.removeMovement = addMovementListener(
element,
(x: Position["x"], y: Position["y"]) => {
// Move the DOM element.
this.moveElement(x, y);
// Run the save function.
this.debouncedMovementSave(x, y);
}
);
}
/**
* Stop the movement fun
*/
private stopMovementListener(): void {
if (this.removeMovement) {
this.removeMovement();
this.removeMovement = null;
}
}
/** /**
* To create a new element which will be inside the item box. * To create a new element which will be inside the item box.
* @return Item. * @return Item.
*/ */
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
@ -173,20 +244,38 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
private createContainerDomElement(): HTMLElement { private createContainerDomElement(): HTMLElement {
let box; let box;
if (this.props.isLinkEnabled) { if (this.props.isLinkEnabled) {
box = document.createElement("a"); box = document.createElement("a") as HTMLAnchorElement;
box as HTMLAnchorElement;
if (this.props.link) box.href = this.props.link; if (this.props.link) box.href = this.props.link;
} else { } else {
box = document.createElement("div"); box = document.createElement("div") as HTMLDivElement;
box as HTMLDivElement;
} }
box.className = "visual-console-item"; box.className = "visual-console-item";
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 => // Init the click listener.
this.clickEventManager.emit({ data: this.props, nativeEvent: e }); box.addEventListener("click", e => {
if (this.meta.editMode) {
e.preventDefault();
e.stopPropagation();
} else {
this.clickEventManager.emit({ data: this.props, nativeEvent: e });
}
});
// Metadata state.
if (this.meta.editMode) {
box.classList.add("is-editing");
// Init the movement listener.
this.initMovementListener(box);
}
if (this.meta.isFetching) {
box.classList.add("is-fetching");
}
if (this.meta.isUpdating) {
box.classList.add("is-updating");
}
return box; return box;
} }
@ -310,7 +399,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 +449,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 +497,31 @@ 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");
this.initMovementListener(this.elementRef);
} else {
this.elementRef.classList.remove("is-editing");
this.stopMovementListener();
}
}
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");
}
}
} }
/** /**
@ -533,6 +677,22 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
return disposable; return disposable;
} }
/**
* To add an event handler to the movement of visual console elements.
* @param listener Function which is going to be executed when a linked console is moved.
*/
public onMoved(listener: Listener<ItemMovedEvent>): Disposable {
/*
* The '.on' function returns a function which will clean the event
* listener when executed. We store all the 'dispose' functions to
* call them when the item should be cleared.
*/
const disposable = this.movedEventManager.on(listener);
this.disposables.push(disposable);
return disposable;
}
/** /**
* To add an event handler to the removal of the item. * To add an event handler to the removal of the item.
* @param listener Function which is going to be executed when a item is removed. * @param listener Function which is going to be executed when a item is removed.

View File

@ -1,15 +1,17 @@
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,
ItemProps, ItemProps,
ItemClickEvent, ItemClickEvent,
ItemRemoveEvent ItemRemoveEvent,
ItemMovedEvent
} from "./Item"; } from "./Item";
import StaticGraph, { staticGraphPropsDecoder } from "./items/StaticGraph"; import StaticGraph, { staticGraphPropsDecoder } from "./items/StaticGraph";
import Icon, { iconPropsDecoder } from "./items/Icon"; import Icon, { iconPropsDecoder } from "./items/Icon";
@ -24,7 +26,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 +34,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 +84,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 +151,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 {
@ -201,6 +205,8 @@ export default class VisualConsole {
private readonly clickEventManager = new TypedEvent< private readonly clickEventManager = new TypedEvent<
ItemClickEvent<ItemProps> ItemClickEvent<ItemProps>
>(); >();
// Event manager for move events.
private readonly movedEventManager = new TypedEvent<ItemMovedEvent>();
// List of references to clean the event listeners. // List of references to clean the event listeners.
private readonly disposables: Disposable[] = []; private readonly disposables: Disposable[] = [];
@ -213,6 +219,15 @@ export default class VisualConsole {
// console.log(`Clicked element #${e.data.id}`, e); // console.log(`Clicked element #${e.data.id}`, e);
}; };
/**
* React to a movement on an element.
* @param e Event object.
*/
private handleElementMovement: (e: ItemMovedEvent) => void = e => {
this.movedEventManager.emit(e);
// console.log(`Moved element #${e.item.props.id}`, e);
};
/** /**
* Clear some element references. * Clear some element references.
* @param e Event object. * @param e Event object.
@ -226,8 +241,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);
@ -261,6 +276,7 @@ export default class VisualConsole {
this.elementIds.push(itemInstance.props.id); this.elementIds.push(itemInstance.props.id);
// Item event handlers. // Item event handlers.
itemInstance.onClick(this.handleElementClick); itemInstance.onClick(this.handleElementClick);
itemInstance.onMoved(this.handleElementMovement);
itemInstance.onRemove(this.handleElementRemove); itemInstance.onRemove(this.handleElementRemove);
// Add the item to the DOM. // Add the item to the DOM.
this.containerRef.append(itemInstance.elementRef); this.containerRef.append(itemInstance.elementRef);
@ -288,13 +304,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 +546,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.
@ -546,7 +565,9 @@ export default class VisualConsole {
* Add an event handler to the click of the linked visual console elements. * Add an event handler to the click of the linked visual console elements.
* @param listener Function which is going to be executed when a linked console is clicked. * @param listener Function which is going to be executed when a linked console is clicked.
*/ */
public onClick(listener: Listener<ItemClickEvent<ItemProps>>): Disposable { public onItemClick(
listener: Listener<ItemClickEvent<ItemProps>>
): Disposable {
/* /*
* The '.on' function returns a function which will clean the event * The '.on' function returns a function which will clean the event
* listener when executed. We store all the 'dispose' functions to * listener when executed. We store all the 'dispose' functions to
@ -557,4 +578,40 @@ export default class VisualConsole {
return disposable; return disposable;
} }
/**
* Add an event handler to the movement of the visual console elements.
* @param listener Function which is going to be executed when a linked console is moved.
*/
public onItemMoved(listener: Listener<ItemMovedEvent>): Disposable {
/*
* The '.on' function returns a function which will clean the event
* listener when executed. We store all the 'dispose' functions to
* call them when the item should be cleared.
*/
const disposable = this.movedEventManager.on(listener);
this.disposables.push(disposable);
return disposable;
}
/**
* Enable the edition mode.
*/
public enableEditMode(): void {
this.elements.forEach(item => {
item.meta = { ...item.meta, editMode: true };
});
this.containerRef.classList.add("is-editing");
}
/**
* Disable the edition mode.
*/
public disableEditMode(): void {
this.elements.forEach(item => {
item.meta = { ...item.meta, editMode: false };
});
this.containerRef.classList.remove("is-editing");
}
} }

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.
@ -332,3 +374,187 @@ export function replaceMacros(macros: Macro[], text: string): string {
text text
); );
} }
/**
* Create a function which will limit the rate of execution of
* the selected function to one time for the selected interval.
* @param delay Interval.
* @param fn Function to be executed at a limited rate.
*/
export function throttle<T, R>(delay: number, fn: (...args: T[]) => R) {
let last = 0;
return (...args: T[]) => {
const now = Date.now();
if (now - last < delay) return;
last = now;
return fn(...args);
};
}
/**
* Create a function which will call the selected function only
* after the interval time has passed after its last execution.
* @param delay Interval.
* @param fn Function to be executed after the last call.
*/
export function debounce<T>(delay: number, fn: (...args: T[]) => void) {
let timerRef: number | null = null;
return (...args: T[]) => {
if (timerRef !== null) window.clearTimeout(timerRef);
timerRef = window.setTimeout(() => {
fn(...args);
timerRef = null;
}, delay);
};
}
/**
* Retrieve the offset of an element relative to the page.
* @param el Node used to calculate the offset.
*/
function getOffset(el: HTMLElement | null) {
let x = 0;
let y = 0;
while (el && !Number.isNaN(el.offsetLeft) && !Number.isNaN(el.offsetTop)) {
x += el.offsetLeft - el.scrollLeft;
y += el.offsetTop - el.scrollTop;
el = el.offsetParent as HTMLElement | null;
}
return { top: y, left: x };
}
/**
* Add the grab & move functionality to a certain element inside it's container.
*
* @param element Element to move.
* @param onMoved Function to execute when the element moves.
*
* @return A function which will clean the event handlers when executed.
*/
export function addMovementListener(
element: HTMLElement,
onMoved: (x: Position["x"], y: Position["y"]) => void
): Function {
const container = element.parentElement as HTMLElement;
// Store the initial draggable state.
const isDraggable = element.draggable;
// Init the coordinates.
let lastX: Position["x"] = 0;
let lastY: Position["y"] = 0;
let lastMouseX: Position["x"] = 0;
let lastMouseY: Position["y"] = 0;
let mouseElementOffsetX: Position["x"] = 0;
let mouseElementOffsetY: Position["y"] = 0;
// Bounds.
let containerBounds = container.getBoundingClientRect();
let containerTop = getOffset(container).top;
let containerBottom = containerTop + containerBounds.height;
let containerLeft = getOffset(container).left;
let containerRight = containerLeft + containerBounds.width;
let elementBounds = element.getBoundingClientRect();
let borderWidth = window.getComputedStyle(element).borderWidth || "0";
let borderFix = Number.parseInt(borderWidth) * 2;
// Will run onMoved 32ms after its last execution.
const debouncedMovement = debounce(32, (x: Position["x"], y: Position["y"]) =>
onMoved(x, y)
);
// Will run onMoved one time max every 16ms.
const throttledMovement = throttle(16, (x: Position["x"], y: Position["y"]) =>
onMoved(x, y)
);
const handleMove = (e: MouseEvent) => {
// Calculate the new element coordinates.
let x = 0;
let y = 0;
// TODO: Document.
if (e.pageX < containerLeft) x = 0;
else if (e.pageX > containerRight) x = containerBounds.width;
else x = e.pageX - lastMouseX + lastX;
if (e.pageY < containerTop) y = 0;
else if (e.pageY > containerBottom)
y = containerBounds.height - elementBounds.height + borderFix;
else y = e.pageY - lastMouseY + lastY;
if (x < 0) x = 0;
else if (x + elementBounds.width - borderFix > containerBounds.width)
x = containerBounds.width - elementBounds.width + borderFix;
if (y < 0) y = 0;
else if (y + elementBounds.height - borderFix > containerBounds.height)
y = containerBounds.height - elementBounds.height + borderFix;
// Run the movement events.
throttledMovement(x, y);
debouncedMovement(x, y);
// Store the coordinates of the element.
lastX = x;
lastY = y;
// Store the last mouse coordinates.
lastMouseX = e.pageX;
lastMouseY = e.pageY;
};
const handleEnd = () => {
// Reset the positions.
lastX = 0;
lastY = 0;
lastMouseX = 0;
lastMouseY = 0;
// Remove the move event.
document.removeEventListener("mousemove", handleMove);
// Clean itself.
document.removeEventListener("mouseup", handleEnd);
// Reset the draggable property to its initial state.
element.draggable = isDraggable;
// Reset the body selection property to a default state.
document.body.style.userSelect = "auto";
};
const handleStart = (e: MouseEvent) => {
e.stopPropagation();
// Disable the drag temporarily.
element.draggable = false;
// Store the difference between the cursor and
// the initial coordinates of the element.
lastX = element.offsetLeft;
lastY = element.offsetTop;
// Store the mouse position.
lastMouseX = e.pageX;
lastMouseY = e.pageY;
// Store the relative position between the mouse and the element.
mouseElementOffsetX = e.offsetX;
mouseElementOffsetY = e.offsetY;
// Initialize the bounds.
containerBounds = container.getBoundingClientRect();
containerTop = getOffset(container).top;
containerBottom = containerTop + containerBounds.height;
containerLeft = getOffset(container).left;
containerRight = containerLeft + containerBounds.width;
elementBounds = element.getBoundingClientRect();
borderWidth = window.getComputedStyle(element).borderWidth || "0";
borderFix = Number.parseInt(borderWidth) * 2;
// Listen to the mouse movement.
document.addEventListener("mousemove", handleMove);
// Listen to the moment when the mouse click is not pressed anymore.
document.addEventListener("mouseup", handleEnd);
// Limit the mouse selection of the body.
document.body.style.userSelect = "none";
};
// Event to listen the init of the movement.
element.addEventListener("mousedown", handleStart);
// Returns a function to clean the event listeners.
return () => {
element.removeEventListener("mousedown", handleStart);
handleEnd();
};
}

View File

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

View File

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

View File

@ -14,3 +14,9 @@
align-items: center; align-items: center;
user-select: text; user-select: text;
} }
.visual-console-item.is-editing {
border: 2px dashed #33ccff;
transform: translateX(-2px) translateY(-2px);
cursor: move;
}