Only reveal hashids to the ui

This commit is contained in:
Patrick Jentsch
2021-11-30 16:22:16 +01:00
parent 3e227dc4cf
commit 72ba61f369
39 changed files with 1098 additions and 1083 deletions

View File

@ -0,0 +1,80 @@
class App {
constructor() {
this.data = {users: {}};
this.eventListeners = {'users.patch': []};
this.socket = io({transports: ['websocket'], upgrade: false});
this.socket.on('users.patch', patch => this.usersPatchHandler(patch));
}
get users() {return this.data.users;}
addEventListener(type, listener) {
if (!(type in this.eventListeners)) {throw `Unknown event type: ${type}`;}
this.eventListeners[type].push(listener);
}
flash(message, category) {
let toast, toastCloseActionElement;
switch (category) {
case "corpus":
message = `<i class="left material-icons">book</i>${message}`;
break;
case "error":
message = `<i class="left material-icons error-color-text">error</i>${message}`;
break;
case "job":
message = `<i class="left nopaque-icons">J</i>${message}`;
break;
default:
message = `<i class="left material-icons">notifications</i>${message}`;
}
toast = M.toast({
html: `
<span>${message}</span>
<button class="btn-flat toast-action white-text" data-action="close">
<i class="material-icons">close</i>
</button>
`.trim()
});
toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
}
getUserById(userId) {
return new Promise((resolve, reject) => {
if (userId in this.data.users) {resolve(this.users[userId]);}
this.socket.emit('users.user.get', userId, response => {
if (response.code === 200) {
this.data.users[userId] = response.payload;
resolve(this.data.users[userId]);
} else {
reject(response);
}
});
});
}
usersPatchHandler(patch) {
let re, match, userId, ressourceId, jobId, relationship;
for (let operation of patch.filter(operation => operation.op === 'add')) {
re = new RegExp(`^/users/([A-Za-z0-9]*)/corpora/([A-Za-z0-9]*)/(files)`);
if (re.test(operation.path)) {
[match, userId, ressourceId, relationship] = operation.path.match(re);
if (!(relationship in this.users[userId].corpora[ressourceId])) {
this.users[userId].corpora[ressourceId][relationship] = {};
}
continue;
}
re = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/(inputs|results)`);
if (re.test(operation.path)) {
[match, userId, ressourceId, relationship] = operation.path.match(re);
if (!(relationship in this.users[userId].jobs[ressourceId])) {
this.users[userId].jobs[ressourceId][relationship] = {};
}
continue;
}
}
this.data = jsonpatch.apply_patch(this.data, patch);
for (let listener of this.eventListeners['users.patch']) {listener(patch);}
}
}

View File

@ -0,0 +1,17 @@
class JobStatusNotifier {
constructor(userId) {
this.userId = userId;
}
usersPatchHandler(patch) {
let re, filteredPatch, match, jobId;
re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/status$`)
filteredPatch = patch
.filter(operation => operation.op === 'replace')
.filter(operation => re.test(operation.path));
for (let operation of filteredPatch) {
[match, jobId] = operation.path.match(re);
app.flash(`[<a href="/jobs/${jobId}">${app.users[this.userId].jobs[jobId].title}</a>] New status: ${operation.value}`, 'job');
}
}
}

View File

