[pmg-devel] [PATCH pmg-gui 1/1] close #1671: implement mobile UI for quarantine
Dominik Csapak
d.csapak at proxmox.com
Mon Feb 18 13:50:50 CET 2019
this patch implements a UI for the Quarantine, designed to
be looked at on mobile phones
for this we use Framework7 instead of extjs, since it has much more
features and looks more native on phones
Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
Makefile | 9 +-
css/ext6-pmg-mobile.css | 46 +++++++
debian/control | 12 +-
js/Makefile | 17 ++-
js/mobile/app.js | 80 +++++++++++
js/mobile/component.js | 26 ++++
js/mobile/loginscreen.js | 114 +++++++++++++++
js/mobile/mailview.js | 62 +++++++++
js/mobile/quarantineview.js | 329 ++++++++++++++++++++++++++++++++++++++++++++
js/mobile/utils.js | 163 ++++++++++++++++++++++
pmg-mobile-index.html.tt | 36 +++++
11 files changed, 888 insertions(+), 6 deletions(-)
create mode 100644 css/ext6-pmg-mobile.css
create mode 100644 js/mobile/app.js
create mode 100644 js/mobile/component.js
create mode 100644 js/mobile/loginscreen.js
create mode 100644 js/mobile/mailview.js
create mode 100644 js/mobile/quarantineview.js
create mode 100644 js/mobile/utils.js
create mode 100644 pmg-mobile-index.html.tt
diff --git a/Makefile b/Makefile
index 045f273..52a16ff 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@ IMAGES= \
logo-128.png \
proxmox_logo.png
-CSSFILES = ext6-pmg.css
+CSSFILES = ext6-pmg.css ext6-pmg-mobile.css
all:
@@ -31,12 +31,17 @@ deb ${DEB}:
js/pmgmanagerlib.js:
make -C js pmgmanagerlib.js
-install: pmg-index.html.tt js/pmgmanagerlib.js
+js/pmgmanagerlib-mobile.js:
+ make -C js pmgmanagerlib-mobile.js
+
+install: pmg-index.html.tt js/pmgmanagerlib.js js/pmgmanagerlib-mobile.js
install -d -m 755 ${WWWCSSDIR}
install -d -m 755 ${WWWIMAGESDIR}
install -d -m 755 ${WWWJSDIR}
install -m 0644 pmg-index.html.tt ${WWWBASEDIR}
+ install -m 0644 pmg-mobile-index.html.tt ${WWWBASEDIR}
install -m 0644 js/pmgmanagerlib.js ${WWWJSDIR}
+ install -m 0644 js/pmgmanagerlib-mobile.js ${WWWJSDIR}
for i in ${IMAGES}; do install -m 0644 images/$$i ${WWWIMAGESDIR}; done
for i in ${CSSFILES}; do install -m 0644 css/$$i ${WWWCSSDIR}; done
diff --git a/css/ext6-pmg-mobile.css b/css/ext6-pmg-mobile.css
new file mode 100644
index 0000000..adbd88b
--- /dev/null
+++ b/css/ext6-pmg-mobile.css
@@ -0,0 +1,46 @@
+.item-title .item-header {
+ white-space: inherit;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.empty {
+ padding: 1em;
+ color: var(--f7-label-text-color);
+}
+
+img.logo {
+ padding: 0 10px;
+ vertical-align: middle;
+ height: 64px;
+}
+
+img.logo-navbar {
+ padding: 0 10px;
+ height: 32;
+}
+
+.settings-form {
+ position: absolute;
+ bottom: calc(var(--f7-fab-margin) + var(--f7-safe-area-bottom));
+ right: calc(var(--f7-fab-margin) + var(--f7-safe-area-right));
+ z-index: 1500;
+ width: 200px;
+ background-color: var(--f7-list-bg-color);
+}
+
+.button.subscription i.icon {
+ display: inline;
+}
+
+ at media only screen and (max-width: 500px) {
+ .login-screen-title {
+ font-size: 6vw;
+ }
+}
+
+ at media only screen and (min-width: 500px) {
+ .login-screen-title {
+ font-size: 32px;
+ }
+}
diff --git a/debian/control b/debian/control
index a4e93ed..74a3d84 100644
--- a/debian/control
+++ b/debian/control
@@ -2,12 +2,20 @@ Source: pmg-gui
Section: perl
Priority: optional
Maintainer: Proxmox Support Team <support at proxmox.com>
-Build-Depends: debhelper (>= 9), perl (>= 5.10.0-19), libtemplate-perl
+Build-Depends: debhelper (>= 9),
+ libtemplate-perl,
+ perl (>= 5.10.0-19),
Standards-Version: 3.9.5
Homepage: http://www.proxmox.com
Package: pmg-gui
Architecture: all
-Depends: ${perl:Depends}, libtemplate-perl, libjs-extjs (>= 6.0.1), fonts-font-awesome, pmg-i18n, proxmox-widget-toolkit
+Depends: fonts-font-awesome,
+ libjs-extjs (>= 6.0.1),
+ libjs-framework7,
+ libtemplate-perl,
+ pmg-i18n,
+ proxmox-widget-toolkit,
+ ${perl:Depends},
Description: Proxmox Mail Gateway GUI
Graphical user interface for Proxmox Mail Gateway.
diff --git a/js/Makefile b/js/Makefile
index 882cbc4..6f4d449 100644
--- a/js/Makefile
+++ b/js/Makefile
@@ -84,6 +84,15 @@ JSSRC= \
SpamContextMenu.js \
Application.js
+# caution: order is important
+MOBILESRC= \
+ mobile/component.js \
+ mobile/loginscreen.js \
+ mobile/mailview.js \
+ mobile/quarantineview.js \
+ mobile/utils.js \
+ mobile/app.js \
+
OnlineHelpInfo.js: /usr/bin/asciidoc-pmg
/usr/bin/asciidoc-pmg scan-extjs ${JSSRC} >$@.tmp
mv $@.tmp $@
@@ -95,12 +104,16 @@ pmgmanagerlib.js: OnlineHelpInfo.js ${JSSRC}
cat OnlineHelpInfo.js ${JSSRC} >$@.tmp
mv $@.tmp $@
-all: pmgmanagerlib.js
+pmgmanagerlib-mobile.js: ${MOBILESRC}
+ cat ${MOBILESRC} >$@.tmp
+ mv $@.tmp $@
+
+all: pmgmanagerlib.js pmgmanagerlib-mobile.js
.PHONY: clean
clean:
find . -name '*~' -exec rm {} ';'
- rm -rf pmgmanagerlib.js OnlineHelpInfo.js
+ rm -rf pmgmanagerlib.js pmgmanagerlib-mobile.js OnlineHelpInfo.js
diff --git a/js/mobile/app.js b/js/mobile/app.js
new file mode 100644
index 0000000..68fb9e5
--- /dev/null
+++ b/js/mobile/app.js
@@ -0,0 +1,80 @@
+var $$ = Dom7;
+var app = new Framework7({
+ root: '#app',
+ init: false,
+ name: 'Proxmox Mail Gateway',
+ routes: [
+ {
+ path: '/:path/:subpath?',
+ async: function(routeTo, routeFrom, resolve, reject) {
+ if (routeTo.params.path === 'mail') {
+ let mail = new MailView();
+ resolve({
+ template: mail.getTpl()
+ },{
+ context: {
+ mailid: routeTo.params.subpath
+ }
+ });
+ } else {
+ reject();
+ }
+ }
+ },
+ {
+ path: '/mail/:mailid/:action',
+ async: function(routeTo, routeFrom, resolve, reject) {
+ let action = routeTo.params.action;
+ let mailid = routeTo.params.mailid;
+ let confirmText = gettext('')
+ app.dialog.confirm(
+ `${action}: ${mailid}`,
+ gettext('Confirm'),
+ () => {
+ let loader = app.dialog.preloader();
+ app.request({
+ method: 'POST',
+ url: '/api2/json/quarantine/content/',
+ data: {
+ action: action,
+ id: mailid
+ },
+ headers: {
+ CSRFPreventionToken: Proxmox.CSRFPreventionToken
+ },
+ success: (data, status, xhr) => {
+ loader.close();
+ app.dialog.alert(
+ `Action '${action}' successful`,
+ gettext("Info"),
+ () => {
+ if (action === 'delete' ||
+ action === 'deliver')
+ {
+ // refresh the main list when a mail
+ // got deleted or delivered
+ app.ptr.refresh();
+ }
+ }
+ );
+ reject();
+ },
+ error: xhr => {
+ loader.close();
+ PMG.Utils.showError(xhr);
+ reject();
+ }
+ })
+ },
+ () => {
+ reject();
+ }
+ );
+ }
+ }
+ ]
+});
+
+let quarlist = new QuarantineView();
+
+app.init();
diff --git a/js/mobile/component.js b/js/mobile/component.js
new file mode 100644
index 0000000..72312a7
--- /dev/null
+++ b/js/mobile/component.js
@@ -0,0 +1,26 @@
+class Component {
+ constructor(config = {}) {
+ var me = this;
+ me.config = config;
+ me.data = config.data || {};
+ me.tpl = me.config.tpl || '<div class="component"></div>';
+ }
+ getTpl() {
+ var me = this;
+ if (!me._compiledtpl) {
+ me._compiledtpl = Template7.compile(me.tpl);
+ }
+ return me._compiledtpl;
+ }
+ getEl(data) {
+ var me = this;
+ if (data === undefined && me._el) {
+ return me._el;
+ } else if (data !== undefined) {
+ me.data =data;
+ }
+ me._el = Dom7(me.getTpl()(me.data));
+ return me._el;
+ }
+}
+
diff --git a/js/mobile/loginscreen.js b/js/mobile/loginscreen.js
new file mode 100644
index 0000000..36dc994
--- /dev/null
+++ b/js/mobile/loginscreen.js
@@ -0,0 +1,114 @@
+class LoginScreen extends Component {
+ constructor(config = {}) {
+ config.tpl = `
+ <div class="login-screen">
+ <div class="view">
+ <div class="page">
+ <div class="page-content login-screen-content">
+ <div class="login-screen-title">
+ <img class="logo" src="pve2/images/logo-128.png" />
+ Proxmox Mail Gateway
+ </div>
+ <form action="/api2/json/access/ticket" method="POST" class="form-ajax-submit">
+ <div class="list">
+ <ul>
+ <li class="item-content item-input">
+ <div class="item-inner">
+ <div class="item-title item-label">Username</div>
+ <div class="item-input-wrap">
+ <input type="text" name="username" placeholder="{{gettext 'Username'}}" required validate>
+ <span class="input-clear-button"></span>
+ </div>
+ </div>
+ </li>
+ <li class="item-content item-input">
+ <div class="item-inner">
+ <div class="item-title item-label">Password</div>
+ <div class="item-input-wrap">
+ <input type="password" name="password" placeholder="{{gettext 'Password'}}" required validate>
+ <span class="input-clear-button"></span>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
+ <div class="list">
+ <ul>
+ <li>
+ <input type="submit" class="button" value='{{gettext "Log In"}}'>
+ </li>
+ </ul>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ `;
+ super(config);
+ var me = this;
+ me._screen = app.loginScreen.create({
+ content: me.getEl(),
+ });
+
+ let login = config.loginInfo;
+ me._form = me.getEl().find('form');
+
+ if (login.username && login.ticket) {
+ app.form.fillFromData(me._form, {
+ username: login.username,
+ password: login.ticket,
+ });
+ me._autoLogin = true;
+ } else if (PMG.Utils.authOK()) {
+ app.form.fillFromData(me._form, {
+ username: Proxmox.UserName,
+ password: decodeURIComponent(PMG.Utils.getCookie('PMGAuthCookie')),
+ });
+ me._autoLogin = true;
+ }
+ }
+ open(onLogin) {
+ var me = this;
+ return new Promise(function(resolve, reject) {
+ me._form.on('formajax:beforesend', (el, data, xhr) => {
+ me.loader = app.dialog.preloader();
+ });
+
+ me._form.on('formajax:success', (el, data, xhr) => {
+ let json;
+ try {
+ json = JSON.parse(xhr.responseText);
+ } catch (err) {
+ xhr.error = err;
+ PMG.Utils.showError(xhr);
+ return;
+ }
+
+ resolve(json);
+ });
+
+ me._form.on('formajax:error', (el, data, xhr) => {
+ me.loader.close();
+ PMG.Utils.showError(xhr);
+ });
+
+ if (me._autoLogin) {
+ delete me._autoLogin;
+ me._screen.on('open', () => {
+ me._form.trigger('submit');
+ })
+ }
+
+ me._screen.open();
+ });
+ }
+ close() {
+ var me = this;
+ if (me.loader) {
+ me.loader.close();
+ }
+ me._screen.close(false);
+ }
+}
+
diff --git a/js/mobile/mailview.js b/js/mobile/mailview.js
new file mode 100644
index 0000000..c89d134
--- /dev/null
+++ b/js/mobile/mailview.js
@@ -0,0 +1,62 @@
+class MailView extends Component {
+ constructor(config = {}) {
+ config.tpl = `
+ <div class="page">
+ <div class="navbar sliding">
+ <div class="navbar-inner">
+ <div class="left">
+ <a href="#" class="link back">
+ <i class="icon icon-back"></i>
+ <span class="ios-only">{{gettext "Back"}}</span>
+ </a>
+ </div>
+ <div class="title">Preview</div>
+ </div>
+ </div>
+ <div class="fab fab-right-bottom">
+ <a href="#">
+ <i class="icon f7-icons ios-only">menu</i>
+ <i class="icon f7-icons ios-only">close</i>
+ <i class="icon material-icons md-only">menu</i>
+ <i class="icon material-icons md-only">close</i>
+ </a>
+ <div class="fab-buttons fab-buttons-top">
+ <a href="/mail/{{mailid}}/blacklist" class="fab-label-button fab-close">
+ <span>
+ <i class="icon f7-icons ios-only">close</i>
+ <i class="icon material-icons md-only">close</i>
+ </span>
+ <span class="fab-label">{{gettext "Blacklist"}}</span>
+ </a>
+ <a href="/mail/{{mailid}}/whitelist" class="fab-label-button fab-close">
+ <span>
+ <i class="icon f7-icons ios-only">check</i>
+ <i class="icon material-icons md-only">check</i>
+ </span>
+ <span class="fab-label">{{gettext "Whitelist"}}</span>
+ </a>
+ <a href="/mail/{{mailid}}/delete" class="fab-label-button fab-close">
+ <span>
+ <i class="icon f7-icons ios-only">trash</i>
+ <i class="icon material-icons md-only">delete</i>
+ </span>
+ <span class="fab-label">{{gettext "Delete"}}</span>
+ </a>
+ <a href="/mail/{{mailid}}/deliver" class="fab-label-button fab-close">
+ <span>
+ <i class="icon f7-icons ios-only">paper_plane</i>
+ <i class="icon material-icons md-only">send</i>
+ </span>
+ <span class="fab-label">{{gettext "Deliver"}}</span>
+ </a>
+ </div>
+ </div>
+ <div class="page-content">
+ <iframe frameborder=0 width="100%" height="100%" sandbox="allow-same-origin" src="/api2/htmlmail/quarantine/content?id={{mailid}}"></iframe>
+ </div>
+ </div>
+ `;
+ super(config);
+ }
+}
+
diff --git a/js/mobile/quarantineview.js b/js/mobile/quarantineview.js
new file mode 100644
index 0000000..c2e7e94
--- /dev/null
+++ b/js/mobile/quarantineview.js
@@ -0,0 +1,329 @@
+class QuarantineView extends Component {
+ constructor(config = {}) {
+ config.tpl = config.tpl || `
+ <div class="view view-quarantine">
+ <div data-name="quarantine-list" class="page">
+ <div class="navbar">
+ <div class="navbar-inner">
+ <div class="left">
+ <img class="logo-navbar" style="padding: 0 10px" src="pve2/images/logo-128.png" height=32 />
+ </div>
+ <div class="title">Mail Gateway</div>
+ </div>
+ </div>
+ <div class="settings-form elevation-5 fab-morph-target">
+ <div class="block-title block-title-medium">{{gettext "Range"}}</div>
+ <div class="list no-hairlines-md">
+ <ul>
+ <li class="item-content item-input">
+ <div class="item-inner">
+ <div class="item-title item-label">{{gettext "From"}}</div>
+ <div class="item-input-wrap">
+ <input type="date" name="from" placeholder="from" required validate>
+ </div>
+ </div>
+ </li>
+ <li class="item-content item-input">
+ <div class="item-inner">
+ <div class="item-title item-label">{{gettext "To"}}</div>
+ <div class="item-input-wrap">
+ <input type="date" name="to" placeholder="to" required validate>
+ </div>
+ </div>
+ </li>
+ </ul>
+ <a class="button fab-close range-form">{{gettext "OK"}}</a>
+ </div>
+ </div>
+ <div class="fab fab-morph fab-right-bottom" data-morph-to=".settings-form">
+ <a href="#">
+ <i class="icon f7-icons ios-only">calendar</i>
+ <i class="icon material-icons md-only">date_range</i>
+ </a>
+ </div>
+ <div class="toolbar subscription toolbar-hidden toolbar-bottom">
+ <div class="toolbar-inner">
+ <a class="button subscription">
+ <i class="icon f7-icons ios-only color-yellow">alert</i>
+ <i class="icon material-icons md-only color-yellow">warning</i>
+ <span class="subscription-text">
+ {{gettext "No valid subscription"}}
+ </span>
+ </a>
+ </div>
+ </div>
+ <div class="page-content ptr-content">
+ <div class="ptr-preloader">
+ <div class="preloader"></div>
+ <div class="ptr-arrow"></div>
+ </div>
+ <div class="list virtual-list"></div>
+ </div>
+ </div>
+ </div>`;
+ config.itemTemplate = config.itemTemplate || `
+ <li class="swipeout">
+ <div class="swipeout-content">
+ <a href="/mail/{{id}}/" class="item-link item-content">
+ <div class="item-inner">
+ <div class="item-title">
+ <div class="item-header">{{escape from}}</div>
+ {{escape subject}}
+ </div>
+ <div class="item-after">Score: {{js "this.spamlevel || 0"}}</div>
+ </div>
+ </a>
+ </div>
+ <div class="swipeout-actions-left">
+ <a href="/mail/{{id}}/deliver" class="color-green swipeout-close">
+ <i class="icon f7-icons ios-only">paper_plane</i>
+ <i class="icon material-icons md-only">send</i>
+ {{gettext "Deliver"}}
+ </a>
+ <a href="/mail/{{id}}/whitelist" class="swipeout-close">
+ <i class="icon f7-icons ios-only">check</i>
+ <i class="icon material-icons md-only">check</i>
+ {{gettext "Whitelist"}}
+ </a>
+ </div>
+ <div class="swipeout-actions-right">
+ <a href="/mail/{{id}}/blacklist" class="color-orange swipeout-close">
+ <i class="icon f7-icons ios-only">close</i>
+ <i class="icon material-icons md-only">close</i>
+ {{gettext "Blacklist"}}
+ </a>
+ <a href="/mail/{{id}}/delete" class="color-red swipeout-close">
+ <i class="icon f7-icons ios-only">trash</i>
+ <i class="icon material-icons md-only">delete</i>
+ {{gettext "Delete"}}
+ </a>
+ </div>
+ </li>`;
+ config.dividerTemplate = config.dividerTemplate ||
+ '<li class="item-divider">{{group}}</li>';
+ super(config);
+
+ var me = this;
+
+ me._compiledItemTemplate = Template7.compile(me.config.itemTemplate);
+ me._compiledDividerTemplate = Template7.compile(me.config.dividerTemplate);
+ me.skelTpl = `
+ <li class="skeleton-text skeleton-effect-fade">
+ <a href="#" class="item-content item-link">
+ <div class="item-inner">
+ <div class="item-title">
+ <div class="item-header">_______________________</div>
+ ____ ______ __ _______ ____ _______ _______ ___
+ </div>
+ <div class="item-after">Score: 15</div>
+ </div>
+ </a>
+ </li>`;
+ me.skelDividerTpl = '<li class="item-divider skeleton-text">____-__-__</li>';
+ me.setEndtime(new Date());
+ let startdate = new Date();
+ startdate.setDate(startdate.getDate() - 7);
+ me.setStarttime(startdate);
+
+ // add to dom
+ $$(me.config.target || '#app').append(me.getEl());
+
+ $$(document).on('page:init', '.page[data-name=quarantine-list]', (e, page) => {
+ me.vList = app.virtualList.create({
+ el: '.virtual-list',
+ items: [],
+ renderItem: function(item) {
+ return me._renderItem(item);
+ },
+ emptyTemplate: '<div class="empty">No data in database</div>'
+ });
+
+ // setup pull to refresh
+ $$('.ptr-content').on('ptr:refresh', (e) => {
+ me.setItems([
+ { skel: true, divider: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true, divider: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ ]);
+ me.load().then(data => {
+ me.setItems(data, {
+ sorter: {
+ property: 'time',
+ numeric: true,
+ direction: 'DESC'
+ },
+ grouperFn: (val) => PMG.Utils.unixToIso(val['time'])
+ });
+ }).catch(PMG.Utils.showError).then(() => {
+ e.detail();
+ });
+ });
+
+ // process query parameters
+ let { mail, action, date, username, ticket } = PMG.Utils.extractParams();
+ if (date) {
+ me.setStarttime(date);
+ }
+
+ // setup range form
+ $$('input[name=from]').val(PMG.Utils.unixToIso(me.starttime));
+ $$('input[name=to]').val(PMG.Utils.unixToIso(me.endtime));
+
+ $$('.fab').on('fab:close', () => {
+ let fromChanged = me.setStarttime($$('input[name=from]').val());
+ let toChanged = me.setEndtime($$('input[name=to]').val());
+ if (fromChanged || toChanged) {
+ app.ptr.refresh();
+ }
+ });
+
+ // check login
+
+ let loginInfo = { username, ticket };
+ let showPopup = (username && ticket) || !PMG.Utils.authOK();
+ me._loginScreen = new LoginScreen({ loginInfo });
+
+ me._loginScreen.open().then(data => {
+ me._loginScreen.close();
+ PMG.Utils.setLoginInfo(data);
+ return PMG.Utils.getSubscriptionInfo();
+ }).then(data => {
+ return PMG.Utils.checkSubscription(data, showPopup);
+ }).then(data => {
+ app.ptr.refresh();
+ if (mail) {
+ let url = "/mail/" + mail + "/" + (action || "");
+ me._view.router.navigate(url);
+ }
+ }).catch(PMG.Utils.showError);
+ });
+
+ me._view = app.views.create('.view-quarantine', {
+ main: me.config.mainView !== undefined ? me.config.mainView : true,
+ url: '/',
+ pushState: true,
+ pushStateAnimateOnLoad: true
+ });
+ }
+ setStarttime(starttime) {
+ var me = this;
+ let date = starttime;
+ if (!(starttime instanceof Date)) {
+ // we assume an ISO string
+ if (starttime == '') {
+ return;
+ }
+ date = new Date(PMG.Utils.isoToUnix(starttime)*1000);
+ }
+ // starttime is at beginning of date
+ date.setHours(0,0,0,0);
+ let result = Math.round(date.getTime()/1000);
+ if (result !== me.starttime) {
+ me.starttime = result;
+ return true;
+ }
+ return false
+ }
+ setEndtime(endtime) {
+ var me = this;
+ let date = endtime;
+ if (!(endtime instanceof Date)) {
+ if (endtime == '') {
+ return;
+ }
+ // we assume an ISO string
+ date = new Date(PMG.Utils.isoToUnix(endtime)*1000);
+ }
+ // endtime is at the end of the day
+ date.setHours(23, 59, 59);
+ let result = Math.round(date.getTime()/1000);
+ if (result !== me.endtime) {
+ me.endtime = result;
+ return true;
+ }
+ return false;
+ }
+ _renderItem(item) {
+ var me = this;
+
+ if(typeof item === 'object') {
+ if (item.skel) {
+ return item.divider? me.skelDividerTpl : me.skelTpl;
+ } else if (item.divider) {
+ return me._compiledDividerTemplate(item);
+ } else {
+ return me._compiledItemTemplate(item);
+ }
+ }
+
+ return item.toString();
+ }
+ setItems(items, options) {
+ var me = this;
+ if (options && options.sorter) {
+ if (options.sorter.sorterFn) {
+ items.sort(options.sorter.sorterFn);
+ } else {
+ let prop = options.sorter.property;
+ let numeric = options.sorter.numeric;
+ let dir = options.sorter.direction === "ASC" ? 1 : -1;
+ items.sort((a,b) => {
+ let result;
+
+ if (numeric) {
+ result = a[prop] - b[prop];
+ } else {
+ result = a[prop] === b[prop] ? 0 : (a[prop] < b[prop] ? 1 : -1);
+ }
+
+ return result * dir;
+ });
+ }
+ }
+ me.vList.replaceAllItems(items);
+ if (options && options.grouperFn) {
+ let lastgroup;
+ let offset = 0;
+ for (let i = 0; i+offset < items.length; i++) {
+ let item = items[i+offset];
+ let curgroup = options.grouperFn(item);
+ if (curgroup != lastgroup) {
+ me.vList.insertItemBefore(i+(offset++), {
+ divider: true,
+ group: curgroup
+ });
+ lastgroup = curgroup;
+ }
+ }
+ }
+ }
+ load() {
+ var me = this;
+ return new Promise(function(resolve, reject) {
+ app.request({
+ url: '/api2/json/quarantine/spam',
+ data: {
+ starttime: me.starttime,
+ endtime: me.endtime
+ },
+ dataType: 'json',
+ success: (response, status, xhr) => {
+ resolve(response.data);
+ },
+ error: xhr => {
+ reject(xhr);
+ }
+ });
+ });
+ }
+}
+
diff --git a/js/mobile/utils.js b/js/mobile/utils.js
new file mode 100644
index 0000000..b0e082b
--- /dev/null
+++ b/js/mobile/utils.js
@@ -0,0 +1,163 @@
+Template7.registerHelper('gettext', function(value) {
+ return gettext(value);
+});
+
+var PMG = {
+ Utils: {
+ getCookie(name) {
+ let cookies = document.cookie.split(/;\s*/);
+ for (let i = 0; i < cookies.length; i++) {
+ let cookie = cookies[i].split('=');
+ if (cookie[0] === name && cookie.length > 1) {
+ return cookie[1];
+ }
+ }
+ return undefined;
+ },
+ setCookie(name, value, expires) {
+ value = encodeURIComponent(value);
+ let cookie = `${name}=${value}`;
+ if (expires) {
+ cookie += `; expires=${expires}`;
+ }
+ document.cookie = cookie;
+ },
+ deleteCookie(name) {
+ PMG.Utils.setCookie(name, "", "Thu, 01 Jan 1970 00:00:00 UTC");
+ },
+ authOK(options) {
+ var authCookie = PMG.Utils.getCookie('PMGAuthCookie') || "";
+ return (authCookie.substr(0,7) === 'PMGQUAR' && Proxmox.UserName !== '');
+ },
+ isoToUnix(iso) {
+ let fields = iso.split('-').map((field) => parseInt(field, 10));
+ // monthIndex starts at 0
+ let date = new Date(fields[0],fields[1]-1, fields[2]);
+ return Math.round(date.getTime()/1000);
+ },
+ unixToIso(unix) {
+ let date = new Date(unix*1000);
+ let year = date.getFullYear().toString();
+ let month = (date.getMonth()+1).toString().padStart(2, "0");
+ let day = date.getDate().toString().padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ },
+ showError(xhr) {
+ let statusText = "", errorText = "";
+ if (xhr instanceof Error) {
+ statusText = gettext("Error");
+ errorText = xhr.message;
+ } else if (xhr.error instanceof Error) {
+ statusText = gettext("Error");
+ errorText = xhr.error.message;
+ } else {
+ statusText = xhr.status.toString() + ' ' + xhr.statusText;
+ try {
+ let errorObj = JSON.parse(xhr.responseText);
+ if (errorObj.errors) {
+ let errors = Object.keys(errorObj.errors).map((key) => key + ": " + errorObj.errors[key]);
+ errorText = errors.join('<br>');
+ }
+ } catch (e) {
+ statusText = gettext("Error");
+ errorText = e.message;
+ }
+ }
+ app.toast.show({
+ text: `Error:<br>
+ ${statusText}<br>
+ ${errorText}
+ `,
+ closeButton: true,
+ destroyOnClose: true
+ });
+ },
+ extractParams() {
+ let queryObj = app.utils.parseUrlQuery(location.search);
+ let mail, action, date, username, ticket;
+ if (queryObj.ticket) {
+ let tocheck = decodeURIComponent(queryObj.ticket);
+ let match = tocheck.match(/^PMGQUAR:([^\s\:]+):/);
+ if (match) {
+ ticket = tocheck;
+ username = match[1];
+ }
+ delete queryObj.ticket;
+ }
+
+ if (queryObj.date) {
+ date =queryObj.date;
+ delete queryObj.date;
+ }
+
+ if (queryObj.cselect) {
+ mail = queryObj.cselect;
+ action = queryObj.action;
+ delete queryObj.cselect;
+ delete queryObj.action;
+ }
+
+ if (mail || action || date || ticket) {
+ let queryString = app.utils.serializeObject(queryObj);
+ window.history.replaceState(
+ window.history.state,
+ document.title,
+ location.pathname + (queryString? "?" + queryString : '')
+ );
+ }
+
+ return { mail, action, date, username, ticket };
+ },
+ setLoginInfo(result) {
+ PMG.Utils.setCookie('PMGAuthCookie', result.data.ticket);
+ Proxmox.CSRFPreventionToken = result.data.CSRFPreventionToken;
+ },
+ getSubscriptionInfo() {
+ return new Promise(function(resolve, reject) {
+ app.request({
+ url: '/api2/json/nodes/localhost/subscription',
+ dataType: 'json',
+ success: (result, status, xhr) => {
+ resolve(result.data);
+ },
+ error: (xhr, status) => {
+ reject(xhr);
+ }
+ });
+ });
+ },
+ checkSubscription(data, showPopup) {
+ return new Promise(function(resolve, reject) {
+ if (data.status !== 'Active') {
+ let url = data.url || 'https://wwww.proxmox.com';
+ let err = `You do not have a valid subscription for this server.
+ Please visit
+ <a target="_blank" href="${url}">www.proxmox.com</a>
+ to get a list of available options.`;
+ app.toolbar.show('.toolbar.subscription');
+ $$('.button.subscription').on('click', () => {
+ app.dialog.alert(
+ err,
+ gettext("No valid subscription"),
+ );
+ });
+ if (showPopup) {
+ app.dialog.alert(
+ err,
+ gettext("No valid subscription"),
+ () => {
+ resolve(data);
+ }
+ );
+ } else {
+ resolve();
+ }
+ } else {
+ app.toolbar.hide('.toolbar.subscription');
+ resolve();
+ }
+ });
+ }
+ }
+};
+
diff --git a/pmg-mobile-index.html.tt b/pmg-mobile-index.html.tt
new file mode 100644
index 0000000..cb08971
--- /dev/null
+++ b/pmg-mobile-index.html.tt
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui, viewport-fit=cover">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="theme-color" content="#2196f3">
+ <title>Proxmox Mail Gateway - Quarantine</title>
+ <link rel="stylesheet" href="framework7/css/framework7.bundle.min.css">
+ <link rel="stylesheet" href="framework7/css/framework7-icons.css">
+ <link rel="stylesheet" href="framework7/css/material-icons.css">
+ <link rel="stylesheet" href="pve2/css/ext6-pmg-mobile.css">
+ [% IF langfile %]
+ <script type='text/javascript' src='/pve2/locale/pmg-lang-[% lang %].js'></script>
+ [% ELSE %]
+ <script type='text/javascript'> function gettext(buf) { return buf; } </script>
+ [%- END %]
+ <script type="text/javascript">
+ Proxmox = {
+ UserName: '[% username %]',
+ CSRFPreventionToken: '[% token %]'
+ };
+ </script>
+ </head>
+ <body>
+ <div id="app">
+ <div class="statusbar"></div>
+ </div>
+ [% IF debug %]
+ <script type="text/javascript" src="/framework7/js/framework7.bundle.js"></script>
+ [% ELSE %]
+ <script type="text/javascript" src="/framework7/js/framework7.bundle.min.js"></script>
+ [% END %]
+ <script type="text/javascript" src="/pve2/js/pmgmanagerlib-mobile.js"></script>
+ </body>
+</html>
--
2.11.0
More information about the pmg-devel
mailing list