extract ui into dedicated module

This commit is contained in:
Carsten Otto
2022-05-11 08:17:54 +02:00
parent 14f70219b4
commit 5959ee5b38
64 changed files with 50 additions and 23 deletions

View File

@@ -1,108 +0,0 @@
package de.cotto.lndmanagej;
import de.cotto.lndmanagej.controller.ChannelController;
import de.cotto.lndmanagej.controller.NodeController;
import de.cotto.lndmanagej.controller.NotFoundException;
import de.cotto.lndmanagej.controller.StatusController;
import de.cotto.lndmanagej.controller.WarningsController;
import de.cotto.lndmanagej.controller.dto.BalanceInformationDto;
import de.cotto.lndmanagej.controller.dto.ChannelsDto;
import de.cotto.lndmanagej.controller.dto.NodeDetailsDto;
import de.cotto.lndmanagej.controller.dto.PoliciesDto;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.model.Node;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.NodeService;
import de.cotto.lndmanagej.ui.UiDataService;
import de.cotto.lndmanagej.ui.dto.ChannelDetailsDto;
import de.cotto.lndmanagej.ui.dto.NodeDto;
import de.cotto.lndmanagej.ui.dto.OpenChannelDto;
import de.cotto.lndmanagej.ui.dto.StatusModel;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class UiDataServiceImpl extends UiDataService {
private final StatusController statusController;
private final WarningsController warningsController;
private final ChannelController channelController;
private final NodeController nodeController;
private final NodeService nodeService;
private final ChannelService channelService;
public UiDataServiceImpl(
ChannelController channelController,
StatusController statusController,
WarningsController warningsController,
NodeController nodeController,
NodeService nodeService,
ChannelService channelService
) {
super();
this.channelController = channelController;
this.statusController = statusController;
this.warningsController = warningsController;
this.nodeController = nodeController;
this.nodeService = nodeService;
this.channelService = channelService;
}
@Override
public StatusModel getStatus() {
return new StatusModel(
statusController.isSyncedToChain(),
statusController.getBlockHeight(),
warningsController.getWarnings()
);
}
@Override
public List<OpenChannelDto> getOpenChannels() {
ChannelsDto openChannels = statusController.getOpenChannels();
return openChannels.channels().stream()
.map(this::toOpenChannelDto)
.toList();
}
private OpenChannelDto toOpenChannelDto(ChannelId channelId) {
LocalChannel localChannel = channelService.getLocalChannel(channelId).orElseThrow();
Pubkey remotePubkey = localChannel.getRemotePubkey();
String alias = nodeController.getAlias(remotePubkey);
PoliciesDto policies = channelController.getPolicies(channelId);
BalanceInformationDto balance = channelController.getBalance(channelId);
return new OpenChannelDto(channelId, alias, remotePubkey, policies, balance);
}
@Override
public ChannelDetailsDto getChannelDetails(ChannelId channelId) throws NotFoundException {
de.cotto.lndmanagej.controller.dto.ChannelDetailsDto details = channelController.getDetails(channelId);
return new ChannelDetailsDto(
channelId,
details.remotePubkey(),
details.remoteAlias(),
details.openInitiator(),
details.balance(),
details.onChainCosts(),
details.policies(),
details.feeReport(),
details.flowReport(),
details.rebalanceReport(),
details.warnings());
}
@Override
public NodeDto getNode(Pubkey pubkey) {
Node node = nodeService.getNode(pubkey);
return new NodeDto(node.pubkey().toString(), node.alias(), node.online());
}
@Override
public NodeDetailsDto getNodeDetails(Pubkey pubkey) {
return nodeController.getDetails(pubkey);
}
}

View File

@@ -1,46 +0,0 @@
package de.cotto.lndmanagej.ui;
import de.cotto.lndmanagej.controller.NotFoundException;
import de.cotto.lndmanagej.controller.dto.NodeDetailsDto;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.ui.dto.ChannelDetailsDto;
import de.cotto.lndmanagej.ui.dto.NodeDto;
import de.cotto.lndmanagej.ui.dto.OpenChannelDto;
import de.cotto.lndmanagej.ui.dto.StatusModel;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
public abstract class UiDataService {
public UiDataService() {
// default constructor
}
public abstract StatusModel getStatus();
public abstract List<OpenChannelDto> getOpenChannels();
public abstract ChannelDetailsDto getChannelDetails(ChannelId channelId) throws NotFoundException;
public abstract NodeDto getNode(Pubkey pubkey);
public abstract NodeDetailsDto getNodeDetails(Pubkey pubkey);
public List<NodeDto> createNodeList(Collection<OpenChannelDto> openChannels) {
Set<Pubkey> pubkeys = openChannels.stream()
.map(OpenChannelDto::remotePubkey)
.collect(toSet());
return pubkeys.stream()
.map(this::getNode)
.toList();
}
public List<NodeDto> createNodeList() {
return createNodeList(getOpenChannels());
}
}

View File

@@ -1,29 +0,0 @@
package de.cotto.lndmanagej.ui.controller;
import de.cotto.lndmanagej.controller.NotFoundException;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.ui.page.PageService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class ChannelDetailsController {
private final PageService page;
public ChannelDetailsController(PageService pageService) {
this.page = pageService;
}
@GetMapping("/channel/{channelId}")
public String channelDetails(@PathVariable ChannelId channelId, Model model) {
try {
return page.channelDetails(channelId).create(model);
} catch (NotFoundException e) {
return page.error("Channel not found.").create(model);
}
}
}

View File

@@ -1,32 +0,0 @@
package de.cotto.lndmanagej.ui.controller;
import de.cotto.lndmanagej.ui.page.PageService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class DashboardController {
private final PageService page;
public DashboardController(PageService pageService) {
this.page = pageService;
}
@GetMapping("/")
public String dashboard(Model model) {
return page.dashboard().create(model);
}
@GetMapping(path = {"/channel", "/channels"})
public String channels(Model model) {
return page.channels().create(model);
}
@GetMapping(path = {"/node", "/nodes"})
public String nodes(Model model) {
return page.nodes().create(model);
}
}

View File

@@ -1,32 +0,0 @@
package de.cotto.lndmanagej.ui.controller;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.ui.page.PageService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.NoSuchElementException;
@Controller
public class NodeDetailsController {
private final PageService pageService;
public NodeDetailsController(PageService pageService) {
this.pageService = pageService;
}
@GetMapping("/node/{pubkey}")
public String nodeDetails(@PathVariable Pubkey pubkey, Model model) {
try {
return pageService.nodeDetails(pubkey).create(model);
} catch (NoSuchElementException e) {
return pageService.error("Node not found.").create(model);
} catch (IllegalArgumentException e) {
return pageService.error("Invalid public key.").create(model);
}
}
}

View File

@@ -1,102 +0,0 @@
package de.cotto.lndmanagej.ui.controller;
import de.cotto.lndmanagej.controller.ChannelIdConverter;
import de.cotto.lndmanagej.controller.NotFoundException;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.ui.UiDataService;
import de.cotto.lndmanagej.ui.dto.OpenChannelDto;
import de.cotto.lndmanagej.ui.page.PageService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@Controller
public class SearchController {
private static final int SINGLE_NODE = 1;
private final UiDataService dataService;
private final PageService pageService;
private final ChannelIdConverter channelIdConverter;
public SearchController(
UiDataService dataService,
PageService pageService,
ChannelIdConverter channelIdConverter
) {
this.dataService = dataService;
this.pageService = pageService;
this.channelIdConverter = channelIdConverter;
}
@GetMapping("/search")
public String search(@RequestParam("q") String query, Model model) {
List<OpenChannelDto> openChannels = dataService.getOpenChannels();
ChannelId channelId = getForChannelId(query, openChannels).orElse(null);
if (channelId != null) {
return detailsPage(channelId, model);
}
Pubkey pubkey = getForPubkey(query, openChannels).orElse(null);
if (pubkey != null) {
return pageService.nodeDetails(pubkey).create(model);
}
String lowercaseQuery = query.toLowerCase(Locale.US);
List<OpenChannelDto> matchingChannels = openChannels.stream()
.filter(channel -> containsInPeerAlias(channel, lowercaseQuery))
.toList();
if (matchingChannels.isEmpty()) {
return pageService.error("No search result.").create(model);
}
if (matchingChannels.size() == SINGLE_NODE) {
return pageService.nodeDetails(matchingChannels.get(0).remotePubkey()).create(model);
}
return pageService.nodes(matchingChannels).create(model);
}
private Optional<ChannelId> getForChannelId(String query, List<OpenChannelDto> openChannels) {
ChannelId channelId = channelIdConverter.tryToConvert(query).orElse(null);
if (channelId == null) {
return Optional.empty();
}
return openChannels.stream()
.map(OpenChannelDto::channelId)
.filter(channelId::equals)
.findFirst();
}
private Optional<Pubkey> getForPubkey(String query, List<OpenChannelDto> openChannels) {
try {
Pubkey pubkey = Pubkey.create(query);
return openChannels.stream()
.map(OpenChannelDto::remotePubkey)
.filter(pubkey::equals)
.findFirst();
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}
private boolean containsInPeerAlias(OpenChannelDto channel, String lowercaseQuery) {
return channel.remoteAlias().toLowerCase(Locale.US).contains(lowercaseQuery);
}
private String detailsPage(ChannelId channelId, Model model) {
try {
return pageService.channelDetails(channelId).create(model);
} catch (NotFoundException e) {
return pageService.error("Channel not found.").create(model);
}
}
}

View File

@@ -1,28 +0,0 @@
package de.cotto.lndmanagej.ui.dto;
import de.cotto.lndmanagej.controller.dto.BalanceInformationDto;
import de.cotto.lndmanagej.controller.dto.FeeReportDto;
import de.cotto.lndmanagej.controller.dto.FlowReportDto;
import de.cotto.lndmanagej.controller.dto.OnChainCostsDto;
import de.cotto.lndmanagej.controller.dto.PoliciesDto;
import de.cotto.lndmanagej.controller.dto.RebalanceReportDto;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.OpenInitiator;
import de.cotto.lndmanagej.model.Pubkey;
import java.util.Set;
public record ChannelDetailsDto(
ChannelId channelId,
Pubkey remotePubkey,
String remoteAlias,
OpenInitiator openInitiator,
BalanceInformationDto balanceInformation,
OnChainCostsDto onChainCosts,
PoliciesDto policies,
FeeReportDto feeReport,
FlowReportDto flowReport,
RebalanceReportDto rebalanceReport,
Set<String> warnings
) {
}

View File

@@ -1,4 +0,0 @@
package de.cotto.lndmanagej.ui.dto;
public record NodeDto(String pubkey, String alias, boolean online) {
}

View File

@@ -1,55 +0,0 @@
package de.cotto.lndmanagej.ui.dto;
import de.cotto.lndmanagej.controller.dto.BalanceInformationDto;
import de.cotto.lndmanagej.controller.dto.PoliciesDto;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Pubkey;
import java.text.NumberFormat;
public record OpenChannelDto(
ChannelId channelId,
String remoteAlias,
Pubkey remotePubkey,
PoliciesDto policies,
BalanceInformationDto balanceInformation
) {
public String getRatio() {
double percentLocal = getOutboundPercentage();
double percentRemote = 100 - percentLocal;
int leftDots = (int) (percentRemote / 5);
int rightDots = (int) (percentLocal / 5);
return dots(leftDots) + " | " + dots(rightDots);
}
private String dots(int numberOfDots) {
if (numberOfDots == 0) {
return "";
}
String dotsString = "· ".repeat(numberOfDots);
return dotsString.substring(0, dotsString.length() - 1); // remove excess space
}
public double getOutboundPercentage() {
long outbound = getOutbound();
long routableCapacity = outbound + getInbound();
return (1.0 * outbound / routableCapacity) * 100;
}
public long getOutbound() {
return Long.parseLong(balanceInformation.localBalanceSat());
}
public long getInbound() {
return Long.parseLong(balanceInformation.remoteBalanceSat());
}
public String formatOutbound() {
return NumberFormat.getInstance().format(getOutbound());
}
public String formatInbound() {
return NumberFormat.getInstance().format(getInbound());
}
}

View File

@@ -1,6 +0,0 @@
package de.cotto.lndmanagej.ui.dto;
import de.cotto.lndmanagej.controller.dto.NodesAndChannelsWithWarningsDto;
public record StatusModel(boolean synced, int blockHeight, NodesAndChannelsWithWarningsDto warnings) {
}

View File

@@ -1,54 +0,0 @@
package de.cotto.lndmanagej.ui.page;
import de.cotto.lndmanagej.controller.NotFoundException;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.ui.UiDataService;
import de.cotto.lndmanagej.ui.dto.OpenChannelDto;
import de.cotto.lndmanagej.ui.page.channel.ChannelDetailsPage;
import de.cotto.lndmanagej.ui.page.channel.ChannelsPage;
import de.cotto.lndmanagej.ui.page.general.DashboardPage;
import de.cotto.lndmanagej.ui.page.general.ErrorPage;
import de.cotto.lndmanagej.ui.page.node.NodeDetailsPage;
import de.cotto.lndmanagej.ui.page.node.NodesPage;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class PageService {
private final UiDataService dataService;
public PageService(UiDataService dataService) {
this.dataService = dataService;
}
public DashboardPage dashboard() {
return new DashboardPage(dataService.getOpenChannels(), dataService.createNodeList(), dataService.getStatus());
}
public ChannelsPage channels() {
return new ChannelsPage(dataService.getOpenChannels());
}
public ChannelDetailsPage channelDetails(ChannelId channelId) throws NotFoundException {
return new ChannelDetailsPage(dataService.getChannelDetails(channelId));
}
public NodesPage nodes() {
return new NodesPage(dataService.createNodeList());
}
public NodesPage nodes(List<OpenChannelDto> channels) {
return new NodesPage(dataService.createNodeList(channels));
}
public NodeDetailsPage nodeDetails(Pubkey pubkey) {
return new NodeDetailsPage(dataService.getNodeDetails(pubkey));
}
public ErrorPage error(String errorMessage) {
return new ErrorPage(errorMessage);
}
}

View File

@@ -1,18 +0,0 @@
package de.cotto.lndmanagej.ui.page.channel;
import de.cotto.lndmanagej.ui.dto.ChannelDetailsDto;
import de.cotto.lndmanagej.ui.page.general.ThymeleafPage;
public class ChannelDetailsPage extends ThymeleafPage {
public ChannelDetailsPage(ChannelDetailsDto channel) {
super();
add("id", channel.channelId());
add("channel", channel);
}
@Override
public String getView() {
return "channel-details";
}
}

View File

@@ -1,23 +0,0 @@
package de.cotto.lndmanagej.ui.page.channel;
import de.cotto.lndmanagej.ui.dto.OpenChannelDto;
import de.cotto.lndmanagej.ui.page.general.ThymeleafPage;
import java.util.Comparator;
import java.util.List;
public class ChannelsPage extends ThymeleafPage {
public ChannelsPage(List<OpenChannelDto> channels) {
super();
List<OpenChannelDto> sortedChannels = channels.stream()
.sorted(Comparator.comparing(OpenChannelDto::getOutboundPercentage))
.toList();
add("channels", sortedChannels);
}
@Override
public String getView() {
return "channels";
}
}

View File

@@ -1,26 +0,0 @@
package de.cotto.lndmanagej.ui.page.general;
import de.cotto.lndmanagej.ui.dto.NodeDto;
import de.cotto.lndmanagej.ui.dto.OpenChannelDto;
import de.cotto.lndmanagej.ui.dto.StatusModel;
import java.util.Comparator;
import java.util.List;
public class DashboardPage extends ThymeleafPage {
public DashboardPage(List<OpenChannelDto> channels, List<NodeDto> nodes, StatusModel statusModel) {
super();
List<OpenChannelDto> sortedChannels = channels.stream()
.sorted(Comparator.comparing(OpenChannelDto::getOutboundPercentage))
.toList();
add("status", statusModel);
add("channels", sortedChannels);
add("nodes", nodes);
}
@Override
public String getView() {
return "dashboard";
}
}

View File

@@ -1,14 +0,0 @@
package de.cotto.lndmanagej.ui.page.general;
public class ErrorPage extends ThymeleafPage {
public ErrorPage(String message) {
super();
add("error", message);
}
@Override
public String getView() {
return "error";
}
}

View File

@@ -1,30 +0,0 @@
package de.cotto.lndmanagej.ui.page.general;
import org.springframework.ui.Model;
import java.util.HashMap;
import java.util.Map;
public abstract class ThymeleafPage {
private final Map<String, Object> modelAttributes = new HashMap<>();
public ThymeleafPage() {
// default constructor
}
public abstract String getView();
public Map<String, Object> getModelAttributes() {
return modelAttributes;
}
protected void add(String attributeName, Object data) {
modelAttributes.put(attributeName, data);
}
public String create(Model model) {
model.addAllAttributes(getModelAttributes());
return getView();
}
}

View File

@@ -1,17 +0,0 @@
package de.cotto.lndmanagej.ui.page.node;
import de.cotto.lndmanagej.controller.dto.NodeDetailsDto;
import de.cotto.lndmanagej.ui.page.general.ThymeleafPage;
public class NodeDetailsPage extends ThymeleafPage {
public NodeDetailsPage(NodeDetailsDto nodeDetails) {
super();
add("pubkey", nodeDetails.node());
add("node", nodeDetails);
}
@Override
public String getView() {
return "node-details";
}
}

View File

@@ -1,19 +0,0 @@
package de.cotto.lndmanagej.ui.page.node;
import de.cotto.lndmanagej.ui.dto.NodeDto;
import de.cotto.lndmanagej.ui.page.general.ThymeleafPage;
import java.util.List;
public class NodesPage extends ThymeleafPage {
public NodesPage(List<NodeDto> nodes) {
super();
add("nodes", nodes);
}
@Override
public String getView() {
return "nodes";
}
}