Skip to content

Commit

Permalink
WebJars locator extension Dev UI support
Browse files Browse the repository at this point in the history
  • Loading branch information
majlantalik committed Oct 14, 2023
1 parent ec4074a commit 5815042
Show file tree
Hide file tree
Showing 6 changed files with 376 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.quarkus.webjar.locator.deployment.devui;

import java.util.List;

public class WebJarAsset {

private String name;
private List<WebJarAsset> children;
private boolean fileAsset;
private String urlPart;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<WebJarAsset> getChildren() {
return children;
}

public void setChildren(List<WebJarAsset> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<WebJarLibrary> webJarLibraries;

public WebJarLibrariesBuildItem(List<WebJarLibrary> webJarLibraries) {
this.webJarLibraries = webJarLibraries;
}

public List<WebJarLibrary> getWebJarLibraries() {
return webJarLibraries;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<WebJarLibrariesBuildItem> webJarLibrariesProducer) {

final List<WebJarLibrary> webJarLibraries = new ArrayList<>();
final List<ClassPathElement> providers = QuarkusClassLoader.getElements(WEBJARS_PREFIX, false);
if (!providers.isEmpty()) {
// Map of webjar artifact keys to class path elements
final Map<ArtifactKey, ClassPathElement> 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<ArtifactKey, ClassPathElement> 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<Path> 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<Path> 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;
}

}
Original file line number Diff line number Diff line change
@@ -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<CardPageBuildItem> cardPageProducer,
WebJarLibrariesBuildItem webJarLibrariesBuildItem) {

CardPageBuildItem cardPageBuildItem = new CardPageBuildItem();
List<WebJarLibrary> 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);
}

}
Original file line number Diff line number Diff line change
@@ -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`
<vaadin-tabsheet class="full-height">
<vaadin-tabs slot="tabs">
${this._webJarLibraries.map(webjar => html`
<vaadin-tab id="${webjar.webJarName}">
${webjar.webJarName + " (" + webjar.version + ")"}
</vaadin-tab>`)}
</vaadin-tabs>
${this._webJarLibraries.map(webjar => this._renderLibraryAssets(webjar))}
</vaadin-tabsheet>
`;
}

_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`
<div tab="${library.webJarName}" class="full-height">
<vaadin-grid .itemHasChildrenPath="${'children'}" .dataProvider="${dataProvider}"
theme="compact no-border" class="full-height">
<vaadin-grid-tree-column path="name"></vaadin-grid-tree-column>
<vaadin-grid-column width="5em" header="Copy link" flex-grow="0"
${columnBodyRenderer(this._assetCopyRenderer, [])}></vaadin-grid-column>
<vaadin-grid-column width="6em" header="Open asset" flex-grow="0"
${columnBodyRenderer(this._assetLinkRenderer, [])}></vaadin-grid-column>
</vaadin-grid>
</div>`;
}

_assetLinkRenderer(item) {
if (item.fileAsset) {
return html`<a href="${item.urlPart}" target="_blank" style="color: var(--lumo-contrast-80pct);">
<vaadin-icon style="font-size: small;" icon="font-awesome-solid:up-right-from-square" role="link"
title="Open link to ${item.name} in a new tab">
</vaadin-icon>
</a>`;
} else {
return html``;
}
}

_assetCopyRenderer(item) {
if (item.fileAsset) {
return html`
<vaadin-button theme="icon" style="margin: 0; height: 2em;"
@click=${() => {this._onCopyLinkClick(item)}}
aria-label="Copy link to ${item.name} to clipboard"
title="Copy link to ${item.name} to clipboard">
<vaadin-icon style="font-size: small; cursor: pointer" icon="font-awesome-regular:copy"
role="button">
</vaadin-icon>
</vaadin-button>`;
} 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)

0 comments on commit 5815042

Please sign in to comment.