diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java index 2edbb77063bbe38af44bdf6a4b6d5006af615d79..04beff899a6c774dffaceb72a6af006a9cfdc614 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java @@ -32,7 +32,7 @@ public class NodesService { if (node instanceof ContainerNode) { ContainerNode folder = (ContainerNode) node; sw.write("<tbody id=\"nodes\">"); - for (Node child : folder.getNodes().getNode()) { + for (Node child : folder.getNodes()) { sw.write(getNodeHtml(child)); } sw.write("</tbody>"); diff --git a/vospace-ui-frontend/.env.development b/vospace-ui-frontend/.env.development new file mode 100644 index 0000000000000000000000000000000000000000..b4b1787f8fc876c82213c6344e414c0329d867d2 --- /dev/null +++ b/vospace-ui-frontend/.env.development @@ -0,0 +1,2 @@ +VUE_APP_API_CLIENT = 'mock' +VUE_APP_API_BASE_URL = 'http://localhost:8085/' diff --git a/vospace-ui-frontend/package-lock.json b/vospace-ui-frontend/package-lock.json index 80ecd14f2baab7fa25a6b2b9535978bf8550446d..94330cbb4d77b06fe24c042adb03efca63a36f5e 100644 --- a/vospace-ui-frontend/package-lock.json +++ b/vospace-ui-frontend/package-lock.json @@ -9039,6 +9039,40 @@ "unpipe": "1.0.0" } }, + "raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", diff --git a/vospace-ui-frontend/package.json b/vospace-ui-frontend/package.json index c452bb8b40c5640b7f051e973b68f50f1bb4a4cf..d171beb47fbc164c5429949e64f3b3714a206123 100644 --- a/vospace-ui-frontend/package.json +++ b/vospace-ui-frontend/package.json @@ -22,6 +22,7 @@ "babel-eslint": "^10.1.0", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", + "raw-loader": "^4.0.2", "vue-template-compiler": "^2.6.11" }, "eslintConfig": { diff --git a/vospace-ui-frontend/src/App.vue b/vospace-ui-frontend/src/App.vue index b9a76bea0c22ec31f09d35f0f43135b2eea6c5b3..1aced9b4e5ba226a94d09686a6d0efa0fa6069cc 100644 --- a/vospace-ui-frontend/src/App.vue +++ b/vospace-ui-frontend/src/App.vue @@ -28,19 +28,6 @@ export default { }), components: { TopMenu - }, - mounted: function() { - var self = this; - document.addEventListener('apiError', function(event) { - self.$bvToast.toast(event.message.body, { - title: event.message.title, - variant: 'danger', - solid: true - }); - }); - document.addEventListener('loading', function(event) { - self.$store.commit('setLoading', event.value); - }); } } </script> @@ -50,7 +37,6 @@ export default { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - text-align: center; color: #2c3e50; } diff --git a/vospace-ui-frontend/src/api/mock/data/job.json b/vospace-ui-frontend/src/api/mock/data/job.json new file mode 100644 index 0000000000000000000000000000000000000000..633abc9aaca6432168df723153c54d934763c4f3 --- /dev/null +++ b/vospace-ui-frontend/src/api/mock/data/job.json @@ -0,0 +1,4 @@ +{ + "id": "1234", + "status": "EXECUTING" +} diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html b/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html new file mode 100644 index 0000000000000000000000000000000000000000..ee2b379d89422fb314d982e9994b2cbf500cb2fb --- /dev/null +++ b/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html @@ -0,0 +1,32 @@ +<tbody id="nodes"> + <tr> + <td><input type="checkbox" class="tape" data-node="/folder1/folder2" /></td> + <td> + <span class="icon folder-x-icon"></span> + <a href="#/nodes/folder1/folder2">folder2</a> + </td> + <td>0 B</td> + <td>group1</td> + <td>group2</td> + </tr> + <tr> + <td><input type="checkbox" data-node="/folder1/file2" /></td> + <td> + <span class="icon file-icon"></span> + <a href="download/file2">file2</a> + </td> + <td>30 KB</td> + <td>group1</td> + <td>group2</td> + </tr> + <tr> + <td><input type="checkbox" class="tape" data-node="/folder1/file3" /></td> + <td> + <span class="icon file-x-icon"></span> + file3 + </td> + <td>12 MB</td> + <td>group3</td> + <td>group4</td> + </tr> +</tbody> diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/folder2.html b/vospace-ui-frontend/src/api/mock/data/nodes/folder2.html new file mode 100644 index 0000000000000000000000000000000000000000..b861b45707302c0210ff5dc45b595d7922f8ee36 --- /dev/null +++ b/vospace-ui-frontend/src/api/mock/data/nodes/folder2.html @@ -0,0 +1,22 @@ +<tbody id="nodes"> + <tr> + <td><input type="checkbox" class="tape" data-node="/folder1/folder2/file4" /></td> + <td> + <span class="icon file-x-icon"></span> + file4 + </td> + <td>10 KB</td> + <td>group1</td> + <td>group2</td> + </tr> + <tr> + <td><input type="checkbox" class="tape" data-node="/folder1/folder2/file5" /></td> + <td> + <span class="icon file-x-icon"></span> + file5 + </td> + <td>15 MB</td> + <td>group3</td> + <td>group4</td> + </tr> +</tbody> diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/root.html b/vospace-ui-frontend/src/api/mock/data/nodes/root.html new file mode 100644 index 0000000000000000000000000000000000000000..f26856fff011b92a62a52417370a788e598e3d24 --- /dev/null +++ b/vospace-ui-frontend/src/api/mock/data/nodes/root.html @@ -0,0 +1,22 @@ +<tbody id="nodes"> + <tr> + <td><input type="checkbox" data-node="/folder1" /></td> + <td> + <span class="icon folder-icon"></span> + <a href="#/nodes/folder1">folder1</a> + </td> + <td>0 B</td> + <td>group1</td> + <td>group2</td> + </tr> + <tr> + <td><input type="checkbox" data-node="/file1" /></td> + <td> + <span class="icon file-icon"></span> + <a href="download/file1">file1</a> + </td> + <td>12 MB</td> + <td>group1</td> + <td>group2</td> + </tr> +</tbody> diff --git a/vospace-ui-frontend/src/api/mock/index.js b/vospace-ui-frontend/src/api/mock/index.js new file mode 100644 index 0000000000000000000000000000000000000000..45c0226b944d9a2062a5c3c22511da896bbb4992 --- /dev/null +++ b/vospace-ui-frontend/src/api/mock/index.js @@ -0,0 +1,40 @@ +import root from 'raw-loader!./data/nodes/root.html'; +import folder1 from 'raw-loader!./data/nodes/folder1.html'; +import folder2 from 'raw-loader!./data/nodes/folder2.html'; +import job from './data/job'; +import store from '../../store'; + +const fetch = (mockData, showLoading = true, time = 500) => { + return new Promise((resolve) => { + if (showLoading) { + store.commit('setLoading', true); + } + setTimeout(() => { + resolve(mockData) + if (showLoading) { + store.commit('setLoading', false); + } + }, time); + }); +}; + +export default { + getNode(path) { + let response; + switch (path) { + case '': + response = root; + break; + case 'folder1': + response = folder1; + break; + case 'folder1/folder2': + response = folder2; + break; + } + return fetch(response); + }, + startRecallFromTapeJob() { + return fetch(job); + } +} diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js index a86c032ddd5f00dc7bc2d10b2afb7d44647df831..6162815c32fb0916150088678cf2ca694ff4906b 100644 --- a/vospace-ui-frontend/src/api/server/index.js +++ b/vospace-ui-frontend/src/api/server/index.js @@ -1,10 +1,12 @@ const BASE_API_URL = process.env.VUE_APP_API_BASE_URL; import axios from 'axios'; +import store from '../../store'; +import main from '../../main'; function apiRequest(options, showLoading = true, handleValidationErrors = false) { if (showLoading) { - loading(true); + store.commit('setLoading', true); } return new Promise((resolve, reject) => { axios(options) @@ -14,41 +16,27 @@ function apiRequest(options, showLoading = true, handleValidationErrors = false) } else { resolve(response.data); } - loading(false); + store.commit('setLoading', false); }) .catch(error => { - loading(false); + store.commit('setLoading', false); if (handleValidationErrors && error.response && error.response.status === 400) { reject(error.response.data); } else { - dispatchApiErrorEvent(error); + main.showError(getErrorMessage(error)); } }); }); } -function dispatchApiErrorEvent(error) { - let event = new CustomEvent('apiError'); - let errorMessage; +function getErrorMessage(error) { if (error.response && error.response.data && error.response.data.message) { - errorMessage = error.response.data.message; + return error.response.data.message; } else if (error.message) { - errorMessage = error.message; + return error.message; } else { - errorMessage = 'Unknown error'; + return 'Unknown error'; } - event.message = { - title: error.error || 'Error', - body: errorMessage - }; - document.dispatchEvent(event); -} - -/* For loading animation */ -function loading(value) { - let event = new CustomEvent('loading'); - event.value = value; - document.dispatchEvent(event); } export default { diff --git a/vospace-ui-frontend/src/assets/css/fonts.css b/vospace-ui-frontend/src/assets/css/fonts.css index c59766b1687a6a4eab3c1b54de0f9968aa116578..06fbcfffbe7b17491b61ab90e1248f2615f81756 100644 --- a/vospace-ui-frontend/src/assets/css/fonts.css +++ b/vospace-ui-frontend/src/assets/css/fonts.css @@ -9,6 +9,14 @@ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='folder' xmlns='http://www.w3.org/2000/svg' fill='currentColor' %3E%3Cg%3E%3Cpath fill-rule='evenodd' d='M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v.64c.57.265.94.876.856 1.546l-.64 5.124A2.5 2.5 0 0 1 12.733 15H3.266a2.5 2.5 0 0 1-2.481-2.19l-.64-5.124A1.5 1.5 0 0 1 1 6.14V3.5zM2 6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5a.5.5 0 0 0-.5.5V6zm-.367 1a.5.5 0 0 0-.496.562l.64 5.124A1.5 1.5 0 0 0 3.266 14h9.468a1.5 1.5 0 0 0 1.489-1.314l.64-5.124A.5.5 0 0 0 14.367 7H1.633z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); } +.folder-x-icon { + background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='folder x' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-folder-x mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath fill-rule='evenodd' d='M9.828 4H2.19a1 1 0 0 0-.996 1.09l.637 7a1 1 0 0 0 .995.91H9v1H2.826a2 2 0 0 1-1.991-1.819l-.637-7a1.99 1.99 0 0 1 .342-1.31L.5 3a2 2 0 0 1 2-2h3.672a2 2 0 0 1 1.414.586l.828.828A2 2 0 0 0 9.828 3h3.982a2 2 0 0 1 1.992 2.181L15.546 8H14.54l.265-2.91A1 1 0 0 0 13.81 4H9.828zm-2.95-1.707L7.587 3H2.19c-.24 0-.47.042-.684.12L1.5 2.98a1 1 0 0 1 1-.98h3.672a1 1 0 0 1 .707.293z'%3E%3C/path%3E%3Cpath fill-rule='evenodd' d='M11.146 10.146a.5.5 0 0 1 .708 0L13 11.293l1.146-1.147a.5.5 0 0 1 .708.708L13.707 12l1.147 1.146a.5.5 0 0 1-.708.708L13 12.707l-1.146 1.147a.5.5 0 0 1-.708-.708L12.293 12l-1.147-1.146a.5.5 0 0 1 0-.708z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); +} + .file-icon { background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='file earmark' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-file-earmark b-icon bi'%3E%3Cg%3E%3Cpath d='M4 0h5.5v1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h1V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z'%3E%3C/path%3E%3Cpath d='M9.5 3V0L14 4.5h-3A1.5 1.5 0 0 1 9.5 3z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); } + +.file-x-icon { + background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='file earmark x' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-file-earmark-x mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M4 0h5.5v1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h1V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z'%3E%3C/path%3E%3Cpath d='M9.5 3V0L14 4.5h-3A1.5 1.5 0 0 1 9.5 3z'%3E%3C/path%3E%3Cpath fill-rule='evenodd' d='M6.146 7.146a.5.5 0 0 1 .708 0L8 8.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 9l1.147 1.146a.5.5 0 0 1-.708.708L8 9.707l-1.146 1.147a.5.5 0 0 1-.708-.708L7.293 9 6.146 7.854a.5.5 0 0 1 0-.708z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); +} diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue index 5263ff410915dd1e46b859fd7d3859e9612c8e38..99455ce8e06b6278aa070c3346a215bcb068ef22 100644 --- a/vospace-ui-frontend/src/components/Main.vue +++ b/vospace-ui-frontend/src/components/Main.vue @@ -1,6 +1,11 @@ <template> <div class="container"> <b-breadcrumb :items="breadcrumbs"></b-breadcrumb> + <div class="mb-3"> + <b-button variant="success" class="mr-2" :disabled="true">New folder</b-button> + <b-button variant="success" class="mr-2" :disabled="true">Upload files</b-button> + <b-button variant="primary" class="mr-2" v-if="tapeButtonEnabled" @click="startRecallFromTapeJob">Recall from tape</b-button> + </div> <b-card> <table class="table b-table table-striped table-hover"> <thead> @@ -50,6 +55,9 @@ export default { } } return items; + }, + tapeButtonEnabled() { + return this.$store.state.tapeButtonEnabled; } }, created() { @@ -69,7 +77,13 @@ export default { this.selectInputs(false); }, selectInputs(value) { - document.querySelectorAll('#nodes input').forEach(input => input.checked = value); + document.querySelectorAll('#nodes input').forEach(input => { + input.checked = value; + this.$store.dispatch('computeButtonVisibility'); + }); + }, + startRecallFromTapeJob() { + this.$store.dispatch('startRecallFromTapeJob'); } } } diff --git a/vospace-ui-frontend/src/main.js b/vospace-ui-frontend/src/main.js index 9324cf322260b752aa4ad03f43c26bb27a4823f8..857bdf950bd2956263b8a947daf4c8c4727175d8 100644 --- a/vospace-ui-frontend/src/main.js +++ b/vospace-ui-frontend/src/main.js @@ -12,8 +12,25 @@ import VueRouter from 'vue-router' Vue.use(VueRouter) import router from './router.js' -new Vue({ +let vm = new Vue({ render: h => h(App), store, router }).$mount('#app') + +export default { + showError(message) { + vm.$bvToast.toast(message, { + title: 'Error', + variant: 'danger', + solid: true + }); + }, + showInfo(message) { + vm.$bvToast.toast(message, { + title: 'Info', + variant: 'info', + solid: true + }); + } +} diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js index 6e4ed2cea1121cd173cfc150622c88d0f9a4e405..f4b4a331d95c9bd753e5b293507ca1538090ccda 100644 --- a/vospace-ui-frontend/src/store.js +++ b/vospace-ui-frontend/src/store.js @@ -3,14 +3,15 @@ import Vue from 'vue'; import Vuex from 'vuex'; import client from 'api-client'; +import main from './main'; Vue.use(Vuex); export default new Vuex.Store({ state: { - //breadcrumbs: [], path: '', - loading: true + loading: true, + tapeButtonEnabled: false }, mutations: { setLoading(state, loading) { @@ -21,15 +22,38 @@ export default new Vuex.Store({ value = ''; } state.path = value; - //state.breadcrumbs = value.split('/') + }, + setTapeButtonEnabled(state, value) { + state.tapeButtonEnabled = value; } }, actions: { - setPath({ state, commit }, path) { + setPath({ state, commit, dispatch }, path) { commit('setPath', path); client.getNode(state.path) .then(res => { - document.getElementById('nodes').outerHTML = res + document.getElementById('nodes').outerHTML = res; + let checkboxes = document.querySelectorAll('#nodes input[type="checkbox"]'); + for (let i = 0; i < checkboxes.length; i++) { + checkboxes[i].addEventListener('change', function() { + dispatch('computeButtonVisibility'); + }); + } + dispatch('computeButtonVisibility'); + }); + }, + computeButtonVisibility({ commit }) { + commit('setTapeButtonEnabled', document.querySelectorAll('#nodes input.tape:checked').length > 0); + }, + startRecallFromTapeJob() { + let tapeCheckboxes = document.querySelectorAll('#nodes input.tape:checked'); + let paths = []; + for (let i = 0; i < tapeCheckboxes.length; i++) { + paths.push(tapeCheckboxes[i].getAttribute('data-node')); + } + client.startRecallFromTapeJob(paths) + .then(() => { + main.showInfo('Job started'); }); } }