diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java index 77b347ba00d2b1f03feb623fce1f67e41f3def5f..36a4324a474ee8e8744726b5794e871ce7cc88aa 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java @@ -15,7 +15,6 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import java.util.ArrayList; import java.util.List; import java.util.Scanner; import java.util.concurrent.CompletionException; @@ -27,7 +26,6 @@ import javax.servlet.http.HttpSession; import javax.xml.bind.JAXB; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.uws.v1.Jobs; -import net.ivoa.xml.uws.v1.ShortJobDescription; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; @@ -115,6 +113,17 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } + + public void deleteNode(String path) { + + HttpRequest request = getRequest("/nodes" + path) + .header("Accept", useJson ? "application/json" : "text/xml") + .header("Content-Type", useJson ? "application/json" : "text/xml") + .DELETE() + .build(); + + call(request, BodyHandlers.ofInputStream(), 200, res -> null); + } public List<Job> getJobs() { diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java index 52f306a7ea3e8aaf362e84ecf9ad9fcbaae119b0..73f129979593a4fb8d39fbf306b701d5f8d24472 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java @@ -14,6 +14,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -50,6 +51,12 @@ public class NodesController extends BaseController { return nodesService.generateNodesHtml(path); } + @DeleteMapping(value = {"/nodes", "/nodes/**"}) + public void deleteNode() { + String path = getPath("/nodes/"); + client.deleteNode(path); + } + @GetMapping(value = "/download/**") public ResponseEntity<?> directDownload() { 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 b7aa7fb888c24caa27dd6b4cfc3e9a8be562aa98..5d347db12aee6063680d1ec6639b227c2fd82aa5 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 @@ -63,6 +63,7 @@ public class NodesService { html += "<td>" + nodeInfo.getSize() + "</td>"; html += "<td>" + nodeInfo.getGroupRead() + "</td>"; html += "<td>" + nodeInfo.getGroupWrite() + "</td>"; + html += "<td><span class=\"icon trash-icon pointer\" onclick=\"deleteNode('" + nodeInfo.getPath() + "')\"></span></td>"; html += "</tr>"; return html; } diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html b/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html index 3b878bdb8a97d2aac055c5f4d751fcdc5c5d7a36..cacb3e10a71ada0a8ea50143c63c2ae4a3c3a88c 100644 --- a/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html +++ b/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html @@ -8,6 +8,7 @@ <td>0 B</td> <td>group1</td> <td>group2</td> + <td></td> </tr> <tr> <td><input type="checkbox" data-node="/folder1/file2" /></td> @@ -18,6 +19,7 @@ <td>30 KB</td> <td>group1</td> <td>group2</td> + <td></td> </tr> <tr> <td><input type="checkbox" class="async" data-node="/folder1/file3" /></td> @@ -28,5 +30,6 @@ <td>12 MB</td> <td>group3</td> <td>group4</td> + <td></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 index 24ac8b8461651c6d94e40c38467a50b68c157747..2c52dd94f004b9b9769f2c619248fd5600ad3cb2 100644 --- a/vospace-ui-frontend/src/api/mock/data/nodes/folder2.html +++ b/vospace-ui-frontend/src/api/mock/data/nodes/folder2.html @@ -8,6 +8,7 @@ <td>10 KB</td> <td>group1</td> <td>group2</td> + <td></td> </tr> <tr> <td><input type="checkbox" class="async" data-node="/folder1/folder2/file5" /></td> @@ -18,5 +19,6 @@ <td>15 MB</td> <td>group3</td> <td>group4</td> + <td></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 index f26856fff011b92a62a52417370a788e598e3d24..d6143ada2949a9b7bf83839faaf6936292cb9ce3 100644 --- a/vospace-ui-frontend/src/api/mock/data/nodes/root.html +++ b/vospace-ui-frontend/src/api/mock/data/nodes/root.html @@ -8,6 +8,9 @@ <td>0 B</td> <td>group1</td> <td>group2</td> + <td> + <span class="icon trash-icon pointer" onclick="deleteNode('/folder1')"></span> + </td> </tr> <tr> <td><input type="checkbox" data-node="/file1" /></td> @@ -18,5 +21,6 @@ <td>12 MB</td> <td>group1</td> <td>group2</td> + <td></td> </tr> </tbody> diff --git a/vospace-ui-frontend/src/api/mock/index.js b/vospace-ui-frontend/src/api/mock/index.js index 7130ca50a19a857a26d46ab3cea9a06890ccc519..9de8cecf14206a496622264a0703b08b479c6de2 100644 --- a/vospace-ui-frontend/src/api/mock/index.js +++ b/vospace-ui-frontend/src/api/mock/index.js @@ -54,5 +54,8 @@ export default { }, uploadFile() { return fetch({}); + }, + deleteNode() { + return fetch({}); } } diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js index 92845514d34110c95902e7c091a65de4eff2aa3b..a4b485f389c846f9cfe9c67581c7bb12036870f0 100644 --- a/vospace-ui-frontend/src/api/server/index.js +++ b/vospace-ui-frontend/src/api/server/index.js @@ -123,5 +123,16 @@ export default { 'Content-Type': 'multipart/form-data' } }) + }, + deleteNode(path) { + let url = BASE_API_URL + 'nodes' + path; + return apiRequest({ + method: 'DELETE', + url: url, + withCredentials: true, + headers: { + 'Cache-Control': 'no-cache' + } + }, true, true); } } diff --git a/vospace-ui-frontend/src/assets/css/fonts.css b/vospace-ui-frontend/src/assets/css/fonts.css index 06fbcfffbe7b17491b61ab90e1248f2615f81756..d24f83f18836fd04721e7c3c3811fc979d0531fd 100644 --- a/vospace-ui-frontend/src/assets/css/fonts.css +++ b/vospace-ui-frontend/src/assets/css/fonts.css @@ -20,3 +20,19 @@ .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"); } + +.trash-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='trash' xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' class='bi-trash mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z'%3E%3C/path%3E%3Cpath fill-rule='evenodd' d='M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); +} + +.pencil-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='pencil' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-pencil mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); +} + +.folder-link-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 symlink' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-folder-symlink mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M11.798 8.271l-3.182 1.97c-.27.166-.616-.036-.616-.372V9.1s-2.571-.3-4 2.4c.571-4.8 3.143-4.8 4-4.8v-.769c0-.336.346-.538.616-.371l3.182 1.969c.27.166.27.576 0 .742z'%3E%3C/path%3E%3Cpath d='M.5 3l.04.87a1.99 1.99 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14h10.348a2 2 0 0 0 1.991-1.819l.637-7A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2zm.694 2.09A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09l-.636 7a1 1 0 0 1-.996.91H2.826a1 1 0 0 1-.995-.91l-.637-7zM6.172 2a1 1 0 0 1 .707.293L7.586 3H2.19c-.24 0-.47.042-.684.12L1.5 2.98a1 1 0 0 1 1-.98h3.672z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); +} + +.gear-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='gear' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-gear mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z'%3E%3C/path%3E%3Cpath d='M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z'%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 3b8fc6f971721b17d6992c0e2587e5bebf5de239..c1ce55f728d76e06926d765782d3b919da8bdce7 100644 --- a/vospace-ui-frontend/src/components/Main.vue +++ b/vospace-ui-frontend/src/components/Main.vue @@ -23,6 +23,7 @@ <th>Size</th> <th>Group read</th> <th>Group write</th> + <th></th> </tr> </thead> <tbody id="nodes"></tbody> @@ -30,6 +31,7 @@ </b-card> <CreateFolderModal /> <UploadFilesModal /> + <ConfirmDeleteModal /> </div> </template> @@ -37,13 +39,15 @@ import { BIconCheckSquare, BIconSquare } from 'bootstrap-vue' import CreateFolderModal from './modal/CreateFolderModal.vue' import UploadFilesModal from './modal/UploadFilesModal.vue' +import ConfirmDeleteModal from './modal/ConfirmDeleteModal.vue' export default { components: { BIconCheckSquare, BIconSquare, CreateFolderModal, - UploadFilesModal + UploadFilesModal, + ConfirmDeleteModal }, computed: { breadcrumbs() { @@ -111,4 +115,8 @@ th#checkboxes { width: 0.1%; white-space: nowrap; } + +.pointer { + cursor: pointer; +} </style> diff --git a/vospace-ui-frontend/src/components/modal/ConfirmDeleteModal.vue b/vospace-ui-frontend/src/components/modal/ConfirmDeleteModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..fb403398a9330225f21ea80cf5178834622cf977 --- /dev/null +++ b/vospace-ui-frontend/src/components/modal/ConfirmDeleteModal.vue @@ -0,0 +1,28 @@ +<template> +<b-modal id="confirm-delete-modal" title="Confirm delete" okTitle="Yes, delete" @ok="deleteNode" @hidden="reset" ok-variant="danger"> + <p>Do you really want to delete node at {{nodeToDelete}}?</p> +</b-modal> +</template> + +<script> +export default { + name: 'ConfirmDeleteModal', + computed: { + nodeToDelete() { return this.$store.state.nodeToDelete } + }, + methods: { + reset() { + this.$store.commit('setNodeToDelete', null); + }, + deleteNode(event) { + // Prevent modal from closing + event.preventDefault(); + + this.$store.dispatch('deleteNode') + .then(() => { + this.$bvModal.hide('confirm-delete-modal'); + }); + } + } +} +</script> diff --git a/vospace-ui-frontend/src/main.js b/vospace-ui-frontend/src/main.js index 857bdf950bd2956263b8a947daf4c8c4727175d8..d93a3b3c1a0f45774607da83c6f76a42b88cb87f 100644 --- a/vospace-ui-frontend/src/main.js +++ b/vospace-ui-frontend/src/main.js @@ -18,6 +18,11 @@ let vm = new Vue({ router }).$mount('#app') +window.deleteNode = function(path) { + store.commit('setNodeToDelete', path); + vm.$bvModal.show('confirm-delete-modal'); +} + export default { showError(message) { vm.$bvToast.toast(message, { diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js index 848ce4567e58b3c48a6f54aef83e5111d4e0fb47..628d91c43e3dce1b25a4d3364bd132a5ec5d0214 100644 --- a/vospace-ui-frontend/src/store.js +++ b/vospace-ui-frontend/src/store.js @@ -13,7 +13,8 @@ export default new Vuex.Store({ loading: true, asyncButtonEnabled: false, jobs: [], - user: 'anonymous' + user: 'anonymous', + nodeToDelete: null }, mutations: { setLoading(state, loading) { @@ -41,6 +42,9 @@ export default new Vuex.Store({ }, setUsername(state, username) { state.user = username; + }, + setNodeToDelete(state, path) { + state.nodeToDelete = path; } }, actions: { @@ -103,6 +107,13 @@ export default new Vuex.Store({ // Reload current node dispatch('setPath', state.path); }); + }, + deleteNode({ state, dispatch }) { + client.deleteNode(state.nodeToDelete) + .then(() => { + // Reload current node + dispatch('setPath', state.path); + }); } } });