From 581fd5bcb58441297430cace55264f8a084a05db Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Mon, 11 Jan 2021 13:36:45 +0100 Subject: [PATCH] Seperate Lists and Displays in seperated files --- web/app/static/js/nopaque/RessorceLists.js | 500 ------------------ .../static/js/nopaque/RessourceDisplays.js | 215 -------- .../js/nopaque/displays/CorpusDisplay.js | 77 +++ .../static/js/nopaque/displays/JobDisplay.js | 88 +++ .../js/nopaque/displays/RessourceDisplay.js | 45 ++ .../static/js/nopaque/lists/CorpusFileList.js | 91 ++++ web/app/static/js/nopaque/lists/CorpusList.js | 100 ++++ .../static/js/nopaque/lists/JobInputList.js | 43 ++ web/app/static/js/nopaque/lists/JobList.js | 91 ++++ .../static/js/nopaque/lists/JobResultList.js | 73 +++ .../js/nopaque/lists/QueryResultList.js | 18 + .../static/js/nopaque/lists/RessourceList.js | 72 +++ .../static/js/nopaque/{index.js => main.js} | 0 web/app/templates/main/dashboard.html.j2 | 3 +- web/app/templates/nopaque.html.j2 | 14 +- 15 files changed, 711 insertions(+), 719 deletions(-) delete mode 100644 web/app/static/js/nopaque/RessorceLists.js delete mode 100644 web/app/static/js/nopaque/RessourceDisplays.js create mode 100644 web/app/static/js/nopaque/displays/CorpusDisplay.js create mode 100644 web/app/static/js/nopaque/displays/JobDisplay.js create mode 100644 web/app/static/js/nopaque/displays/RessourceDisplay.js create mode 100644 web/app/static/js/nopaque/lists/CorpusFileList.js create mode 100644 web/app/static/js/nopaque/lists/CorpusList.js create mode 100644 web/app/static/js/nopaque/lists/JobInputList.js create mode 100644 web/app/static/js/nopaque/lists/JobList.js create mode 100644 web/app/static/js/nopaque/lists/JobResultList.js create mode 100644 web/app/static/js/nopaque/lists/QueryResultList.js create mode 100644 web/app/static/js/nopaque/lists/RessourceList.js rename web/app/static/js/nopaque/{index.js => main.js} (100%) diff --git a/web/app/static/js/nopaque/RessorceLists.js b/web/app/static/js/nopaque/RessorceLists.js deleted file mode 100644 index 236c97e8..00000000 --- a/web/app/static/js/nopaque/RessorceLists.js +++ /dev/null @@ -1,500 +0,0 @@ -class RessourceList { - /* A wrapper class for the list.js list. - * This class is not meant to be used directly, instead it should be used as - * 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 = ` - - file_downloadNothing here... -

No ressource available (yet).

- - `; - } - - 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(ressources) { - this.list.clear(); - this.add(Object.values(ressources)); - this.list.sort('id', {order: 'desc'}); - } - - 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!'); - } - - 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 - this.list.add(ressources, () => {return;}); - } - - remove(id) { - this.list.remove('id', id); - } - - replace(id, valueName, newValue) { - this.list.get('id', id)[0].values({[valueName]: newValue}); - } -} -RessourceList.options = {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]}; - - -class CorpusList extends RessourceList { - constructor(listElement, options = {}) { - super(listElement, {...CorpusList.options, ...options}); - this.corpora = undefined; - this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload)); - listElement.addEventListener('click', event => this.onclick(event)); - } - - init(corpora) { - this.corpora = corpora; - super.init(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; - switch (action) { - case 'analyse': - window.location.href = this.corpora[corpusId].analysis_url; - case 'delete': - let deleteModalHTML = ``; - let deleteModalParentElement = document.querySelector('main'); - deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML); - let deleteModalElement = deleteModalParentElement.lastChild; - let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}}); - deleteModal.open(); - break; - case 'view': - // TODO: handle unprepared corpora - window.location.href = this.corpora[corpusId].url; - break; - default: - console.error(`Unknown action: ${action}`); - break; - } - } - - patch(patch) { - let id, match, re, valueName; - for (let operation of patch) { - switch(operation.op) { - case 'add': - // Matches the only paths that should be handled here: /corpora/{corpusId} - re = /^\/corpora\/(\d+)$/; - if (re.test(operation.path)) {this.add(operation.value);} - break; - case 'remove': - // See case 'add' ;) - re = /^\/corpora\/(\d+)$/; - if (re.test(operation.path)) { - [match, id] = operation.path.match(re); - this.remove(id); - } - break; - case 'replace': - // Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title} - re = /^\/corpora\/(\d+)\/(status|description|title)$/; - if (re.test(operation.path)) { - [match, id, valueName] = operation.path.match(re); - this.replace(id, valueName, operation.value); - } - break; - default: - break; - } - } - } - - preprocessRessource(corpus) { - return {id: corpus.id, - status: corpus.status, - description: corpus.description, - title: corpus.title}; - } -} -CorpusList.options = { - item: ` - book -
- - - delete - search - send - - `, - valueNames: [{data: ['id']}, {name: 'status', attr: 'data-status'}, 'description', 'title'] -}; - - -class CorpusFileList extends RessourceList { - constructor(listElement, options = {}) { - super(listElement, {...CorpusFileList.options, ...options}); - this.corpus = undefined; - this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), listElement.dataset.corpusId); - listElement.addEventListener('click', event => this.onclick(event)); - } - - init(corpus) { - this.corpus = corpus; - super.init(this.corpus.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 action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action; - switch (action) { - case 'delete': - let deleteModalHTML = ``; - let deleteModalParentElement = document.querySelector('main'); - deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML); - let deleteModalElement = deleteModalParentElement.lastChild; - let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}}); - deleteModal.open(); - break; - case 'download': - window.location.href = this.corpus.files[corpusFileId].download_url; - break; - case 'view': - window.location.href = this.corpus.files[corpusFileId].url; - break; - default: - console.error(`Unknown action: "${action}"`); - break; - } - } - - patch(patch) { - let id, match, re; - for (let operation of patch) { - switch(operation.op) { - case 'add': - // Matches the only paths that should be handled here: /corpora/{this.corpus.id}/files/{corpusFileId} - re = new RegExp('^/corpora/' + this.corpus.id + '/files/(\\d+)$'); - if (re.test(operation.path)) {this.add(operation.value);} - break; - case 'remove': - // See case add ;) - re = new RegExp('^/corpora/' + this.corpus.id + '/files/(\\d+)$'); - if (re.test(operation.path)) { - [match, id] = operation.path.match(re); - this.remove(id); - } - break; - default: - break; - } - } - } - - preprocessRessource(corpusFile) { - return {id: corpusFile.id, author: corpusFile.author, filename: corpusFile.filename, 'publishing-year': corpusFile.publishing_year, title: corpusFile.title}; - } -} -CorpusFileList.options = { - item: ` - - - - - - delete - file_download - send - - `, - valueNames: [{data: ['id']}, 'author', 'filename', 'publishing-year', 'title'] -}; - - -class JobList extends RessourceList { - constructor(listElement, options = {}) { - super(listElement, {...JobList.options, ...options}); - this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload)); - listElement.addEventListener('click', event => this.onclick(event)); - } - - 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 action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action; - switch (action) { - case 'delete': - let deleteModalHTML = ``; - let deleteModalParentElement = document.querySelector('main'); - deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML); - let deleteModalElement = deleteModalParentElement.lastChild; - let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}}); - deleteModal.open(); - break; - case 'view': - window.location.href = this.user.data.jobs[jobId].url; - break; - default: - console.error(`Unknown action: "${action}"`); - break; - } - } - - patch(patch) { - let id, match, re, valueName; - for (let operation of patch) { - switch(operation.op) { - case 'add': - // Matches the only paths that should be handled here: /jobs/{jobId} - re = /^\/jobs\/(\d+)$/; - if (re.test(operation.path)) {this.add(operation.value);} - break; - case 'remove': - // See case add ;) - re = /^\/jobs\/(\d+)$/; - if (re.test(operation.path)) { - [match, id] = operation.path.match(re); - this.remove(id); - } - break; - case 'replace': - // Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title} - re = /^\/jobs\/(\d+)\/(status|description|title)$/; - if (re.test(operation.path)) { - [match, id, valueName] = operation.path.match(re); - this.replace(id, valueName, operation.value); - } - break; - default: - break; - } - } - } - - preprocessRessource(job) { - return {id: job.id, - service: job.service, - status: job.status, - description: job.description, - title: job.title}; - } -} -JobList.options = { - item: ` - -
- - - delete - send - - `, - valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, {name: 'status', attr: 'data-status'}, 'description', 'title'] -}; - - -class JobInputList extends RessourceList { - constructor(listElement, options = {}) { - super(listElement, {...JobInputList.options, ...options}); - this.job = undefined; - this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), listElement.dataset.jobId); - listElement.addEventListener('click', event => this.onclick(event)); - } - - init(job) { - this.job = job; - super.init(this.job.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'); - if (actionButtonElement === null) {return;} - let action = actionButtonElement.dataset.action; - switch (action) { - case 'download': - window.location.href = this.job.inputs[jobInputId].download_url; - break; - default: - console.error(`Unknown action: "${action}"`); - break; - } - } - - preprocessRessource(jobInput) { - return {id: jobInput.id, filename: jobInput.filename}; - } -} -JobInputList.options = { - item: ` - - - file_download - - `, - valueNames: [{data: ['id']}, 'filename'] -}; - - -class JobResultList extends RessourceList { - constructor(listElement, options = {}) { - super(listElement, {...JobResultList.options, ...options}); - this.job = undefined; - this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), listElement.dataset.jobId); - listElement.addEventListener('click', event => this.onclick(event)); - } - - init(job) { - this.job = job; - super.init(this.job.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'); - if (actionButtonElement === null) {return;} - let action = actionButtonElement.dataset.action; - switch (action) { - case 'download': - window.location.href = this.job.results[jobResultId].download_url; - break; - default: - console.error(`Unknown action: "${action}"`); - break; - } - } - - patch(patch) { - let re; - for (let operation of patch) { - switch(operation.op) { - case 'add': - // Matches the only paths that should be handled here: /jobs/{this.job.id}/results/{jobResultId} - re = new RegExp('^/jobs/' + this.job.id + '/results/(\\d+)$'); - if (re.test(operation.path)) {this.add(operation.value);} - break; - default: - break; - } - } - } - - preprocessRessource(jobResult) { - let description; - if (jobResult.filename.endsWith('.pdf.zip')) { - description = 'PDF files with text layer'; - } else if (jobResult.filename.endsWith('.txt.zip')) { - description = 'Raw text files'; - } else if (jobResult.filename.endsWith('.vrt.zip')) { - description = 'VRT compliant files including the NLP data'; - } else if (jobResult.filename.endsWith('.xml.zip')) { - description = 'TEI compliant files'; - } else if (jobResult.filename.endsWith('.poco.zip')) { - description = 'HOCR and image files for post correction (PoCo)'; - } else { - description = 'All result files created during this job'; - } - return {id: jobResult.id, description: description, filename: jobResult.filename}; - } -} -JobResultList.options = { - item: ` - - - - file_download - - `, - valueNames: [{data: ['id']}, 'description', 'filename'] -}; - - -class QueryResultList extends RessourceList { - constructor(listElement, options = {}) { - super(listElement, {...QueryResultList.options, ...options}); - this.user.eventListeners.queryResult.addEventListener((eventType, payload) => this.eventHandler(eventType, payload)); - } -} -QueryResultList.options = { - item: ` -

-
- - delete - send - search - - `, - valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title'] -}; diff --git a/web/app/static/js/nopaque/RessourceDisplays.js b/web/app/static/js/nopaque/RessourceDisplays.js deleted file mode 100644 index 67bb7caf..00000000 --- a/web/app/static/js/nopaque/RessourceDisplays.js +++ /dev/null @@ -1,215 +0,0 @@ -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; - } - - eventHandler(eventType, payload) { - switch (eventType) { - case 'init': - this.init(payload); - break; - case 'patch': - this.patch(payload); - break; - default: - console.log(`Unknown event type: ${eventType}`); - break; - } - } - - init() {console.error('init method not implemented!');} - - patch() {console.error('patch method not implemented!');} - - setElement(element, value) { - switch (element.tagName) { - case 'INPUT': - element.value = value; - M.updateTextFields(); - break; - default: - element.innerText = value; - break; - } - } -} - - -class CorpusDisplay extends RessourceDisplay { - constructor(displayElement) { - super(displayElement); - this.corpus = undefined; - this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), displayElement.dataset.corpusId); - } - - init(corpus) { - this.corpus = corpus; - this.setCreationDate(this.corpus.creation_date); - this.setDescription(this.corpus.description); - this.setLastEditedDate(this.corpus.last_edited_date); - this.setStatus(this.corpus.status); - this.setTitle(this.corpus.title); - this.setTokenRatio(this.corpus.current_nr_of_tokens, this.corpus.max_nr_of_tokens); - } - - patch(patch) { - let re; - for (let operation of patch) { - switch(operation.op) { - case 'replace': - // Matches: /jobs/{this.job.id}/status - re = new RegExp('^/corpora/' + this.corpus.id + '/last_edited_date'); - if (re.test(operation.path)) {this.setLastEditedDate(operation.value); break;} - // Matches: /jobs/{this.job.id}/status - re = new RegExp('^/corpora/' + this.corpus.id + '/status$'); - if (re.test(operation.path)) {this.setStatus(operation.value); break;} - break; - default: - break; - } - } - } - - setTitle(title) { - for (let element of this.displayElement.querySelectorAll('.corpus-title')) {this.setElement(element, title);} - } - - setTokenRatio(currentNrOfTokens, maxNrOfTokens) { - let tokenRatio = `${currentNrOfTokens}/${maxNrOfTokens}`; - for (let element of this.displayElement.querySelectorAll('.corpus-token-ratio')) {this.setElement(element, tokenRatio);} - } - - setDescription(description) { - for (let element of this.displayElement.querySelectorAll('.corpus-description')) {this.setElement(element, description);} - } - - setStatus(status) { - for (let element of this.displayElement.querySelectorAll('.corpus-status')) {this.setElement(element, status);} - for (let element of this.displayElement.querySelectorAll('.status')) {element.dataset.status = status;} - for (let element of this.displayElement.querySelectorAll('.status-spinner')) { - if (['complete', 'failed', 'unprepared'].includes(status)) { - element.classList.add('hide'); - } else { - element.classList.remove('hide'); - } - } - for (let element of this.displayElement.querySelectorAll('.build-corpus-trigger')) { - if (['complete', 'failed'].includes(status)) { - element.classList.remove('hide'); - } else { - element.classList.add('hide'); - } - } - } - - setCreationDate(creationDateTimestamp) { - let creationDate = new Date(creationDateTimestamp * 1000).toLocaleString("en-US"); - for (let element of this.displayElement.querySelectorAll('.corpus-creation-date')) {this.setElement(element, creationDate);} - } - - setLastEditedDate(endDateTimestamp) { - let endDate = new Date(endDateTimestamp * 1000).toLocaleString("en-US"); - for (let element of this.displayElement.querySelectorAll('.corpus-end-date')) {this.setElement(element, endDate);} - } -} - - - -class JobDisplay extends RessourceDisplay { - constructor(displayElement) { - super(displayElement); - this.job = undefined; - this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), displayElement.dataset.jobId); - } - - init(job) { - this.job = job; - this.setCreationDate(this.job.creation_date); - this.setEndDate(this.job.creation_date); - this.setDescription(this.job.description); - this.setService(this.job.service); - this.setServiceArgs(this.job.service_args); - this.setServiceVersion(this.job.service_version); - this.setStatus(this.job.status); - this.setTitle(this.job.title); - } - - patch(patch) { - let re; - for (let operation of patch) { - switch(operation.op) { - case 'replace': - // Matches: /jobs/{this.job.id}/status - re = new RegExp('^/jobs/' + this.job.id + '/end_date'); - if (re.test(operation.path)) {this.setEndDate(operation.value); break;} - // Matches: /jobs/{this.job.id}/status - re = new RegExp('^/jobs/' + this.job.id + '/status$'); - if (re.test(operation.path)) {this.setStatus(operation.value); break;} - break; - default: - break; - } - } - } - - setTitle(title) { - for (let element of this.displayElement.querySelectorAll('.job-title')) {this.setElement(element, title);} - } - - setDescription(description) { - for (let element of this.displayElement.querySelectorAll('.job-description')) {this.setElement(element, description);} - } - - setStatus(status) { - for (let element of this.displayElement.querySelectorAll('.job-status')) { - this.setElement(element, status); - } - for (let element of this.displayElement.querySelectorAll('.status')) {element.dataset.status = status;} - for (let element of this.displayElement.querySelectorAll('.status-spinner')) { - if (['complete', 'failed'].includes(status)) { - element.classList.add('hide'); - } else { - element.classList.remove('hide'); - } - } - for (let element of this.displayElement.querySelectorAll('.restart-job-trigger')) { - if (['complete', 'failed'].includes(status)) { - element.classList.remove('hide'); - } else { - element.classList.add('hide'); - } - } - } - - setCreationDate(creationDateTimestamp) { - let creationDate = new Date(creationDateTimestamp * 1000).toLocaleString("en-US"); - for (let element of this.displayElement.querySelectorAll('.job-creation-date')) {this.setElement(element, creationDate);} - } - - setEndDate(endDateTimestamp) { - let endDate = new Date(endDateTimestamp * 1000).toLocaleString("en-US"); - for (let element of this.displayElement.querySelectorAll('.job-end-date')) {this.setElement(element, endDate);} - } - - setService(service) { - for (let element of this.displayElement.querySelectorAll('.job-service')) {this.setElement(element, service);} - } - - setServiceArgs(serviceArgs) { - for (let element of this.displayElement.querySelectorAll('.job-service-args')) {this.setElement(element, serviceArgs);} - } - - setServiceVersion(serviceVersion) { - for (let element of this.displayElement.querySelectorAll('.job-service-version')) {this.setElement(element, serviceVersion);} - } -} diff --git a/web/app/static/js/nopaque/displays/CorpusDisplay.js b/web/app/static/js/nopaque/displays/CorpusDisplay.js new file mode 100644 index 00000000..fb98e2bc --- /dev/null +++ b/web/app/static/js/nopaque/displays/CorpusDisplay.js @@ -0,0 +1,77 @@ +class CorpusDisplay extends RessourceDisplay { + constructor(displayElement) { + super(displayElement); + this.corpus = undefined; + this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), displayElement.dataset.corpusId); + } + + init(corpus) { + this.corpus = corpus; + this.setCreationDate(this.corpus.creation_date); + this.setDescription(this.corpus.description); + this.setLastEditedDate(this.corpus.last_edited_date); + this.setStatus(this.corpus.status); + this.setTitle(this.corpus.title); + this.setTokenRatio(this.corpus.current_nr_of_tokens, this.corpus.max_nr_of_tokens); + } + + patch(patch) { + let re; + for (let operation of patch) { + switch(operation.op) { + case 'replace': + // Matches: /jobs/{this.job.id}/status + re = new RegExp('^/corpora/' + this.corpus.id + '/last_edited_date'); + if (re.test(operation.path)) {this.setLastEditedDate(operation.value); break;} + // Matches: /jobs/{this.job.id}/status + re = new RegExp('^/corpora/' + this.corpus.id + '/status$'); + if (re.test(operation.path)) {this.setStatus(operation.value); break;} + break; + default: + break; + } + } + } + + setTitle(title) { + for (let element of this.displayElement.querySelectorAll('.corpus-title')) {this.setElement(element, title);} + } + + setTokenRatio(currentNrOfTokens, maxNrOfTokens) { + let tokenRatio = `${currentNrOfTokens}/${maxNrOfTokens}`; + for (let element of this.displayElement.querySelectorAll('.corpus-token-ratio')) {this.setElement(element, tokenRatio);} + } + + setDescription(description) { + for (let element of this.displayElement.querySelectorAll('.corpus-description')) {this.setElement(element, description);} + } + + setStatus(status) { + for (let element of this.displayElement.querySelectorAll('.corpus-status')) {this.setElement(element, status);} + for (let element of this.displayElement.querySelectorAll('.status')) {element.dataset.status = status;} + for (let element of this.displayElement.querySelectorAll('.status-spinner')) { + if (['complete', 'failed', 'unprepared'].includes(status)) { + element.classList.add('hide'); + } else { + element.classList.remove('hide'); + } + } + for (let element of this.displayElement.querySelectorAll('.build-corpus-trigger')) { + if (['complete', 'failed'].includes(status)) { + element.classList.remove('hide'); + } else { + element.classList.add('hide'); + } + } + } + + setCreationDate(creationDateTimestamp) { + let creationDate = new Date(creationDateTimestamp * 1000).toLocaleString("en-US"); + for (let element of this.displayElement.querySelectorAll('.corpus-creation-date')) {this.setElement(element, creationDate);} + } + + setLastEditedDate(endDateTimestamp) { + let endDate = new Date(endDateTimestamp * 1000).toLocaleString("en-US"); + for (let element of this.displayElement.querySelectorAll('.corpus-end-date')) {this.setElement(element, endDate);} + } +} diff --git a/web/app/static/js/nopaque/displays/JobDisplay.js b/web/app/static/js/nopaque/displays/JobDisplay.js new file mode 100644 index 00000000..7d69fb82 --- /dev/null +++ b/web/app/static/js/nopaque/displays/JobDisplay.js @@ -0,0 +1,88 @@ +class JobDisplay extends RessourceDisplay { + constructor(displayElement) { + super(displayElement); + this.job = undefined; + this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), displayElement.dataset.jobId); + } + + init(job) { + this.job = job; + this.setCreationDate(this.job.creation_date); + this.setEndDate(this.job.creation_date); + this.setDescription(this.job.description); + this.setService(this.job.service); + this.setServiceArgs(this.job.service_args); + this.setServiceVersion(this.job.service_version); + this.setStatus(this.job.status); + this.setTitle(this.job.title); + } + + patch(patch) { + let re; + for (let operation of patch) { + switch(operation.op) { + case 'replace': + // Matches: /jobs/{this.job.id}/status + re = new RegExp('^/jobs/' + this.job.id + '/end_date'); + if (re.test(operation.path)) {this.setEndDate(operation.value); break;} + // Matches: /jobs/{this.job.id}/status + re = new RegExp('^/jobs/' + this.job.id + '/status$'); + if (re.test(operation.path)) {this.setStatus(operation.value); break;} + break; + default: + break; + } + } + } + + setTitle(title) { + for (let element of this.displayElement.querySelectorAll('.job-title')) {this.setElement(element, title);} + } + + setDescription(description) { + for (let element of this.displayElement.querySelectorAll('.job-description')) {this.setElement(element, description);} + } + + setStatus(status) { + for (let element of this.displayElement.querySelectorAll('.job-status')) { + this.setElement(element, status); + } + for (let element of this.displayElement.querySelectorAll('.status')) {element.dataset.status = status;} + for (let element of this.displayElement.querySelectorAll('.status-spinner')) { + if (['complete', 'failed'].includes(status)) { + element.classList.add('hide'); + } else { + element.classList.remove('hide'); + } + } + for (let element of this.displayElement.querySelectorAll('.restart-job-trigger')) { + if (['complete', 'failed'].includes(status)) { + element.classList.remove('hide'); + } else { + element.classList.add('hide'); + } + } + } + + setCreationDate(creationDateTimestamp) { + let creationDate = new Date(creationDateTimestamp * 1000).toLocaleString("en-US"); + for (let element of this.displayElement.querySelectorAll('.job-creation-date')) {this.setElement(element, creationDate);} + } + + setEndDate(endDateTimestamp) { + let endDate = new Date(endDateTimestamp * 1000).toLocaleString("en-US"); + for (let element of this.displayElement.querySelectorAll('.job-end-date')) {this.setElement(element, endDate);} + } + + setService(service) { + for (let element of this.displayElement.querySelectorAll('.job-service')) {this.setElement(element, service);} + } + + setServiceArgs(serviceArgs) { + for (let element of this.displayElement.querySelectorAll('.job-service-args')) {this.setElement(element, serviceArgs);} + } + + setServiceVersion(serviceVersion) { + for (let element of this.displayElement.querySelectorAll('.job-service-version')) {this.setElement(element, serviceVersion);} + } +} diff --git a/web/app/static/js/nopaque/displays/RessourceDisplay.js b/web/app/static/js/nopaque/displays/RessourceDisplay.js new file mode 100644 index 00000000..0922578e --- /dev/null +++ b/web/app/static/js/nopaque/displays/RessourceDisplay.js @@ -0,0 +1,45 @@ +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; + } + + eventHandler(eventType, payload) { + switch (eventType) { + case 'init': + this.init(payload); + break; + case 'patch': + this.patch(payload); + break; + default: + console.log(`Unknown event type: ${eventType}`); + break; + } + } + + init() {console.error('init method not implemented!');} + + patch() {console.error('patch method not implemented!');} + + setElement(element, value) { + switch (element.tagName) { + case 'INPUT': + element.value = value; + M.updateTextFields(); + break; + default: + element.innerText = value; + break; + } + } +} diff --git a/web/app/static/js/nopaque/lists/CorpusFileList.js b/web/app/static/js/nopaque/lists/CorpusFileList.js new file mode 100644 index 00000000..afd2a42c --- /dev/null +++ b/web/app/static/js/nopaque/lists/CorpusFileList.js @@ -0,0 +1,91 @@ +class CorpusFileList extends RessourceList { + constructor(listElement, options = {}) { + super(listElement, {...CorpusFileList.options, ...options}); + this.corpus = undefined; + this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), listElement.dataset.corpusId); + listElement.addEventListener('click', event => this.onclick(event)); + } + + init(corpus) { + this.corpus = corpus; + super.init(this.corpus.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 action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action; + switch (action) { + case 'delete': + let deleteModalHTML = ``; + let deleteModalParentElement = document.querySelector('main'); + deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML); + let deleteModalElement = deleteModalParentElement.lastChild; + let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}}); + deleteModal.open(); + break; + case 'download': + window.location.href = this.corpus.files[corpusFileId].download_url; + break; + case 'view': + window.location.href = this.corpus.files[corpusFileId].url; + break; + default: + console.error(`Unknown action: "${action}"`); + break; + } + } + + patch(patch) { + let id, match, re; + for (let operation of patch) { + switch(operation.op) { + case 'add': + // Matches the only paths that should be handled here: /corpora/{this.corpus.id}/files/{corpusFileId} + re = new RegExp('^/corpora/' + this.corpus.id + '/files/(\\d+)$'); + if (re.test(operation.path)) {this.add(operation.value);} + break; + case 'remove': + // See case add ;) + re = new RegExp('^/corpora/' + this.corpus.id + '/files/(\\d+)$'); + if (re.test(operation.path)) { + [match, id] = operation.path.match(re); + this.remove(id); + } + break; + default: + break; + } + } + } + + preprocessRessource(corpusFile) { + return {id: corpusFile.id, author: corpusFile.author, filename: corpusFile.filename, 'publishing-year': corpusFile.publishing_year, title: corpusFile.title}; + } +} +CorpusFileList.options = { + item: ` + + + + + + delete + file_download + send + + `, + valueNames: [{data: ['id']}, 'author', 'filename', 'publishing-year', 'title'] +}; diff --git a/web/app/static/js/nopaque/lists/CorpusList.js b/web/app/static/js/nopaque/lists/CorpusList.js new file mode 100644 index 00000000..d81a41d9 --- /dev/null +++ b/web/app/static/js/nopaque/lists/CorpusList.js @@ -0,0 +1,100 @@ +class CorpusList extends RessourceList { + constructor(listElement, options = {}) { + super(listElement, {...CorpusList.options, ...options}); + this.corpora = undefined; + this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload)); + listElement.addEventListener('click', event => this.onclick(event)); + } + + init(corpora) { + this.corpora = corpora; + super.init(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; + switch (action) { + case 'analyse': + window.location.href = this.corpora[corpusId].analysis_url; + case 'delete': + let deleteModalHTML = ``; + let deleteModalParentElement = document.querySelector('main'); + deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML); + let deleteModalElement = deleteModalParentElement.lastChild; + let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}}); + deleteModal.open(); + break; + case 'view': + // TODO: handle unprepared corpora + window.location.href = this.corpora[corpusId].url; + break; + default: + console.error(`Unknown action: ${action}`); + break; + } + } + + patch(patch) { + let id, match, re, valueName; + for (let operation of patch) { + switch(operation.op) { + case 'add': + // Matches the only paths that should be handled here: /corpora/{corpusId} + re = /^\/corpora\/(\d+)$/; + if (re.test(operation.path)) {this.add(operation.value);} + break; + case 'remove': + // See case 'add' ;) + re = /^\/corpora\/(\d+)$/; + if (re.test(operation.path)) { + [match, id] = operation.path.match(re); + this.remove(id); + } + break; + case 'replace': + // Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title} + re = /^\/corpora\/(\d+)\/(status|description|title)$/; + if (re.test(operation.path)) { + [match, id, valueName] = operation.path.match(re); + this.replace(id, valueName, operation.value); + } + break; + default: + break; + } + } + } + + preprocessRessource(corpus) { + return {id: corpus.id, + status: corpus.status, + description: corpus.description, + title: corpus.title}; + } +} +CorpusList.options = { + item: ` + book +
+ + + delete + search + send + + `, + valueNames: [{data: ['id']}, {name: 'status', attr: 'data-status'}, 'description', 'title'] +}; diff --git a/web/app/static/js/nopaque/lists/JobInputList.js b/web/app/static/js/nopaque/lists/JobInputList.js new file mode 100644 index 00000000..c24f6fbe --- /dev/null +++ b/web/app/static/js/nopaque/lists/JobInputList.js @@ -0,0 +1,43 @@ +class JobInputList extends RessourceList { + constructor(listElement, options = {}) { + super(listElement, {...JobInputList.options, ...options}); + this.job = undefined; + this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), listElement.dataset.jobId); + listElement.addEventListener('click', event => this.onclick(event)); + } + + init(job) { + this.job = job; + super.init(this.job.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'); + if (actionButtonElement === null) {return;} + let action = actionButtonElement.dataset.action; + switch (action) { + case 'download': + window.location.href = this.job.inputs[jobInputId].download_url; + break; + default: + console.error(`Unknown action: "${action}"`); + break; + } + } + + preprocessRessource(jobInput) { + return {id: jobInput.id, filename: jobInput.filename}; + } +} +JobInputList.options = { + item: ` + + + file_download + + `, + valueNames: [{data: ['id']}, 'filename'] +}; diff --git a/web/app/static/js/nopaque/lists/JobList.js b/web/app/static/js/nopaque/lists/JobList.js new file mode 100644 index 00000000..52d30825 --- /dev/null +++ b/web/app/static/js/nopaque/lists/JobList.js @@ -0,0 +1,91 @@ +class JobList extends RessourceList { + constructor(listElement, options = {}) { + super(listElement, {...JobList.options, ...options}); + this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload)); + listElement.addEventListener('click', event => this.onclick(event)); + } + + 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 action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action; + switch (action) { + case 'delete': + let deleteModalHTML = ``; + let deleteModalParentElement = document.querySelector('main'); + deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML); + let deleteModalElement = deleteModalParentElement.lastChild; + let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}}); + deleteModal.open(); + break; + case 'view': + window.location.href = this.user.data.jobs[jobId].url; + break; + default: + console.error(`Unknown action: "${action}"`); + break; + } + } + + patch(patch) { + let id, match, re, valueName; + for (let operation of patch) { + switch(operation.op) { + case 'add': + // Matches the only paths that should be handled here: /jobs/{jobId} + re = /^\/jobs\/(\d+)$/; + if (re.test(operation.path)) {this.add(operation.value);} + break; + case 'remove': + // See case add ;) + re = /^\/jobs\/(\d+)$/; + if (re.test(operation.path)) { + [match, id] = operation.path.match(re); + this.remove(id); + } + break; + case 'replace': + // Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title} + re = /^\/jobs\/(\d+)\/(status|description|title)$/; + if (re.test(operation.path)) { + [match, id, valueName] = operation.path.match(re); + this.replace(id, valueName, operation.value); + } + break; + default: + break; + } + } + } + + preprocessRessource(job) { + return {id: job.id, + service: job.service, + status: job.status, + description: job.description, + title: job.title}; + } +} +JobList.options = { + item: ` + +
+ + + delete + send + + `, + valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, {name: 'status', attr: 'data-status'}, 'description', 'title'] +}; diff --git a/web/app/static/js/nopaque/lists/JobResultList.js b/web/app/static/js/nopaque/lists/JobResultList.js new file mode 100644 index 00000000..cd42b702 --- /dev/null +++ b/web/app/static/js/nopaque/lists/JobResultList.js @@ -0,0 +1,73 @@ +class JobResultList extends RessourceList { + constructor(listElement, options = {}) { + super(listElement, {...JobResultList.options, ...options}); + this.job = undefined; + this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), listElement.dataset.jobId); + listElement.addEventListener('click', event => this.onclick(event)); + } + + init(job) { + this.job = job; + super.init(this.job.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'); + if (actionButtonElement === null) {return;} + let action = actionButtonElement.dataset.action; + switch (action) { + case 'download': + window.location.href = this.job.results[jobResultId].download_url; + break; + default: + console.error(`Unknown action: "${action}"`); + break; + } + } + + patch(patch) { + let re; + for (let operation of patch) { + switch(operation.op) { + case 'add': + // Matches the only paths that should be handled here: /jobs/{this.job.id}/results/{jobResultId} + re = new RegExp('^/jobs/' + this.job.id + '/results/(\\d+)$'); + if (re.test(operation.path)) {this.add(operation.value);} + break; + default: + break; + } + } + } + + preprocessRessource(jobResult) { + let description; + if (jobResult.filename.endsWith('.pdf.zip')) { + description = 'PDF files with text layer'; + } else if (jobResult.filename.endsWith('.txt.zip')) { + description = 'Raw text files'; + } else if (jobResult.filename.endsWith('.vrt.zip')) { + description = 'VRT compliant files including the NLP data'; + } else if (jobResult.filename.endsWith('.xml.zip')) { + description = 'TEI compliant files'; + } else if (jobResult.filename.endsWith('.poco.zip')) { + description = 'HOCR and image files for post correction (PoCo)'; + } else { + description = 'All result files created during this job'; + } + return {id: jobResult.id, description: description, filename: jobResult.filename}; + } +} +JobResultList.options = { + item: ` + + + + file_download + + `, + valueNames: [{data: ['id']}, 'description', 'filename'] +}; diff --git a/web/app/static/js/nopaque/lists/QueryResultList.js b/web/app/static/js/nopaque/lists/QueryResultList.js new file mode 100644 index 00000000..c8cf0771 --- /dev/null +++ b/web/app/static/js/nopaque/lists/QueryResultList.js @@ -0,0 +1,18 @@ +class QueryResultList extends RessourceList { + constructor(listElement, options = {}) { + super(listElement, {...QueryResultList.options, ...options}); + this.user.eventListeners.queryResult.addEventListener((eventType, payload) => this.eventHandler(eventType, payload)); + } +} +QueryResultList.options = { + item: ` +

+
+ + delete + send + search + + `, + valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title'] +}; diff --git a/web/app/static/js/nopaque/lists/RessourceList.js b/web/app/static/js/nopaque/lists/RessourceList.js new file mode 100644 index 00000000..c38ecebc --- /dev/null +++ b/web/app/static/js/nopaque/lists/RessourceList.js @@ -0,0 +1,72 @@ +class RessourceList { + /* A wrapper class for the list.js list. + * This class is not meant to be used directly, instead it should be used as + * 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 = ` + + file_downloadNothing here... +

No ressource available (yet).

+ + `; + } + + 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(ressources) { + this.list.clear(); + this.add(Object.values(ressources)); + this.list.sort('id', {order: 'desc'}); + } + + 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!'); + } + + 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 + this.list.add(ressources, () => {return;}); + } + + remove(id) { + this.list.remove('id', id); + } + + replace(id, valueName, newValue) { + this.list.get('id', id)[0].values({[valueName]: newValue}); + } +} +RessourceList.options = {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]}; diff --git a/web/app/static/js/nopaque/index.js b/web/app/static/js/nopaque/main.js similarity index 100% rename from web/app/static/js/nopaque/index.js rename to web/app/static/js/nopaque/main.js diff --git a/web/app/templates/main/dashboard.html.j2 b/web/app/templates/main/dashboard.html.j2 index 8326654a..686c8438 100644 --- a/web/app/templates/main/dashboard.html.j2 +++ b/web/app/templates/main/dashboard.html.j2 @@ -70,7 +70,7 @@ Corpus and
Query - {# Actions #} + @@ -122,6 +122,7 @@

addNew job

+