@ -2,31 +2,34 @@ class CorpusDisplay extends RessourceDisplay {
constructor(displayElement) {
super(displayElement);
this.corpusId = displayElement.dataset.corpusId;
this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.corpusId);
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {
exportCorpusTriggerElement.addEventListener('click', () => this.requestCorpusExport());
}
app.socket.on(`export_corpus_${this.corpusId}`, () => this.downloadCorpus());
}
init() {
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.addEventListener('click', () => this.requestCorpusExport());}
nopaque.appClient.socket.on(`export_corpus_${this.user.data.corpora[this.corpusId].id}`, () => this.downloadCorpus());
this.setCreationDate(this.user.data.corpora[this.corpusId].creation_date);
this.setDescription(this.user.data.corpora[this.corpusId].description);
this.setLastEditedDate(this.user.data.corpora[this.corpusId].last_edited_date);
this.setStatus(this.user.data.corpora[this.corpusId].status);
this.setTitle(this.user.data.corpora[this.corpusId].title);
this.setTokenRatio(this.user.data.corpora[this.corpusId].num_tokens, this.user.data.corpora[this.corpusId].max_num_tokens);
init(user) {
let corpus;
corpus = user.corpora[this.corpusId];
this.setCreationDate(corpus.creation_date);
this.setDescription(corpus.description);
this.setLastEditedDate(corpus.last_edited_date);
this.setStatus(corpus.status);
this.setTitle(corpus.title);
this.setTokenRatio(corpus.num_tokens, corpus.max_num_tokens);
}
patch(patch) {
let re;
for (let operation of patch) {
let re, filteredPatch;
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}`);
filteredPatch = patch.filter(operation => re.test(operation.path));
for (let operation of filteredPatch) {
switch(operation.op) {
case 'replace':
// Matches: /jobs/{this.job.id}/status
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/last_edited_date');
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/last_edited_date$`);
if (re.test(operation.path)) {this.setLastEditedDate(operation.value); break;}
// Matches: /jobs/{this.job.id}/status
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/status$');
if (re.test(operation.path)) {this.setStatus(operation.value); break;}
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/status$`);
if (re.test(operation.path)) {this.status$(operation.value); break;}
break;
default:
break;
@ -35,18 +38,19 @@ class CorpusDisplay extends RessourceDisplay {
}
requestCorpusExport() {
nopaque.appClient.socket.emit('export_corpus', this.user.data.corpora[this.corpusId].id);
nopaque.appClient.flash('Preparing your corpus export...', 'corpus');
app.socket.emit('export_corpus', app.users[this.userId].corpora[this.corpusId]);
app.flash('Preparing your corpus export...', 'corpus');
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.classList.toggle('disabled', true);}
}
downloadCorpus() {
nopaque.appClient.flash('Corpus export is done. Your corpus download is ready!', 'corpus');
let downloadButton;
app.flash('Corpus download is ready!', 'corpus');
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.classList.toggle('disabled', false);}
// Little trick to call the download view after ziping has finished
let fakeBtn = document.createElement('a');
fakeBtn.href = `/corpora/${this.user.data.corpora[this.corpusId].id}/download`;
fakeBtn.click();
downloadButton = document.createElement('a');
downloadButton.href = `/corpora/${app.users[this.userId].corpora[this.corpusId]}/download`;
downloadButton.click();
}
setTitle(title) {
@ -70,7 +74,7 @@ class CorpusDisplay extends RessourceDisplay {
}
}
for (let element of this.displayElement.querySelectorAll('.build-corpus-trigger')) {
if (status === 'unprepared' && Object.values(this.user.data.corpora[this.corpusId].files).length > 0) {
if (status === 'unprepared' && Object.values(app.users[this.userId].corpora[this.corpusId].files).length > 0) {
element.classList.remove('disabled');
} else {
element.classList.add('disabled');
@ -90,13 +94,15 @@ class CorpusDisplay extends RessourceDisplay {
}
}
setCreationDate(iso8601CreationDate) {
let creationDate = new Date(iso8601CreationDate).toLocaleString("en-US");
for (let element of this.displayElement.querySelectorAll('.corpus-creation-date')) {this.setElement(element, creationDate);}
setCreationDate(creationDate) {
for (let element of this.displayElement.querySelectorAll('.corpus-creation-date')) {
this.setElement(element, creationDate.toLocaleString("en-US"));
}
}
setLastEditedDate(iso8601LastEditedDate) {
let endDate = new Date(iso8601LastEditedDate).toLocaleString("en-US");
for (let element of this.displayElement.querySelectorAll('.corpus-end-date')) {this.setElement(element, endDate);}
setLastEditedDate(lastEditedDate) {
for (let element of this.displayElement.querySelectorAll('.corpus-end-date')) {
this.setElement(element, lastEditedDate.toLocaleString("en-US"));
}
}
}

View File

@ -1,32 +1,37 @@
class JobDisplay extends RessourceDisplay {
constructor(displayElement) {
super(displayElement);
this.jobId = displayElement.dataset.jobId;
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
this.jobId = this.displayElement.dataset.jobId;
}
init(job) {
this.setCreationDate(this.user.data.jobs[this.jobId].creation_date);
this.setEndDate(this.user.data.jobs[this.jobId].creation_date);
this.setDescription(this.user.data.jobs[this.jobId].description);
this.setService(this.user.data.jobs[this.jobId].service);
this.setServiceArgs(this.user.data.jobs[this.jobId].service_args);
this.setServiceVersion(this.user.data.jobs[this.jobId].service_version);
this.setStatus(this.user.data.jobs[this.jobId].status);
this.setTitle(this.user.data.jobs[this.jobId].title);
init(user) {
let job = user.jobs[this.jobId];
this.setCreationDate(job.creation_date);
this.setEndDate(job.creation_date);
this.setDescription(job.description);
this.setService(job.service);
this.setServiceArgs(job.service_args);
this.setServiceVersion(job.service_version);
this.setStatus(job.status);
this.setTitle(job.title);
}
patch(patch) {
let re;
for (let operation of patch) {
usersPatchHandler(patch) {
let re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}`);
let filteredPatch = patch.filter(operation => re.test(operation.path));
for (let operation of filteredPatch) {
switch(operation.op) {
case 'replace':
// Matches: /jobs/{this.user.data.jobs[this.jobId].id}/status
re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/end_date');
if (re.test(operation.path)) {this.setEndDate(operation.value); break;}
// Matches: /jobs/{this.user.data.jobs[this.jobId].id}/status
re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/status$');
if (re.test(operation.path)) {this.setStatus(operation.value); break;}
re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/end_date$`);
if (re.test(operation.path)) {
this.setEndDate(operation.value);
break;
}
re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/status$`);
if (re.test(operation.path)) {
this.setStatus(operation.value);
break;
}
break;
default:
break;
@ -63,14 +68,16 @@ class JobDisplay extends RessourceDisplay {
}
}
setCreationDate(iso8601CreationDate) {
let creationDate = new Date(iso8601CreationDate).toLocaleString("en-US");
for (let element of this.displayElement.querySelectorAll('.job-creation-date')) {this.setElement(element, creationDate);}
setCreationDate(creationDate) {
for (let element of this.displayElement.querySelectorAll('.job-creation-date')) {
this.setElement(element, creationDate.toLocaleString('en-US'));
}
}
setEndDate(iso8601EndDate) {
let endDate = new Date(iso8601EndDate).toLocaleString("en-US");
for (let element of this.displayElement.querySelectorAll('.job-end-date')) {this.setElement(element, endDate);}
setEndDate(endDate) {
for (let element of this.displayElement.querySelectorAll('.job-end-date')) {
this.setElement(element, endDate.toLocaleString('en-US'));
}
}
setService(service) {

View File

@ -1,35 +1,14 @@
class RessourceDisplay {
constructor(displayElement) {
if (displayElement.dataset.userId) {
if (displayElement.dataset.userId in nopaque.appClient.users) {
this.user = nopaque.appClient.users[displayElement.dataset.userId];
} else {
console.error(`User not found: ${displayElement.dataset.userId}`);
return;
}
} else {
this.user = nopaque.appClient.users.self;
}
this.displayElement = displayElement;
this.userId = this.displayElement.dataset.userId;
app.addEventListener('users.patch', patch => this.usersPatchHandler(patch));
app.getUserById(this.userId).then(user => this.init(user), error => {throw JSON.stringify(error);});
}
eventHandler(eventType, payload) {
switch (eventType) {
case 'init':
this.init(payload);
break;
case 'patch':
this.patch(payload);
break;
default:
console.error(`Unknown event type: ${eventType}`);
break;
}
}
init(user) {throw 'Not implemented';}
init() {console.error('init method not implemented!');}
patch() {console.error('patch method not implemented!');}
usersPatchHandler(patch) {throw 'Not implemented';}
setElement(element, value) {
switch (element.tagName) {

View File

@ -2,32 +2,32 @@ class CorpusFileList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...CorpusFileList.options, ...options});
this.corpusId = listElement.dataset.corpusId;
this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.corpusId);
}
init() {
super.init(this.user.data.corpora[this.corpusId].files);
init(user) {
this._init(user.corpora[this.corpusId].files);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let corpusFileId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
if (actionButtonElement === null) {return;}
let corpusFileElement = event.target.closest('tr[data-id]');
if (corpusFileElement === null) {throw 'Could not locate corpus file element';}
let corpusFileId = corpusFileElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button[data-action]');
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the corpus file <b>${this.user.data.corpora[this.corpusId].files[corpusFileId].filename}</b>? It will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.corpora[this.corpusId].files[corpusFileId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalHTML = `
<div class="modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the corpus file <b>${app.users[this.userId].corpora[this.corpusId].files[corpusFileId].filename}</b>? It will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="/corpora/${this.corpusId}/files/${corpusFileId}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
`.trim();
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
@ -35,40 +35,37 @@ class CorpusFileList extends RessourceList {
deleteModal.open();
break;
case 'download':
window.location.href = this.user.data.corpora[this.corpusId].files[corpusFileId].download_url;
window.location.href = `/corpora/${this.corpusId}/files/${corpusFileId}/download`;
break;
case 'view':
if (corpusFileId !== '-1') {window.location.href = this.user.data.corpora[this.corpusId].files[corpusFileId].url;}
window.location.href = `/corpora/${this.corpusId}/files/${corpusFileId}`;
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
patch(patch) {
let id, match, re, valueName;
for (let operation of patch) {
usersPatchHandler(patch) {
let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)`);
let filteredPatch = patch.filter(operation => re.test(operation.path));
for (let operation of filteredPatch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /corpora/{this.user.data.corpora[this.corpusId].id}/files/{corpusFileId}
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)$');
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)$`);
if (re.test(operation.path)) {this.add(operation.value);}
break;
case 'remove':
// See case add ;)
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)$');
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)$`);
if (re.test(operation.path)) {
[match, id] = operation.path.match(re);
this.remove(id);
[match, corpusFileId] = operation.path.match(re);
this.remove(corpusFileId);
}
break;
case 'replace':
// Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title}
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)/(author|filename|publishing_year|title)$');
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)/(author|filename|publishing_year|title)$`);
if (re.test(operation.path)) {
[match, id, valueName] = operation.path.match(re);
this.replace(id, valueName, operation.value);
[match, corpusFileId, valueName] = operation.path.match(re);
this.replace(corpusFileId, valueName, operation.value);
}
break;
default:
@ -78,20 +75,29 @@ class CorpusFileList extends RessourceList {
}
preprocessRessource(corpusFile) {
return {id: corpusFile.id, author: corpusFile.author, filename: corpusFile.filename, publishing_year: corpusFile.publishing_year, title: corpusFile.title};
return {
id: corpusFile.id,
author: corpusFile.author,
creationDate: corpusFile.creation_date,
filename: corpusFile.filename,
publishing_year: corpusFile.publishing_year,
title: corpusFile.title
};
}
}
CorpusFileList.options = {
item: `<tr>
<td><span class="filename"></span></td>
<td><span class="author"></span></td>
<td><span class="title"></span></td>
<td><span class="publishing_year"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
item: `
<tr>
<td><span class="filename"></span></td>
<td><span class="author"></span></td>
<td><span class="title"></span></td>
<td><span class="publishing_year"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim(),
valueNames: [{data: ['id']}, 'author', 'filename', 'publishing_year', 'title']
};

View File

@ -1,31 +1,32 @@
class CorpusList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...CorpusList.options, ...options});
this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
}
init() {
super.init(this.user.data.corpora);
init(user) {
super._init(user.corpora);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let corpusId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let action = (actionButtonElement === null) ? 'view' : actionButtonElement.dataset.action;
let corpusElement = event.target.closest('tr[data-id]');
if (corpusElement === null) {throw 'Could not locate corpus element';}
let corpusId = corpusElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button[data-action]');
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the corpus <b>${this.user.data.corpora[corpusId].title}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.corpora[corpusId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalHTML = `
<div class="modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the corpus <b>${app.users[this.userId].corpora[corpusId].title}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="/corpora/${corpusId}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
`.trim();
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
@ -33,37 +34,34 @@ class CorpusList extends RessourceList {
deleteModal.open();
break;
case 'view':
if (corpusId !== '-1') {window.location.href = this.user.data.corpora[corpusId].url;}
window.location.href = `/corpora/${corpusId}`;
break;
default:
console.error(`Unknown action: ${action}`);
break;
}
}
patch(patch) {
let id, match, re, valueName;
for (let operation of patch) {
usersPatchHandler(patch) {
let re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)`);
let filteredPatch = patch.filter(operation => re.test(operation.path));
for (let operation of filteredPatch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /corpora/{corpusId}
re = /^\/corpora\/(\d+)$/;
re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)$`);
if (re.test(operation.path)) {this.add(operation.value);}
break;
case 'remove':
// See case 'add' ;)
re = /^\/corpora\/(\d+)$/;
re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)$`);
if (re.test(operation.path)) {
[match, id] = operation.path.match(re);
this.remove(id);
let [match, corpusId] = operation.path.match(re);
this.remove(corpusId);
}
break;
case 'replace':
// Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title}
re = /^\/corpora\/(\d+)\/(status|description|title)$/;
re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)/(status|description|title)$`);
if (re.test(operation.path)) {
[match, id, valueName] = operation.path.match(re);
this.replace(id, valueName, operation.value);
let [match, corpusId, valueName] = operation.path.match(re);
this.replace(corpusId, valueName, operation.value);
}
break;
default:
@ -73,21 +71,26 @@ class CorpusList extends RessourceList {
}
preprocessRessource(corpus) {
return {id: corpus.id,
status: corpus.status,
description: corpus.description,
title: corpus.title};
return {
id: corpus.id,
creationDate: corpus.creation_date,
description: corpus.description,
status: corpus.status,
title: corpus.title
};
}
}
CorpusList.options = {
item: `<tr>
<td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
item: `
<tr>
<td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim(),
valueNames: [{data: ['id']}, {name: 'status', attr: 'data-status'}, 'description', 'title']
};

View File

@ -2,40 +2,46 @@ class JobInputList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...JobInputList.options, ...options});
this.jobId = listElement.dataset.jobId;
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
}
init() {
super.init(this.user.data.jobs[this.jobId].inputs);
init(user) {
this._init(user.jobs[this.jobId].inputs);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let jobInputId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let jobInputElement = event.target.closest('tr[data-id]');
if (jobInputElement === null) {return;}
let jobInputId = jobInputElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button[data-action]');
if (actionButtonElement === null) {return;}
let action = actionButtonElement.dataset.action;
switch (action) {
case 'download':
window.location.href = this.user.data.jobs[this.jobId].inputs[jobInputId].download_url;
window.location.href = `/jobs/${this.jobId}/inputs/${jobInputId}/download`;
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
usersPatchHandler(patch) {return;}
preprocessRessource(jobInput) {
return {id: jobInput.id, filename: jobInput.filename};
return {
id: jobInput.id,
creationDate: jobInput.creation_date,
filename: jobInput.filename
};
}
}
JobInputList.options = {
item: `<tr>
<td><span class="filename"></span></td>
<td class="right-align">
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
</td>
</tr>`,
item: `
<tr>
<td><span class="filename"></span></td>
<td class="right-align">
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
</td>
</tr>
`.trim(),
valueNames: [{data: ['id']}, 'filename']
};

View File

@ -1,31 +1,32 @@
class JobList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...JobList.options, ...options});
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
}
init() {
super.init(this.user.data.jobs);
init(user) {
this._init(user.jobs);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let jobId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let jobElement = event.target.closest('tr[data-id]');
if (jobElement === null) {throw 'Could not locate job element';}
let jobId = jobElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button[data-action]');
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm job deletion</h4>
<p>Do you really want to delete the job <b>${this.user.data.jobs[jobId].title}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.jobs[jobId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalHTML = `
<div class="modal">
<div class="modal-content">
<h4>Confirm job deletion</h4>
<p>Do you really want to delete the job <b>${app.users[this.userId].jobs[jobId].title}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="/jobs/${jobId}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
`.trim();
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
@ -33,37 +34,34 @@ class JobList extends RessourceList {
deleteModal.open();
break;
case 'view':
if (jobId !== '-1') {window.location.href = this.user.data.jobs[jobId].url;}
window.location.href = `/jobs/${jobId}`;
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
patch(patch) {
let id, match, re, valueName;
for (let operation of patch) {
usersPatchHandler(patch) {
let re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)`);
let filteredPatch = patch.filter(operation => re.test(operation.path));
for (let operation of filteredPatch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /jobs/{jobId}
re = /^\/jobs\/(\d+)$/;
re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)$`);
if (re.test(operation.path)) {this.add(operation.value);}
break;
case 'remove':
// See case add ;)
re = /^\/jobs\/(\d+)$/;
re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)$`);
if (re.test(operation.path)) {
[match, id] = operation.path.match(re);
this.remove(id);
let [match, jobId] = operation.path.match(re);
this.remove(jobId);
}
break;
case 'replace':
// Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title}
re = /^\/jobs\/(\d+)\/(service|status|description|title)$/;
re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/(service|status|description|title)$`);
if (re.test(operation.path)) {
[match, id, valueName] = operation.path.match(re);
this.replace(id, valueName, operation.value);
let [match, jobId, valueName] = operation.path.match(re);
this.replace(jobId, valueName, operation.value);
}
break;
default:
@ -73,22 +71,27 @@ class JobList extends RessourceList {
}
preprocessRessource(job) {
return {id: job.id,
service: job.service,
status: job.status,
description: job.description,
title: job.title};
return {
id: job.id,
creationDate: job.creation_date,
description: job.description,
service: job.service,
status: job.status,
title: job.title
};
}
}
JobList.options = {
item: `<tr>
<td><a class="btn-floating disabled"><i class="nopaque-icons service service-color darken service-icon"></i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
item: `
<tr>
<td><a class="btn-floating disabled"><i class="nopaque-icons service service-color darken service-icon"></i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim(),
valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, {name: 'status', attr: 'data-status'}, 'description', 'title']
};

View File

@ -2,37 +2,35 @@ class JobResultList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...JobResultList.options, ...options});
this.jobId = listElement.dataset.jobId;
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
}
init() {
super.init(this.user.data.jobs[this.jobId].results);
init(user) {
super._init(user.jobs[this.jobId].results);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let jobResultId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let jobResultElement = event.target.closest('tr[data-id]');
if (jobResultElement === null) {return;}
let jobResultId = jobResultElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button[data-action]');
if (actionButtonElement === null) {return;}
let action = actionButtonElement.dataset.action;
switch (action) {
case 'download':
window.location.href = this.user.data.jobs[this.jobId].results[jobResultId].download_url;
window.location.href = `/jobs/${this.jobId}/results/${jobResultId}`;
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
patch(patch) {
let re;
for (let operation of patch) {
usersPatchHandler(patch) {
let re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/results/([A-Za-z0-9]*)`);
let filteredPatch = patch.filter(operation => re.test(operation.path));
for (let operation of filteredPatch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /jobs/{this.user.data.jobs[this.jobId].id}/results/{jobResultId}
re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/results/(\\d+)$');
re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/results/([A-Za-z0-9]*)$`);
if (re.test(operation.path)) {this.add(operation.value);}
break;
default:
@ -56,16 +54,23 @@ class JobResultList extends RessourceList {
} else {
description = 'All result files created during this job';
}
return {id: jobResult.id, description: description, filename: jobResult.filename};
return {
id: jobResult.id,
creationDate: jobResult.creation_date,
description: description,
filename: jobResult.filename
};
}
}
JobResultList.options = {
item: `<tr>
<td><span class="description"></span></td>
<td><span class="filename"></span></td>
<td class="right-align">
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
</td>
</tr>`,
item: `
<tr>
<td><span class="description"></span></td>
<td><span class="filename"></span></td>
<td class="right-align">
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
</td>
</tr>
`.trim(),
valueNames: [{data: ['id']}, 'description', 'filename']
};

View File

@ -1,31 +1,32 @@
class QueryResultList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...QueryResultList.options, ...options});
this.user.eventListeners.queryResult.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
}
init() {
super.init(this.user.data.query_results);
init(user) {
super.init(user.query_results);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let queryResultId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let queryResultElement = event.target.closest('tr[data-id]');
if (queryResultElement === null) {return;}
let queryResultId = queryResultElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button[data-action]');
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm query result deletion</h4>
<p>Do you really want to delete the query result <b>${this.user.data.query_results[queryResultId].title}</b>? It will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.query_results[queryResultId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalHTML = `
<div class="modal">
<div class="modal-content">
<h4>Confirm query result deletion</h4>
<p>Do you really want to delete the query result <b>${app.users[this.userId].query_results[queryResultId].title}</b>? It will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="/query_results/${queryResultId}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
`.trim();
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
@ -33,37 +34,34 @@ class QueryResultList extends RessourceList {
deleteModal.open();
break;
case 'view':
if (queryResultId !== '-1') {window.location.href = this.user.data.query_results[queryResultId].url;}
window.location.href = `/query_results/${queryResultId}`;
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
patch(patch) {
let id, match, re, valueName;
for (let operation of patch) {
usersPatchHandler(patch) {
let re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)`);
let filteredPatch = patch.filter(operation => re.test(operation.path));
for (let operation of filteredPatch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /jobs/{jobId}
re = /^\/query_results\/(\d+)$/;
re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)$`);
if (re.test(operation.path)) {this.add(operation.value);}
break;
case 'remove':
// See case add ;)
re = /^\/query_results\/(\d+)$/;
re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)$`);
if (re.test(operation.path)) {
[match, id] = operation.path.match(re);
this.remove(id);
let [match, queryResultId] = operation.path.match(re);
this.remove(queryResultId);
}
break;
case 'replace':
// Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title}
re = /^\/query_results\/(\d+)\/(corpus_title|description|query|title)$/;
re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)/(corpus_title|description|query|title)$`);
if (re.test(operation.path)) {
[match, id, valueName] = operation.path.match(re);
this.replace(id, valueName, operation.value);
let [match, queryResultId, valueName] = operation.path.match(re);
this.replace(queryResultId, valueName, operation.value);
}
break;
default:
@ -73,21 +71,26 @@ class QueryResultList extends RessourceList {
}
preprocessRessource(queryResult) {
return {id: queryResult.id,
corpus_title: queryResult.corpus_title,
description: queryResult.description,
query: queryResult.query,
title: queryResult.title};
return {
id: queryResult.id,
corpus_title: queryResult.corpus_title,
creationDate: queryResult.creation_date,
description: queryResult.description,
query: queryResult.query,
title: queryResult.title
};
}
}
QueryResultList.options = {
item: `<tr>
<td><b class="title"></b><br><i class="description"></i><br></td>
<td><span class="corpus_title"></span><br><span class="query"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
item: `
<tr>
<td><b class="title"></b><br><i class="description"></i><br></td>
<td><span class="corpus_title"></span><br><span class="query"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim(),
valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title']
};

View File

@ -4,87 +4,68 @@ class RessourceList {
* a base class for concrete ressource list implementations.
*/
constructor(listElement, options = {}) {
if (listElement.dataset.userId) {
if (listElement.dataset.userId in nopaque.appClient.users) {
this.user = nopaque.appClient.users[listElement.dataset.userId];
} else {
console.error(`User not found: ${listElement.dataset.userId}`);
return;
}
} else {
this.user = nopaque.appClient.users.self;
}
this.list = new List(listElement, {...RessourceList.options, ...options});
this.list.list.innerHTML = `<tr>
<td class="row" colspan="100%">
<div class="col s12">&nbsp;</div>
<div class="col s3 m2 xl1">
<div class="preloader-wrapper active">
<div class="spinner-layer spinner-green-only">
<div class="circle-clipper left">
<div class="circle"></div>
</div>
<div class="gap-patch">
<div class="circle"></div>
</div>
<div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
</div>
<div class="col s9 m6 xl5">
<span class="card-title">Waiting for data...</span>
<p>This list is not initialized yet.</p>
</div>
</td>
</tr>`;
this.list.list.innerHTML = `
<tr>
<td class="row" colspan="100%">
<div class="col s12">&nbsp;</div>
<div class="col s3 m2 xl1">
<div class="preloader-wrapper active">
<div class="spinner-layer spinner-green-only">
<div class="circle-clipper left">
<div class="circle"></div>
</div>
<div class="gap-patch">
<div class="circle"></div>
</div>
<div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
</div>
<div class="col s9 m6 xl5">
<span class="card-title">Waiting for data...</span>
<p>This list is not initialized yet.</p>
</div>
</td>
</tr>
`.trim();
this.list.list.style.cursor = 'pointer';
this.userId = listElement.dataset.userId;
if (typeof this.onclick === 'function') {this.list.list.addEventListener('click', event => this.onclick(event));}
}
eventHandler(eventType, payload) {
switch (eventType) {
case 'init':
this.init();
break;
case 'patch':
this.patch(payload);
break;
default:
console.error(`Unknown event type: ${eventType}`);
break;
if (this.userId) {
app.addEventListener('users.patch', patch => this.usersPatchHandler(patch));
app.getUserById(this.userId).then(
user => this.init(user),
error => {throw JSON.stringify(error);}
);
}
}
init(ressources) {
_init(ressources) {
this.list.clear();
this.add(Object.values(ressources));
this.list.sort('id', {order: 'desc'});
let emptyListElementHTML = `<tr class="show-if-only-child" data-id="-1">
<td colspan="100%">
<span class="card-title"><i class="left material-icons" style="font-size: inherit;">file_download</i>Nothing here...</span>
<p>No ressource available.</p>
</td>
</tr>`;
let emptyListElementHTML = `
<tr class="show-if-only-child">
<td colspan="100%">
<span class="card-title"><i class="left material-icons" style="font-size: inherit;">file_download</i>Nothing here...</span>
<p>No ressource available.</p>
</td>
</tr>
`.trim();
this.list.list.insertAdjacentHTML('afterbegin', emptyListElementHTML);
}
patch(patch) {
/*
* It's not possible to generalize a patch Handler for all type of
* ressources. So this method is meant to be an interface.
*/
console.error('patch method not implemented!');
}
init(user) {throw 'Not implemented';}
usersPatchHandler(patch) {throw 'Not implemented';}
preprocessRessource() {throw 'Not implemented'}
add(values) {
let ressources = Array.isArray(values) ? values : [values];
if (typeof this.preprocessRessource === 'function') {
ressources = ressources.map(ressource => this.preprocessRessource(ressource));
}
// Set a callback function ('() => {return;}') to force List.js perform the
// add method asynchronous: https://listjs.com/api/#add
ressources = ressources.map(ressource => this.preprocessRessource(ressource));
this.list.add(ressources, () => {
this.list.sort('id', {order: 'desc'});
});

View File

@ -1,32 +1,32 @@
class UserList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...UserList.options, ...options});
users = undefined;
}
init(users) {
this.users = users;
super.init(users);
super._init(Object.values(users));
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let userId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let userElement = event.target.closest('tr[data-id]');
if (userElement === null) {return;}
let userId = userElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button[data-action]');
let action = (actionButtonElement === null) ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm user deletion</h4>
<p>Do you really want to delete the corpus <b>${this.users[userId].username}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="/admin/users/${userId}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalHTML = `
<div class="modal">
<div class="modal-content">
<h4>Confirm user deletion</h4>
<p>Do you really want to delete user <b>${userId}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="/admin/users/${userId}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
`.trim();
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
@ -37,35 +37,38 @@ class UserList extends RessourceList {
window.location.href = `/admin/users/${userId}/edit`;
break;
case 'view':
if (userId !== '-1') {window.location.href = `/admin/users/${userId}`;}
window.location.href = `/admin/users/${userId}`;
break;
default:
console.error(`Unknown action: ${action}`);
break;
}
}
preprocessRessource(user) {
return {id: user.id,
id_: user.id,
username: user.username,
email: user.email,
last_seen: new Date(user.last_seen).toLocaleString("en-US"),
role: user.role.name};
return {
id: user.id,
id_: user.id,
username: user.username,
email: user.email,
last_seen: user.last_seen.toLocaleString("en-US"),
role: user.role.name
};
}
}
UserList.options = {
item: `<tr>
<td><span class="id_"></span></td>
<td><span class="username"></span></td>
<td><span class="email"></span></td>
<td><span class="last_seen"></span></td>
<td><span class="role"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="edit" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
item: `
<tr>
<td><span class="id_"></span></td>
<td><span class="username"></span></td>
<td><span class="email"></span></td>
<td><span class="last_seen"></span></td>
<td><span class="role"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="edit" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim(),
valueNames: [{data: ['id']}, 'id_', 'username', 'email', 'last_seen', 'role']
};

View File

@ -1,177 +1,3 @@
class AppClient {
constructor(currentUserId) {
if (currentUserId) {
this.socket = io({transports: ['websocket'], upgrade: false});
this.users = {};
this.users.self = this.loadUser(currentUserId);
this.users.self.eventListeners.job.addEventListener((eventType, payload) => this.jobEventHandler(eventType, payload));
}
}
flash(message, category) {
let toast;
let toastCloseActionElement;
switch (category) {
case "corpus":
message = `<i class="left material-icons">book</i>${message}`;
break;
case "error":
message = `<i class="left material-icons error-color-text">error</i>${message}`;
break;
case "job":
message = `<i class="left nopaque-icons">J</i>${message}`;
break;
default:
message = `<i class="left material-icons">notifications</i>${message}`;
}
toast = M.toast({html: `<span>${message}</span>
<button data-action="close" class="btn-flat toast-action white-text">
<i class="material-icons">close</i>
</button>`});
toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
}
jobEventHandler(eventType, payload) {
switch (eventType) {
case 'init':
break;
case 'patch':
this.jobPatch(payload);
break;
default:
console.error(`[AppClient.jobEventHandler] Unknown event type: ${eventType}`);
break;
}
}
loadUser(userId) {
if (userId in this.users) {return this.users[userId];}
let user = new User();
this.users[userId] = user;
this.socket.on(`user_${userId}_init`, msg => user.init(msg));
this.socket.on(`user_${userId}_patch`, msg => user.patch(msg));
this.socket.emit('start_user_session', userId);
return user;
}
jobPatch(patch) {
if (this.users.self.data.settings.job_status_site_notifications === 'none') {return;}
let jobStatusPatches = patch.filter(operation => operation.op === 'replace' && /^\/jobs\/(\d+)\/status$/.test(operation.path));
for (let operation of jobStatusPatches) {
let [match, jobId] = operation.path.match(/^\/jobs\/(\d+)\/status$/);
if (this.users.self.data.settings.job_status_site_notifications === "end" && !['complete', 'failed'].includes(operation.value)) {continue;}
this.flash(`[<a href="/jobs/${jobId}">${this.users.self.data.jobs[jobId].title}</a>] New status: ${operation.value}`, 'job');
}
}
}
class User {
constructor() {
this.data = undefined;
this.eventListeners = {
corpus: {
addEventListener(listener, corpusId='*') {
if (corpusId in this) {this[corpusId].push(listener);} else {this[corpusId] = [listener];}
}
},
job: {
addEventListener(listener, jobId='*') {
if (jobId in this) {this[jobId].push(listener);} else {this[jobId] = [listener];}
}
},
queryResult: {
addEventListener(listener, queryResultId='*') {
if (queryResultId in this) {this[queryResultId].push(listener);} else {this[queryResultId] = [listener];}
}
}
};
}
init(data) {
this.data = data;
for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
if (corpusId === '*') {
for (let eventListener of eventListeners) {eventListener('init');}
} else {
if (corpusId in this.data.corpora) {
for (let eventListener of eventListeners) {eventListener('init');}
}
}
}
for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
if (jobId === '*') {
for (let eventListener of eventListeners) {eventListener('init');}
} else {
if (jobId in this.data.jobs) {
for (let eventListener of eventListeners) {eventListener('init');}
}
}
}
for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
if (queryResultId === '*') {
for (let eventListener of eventListeners) {eventListener('init');}
} else {
if (queryResultId in this.data.query_results) {
for (let eventListener of eventListeners) {eventListener('init');}
}
}
}
}
patch(patch) {
this.data = jsonpatch.apply_patch(this.data, patch);
let corporaPatch = patch.filter(operation => operation.path.startsWith("/corpora"));
if (corporaPatch.length > 0) {
for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
if (corpusId === '*') {
for (let eventListener of eventListeners) {eventListener('patch', corporaPatch);}
} else {
let corpusPatch = corporaPatch.filter(operation => operation.path.startsWith(`/corpora/${corpusId}`));
if (corpusPatch.length > 0) {
for (let eventListener of eventListeners) {eventListener('patch', corpusPatch);}
}
}
}
}
let jobsPatch = patch.filter(operation => operation.path.startsWith("/jobs"));
if (jobsPatch.length > 0) {
for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
if (jobId === '*') {
for (let eventListener of eventListeners) {eventListener('patch', jobsPatch);}
} else {
let jobPatch = jobsPatch.filter(operation => operation.path.startsWith(`/jobs/${jobId}`));
if (jobPatch.length > 0) {
for (let eventListener of eventListeners) {eventListener('patch', jobPatch);}
}
}
}
}
let queryResultsPatch = patch.filter(operation => operation.path.startsWith("/query_results"));
if (queryResultsPatch.length > 0) {
for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
if (queryResultId === '*') {
for (let eventListener of eventListeners) {eventListener('patch', queryResultsPatch);}
} else {
let queryResultPatch = queryResultsPatch.filter(operation => operation.path.startsWith(`/query_results/${queryResultId}`));
if (queryResultPatch.length > 0) {
for (let eventListener of eventListeners) {eventListener('patch', queryResultPatch);}
}
}
}
}
}
}
/*
* The nopaque object is used as a namespace for nopaque specific functions and
* variables.