From 3d9fe230da69d732357626b10c7853fd2bb122a9 Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Mon, 3 May 2021 16:15:27 +0200 Subject: [PATCH] Automatic reload of node states (#3827); bugfix of missing size after upload and other minor issues --- .../inaf/ia2/vospace/ui/service/NodeInfo.java | 4 +- vospace-ui-frontend/src/App.vue | 10 ++- vospace-ui-frontend/src/api/server/index.js | 8 +- vospace-ui-frontend/src/components/Jobs.vue | 1 + vospace-ui-frontend/src/nodesReloader.js | 49 ++++++++++++ vospace-ui-frontend/src/store.js | 77 ++++++++++++++----- 6 files changed, 124 insertions(+), 25 deletions(-) create mode 100644 vospace-ui-frontend/src/nodesReloader.js diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java index 6025e00..a439061 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java @@ -48,8 +48,8 @@ public class NodeInfo { this.sticky = isSticky(node); this.busy = isBusy(node); this.listOfFiles = isListOfFiles(node); - this.writable = NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()); - this.deletable = writable && !sticky; + this.writable = NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()) && !busy; + this.deletable = writable && !sticky && !asyncTrans; } private String getPath(Node node) { diff --git a/vospace-ui-frontend/src/App.vue b/vospace-ui-frontend/src/App.vue index 4844d3e..c2db686 100644 --- a/vospace-ui-frontend/src/App.vue +++ b/vospace-ui-frontend/src/App.vue @@ -21,6 +21,7 @@ import { mapState } from 'vuex' import TopMenu from './components/TopMenu.vue' import client from 'api-client' +import nodesReloader from './nodesReloader.js' export default { name: 'App', @@ -34,6 +35,13 @@ export default { this.$store.dispatch('loadJobs'); this.$store.dispatch('loadUserInfo'); setInterval(client.keepalive, 60000); + setInterval(nodesReloader.checkNodes, 1000); + let self = this; + setInterval(function() { + if (self.$router.currentRoute.path !== '/jobs') { + self.$store.dispatch('checkJobs'); + } + }, 1000); } } </script> @@ -95,7 +103,7 @@ export default { color: #3293f2; } -.node-busy + .icon { +.node-busy+.icon { margin-right: 3px; } </style> diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js index 48974f1..e22598a 100644 --- a/vospace-ui-frontend/src/api/server/index.js +++ b/vospace-ui-frontend/src/api/server/index.js @@ -46,7 +46,7 @@ function escapePath(path) { } export default { - getNode(path) { + getNode(path, loading) { let url = BASE_API_URL + 'nodes/' + escapePath(path); return apiRequest({ method: 'GET', @@ -55,7 +55,7 @@ export default { headers: { 'Cache-Control': 'no-cache' } - }, true, true); + }, (typeof loading !== 'undefined') ? loading : true, true); }, loadJobs() { let url = BASE_API_URL + 'jobs'; @@ -124,11 +124,11 @@ export default { uploadFile(url, file) { let formData = new FormData(); formData.append('file', file); - axios.put(url, formData, { + return axios.put(url, formData, { headers: { 'Content-Type': 'multipart/form-data' } - }) + }); }, deleteNodes(paths) { let url = BASE_API_URL + 'delete'; diff --git a/vospace-ui-frontend/src/components/Jobs.vue b/vospace-ui-frontend/src/components/Jobs.vue index c117dc4..bfb1ffb 100644 --- a/vospace-ui-frontend/src/components/Jobs.vue +++ b/vospace-ui-frontend/src/components/Jobs.vue @@ -1,5 +1,6 @@ <template> <div> + <b-button variant="primary" class="float-right" @click="loadJobs">Reload</b-button> <h3>Async recall jobs</h3> <table class="table b-table table-striped table-hover"> <thead> diff --git a/vospace-ui-frontend/src/nodesReloader.js b/vospace-ui-frontend/src/nodesReloader.js new file mode 100644 index 0000000..b730a68 --- /dev/null +++ b/vospace-ui-frontend/src/nodesReloader.js @@ -0,0 +1,49 @@ +import store from './store.js' +import client from 'api-client' + +let lastResultTime = null; +let times = 0; + +function checkNodes() { + let busyNodes = document.getElementsByClassName('node-busy').length > 0; + let jobsInProgress = store.state.jobs.filter(j => j.phase === 'EXECUTING').length > 0; + if (!busyNodes && !jobsInProgress) { + // reset state + lastResultTime = null; + times = 0; + return; + } + if (lastResultTime !== null) { + // first 10 times check every second, then check every ten seconds + let offset = times < 10 ? 1000 : 10000; + let now = new Date().getTime(); + if (now - lastResultTime < offset) { + return; + } + } + + if (!store.state.nodesLoading) { + let path = store.state.path; + store.commit('setNodesLoading', true); + client.getNode(path, false) + .then(res => { + // check that path didn't change in meantime by user action + if (path === store.state.path) { + let resHasBusyNodes = res.htmlTable.includes('node-busy'); + if ((!busyNodes && resHasBusyNodes) || (busyNodes && !resHasBusyNodes)) { + store.dispatch('setNodes', res); + } else { + times++; + lastResultTime = new Date().getTime(); + } + } + }) + .finally(() => { + store.commit('setNodesLoading', false); + }); + } +} + +export default { + checkNodes +} diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js index 13f4791..bf51a33 100644 --- a/vospace-ui-frontend/src/store.js +++ b/vospace-ui-frontend/src/store.js @@ -20,10 +20,12 @@ export default new Vuex.Store({ state: { path: '', loading: true, + nodesLoading: false, asyncButtonEnabled: false, deleteButtonEnabled: false, jobs: [], jobsLoading: true, + lastJobsCheckTime: null, user: 'anonymous', nodesToDelete: [], writable: false, @@ -43,6 +45,9 @@ export default new Vuex.Store({ } state.path = value; }, + setNodesLoading(state, value) { + state.nodesLoading = value; + }, setAsyncButtonEnabled(state, value) { state.asyncButtonEnabled = value; }, @@ -76,24 +81,29 @@ export default new Vuex.Store({ actions: { setPath({ state, commit, dispatch }, path) { commit('setPath', path); + commit('setNodesLoading', true); client.getNode(state.path) .then(res => { - commit('setWritable', res.writable); - document.getElementById('nodes').outerHTML = res.htmlTable; - let checkboxes = document.querySelectorAll('#nodes input[type="checkbox"]'); - for (let i = 0; i < checkboxes.length; i++) { - checkboxes[i].addEventListener('change', function() { - dispatch('computeButtonsVisibility'); - }); - } + dispatch('setNodes', res); + }) + .finally(() => commit('setNodesLoading', false)); + }, + setNodes({ commit, dispatch }, res) { + commit('setWritable', res.writable); + document.getElementById('nodes').outerHTML = res.htmlTable; + let checkboxes = document.querySelectorAll('#nodes input[type="checkbox"]'); + for (let i = 0; i < checkboxes.length; i++) { + checkboxes[i].addEventListener('change', function() { dispatch('computeButtonsVisibility'); }); + } + dispatch('computeButtonsVisibility'); }, computeButtonsVisibility({ commit }) { commit('setAsyncButtonEnabled', document.querySelectorAll('#nodes input.async:checked').length > 0); commit('setDeleteButtonEnabled', document.querySelectorAll('#nodes input.deletable:checked').length > 0); }, - startAsyncRecallJob({ commit }) { + startAsyncRecallJob({ state, commit, dispatch }) { let asyncCheckboxes = document.querySelectorAll('#nodes input.async:checked'); let paths = []; for (let i = 0; i < asyncCheckboxes.length; i++) { @@ -101,15 +111,45 @@ export default new Vuex.Store({ } client.startAsyncRecallJob(paths) .then(job => { - main.showInfo('Job started'); + main.showInfo('Job queued'); commit('addJob', job); + // Reload current node + dispatch('setPath', state.path); }); }, - loadJobs({ commit }) { + checkJobs({ state, dispatch }) { + if (state.jobs.filter(j => j.phase === 'QUEUED' || j.phase === 'PENDING' || j.phase === 'EXECUTING').length > 0 && + !state.jobsLoading && (state.lastJobsCheckTime !== null && new Date().getTime() - state.lastJobsCheckTime > 5000)) { + dispatch('loadJobs'); + } + }, + loadJobs({ state, commit }) { commit('setJobsLoading', true); client.loadJobs() - .then(jobs => commit('setJobs', jobs)) - .finally(() => commit('setJobsLoading', false)); + .then(jobs => { + for (let previousJob of state.jobs) { + for (let newJob of jobs) { + if (newJob.id === previousJob.id && newJob.phase !== previousJob.phase) { + switch (newJob.phase) { + case 'EXECUTING': + main.showInfo('Job started'); + break; + case 'COMPLETED': + main.showInfo('Job completed'); + break; + case 'ERROR': + main.showError('Job failed'); + break; + } + } + } + } + commit('setJobs', jobs); + }) + .finally(() => { + commit('setJobsLoading', false); + state.lastJobsCheckTime = new Date().getTime(); + }); }, loadUserInfo({ commit }) { client.getUserInfo() @@ -129,13 +169,14 @@ export default new Vuex.Store({ } client.prepareForUpload(state.path, names) .then(uploadUrls => { + let uploads = []; for (let i = 0; i < files.length; i++) { - client.uploadFile(uploadUrls[i], files[i]); + uploads.push(client.uploadFile(uploadUrls[i], files[i])); } - }) - .then(() => { - // Reload current node - dispatch('setPath', state.path); + Promise.all(uploads).then(() => { + // Reload current node when all uploads completed + dispatch('setPath', state.path); + }); }); }, deleteNodes({ state, dispatch }) { -- GitLab