diff --git a/extensions/smallrye-health/deployment/pom.xml b/extensions/smallrye-health/deployment/pom.xml index e3a17ebe3cacd..50ba3ea09d60b 100644 --- a/extensions/smallrye-health/deployment/pom.xml +++ b/extensions/smallrye-health/deployment/pom.xml @@ -66,6 +66,16 @@ quarkus-smallrye-openapi-deployment test + + io.quarkus + quarkus-security-test-utils + test + + + io.vertx + vertx-web-client + test + diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/PathHandler.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/PathHandler.java new file mode 100644 index 0000000000000..b49463e2c0fa1 --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/PathHandler.java @@ -0,0 +1,29 @@ +package io.quarkus.smallrye.health.test; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class PathHandler { + + public void setup(@Observes Router router) { + router.route().handler(new Handler() { + @Override + public void handle(RoutingContext event) { + QuarkusHttpUser user = (QuarkusHttpUser) event.user(); + StringBuilder ret = new StringBuilder(); + if (user != null) { + ret.append(user.getSecurityIdentity().getPrincipal().getName()); + } + ret.append(":"); + ret.append(event.normalizedPath()); + event.response().end(ret.toString()); + } + }); + } +} diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/PathMatchingHttpSecurityPolicyTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/PathMatchingHttpSecurityPolicyTest.java new file mode 100644 index 0000000000000..6d0ba5b8185e4 --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/PathMatchingHttpSecurityPolicyTest.java @@ -0,0 +1,101 @@ +package io.quarkus.smallrye.health.test; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URL; +import java.time.Duration; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.vertx.core.Vertx; +import io.vertx.ext.web.client.WebClient; + +public class PathMatchingHttpSecurityPolicyTest { + + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(20); + private static final String APP_PROPS = """ + quarkus.http.auth.permission.management.paths=/q/* + quarkus.http.auth.permission.management.policy=authenticated + """; + private static WebClient client; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestIdentityController.class, TestIdentityProvider.class, PathHandler.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles() + .add("test", "test", "test"); + } + + @AfterAll + public static void cleanup() { + if (client != null) { + client.close(); + } + } + + @Inject + Vertx vertx; + + @TestHTTPResource + URL url; + + private WebClient getClient() { + if (client == null) { + client = WebClient.create(vertx); + } + return client; + } + + @ParameterizedTest + @ValueSource(strings = { + "/q/health", "/q/health/live", "/q/health/ready", "//q/health", "///q/health", "///q///health", + "/q/health/", "/q///health/", "/q///health////live" + }) + public void testHealthCheckPaths(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, "UP"); + } + + private void assurePath(String path, int expectedStatusCode) { + assurePath(path, expectedStatusCode, null, null, null); + } + + private void assurePathAuthenticated(String path, String body) { + assurePath(path, 200, body, "test", null); + } + + private void assurePath(String path, int expectedStatusCode, String body, String auth, String header) { + var req = getClient().get(url.getPort(), url.getHost(), path); + if (auth != null) { + req.basicAuthentication(auth, auth); + } + if (header != null) { + req.putHeader(header, header); + } + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + + if (body != null) { + Assertions.assertTrue(result.result().bodyAsString().contains(body), path); + } + } +} diff --git a/extensions/smallrye-openapi/deployment/pom.xml b/extensions/smallrye-openapi/deployment/pom.xml index 944f0c1f4986e..d7dba9bd236c3 100644 --- a/extensions/smallrye-openapi/deployment/pom.xml +++ b/extensions/smallrye-openapi/deployment/pom.xml @@ -77,6 +77,16 @@ quarkus-reactive-routes-deployment test + + io.quarkus + quarkus-security-test-utils + test + + + io.vertx + vertx-web-client + test + io.quarkus quarkus-junit5-internal diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/PathHandler.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/PathHandler.java new file mode 100644 index 0000000000000..9d956f0fa4810 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/PathHandler.java @@ -0,0 +1,29 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class PathHandler { + + public void setup(@Observes Router router) { + router.route().handler(new Handler() { + @Override + public void handle(RoutingContext event) { + QuarkusHttpUser user = (QuarkusHttpUser) event.user(); + StringBuilder ret = new StringBuilder(); + if (user != null) { + ret.append(user.getSecurityIdentity().getPrincipal().getName()); + } + ret.append(":"); + ret.append(event.normalizedPath()); + event.response().end(ret.toString()); + } + }); + } +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/PathMatchingHttpSecurityPolicyTest.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/PathMatchingHttpSecurityPolicyTest.java new file mode 100644 index 0000000000000..4cfa8265e3da5 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/PathMatchingHttpSecurityPolicyTest.java @@ -0,0 +1,100 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URL; +import java.time.Duration; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.vertx.core.Vertx; +import io.vertx.ext.web.client.WebClient; + +public class PathMatchingHttpSecurityPolicyTest { + + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(20); + private static final String APP_PROPS = """ + quarkus.http.auth.permission.management.paths=/q/* + quarkus.http.auth.permission.management.policy=authenticated + """; + private static WebClient client; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestIdentityController.class, TestIdentityProvider.class, PathHandler.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles() + .add("test", "test", "test"); + } + + @AfterAll + public static void cleanup() { + if (client != null) { + client.close(); + } + } + + @Inject + Vertx vertx; + + @TestHTTPResource + URL url; + + private WebClient getClient() { + if (client == null) { + client = WebClient.create(vertx); + } + return client; + } + + @ParameterizedTest + @ValueSource(strings = { + "/q/openapi", "///q/openapi", "/q///openapi", "/q/openapi/", "/q/openapi///" + }) + public void testOpenApiPath(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, "openapi"); + } + + private void assurePath(String path, int expectedStatusCode) { + assurePath(path, expectedStatusCode, null, null, null); + } + + private void assurePathAuthenticated(String path, String body) { + assurePath(path, 200, body, "test", null); + } + + private void assurePath(String path, int expectedStatusCode, String body, String auth, String header) { + var req = getClient().get(url.getPort(), url.getHost(), path); + if (auth != null) { + req.basicAuthentication(auth, auth); + } + if (header != null) { + req.putHeader(header, header); + } + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + + if (body != null) { + Assertions.assertTrue(result.result().bodyAsString().contains(body), path); + } + } +} diff --git a/extensions/vertx-http/deployment/pom.xml b/extensions/vertx-http/deployment/pom.xml index 2ba139c19f25c..464a4237a9d91 100644 --- a/extensions/vertx-http/deployment/pom.xml +++ b/extensions/vertx-http/deployment/pom.xml @@ -76,7 +76,17 @@ com.fasterxml.jackson.datatype jackson-datatype-jdk8 - + + io.quarkus + quarkus-devtools-common + + + org.apache.maven.resolver + maven-resolver-connector-basic + + + + io.quarkus diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ExtensionsProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ExtensionsProcessor.java index 856d8d0fbf440..846ce1df73570 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ExtensionsProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ExtensionsProcessor.java @@ -1,14 +1,31 @@ package io.quarkus.devui.deployment.menu; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.dev.spi.DevModeType; +import io.quarkus.devtools.commands.AddExtensions; +import io.quarkus.devtools.commands.ListCategories; +import io.quarkus.devtools.commands.ListExtensions; +import io.quarkus.devtools.commands.RemoveExtensions; +import io.quarkus.devtools.commands.data.QuarkusCommandException; +import io.quarkus.devtools.commands.data.QuarkusCommandOutcome; +import io.quarkus.devtools.project.QuarkusProject; +import io.quarkus.devtools.project.QuarkusProjectHelper; import io.quarkus.devui.deployment.ExtensionsBuildItem; import io.quarkus.devui.deployment.InternalPageBuildItem; import io.quarkus.devui.deployment.extension.Extension; import io.quarkus.devui.deployment.extension.ExtensionGroup; +import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; import io.quarkus.devui.spi.page.Page; /** @@ -30,11 +47,160 @@ InternalPageBuildItem createExtensionsPages(ExtensionsBuildItem extensionsBuildI // Page extensionsPages.addPage(Page.webComponentPageBuilder() - .namespace("devui-extensions") + .namespace(NAMESPACE) .title("Extensions") .icon("font-awesome-solid:puzzle-piece") .componentLink("qwc-extensions.js")); return extensionsPages; } -} \ No newline at end of file + + @BuildStep(onlyIf = IsDevelopment.class) + void createBuildTimeActions(BuildProducer buildTimeActionProducer, + LaunchModeBuildItem launchModeBuildItem) { + + if (launchModeBuildItem.getDevModeType().isPresent() + && launchModeBuildItem.getDevModeType().get().equals(DevModeType.LOCAL)) { + + BuildTimeActionBuildItem buildTimeActions = new BuildTimeActionBuildItem(NAMESPACE); + + getCategories(buildTimeActions); + getInstallableExtensions(buildTimeActions); + getInstalledNamespaces(buildTimeActions); + removeExtension(buildTimeActions); + addExtension(buildTimeActions); + buildTimeActionProducer.produce(buildTimeActions); + } + } + + private void getCategories(BuildTimeActionBuildItem buildTimeActions) { + buildTimeActions.addAction(new Object() { + }.getClass().getEnclosingMethod().getName(), ignored -> { + return CompletableFuture.supplyAsync(() -> { + try { + QuarkusCommandOutcome outcome = new ListCategories(getQuarkusProject()) + .format("object") + .execute(); + + if (outcome.isSuccess()) { + return outcome.getResult(); + } + } catch (QuarkusCommandException ex) { + throw new RuntimeException(ex); + } + return null; + }); + }); + } + + private void getInstallableExtensions(BuildTimeActionBuildItem buildTimeActions) { + buildTimeActions.addAction(new Object() { + }.getClass().getEnclosingMethod().getName(), ignored -> { + return CompletableFuture.supplyAsync(() -> { + try { + QuarkusCommandOutcome outcome = new ListExtensions(getQuarkusProject()) + .installed(false) + .all(false) + .format("object") + .execute(); + + if (outcome.isSuccess()) { + return outcome.getResult(); + } + + return null; + } catch (QuarkusCommandException e) { + throw new RuntimeException(e); + } + }); + }); + } + + private void getInstalledNamespaces(BuildTimeActionBuildItem buildTimeActions) { + buildTimeActions.addAction(new Object() { + }.getClass().getEnclosingMethod().getName(), ignored -> { + return CompletableFuture.supplyAsync(() -> { + try { + QuarkusCommandOutcome outcome = new ListExtensions(getQuarkusProject()) + .installed(true) + .all(false) + .format("object") + .execute(); + + if (outcome.isSuccess()) { + + List extensionList = (List) outcome + .getResult(); + + List namespaceList = new ArrayList<>(); + + if (!extensionList.isEmpty()) { + for (io.quarkus.registry.catalog.Extension e : extensionList) { + String groupId = e.getArtifact().getGroupId(); + String artifactId = e.getArtifact().getArtifactId(); + namespaceList.add(groupId + "." + artifactId); + } + } + return namespaceList; + } + + return null; + } catch (QuarkusCommandException e) { + throw new RuntimeException(e); + } + }); + }); + } + + private void removeExtension(BuildTimeActionBuildItem buildTimeActions) { + buildTimeActions.addAction(new Object() { + }.getClass().getEnclosingMethod().getName(), params -> { + return CompletableFuture.supplyAsync(() -> { + String extensionArtifactId = params.get("extensionArtifactId"); + try { + QuarkusCommandOutcome outcome = new RemoveExtensions(getQuarkusProject()) + .extensions(Set.of(extensionArtifactId)) + .execute(); + + if (outcome.isSuccess()) { + return true; + } else { + return false; + } + } catch (QuarkusCommandException e) { + throw new RuntimeException(e); + } + }); + }); + } + + private void addExtension(BuildTimeActionBuildItem buildTimeActions) { + buildTimeActions.addAction(new Object() { + }.getClass().getEnclosingMethod().getName(), params -> { + return CompletableFuture.supplyAsync(() -> { + String extensionArtifactId = params.get("extensionArtifactId"); + + try { + QuarkusCommandOutcome outcome = new AddExtensions(getQuarkusProject()) + .extensions(Set.of(extensionArtifactId)) + .execute(); + + if (outcome.isSuccess()) { + return true; + } else { + return false; + } + } catch (QuarkusCommandException e) { + throw new RuntimeException(e); + } + }); + }); + } + + private QuarkusProject getQuarkusProject() { + Path projectRoot = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize(); + return QuarkusProjectHelper.getCachedProject(projectRoot); + } + + private static final String NAMESPACE = "devui-extensions"; +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java index a599be732321f..534868cf3189d 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java @@ -5,7 +5,6 @@ import java.net.URL; import java.time.Duration; -import java.util.List; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; @@ -23,8 +22,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import io.quarkus.builder.Version; -import io.quarkus.maven.dependency.Dependency; import io.quarkus.security.ForbiddenException; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.test.utils.TestIdentityController; @@ -95,9 +92,7 @@ public class PathMatchingHttpSecurityPolicyTest { .addClasses(TestIdentityController.class, TestIdentityProvider.class, PathHandler.class, RouteHandler.class, CustomNamedPolicy.class) .addAsResource("static-file.html", "META-INF/resources/static-file.html") - .addAsResource(new StringAsset(APP_PROPS), "application.properties")).setForcedDependencies(List.of( - Dependency.of("io.quarkus", "quarkus-smallrye-health", Version.getVersion()), - Dependency.of("io.quarkus", "quarkus-smallrye-openapi", Version.getVersion()))); + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); @BeforeAll public static void setup() { @@ -190,25 +185,6 @@ public void testStaticResource(String path) { assurePathAuthenticated(path); } - @ParameterizedTest - @ValueSource(strings = { - "///q/openapi", "/q///openapi", "/q/openapi/", "/q/openapi///" - }) - public void testOpenApiPath(String path) { - assurePath(path, 401); - assurePathAuthenticated(path, "openapi"); - } - - @ParameterizedTest - @ValueSource(strings = { - "/q/health", "/q/health/live", "/q/health/ready", "//q/health", "///q/health", "///q///health", - "/q/health/", "/q///health/", "/q///health////live" - }) - public void testHealthCheckPaths(String path) { - assurePath(path, 401); - assurePathAuthenticated(path, "UP"); - } - @Test public void testMiscellaneousPaths() { // /api/baz with segment indicating version shouldn't match /api/baz path policy diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js index ee3729fd0beba..ae022d1abe8c4 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js @@ -252,9 +252,9 @@ export class QwcConfiguration extends observeState(LitElement) { return html` { + this._categories = jsonRpcResponse.result; + this._categories.push({name:'Uncategorised', id: 'uncategorised'}); + }); + + if(!this._extensions){ + this.hotReload(); + } + } + + hotReload(){ + this.jsonRpc.getInstallableExtensions().then(jsonRpcResponse => { + this._extensions = jsonRpcResponse.result; + this._filteredExtensions = this._extensions; + }); + } + + render() { + if(this._filteredExtensions){ + return html`
+ ${this._renderFilterBar()} + ${this._renderGrid()} +
`; + }else{ + return html`
+ + + + +
`; + } + } + + _renderGrid(){ + return html` + + + `; + } + + _renderFilterBar(){ + return html`
+ + + ${this._filteredExtensions.length} + + ${this._renderCategoryDropdown()} +
`; + + } + + _renderCategoryDropdown(){ + if(this._categories){ + return html``; + } + } + + _filterCategoryChanged(e){ + this._filteredCategory = (e.detail.value || '').trim(); + return this._filterGrid(); + } + + _filterTextChanged(e) { + this._filteredValue = (e.detail.value || '').trim(); + return this._filterGrid(); + } + + _filterGrid(){ + this._filteredExtensions = this._extensions.filter((prop) => { + if(this._filteredValue && this._filteredValue !== '' && this._filteredCategory && this._filteredCategory !== ''){ + return this._filterByTerm(prop) && this._filterByCategory(prop); + }else if(this._filteredValue && this._filteredValue !== ''){ + return this._filterByTerm(prop); + }else if(this._filteredCategory && this._filteredCategory !== ''){ + return this._filterByCategory(prop); + }else{ + return true; + } + }); + } + + _filterByTerm(prop){ + if(prop.metadata && prop.metadata.keywords){ + return this._match(prop.name, this._filteredValue) || this._match(prop.description, this._filteredValue) || prop.metadata.keywords.includes(this._filteredValue); + }else{ + return this._match(prop.name, this._filteredValue) || this._match(prop.description, this._filteredValue); + } + } + + _filterByCategory(prop){ + if(prop.metadata && prop.metadata.categories){ + return prop.metadata.categories.includes(this._filteredCategory); + }else if(this._filteredCategory === "uncategorised"){ + return true; + }else { + return false; + } + } + + _match(value, term) { + if (! value) { + return false; + } + + return value.toLowerCase().includes(term.toLowerCase()); + } + + _descriptionRenderer(prop) { + + return html`
+
+ Artifact: ${prop.artifact.groupId}:${prop.artifact.artifactId} + Version: ${prop.artifact.version} + ${this._renderIsPlatform(prop)} + ${this._renderMetadata1(prop)} +
+
+ ${this._renderMetadata2(prop)} +
+
+ + + Add Extension + + `; + } + + _renderMetadata1(prop){ + if(prop.metadata){ + return html`${this._renderGuide(prop.metadata)} + ${this._renderScmUrl(prop.metadata)} + ${this._renderStatus(prop.metadata)} + ${this._renderMinJavaVersion(prop.metadata)}`; + } + } + + _renderIsPlatform(prop){ + if (prop.origins && prop.origins.some(str => str.startsWith("io.quarkus:quarkus-bom-quarkus-platform"))){ + return html`Platform: `; + } else { + return html`Platform: `; + } + } + + _renderGuide(metadata){ + if (metadata.guide){ + return html`Guide: ${metadata.guide}`; + } + } + + _renderScmUrl(metadata){ + if (metadata['scm-url']){ + return html`SCM: ${metadata['scm-url']}`; + } + } + + _renderStatus(metadata){ + if(metadata.status){ + return html`Status: ${metadata.status.toUpperCase()}`; + } + } + + _renderMinJavaVersion(metadata){ + if(metadata['minimum-java-version']){ + return html`Minimum Java version: ${metadata['minimum-java-version']}`; + } + } + + _renderMetadata2(prop){ + if(prop.metadata){ + return html`${this._renderKeywords(prop.metadata)} + ${this._renderCategories(prop.metadata)} + ${this._renderExtensionDependencies(prop.metadata)}`; + } + } + + + + + + _statusLevel(s){ + if(s === "stable") { + return "success"; + } else if(s === "experimental") { + return "warning"; + } else if(s === "preview") { + return "contrast"; + } + return null; + } + + _renderCategories(metadata){ + if(metadata.categories){ + return this._renderList("Categories", metadata.categories); + } + } + + _renderKeywords(metadata){ + if(metadata.keywords){ + return this._renderList("Keywords", metadata.keywords); + } + } + + _renderExtensionDependencies(metadata){ + if(metadata['extension-dependencies']){ + return html` + + ${this._renderExtensionDependenciesLines(metadata['extension-dependencies'])} + + `; + } + } + + _renderExtensionDependenciesLines(lines){ + return html` + ${lines.map((line) => + html`${line}` + )} + `; + } + + _renderList(heading, list) { + return html`${heading}: ${this._renderListLines(list)}`; + } + + _renderListLines(list) { + return html` +
    + ${list.map((item) => + html`
  • ${item}
  • ` + )} +
+ `; + } + + _install(prop){ + let extensionArtifactId = prop.artifact.groupId + ':' + prop.artifact.artifactId; + this.jsonRpc.addExtension({extensionArtifactId:extensionArtifactId}).then(jsonRpcResponse => { + let outcome = jsonRpcResponse.result; + + const options = { + detail: {outcome: outcome, name: prop.name}, + bubbles: true, + composed: true, + }; + this.dispatchEvent(new CustomEvent('inprogress', options)); + + }); + } +} +customElements.define('qwc-extension-add', QwcExtensionAdd); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js index 5c76cb0bfe8bd..7f719cca05077 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js @@ -1,14 +1,19 @@ import { LitElement, html, css} from 'lit'; +import { observeState } from 'lit-element-state'; import '@vaadin/icon'; import '@vaadin/dialog'; import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; import '@qomponent/qui-badge'; +import { JsonRpc } from 'jsonrpc'; +import { notifier } from 'notifier'; +import { connectionState } from 'connection-state'; /** * This component represent one extension * It's a card on the extension board */ -export class QwcExtension extends LitElement { +export class QwcExtension extends observeState(LitElement) { + jsonRpc = new JsonRpc("devui-extensions", false); static styles = css` .card { @@ -95,13 +100,15 @@ export class QwcExtension extends LitElement { builtWith: {type: String}, providesCapabilities: {}, extensionDependencies: {}, - favourite: {type: Boolean}, + favourite: {type: Boolean}, + installed: {type: Boolean}, }; constructor() { super(); this._dialogOpened = false; this.favourite = false; + this.installed = false; } render() { @@ -260,9 +267,30 @@ export class QwcExtension extends LitElement { ${this._renderExtensionDependencies()} + ${this._renderUninstallButton()} `; } + _renderUninstallButton(){ + if(connectionState.current.isConnected && this.installed){ + return html` + + Remove this extension + `; + } + } + + _uninstall(){ + this._dialogOpened = false; + notifier.showInfoMessage(this.name + " removal in progress"); + this.jsonRpc.removeExtension({extensionArtifactId:this.artifact}).then(jsonRpcResponse => { + let outcome = jsonRpcResponse.result; + if(!outcome){ + notifier.showErrorMessage(name + " removal failed"); + } + }); + } + _renderGuideDetails() { return this.guide ? html`${this.guide}` diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js index d555580dcaf02..14ef09471e3d0 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js @@ -5,7 +5,13 @@ import { devuiState } from 'devui-state'; import { observeState } from 'lit-element-state'; import 'qwc/qwc-extension.js'; import 'qwc/qwc-extension-link.js'; +import 'qwc/qwc-extension-add.js'; import { StorageController } from 'storage-controller'; +import '@vaadin/dialog'; +import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; +import { notifier } from 'notifier'; +import { connectionState } from 'connection-state'; +import { JsonRpc } from 'jsonrpc'; /** * This component create cards of all the extensions @@ -13,7 +19,7 @@ import { StorageController } from 'storage-controller'; export class QwcExtensions extends observeState(LitElement) { routerController = new RouterController(this); storageController = new StorageController(this); - + jsonRpc = new JsonRpc("devui-extensions", false); static styles = css` .grid { display: flex; @@ -49,22 +55,46 @@ export class QwcExtensions extends observeState(LitElement) { qwc-extension-link { cursor: grab; } + .addExtensionButton { + position: absolute; + bottom: 40px; + right: 40px; + width: 3em; + height: 3em; + box-shadow: var(--lumo-shade) 5px 5px 15px 3px; + } + .addExtensionIcon { + width: 2em; + height: 2em; + } `; static properties = { _favourites: {state: true}, + _addDialogOpened: {state: true}, + _installedExtensions: {state: true, type: Array}, } constructor() { super(); this._favourites = this._getStoredFavourites(); + this._addDialogOpened = false; + this._installedExtensions = []; + } + + connectedCallback() { + super.connectedCallback(); + this.jsonRpc.getInstalledNamespaces().then(jsonRpcResponse => { + this._installedExtensions = jsonRpcResponse.result; + }); } render() { return html`
${this._renderActives(devuiState.cards.active)} ${devuiState.cards.inactive.map(extension => this._renderInactive(extension))} -
`; + + ${this._renderAddDialog()}`; } _renderActives(extensions){ @@ -97,7 +127,6 @@ export class QwcExtensions extends observeState(LitElement) { } _renderActive(extension, fav){ - return html` @@ -243,5 +273,55 @@ export class QwcExtensions extends observeState(LitElement) { `; } } + + _renderAddDialog(){ + return html` + html` + + + + `, + [] + )} + ${dialogRenderer( + () => html`` + )} + > + ${this._renderAddExtensionButton()} + `; + } + + _renderAddExtensionButton(){ + if(connectionState.current.isConnected){ + return html` + + `; + } + } + + _installRequest(e){ + this._addDialogOpened = false; + let name = e.detail.name; + if(e.detail.outcome){ + notifier.showInfoMessage(name + " installation in progress"); + }else{ + notifier.showErrorMessage(name + " installation failed"); + } + } + + _openAddDialog() { + this._addDialogOpened = true; + } + } customElements.define('qwc-extensions', QwcExtensions); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/data/QuarkusCommandOutcome.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/data/QuarkusCommandOutcome.java index 4bca9c6c87856..f423d663e73b4 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/data/QuarkusCommandOutcome.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/data/QuarkusCommandOutcome.java @@ -2,25 +2,32 @@ import java.util.Objects; -public class QuarkusCommandOutcome extends ValueMap { +public class QuarkusCommandOutcome extends ValueMap> { - public static QuarkusCommandOutcome success() { - return new QuarkusCommandOutcome(true, null); + public static QuarkusCommandOutcome success() { + return new QuarkusCommandOutcome<>(true, null, null); } - public static QuarkusCommandOutcome failure(String message) { + public static QuarkusCommandOutcome success(T result) { + return new QuarkusCommandOutcome<>(true, null, result); + } + + public static QuarkusCommandOutcome failure(String message) { Objects.requireNonNull(message, "Message may not be null in case of a failure"); - return new QuarkusCommandOutcome(false, message); + return new QuarkusCommandOutcome(false, message, null); } private final boolean success; private final String message; - private QuarkusCommandOutcome(boolean success, String message) { + private final T result; + + private QuarkusCommandOutcome(boolean success, String message, T result) { this.success = success; this.message = message; + this.result = result; } public boolean isSuccess() { @@ -30,4 +37,8 @@ public boolean isSuccess() { public String getMessage() { return message; } + + public T getResult() { + return result; + } } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListCategoriesCommandHandler.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListCategoriesCommandHandler.java index e7a6a5f6ae4c3..a0bba897a88fa 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListCategoriesCommandHandler.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListCategoriesCommandHandler.java @@ -2,7 +2,9 @@ import java.util.Collection; import java.util.Comparator; +import java.util.List; import java.util.function.BiConsumer; +import java.util.stream.Collectors; import io.quarkus.devtools.commands.ListCategories; import io.quarkus.devtools.commands.data.QuarkusCommandException; @@ -30,13 +32,16 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws final Collection categories = invocation.getExtensionsCatalog().getCategories(); - if (!batchMode) { + if (!batchMode && !format.equalsIgnoreCase("object")) { log.info("Available Quarkus extension categories: "); log.info(""); } BiConsumer formatter; switch (format.toLowerCase()) { + case "object": + formatter = null; + break; case "full": log.info(String.format(FULL_FORMAT, "Category", "CategoryId", "Description")); formatter = this::fullFormatter; @@ -50,10 +55,16 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws break; } - categories.stream() - .sorted(Comparator.comparing(Category::getName)) - .forEach(c -> formatter.accept(log, c)); - + if (formatter != null) { + categories.stream() + .sorted(Comparator.comparing(Category::getName)) + .forEach(c -> formatter.accept(log, c)); + } else { + List sortedCategories = categories.stream() + .sorted(Comparator.comparing(Category::getName)) + .collect(Collectors.toList()); + return QuarkusCommandOutcome.success(sortedCategories); + } return QuarkusCommandOutcome.success(); } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListExtensionsCommandHandler.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListExtensionsCommandHandler.java index ca7154bc9f64c..0f05b84a02513 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListExtensionsCommandHandler.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListExtensionsCommandHandler.java @@ -61,11 +61,13 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws .getExtensions(); if (extensions.isEmpty()) { - log.info("No extension found with pattern '%s'", search); + if (!format.equalsIgnoreCase("object")) { + log.info("No extension found with pattern '%s'", search); + } return QuarkusCommandOutcome.success(); } - if (!batchMode) { + if (!batchMode && !format.equalsIgnoreCase("object")) { String extensionStatus = all ? "available" : "installable"; if (installedOnly) extensionStatus = "installed"; @@ -75,6 +77,9 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws BiConsumer currentFormatter; switch (format.toLowerCase()) { + case "object": + currentFormatter = null; + break; case "id": case "name": currentFormatter = this::idFormatter; @@ -110,11 +115,30 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws categoryFilter = e -> true; } - extensions.stream() - .filter(e -> !ExtensionProcessor.of(e).isUnlisted()) - .filter(categoryFilter) - .sorted(Comparator.comparing(e -> e.getArtifact().getArtifactId())) - .forEach(e -> display(log, e, installedByKey.get(toKey(e)), all, installedOnly, currentFormatter)); + if (currentFormatter != null) { + extensions.stream() + .filter(e -> !ExtensionProcessor.of(e).isUnlisted()) + .filter(categoryFilter) + .sorted(Comparator.comparing(e -> e.getArtifact().getArtifactId())) + .forEach(e -> display(log, e, installedByKey.get(toKey(e)), all, installedOnly, currentFormatter)); + } else { + List filteredExtensions = extensions.stream() + .filter(e -> !ExtensionProcessor.of(e).isUnlisted()) + .filter(categoryFilter) + .filter(e -> { + ArtifactCoords installed = installedByKey.get(toKey(e)); + if (installedOnly && installed == null) { + return false; + } + if (!installedOnly && !all && installed != null) { + return false; + } + return true; + }) + .sorted(Comparator.comparing(e -> e.getArtifact().getArtifactId())) + .collect(Collectors.toList()); + return QuarkusCommandOutcome.success(filteredExtensions); + } return QuarkusCommandOutcome.success(); } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/QuarkusProjectHelper.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/QuarkusProjectHelper.java index bf50251b319e1..379cf8460636a 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/QuarkusProjectHelper.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/QuarkusProjectHelper.java @@ -2,6 +2,8 @@ import static io.quarkus.devtools.project.CodestartResourceLoadersBuilder.getCodestartResourceLoaders; +import java.io.OutputStream; +import java.io.PrintStream; import java.nio.file.Path; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; @@ -16,7 +18,7 @@ import io.quarkus.registry.config.RegistriesConfig; public class QuarkusProjectHelper { - + private static QuarkusProject cachedProject; private static RegistriesConfig toolsConfig; private static MessageWriter log; private static MavenArtifactResolver artifactResolver; @@ -44,6 +46,33 @@ public static BuildTool detectExistingBuildTool(Path projectDirPath) { return BuildTool.fromProject(projectDirPath); } + public static QuarkusProject getCachedProject(Path projectDir) { + if (cachedProject == null) { + PrintStream nullPrintStream = new PrintStream(OutputStream.nullOutputStream()); + log = MessageWriter.info(nullPrintStream); + BuildTool buildTool = detectExistingBuildTool(projectDir); + if (buildTool == null) { + buildTool = BuildTool.MAVEN; + } + if (BuildTool.MAVEN.equals(buildTool)) { + try { + return MavenProjectBuildFile.getProject(projectDir, log, null); + } catch (RegistryResolutionException e) { + throw new RuntimeException("Failed to initialize the Quarkus Maven extension manager", e); + } + } + final ExtensionCatalog catalog; + try { + catalog = resolveExtensionCatalog(); + } catch (Exception e) { + throw new RuntimeException("Failed to resolve the Quarkus extension catalog", e); + } + cachedProject = getProject(projectDir, catalog, buildTool, JavaVersion.NA, log); + } + + return cachedProject; + } + public static QuarkusProject getProject(Path projectDir) { BuildTool buildTool = detectExistingBuildTool(projectDir); if (buildTool == null) { diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/CompileOnlyDependencyFlagsTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/CompileOnlyDependencyFlagsTest.java index b7e8ed45f80da..d6e61194b96b8 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/CompileOnlyDependencyFlagsTest.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/CompileOnlyDependencyFlagsTest.java @@ -86,7 +86,8 @@ public void compileOnlyFlags() throws Exception { DependencyFlags.COMPILE_ONLY); assertOnlyFlagsSet(bootstrapResolver, compileOnly.get(bootstrapResolver), DependencyFlags.COMPILE_ONLY, - DependencyFlags.CLASSLOADER_PARENT_FIRST); + DependencyFlags.CLASSLOADER_PARENT_FIRST, + DependencyFlags.DEPLOYMENT_CP); compileOnly = compileOnlyDeps.get(LaunchMode.NORMAL.name()); assertEqual(compileOnly, expectedCompileOnly); @@ -99,7 +100,8 @@ public void compileOnlyFlags() throws Exception { DependencyFlags.COMPILE_ONLY); assertOnlyFlagsSet(bootstrapResolver, compileOnly.get(bootstrapResolver), DependencyFlags.COMPILE_ONLY, - DependencyFlags.CLASSLOADER_PARENT_FIRST); + DependencyFlags.CLASSLOADER_PARENT_FIRST, + DependencyFlags.DEPLOYMENT_CP); } private static void assertOnlyFlagsSet(String coords, int flags, int... expectedFlags) {