Skip to content
Snippets Groups Projects
Commit 82bda48c authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Handled tar/zip creation and download of resulting file

parent 3f92506f
No related branches found
No related tags found
No related merge requests found
Pipeline #2115 passed
Showing
with 464 additions and 69 deletions
......@@ -8,6 +8,7 @@ package it.inaf.ia2.vospace.ui;
import it.inaf.ia2.aa.AuthConfig;
import it.inaf.ia2.aa.LoginFilter;
import it.inaf.ia2.aa.ServiceLocator;
import it.inaf.ia2.aa.ServletRapClient;
import it.inaf.ia2.aa.UserManager;
import it.inaf.ia2.gms.client.GmsClient;
import it.inaf.ia2.rap.client.ClientCredentialsRapClient;
......@@ -86,4 +87,9 @@ public class VOSpaceUiApplication {
String gmsBaseUri = ServiceLocator.getInstance().getConfig().getGmsUri();
return new GmsClient(gmsBaseUri);
}
@Bean
public ServletRapClient servletRapClient() {
return (ServletRapClient) ServiceLocator.getInstance().getRapClient();
}
}
......@@ -175,9 +175,20 @@ public class VOSpaceClient {
return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class));
}
public List<Job> getJobs() {
public List<Job> getAsyncRecallJobs() {
return getJobs("direction=pullToVoSpace", Job.JobType.ASYNC_RECALL);
}
public List<Job> getArchiveJobs() {
return getJobs("direction=pullFromVoSpace"
+ "&VIEW=ivo://ia2.inaf.it/vospace/views%23tar"
+ "&VIEW=ivo://ia2.inaf.it/vospace/views%23zip",
Job.JobType.ARCHIVE);
}
private List<Job> getJobs(String queryString, Job.JobType type) {
HttpRequest request = getRequest("/transfers?direction=pullToVoSpace")
HttpRequest request = getRequest("/transfers?" + queryString)
.header("Accept", useJson ? "application/json" : "text/xml")
.header("Content-Type", useJson ? "application/json" : "text/xml")
.GET()
......@@ -185,11 +196,27 @@ public class VOSpaceClient {
return call(request, BodyHandlers.ofInputStream(), 200, res -> {
return unmarshal(res, Jobs.class).getJobref().stream()
.map(jobDesc -> new Job(jobDesc))
.map(jobDesc -> new Job(jobDesc, type))
.collect(Collectors.toList());
});
}
public String getArchiveJobHref(String jobId) {
List<Protocol> protocols = getTransferDetails(jobId).getProtocols();
if (!protocols.isEmpty()) {
return protocols.get(0).getEndpoint();
}
return null;
}
private Transfer getTransferDetails(String jobId) {
HttpRequest request = getRequest("/transfers/" + jobId + "/results/transferDetails")
.GET().build();
return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class));
}
public ExecutionPhase getJobPhase(String jobId) {
HttpRequest request = getRequest("/transfers/" + jobId + "/phase")
......
/*
* This file is part of vospace-ui
* Copyright (C) 2021 Istituto Nazionale di Astrofisica
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.aa.ServletRapClient;
import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.rap.client.call.TokenExchangeRequest;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.exception.PermissionDeniedException;
import it.inaf.oats.vospace.datamodel.NodeUtils;
import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import net.ivoa.xml.vospace.v2.Protocol;
import net.ivoa.xml.vospace.v2.Transfer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DownloadController {
private static final Logger LOG = LoggerFactory.getLogger(DownloadController.class);
@Value("${vospace-authority}")
private String authority;
@Autowired
private VOSpaceClient client;
@Autowired
private HttpServletRequest servletRequest;
@Autowired
private ServletRapClient rapClient;
@GetMapping(value = "/download/**")
public ResponseEntity<?> directDownload() {
String path = getPath("/download/");
LOG.debug("directDownload called for path {}", path);
Transfer transfer = new Transfer();
transfer.setDirection("pullFromVoSpace");
transfer.setTarget(Arrays.asList("vos://" + authority + urlEncodePath(path)));
Protocol protocol = new Protocol();
protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
transfer.getProtocols().add(protocol);
String url = client.getFileServiceEndpoint(transfer);
HttpHeaders headers = new HttpHeaders();
headers.set("Location", url);
return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
}
@GetMapping(value = "/download")
public ResponseEntity<?> downloadJobResult(@RequestParam("jobId") String jobId) {
LOG.debug("job result download called for jobId {}", jobId);
String token = ((User) servletRequest.getUserPrincipal()).getAccessToken();
if (token == null) {
throw new PermissionDeniedException("Token is null");
}
String endpoint = client.getArchiveJobHref(jobId);
TokenExchangeRequest exchangeRequest = new TokenExchangeRequest()
.setSubjectToken(token)
.setResource(endpoint);
String newToken = rapClient.exchangeToken(exchangeRequest, servletRequest);
String url = endpoint + "?token=" + newToken;
HttpHeaders headers = new HttpHeaders();
headers.set("Location", url);
return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
}
protected String getPath(String prefix) {
String requestURL = servletRequest.getRequestURL().toString();
return NodeUtils.getPathFromRequestURLString(requestURL, prefix);
}
}
......@@ -8,7 +8,9 @@ package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.Job;
import it.inaf.ia2.vospace.ui.exception.BadRequestException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import net.ivoa.xml.uws.v1.ErrorType;
import net.ivoa.xml.uws.v1.ExecutionPhase;
......@@ -59,7 +61,7 @@ public class JobController extends BaseController {
if (job.getPhase() == ExecutionPhase.QUEUED
|| job.getPhase() == ExecutionPhase.PENDING
|| job.getPhase() == ExecutionPhase.EXECUTING) {
return ResponseEntity.ok(new Job(job));
return ResponseEntity.ok(new Job(job, Job.JobType.ASYNC_RECALL));
}
String errorMessage;
if (job.getPhase() == ExecutionPhase.ERROR) {
......@@ -87,7 +89,22 @@ public class JobController extends BaseController {
}
@GetMapping(value = "/jobs", produces = MediaType.APPLICATION_JSON_VALUE)
public List<Job> getJobs() {
return client.getJobs();
public List<Job> getJobs() throws Exception {
CompletableFuture<List<Job>> asyncRecallJobsCall
= CompletableFuture.supplyAsync(() -> client.getAsyncRecallJobs(),
Runnable::run);
CompletableFuture<List<Job>> archiveJobsCall
= CompletableFuture.supplyAsync(() -> client.getArchiveJobs(),
Runnable::run);
CompletableFuture.allOf(asyncRecallJobsCall, archiveJobsCall).join();
List<Job> jobs = new ArrayList<>();
jobs.addAll(asyncRecallJobsCall.get());
jobs.addAll(archiveJobsCall.get());
return jobs;
}
}
......@@ -25,15 +25,15 @@ import net.ivoa.xml.uws.v1.ExecutionPhase;
import net.ivoa.xml.uws.v1.JobSummary;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Param;
import net.ivoa.xml.vospace.v2.Property;
import net.ivoa.xml.vospace.v2.Protocol;
import net.ivoa.xml.vospace.v2.Transfer;
import net.ivoa.xml.vospace.v2.View;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.GetMapping;
......@@ -94,26 +94,6 @@ public class NodesController extends BaseController {
return ResponseEntity.ok(listNodeData);
}
@GetMapping(value = "/download/**")
public ResponseEntity<?> directDownload() {
String path = getPath("/download/");
LOG.debug("directDownload called for path {}", path);
Transfer transfer = new Transfer();
transfer.setDirection("pullFromVoSpace");
transfer.setTarget(Arrays.asList("vos://" + authority + urlEncodePath(path)));
Protocol protocol = new Protocol();
protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
transfer.getProtocols().add(protocol);
String url = client.getFileServiceEndpoint(transfer);
HttpHeaders headers = new HttpHeaders();
headers.set("Location", url);
return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
}
@PostMapping(value = "/folder")
public void newFolder(@RequestBody Map<String, Object> params) {
......@@ -162,6 +142,68 @@ public class NodesController extends BaseController {
return ResponseEntity.noContent().build();
}
@PostMapping(value = "/zip", consumes = MediaType.APPLICATION_JSON_VALUE)
public Job createZip(@RequestBody List<String> paths) {
return createArchive(paths, "ivo://ia2.inaf.it/vospace/views#zip");
}
@PostMapping(value = "/tar", consumes = MediaType.APPLICATION_JSON_VALUE)
public Job createTar(@RequestBody List<String> paths) {
return createArchive(paths, "ivo://ia2.inaf.it/vospace/views#tar");
}
private Job createArchive(List<String> paths, String viewUri) {
Transfer transfer = new Transfer();
View view = new View();
view.setUri(viewUri);
transfer.setView(view);
if (paths.size() == 1) {
transfer.setTarget(Arrays.asList("vos://" + authority + paths.get(0)));
} else {
String parent = getCommonParent(paths);
transfer.setTarget(Arrays.asList("vos://" + authority + parent));
for (String path : paths) {
String childName = path.substring(parent.length() + 1);
Param param = new Param();
param.setUri(viewUri + "/include");
param.setValue(childName);
view.getParam().add(param);
}
}
transfer.setDirection("pullFromVoSpace");
Protocol protocol = new Protocol();
protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
transfer.getProtocols().add(protocol);
return new Job(client.startTransferJob(transfer), Job.JobType.ARCHIVE);
}
private String getCommonParent(List<String> vosPaths) {
String commonParent = null;
for (String vosPath : vosPaths) {
if (commonParent == null) {
commonParent = vosPath;
} else {
StringBuilder newCommonParent = new StringBuilder();
boolean same = true;
int lastSlashPos = vosPath.lastIndexOf("/");
for (int i = 0; same && i < Math.min(commonParent.length(), vosPath.length()) && i < lastSlashPos; i++) {
if (commonParent.charAt(i) == vosPath.charAt(i)) {
newCommonParent.append(commonParent.charAt(i));
} else {
same = false;
}
}
commonParent = newCommonParent.toString();
}
}
return commonParent;
}
@PostMapping(value = "/move")
public ResponseEntity<Job> moveNode(@RequestBody Map<String, Object> params) {
......@@ -195,7 +237,7 @@ public class NodesController extends BaseController {
job.setPhase(phase);
return ResponseEntity.ok(new Job(job));
return ResponseEntity.ok(new Job(job, Job.JobType.MOVE));
}
protected String getPath(String prefix) {
......
......@@ -13,24 +13,33 @@ import net.ivoa.xml.uws.v1.ShortJobDescription;
public class Job {
public static enum JobType {
ASYNC_RECALL,
ARCHIVE,
MOVE
}
private String id;
private String creationTime;
private ExecutionPhase phase;
private boolean read;
private JobType type;
public Job() {
}
public Job(JobSummary job) {
public Job(JobSummary job, JobType type) {
this.id = job.getJobId();
this.creationTime = formatCreationTime(job.getCreationTime());
this.phase = job.getPhase();
this.type = type;
}
public Job(ShortJobDescription job) {
public Job(ShortJobDescription job, JobType type) {
this.id = job.getId();
this.creationTime = formatCreationTime(job.getCreationTime());
this.phase = job.getPhase();
this.type = type;
}
private String formatCreationTime(XMLGregorianCalendar calendar) {
......@@ -72,4 +81,12 @@ public class Job {
public void setRead(boolean read) {
this.read = read;
}
public JobType getType() {
return type;
}
public void setType(JobType type) {
this.type = type;
}
}
/*
* This file is part of vospace-ui
* Copyright (C) 2021 Istituto Nazionale di Astrofisica
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.aa.ServletRapClient;
import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
@TestPropertySource(properties = {"vospace-authority=example.com!vospace"})
public class DownloadControllerTest {
@MockBean
private VOSpaceClient client;
@MockBean
private ServletRapClient rapClient;
@Autowired
private MockMvc mockMvc;
@Test
public void testDirectDownload() throws Exception {
when(client.getFileServiceEndpoint(any())).thenReturn("http://redirect");
mockMvc.perform(get("/download/myfile"))
.andExpect(status().is3xxRedirection());
verify(client, times(1)).getFileServiceEndpoint(any());
}
@Test
public void testDownloadJobResult() throws Exception {
User user = mock(User.class);
when(user.getAccessToken()).thenReturn("<token>");
when(client.getArchiveJobHref("job123")).thenReturn("http://file-service/job123.zip");
when(rapClient.exchangeToken(any(), any())).thenReturn("<new-token>");
String redirect = mockMvc.perform(get("/download?jobId=job123")
.principal(user))
.andExpect(status().is3xxRedirection())
.andReturn().getResponse().getRedirectedUrl();
assertEquals("http://file-service/job123.zip?token=<new-token>", redirect);
}
}
......@@ -6,6 +6,8 @@
package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.Job;
import java.util.List;
import java.util.function.Consumer;
import net.ivoa.xml.uws.v1.ErrorSummary;
import net.ivoa.xml.uws.v1.ErrorType;
......@@ -24,8 +26,10 @@ import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import org.springframework.web.client.RestTemplate;
@SpringBootTest
......@@ -121,6 +125,19 @@ public class JobControllerTest {
testErrorCall(ex -> assertTrue(ex.getMessage().contains("error")));
}
@Test
public void testGetJobs() throws Exception {
when(client.getAsyncRecallJobs()).thenReturn(List.of(new Job(new JobSummary(), Job.JobType.ASYNC_RECALL)));
when(client.getArchiveJobs()).thenReturn(List.of(new Job(new JobSummary(), Job.JobType.ARCHIVE)));
mockMvc.perform(get("/jobs"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$[0].type").value("ASYNC_RECALL"))
.andExpect(jsonPath("$[1].type").value("ARCHIVE"));
}
private void testErrorCall(Consumer<Exception> exceptionChecker) throws Exception {
try {
mockMvc.perform(post("/recall")
......
......@@ -205,17 +205,6 @@ public class NodesControllerTest {
}
}
@Test
public void testDirectDownload() throws Exception {
when(client.getFileServiceEndpoint(any())).thenReturn("http://redirect");
mockMvc.perform(get("/download/myfile"))
.andExpect(status().is3xxRedirection());
verify(client, times(1)).getFileServiceEndpoint(any());
}
@Test
public void testNewFolder() throws Exception {
......
......@@ -203,5 +203,17 @@ export default {
},
data
}, true, true);
},
createArchive(paths, type) {
let url = BASE_API_URL + type;
return apiRequest({
method: 'POST',
url: url,
withCredentials: true,
headers: {
'Cache-Control': 'no-cache'
},
data: paths
}, true, true);
}
}
......@@ -6,23 +6,32 @@
<template>
<div>
<b-button variant="primary" class="float-right" @click="loadJobs">Reload</b-button>
<h3>Async recall jobs</h3>
<h3>Jobs</h3>
<div v-if="jobs.length > 0" class="mb-3">
<table class="table b-table table-striped table-hover">
<thead>
<tr>
<th>Type</th>
<th>Creation time</th>
<th>Id</th>
<th>Link</th>
<th>Phase</th>
</tr>
</thead>
<tbody>
<tr v-for="job in jobs" :key="job.id">
<td>{{job.type}}</td>
<td>{{job.creationTime}}</td>
<td>{{job.id}}</td>
<td><a :href="'download?jobId=' + job.id" v-if="job.phase === 'COMPLETED' && job.type === 'ARCHIVE'">Download archive</a></td>
<td>{{job.phase}}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="jobs.length === 0">
No jobs
</div>
<div id="jobs-loading" v-if="jobsLoading" class="loading">
<div class="spinner-wrapper">
<b-spinner variant="primary" style="width: 3rem; height: 3rem;" label="Loading"></b-spinner>
......
......@@ -9,8 +9,12 @@
<div class="mb-3">
<b-button variant="success" class="mr-2" :disabled="!writable" v-b-modal.create-folder-modal>New folder</b-button>
<b-button variant="success" class="mr-2" :disabled="!writable" v-b-modal.upload-files-modal>Upload files</b-button>
<b-button variant="primary" class="mr-2" v-if="asyncButtonEnabled" @click="startAsyncRecallJob">Async recall</b-button>
<b-button variant="danger" class="mr-2" v-if="deleteButtonEnabled" @click="deleteNodes">Delete</b-button>
<b-dropdown variant="primary" text="Actions" v-if="asyncButtonEnabled || deleteButtonEnabled || archiveButtonEnabled">
<b-dropdown-item :disabled="!asyncButtonEnabled" @click="startAsyncRecallJob">Async recall</b-dropdown-item>
<b-dropdown-item :disabled="!deleteButtonEnabled" @click="deleteNodes">Delete</b-dropdown-item>
<b-dropdown-item :disabled="!archiveButtonEnabled" @click="createArchive('zip')">Create zip archive</b-dropdown-item>
<b-dropdown-item :disabled="!archiveButtonEnabled" @click="createArchive('tar')">Create tar archive</b-dropdown-item>
</b-dropdown>
</div>
<b-card>
<table class="table b-table table-striped table-hover">
......@@ -41,6 +45,7 @@
<ShareModal />
<RenameModal />
<MoveModal />
<ConfirmArchiveModal />
</div>
</template>
......@@ -52,6 +57,7 @@ import ConfirmDeleteModal from './modal/ConfirmDeleteModal.vue'
import ShareModal from './modal/ShareModal.vue'
import RenameModal from './modal/RenameModal.vue'
import MoveModal from './modal/MoveModal.vue'
import ConfirmArchiveModal from './modal/ConfirmArchiveModal.vue'
export default {
components: {
......@@ -62,7 +68,8 @@ export default {
ConfirmDeleteModal,
ShareModal,
RenameModal,
MoveModal
MoveModal,
ConfirmArchiveModal
},
computed: {
breadcrumbs() {
......@@ -87,6 +94,9 @@ export default {
deleteButtonEnabled() {
return this.$store.state.deleteButtonEnabled;
},
archiveButtonEnabled() {
return this.$store.state.archiveButtonEnabled;
},
writable() {
return this.$store.state.writable;
}
......@@ -118,7 +128,8 @@ export default {
},
deleteNodes() {
let selectedNodesCheckboxes = document.querySelectorAll('#nodes input:checked');
let paths = [], unDeletablePaths = [];
let paths = [];
let unDeletablePaths = [];
for (let i = 0; i < selectedNodesCheckboxes.length; i++) {
let checkbox = selectedNodesCheckboxes[i];
let dataNode = checkbox.getAttribute('data-node');
......@@ -131,6 +142,28 @@ export default {
this.$store.commit('setNodesToDelete', paths);
this.$store.commit('setSelectedUndeletableNodes', unDeletablePaths);
this.$bvModal.show('confirm-delete-modal');
},
createArchive(type) {
let selectedNodesCheckboxes = document.querySelectorAll('#nodes input:checked');
let nodesToArchive = [];
let notArchivableNodes = [];
for (let i = 0; i < selectedNodesCheckboxes.length; i++) {
let checkbox = selectedNodesCheckboxes[i];
let dataNode = checkbox.getAttribute('data-node');
if (checkbox.classList.contains('async')) {
notArchivableNodes.push(dataNode);
} else {
nodesToArchive.push(dataNode);
}
}
this.$store.commit('setArchiveType', type);
this.$store.commit('setNodesToArchives', nodesToArchive);
if (notArchivableNodes.length === 0) {
this.$store.dispatch('createArchive', type)
} else {
this.$store.commit('setSelectedNotArchivableNodes', notArchivableNodes);
this.$bvModal.show('confirm-archive-modal');
}
}
}
}
......
<!--
This file is part of vospace-ui
Copyright (C) 2021 Istituto Nazionale di Astrofisica
SPDX-License-Identifier: GPL-3.0-or-later
-->
<template>
<b-modal id="confirm-archive-modal" title="Archive creation warning" okTitle="Ok, proceed anyway" @ok.prevent="createArchive" size="lg">
<p><strong>Warning</strong>: some of the nodes you selected require to be asynchronously retrieved before including them into an archive:</p>
<p>
<ul>
<li v-for="node in notArchivableNodes" :key="node">{{node}}</li>
</ul>
</p>
<p>If you proceed these files will be ignored and not included in the resulting archive file.</p>
</b-modal>
</template>
<script>
export default {
name: 'ConfirmArchiveModal',
computed: {
notArchivableNodes() { return this.$store.state.selectedNotArchivableNodes }
},
methods: {
createArchive() {
this.$store.dispatch('createArchive')
.then(() => {
this.$bvModal.hide('confirm-archive-modal');
});
}
}
}
</script>
......@@ -28,6 +28,7 @@ export default new Vuex.Store({
nodesLoading: false,
asyncButtonEnabled: false,
deleteButtonEnabled: false,
archiveButtonEnabled: false,
jobs: [],
jobsLoading: true,
lastJobsCheckTime: null,
......@@ -43,7 +44,10 @@ export default new Vuex.Store({
nodeToRename: null,
nodeToMove: null,
nodeToMoveDestination: null,
nodeToMoveDestinationWritable: false
nodeToMoveDestinationWritable: false,
archiveType: null,
nodesToArchive: [],
selectedNotArchivableNodes: []
},
mutations: {
setLoading(state, loading) {
......@@ -64,6 +68,9 @@ export default new Vuex.Store({
setDeleteButtonEnabled(state, value) {
state.deleteButtonEnabled = value;
},
setArchiveButtonEnabled(state, value) {
state.archiveButtonEnabled = value;
},
setJobs(state, jobs) {
updateArray(state.jobs, jobs);
},
......@@ -101,6 +108,15 @@ export default new Vuex.Store({
},
setNodeToMoveDestinationWritable(state, value) {
state.nodeToMoveDestinationWritable = value;
},
setArchiveType(state, type) {
state.archiveType = type;
},
setNodesToArchives(state, paths) {
state.nodesToArchive = paths;
},
setSelectedNotArchivableNodes(state, paths) {
state.selectedNotArchivableNodes = paths;
}
},
actions: {
......@@ -127,6 +143,7 @@ export default new Vuex.Store({
computeButtonsVisibility({ commit }) {
commit('setAsyncButtonEnabled', document.querySelectorAll('#nodes input.async:checked').length > 0);
commit('setDeleteButtonEnabled', document.querySelectorAll('#nodes input.deletable:checked').length > 0);
commit('setArchiveButtonEnabled', document.querySelectorAll('#nodes input:not(.async):checked').length > 0);
},
startAsyncRecallJob({ state, commit, dispatch }) {
let asyncCheckboxes = document.querySelectorAll('#nodes input.async:checked');
......@@ -239,6 +256,17 @@ export default new Vuex.Store({
commit('setNodeToMoveDestinationWritable', res.writable);
document.getElementById('move-nodes').outerHTML = res.html;
});
},
createArchive({ state, commit }) {
client.createArchive(state.nodesToArchive, state.archiveType)
.then(job => {
if (job.phase === 'ERROR') {
main.showError('Error creating ' + state.archiveType + ' archive');
} else {
main.showInfo(state.archiveType + ' creation started');
commit('addJob', job);
}
});
}
}
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment