Frontend user list and modal dialog fixes

This commit is contained in:
Jamie Curnow 2018-06-20 16:51:18 +10:00
parent 8942b99372
commit bfc319cff9
20 changed files with 549 additions and 44 deletions

View File

@ -163,17 +163,22 @@ module.exports = {
},
/**
* @param {Integer} [offset]
* @param {Integer} [limit]
* @param {String} [sort]
* @param {Array} [expand]
* @param {String} [query]
* @returns {Promise}
*/
getAll: function (offset, limit, sort, expand, query) {
return fetch('get', 'users?offset=' + (offset ? offset : 0) + '&limit=' + (limit ? limit : 20) + (sort ? '&sort=' + sort : '') +
(typeof expand === 'object' && expand !== null && expand.length ? '&expand=' + makeExpansionString(expand) : '') +
(typeof query === 'string' ? '&query=' + query : ''));
getAll: function (expand, query) {
let params = [];
if (typeof expand === 'object' && expand !== null && expand.length) {
params.push('expand=' + makeExpansionString(expand));
}
if (typeof query === 'string') {
params.push('query=' + query);
}
return fetch('get', 'users' + (params.length ? '?' + params.join('&') : ''));
},
/**

View File

@ -26,34 +26,30 @@ module.exports = {
/**
* Users
*
* @param {Number} [offset]
* @param {Number} [limit]
* @param {String} [sort]
*/
showUsers: function (offset, limit, sort) {
/*
showUsers: function () {
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './users/main'], (App, View) => {
controller.navigate('/users');
App.UI.showMainLoading();
let view = new View({
sort: (typeof sort !== 'undefined' && sort ? sort : Cache.Session.Users.sort),
offset: (typeof offset !== 'undefined' ? offset : Cache.Session.Users.offset),
limit: (typeof limit !== 'undefined' && limit ? limit : Cache.Session.Users.limit)
});
view.on('loaded', function () {
App.UI.hideMainLoading();
});
App.UI.showAppContent(view);
App.UI.showAppContent(new View());
});
} else {
this.showRules();
this.showDashboard();
}
},
/**
* User Form
*
* @param model
*/
showUserForm: function (model) {
if (Cache.User.isAdmin()) {
require(['./main', './user/form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
*/
},
/**

View File

@ -12,13 +12,17 @@ const UI = require('./ui/main');
const App = Mn.Application.extend({
region: '#app',
Cache: Cache,
Api: Api,
UI: null,
Controller: Controller,
version: null,
region: {
el: '#app',
replaceElement: true
},
onStart: function (app, options) {
console.log('Welcome to Nginx Proxy Manager');

View File

@ -9,6 +9,6 @@
</div>
</div>
<div class="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
v<%- getVersion() %> &copy; 2018 <a href="https://jc21.com?utm_source=docker-registry-ui" target="_blank">jc21.com</a>. Theme by <a href="https://github.com/tabler/tabler?utm_source=docker-registry-ui" target="_blank">Tabler</a>
v<%- getVersion() %> &copy; 2018 <a href="https://jc21.com?utm_source=nginx-proxy-manager" target="_blank">jc21.com</a>. Theme by <a href="https://tabler.github.io/?utm_source=nginx-proxy-manager" target="_blank">Tabler</a>
</div>
</div>

View File

@ -1,7 +1,7 @@
<div class="container">
<div class="d-flex">
<a class="navbar-brand" href="/">
<img src="/images/favicons/favicon-32x32.png" border="0"> &nbsp; Docker Registry
<img src="/images/favicons/favicon-32x32.png" border="0"> &nbsp; Nginx Proxy Manager
</a>
<div class="d-flex order-lg-2 ml-auto">
@ -9,7 +9,7 @@
<a href="#" class="nav-link pr-0 leading-none" data-toggle="dropdown">
<span class="avatar" style="background-image: url(<%- getUserField('avatar', '/images/default-avatar.jpg') %>)"></span>
<span class="ml-2 d-none d-lg-block">
<span class="text-default"><%- getUserField('name', 'Unknown User') %></span>
<span class="text-default"><%- getUserField('nickname', null) || getUserField('name', 'Unknown User') %></span>
<small class="text-muted d-block mt-1"><%- getRole() %></small>
</span>
</a>

View File

@ -1,5 +1,4 @@
<div class="page-main">
<div class="header" id="header">
<!-- Header View -->
</div>
@ -15,4 +14,6 @@
<footer class="footer">
<!-- Footer View -->
</footer>
</footer>
<div class="modal fade" id="modal-dialog" tabindex="-1" role="dialog" aria-hidden="true"></div>

View File

@ -8,8 +8,16 @@ const FooterView = require('./footer/main');
const Cache = require('../cache');
module.exports = Mn.View.extend({
className: 'page',
template: template,
id: 'app',
className: 'page',
template: template,
modal_setup: false,
modal: null,
ui: {
modal: '#modal-dialog'
},
regions: {
header_region: {
@ -21,13 +29,60 @@ module.exports = Mn.View.extend({
replaceElement: true
},
footer_region: '.footer',
app_content_region: '#app-content'
app_content_region: '#app-content',
modal_region: '#modal-dialog'
},
/**
* @param {Object} view
*/
showAppContent: function (view) {
this.showChildView('app_content_region', view);
},
/**
* @param {Object} view
* @param {Function} [show_callback]
* @param {Function} [shown_callback]
*/
showModalDialog: function (view, show_callback, shown_callback) {
this.showChildView('modal_region', view);
let modal = this.getRegion('modal_region').$el.modal('show');
modal.on('hidden.bs.modal', function (/*e*/) {
if (show_callback) {
modal.off('show.bs.modal', show_callback);
}
if (shown_callback) {
modal.off('shown.bs.modal', shown_callback);
}
modal.off('hidden.bs.modal');
view.destroy();
});
if (show_callback) {
modal.on('show.bs.modal', show_callback);
}
if (shown_callback) {
modal.on('shown.bs.modal', shown_callback);
}
},
/**
*
* @param {Function} [hidden_callback]
*/
closeModal: function (hidden_callback) {
let modal = this.getRegion('modal_region').$el.modal('hide');
if (hidden_callback) {
modal.on('hidden.bs.modal', hidden_callback);
}
},
onRender: function () {
this.showChildView('header_region', new HeaderView({
model: Cache.User
@ -40,5 +95,6 @@ module.exports = Mn.View.extend({
reset: function () {
this.getRegion('header_region').reset();
this.getRegion('footer_region').reset();
this.getRegion('modal_region').reset();
}
});

View File

@ -3,10 +3,15 @@
<div class="col-lg order-lg-first">
<ul class="nav nav-tabs border-0 flex-column flex-lg-row">
<li class="nav-item">
<a href="../index.html" class="nav-link"><i class="fe fe-home"></i> Home</a>
<a href="/" class="nav-link"><i class="fe fe-home"></i> Home</a>
</li>
<% if (showUsers()) { %>
<li class="nav-item">
<a href="javascript:void(0)" class="nav-link" data-toggle="dropdown"><i class="fe fe-box"></i> Interface</a>
<a href="/users" class="nav-link"><i class="fe fe-users"></i> Users</a>
</li>
<% } %>
<li class="nav-item">
<a href="#" class="nav-link" data-toggle="dropdown"><i class="fe fe-box"></i> Interface</a>
<div class="dropdown-menu dropdown-menu-arrow">
<a href="../cards.html" class="dropdown-item ">Cards design</a>
<a href="../charts.html" class="dropdown-item ">Charts</a>
@ -14,7 +19,7 @@
</div>
</li>
<li class="nav-item dropdown">
<a href="javascript:void(0)" class="nav-link" data-toggle="dropdown"><i class="fe fe-calendar"></i> Components</a>
<a href="#" class="nav-link" data-toggle="dropdown"><i class="fe fe-calendar"></i> Components</a>
<div class="dropdown-menu dropdown-menu-arrow">
<a href="../maps.html" class="dropdown-item ">Maps</a>
<a href="../icons.html" class="dropdown-item ">Icons</a>
@ -24,7 +29,7 @@
</div>
</li>
<li class="nav-item dropdown">
<a href="javascript:void(0)" class="nav-link" data-toggle="dropdown"><i class="fe fe-file"></i> Pages</a>
<a href="#" class="nav-link" data-toggle="dropdown"><i class="fe fe-file"></i> Pages</a>
<div class="dropdown-menu dropdown-menu-arrow">
<a href="../profile.html" class="dropdown-item ">Profile</a>
<a href="../login.html" class="dropdown-item ">Login</a>

View File

@ -1,10 +1,39 @@
'use strict';
const Mn = require('backbone.marionette');
const template = require('./main.ejs');
const $ = require('jquery');
const Mn = require('backbone.marionette');
const Controller = require('../../controller');
const Cache = require('../../cache');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'menu',
className: 'header collapse d-lg-flex p-0',
template: template
template: template,
ui: {
link: 'a'
},
events: {
'click @ui.link': function (e) {
e.preventDefault();
let href = $(e.currentTarget).attr('href');
switch (href) {
case '/':
Controller.showDashboard();
break;
case '/users':
Controller.showUsers();
break;
}
}
},
templateContext: {
showUsers: function () {
return Cache.User.isAdmin();
}
}
});

View File

@ -0,0 +1,53 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><% if (typeof id !== 'undefined') { %>Edit<% } else { %>New<% } %> User</h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="form-label">Full Name <span class="form-required">*</span></label>
<input name="name" type="text" class="form-control" placeholder="Joe Citizen" value="<%- name %>" required>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="form-label">Nickname</label>
<input name="nickname" type="text" class="form-control" placeholder="Joe" value="<%- nickname %>">
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label">Email <span class="form-required">*</span></label>
<input name="email" type="email" class="form-control" placeholder="joe@example.com" value="<%- email %>" required>
</div>
</div>
<% if (!isSelf()) { %>
<div class="col-sm-12 col-md-12">
<div class="form-group">
<div class="form-label">Switches</div>
<div class="custom-switches-stacked">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="is_admin" value="1"<%- isAdmin() ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description">Administrator</span>
</label>
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="is_disabled" value="1"<%- is_disabled ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description">Disabled</span>
</label>
</div>
</div>
</div>
<% } %>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-teal save">Save</button>
</div>
</div>

View File

@ -0,0 +1,99 @@
'use strict';
const Mn = require('backbone.marionette');
const template = require('./form.ejs');
const Controller = require('../controller');
const Cache = require('../cache');
const Api = require('../api');
const App = require('../main');
const UserModel = require('../../models/user');
require('jquery-serializejson');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save'
},
events: {
/*
'click @ui.cancel': function (e) {
e.preventDefault();
App.UI.closeModal();
},
*/
'submit @ui.form': function (e) {
e.preventDefault();
let view = this;
let data = this.ui.form.serializeJSON();
// Manipulate
data.roles = [];
if (
(this.model.get('id') === Cache.User.get('id') && this.model.isAdmin()) ||
(typeof data.is_admin !== 'undefined' && data.is_admin)) {
data.roles.push('admin');
delete data.is_admin;
}
data.is_disabled = typeof data.is_disabled !== 'undefined' ? !!data.is_disabled : false;
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
let method = Api.Users.create;
if (this.model.get('id')) {
// edit
method = Api.Users.update;
data.id = this.model.get('id');
}
method(data)
.then(result => {
if (result.id === Cache.User.get('id')) {
Cache.User.set(result);
}
view.model.set(result);
App.UI.closeModal();
if (view.model.get('id') !== Cache.User.get('id')) {
Controller.showUsers();
}
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
templateContext: function () {
let view = this;
return {
isSelf: function () {
return view.model.get('id') === Cache.User.get('id');
},
isAdmin: function () {
return view.model.isAdmin();
},
isDisabled: function () {
return view.model.isDisabled();
}
};
},
initialize: function (options) {
if (typeof options.model === 'undefined' || !options.model) {
this.model = new UserModel.Model();
}
}
});

View File

@ -0,0 +1,31 @@
<td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- avatar || '/images/default-avatar.jpg' %>)">
<span class="avatar-status <%- is_disabled ? 'bg-red' : 'bg-green' %>"></span>
</div>
</td>
<td>
<div><%- name %></div>
<div class="small text-muted">
Created: Mar 19, 2018
</div>
</td>
<td>
<div><%- email %></div>
</td>
<td>
<div><%- roles.join(', ') %></div>
</td>
<td class="text-center">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a href="#" class="edit-user dropdown-item"><i class="dropdown-icon fe fe-edit"></i> Edit User</a>
<a href="#" class="set-password dropdown-item"><i class="dropdown-icon fe fe-shield"></i> Set Password</a>
<% if (!isSelf()) { %>
<a href="#" class="login dropdown-item"><i class="dropdown-icon fe fe-log-in"></i> Sign in as User</a>
<div class="dropdown-divider"></div>
<a href="#" class="delete-user dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> Delete User</a>
<% } %>
</div>
</div>
</td>

View File

@ -0,0 +1,66 @@
'use strict';
const Mn = require('backbone.marionette');
const Controller = require('../../controller');
const Api = require('../../api');
const Cache = require('../../cache');
const Tokens = require('../../tokens');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
edit: 'a.edit-user',
password: 'a.set-password',
login: 'a.login',
delete: 'a.delete-user'
},
events: {
'click @ui.edit': function (e) {
e.preventDefault();
Controller.showUserForm(this.model);
},
'click @ui.password': function (e) {
e.preventDefault();
//Controller.showUserPasswordForm(this.model);
},
'click @ui.delete': function (e) {
e.preventDefault();
//Controller.showUserDeleteConfirm(this.model);
},
'click @ui.login': function (e) {
e.preventDefault();
if (Cache.User.get('id') !== this.model.get('id')) {
this.ui.login.prop('disabled', true).addClass('btn-disabled');
Api.Users.loginAs(this.model.get('id'))
.then(res => {
Tokens.addToken(res.token, res.user.nickname || res.user.name);
window.location = '/';
window.location.reload();
})
.catch(err => {
alert(err.message);
this.ui.login.prop('disabled', false).removeClass('btn-disabled');
});
}
}
},
templateContext: {
isSelf: function () {
return Cache.User.get('id') === this.id;
}
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});

View File

@ -0,0 +1,10 @@
<thead>
<th width="30">&nbsp;</th>
<th>Name</th>
<th>Email</th>
<th>Roles</th>
<th>&nbsp;</th>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,29 @@
'use strict';
const Mn = require('backbone.marionette');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter text-nowrap card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,17 @@
<div class="card">
<div class="card-header">
<h3 class="card-title">Users</h3>
<div class="card-options">
<a href="#" class="btn btn-outline-teal btn-sm ml-2 add-user">Add User</a>
</div>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,51 @@
'use strict';
const Mn = require('backbone.marionette');
const UserModel = require('../../models/user');
const Api = require('../api');
const Controller = require('../controller');
const ListView = require('./list/main');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'users',
template: template,
ui: {
list_region: '.list-region',
add_user: '.add-user',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
events: {
'click @ui.add_user': function (e) {
e.preventDefault();
Controller.showUserForm(new UserModel.Model());
}
},
onRender: function () {
let view = this;
Api.Users.getAll()
.then(response => {
if (!view.isDestroyed() && response && response.length) {
view.showChildView('list_region', new ListView({
collection: new UserModel.Collection(response)
}));
// Remove loader
view.ui.dimmer.removeClass('active');
}
})
.catch(err => {
console.log(err);
//Controller.showError(err, 'Could not fetch Users');
//view.trigger('loaded');
});
}
});

View File

@ -0,0 +1,25 @@
$primary-color: #2bcbba;
.loader {
color: $primary-color;
}
a {
color: $primary-color;
}
a:hover {
color: darken($primary-color, 10%);
}
.dropdown-item.active, .dropdown-item:active {
background-color: $primary-color;
}
.custom-switch-input:checked ~ .custom-switch-indicator {
background: $primary-color;
}
.min-100 {
min-height: 100px;
}

View File

@ -1,4 +1,6 @@
@import "~tabler-ui/dist/assets/css/dashboard";
@import "tabler-extra";
@import "custom";
/* Before any JS content is loaded */
#app > .loader, #login > .loader, .container > .loader {
@ -10,4 +12,4 @@
.no-js-warning {
margin-top: 100px;
}
}

View File

@ -0,0 +1,26 @@
$teal: #2bcbba;
/* For Card bodies where I don't want padding */
.card-body.no-padding {
padding: 0;
}
/* Teal Outline Buttons */
.btn-outline-teal {
color: $teal;
background-color: transparent;
background-image: none;
border-color: $teal;
}
.btn-outline-teal:hover {
color: #fff;
background-color: $teal;
border-color: $teal;
}
.btn-outline-teal:not(:disabled):not(.disabled):active, .btn-outline-teal:not(:disabled):not(.disabled).active, .show > .btn-outline-teal.dropdown-toggle {
color: #fff;
background-color: $teal;
border-color: $teal;
}