mirror of
https://git.proxmox.com/git/pmg-gui
synced 2025-04-28 12:39:16 +00:00
close #1671: implement mobile UI for quarantine
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@proxmox.com>
This commit is contained in:
parent
80ccc3425b
commit
43f0b189d9
9
Makefile
9
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
|
||||
|
||||
|
46
css/ext6-pmg-mobile.css
Normal file
46
css/ext6-pmg-mobile.css
Normal file
@ -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;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.login-screen-title {
|
||||
font-size: 6vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 500px) {
|
||||
.login-screen-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
12
debian/control
vendored
12
debian/control
vendored
@ -2,12 +2,20 @@ Source: pmg-gui
|
||||
Section: perl
|
||||
Priority: optional
|
||||
Maintainer: Proxmox Support Team <support@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.
|
||||
|
17
js/Makefile
17
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
|
||||
|
||||
|
||||
|
||||
|
80
js/mobile/app.js
Normal file
80
js/mobile/app.js
Normal file
@ -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();
|
26
js/mobile/component.js
Normal file
26
js/mobile/component.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
114
js/mobile/loginscreen.js
Normal file
114
js/mobile/loginscreen.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
62
js/mobile/mailview.js
Normal file
62
js/mobile/mailview.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
329
js/mobile/quarantineview.js
Normal file
329
js/mobile/quarantineview.js
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
163
js/mobile/utils.js
Normal file
163
js/mobile/utils.js
Normal file
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
36
pmg-mobile-index.html.tt
Normal file
36
pmg-mobile-index.html.tt
Normal file
@ -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>
|
Loading…
Reference in New Issue
Block a user