Skip to content

Commit

Permalink
Added marketplace for plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Mucha committed Feb 16, 2024
1 parent 068245f commit 83ce5c5
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 6 deletions.
5 changes: 4 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog

version 5.8.0 - upcoming
version 5.9.0
* added [marketplace](/milkman-plugins-management#marketplace) for plugins

version 5.8.0
* base64 keytype
* Oauth: Authentication Grant flow now supports choosing port for return url
* extended information on http responses (ssl certificate, body size etc)
Expand Down
5 changes: 5 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Features

## Marketplace

There is a marketplace where plugins are shown that are released on github.
Instructions on how to publish your own plugins can be seen (here)[/docs/plugin-development.md].

![Marketplace](/img/marketplace.png)

## Code Folding

Expand Down
18 changes: 18 additions & 0 deletions docs/plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ A [sample plugin](https://github.com/warmuuh/milkman/tree/master/milkman-note) w

if you want to setup a new project, an exemplary pom can be found [here](/docs/plugin-development-setup.md).


## Marketplace registration
if you want your plugin to show up in the milkman marketplace, you need to add a topic (aka tag) to your repository `milkman-plugins` and a
descriptor file `milkman-plugin.json` at the root which looks like this:

```json
{
"plugins": [
{
"author": "your name",
"name": "your plugin id",
"artifact": "the filename of the according artifact in your releases",
"description": "some description"
}
]
}
```

# Data Model

![img](http://www.gravizo.com/svg?@startuml;object%20Workspace;object%20Environment;Environment%20:%20isGlobal;Workspace%20o--%20%22*%22%20Environment;object%20Collection;Workspace%20o--%20%22*%22%20Collection;object%20Request;Collection%20o--%20%22*%22%20Request;%20Request%20--%3E%20HtmlRequest%20;%20Request%20--%3E%20SqlRequest%20;Request%20o-%20%22*%22%20RequestAspect;RequestAspect%20--%3E%20HttpHeaderRequestAspect;RequestAspect%20--%3E%20HttpBodyRequestAspect;@enduml)
Expand Down
Binary file added img/marketplace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions milkman-plugins-management/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ Click the Add button and choose the plugin file in the open dialog. After instal
### Uninstalling Plugins

All currently installed plugins are shown in the table. Find the plugin you don't want to use anymore and click the `X` button.


## <a name="marketplace"></a> Marketplace

There is a marketplace where plugins are shown that are released on github.
Instructions on how to publish your own plugins can be seen (here)[/docs/plugin-development.md].

![Marketplace](/img/marketplace.png)
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package milkman.ui.plugins.management.market;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import lombok.SneakyThrows;

public class GithubApiClient {


private ObjectMapper mapper = new ObjectMapper();

public GithubApiClient() {
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}

@SneakyThrows
public List<MarketplacePlugin> fetchPluginRepos() {
return searchForRepos("milkman-plugins")
.items().stream()
.flatMap(res -> loadPluginDescriptorForRepo(res.fullName())
.stream())
.toList();
}

private GithubRepoSearchResultList searchForRepos(String topic) throws IOException {
InputStream inputStream = new URL(
"https://api.github.com/search/repositories?q=topic:" + topic +
"&sort=created&order=asc").openStream();
GithubRepoSearchResultList searchResult =
mapper.readValue(inputStream, GithubRepoSearchResultList.class);
return searchResult;
}

@SneakyThrows
private List<MarketplacePlugin> loadPluginDescriptorForRepo(String fullRepoName) {
HttpURLConnection con =
(HttpURLConnection) new URL("https://api.github.com/repos/" + fullRepoName +
"/contents/milkman-plugin.json").openConnection();
con.setRequestProperty("Accept", "application/vnd.github.raw+json");
con.connect();

if (con.getResponseCode() != 200) {
return List.of();
}

GithubPluginRepoDescription result =
mapper.readValue(con.getInputStream(), GithubPluginRepoDescription.class);


return result.plugins().stream().map(
p -> new MarketplacePlugin(p.author(), p.name(), p.description(),
"https://github.com/" + fullRepoName + "/releases/latest/download/" + p.artifact(),
"https:/github.com/" + fullRepoName)
).toList();
}


record GithubRepoSearchResultList(
List<GithubRepoSearchResult> items
) {

}

record GithubRepoSearchResult(@JsonProperty("full_name") String fullName) {

}

public record GithubPluginRepoDescription(List<GithubPluginDescription> plugins) {
}

public record GithubPluginDescription(String author, String name, String artifact, String description) {

}

public record MarketplacePlugin(String author, String name, String description, String artifactUrl, String documentationUrl) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package milkman.ui.plugins.management.market;

import static milkman.utils.fxml.FxmlBuilder.button;
import static milkman.utils.fxml.FxmlBuilder.cancel;
import static milkman.utils.fxml.FxmlBuilder.hbox;
import static milkman.utils.fxml.FxmlBuilder.icon;
import static milkman.utils.fxml.FxmlBuilder.label;
import static milkman.utils.fxml.FxmlBuilder.text;
import static milkman.utils.fxml.FxmlBuilder.vbox;

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListCell;
import com.jfoenix.controls.JFXTextField;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
import java.time.Duration;
import java.util.Comparator;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.scene.Node;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import milkman.PlatformUtil;
import milkman.ui.plugins.management.market.GithubApiClient.MarketplacePlugin;
import milkman.utils.fxml.FxmlBuilder;
import milkman.utils.fxml.FxmlUtil;
import org.reactfx.EventStreams;

public class MarketplaceDialog {

private JFXTextField txt_search;
private GithubApiClient apiClient = new GithubApiClient();
private Dialog dialog;
private ListView<MarketplacePlugin> pluginList;
@Getter
private MarketplacePlugin chosenPlugin;


@Getter
boolean cancelled = true;

public MarketplaceDialog() {
}

public void showAndWait() {
JFXDialogLayout content = new MarketplaceDialogFxml(this);

pluginList.setCellFactory(view -> new MarketplacePluginCell());
pluginList.setMinWidth(600);
pluginList.setBorder(new Border(
new BorderStroke(Paint.valueOf("black"), BorderStrokeStyle.SOLID, null,
new BorderWidths(1))));
ObservableList<MarketplacePlugin> observableList =
FXCollections.observableArrayList(apiClient.fetchPluginRepos());
SortedList<MarketplacePlugin> sortedList =
observableList.sorted(Comparator.comparing(MarketplacePlugin::name));
FilteredList<MarketplacePlugin> filteredList = new FilteredList<>(sortedList);


EventStreams.nonNullValuesOf(txt_search.textProperty())
.successionEnds(Duration.ofMillis(250))
.subscribe(qry -> setFilterPredicate(filteredList, qry));


pluginList.setItems(filteredList);




dialog = FxmlUtil.createDialog(content);
dialog.showAndWait();
}

private void setFilterPredicate(FilteredList<MarketplacePlugin> filteredList, String searchTerm) {
if (searchTerm != null && !searchTerm.isEmpty()) {
filteredList.setPredicate(p -> p.name().toLowerCase().contains(searchTerm.toLowerCase())
|| p.description().toLowerCase().contains(searchTerm.toLowerCase())
|| p.author().toLowerCase().contains(searchTerm.toLowerCase()));
} else {
filteredList.setPredicate(o -> true);
}

}

private void onSave(MarketplacePlugin chosenPlugin) {
cancelled = false;
this.chosenPlugin = chosenPlugin;
dialog.close();
}

private void onCancel() {
cancelled = true;
dialog.close();
}

public static class MarketplaceDialogFxml extends JFXDialogLayout {

public MarketplaceDialogFxml(MarketplaceDialog controller) {
setHeading(label("Plugin Marketplace"));

setBody(vbox(
controller.txt_search = text("plugin-search", "search for plugins..."),
controller.pluginList = new ListView<>()
)
);

setActions(cancel(controller::onCancel));
}
}

@RequiredArgsConstructor
class MarketplacePluginCell extends JFXListCell<MarketplacePlugin> {
@Override
protected void updateItem(MarketplacePlugin plugin, boolean empty) {
super.updateItem(plugin, empty);
if (empty || plugin == null) {
setText(null);
setGraphic(null);
} else {
setText(null);
setGraphic(createEntry(plugin));
}
}

private Node createEntry(MarketplacePlugin plugin) {
Label title = label(plugin.name());
title.setFont(Font.font(title.getFont().getFamily(), FontWeight.EXTRA_BOLD,
title.getFont().getSize() + 4));

Label label = label("Author: " + plugin.author());
label.setOpacity(0.7);

Label desc = label(plugin.description());
desc.setMaxWidth(400);
FxmlBuilder.VboxExt pluginDesc = vbox(
title,
desc,
hbox(label)
);
HBox.setHgrow(pluginDesc, Priority.ALWAYS);

JFXButton visitButton = button("marketplace.goto-plugin", icon(FontAwesomeIcon.GLOBE), () -> {
PlatformUtil.tryOpenBrowser(plugin.documentationUrl());
}
);
JFXButton installButton = button("marketplace.install-plugin", icon(FontAwesomeIcon.DOWNLOAD), () -> {
Platform.runLater(() -> onSave(plugin));
}
);

FxmlBuilder.HboxExt content = hbox(pluginDesc, vbox(visitButton, installButton));
return content;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
Expand All @@ -15,7 +17,10 @@

import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import static milkman.ExceptionDialog.showExceptionDialog;
Expand All @@ -25,6 +30,18 @@
@Slf4j
public class PluginManager {

@SneakyThrows
public Optional<PluginMetaData> downloadAndInstallPlugin(String pluginFileUrl) {
InputStream in = new URL(pluginFileUrl).openStream();
File tempFile = File.createTempFile("milkman-plugin-", ".jar");
FileUtils.copyInputStreamToFile(in, tempFile);
try {
return installPlugin(tempFile);
} finally {
tempFile.delete();
}
}

public Optional<PluginMetaData> installPlugin(File pluginFile) {
if (isValidPluginFile(pluginFile)) {
return copyPluginFile(pluginFile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import milkman.ui.main.options.OptionDialogBuilder;
import milkman.ui.main.options.OptionDialogPane;
import milkman.ui.plugin.OptionPageProvider;
import milkman.ui.plugins.management.market.MarketplaceDialog;
import milkman.utils.fxml.FxmlUtil;

import static milkman.ui.plugins.management.options.PluginManagementMessages.CONFIRMATION_MESSAGE_UNINSTALL_PLUGIN;
Expand Down Expand Up @@ -53,16 +54,25 @@ public OptionDialogPane getOptionsDialog(OptionDialogBuilder builder) {
.page("Plugins", getOptions())
.section("Plugins")
.<PluginMetaData>list()
.itemProvider(PluginsManagementOptions::getPlugins)
.columnValueProviders(columnValueProviders)
.columnValueProviders(columnValueProviders)
.newValueProvider(this::installPlugin)
.vetoableListChangeListener(this::uninstallPlugin)
.itemProvider(PluginsManagementOptions::getPlugins)
.columnValueProviders(columnValueProviders)
.columnValueProviders(columnValueProviders)
.newValueProvider(this::installPlugin)
.vetoableListChangeListener(this::uninstallPlugin)
.endList()
.button("Marketplace...", this::installFromMarketplace)
.endSection()
.build();
}

private void installFromMarketplace() {
var marketplaceDialog = new MarketplaceDialog();
marketplaceDialog.showAndWait();
if (!marketplaceDialog.isCancelled()) {
pluginManager.downloadAndInstallPlugin(marketplaceDialog.getChosenPlugin().artifactUrl());
}
}

private PluginMetaData installPlugin() {
var fileChooser = new FileChooser();
fileChooser.setTitle("Use Plugin");
Expand Down

0 comments on commit 83ce5c5

Please sign in to comment.