From 58f6570bc581d58110e7f95ffcefaa0e0e4ef161 Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Thu, 19 Nov 2020 17:32:40 +0100 Subject: [PATCH] Started node listing implementation --- .../inaf/ia2/vospace/ui/SecurityConfig.java | 41 ++++++ .../inaf/ia2/vospace/ui/VOSpaceException.java | 12 ++ ...ication.java => VOSpaceUiApplication.java} | 4 +- .../ia2/vospace/ui/client/VOSpaceClient.java | 121 ++++++++++++++++++ .../ui/controller/IndexController.java | 16 +++ .../ui/controller/NodesController.java | 32 +++++ .../ia2/vospace/ui/service/NodesService.java | 60 +++++++++ .../src/main/resources/application.properties | 7 + vospace-ui-frontend/.env.production | 2 + vospace-ui-frontend/package-lock.json | 11 +- vospace-ui-frontend/package.json | 1 + vospace-ui-frontend/src/App.vue | 75 ++++++++++- vospace-ui-frontend/src/api/server/index.js | 66 ++++++++++ .../src/assets/ia2-logo-footer.png | Bin 0 -> 1446 bytes vospace-ui-frontend/src/components/Main.vue | 26 +++- .../src/components/TopMenu.vue | 14 ++ vospace-ui-frontend/src/main.js | 1 + .../src/plugins/bootstrap-vue.js | 7 + vospace-ui-frontend/src/store.js | 8 +- vospace-ui-frontend/vue.config.js | 12 ++ 20 files changed, 508 insertions(+), 8 deletions(-) create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SecurityConfig.java create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceException.java rename vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/{VospaceUiApplication.java => VOSpaceUiApplication.java} (69%) create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/IndexController.java create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java create mode 100644 vospace-ui-frontend/.env.production create mode 100644 vospace-ui-frontend/src/api/server/index.js create mode 100644 vospace-ui-frontend/src/assets/ia2-logo-footer.png create mode 100644 vospace-ui-frontend/src/components/TopMenu.vue create mode 100644 vospace-ui-frontend/src/plugins/bootstrap-vue.js create mode 100644 vospace-ui-frontend/vue.config.js diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SecurityConfig.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SecurityConfig.java new file mode 100644 index 0000000..fa12343 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SecurityConfig.java @@ -0,0 +1,41 @@ +package it.inaf.ia2.vospace.ui; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class SecurityConfig { + + private static final Logger LOG = LoggerFactory.getLogger(SecurityConfig.class); + + @Value("${cors.allowed.origin}") + private String corsAllowedOrigin; + + /** + * CORS are necessary only for development (API access from npm server). + */ + @Bean + @Profile("dev") + public WebMvcConfigurer corsConfigurer() { + + return new WebMvcConfigurer() { + + @Override + public void addCorsMappings(CorsRegistry registry) { + + LOG.warn("Development profile active: CORS filter enabled"); + + registry.addMapping("/**") + .allowedOrigins(corsAllowedOrigin) + .allowedMethods("*") + .allowCredentials(true); + } + }; + } +} diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceException.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceException.java new file mode 100644 index 0000000..2b227f5 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceException.java @@ -0,0 +1,12 @@ +package it.inaf.ia2.vospace.ui; + +public class VOSpaceException extends RuntimeException { + + public VOSpaceException(String message) { + super(message); + } + + public VOSpaceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VospaceUiApplication.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java similarity index 69% rename from vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VospaceUiApplication.java rename to vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java index ad9aac9..010de2f 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VospaceUiApplication.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java @@ -4,9 +4,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class VospaceUiApplication { +public class VOSpaceUiApplication { public static void main(String[] args) { - SpringApplication.run(VospaceUiApplication.class, args); + SpringApplication.run(VOSpaceUiApplication.class, args); } } 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 new file mode 100644 index 0000000..9a0aebb --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java @@ -0,0 +1,121 @@ +package it.inaf.ia2.vospace.ui.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.inaf.ia2.vospace.ui.VOSpaceException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.ConnectException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Scanner; +import java.util.concurrent.CompletionException; +import java.util.function.Function; +import net.ivoa.xml.vospace.v2.Node; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class VOSpaceClient { + + private static final Logger LOG = LoggerFactory.getLogger(VOSpaceClient.class); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final HttpClient httpClient; + private final String baseUrl; + + public VOSpaceClient(@Value("${vospace-backend-url}") String backendUrl) { + if (backendUrl.endsWith("/")) { + // Remove final slash from configured URL + backendUrl = backendUrl.substring(0, backendUrl.length() - 1); + } + baseUrl = backendUrl; + + httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .version(HttpClient.Version.HTTP_1_1) + .build(); + } + + public Node getNode(String path) { + + HttpRequest request = getRequest("/nodes/" + path) + .header("Accept", "application/json") + .build(); + + return call(request, BodyHandlers.ofInputStream(), 200, res -> parseJson(res, Node.class)); + } + + private <T, U> U call(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, int expectedStatusCode, Function<T, U> responseHandler) { + try { + return httpClient.sendAsync(request, responseBodyHandler) + .thenApply(response -> { + if (response.statusCode() == expectedStatusCode) { + return response.body(); + } + logServerError(request, response); + throw new VOSpaceException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode()); + }) + .thenApply(response -> responseHandler.apply(response)) + .join(); + } catch (CompletionException ex) { + if (ex.getCause() != null) { + if (ex.getCause() instanceof ConnectException) { + throw new VOSpaceException("Cannot connect to " + request.uri().getHost() + " on port " + request.uri().getPort()); + } + if (ex.getCause() instanceof VOSpaceException) { + throw (VOSpaceException) ex.getCause(); + } + LOG.error("Error calling " + request.uri().toString(), ex.getCause()); + throw new VOSpaceException("Error calling " + request.uri().toString(), ex.getCause()); + } + LOG.error("Error calling " + request.uri().toString(), ex); + throw new VOSpaceException("Error calling " + request.uri().toString(), ex); + } + } + + private HttpRequest.Builder getRequest(String path) { + return HttpRequest.newBuilder(URI.create(baseUrl + path)); + } + + private static <T> T parseJson(InputStream in, Class<T> type) { + try { + return MAPPER.readValue(in, type); + } catch (IOException ex) { + LOG.error("Invalid JSON for class {}", type.getCanonicalName()); + throw new UncheckedIOException(ex); + } + } + + private static <T> void logServerError(HttpRequest request, HttpResponse<T> response) { + if (response.body() instanceof String) { + logServerErrorString(request, (HttpResponse<String>) response); + } else if (response.body() instanceof InputStream) { + logServerErrorInputStream(request, (HttpResponse<InputStream>) response); + } else { + throw new UnsupportedOperationException("Unable to log error for response body type " + response.body().getClass().getCanonicalName()); + } + } + + private static void logServerErrorString(HttpRequest request, HttpResponse<String> response) { + + LOG.error("Error while reading " + request.uri() + + "\nServer response status code is " + response.statusCode() + + "\nServer response text is " + response.body()); + } + + private static void logServerErrorInputStream(HttpRequest request, HttpResponse<InputStream> response) { + Scanner s = new Scanner(response.body()).useDelimiter("\\A"); + String responseBody = s.hasNext() ? s.next() : ""; + String error = "Error while reading " + request.uri() + + "\nServer response status code is " + response.statusCode() + + "\nServer response text is " + responseBody; + LOG.error(error); + } +} diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/IndexController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/IndexController.java new file mode 100644 index 0000000..a0c8265 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/IndexController.java @@ -0,0 +1,16 @@ +package it.inaf.ia2.vospace.ui.controller; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class IndexController { + + @GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE) + public String index(HttpServletRequest request, HttpServletResponse response) { + return "index.html"; + } +} 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 new file mode 100644 index 0000000..9f406d1 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java @@ -0,0 +1,32 @@ +package it.inaf.ia2.vospace.ui.controller; + +import it.inaf.ia2.vospace.ui.service.NodesService; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class NodesController { + + @Autowired + private NodesService nodesService; + + /** + * This is the only API endpoint that returns HTML code instead of JSON. The + * reason is that JavaScript frameworks are not very efficient in handling + * very long lists and tables, so this part of the code is generated + * server-side. + */ + @GetMapping(value = {"/nodes", "/nodes/{path}"}, produces = MediaType.TEXT_HTML_VALUE) + public void listNodes(@PathVariable(value = "path", required = false) String path, HttpServletResponse response) throws Exception { + + if (path == null || path.isBlank()) { + path = "/"; + } + + nodesService.generateNodesHtml(path, response.getOutputStream()); + } +} 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 new file mode 100644 index 0000000..96c21b8 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java @@ -0,0 +1,60 @@ +package it.inaf.ia2.vospace.ui.service; + +import it.inaf.ia2.vospace.ui.VOSpaceException; +import it.inaf.ia2.vospace.ui.client.VOSpaceClient; +import java.io.OutputStream; +import java.io.PrintWriter; +import net.ivoa.xml.vospace.v2.ContainerNode; +import net.ivoa.xml.vospace.v2.Node; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class NodesService { + + @Autowired + private VOSpaceClient client; + + @Value("${vospace-authority}") + private String authority; + + public void generateNodesHtml(String path, OutputStream out) { + + Node node = client.getNode(path); + + try ( PrintWriter pw = new PrintWriter(out)) { + + if (node instanceof ContainerNode) { + ContainerNode folder = (ContainerNode) node; + pw.println("<tbody id=\"nodes\">"); + for (Node child : folder.getNodes().getNode()) { + pw.println(getNodeHtml(child)); + } + pw.println("</tbody>"); + } + } + } + + private String getNodeHtml(Node node) { + String html = "<tr>"; + html += "<td><a href=\"#\">"; + html += getName(node); + html += "</a></td>"; + html += "</tr>"; + return html; + } + + private String getName(Node node) { + + String uri = node.getUri(); + + String prefix = "vos://" + authority; + + if (!uri.startsWith(prefix)) { + throw new VOSpaceException("Node authority is different from configured one! Configured is " + authority + ", but node URI is " + uri); + } + + return uri.substring(prefix.length() + 1); + } +} diff --git a/vospace-ui-backend/src/main/resources/application.properties b/vospace-ui-backend/src/main/resources/application.properties index 8b13789..934e284 100644 --- a/vospace-ui-backend/src/main/resources/application.properties +++ b/vospace-ui-backend/src/main/resources/application.properties @@ -1 +1,8 @@ +server.port=8085 +vospace-backend-url=http://localhost:8083/vospace +vospace-authority=example.com!vospace + +# For development only: +spring.profiles.active=dev +cors.allowed.origin=http://localhost:8080 diff --git a/vospace-ui-frontend/.env.production b/vospace-ui-frontend/.env.production new file mode 100644 index 0000000..6010f55 --- /dev/null +++ b/vospace-ui-frontend/.env.production @@ -0,0 +1,2 @@ +VUE_APP_API_CLIENT = 'server' +VUE_APP_API_BASE_URL = '' diff --git a/vospace-ui-frontend/package-lock.json b/vospace-ui-frontend/package-lock.json index 178aac5..80ecd14 100644 --- a/vospace-ui-frontend/package-lock.json +++ b/vospace-ui-frontend/package-lock.json @@ -2538,6 +2538,14 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axios": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-eslint": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", @@ -5523,8 +5531,7 @@ "follow-redirects": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", - "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", - "dev": true + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" }, "for-in": { "version": "1.0.2", diff --git a/vospace-ui-frontend/package.json b/vospace-ui-frontend/package.json index 1864acb..c452bb8 100644 --- a/vospace-ui-frontend/package.json +++ b/vospace-ui-frontend/package.json @@ -8,6 +8,7 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "axios": "^0.21.0", "bootstrap-vue": "^2.19.0", "core-js": "^3.6.5", "vue": "^2.6.11", diff --git a/vospace-ui-frontend/src/App.vue b/vospace-ui-frontend/src/App.vue index 2363d6b..938c672 100644 --- a/vospace-ui-frontend/src/App.vue +++ b/vospace-ui-frontend/src/App.vue @@ -1,9 +1,48 @@ <template> <div id="app"> + <TopMenu /> + <div class="container"> + <router-view></router-view> + </div> + <div id="footer-fix"></div> + <footer class="text-center" id="site-footer"> + — Powered by <img alt="IA2 logo" src="./assets/ia2-logo-footer.png"> + <strong class="text-primary"><a href="http://www.ia2.inaf.it/" target="blank_">IA2</a></strong> — + </footer> + <div id="loading" v-if="loading"> + <div class="spinner-wrapper"> + <b-spinner variant="primary" style="width: 3rem; height: 3rem;" label="Loading"></b-spinner> + </div> + </div> </div> </template> <script> +import { mapState } from 'vuex' +import TopMenu from './components/TopMenu.vue' + +export default { + name: 'App', + computed: mapState({ + loading: state => state.loading + }), + 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> <style> @@ -13,6 +52,40 @@ -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; - margin-top: 60px; +} + +#footer-fix { + display: block; + width: 100%; + height: 100px; +} + +#site-footer { + color: #666; + border-top: 1px #e7e7e7 solid; + background-color: #f8f8f8; + padding: 5px 0; + position: fixed; + bottom: 0; + right: 0; + left: 0; +} + +#loading { + position: fixed; + top: 0; + bottom: 0; + right: 0; + left: 0; + background-color: rgba(255, 255, 255, 0.7); + z-index: 1000; +} + +.spinner-wrapper { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; } </style> diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js new file mode 100644 index 0000000..31c8eb7 --- /dev/null +++ b/vospace-ui-frontend/src/api/server/index.js @@ -0,0 +1,66 @@ +const BASE_API_URL = process.env.VUE_APP_API_BASE_URL; + +import axios from 'axios'; + +function apiRequest(options, showLoading = true, handleValidationErrors = false) { + if (showLoading) { + loading(true); + } + return new Promise((resolve, reject) => { + axios(options) + .then(response => { + if (response.status === 204) { + resolve({}); + } else { + resolve(response.data); + } + loading(false); + }) + .catch(error => { + loading(false); + if (handleValidationErrors && error.response && error.response.status === 400) { + reject(error.response.data); + } else { + dispatchApiErrorEvent(error); + } + }); + }); +} + +function dispatchApiErrorEvent(error) { + let event = new CustomEvent('apiError'); + let errorMessage; + if (error.response && error.response.data && error.response.data.message) { + errorMessage = error.response.data.message; + } else if (error.message) { + errorMessage = error.message; + } else { + errorMessage = '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 { + getNode(path) { + let url = BASE_API_URL + 'nodes' + path; + return apiRequest({ + method: 'GET', + url: url, + withCredentials: true, + headers: { + 'Cache-Control': 'no-cache' + } + }, true, true); + } +} diff --git a/vospace-ui-frontend/src/assets/ia2-logo-footer.png b/vospace-ui-frontend/src/assets/ia2-logo-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..cb913c9d1bfb1815de802febddfdec6dd2b56552 GIT binary patch literal 1446 zcmeAS@N?(olHy`uVBq!ia0y~yU{GgZU@+ofV_;yozvGZB0|NtFlDE4H!+#K5uy^@n z1_lPs0*}aI1_s{iAk65bF}s3+fq}im)7O>#0jD^x2#a;7O%?+KtFfnxV@Sl|w=<)2 zLQ(~e*MEMixqG?hiVn3dj}Jme)NXN<Zr_sReS}5sNJXdHhAn0dj+&0lTq2vk9a`$9 z$`Q6WVtbpAp;Rqnvth=flI<bRCRrvM*-VaUOKIKBD?e{;*U)l$o5Sw6LXW4<Y4?3L z=Wpt>GiU0b9r*uT$KSPBAz<U)FZbs^t4ZSS*d6F%_aS`CIfjCB>K4a0+zc)fwv3hk z_g{+l*@+GRX0Ni)-59hws^>!P+I{{{ujp^$-=6G0@5_$$okia}cW|7)eeOZfp(Ovk zsW&!VJ#v?Kd4p_eUr?^v7JaYk^BX4Y-BfO}VZ;0i`#H8d1dDVZygvS@>f4*bP0Y_X z+s{Av$p6%-pqq`egKHcfJgz%*Yu4TkO!KNl5A<iAa&0fI4olg8ZfjYO;mMS#hj;}1 zTVk(u@ApWJ^bdRR$My9(!)w(urx@%1dpVi?{K`K(nm5n*)+O&XZ-`bse%k)|EsH5R z?-s7(J>a|Ge3^UrTdVvBMa`v~9acNq_5Odm)+S$Se{_hw*y%cjgsVP&Th{+6bNHqA zIU})oDXYZdLw8(q5)zz+uHBiC&ub9dvex$EtlVZHZppcfT(6g}^y-bX`Q)(Z(X?%j z+B1C`d$qXw#pdthQ<=Q|`TphW>i-&=PRg~Iy5wx8+fy&W+B4FU5o{`FIO1!sJ-ice zIOu|P!}eW^SI=Id^-^f!(-nFuo4!9=bMPr!w))~V%ylOWSF#?LuC3e<`p{WEjXTlw zFn_qzx!^Nq2NHJtFNpqe!Smp^jS&ys?uTAnmDpbVc%NFrqCT!Qz1y$8u|1oR_d<J$ z@2XAnuJsunW)tTyvtcp19#?wu-@TnjH(x%P!|;0PJf=r#GgrxrS=4WM7v*Tqw7)7@ zKZ?cZqD+arXLd~JB~`7{4L`SRVQt^G{oI`=<@~aJOy&_KX-eCwwr6%;TNrXXBugk% z)n2euRB}eG*Um~8m;K+YoEMfDXSvxt|9A1LOaOz4TGHxr<{J&M+<y{f(*JI3zo+~n zS3SOFu@-BZ)9*iuALmG~+RZ(6yTDEH_g>RJr0)3sqv31S8^5i;wr#bVduq{^XeHrJ zGp5jMulM$~FZ(;q@cho!ua5&NC4YFY-TU6ffZ_eY2i96|Ox(;q2f6#*dh(+1ki+zk zy%{HBBD@dtxou}&94Pj6%ldzIxx$<82|dVqyzG+t@k5$-o9q}vF3Rwxvgnz=Tg|HJ zwp8<8+~&Rhhr1rVZ&)6fb@SK9r<v|YDqZteoXAtoNj%PXf58K$g}Z`Wr)k$ne&5+Y z|Kz=0WwIZ37tZ0WJ;ma`TJWU#-N#})7K)#iwdsGgNw+VU=-A|*wA#)-_p(6exv&ho zryF-JP+oJx=+yGMDR!1guN}oB)|V&6pG^EU*>PU!Hm2z-UoC!lpwI8qGylMOHa{<A ziP{<6iZA++)p+>WlQq&EJXMEyudS9|b>DEozin?Pe^e8)O;XD3|GM*jX6M~(rNI7a z+dR|X?!0s8mQzS}<2i>*Gwl_2SRQ3}n9s&3eAC)&?e){m`HQYxko@&*&Apx6+=s-~ z{+vrcAbMa+#TqrMgqxc(m*0;u_*Hy?&w72vxdqbohu-gD@L`BF)G%mx70Ia_#QUk? zve0b16$`DtKl{}lni2GL@4RoJn;lGqjpCFpmb>merZ$I3%+GV)=I5WdvOkAbu>Cl? zeB~*hM%lB~>4t|Y9{cNBe%~;yjd`BoS_g-<XDm1A|1q9v^E8-G$wX<*OKr<vE!;9E zHQCfSZ(Z1&f0f;Nqy5IXJ%T@usy{o%xJFPaZ;^@37Wr3kx$?*BpZ;M^oXGfCeG+RX Q0|Nttr>mdKI;Vst0CEAgYXATM literal 0 HcmV?d00001 diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue index 7b8b46c..482dffd 100644 --- a/vospace-ui-frontend/src/components/Main.vue +++ b/vospace-ui-frontend/src/components/Main.vue @@ -1,3 +1,27 @@ <template> - <div></div> +<div class="container"> + <b-card> + <table class="table b-table table-striped table-hover"> + <thead> + <tr> + <th>File</th> + </tr> + </thead> + <tbody id="nodes"></tbody> + </table> + </b-card> +</div> </template> + +<script> +import client from 'api-client'; + +export default { + mounted() { + client.getNode(this.$store.state.path) + .then(res => { + document.getElementById('nodes').outerHTML = res; + }); + } +} +</script> diff --git a/vospace-ui-frontend/src/components/TopMenu.vue b/vospace-ui-frontend/src/components/TopMenu.vue new file mode 100644 index 0000000..1115343 --- /dev/null +++ b/vospace-ui-frontend/src/components/TopMenu.vue @@ -0,0 +1,14 @@ +<template> +<div> + <b-navbar toggleable="lg" type="dark" id="top-menu"> + <b-navbar-brand href="#" class="d-none d-md-block">VOSpace</b-navbar-brand> + </b-navbar> +</div> +</template> + +<style> +#top-menu { + background-color: #093784; + margin-bottom: 20px; +} +</style> diff --git a/vospace-ui-frontend/src/main.js b/vospace-ui-frontend/src/main.js index 8761db3..9324cf3 100644 --- a/vospace-ui-frontend/src/main.js +++ b/vospace-ui-frontend/src/main.js @@ -1,6 +1,7 @@ import Vue from 'vue' import App from './App.vue' import store from './store.js' +import './plugins/bootstrap-vue' import { BootstrapVue } from 'bootstrap-vue' diff --git a/vospace-ui-frontend/src/plugins/bootstrap-vue.js b/vospace-ui-frontend/src/plugins/bootstrap-vue.js new file mode 100644 index 0000000..b9d414f --- /dev/null +++ b/vospace-ui-frontend/src/plugins/bootstrap-vue.js @@ -0,0 +1,7 @@ +import Vue from 'vue'; + +import { BootstrapVue } from 'bootstrap-vue'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap-vue/dist/bootstrap-vue.css'; + +Vue.use(BootstrapVue); diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js index 4eadaf1..464672b 100644 --- a/vospace-ui-frontend/src/store.js +++ b/vospace-ui-frontend/src/store.js @@ -7,9 +7,13 @@ Vue.use(Vuex); export default new Vuex.Store({ state: { + path: '/', + loading: true }, mutations: { + setLoading(state, loading) { + state.loading = loading; + } }, - actions: { - } + actions: {} }); diff --git a/vospace-ui-frontend/vue.config.js b/vospace-ui-frontend/vue.config.js new file mode 100644 index 0000000..93ef4cb --- /dev/null +++ b/vospace-ui-frontend/vue.config.js @@ -0,0 +1,12 @@ +const path = require('path'); + +module.exports = { + publicPath: '', + chainWebpack: config => { + const apiClient = process.env.VUE_APP_API_CLIENT // mock or server + config.resolve.alias.set( + 'api-client', + path.resolve(__dirname, `src/api/${apiClient}`) + ) + } +} -- GitLab