updating jhipster endpoints for REST APIs
This commit is contained in:
parent
affff0e800
commit
24f9d8ec19
|
@ -18,6 +18,7 @@
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="test" value="true"/>
|
<attribute name="test" value="true"/>
|
||||||
<attribute name="maven.pomderived" value="true"/>
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||||
|
@ -32,18 +33,12 @@
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
<classpathentry kind="src" path="target/generated-sources/annotations">
|
<classpathentry kind="src" path="target/generated-sources/annotations">
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="ignore_optional_problems" value="true"/>
|
|
||||||
<attribute name="optional" value="true"/>
|
<attribute name="optional" value="true"/>
|
||||||
<attribute name="maven.pomderived" value="true"/>
|
|
||||||
<attribute name="m2e-apt" value="true"/>
|
|
||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
|
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="optional" value="true"/>
|
<attribute name="optional" value="true"/>
|
||||||
<attribute name="maven.pomderived" value="true"/>
|
|
||||||
<attribute name="ignore_optional_problems" value="true"/>
|
|
||||||
<attribute name="m2e-apt" value="true"/>
|
|
||||||
<attribute name="test" value="true"/>
|
<attribute name="test" value="true"/>
|
||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
eclipse.preferences.version=1
|
eclipse.preferences.version=1
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
|
||||||
org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
|
org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
||||||
org.eclipse.jdt.core.compiler.compliance=11
|
org.eclipse.jdt.core.compiler.compliance=11
|
||||||
|
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
|
||||||
|
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
||||||
|
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||||
|
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||||
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
|
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
|
||||||
|
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||||
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
|
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
|
||||||
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
|
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
|
||||||
org.eclipse.jdt.core.compiler.processAnnotations=enabled
|
org.eclipse.jdt.core.compiler.processAnnotations=enabled
|
||||||
org.eclipse.jdt.core.compiler.release=disabled
|
org.eclipse.jdt.core.compiler.release=disabled
|
||||||
org.eclipse.jdt.core.compiler.source=11
|
org.eclipse.jdt.core.compiler.source=11
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"bootstrap-icons": "^1.11.1",
|
"bootstrap-icons": "^1.11.1",
|
||||||
"cytoscape": "3.25.0",
|
"cytoscape": "3.25.0",
|
||||||
"dayjs": "1.11.5",
|
"dayjs": "1.11.5",
|
||||||
|
"mat-select-filter": "^2.4.1",
|
||||||
"material-icons-font": "^2.1.0",
|
"material-icons-font": "^2.1.0",
|
||||||
"mdb-angular-ui-kit": "^3.0.1",
|
"mdb-angular-ui-kit": "^3.0.1",
|
||||||
"ngx-infinite-scroll": "14.0.0",
|
"ngx-infinite-scroll": "14.0.0",
|
||||||
|
@ -17774,6 +17775,18 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mat-select-filter": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mat-select-filter/-/mat-select-filter-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-8MhkuFlWNACOGdR2F/YskeeZ17U1ym4dXIfyfewotjMO9KZ+t9M++WKUD6pZ6n/0EpSx24CvsEeTL79eOtiJiQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">=9.0.0 <14.0.0",
|
||||||
|
"@angular/core": ">=9.0.0 <14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/material-icons-font": {
|
"node_modules/material-icons-font": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/material-icons-font/-/material-icons-font-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/material-icons-font/-/material-icons-font-2.1.0.tgz",
|
||||||
|
@ -39165,6 +39178,14 @@
|
||||||
"p-defer": "^1.0.0"
|
"p-defer": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mat-select-filter": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mat-select-filter/-/mat-select-filter-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-8MhkuFlWNACOGdR2F/YskeeZ17U1ym4dXIfyfewotjMO9KZ+t9M++WKUD6pZ6n/0EpSx24CvsEeTL79eOtiJiQ==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"material-icons-font": {
|
"material-icons-font": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/material-icons-font/-/material-icons-font-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/material-icons-font/-/material-icons-font-2.1.0.tgz",
|
||||||
|
|
|
@ -96,6 +96,7 @@
|
||||||
"bootstrap-icons": "^1.11.1",
|
"bootstrap-icons": "^1.11.1",
|
||||||
"cytoscape": "3.25.0",
|
"cytoscape": "3.25.0",
|
||||||
"dayjs": "1.11.5",
|
"dayjs": "1.11.5",
|
||||||
|
"mat-select-filter": "^2.4.1",
|
||||||
"material-icons-font": "^2.1.0",
|
"material-icons-font": "^2.1.0",
|
||||||
"mdb-angular-ui-kit": "^3.0.1",
|
"mdb-angular-ui-kit": "^3.0.1",
|
||||||
"ngx-infinite-scroll": "14.0.0",
|
"ngx-infinite-scroll": "14.0.0",
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.gcube.fullstackapps.informationsystemmonitor.service;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
@ -9,6 +10,9 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetails;
|
import org.springframework.security.web.authentication.WebAuthenticationDetails;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
/*
|
||||||
|
* Servizio per layer jhipster
|
||||||
|
*/
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ContextService {
|
public class ContextService {
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
package org.gcube.fullstackapps.informationsystemmonitor.service;
|
package org.gcube.fullstackapps.informationsystemmonitor.service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
import org.gcube.common.authorization.utils.manager.SecretManager;
|
import org.gcube.common.authorization.utils.manager.SecretManager;
|
||||||
import org.gcube.common.authorization.utils.manager.SecretManagerProvider;
|
import org.gcube.common.authorization.utils.manager.SecretManagerProvider;
|
||||||
import org.gcube.common.authorization.utils.secret.JWTSecret;
|
import org.gcube.common.authorization.utils.secret.JWTSecret;
|
||||||
import org.gcube.common.authorization.utils.secret.Secret;
|
import org.gcube.common.authorization.utils.secret.Secret;
|
||||||
import org.gcube.informationsystem.contexts.reference.entities.Context;
|
import org.gcube.informationsystem.contexts.reference.entities.Context;
|
||||||
|
import org.gcube.informationsystem.model.reference.entities.Resource;
|
||||||
|
import org.gcube.informationsystem.resourceregistry.api.exceptions.entities.resource.ResourceAvailableInAnotherContextException;
|
||||||
import org.gcube.informationsystem.resourceregistry.client.ResourceRegistryClient;
|
import org.gcube.informationsystem.resourceregistry.client.ResourceRegistryClient;
|
||||||
import org.gcube.informationsystem.resourceregistry.client.ResourceRegistryClientFactory;
|
import org.gcube.informationsystem.resourceregistry.client.ResourceRegistryClientFactory;
|
||||||
|
import org.gcube.informationsystem.serialization.ElementMapper;
|
||||||
|
import org.gcube.informationsystem.types.reference.Type;
|
||||||
|
import org.gcube.informationsystem.types.reference.entities.ResourceType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
@ -37,23 +43,53 @@ public class InformationSystemService {
|
||||||
public List<Context> getAllContext(String rootCtx) throws Exception {
|
public List<Context> getAllContext(String rootCtx) throws Exception {
|
||||||
log.debug("GetAllContext: [rootCtx=]",rootCtx);
|
log.debug("GetAllContext: [rootCtx=]",rootCtx);
|
||||||
ResourceRegistryClient resourceRegistryClient= ResourceRegistryClientFactory.create(rootCtx);
|
ResourceRegistryClient resourceRegistryClient= ResourceRegistryClientFactory.create(rootCtx);
|
||||||
//List<Context> contexts=new ArrayList<>();
|
|
||||||
//contexts.add(new ContextImpl("/gcube"));
|
|
||||||
List<Context>contexts=resourceRegistryClient.getAllContext();
|
List<Context>contexts=resourceRegistryClient.getAllContext();
|
||||||
log.debug("AllContext: {}",contexts);
|
log.debug("AllContext: {}",contexts);
|
||||||
return contexts;
|
return contexts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Context> testResources(String rootCtx) throws Exception {
|
/*
|
||||||
log.debug("GetAllContext: [rootCtx=]",rootCtx);
|
* per prendere tutti i tipi (albero a sin)
|
||||||
ResourceRegistryClient resourceRegistryClient= ResourceRegistryClientFactory.create(rootCtx);
|
*/
|
||||||
//TODO: cosa ritornano questi metodi?
|
public List<Type> getResourceTypes() throws Exception {
|
||||||
//resourceRegistryClient.getFilteredResources(null, null, null, false, null);
|
|
||||||
//resourceRegistryClient.getFilteredResources(rootCtx, rootCtx, rootCtx, false, null)
|
String currentCtx = SecretManagerProvider.instance.get().getContext();
|
||||||
List<Context>contexts=resourceRegistryClient.getAllContext();
|
log.debug("getResourceTypes : [currentCtx=]",currentCtx);
|
||||||
log.debug("AllContext: {}",contexts);
|
ResourceRegistryClient resourceRegistryClient= ResourceRegistryClientFactory.create(currentCtx);
|
||||||
return contexts;
|
List<Type> types = resourceRegistryClient.getType(Resource.class, true);
|
||||||
}
|
/*
|
||||||
|
for(Type t : types) {
|
||||||
|
ResourceType rt = (ResourceType) t;
|
||||||
|
rt.getExtendedTypes();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
log.debug("getResourceTypes:",types);
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* per prendere tutti le istanze di un certo tipo (per riempire la tabella)
|
||||||
|
*/
|
||||||
|
//TODO: per ora fare EService e HostingNode
|
||||||
|
public List<Resource> getResourceInstances(String resourceType) throws Exception {
|
||||||
|
String currentCtx = SecretManagerProvider.instance.get().getContext();
|
||||||
|
List<Resource> instancesAsObject;
|
||||||
|
log.debug("getResourceInstances : [currentCtx=]",currentCtx);
|
||||||
|
ResourceRegistryClient resourceRegistryClient= ResourceRegistryClientFactory.create(currentCtx);
|
||||||
|
String instances = resourceRegistryClient.getInstances(resourceType, false);
|
||||||
|
instancesAsObject = ElementMapper.unmarshalList(Resource.class, instances);
|
||||||
|
for(Resource r : instancesAsObject) {
|
||||||
|
r.getFacets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
for(Type t : types) {
|
||||||
|
ResourceType rt = (ResourceType) t;
|
||||||
|
rt.getExtendedTypes();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
return instancesAsObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.gcube.fullstackapps.informationsystemmonitor.service.dto;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class ResourceTypeDTO {
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
private ResourceTypeDTO[] children;
|
||||||
|
}
|
|
@ -58,12 +58,10 @@ public class ContextResource {
|
||||||
|
|
||||||
model.addAttribute("name", userAttributes.get("name"));
|
model.addAttribute("name", userAttributes.get("name"));
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return contextService.all();
|
return contextService.all();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/encode")
|
@GetMapping("/encode")
|
||||||
public String encodeContext() {
|
public String encodeContext() {
|
||||||
String contextEncoded=UriUtils.encode("/gcube", "UTF-8");
|
String contextEncoded=UriUtils.encode("/gcube", "UTF-8");
|
||||||
|
|
|
@ -1,20 +1,30 @@
|
||||||
package org.gcube.fullstackapps.informationsystemmonitor.web.rest;
|
package org.gcube.fullstackapps.informationsystemmonitor.web.rest;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import org.gcube.com.fasterxml.jackson.databind.ObjectMapper;
|
import org.gcube.com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.gcube.fullstackapps.informationsystemmonitor.config.TokenManager;
|
import org.gcube.fullstackapps.informationsystemmonitor.config.TokenManager;
|
||||||
import org.gcube.fullstackapps.informationsystemmonitor.service.InformationSystemService;
|
import org.gcube.fullstackapps.informationsystemmonitor.service.InformationSystemService;
|
||||||
|
import org.gcube.fullstackapps.informationsystemmonitor.service.dto.ResourceTypeDTO;
|
||||||
import org.gcube.informationsystem.contexts.reference.entities.Context;
|
import org.gcube.informationsystem.contexts.reference.entities.Context;
|
||||||
|
import org.gcube.informationsystem.model.reference.entities.Resource;
|
||||||
|
import org.gcube.informationsystem.serialization.ElementMapper;
|
||||||
|
import org.gcube.informationsystem.types.reference.Type;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.util.UriUtils;
|
import org.springframework.web.util.UriUtils;
|
||||||
|
|
||||||
|
import io.swagger.v3.core.util.Json;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import tech.jhipster.web.util.HeaderUtil;
|
import tech.jhipster.web.util.HeaderUtil;
|
||||||
|
|
||||||
|
@ -33,22 +43,28 @@ public class InformationSystemResource {
|
||||||
|
|
||||||
private final TokenManager tokenManager;
|
private final TokenManager tokenManager;
|
||||||
private final InformationSystemService informationSystemService;
|
private final InformationSystemService informationSystemService;
|
||||||
|
|
||||||
@GetMapping("/allcontext")
|
private String createUmaToken(String currentContext) {
|
||||||
public ResponseEntity<String> allcontext() {
|
|
||||||
log.debug("Request uma token");
|
log.debug("Request uma token");
|
||||||
String rootContext = tokenManager.getRootContext();
|
if(currentContext==null || currentContext.isBlank()) {
|
||||||
String contextEncoded = UriUtils.encode(rootContext, "UTF-8");
|
currentContext = tokenManager.getRootContext();
|
||||||
String umaToken = tokenManager.getUmaToken(contextEncoded);
|
}
|
||||||
|
String umaToken = tokenManager.getUmaToken(UriUtils.encode(currentContext, "UTF-8"));
|
||||||
|
return umaToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/allcontext")
|
||||||
|
public ResponseEntity<String> allContext(@RequestParam String currentContext) {
|
||||||
try {
|
try {
|
||||||
informationSystemService.setUma(umaToken);
|
informationSystemService.setUma(createUmaToken(currentContext));
|
||||||
List<Context> contexts = informationSystemService.getAllContext(rootContext);
|
List<Context> contexts = informationSystemService.getAllContext(currentContext);
|
||||||
ObjectMapper om = new ObjectMapper();
|
/*
|
||||||
String sc = om.writeValueAsString(contexts);
|
for(Context c : contexts) {
|
||||||
|
c.getID();
|
||||||
|
c.getName();
|
||||||
|
}*/
|
||||||
|
String sc = ElementMapper.marshal(contexts);
|
||||||
return ResponseEntity.ok().body(sc);
|
return ResponseEntity.ok().body(sc);
|
||||||
|
|
||||||
//return new ResponseEntity<List<Context>>(contexts, HttpStatus.OK);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(e.getLocalizedMessage(), e);
|
log.error(e.getLocalizedMessage(), e);
|
||||||
return ResponseEntity.noContent()
|
return ResponseEntity.noContent()
|
||||||
|
@ -56,5 +72,78 @@ public class InformationSystemResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/resourcetypes")
|
||||||
|
public ResponseEntity<String> resourceTypes(@RequestParam @Nullable String currentContext) {
|
||||||
|
log.debug("******Request resource types");
|
||||||
|
List<ResourceTypeDTO> typeDtos = new ArrayList<ResourceTypeDTO>();
|
||||||
|
try {
|
||||||
|
informationSystemService.setUma(createUmaToken(currentContext));
|
||||||
|
List<Type> resTypes = informationSystemService.getResourceTypes();
|
||||||
|
Iterator<Type> it = resTypes.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Type t = it.next();
|
||||||
|
//at the moment, an empty array
|
||||||
|
ResourceTypeDTO[] children = new ResourceTypeDTO[0];
|
||||||
|
ResourceTypeDTO dto = new ResourceTypeDTO(t.getID().toString(),t.getName(),children);
|
||||||
|
typeDtos.add(dto);
|
||||||
|
}
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
String sc = objectMapper.writeValueAsString(typeDtos);
|
||||||
|
return ResponseEntity.ok().body(sc);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getLocalizedMessage(), e);
|
||||||
|
return ResponseEntity.noContent()
|
||||||
|
.headers(HeaderUtil.createAlert(applicationName, e.getLocalizedMessage(), "")).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
@GetMapping("/resourcetypes")
|
||||||
|
public ResponseEntity<String> resourceTypes2(@RequestParam String currentContext) {
|
||||||
|
log.debug("Request resource types");
|
||||||
|
|
||||||
|
try {
|
||||||
|
informationSystemService.setUma(createUmaToken(currentContext));
|
||||||
|
List<Type> resTypes = informationSystemService.getResourceTypes();
|
||||||
|
String sc = ElementMapper.marshal(resTypes);
|
||||||
|
return ResponseEntity.ok().body(sc);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getLocalizedMessage(), e);
|
||||||
|
return ResponseEntity.noContent()
|
||||||
|
.headers(HeaderUtil.createAlert(applicationName, e.getLocalizedMessage(), "")).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ritorna le istanze delle risorse di un certo tipo
|
||||||
|
*/
|
||||||
|
@GetMapping("/resourceinstances")
|
||||||
|
public ResponseEntity<String> resourceInstances(@RequestParam String currentContext, @RequestParam String resourceType) {
|
||||||
|
log.debug("Request resource types");
|
||||||
|
|
||||||
|
try {
|
||||||
|
informationSystemService.setUma(createUmaToken(currentContext));
|
||||||
|
List<Resource> resIntances = informationSystemService.getResourceInstances(resourceType);
|
||||||
|
/*
|
||||||
|
for(Type t : resTypes) {
|
||||||
|
t.getID();
|
||||||
|
t.getName();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
informationSystemService.setUma(umaToken);
|
||||||
|
List<Type> resTypes = informationSystemService.getResourceTypes(currentContext);
|
||||||
|
*/
|
||||||
|
|
||||||
|
String sc = ElementMapper.marshal(resIntances);
|
||||||
|
return ResponseEntity.ok().body(sc);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getLocalizedMessage(), e);
|
||||||
|
return ResponseEntity.noContent()
|
||||||
|
.headers(HeaderUtil.createAlert(applicationName, e.getLocalizedMessage(), "")).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { NgModule, LOCALE_ID } from '@angular/core';
|
import { NgModule, LOCALE_ID, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { registerLocaleData } from '@angular/common';
|
import { registerLocaleData } from '@angular/common';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import locale from '@angular/common/locales/en';
|
import locale from '@angular/common/locales/en';
|
||||||
|
@ -47,7 +47,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
NgbModule,
|
NgbModule,
|
||||||
RawjsonPaneComponent,
|
RawjsonPaneComponent,
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
BrowserAnimationsModule
|
BrowserAnimationsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
Title,
|
Title,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export class appProperties {
|
export class appProperties {
|
||||||
|
|
||||||
public static BASEURL_API = "http://localhost:8080/";
|
public static BASEURL_API = "http://localhost:9000";
|
||||||
|
|
||||||
}
|
}
|
|
@ -31,13 +31,9 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="d-inline-block my-3" ngbDropdown #myDrop="ngbDropdown" >
|
<div class="d-inline-block my-3" ngbDropdown #myDrop="ngbDropdown" >
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="col-md-9 mt-2">
|
<div class="col-md-9 mt-2">
|
||||||
<form [formGroup]="chooseContextForm">
|
<form [formGroup]="chooseContextForm">
|
||||||
<mat-form-field appearance="outline" class="form-field">
|
<mat-form-field appearance="outline" class="form-field">
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Account } from 'app/core/auth/account.model';
|
||||||
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { Clipboard } from '@angular/cdk/clipboard'
|
import { Clipboard } from '@angular/cdk/clipboard'
|
||||||
import { Observable, map, startWith } from 'rxjs';
|
import { Observable, map, startWith } from 'rxjs';
|
||||||
import { IContextNode } from 'app/services/i-context-node';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'jhi-home',
|
selector: 'jhi-home',
|
||||||
|
@ -114,7 +114,6 @@ export class HomeComponent implements OnInit {
|
||||||
return tmp;
|
return tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getContexts(): string[] {
|
getContexts(): string[] {
|
||||||
if (this.account != null && this.account.resourceAccessDTO != null) {
|
if (this.account != null && this.account.resourceAccessDTO != null) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { RawjsonPaneComponent } from 'app/rawjson-pane/rawjson-pane.component';
|
||||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MatSelectFilterModule } from 'mat-select-filter';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule,
|
imports: [SharedModule,
|
||||||
|
@ -21,7 +22,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
RawjsonPaneComponent,
|
RawjsonPaneComponent,
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
BrowserAnimationsModule
|
BrowserAnimationsModule,
|
||||||
|
MatSelectFilterModule
|
||||||
],
|
],
|
||||||
declarations: [HomeComponent],
|
declarations: [HomeComponent],
|
||||||
schemas:[CUSTOM_ELEMENTS_SCHEMA]
|
schemas:[CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface ITabbedEntity {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
//prevediamo contenuti di tipo diverso (quindi switch al momento della costruzione dei tab)
|
||||||
|
type: number;
|
||||||
|
}
|
|
@ -2,26 +2,33 @@
|
||||||
import { Component , OnInit } from '@angular/core';
|
import { Component , OnInit } from '@angular/core';
|
||||||
import { MatTreeNestedDataSource } from '@angular/material/tree';
|
import { MatTreeNestedDataSource } from '@angular/material/tree';
|
||||||
import { NestedTreeControl } from '@angular/cdk/tree';
|
import { NestedTreeControl } from '@angular/cdk/tree';
|
||||||
//import { IContextNode } from 'app/services/i-context-node';
|
|
||||||
import { RESOURCES } from 'app/services/restypes';
|
|
||||||
import { IResource } from 'app/services/i-resource';
|
import { IResource } from 'app/services/i-resource';
|
||||||
|
import { RestypesService } from 'app/services/restypes.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'jhi-rsc-tree',
|
selector: 'jhi-rsc-tree',
|
||||||
templateUrl: './rsc-tree.component.html',
|
templateUrl: './rsc-tree.component.html',
|
||||||
styleUrls: ['./rsc-tree.component.scss'],
|
styleUrls: ['./rsc-tree.component.scss'],
|
||||||
|
providers: [RestypesService],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class RscTreeComponent{
|
export class RscTreeComponent implements OnInit{
|
||||||
nestedTreeControl = new NestedTreeControl<IResource>(node => node.children);
|
nestedTreeControl = new NestedTreeControl<IResource>(node => node.children);
|
||||||
nestedDataSource = new MatTreeNestedDataSource<IResource>();
|
nestedDataSource = new MatTreeNestedDataSource<IResource>();
|
||||||
//@Output() treeNode = new EventEmitter<IResource>(); //emitting event to parent
|
|
||||||
|
|
||||||
|
constructor(private rtService:RestypesService) {
|
||||||
constructor() {
|
|
||||||
this.nestedDataSource.data = RESOURCES;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.rtService.fetchAll().subscribe(res => {
|
||||||
|
this.nestedDataSource.data = res;
|
||||||
|
console.debug("*****");
|
||||||
|
console.debug(JSON.stringify(res));
|
||||||
|
console.debug("*****");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
hasNestedChild(_: number, node: IResource): boolean {
|
hasNestedChild(_: number, node: IResource): boolean {
|
||||||
if (node.children == null) {
|
if (node.children == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
import { Component , OnInit } from '@angular/core';
|
||||||
|
import { MatTreeNestedDataSource } from '@angular/material/tree';
|
||||||
|
import { NestedTreeControl } from '@angular/cdk/tree';
|
||||||
|
//import { IContextNode } from 'app/services/i-context-node';
|
||||||
|
import { RESOURCES } from 'app/services/restypes';
|
||||||
|
import { IResource } from 'app/services/i-resource';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'jhi-rsc-tree',
|
||||||
|
templateUrl: './rsc-tree.component.html',
|
||||||
|
styleUrls: ['./rsc-tree.component.scss'],
|
||||||
|
})
|
||||||
|
|
||||||
|
export class RscTreeComponent{
|
||||||
|
nestedTreeControl = new NestedTreeControl<IResource>(node => node.children);
|
||||||
|
nestedDataSource = new MatTreeNestedDataSource<IResource>();
|
||||||
|
//@Output() treeNode = new EventEmitter<IResource>(); //emitting event to parent
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.nestedDataSource.data = RESOURCES;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNestedChild(_: number, node: IResource): boolean {
|
||||||
|
if (node.children == null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return node.children.length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTable(choosenResType:IResource):boolean{
|
||||||
|
//TODO: qui va chiamato il componente hnodes-table e conseguentemente visualizzato
|
||||||
|
//this.showTable = true;
|
||||||
|
//return this.showTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { Observable, map, tap } from 'rxjs';
|
import { Observable, map, tap } from 'rxjs';
|
||||||
import { appProperties } from 'app/config/app-properties';
|
import { appProperties } from 'app/config/app-properties';
|
||||||
import { IContextNode } from './i-context-node';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
@ -13,15 +12,10 @@ export class ContextsLoaderService {
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
/*
|
|
||||||
getData(): Observable<IContextNode[]> {
|
|
||||||
//TODO: pipe per gestione errori
|
|
||||||
return this.http.get<IContextNode[]>(appProperties.MOCK_BASEURL_API + '/allcontext');
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
getRawData(): Observable<string> {
|
getRawData(): Observable<string> {
|
||||||
//TODO: pipe per gestione errori
|
//TODO: pipe per gestione errori
|
||||||
return this.http.get<string>(appProperties.BASEURL_API + 'is/allcontext');
|
return this.http.get<string>(appProperties.BASEURL_API + 'is/resourcetypes');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export interface IResource {
|
export interface IResource {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
type: number; //TODO: codice da assegnare in constants per ogni tipo di risorsa
|
//type: number; //TODO: codice da assegnare in constants per ogni tipo di risorsa
|
||||||
children?: IResource[];
|
children?: IResource[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,26 +14,25 @@ export class ResourcesLoaderService {
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
//TODO: a regime questo metodo prende un parametro nella get
|
|
||||||
|
/* metodi mock
|
||||||
|
//TODO: pipe per gestione errori
|
||||||
|
*/
|
||||||
|
|
||||||
getHostingNodes(): Observable<IHostingnode[]> {
|
getHostingNodes(): Observable<IHostingnode[]> {
|
||||||
//TODO: pipe per gestione errori
|
|
||||||
return this.http.get<IHostingnode[]>('http://localhost:3002/is/hostingnodes/all');
|
return this.http.get<IHostingnode[]>('http://localhost:3002/is/hostingnodes/all');
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourcesByContext(): Observable<IResource[]> {
|
getResourcesByContext(): Observable<IResource[]> {
|
||||||
//TODO: pipe per gestione errori
|
|
||||||
return this.http.get<IResource[]>('http://localhost:3002/is/ctxresources');
|
return this.http.get<IResource[]>('http://localhost:3002/is/ctxresources');
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: a regime questo metodo prende un parametro nella get
|
|
||||||
//(e probabilmente anche un tipo per fare uno switch sul tipo di risorsa da prendere)
|
|
||||||
getJsonDetail(uid: string): Observable<string> {
|
getJsonDetail(uid: string): Observable<string> {
|
||||||
//TODO: pipe per gestione errori
|
|
||||||
return this.http.get<string>('http://localhost:3002/is/hostingnodes/detail');
|
return this.http.get<string>('http://localhost:3002/is/hostingnodes/detail');
|
||||||
}
|
}
|
||||||
|
|
||||||
getHostingNodeDetail(): Observable<string> {
|
getHostingNodeDetail(): Observable<string> {
|
||||||
//TODO: pipe per gestione errori
|
|
||||||
return this.http.get<string>('http://localhost:3002/is/hostingnodes/detail');
|
return this.http.get<string>('http://localhost:3002/is/hostingnodes/detail');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { IResource} from './i-resource';
|
||||||
|
import { appProperties } from 'app/config/app-properties';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
|
||||||
|
export class RestypesService {
|
||||||
|
httpOptions = {
|
||||||
|
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
//TODO: pipe per gestione errori
|
||||||
|
|
||||||
|
fetchAll(): Observable<IResource[]> {
|
||||||
|
return this.http.get<IResource[]>(appProperties.BASEURL_API+'/is/resourcetypes');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -20,6 +20,7 @@
|
||||||
<th scope="col" sortable="lastmod" (sort)="onSort($event)">Last Modified</th>
|
<th scope="col" sortable="lastmod" (sort)="onSort($event)">Last Modified</th>
|
||||||
<th scope="col" sortable="memavailable">Available Memory</th>
|
<th scope="col" sortable="memavailable">Available Memory</th>
|
||||||
<th scope="col" sortable="hdspace" (sort)="onSort($event)">HD Space</th>
|
<th scope="col" sortable="hdspace" (sort)="onSort($event)">HD Space</th>
|
||||||
|
<th scope="col" ></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
<td><ngb-highlight [result]="hnode.lastmod"></ngb-highlight></td>
|
<td><ngb-highlight [result]="hnode.lastmod"></ngb-highlight></td>
|
||||||
<td><ngb-highlight [result]="hnode.memavailable"></ngb-highlight></td>
|
<td><ngb-highlight [result]="hnode.memavailable"></ngb-highlight></td>
|
||||||
<td><ngb-highlight [result]="hnode.hdspace"></ngb-highlight></td>
|
<td><ngb-highlight [result]="hnode.hdspace"></ngb-highlight></td>
|
||||||
|
<td><button mat-button color="primary" (click)="addTab(hnode.id)"><mat-icon>visibility</mat-icon></button></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { Component, QueryList, ViewChildren } from '@angular/core';
|
import { Component, QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||||
import { IHostingnode } from 'app/services/i-hostinngnode';
|
import { IHostingnode } from 'app/services/i-hostinngnode';
|
||||||
import { HnodesLoaderService } from 'app/services/hnodes-loader.service';
|
import { HnodesLoaderService } from 'app/services/hnodes-loader.service';
|
||||||
import { DecimalPipe, NgIf } from '@angular/common';
|
import { DecimalPipe, NgIf } from '@angular/common';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { SortableDirective, SortEvent } from './sortable.directive';
|
import { SortableDirective, SortEvent } from './sortable.directive';
|
||||||
|
import { MatTab, MatTabGroup } from '@angular/material/tabs';
|
||||||
|
import { ITabbedEntity } from 'app/i-tabbed-entity';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Tabella con paginazione, ordinamento e ricerca - dati presi da servizio
|
Tabella con paginazione, ordinamento e ricerca - dati presi da servizio
|
||||||
|
@ -18,9 +20,22 @@ Tabella con paginazione, ordinamento e ricerca - dati presi da servizio
|
||||||
|
|
||||||
})
|
})
|
||||||
export class TableNodesComponent{
|
export class TableNodesComponent{
|
||||||
|
|
||||||
|
//per tabbed view
|
||||||
|
@ViewChild(MatTab)
|
||||||
|
public tabGroup: MatTabGroup | any;
|
||||||
|
public tabNodes: QueryList<MatTab> | any;
|
||||||
|
public closedTabs = [];
|
||||||
|
public tabs: ITabbedEntity[] = [{ title: 'JSON View', content: '', type: 0, id: '' }];
|
||||||
|
selectedIdx = 0;
|
||||||
|
chosenIds: string[] = [];
|
||||||
|
////////// fine tabbed view(MatTab)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
hnodes$: Observable<IHostingnode[]>;
|
hnodes$: Observable<IHostingnode[]>;
|
||||||
total$: Observable<number>;
|
total$: Observable<number>;
|
||||||
|
|
||||||
|
|
||||||
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>|any;
|
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>|any;
|
||||||
|
|
||||||
|
@ -41,5 +56,50 @@ export class TableNodesComponent{
|
||||||
this.service.sortColumn = column;
|
this.service.sortColumn = column;
|
||||||
this.service.sortDirection = direction;
|
this.service.sortDirection = direction;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
addTab(id:string):void{
|
||||||
|
alert("id?..."+id);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// per tabbed pane (versione con aggiunta dinamica)
|
||||||
|
|
||||||
|
removeTab(index: number): void {
|
||||||
|
this.tabs.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
addTab(itemId: string): void {
|
||||||
|
if (!this.chosenIds.includes(itemId)) {
|
||||||
|
const newItem = {
|
||||||
|
id: itemId,
|
||||||
|
title: itemId.substring(0, 20) + '...',
|
||||||
|
//TODO: content a regime è la stringa JSON
|
||||||
|
content: itemId,
|
||||||
|
type: 0,
|
||||||
|
};
|
||||||
|
this.selectedIdx++;
|
||||||
|
this.chosenIds.push(itemId);
|
||||||
|
this.tabs.push(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTab(index: number): void {
|
||||||
|
/*
|
||||||
|
console.debug('---------');
|
||||||
|
console.debug(index);
|
||||||
|
console.debug('---IDs:');
|
||||||
|
console.debug(this.chosenIds);
|
||||||
|
console.debug(this.tabs[index].id);
|
||||||
|
console.debug('++++++++++');
|
||||||
|
*/
|
||||||
|
const x = this.chosenIds.indexOf(this.tabs[index].id);
|
||||||
|
if (x !== -1) {
|
||||||
|
this.chosenIds.splice(x, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this.closedTabs.push(index);
|
||||||
|
this.tabGroup.selectedIndex = this.tabs.length - 1;
|
||||||
|
//this.chosenIds.indexOf();
|
||||||
|
this.tabs.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue