Initial features of accounts tab

It's rough right now, but the accounts tab shows a list of users and
info. Right now the only action available is to apply settings (from
template or another user) to a selection of users. More to come.
This commit is contained in:
Harvey Tindall
2020-09-17 16:51:19 +01:00
parent a8b4842895
commit cd61989495
12 changed files with 481 additions and 122 deletions
+81 -4
View File
@@ -575,22 +575,24 @@ func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
} }
type defaultsReq struct { type defaultsReq struct {
Username string `json:"username"` From string `json:"from"`
ApplyTo []string `json:"apply_to"`
ID string `json:"id"`
Homescreen bool `json:"homescreen"` Homescreen bool `json:"homescreen"`
} }
func (app *appContext) SetDefaults(gc *gin.Context) { func (app *appContext) SetDefaults(gc *gin.Context) {
var req defaultsReq var req defaultsReq
gc.BindJSON(&req) gc.BindJSON(&req)
app.info.Printf("Getting user defaults from \"%s\"", req.Username) userID := req.ID
user, status, err := app.jf.userByName(req.Username, false) user, status, err := app.jf.userById(userID, false)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to get user from Jellyfin: Code %d", status) app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
app.debug.Printf("Error: %s", err) app.debug.Printf("Error: %s", err)
respond(500, "Couldn't get user", gc) respond(500, "Couldn't get user", gc)
return return
} }
userID := user["Id"].(string) app.info.Printf("Getting user defaults from \"%s\"", user["Name"].(string))
policy := user["Policy"].(map[string]interface{}) policy := user["Policy"].(map[string]interface{})
app.storage.policy = policy app.storage.policy = policy
app.storage.storePolicy() app.storage.storePolicy()
@@ -615,6 +617,81 @@ func (app *appContext) SetDefaults(gc *gin.Context) {
gc.JSON(200, map[string]bool{"success": true}) gc.JSON(200, map[string]bool{"success": true})
} }
func (app *appContext) ApplySettings(gc *gin.Context) {
var req defaultsReq
gc.BindJSON(&req)
applyingFrom := "template"
var policy, configuration, displayprefs map[string]interface{}
if req.From == "template" {
if len(app.storage.policy) == 0 {
respond(500, "No policy template available", gc)
return
}
app.storage.loadPolicy()
policy = app.storage.policy
if req.Homescreen {
if len(app.storage.configuration) == 0 || len(app.storage.displayprefs) == 0 {
respond(500, "No homescreen template available", gc)
return
}
configuration = app.storage.configuration
displayprefs = app.storage.displayprefs
}
} else if req.From == "user" {
applyingFrom = "user"
user, status, err := app.jf.userById(req.ID, false)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
app.debug.Printf("Error: %s", err)
respond(500, "Couldn't get user", gc)
return
}
applyingFrom = "\"" + user["Name"].(string) + "\""
policy = user["Policy"].(map[string]interface{})
if req.Homescreen {
displayprefs, status, err = app.jf.getDisplayPreferences(req.ID)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
app.debug.Printf("Error: %s", err)
respond(500, "Couldn't get displayprefs", gc)
return
}
configuration = user["Configuration"].(map[string]interface{})
}
}
app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom)
errors := map[string]map[string]string{
"policy": map[string]string{},
"homescreen": map[string]string{},
}
for _, id := range req.ApplyTo {
status, err := app.jf.setPolicy(id, policy)
if !(status == 200 || status == 204) || err != nil {
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
}
if req.Homescreen {
status, err = app.jf.setConfiguration(id, configuration)
errorString := ""
if !(status == 200 || status == 204) || err != nil {
errorString += fmt.Sprintf("Configuration %d: %s ", status, err)
} else {
status, err = app.jf.setDisplayPreferences(id, displayprefs)
if !(status == 200 || status == 204) || err != nil {
errorString += fmt.Sprintf("Displayprefs %d: %s ", status, err)
}
}
if errorString != "" {
errors["homescreen"][id] = errorString
}
}
}
code := 200
if len(errors["policy"]) == len(req.ApplyTo) || len(errors["homescreen"]) == len(req.ApplyTo) {
code = 500
}
gc.JSON(code, errors)
}
func (app *appContext) GetConfig(gc *gin.Context) { func (app *appContext) GetConfig(gc *gin.Context) {
app.info.Println("Config requested") app.info.Println("Config requested")
resp := map[string]interface{}{} resp := map[string]interface{}{}
+135
View File
@@ -0,0 +1,135 @@
document.getElementById('selectAll').onclick = function() {
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
for (check of checkboxes) {
check.checked = this.checked;
}
checkCheckboxes();
};
function checkCheckboxes() {
const defaultsButton = document.getElementById('accountsTabSetDefaults');
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
let checked = false;
for (check of checkboxes) {
if (check.checked) {
checked = true;
break;
}
}
if (!checked) {
defaultsButton.classList.add('unfocused');
} else if (defaultsButton.classList.contains('unfocused')) {
defaultsButton.classList.remove('unfocused');
}
}
var jfUsers = [];
function populateUsers() {
const acList = document.getElementById('accountsList');
acList.innerHTML = `
<div class="d-flex align-items-center">
<strong>Getting Users...</strong>
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</div>`;
acList.parentNode.querySelector('thead').classList.add('unfocused');
const accountsList = document.createElement('tbody');
accountsList.id = 'accountsList';
const template = function(id, username, email, lastActive, admin) {
let isAdmin = "No";
if (admin) {
isAdmin = "Yes";
}
return `
<td scope="row"><input class="form-check-input" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
<td>${username}</td>
<td>${email}</td>
<td>${lastActive}</td>
<td>${isAdmin}</td>
<td><i class="fa fa-eye icon-button" id="viewConfig_${id}"></i></td>`;
};
let req = new XMLHttpRequest();
req.responseType = 'json';
req.open("GET", "/getUsers", true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200) {
jfUsers = req.response['users'];
for (user of jfUsers) {
let tr = document.createElement('tr');
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
accountsList.appendChild(tr);
}
const header = acList.parentNode.querySelector('thead');
if (header.classList.contains('unfocused')) {
header.classList.remove('unfocused');
}
acList.replaceWith(accountsList);
}
}
};
req.send();
}
document.getElementById('selectAll').checked = false;
document.getElementById('accountsTabSetDefaults').onclick = function() {
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
let userIDs = [];
for (check of checkboxes) {
if (check.checked) {
userIDs.push(check.id.replace('select_', ''));
}
}
if (userIDs.length == 0) {
return;
}
let radioList = document.getElementById('defaultUserRadios');
radioList.textContent = '';
let first = true;
for (user of jfUsers) {
let radio = document.createElement('div');
radio.classList.add('radio');
let checked = 'checked';
if (first) {
first = false;
} else {
checked = '';
}
radio.innerHTML = `
<label><input type="radio" name="defaultRadios" id="select_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
radioList.appendChild(radio);
}
let userstring = 'user';
if (userIDs.length > 1) {
userstring += 's';
}
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userstring}`;
document.getElementById('userDefaultsDescription').textContent = `
Create an account and configure it to your liking, then choose it from below to apply to your selected users.`;
document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`;
if (document.getElementById('defaultsSourceSection').classList.contains('unfocused')) {
document.getElementById('defaultsSourceSection').classList.remove('unfocused');
}
document.getElementById('defaultsSource').value = 'userTemplate';
document.getElementById('defaultUserRadios').classList.add('unfocused');
document.getElementById('storeDefaults').onclick = function() {
storeDefaults(userIDs);
};
userDefaultsModal.show();
};
document.getElementById('defaultsSource').addEventListener('change', function() {
const radios = document.getElementById('defaultUserRadios');
if (this.value == 'userTemplate') {
radios.classList.add('unfocused');
} else if (radios.classList.contains('unfocused')) {
radios.classList.remove('unfocused');
}
})
+81 -8
View File
@@ -1,3 +1,36 @@
const tabs = {
invitesEl: document.getElementById('invitesTab'),
accountsEl: document.getElementById('accountsTab'),
invitesTabButton: document.getElementById('invitesTabButton'),
accountsTabButton: document.getElementById('accountsTabButton'),
invites: function() {
if (tabs.invitesEl.classList.contains('unfocused')) {
tabs.accountsEl.classList.add('unfocused');
tabs.invitesEl.classList.remove('unfocused');
}
if (tabs.invitesTabButton.classList.contains("text-muted")) {
tabs.invitesTabButton.classList.remove("text-muted");
tabs.accountsTabButton.classList.add("text-muted");
}
},
accounts: function() {
populateUsers();
if (tabs.accountsEl.classList.contains('unfocused')) {
tabs.invitesEl.classList.add('unfocused');
tabs.accountsEl.classList.remove('unfocused');
}
if (tabs.accountsTabButton.classList.contains("text-muted")) {
tabs.accountsTabButton.classList.remove("text-muted");
tabs.invitesTabButton.classList.add("text-muted");
}
}
};
tabs.invitesTabButton.onclick = tabs.invites;
tabs.accountsTabButton.onclick = tabs.accounts;
tabs.invites();
// Used for theme change animation // Used for theme change animation
function whichTransitionEvent() { function whichTransitionEvent() {
let t; let t;
@@ -638,7 +671,7 @@ document.getElementById('openDefaultsWizard').onclick = function() {
checked = ''; checked = '';
} }
radio.innerHTML = radio.innerHTML =
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`; `<label><input type="radio" name="defaultRadios" id="select_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
radioList.appendChild(radio); radioList.appendChild(radio);
} }
let button = document.getElementById('openDefaultsWizard'); let button = document.getElementById('openDefaultsWizard');
@@ -655,6 +688,19 @@ document.getElementById('openDefaultsWizard').onclick = function() {
submitButton.classList.add('btn-primary'); submitButton.classList.add('btn-primary');
} }
settingsModal.hide(); settingsModal.hide();
document.getElementById('defaultsTitle').textContent = `New user defaults`;
document.getElementById('userDefaultsDescription').textContent = `
Create an account and configure it to your liking, then choose it from below to store the settings as a template for all new users.`;
document.getElementById('storeHomescreenLabel').textContent = `Store homescreen layout`;
document.getElementById('defaultsSource').value = 'fromUser';
document.getElementById('defaultsSourceSection').classList.add('unfocused');
document.getElementById('storeDefaults').onclick = function() {
storeDefaults('all');
};
const list = document.getElementById('defaultUserRadios');
if (list.classList.contains('unfocused')) {
list.classList.remove('unfocused');
}
userDefaultsModal.show(); userDefaultsModal.show();
} }
} }
@@ -662,23 +708,39 @@ document.getElementById('openDefaultsWizard').onclick = function() {
req.send(); req.send();
}; };
document.getElementById('storeDefaults').onclick = function () { function storeDefaults(users) {
this.disabled = true; this.disabled = true;
this.innerHTML = this.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' + '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...'; 'Loading...';
let button = document.getElementById('storeDefaults'); let button = document.getElementById('storeDefaults');
let radios = document.getElementsByName('defaultRadios'); let radios = document.getElementsByName('defaultRadios');
let id = '';
for (let radio of radios) { for (let radio of radios) {
if (radio.checked) { if (radio.checked) {
id = radio.id.replace('select_', '');
break;
}
}
let route = '/setDefaults';
let data = { let data = {
'username': radio.id.slice(8), 'from': 'user',
'homescreen': false}; 'id': id,
'homescreen': false
};
if (document.getElementById('defaultsSource').value == 'userTemplate') {
data['from'] = 'template';
}
if (users != 'all') {
data['apply_to'] = users;
route = '/applySettings';
}
if (document.getElementById('storeDefaultHomescreen').checked) { if (document.getElementById('storeDefaultHomescreen').checked) {
data['homescreen'] = true; data['homescreen'] = true;
} }
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
req.open("POST", "/setDefaults", true); req.open("POST", route, true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = function() { req.onreadystatechange = function() {
@@ -692,9 +754,22 @@ document.getElementById('storeDefaults').onclick = function () {
} }
button.classList.add('btn-success'); button.classList.add('btn-success');
button.disabled = false; button.disabled = false;
setTimeout(function() { userDefaultsModal.hide(); }, 1000); setTimeout(function() {
let button = document.getElementById('storeDefaults');
button.textContent = "Submit";
button.classList.remove('btn-success');
button.classList.add('btn-primary');
button.disabled = false;
userDefaultsModal.hide();
}, 1000);
} else {
if ("error" in req.response) {
button.textContent = req.response["error"];
} else if (("policy" in req.response) || ("homescreen" in req.response)) {
button.textContent = "Failed (Check JS Console)";
} else { } else {
button.textContent = "Failed"; button.textContent = "Failed";
}
button.classList.remove('btn-primary'); button.classList.remove('btn-primary');
button.classList.add('btn-danger'); button.classList.add('btn-danger');
setTimeout(function() { setTimeout(function() {
@@ -708,8 +783,6 @@ document.getElementById('storeDefaults').onclick = function () {
} }
}; };
req.send(JSON.stringify(data)); req.send(JSON.stringify(data));
}
}
}; };
var ombiDefaultsModal = ''; var ombiDefaultsModal = '';
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+43 -5
View File
@@ -150,16 +150,24 @@
<div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="defaultsTitle">New user defaults</h5> <h5 class="modal-title" id="defaultsTitle"></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Create an account and configure it to your liking, then choose it from below to store the settings as a template for all new users.</p> <p id="userDefaultsDescription"></p>
<div class="mb-3" id="defaultsSourceSection">
<label for="defaultsSource">Use settings from:</label>
<select class="form-select" id="defaultsSource" aria-label="User settings source">
<option value="userTemplate" selected>Use existing user template</option>
<option value="fromUser">Source from existing user</option>
</select>
</div>
<div id="defaultUserRadios"></div> <div id="defaultUserRadios"></div>
<div class="checkbox"> <div class="checkbox">
<label><input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>Store homescreen layout</label> <input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>
<label for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
</div> </div>
</div> </div>
<div class="modal-footer" id="defaultsFooter"> <div class="modal-footer" id="defaultsFooter">
@@ -240,7 +248,7 @@
</div> </div>
</div> </div>
<div class="pageContainer"> <div class="pageContainer">
<h1><a class="text-button">Invites </a></h1> <h1><a id="invitesTabButton" class="text-button">invites </a><a id="accountsTabButton" class="text-button text-muted">accounts</a></h1>
<div class="btn-group" role="group" id="headerButtons"> <div class="btn-group" role="group" id="headerButtons">
<button type="button" class="btn btn-primary" id="openSettings"> <button type="button" class="btn btn-primary" id="openSettings">
Settings <i class="fa fa-cog"></i> Settings <i class="fa fa-cog"></i>
@@ -249,7 +257,8 @@
Logout <i class="fa fa-sign-out"></i> Logout <i class="fa fa-sign-out"></i>
</button> </button>
</div> </div>
<div class="card mb-3 linkGroup"> <div id="invitesTab">
<div class="card mb-3 tabGroup">
<div class="card-header">Current Invites</div> <div class="card-header">Current Invites</div>
<ul class="list-group list-group-flush" id="invites"> <ul class="list-group list-group-flush" id="invites">
</ul> </ul>
@@ -322,6 +331,34 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div id="accountsTab" class="unfocused">
<div class="card mb-3 tabGroup">
<div class="card-header">
Accounts
<div class="btn-group d-flex float-right" role="group" aria-label="Account control buttons">
<button type="button" class="btn btn-secondary">Add User</button>
<button type="button" class="btn btn-primary unfocused" id="accountsTabSetDefaults">Set Defaults</button>
</div>
</div>
<div class="card-body">
<table class="table table-hover table-striped table-borderless">
<thead>
<tr>
<th scope="col"><input class="form-check-input" type="checkbox" value="" id="selectAll"></th>
<th scope="col">Username</th>
<th scope="col">Email Address</th>
<th scope="col">Last Active</th>
<th scope="col">Admin?</th>
<th scope="col"><i>View settings</i></th>
</tr>
</thead>
<tbody id="accountsList">
</tbody>
</table>
</div>
</div>
</div>
<div class="contactBox"> <div class="contactBox">
<p>{{ .contactMessage }}</p> <p>{{ .contactMessage }}</p>
</div> </div>
@@ -401,6 +438,7 @@
const notifications_enabled = false; const notifications_enabled = false;
{{ end }} {{ end }}
</script> </script>
<script src="accounts.js"></script>
<script src="admin.js"></script> <script src="admin.js"></script>
</body> </body>
</html> </html>
+2 -1
View File
@@ -322,7 +322,7 @@ func start(asDaemon, firstCall bool) {
app.storage.policy_path = app.config.Section("files").Key("user_template").String() app.storage.policy_path = app.config.Section("files").Key("user_template").String()
app.storage.loadPolicy() app.storage.loadPolicy()
// app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json") // app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
app.storage.policy_path = app.config.Section("files").Key("user_configuration").String() app.storage.configuration_path = app.config.Section("files").Key("user_configuration").String()
app.storage.loadConfiguration() app.storage.loadConfiguration()
// app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json") // app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String() app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
@@ -442,6 +442,7 @@ func start(asDaemon, firstCall bool) {
api.GET("/getUsers", app.GetUsers) api.GET("/getUsers", app.GetUsers)
api.POST("/modifyUsers", app.ModifyEmails) api.POST("/modifyUsers", app.ModifyEmails)
api.POST("/setDefaults", app.SetDefaults) api.POST("/setDefaults", app.SetDefaults)
api.POST("/applySettings", app.ApplySettings)
api.GET("/getConfig", app.GetConfig) api.GET("/getConfig", app.GetConfig)
api.POST("/modifyConfig", app.ModifyConfig) api.POST("/modifyConfig", app.ModifyConfig)
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
+10 -1
View File
@@ -10,7 +10,7 @@ h1 {
/*margin: 20%;*/ /*margin: 20%;*/
margin-bottom: 5%; margin-bottom: 5%;
} }
.linkGroup { .tabGroup {
/*margin: 20%;*/ /*margin: 20%;*/
margin-bottom: 5%; margin-bottom: 5%;
margin-top: 5%; margin-top: 5%;
@@ -84,6 +84,7 @@ body.modal-open {
font-weight: inherit; font-weight: inherit;
line-height: inherit; line-height: inherit;
font-family: inherit; font-family: inherit;
margin-right: 1rem;
} }
} }
@@ -94,3 +95,11 @@ body.modal-open {
.text-button { .text-button {
@extend %scut-link-unstyled; @extend %scut-link-unstyled;
} }
.text-button:hover {
@extend %scut-link-unstyled;
}
.unfocused {
display: none;
}
+13
View File
@@ -1,2 +1,15 @@
@import "../../node_modules/bootstrap4/scss/bootstrap"; @import "../../node_modules/bootstrap4/scss/bootstrap";
.icon-button {
color: $text-muted;
}
.icon-button:hover {
color: inherit;
}
.icon-button:active {
color: $text-muted;
}
@import "../base.scss"; @import "../base.scss";
+13
View File
@@ -1,2 +1,15 @@
@import "../../node_modules/bootstrap/scss/bootstrap"; @import "../../node_modules/bootstrap/scss/bootstrap";
.icon-button {
color: $text-muted;
}
.icon-button:hover {
color: inherit;
}
.icon-button:active {
color: $text-muted;
}
@import "../base.scss"; @import "../base.scss";