diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarAsset.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarAsset.java new file mode 100644 index 0000000000000..75b488dfd92bb --- /dev/null +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarAsset.java @@ -0,0 +1,43 @@ +package io.quarkus.webjar.locator.deployment.devui; + +import java.util.List; + +public class WebJarAsset { + + private String name; + private List children; + private boolean fileAsset; + private String urlPart; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public boolean isFileAsset() { + return fileAsset; + } + + public void setFileAsset(boolean fileAsset) { + this.fileAsset = fileAsset; + } + + public String getUrlPart() { + return urlPart; + } + + public void setUrlPart(String urlPart) { + this.urlPart = urlPart; + } +} diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java new file mode 100644 index 0000000000000..87003d19c4908 --- /dev/null +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.webjar.locator.deployment.devui; + +import java.util.List; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class WebJarLibrariesBuildItem extends SimpleBuildItem { + + private final List webJarLibraries; + + public WebJarLibrariesBuildItem(List webJarLibraries) { + this.webJarLibraries = webJarLibraries; + } + + public List getWebJarLibraries() { + return webJarLibraries; + } +} diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrary.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrary.java new file mode 100644 index 0000000000000..27138ebc39448 --- /dev/null +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrary.java @@ -0,0 +1,32 @@ +package io.quarkus.webjar.locator.deployment.devui; + +public class WebJarLibrary { + + private final String webJarName; + private String version; + private WebJarAsset rootAsset; // must be a list to work with vaadin-grid + + public WebJarLibrary(String webJarName) { + this.webJarName = webJarName; + } + + public String getWebJarName() { + return webJarName; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public WebJarAsset getRootAsset() { + return rootAsset; + } + + public void setRootAsset(WebJarAsset rootAsset) { + this.rootAsset = rootAsset; + } +} diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java new file mode 100644 index 0000000000000..ee17abebef60c --- /dev/null +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java @@ -0,0 +1,144 @@ +package io.quarkus.webjar.locator.deployment.devui; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.logging.Logger; + +import io.quarkus.bootstrap.classloading.ClassPathElement; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; + +public class WebJarLocatorDevModeApiProcessor { + + private static final String WEBJARS_PREFIX = "META-INF/resources/webjars"; + private static final Logger log = Logger.getLogger(WebJarLocatorDevModeApiProcessor.class.getName()); + + @BuildStep(onlyIf = IsDevelopment.class) + public void findWebjarsAssets( + HttpBuildTimeConfig httpConfig, + CurateOutcomeBuildItem curateOutcome, + BuildProducer webJarLibrariesProducer) { + + final List webJarLibraries = new ArrayList<>(); + final List providers = QuarkusClassLoader.getElements(WEBJARS_PREFIX, false); + if (!providers.isEmpty()) { + // Map of webjar artifact keys to class path elements + final Map webJarKeys = providers.stream() + .filter(provider -> provider.getDependencyKey() != null && provider.isRuntime()) + .collect(Collectors.toMap(ClassPathElement::getDependencyKey, provider -> provider, (a, b) -> b, + () -> new HashMap<>(providers.size()))); + if (!webJarKeys.isEmpty()) { + // The root path of the application + final String rootPath = httpConfig.rootPath; + // The root path of the webjars + final String webjarRootPath = (rootPath.endsWith("/")) ? rootPath + "webjars/" : rootPath + "/webjars/"; + + // For each packaged webjar dependency, create a WebJarLibrary object + curateOutcome.getApplicationModel().getDependencies().stream() + .map(dep -> createWebJarLibrary(dep, webjarRootPath, webJarKeys)) + .filter(Objects::nonNull).forEach(webJarLibraries::add); + } + } + webJarLibrariesProducer.produce(new WebJarLibrariesBuildItem(webJarLibraries)); + } + + private WebJarLibrary createWebJarLibrary(ResolvedDependency dep, String webjarRootPath, + Map webJarKeys) { + // If the dependency is not a runtime class path dependency, return null + if (!dep.isRuntimeCp()) { + return null; + } + final ClassPathElement provider = webJarKeys.get(dep.getKey()); + if (provider == null) { + return null; + } + final WebJarLibrary webJarLibrary = new WebJarLibrary(provider.getDependencyKey().getArtifactId()); + provider.apply(tree -> { + final Path webjarsDir = tree.getPath(WEBJARS_PREFIX); + final Path nameDir; + try (Stream webjarsDirPaths = Files.list(webjarsDir)) { + nameDir = webjarsDirPaths.filter(Files::isDirectory).findFirst().orElseThrow(() -> new IOException( + "Could not find name directory for " + dep.getKey().getArtifactId() + " in " + webjarsDir)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + final Path versionDir; + Path root = nameDir; + // The base URL for the webjar + final StringBuilder urlBase = new StringBuilder(webjarRootPath); + boolean appendRootPart = true; + try { + // If the version directory exists, use it as a root, otherwise use the name directory + versionDir = nameDir.resolve(dep.getVersion()); + root = Files.isDirectory(versionDir) ? versionDir : nameDir; + urlBase.append(nameDir.getFileName().toString()) + .append("/"); + appendRootPart = false; + } catch (InvalidPathException e) { + log.warn("Could not find version directory for " + dep.getKey().getArtifactId() + " " + + dep.getVersion() + " in " + nameDir + ", falling back to name directory"); + } + webJarLibrary.setVersion(dep.getVersion()); + try { + // Create the asset tree for the webjar and set it as the root asset + var asset = createAssetForLibrary(root, urlBase.toString(), appendRootPart); + webJarLibrary.setRootAsset(asset); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return null; + }); + + return webJarLibrary; + } + + private WebJarAsset createAssetForLibrary(Path rootPath, String urlBase, boolean appendRootPart) + throws IOException { + //If it is a directory, go deeper, otherwise add the file + var root = new WebJarAsset(); + root.setName(rootPath.getFileName().toString()); + root.setChildren(new LinkedList<>()); + root.setFileAsset(false); + urlBase = appendRootPart ? urlBase + root.getName() + "/" : urlBase; + + try (DirectoryStream directoryStream = Files.newDirectoryStream(rootPath)) { + for (Path childPath : directoryStream) { + if (Files.isDirectory(childPath)) { // If it is a directory, go deeper, otherwise add the file + var childDir = createAssetForLibrary(childPath, urlBase, true); + root.getChildren().add(childDir); + } else { + var childFile = new WebJarAsset(); + childFile.setName(childPath.getFileName().toString()); + childFile.setFileAsset(true); + childFile.setUrlPart(urlBase + childFile.getName()); + root.getChildren().add(childFile); + } + } + } + // Sort the children by name + root.getChildren().sort(Comparator.comparing(WebJarAsset::getName)); + return root; + } + +} diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java new file mode 100644 index 0000000000000..9fb2440b5bdaa --- /dev/null +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java @@ -0,0 +1,35 @@ +package io.quarkus.webjar.locator.deployment.devui; + +import java.util.List; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; + +public class WebJarLocatorDevUIProcessor { + + @BuildStep(onlyIf = IsDevelopment.class) + public void createPages(BuildProducer cardPageProducer, + WebJarLibrariesBuildItem webJarLibrariesBuildItem) { + + CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); + List webJarLibraries = webJarLibrariesBuildItem.getWebJarLibraries(); + + if (!webJarLibraries.isEmpty()) { + // WebJar Libraries + cardPageBuildItem.addBuildTimeData("webJarLibraries", webJarLibraries); + + // WebJar Asset List + cardPageBuildItem.addPage(Page.webComponentPageBuilder() + .componentLink("qwc-webjar-locator-webjar-libraries.js") + .title("WebJar Libraries") + .icon("font-awesome-solid:folder-tree") + .staticLabel(String.valueOf(webJarLibraries.size()))); + } + + cardPageProducer.produce(cardPageBuildItem); + } + +} diff --git a/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-webjar-libraries.js b/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-webjar-libraries.js new file mode 100644 index 0000000000000..576e6fe5d75ec --- /dev/null +++ b/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-webjar-libraries.js @@ -0,0 +1,104 @@ +import {LitElement, html, css} from 'lit'; +import {webJarLibraries} from 'build-time-data'; +import '@vaadin/tabsheet'; +import '@vaadin/tabs'; +import '@vaadin/grid'; +import '@vaadin/icon'; +import '@vaadin/button'; +import '@vaadin/grid/vaadin-grid-tree-column.js'; +import {notifier} from 'notifier'; +import {columnBodyRenderer} from '@vaadin/grid/lit.js'; + + +export class QwcWebjarLocatorWebjarLibraries extends LitElement { + + static styles = css` + .full-height { + height: 100%; + } + `; + + static properties = { + _webJarLibraries: {}, + }; + + constructor() { + super(); + this._webJarLibraries = webJarLibraries; + } + + render() { + return html` + + + ${this._webJarLibraries.map(webjar => html` + + ${webjar.webJarName + " (" + webjar.version + ")"} + `)} + + + ${this._webJarLibraries.map(webjar => this._renderLibraryAssets(webjar))} + + + + `; + } + + _renderLibraryAssets(library) { + const dataProvider = function (params, callback) { + if (params.parentItem === undefined) { + callback(library.rootAsset.children, library.rootAsset.children.length); + } else { + callback(params.parentItem.children, params.parentItem.children.length) + } + }; + + return html` +
+ + + + + +
`; + } + + _assetLinkRenderer(item) { + if (item.fileAsset) { + return html` + + + `; + } else { + return html``; + } + } + + _assetCopyRenderer(item) { + if (item.fileAsset) { + return html` + {this._onCopyLinkClick(item)}} + aria-label="Copy link to ${item.name} to clipboard" + title="Copy link to ${item.name} to clipboard"> + + + `; + } else { + return html``; + } + } + + _onCopyLinkClick(item) { + navigator.clipboard.writeText(item.urlPart); + notifier.showInfoMessage('URL for ' + item.name + ' copied to clipboard', 'top-end'); + } + +} + +customElements.define('qwc-webjar-locator-webjar-libraries', QwcWebjarLocatorWebjarLibraries) \ No newline at end of file