Commit 9c706148 authored by Alain Takoudjou's avatar Alain Takoudjou

slapos_monitoring: improve monitor sync and fix bugs

Remove sync of all public folder, which reduce the amount of file to sync, fix the problem when two sync start at the same time.
Import settings doesn't sync automatically, same thing for configure monitor (useful when registering big amount of monitor instances)
Log Messages contains result of background sync and other jio error logs
parent b3b9da61
......@@ -107,7 +107,7 @@
<value> <string encoding="cdata"><![CDATA[
CACHE MANIFEST\n
# generated on Fri, 30 June 2016 17:47:33 +0000\n
# generated on Fri, 01 July 2016 11:47:33 +0000\n
# XXX + fonts\n
# images/ajax-loader.gif\n
CACHE:\n
......@@ -340,7 +340,7 @@ NETWORK:\n
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.14160.36945.51916</string> </value>
<value> <string>952.17042.64069.1194</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -358,7 +358,7 @@ NETWORK:\n
</tuple>
<state>
<tuple>
<float>1467299069.5</float>
<float>1467366906.34</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -44,6 +44,7 @@
<li><a href="#page=status_list"><i class="fa fa-th-list"></i><span data-i18n="Promises Status">Promises Status</span></a></li>
<li><a href="#page=software_instance_list"><i class="fa fa-cube"></i><span data-i18n="Software Instances">Software Instances</span></a></li>
<li><a href="#page=hosting_subscription_list"><i class="fa fa-globe"></i><span data-i18n="Hosting Subscriptions">Hosting Subscriptions</span></a></li>
<li><a href="#page=message_log"><i class="fa fa-newspaper-o" aria-hidden="true"></i><span data-i18n="Log Messages">Log Messages</span></a></li>
<li><a href="#page=settings_configurator"><i class="fa fa-cog"></i><span data-i18n="Monitoring Configuration">Monitoring Configuration</span></a></li>
<li><a href="#page=import_export"><i class="fa fa-exchange"></i><span data-i18n="Import / Export">Import / Export</span></a></li>
</ul>
......
......@@ -237,7 +237,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.29615.38155.21196</string> </value>
<value> <string>952.23194.22431.42598</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -255,7 +255,7 @@
</tuple>
<state>
<tuple>
<float>1464275565.87</float>
<float>1467794589.7</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -139,6 +139,7 @@ html.ui-mobile .ui-grid-container.ui-responsive > ul > li, html.ui-mobile .ui-gr
color: #535354;
background-color: transparent;
padding: 10px 15px 5px 15px;
line-height: 16px;
}
.overview-header > .content-title:after {
......@@ -160,8 +161,8 @@ html.ui-mobile .ui-grid-container.ui-responsive > ul > li, html.ui-mobile .ui-gr
display: block;
padding: 0;
margin: 0;
margin-top: 3px;
float: right;
font-size: 1.2em;
}
.content-details {
......@@ -313,6 +314,9 @@ html head + body .ui-panel-overview > h2 {
.label-ok, .label-OK {
background-color: green;
}
.label-info, .label-INFO {
background-color: #79c1d9;
}
.label {
display: inline;
padding: .2em .6em .3em;
......@@ -485,6 +489,7 @@ html .custom-grid-wrap table thead tr th {
}
.graph-full {
margin: 0 20px;
position: relative;
}
.graph-medium {
height: 200px;
......@@ -559,6 +564,30 @@ html .custom-grid-wrap table thead tr th {
max-width: 140px;
}
/* Log Box */
.logbox {
padding: 15px 20px 20px;
}
.logbox .description {
padding-bottom: 15px;
border-bottom: 1px dashed #e6e6e6;
display: block;
color: #7b7b7b;
}
.logbox tr {
line-height: 23px;
border-bottom: 1px solid #e6e6e6;
}
.logbox tr td {
color: #7b7b7b;
padding: 10px;
cursor: pointer;
}
.tooltipster-shadow {
border-radius: 5px;
background: #fff;
......@@ -799,11 +828,11 @@ margin-top: 5px;
border: 4px solid #024352;
border-radius: 50px;
height: 50px;
left: 75%;
left: 50%;
margin: -15px 0 0 -15px;
/*opacity: 0;*/
position: fixed;
top: 200px;
position: absolute;
top: 40px;
width: 50px;
/*animation: pulsate .5s ease-out;
animation-iteration-count: infinite;*/
......@@ -831,6 +860,33 @@ margin-top: 5px;
}
}
.loadbox {
width: 100%;
overflow: auto;
position: relative;
cursor: pointer;
border: 1px solid #efefef;
background-color: #f1f1f1;
}
.load-content {
overflow: auto;
min-height: 250px;
background-color: #fff;
}
html body div.loadbox .loadwait > a {
color: #335d77;
text-decoration: none;
font-size: 1.5em;
text-align: center;
display: block;
margin: 99px auto;
padding: 15px;
width: 40%;
cursor: pointer;
border: 1px solid #dadada;
}
/************** Media @ **********************/
@media all and (max-width: 62em) {
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.2937.49164.58675</string> </value>
<value> <string>952.27190.36100.52957</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1466607152.09</float>
<float>1467987759.33</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -4,7 +4,8 @@
"use strict";
var gadget_klass = rJS(window),
templater = gadget_klass.__template_element;
templater = gadget_klass.__template_element,
hashCode = new Rusha().digestFromString;
function getHtmlFromJson(parameter_list) {
var i,
......@@ -42,18 +43,17 @@
function saveDocument(gadget, jio_document) {
// Authenticate before save
// Normally password should already exists otherwise will redirect to login box and edited data will be lost!!!
return gadget.props.login_gadget.loginRedirect(
gadget.props.options.url,
gadget.props.options.page_options,
gadget.props.options.title,
gadget.props.options.root_title
return gadget.props.login_gadget.getUrlInfo(
hashCode(gadget.props.options.url)
)
.push(function (cred) {
var url = gadget.props.options.url;
if (gadget.props.options.path) {
url += (url.endsWith('/') ? '':'/') + gadget.props.options.path;
}
if (cred === undefined) {
cred = {};
}
gadget.props.jio_gadget.createJio({
type: "query",
sub_storage: {
......@@ -157,7 +157,8 @@
.push(function (result) {
if (result[0].status === 'ERROR') {
document.querySelector('.mfp-content .ui-text-error')
.innerHTML = 'ERROR ' + result[0].code + ': Failed to save your document!';
.innerHTML = 'ERROR ' + result[0].code + ': Failed to save your document! ' +
"Parameters cannot be saved in Offline mode.";
} else {
$.magnificPopup.close();
return updateMethod(data);}
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>950.63263.62080.55091</string> </value>
<value> <string>952.43155.12406.49305</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1463069373.0</float>
<float>1468934010.87</float>
<string>UTC</string>
</tuple>
</state>
......
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<title>Monitoring Document Edit Page</title>
<link rel="stylesheet" href="magnific-popup.css">
<link href="gadget_monitoring_custom.css" rel="stylesheet" type="text/css"/>
<!-- renderjs -->
<script src="rsvp.js" type="text/javascript"></script>
<script src="renderjs.js" type="text/javascript"></script>
<script id="login-toltip-template" type="text/x-handlebars-template">
</script>
<!-- magnific-popup -->
<script src="jquery.magnific-popup.min.js" type="text/javascript"></script>
<!-- custom script -->
<script src="gadget_monitoring_document_edit.js" type="text/javascript"></script>
</head>
<body>
<div data-gadget-url="gadget_monitoring_jio.html" data-gadget-scope="jio_gadget" data-gadget-sandbox="public"></div>
<div data-gadget-url="gadget_monitoring_login_widget.html" data-gadget-scope="login_gadget" data-gadget-sandbox="public"></div>
<div class="white-popup mfp-hide">
<div class="ui-promise-title"><h2 style="font-size: 1.1em;"></h2></div>
<form>
<div data-role="content">
<div class="form-controlgroup">
</div>
</div>
<div class="padding-5">
<span class="ui-text-error"></span>
</div>
<div>
<button type="button" class="ui-btn ui-corner-all ui-btn-inline cancel"><i class="fa fa-times"></i> Cancel</button>
<button type="submit" class="ui-btn ui-corner-all ui-btn-inline save"><i class="fa fa-floppy-o"></i> Save</button>
<div class="ui-content-hidden spinner">
<i class="fa fa-spinner fa-spin"></i>
</div>
</div>
</form>
</div>
</body>
</html>
\ No newline at end of file
......@@ -37,6 +37,7 @@
<body>
<div data-gadget-url="gadget_monitoring_jio.html" data-gadget-scope="jio_gadget" data-gadget-sandbox="public"></div>
<div data-gadget-url="gadget_monitoring_login_widget.html" data-gadget-scope="login_gadget" data-gadget-sandbox="public"></div>
<div data-gadget-url="gadget_monitoring_log_widget.html" data-gadget-scope="log_gadget" data-gadget-sandbox="public"></div>
<!--<div data-gadget-url="gadget_monitoring_document_edit.html" data-gadget-scope="config_gadget" data-gadget-sandbox="public"></div>-->
<div class="ui-panel-overview hosting-list" style="min-height: 450px;">
<div class="overview-header">
......
......@@ -243,7 +243,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.2597.36910.56320</string> </value>
<value> <string>952.2597.51260.64238</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -261,7 +261,7 @@
</tuple>
<state>
<tuple>
<float>1466586586.9</float>
<float>1467814212.91</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -10,7 +10,7 @@
templater.getElementById("template-hostings-list").innerHTML
);
function safeGet(gadget, url) {
function safeGet(gadget, title, url) {
var document_id = 'monitor.global';
gadget.props.jio_gadget.createJio({
type: "query",
......@@ -26,26 +26,27 @@
.push(function (doc) {
return doc;
}, function (error) {
var value = {type: 'error', method: 'Hosting Subscription list'};
console.log(error);
return undefined;
if (error.status_code) {
value.title = title + ": " + error.name + " "+ error.status_code + ", " + error.message;
} else {
value.title = title + ": Cannot find document: " + document_id;
}
value.message = "Failed to get <strong>" + document_id + "</strong> from local indexdb storage.\n" +
"The document is probably not synchronized yet.\n Source URL: " + url;
return gadget.props.log_gadget.log(value)
.push(function () {
return undefined;
});
});
}
function getHostingData(gadget, url_list) {
function getHostingData(gadget, title, url_list) {
var promise_list = [],
i;
for (i = 0; i < url_list.length; i += 1) {
gadget.props.jio_gadget.createJio({
type: "query",
sub_storage: {
type: "drivetojiomapping",
sub_storage: {
type: "dav",
url: url_list[i]
}
}
});
promise_list.push(safeGet(gadget, url_list[i]));
promise_list.push(safeGet(gadget, title, url_list[i]));
}
return RSVP.all(promise_list);
}
......@@ -74,6 +75,12 @@
gadget.props.jio_gadget = jio_gadget;
});
})
.ready(function (g) {
return g.getDeclaredGadget('log_gadget')
.push(function (log_gadget) {
g.props.log_gadget = log_gadget;
});
})
.declareAcquiredMethod("getSetting", "getSetting")
.declareAcquiredMethod("setSetting", "setSetting")
.declareAcquiredMethod("updateHeader", "updateHeader")
......@@ -107,7 +114,10 @@
promise_list = [];
for (i = 0; i < url_list.length; i += 1) {
if (url_list[i]) {
promise_list.push(getHostingData(gadget, url_list[i]));
promise_list.push(getHostingData(
gadget,
gadget.props.opml_dict[opml_tmp_key_list[i]].title,
url_list[i]));
} else {
//This Feed is not available, remove from key_list
opml_tmp_key_list[i] = undefined;
......@@ -146,7 +156,7 @@
hosting_dict.instance_list.push({
title: document_list[i][j].title,
status: document_list[i][j].status,
url: document_list[i][j]._links.monitor.href
url: document_list[i][j]._links.private_url.href
});
if (document_list[i][j].status == "OK") {
hosting_dict.success += 1;
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.2580.7780.65228</string> </value>
<value> <string>952.25666.33076.8482</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1466585583.31</float>
<float>1467981701.32</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -242,7 +242,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.57042.58652.32665</string> </value>
<value> <string>952.2606.28566.7014</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -260,7 +260,7 @@
</tuple>
<state>
<tuple>
<float>1466587106.1</float>
<float>1467190880.03</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -224,10 +224,19 @@
$(gadget.props.element.querySelector('.btn-continue'))
.addClass('ui-content-hidden');
gadget.props.element.querySelector('.btn-save')
.innerHTML = "Please wait...";
.innerHTML = "Sync OPML Files...";
gadget.props.element.querySelector('.btn-save').disabled = true;
return gadget.props.login_gadget.setUrlDict(monitor_url_dict)
.push(function () {
return gadget.setSetting('monitor_url_description', monitor_opml_url_dict);
return gadget.getSetting('monitor_url_description');
})
.push(function (monitor_opml_dict) {
// merge current opml description with imported list
if (monitor_opml_dict === undefined) {
monitor_opml_dict = {};
}
var opml_dict = $.extend(monitor_opml_dict, monitor_opml_url_dict);
return gadget.setSetting('monitor_url_description', opml_dict);
})
.push(function () {
return gadget.props.jio_gadget.syncMonitoringOpmlData();
......@@ -237,14 +246,15 @@
for (i = 0; i < result_list.length; i += 1) {
if (result_list[i].error) {
error_msg += 'Failed to sync data at "' + result_list[i].url + '" <br/>';
error_msg += 'Failed to sync OPML at "' + result_list[i].url + '" <br/>';
}
}
/*
gadget.props.element.querySelector('.btn-save')
.innerHTML = "Sync Data...";
return gadget.props.sync_gadget.startSync({now: true});
})
.push(function (){
.push(function (){*/
if (error_msg) {
$(gadget.props.element.querySelector('.alert-error'))
.removeClass('ui-content-hidden')
......@@ -253,6 +263,7 @@
.removeClass('ui-content-hidden');
gadget.props.element.querySelector('.btn-save')
.innerHTML = "Retry Import";
gadget.props.element.querySelector('.btn-save').disabled = false;
} else {
return gadget.redirect({
page: 'settings_configurator',
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.20891.29228.48298</string> </value>
<value> <string>952.25745.10452.17408</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1463752927.42</float>
<float>1467889452.73</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -156,7 +156,9 @@
if (url_list.data.rows[i].doc.url) {
if ((opml_title && url_list.data.rows[i].doc.opml_title === opml_title)
|| (opml_title === undefined)) {
monitor_url_list.push(url_list.data.rows[i].doc.url);
monitor_url_list.push(
url_list.data.rows[i].doc.url.replace('jio_public', 'jio_private') // XXX Compatibility force use private url
);
}
}
}
......
......@@ -235,7 +235,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.20849.6596.3413</string> </value>
<value> <string>952.27460.59642.13687</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -253,7 +253,7 @@
</tuple>
<state>
<tuple>
<float>1463759566.63</float>
<float>1467993818.78</float>
<string>UTC</string>
</tuple>
</state>
......
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Monitoring Logs Widget</title>
<link href="gadget_monitoring_custom.css" rel="stylesheet" type="text/css"/>
<script src="rsvp.js"></script>
<script src="renderjs.js"></script>
<script src="gadget_monitoring_log_widget.js"></script>
</head>
<body>
<div data-gadget-url="gadget_monitoring_jio.html" data-gadget-scope="jio_gadget" data-gadget-sandbox="public"></div>
</body>
</html>
/*global window, rJS, btoa */
/*jslint nomen: true, indent: 2, maxerr: 3*/
(function (window, rJS) {
"use strict";
var gadget_klass = rJS(window);
function getMessageList(gadget, limit) {
return gadget.getDeclaredGadget("jio_gadget")
.push(function (jio_gadget) {
return jio_gadget.get("logs");
})
.push(function (doc) {
var error_list = [],
key_list,
size,
i;
if (doc === undefined) {
doc = {};
}
key_list = Object.keys(doc).reverse();
size = key_list.length;
if (size < limit) {
limit = size;
}
if (limit === undefined) {
limit = 150;
}
for (i = 0; i < limit; i += 1) {
error_list.push(doc[key_list[i]]);
}
return error_list;
}, function (error) {
if (error.status_code === 404) {
return [];
}
throw error;
});
}
function log(gadget, logs) {
var jio_gadget;
return gadget.getDeclaredGadget("jio_gadget")
.push(function (result) {
jio_gadget = result;
return jio_gadget.get("logs");
})
.push(undefined, function (error) {
if (error.status_code === 404) {
return {};
}
throw error;
})
.push(function (doc) {
var value,
d = new Date(),
key = d.getTime(),
key_list = Object.keys(doc),
size = key_list.length;
if (logs === undefined) {
logs = {};
}
if (size >= 150) {
// Reduce logs amount to not exceed the limit.
delete doc[key_list[0]];
}
value = {
date: d.toISOString().slice(0,10) + ' ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds(),
title: logs.title || '',
message: logs.message.replace(/\n/g, '<br/>') || '',
type: (logs.type || 'ERROR').toUpperCase(),
method: logs.method || ''
};
doc[key] = value;
return jio_gadget.put('logs', doc);
});
}
gadget_klass
.ready(function (g) {
g.props = {};
})
.ready(function (g) {
return g.getDeclaredGadget("jio_gadget")
.push(function (jio_gadget) {
return jio_gadget.createJio({
type: "indexeddb",
database: "setting"
}, false);
});
})
.declareMethod('log', function (logs) {
var gadget = this;
return log(gadget, logs);
})
.declareMethod('getMessageList', function (limit) {
var gadget = this;
return getMessageList(gadget, limit);
})
.declareService(function () {
var gadget = this;
});
}(window, rJS));
\ No newline at end of file
......@@ -237,7 +237,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>949.21109.24288.21538</string> </value>
<value> <string>951.17777.16093.25088</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -255,7 +255,7 @@
</tuple>
<state>
<tuple>
<float>1456234720.55</float>
<float>1467791635.2</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -42,7 +42,7 @@
gadget.props.url = options.url;
}
gadget.props.title = options.title || 'Please sign in to Monitoring';
gadget.props.root = options.root || 'UNKNOW';
gadget.props.root = options.root || 'UNKNOWN';
return gadget.getSetting('redirect_after_login')
.push(function (redirect_after_login) {
if (redirect_after_login) {
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>950.13111.28749.20002</string> </value>
<value> <string>950.63263.62080.55091</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1459439547.16</float>
<float>1467727630.66</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -237,7 +237,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>949.39647.52503.63692</string> </value>
<value> <string>951.17777.16093.25088</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -255,7 +255,7 @@
</tuple>
<state>
<tuple>
<float>1457099647.3</float>
<float>1467724692.58</float>
<string>UTC</string>
</tuple>
</state>
......
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Monitoring Errors Page</title>
<link href="gadget_monitoring_custom.css" rel="stylesheet" type="text/css"/>
<script src="rsvp.js"></script>
<script src="renderjs.js"></script>
<script id="template-error-list" type="text/x-handlebars-template">
{{#each error_list}}
<tr class="tooltip" title="<h2 class='ui-promise-title '><strong>{{title}}</strong><h2><br/><p><strong>On {{date}}:<br/></strong>{{message}}</p>">
<td>
<span class="label label-{{type}}">{{type}}</span>
</td>
<td class="title">
<span>{{title}}</span>
</td>
<td class="m-hidden">
<span>{{date}}</span>
</td>
<td class="m-hidden">
<span>{{method}}</span>
</td>
</tr>
{{/each}}
</script>
<script src="gadget_monitoring_message_log.js"></script>
<script src="gadget_monitoring_tooltipster.min.js" type="text/javascript"></script>
</head>
<body>
<div data-gadget-url="gadget_monitoring_log_widget.html" data-gadget-scope="log_gadget" data-gadget-sandbox="public"></div>
<div class="ui-panel-overview error-list" style="min-height: 450px;">
<div class="overview-header">
<div class='content-title ui-instance-title'>
<div class="overview-title">
<i class="fa fa-newspaper-o" aria-hidden="true"></i> <span>Monitoring Messages and Errors</span>
</div>
<div class="commands">
<a href="#"><i class="fa fa-refresh" aria-hidden="true"></i></a>
</div>
</div>
</div>
<div class="overview-content">
<div class="logbox">
<table>
<tbody></tbody>
</table>
</div>
</div>
</div>
</body>
</html>
/*global window, rJS, btoa */
/*jslint nomen: true, indent: 2, maxerr: 3*/
(function (window, rJS) {
"use strict";
var gadget_klass = rJS(window),
templater = gadget_klass.__template_element,
error_log_template = Handlebars.compile(
templater.getElementById("template-error-list").innerHTML
);
gadget_klass
.ready(function (g) {
g.props = {};
return g.getElement()
.push(function (element) {
g.props.element = element;
g.props.deferred = RSVP.defer();
});
})
.ready(function (g) {
return g.getDeclaredGadget("log_gadget")
.push(function (log_gadget) {
g.props.log_gadget = log_gadget;
});
})
.declareAcquiredMethod("redirect", "redirect")
.declareMethod("render", function (options) {
var gadget = this;
return gadget.props.log_gadget.getMessageList(150)
.push(function (error_list) {
var content = error_log_template({error_list: error_list});
gadget.props.element.querySelector('.logbox table tbody')
.innerHTML = content;
});
})
.declareService(function () {
var gadget = this;
return new RSVP.Queue()
.push(function () {
$('.tooltip').tooltipster({
animation: 'fade',
delay: 200,
theme: 'tooltipster-shadow',
touchDevices: true,
interactive: true,
trigger: 'click',
contentAsHTML: true,
minWidth: 300
});
})
.push(function () {
var promise_list = [];
promise_list.push(loopEventListener(
gadget.props.element.querySelector('.commands a'),
'click',
false,
function (evt) {
return gadget.props.log_gadget.getMessageList(150)
.push(function (error_list) {
var content = error_log_template({error_list: error_list});
gadget.props.element.querySelector('.logbox table tbody')
.innerHTML = content;
$('.tooltip').tooltipster({
animation: 'fade',
delay: 200,
theme: 'tooltipster-shadow',
touchDevices: true,
interactive: true,
trigger: 'click',
contentAsHTML: true,
minWidth: 300
});
});
}
));
return RSVP.all(promise_list);
});
});
}(window, rJS));
\ No newline at end of file
......@@ -11,7 +11,8 @@
),
infobox_widget_template = Handlebars.compile(
templater.getElementById("infobox-widget-template").innerHTML
);
),
hashCode = new Rusha().digestFromString;
gadget_klass
.ready(function (gadget) {
......@@ -50,8 +51,11 @@
}
};
return gadget.property_dict.login_gadget.loginRedirect(options.jio_for, options, options.title, options.root)
return gadget.property_dict.login_gadget.getUrlInfo(hashCode(options.jio_for))
.push(function (cred) {
if (cred === undefined) {
cred = {};
}
jio_options.sub_storage.sub_storage.basic_login = cred.hash;
gadget.property_dict.jio_gadget.createJio(jio_options, false);
return gadget.updateHeader({
......@@ -64,10 +68,10 @@
.push(undefined, function(error) {
console.log(error);
$.notify(
"Error: Failed to get running process data!",
"Error: Failed to download processes data file!",
{
position:"top right",
autoHideDelay: 5000,
autoHideDelay: 7000,
className: "error"
}
);
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.2947.62827.65245</string> </value>
<value> <string>952.2952.36476.16930</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1466607873.38</float>
<float>1467722270.25</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -29,11 +29,11 @@
<td title="The date when the promise result was collected.">{{start-date}}</td>
</tr>
<tr>
<th><i class="fa fa-calendar"></i> Promise name</th>
<th><i class="fa fa-file-o" aria-hidden="true"></i> Promise name</th>
<td>{{title}}</td>
</tr>
</table>
<h2>Output content</h2>
<h2>Output message</h2>
<div style="border: 1px solid rgba(0,0,0,0.1); margin-top: 10px;">
<div class="ui-body">
{{#if message}}
......@@ -78,12 +78,12 @@
<td title="The date when the instance status was collected.">{{date}}</td>
</tr>
<tr>
<th>Errors Ratio</th>
<td title="Percentage of promises errors in Software Instance">{{errors}} %</td>
<th>Promises Errors</th>
<td title="Percentage of promises errors in Software Instance">{{errors}}</td>
</tr>
<tr>
<th>Success Ratio</th>
<td title="Percentage of promises success in Software Instance">{{success}} %</td>
<th>Promises Success</th>
<td title="Percentage of promises success in Software Instance">{{success}}</td>
</tr>
</table>
{{#with instance}}
......@@ -136,8 +136,18 @@
<script id="phistory-widget-template" type="text/x-handlebars-template">
<h2><i class="fa fa-history"></i> <strong>Promise Status History</strong></h2>
<div style="max-height: 250px; overflow: auto;">
{{#if history_list}}
<div class="loadbox">
<div class="signal ui-content-hidden"></div>
<div class="loadwait">
<a>Load History</a>
</div>
</div>
</script>
<script id="load-history-template" type="text/x-handlebars-template">
<div class="signal ui-content-hidden"></div>
{{#if history_list}}
<div class="load-content" style="max-height: 300px;">
<table data-role="table" data-mode="columntoggle" class="table-stroke">
{{#each history_list}}
<tr title="{{message}}">
......@@ -147,10 +157,12 @@
</tr>
{{/each}}
</table>
{{else}}
History not found!
{{/if}}
<div>
{{else}}
<div class="loadwait">
<a>No History Found!</a>
</div>
{{/if}}
</script>
</head>
......
......@@ -243,7 +243,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.2877.28811.46933</string> </value>
<value> <string>952.41717.43788.11008</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -261,7 +261,7 @@
</tuple>
<state>
<tuple>
<float>1466603419.16</float>
<float>1468847455.64</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.2808.64653.48657</string> </value>
<value> <string>952.41708.59200.61013</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1466599317.84</float>
<float>1468846875.97</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -7,7 +7,8 @@
templater = gadget_klass.__template_element,
graph_labels_widget = Handlebars.compile(
templater.getElementById("graph-label-widget-template").innerHTML
);
),
hashCode = new Rusha().digestFromString;
gadget_klass
.ready(function (gadget) {
......@@ -81,28 +82,16 @@
i,
key;
function loadGraphData (jio_url, key, credential) {
var jio_options = {
type: "query",
sub_storage: {
type: "drivetojiomapping",
sub_storage: {
type: "dav",
url: jio_url,
basic_login: credential.hash
}
}
},
resource_key = gadget.property_dict.ressource_dict[key];
gadget.property_dict.jio_gadget.createJio(jio_options, false);
function loadGraphData (key) {
var resource_key = gadget.property_dict.ressource_dict[key];
return gadget.property_dict.jio_gadget.get(resource_key)
.push(undefined, function(error) {
console.log(error);
$.notify(
"Error: Failed to get resource file '" + resource_key + "'",
"Error: Failed to download resource file '" + resource_key + "'.",
{
position:"top right",
autoHideDelay: 5000,
autoHideDelay: 7000,
className: "error"
}
);
......@@ -165,20 +154,36 @@
]);
})
.push(function () {
return gadget.property_dict.login_gadget.loginRedirect(
options.jio_for,
options,
options.title,
options.root);
return gadget.property_dict.login_gadget.getUrlInfo(
hashCode(options.jio_for));
})
.push(function (cred) {
var key;
var key,
jio_options;
gadget.property_dict.element.querySelector(".ui-panel-overview .overview-title > span")
.innerHTML += options.root + ' > ' + options.title;
if (cred === undefined) {
cred = {};
}
jio_options = {
type: "query",
sub_storage: {
type: "drivetojiomapping",
sub_storage: {
type: "dav",
url: options.jio_for + 'data/',
basic_login: cred.hash
}
}
};
gadget.property_dict.jio_gadget.createJio(jio_options, false);
for (key in gadget.property_dict.ressource_dict) {
promise_list.push(loadGraphData (options.jio_for + 'data/', key, cred));
gadget.property_dict.ressource_list.push({url: options.jio_for + 'data/', key: key});
promise_list.push(loadGraphData (key));
gadget.property_dict.ressource_list.push({
url: options.jio_for + 'data/',
key: key
});
}
return RSVP.all(promise_list);
})
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.2983.39417.57600</string> </value>
<value> <string>952.22944.39409.24251</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1466609798.4</float>
<float>1467721785.89</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -177,11 +177,11 @@
</fieldset>
<p>Last sync date: <span class="last-sync"></span></p>
<h2><strong>Manual Sync:</strong></h2>
<button class="ui-btn ui-corner-all ui-btn-inline sync-all"><i class="fa fa-download"></i> Sync Data Now</button>
<p style="margin: 0; font-style: italic;" class="ui-text-ERROR">
<i class="fa fa-info" aria-hidden="true"></i>
If you just registered a new monitor, sync data now!
</p>
<button class="ui-btn ui-corner-all ui-btn-inline sync-all"><i class="fa fa-download"></i> Sync Data Now</button>
</div>
<div class="padding-5">
<h2><strong>Access links:</strong></h2>
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.14139.17130.256</string> </value>
<value> <string>952.43196.63563.46984</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1467279105.93</float>
<float>1468936547.51</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -139,6 +139,7 @@
<div class="overview-details">
</div>
<div class="graph-full">
<div class="signal ui-content-hidden"></div>
<h2><i class="fa fa-line-chart" aria-hidden="true"></i> <strong>Promises result progression</strong></h2>
<div data-gadget-url="gadget_erp5_graph.html" data-gadget-scope="graph_gadget" class="graph graph-medium "></div>
</div>
......
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.29605.58541.5853</string> </value>
<value> <string>952.2645.30987.35874</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -264,7 +264,7 @@
</tuple>
<state>
<tuple>
<float>1466589448.35</float>
<float>1467987678.83</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -242,7 +242,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.29601.58484.42154</string> </value>
<value> <string>952.27413.56408.50005</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -260,7 +260,7 @@
</tuple>
<state>
<tuple>
<float>1464274806.79</float>
<float>1467989745.98</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -25,7 +25,7 @@
return d.getFullYear() + "-" + addZero(d.getMonth()+1)
+ "-" + addZero(d.getDate()) + " " + addZero(d.getHours())
+ ":" + addZero(d.getMinutes()) + ":" + addZero(d.getMinutes());
+ ":" + addZero(d.getMinutes()) + ":" + addZero(d.getSeconds());
}
function setUrlConfiguration(gadget, url, title) {
......@@ -336,8 +336,12 @@
}
})
.push(function () {
if (!options.tab || options.tab !== 'manage') {
options.tab = 'add';
if (!options.tab) {
if (!options.url) {
options.tab = 'manage';
} else {
options.tab = 'add';
}
}
gadget.props.selected = options.tab;
return gadget.props.deferred.resolve();
......@@ -801,8 +805,11 @@
if (credential_dict){
// Only save provided credentials
return gadget.props.login_gadget.setUrlDict(credential_dict)
/*.push(function () {
return gadget.props.sync_gadget.startSync({now: true});
})*/
.push(function () {
has_error;
return has_error;
});
}
return has_error;
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.27964.58076.47957</string> </value>
<value> <string>952.31668.17083.22784</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1464176559.4</float>
<float>1468600695.1</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -31,11 +31,11 @@
search_page: 'status_list',
search: options.search,
filter: options.filter || '',
column_link: {select: 'source'},
column_id: {select: 'title'},
column_link: {select: 'sourceUrl'},
column_id: {select: 'guid'},
column_list: [{
title: 'Promise',
select: 'title'
select: 'source'
}, {
title: 'Instance',
select: 'siteTitle'
......@@ -47,7 +47,7 @@
title: 'Report Date',
convertDate: true
}, {
select: 'message',
select: 'comments',
title: 'Message',
css_class: 'text-overview'
}, {
......@@ -61,8 +61,8 @@
{select: 'title', title: 'Title'}
],
query: {
select_list: ['title', 'siteTitle', 'reference', 'category',
'date', 'message', 'link', 'source', 'lastBuildDate'],
select_list: ['source', 'siteTitle', 'reference', 'category',
'date', 'comments', 'link', 'sourceUrl', 'lastBuildDate', 'guid'],
query: '_id: (NOT "_replicate_%")',
sort_on: [["category", "ascending"]]
}
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.30706.51220.30105</string> </value>
<value> <string>952.27207.37497.31300</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1464340992.01</float>
<float>1467976809.38</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -19,5 +19,6 @@
<body>
<div data-gadget-url="gadget_monitoring_jio.html" data-gadget-scope="jio_gadget" data-gadget-sandbox="public"></div>
<div data-gadget-url="gadget_monitoring_login_widget.html" data-gadget-scope="login_gadget" data-gadget-sandbox="public"></div>
<div data-gadget-url="gadget_monitoring_log_widget.html" data-gadget-scope="log_gadget" data-gadget-sandbox="public"></div>
</body>
</html>
......@@ -246,7 +246,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.15436.10786.58709</string> </value>
<value> <string>952.23149.28681.64017</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -264,7 +264,7 @@
</tuple>
<state>
<tuple>
<float>1464108182.16</float>
<float>1467735816.69</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -20,6 +20,12 @@
g.props.login_gadget = login_gadget;
});
})
.ready(function (g) {
return g.getDeclaredGadget('log_gadget')
.push(function (log_gadget) {
g.props.log_gadget = log_gadget;
});
})
.declareAcquiredMethod("getSetting", "getSetting")
.declareAcquiredMethod("setSetting", "setSetting")
......@@ -27,7 +33,7 @@
.declareMethod("startSync", function (options) {
var gadget = this,
monitor_cred_dict = {},
sync_lock = false;
sync_error;
function formatDate(d){
function addZero(n){
......@@ -36,7 +42,30 @@
return d.getFullYear() + "-" + addZero(d.getMonth()+1)
+ "-" + addZero(d.getDate()) + " " + addZero(d.getHours())
+ ":" + addZero(d.getMinutes()) + ":" + addZero(d.getMinutes());
+ ":" + addZero(d.getMinutes()) + ":" + addZero(d.getSeconds());
}
function getErrorLog(error_list) {
// Build error msg from failed sync
var i,
tmp_url,
error_message = "";
for (i = 0; i < error_list.length; i += 1) {
if (error_list[i].storage_dict.hasOwnProperty('sub_storage')) {
if (error_list[i].storage_dict.sub_storage.hasOwnProperty('sub_storage')) {
tmp_url = error_list[i].storage_dict.sub_storage.sub_storage.url;
} else {
tmp_url = error_list[i].storage_dict.sub_storage.url;
}
} else {
tmp_url = error_list[i].storage_dict.url;
}
error_message += "> " + error_list[i].storage_dict.hosting + " > " +
error_list[i].storage_dict.title + "\n";
error_message += "Cannot download file(s) at " + tmp_url + ".\n\n";
}
return error_message;
}
function syncStorage(gadget, storage_dict, query) {
......@@ -79,22 +108,29 @@
};
storage_type_list = [
{path: 'jio_public/', query: {}},
{path: 'jio_private/', query: {}, private_access: true},
/*{path: 'jio_private/data/', query: {
//query: '_id: "%.data" AND _id: "%.status"' // Skip history
}}*/
/*{path: 'jio_public/', query: {}},*/
{path: '', query: {}, private_access: true},
];
for (i = 0; i < feed_url_list.length; i += 1) {
feed_config = JSON.parse(JSON.stringify(feed_storage));
feed_config.sub_storage.url = feed_url_list[i].htmlurl;
// put Monitor instance name, usefull to track error
feed_config.title = feed_url_list[i].title;
feed_config.hosting = feed_url_list[i].opml_title;
monitor_storage_list.push({
storage: feed_config,
query: {}
});
base_url = feed_url_list[i].url.replace('jio_public/', ''); // Hard coded!!
base_url_hash = rusha.digestFromString(base_url + 'jio_private/'); // hard coded!!
base_url = feed_url_list[i].url;
// XXX - compatibility
if (base_url.endsWith('jio_public/')) {
base_url = base_url.replace('jio_public/', 'jio_private/'); // Hard coded!!
}
base_url_hash = rusha.digestFromString(base_url);
for (j = 0; j < storage_type_list.length; j += 1) {
dav_config = JSON.parse(JSON.stringify(dav_storage));
dav_config.sub_storage.sub_storage.url = base_url + storage_type_list[j].path;
......@@ -102,9 +138,14 @@
if (monitor_cred_dict.hasOwnProperty(base_url_hash)) {
dav_config.sub_storage.sub_storage.basic_login = monitor_cred_dict[base_url_hash].hash;
} else {
console.log("skipping " + dav_config.sub_storage.sub_storage.url + " ...");
continue;
}
}
// put Monitor instance name, usefull to track error
dav_config.title = feed_url_list[i].title;
dav_config.hosting = feed_url_list[i].opml_title;
monitor_storage_list.push({
storage: dav_config,
query: storage_type_list[j].query
......@@ -117,13 +158,12 @@
function syncAllStorage() {
var monitor_storage_list = [],
last_sync_time;
if (sync_lock === true) {
return [];
}
if (options.now) {
sync_lock = true;
}
return new RSVP.Queue()
.push(function () {
if (options.now) {
return gadget.setSetting('manual_sync_time', new Date().getTime());
}
})
.push(function () {
$(".notifyjs-wrapper").remove();
return $.notify(
......@@ -138,11 +178,17 @@
.push(function () {
return gadget.props.login_gadget.getUrlDict();
})
.push(undefined, function () {
return {};
})
.push(function(url_dict) {
monitor_cred_dict = url_dict;
return gadget.props.jio_gadget.getUrlFeedDescription(options.query);
})
.push(undefined, function () {
return [];
})
.push(function (url_list) {
if (url_list.length <= 0) {
// For backward compatibility, sync OMPL if there are empty!
......@@ -152,6 +198,9 @@
})
.push(function () {
return gadget.props.jio_gadget.getUrlFeedDescription(options.query);
})
.push(undefined, function () {
return [];
});
} else {
return url_list;
......@@ -201,30 +250,39 @@
})
.push(function () {
last_sync_time = new Date().getTime();
sync_lock = false;
return gadget.setSetting('latest_sync_time', last_sync_time);
})
.push(function () {
var time = 3000,
classname = "info",
error_amount = gadget.props.error_list.length,
message = "Synchronisation finished.";
message = "Synchronisation finished.",
log_message = '',
log_title = "OK: " + message;
if ( error_amount > 0) {
classname = "warning";
time = 5000;
message = "Synchronisation finished with " + error_amount + "error(s). \n" +
"You can retry with manual sync.";
log_message = getErrorLog(gadget.props.error_list);
log_title = "Synchronisation finished with " + error_amount + "error(s).";
message = log_title + "\nYou can retry with manual sync.";
}
$(".notifyjs-wrapper").remove();
return $.notify(
message,
{
position:"bottom right",
autoHide: true,
className: classname,
autoHideDelay: time
}
);
return new RSVP.all([$.notify(
message,
{
position:"bottom right",
autoHide: true,
className: classname,
autoHideDelay: time
}
),
gadget.props.log_gadget.log({
message: log_message,
type: classname,
title: log_title,
method: 'Monitoring Sync'
})]);
})
.push(function () {
return $.notify(
......@@ -246,8 +304,21 @@
gadget.props.timer = setTimeout(function(){
return new RSVP.Queue()
.push(function () {
return gadget.getSetting('manual_sync_time');
})
.push(function (manual_timestamp) {
var current_time = new Date().getTime();
if (manual_timestamp !== undefined &&
current_time - gadget.props.timer_interval <= manual_timestamp) {
// There was a manual sync don't start a new sync again!
return;
}
return syncAllStorage();
})
.push(undefined, function (error) {
console.log(error);
return;
})
.push(function () {
return gadget.getSetting('sync_data_interval');
})
......@@ -260,6 +331,7 @@
});
}, gadget.props.timer_interval);
return gadget.props.timer;
}
......
......@@ -242,7 +242,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.14470.40610.22152</string> </value>
<value> <string>952.31612.10711.16298</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -260,7 +260,7 @@
</tuple>
<state>
<tuple>
<float>1467299177.03</float>
<float>1468600685.94</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -208,7 +208,7 @@
return d.getFullYear() + "-" + addZero(d.getMonth()+1)
+ "-" + addZero(d.getDate()) + " " + addZero(d.getHours())
+ ":" + addZero(d.getMinutes()) + ":" + addZero(d.getMinutes());
+ ":" + addZero(d.getMinutes()) + ":" + addZero(d.getSeconds());
}
gadget.property_dict.data_result = [];
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>951.28151.1724.37000</string> </value>
<value> <string>951.28152.13474.61457</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1464187692.31</float>
<float>1468600739.56</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -168,7 +168,7 @@
promise_list.push(gadget.getUrlFor({
title: all_document_list[i].title,
root_title: all_document_list[i]['hosting-title'],
url: all_document_list[i]._links.monitor.href,
url: all_document_list[i]._links.private_url.href,
page: 'software_instance_view'
}));
}
......
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1466589417.88</float>
<float>1467982701.05</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -76,12 +76,12 @@
item.link = (entry.querySelector("item > link") || dummy).textContent;
item.date = (entry.querySelector("item > pubDate") || dummy).textContent;
item.subject = (entry.querySelector("item > title") || dummy).textContent;
item.title = (entry.querySelector("item > title") || dummy).textContent;
item.author = (entry.querySelector("item > author") || dummy).textContent;
item.category = (entry.querySelector("item > category") || dummy).textContent;
item.message = (entry.querySelector("item > comments") || dummy).textContent;
item.source = (entry.querySelector("item > source") || dummy).getAttribute('url');
item.title = (entry.querySelector("item > source") || dummy).textContent;
item.comments = (entry.querySelector("item > comments") || dummy).textContent;
item.sourceUrl = (entry.querySelector("item > source") || dummy).getAttribute('url');
item.source = (entry.querySelector("item > source") || dummy).textContent;
item.description = (entry.querySelector("item > description") || dummy).textContent;
item.guid = (entry.querySelector("item > guid") || dummy).textContent || (entry.querySelector("item > link") || dummy).textContent;
return item;
......
......@@ -239,7 +239,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>949.23984.20073.16930</string> </value>
<value> <string>952.27509.18292.12373</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -257,7 +257,7 @@
</tuple>
<state>
<tuple>
<float>1457363302.84</float>
<float>1467994988.32</float>
<string>UTC</string>
</tuple>
</state>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment