Rework for host hierarchies

This commit is contained in:
crschnick
2025-10-20 14:02:56 +00:00
parent 183476ea02
commit c3c415bb5a
25 changed files with 355 additions and 90 deletions
@@ -1,8 +0,0 @@
package io.xpipe.app.ext;
import java.util.Optional;
public interface HostAddressStore extends DataStore {
HostAddress getHostAddress();
}
@@ -673,14 +673,19 @@ public class DataStoreEntry extends StorageElement {
return;
}
var newComplete = newStore.isComplete();
if (!newComplete) {
var changed = !Objects.equals(store, newStore) || validity != Validity.INCOMPLETE;
validity = Validity.INCOMPLETE;
store = newStore;
if (changed) {
notifyUpdate(false, false);
try {
var newComplete = newStore.isComplete();
if (!newComplete) {
var changed = !Objects.equals(store, newStore) || validity != Validity.INCOMPLETE;
validity = Validity.INCOMPLETE;
store = newStore;
if (changed) {
notifyUpdate(false, false);
}
return;
}
} catch (Exception e) {
ErrorEventFactory.fromThrowable(e).handle();
return;
}
-1
View File
@@ -128,7 +128,6 @@ open module io.xpipe.app {
provides ActionProvider with
SetupToolActionProvider,
XPipeUrlProvider,
HostAddressSwitchBranchProvider,
OpenHubMenuLeafProvider,
EditHubLeafProvider,
CloneHubLeafProvider,
@@ -0,0 +1,54 @@
package io.xpipe.ext.base.host;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.hub.action.HubLeafProvider;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.FileOpener;
import javafx.beans.value.ObservableValue;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class AbstractHostCreationActionProvider implements HubLeafProvider<AbstractHostTransformStore> {
@Override
public ObservableValue<String> getName(DataStoreEntryRef<AbstractHostTransformStore> store) {
return AppI18n.observable("abstractHostConvert");
}
@Override
public LabelGraphic getIcon(DataStoreEntryRef<AbstractHostTransformStore> store) {
return new LabelGraphic.IconGraphic("mdi2c-cog-transfer-outline");
}
@Override
public Class<?> getApplicableClass() {
return AbstractHostTransformStore.class;
}
@Override
public boolean isApplicable(DataStoreEntryRef<AbstractHostTransformStore> o) {
return o.getStore().canConvertToAbstractHost();
}
@Jacksonized
@SuperBuilder
public static class Action extends StoreAction<AbstractHostTransformStore> {
@Override
public void executeImpl() throws Exception {
var d = ref.getStore();
var ah = d.createAbstractHostStore();
var entry = DataStorage.get().addStoreIfNotPresent(ref.get().getName(), ah);
entry.setExpanded(true);
var newStore = d.withNewParent(entry.ref());
DataStorage.get().updateEntryStore(ref.get(), newStore);
entry.setChildrenCache(null);
StoreViewState.get().triggerStoreListUpdate();
}
}
}
@@ -1,10 +1,9 @@
package io.xpipe.ext.base.store;
package io.xpipe.ext.base.host;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.ext.*;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators;
import io.xpipe.ext.base.identity.IdentityValue;
import lombok.ToString;
import lombok.Value;
import lombok.experimental.SuperBuilder;
@@ -15,9 +14,10 @@ import lombok.extern.jackson.Jacksonized;
@SuperBuilder
@Jacksonized
@JsonTypeName("abstractHost")
public class AbstractHostStore implements DataStore, HostAddressStore {
public class AbstractHostStore implements DataStore, HostAddressStore, HostAddressGatewayStore {
String host;
DataStoreEntryRef<NetworkTunnelStore> gateway;
@Override
public void checkComplete() throws Throwable {
@@ -28,4 +28,9 @@ public class AbstractHostStore implements DataStore, HostAddressStore {
public HostAddress getHostAddress() {
return HostAddress.of(host);
}
@Override
public DataStoreEntryRef<NetworkTunnelStore> getGateway() {
return gateway;
}
}
@@ -1,14 +1,15 @@
package io.xpipe.ext.base.store;
package io.xpipe.ext.base.host;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.*;
import io.xpipe.app.hub.comp.*;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import lombok.SneakyThrows;
@@ -36,16 +37,46 @@ public class AbstractHostStoreProvider implements DataStoreProvider {
return DataStoreUsageCategory.GROUP;
}
@Override
public ObservableValue<String> informationString(StoreSection section) {
return Bindings.createStringBinding(
() -> {
var all = section.getAllChildren().getList();
var shown = section.getShownChildren().getList();
if (shown.size() == 0) {
return null;
}
var string = all.size() == shown.size() ? all.size() : shown.size() + "/" + all.size();
return all.size() > 0
? (all.size() == 1 ? AppI18n.get("abstractHostHasConnection", string) : AppI18n.get("abstractHostHasConnections", string))
: AppI18n.get("abstractHostNoConnections");
},
section.getShownChildren().getList(),
section.getAllChildren().getList(),
AppI18n.activeLanguage());
}
@SneakyThrows
@Override
public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
AbstractHostStore st = store.getValue().asNeeded();
Property<String> host = new SimpleObjectProperty<>(st.getHost());
var host = new SimpleObjectProperty<>(st.getHost());
var gateway = new SimpleObjectProperty<>(st.getGateway());
return new OptionsBuilder()
.nameAndDescription("abstractHostAddress")
.addString(host)
.nonNull()
.nameAndDescription("abstractHostGateway")
.addComp(new StoreChoiceComp<>(StoreChoiceComp.Mode.PROXY,
entry,
gateway,
NetworkTunnelStore.class,
ref -> true,
StoreViewState.get().getAllConnectionsCategory()
), gateway)
.bind(
() -> {
return AbstractHostStore.builder()
@@ -0,0 +1,13 @@
package io.xpipe.ext.base.host;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.storage.DataStoreEntryRef;
public interface AbstractHostTransformStore extends DataStore {
boolean canConvertToAbstractHost();
AbstractHostStore createAbstractHostStore();
AbstractHostTransformStore withNewParent(DataStoreEntryRef<AbstractHostStore> newParent);
}
@@ -1,5 +1,6 @@
package io.xpipe.app.ext;
package io.xpipe.ext.base.host;
import io.xpipe.app.ext.HostAddress;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.platform.OptionsBuilder;
@@ -1,4 +1,4 @@
package io.xpipe.app.ext;
package io.xpipe.ext.base.host;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
@@ -0,0 +1,9 @@
package io.xpipe.ext.base.host;
import io.xpipe.app.ext.NetworkTunnelStore;
import io.xpipe.app.storage.DataStoreEntryRef;
public interface HostAddressGatewayStore extends HostAddressStore {
DataStoreEntryRef<NetworkTunnelStore> getGateway();
}
@@ -1,6 +1,5 @@
package io.xpipe.ext.base.store;
package io.xpipe.ext.base.host;
import io.xpipe.app.ext.HostAddressStore;
import io.xpipe.ext.base.identity.IdentityValue;
public interface HostAddressIdentityStore extends HostAddressStore {
@@ -0,0 +1,9 @@
package io.xpipe.ext.base.host;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.HostAddress;
public interface HostAddressStore extends DataStore {
HostAddress getHostAddress();
}
@@ -1,7 +1,6 @@
package io.xpipe.app.hub.action.impl;
package io.xpipe.ext.base.host;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.HostAddressSwitchStore;
import io.xpipe.app.hub.action.HubBranchProvider;
import io.xpipe.app.hub.action.HubLeafProvider;
import io.xpipe.app.hub.action.HubMenuItemProvider;
@@ -1,4 +1,6 @@
package io.xpipe.app.ext;
package io.xpipe.ext.base.host;
import io.xpipe.app.ext.HostAddress;
import java.util.Optional;
@@ -1,4 +1,6 @@
package io.xpipe.app.ext;
package io.xpipe.ext.base.host;
import io.xpipe.app.ext.NetworkTunnelStore;
public interface HostAddressTunnelStore extends HostAddressStore, NetworkTunnelStore {
@@ -1,12 +1,10 @@
package io.xpipe.ext.base.store;
package io.xpipe.ext.base.host;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.HostAddress;
import io.xpipe.app.ext.NetworkTunnelStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.ext.base.identity.IdentityValue;
public interface HostStore extends DataStore, ShellStore, NetworkTunnelStore {
public interface HostStore extends DataStore {
HostAddress getHostAddress();
@@ -5,6 +5,7 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreUsageCategory;
import io.xpipe.app.ext.NetworkTunnelStore;
import io.xpipe.app.hub.comp.*;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
@@ -15,42 +15,15 @@ import lombok.Getter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
@SuperBuilder
@SuperBuilder(toBuilder = true)
@Getter
@EqualsAndHashCode
@ToString
public abstract class AbstractServiceStore implements SingletonSessionStore<NetworkTunnelSession>, DataStore {
private final Integer remotePort;
private final Integer localPort;
private final ServiceProtocolType serviceProtocolType;
public abstract DataStoreEntryRef<NetworkTunnelStore> getHost();
public boolean licenseRequired() {
return true;
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(getHost());
NetworkTunnelStore.checkTunnelable(getHost());
Validators.nonNull(remotePort);
Validators.nonNull(serviceProtocolType);
}
public String getOpenTargetUrl() {
return ServiceAddressRotation.getRotatedAddress(this);
}
public boolean requiresTunnel() {
if (getHost() == null) {
return false;
}
if (!getHost().getStore().isLocallyTunnelable()) {
var parent = getHost().getStore().getNetworkParent();
public static boolean requiresTunnel(NetworkTunnelStore t) {
if (!t.isLocallyTunnelable()) {
var parent = t.getNetworkParent();
if (!(parent instanceof NetworkTunnelStore nts)) {
return false;
}
@@ -58,11 +31,78 @@ public abstract class AbstractServiceStore implements SingletonSessionStore<Netw
return nts.requiresTunnel();
}
return getHost().getStore().requiresTunnel();
return t.requiresTunnel();
}
public static boolean requiresManualAddress(DataStore s) {
if (!(s instanceof NetworkTunnelStore t)) {
return true;
}
if (!t.isLocallyTunnelable()) {
var parent = t.getNetworkParent();
if (!(parent instanceof NetworkTunnelStore nts)) {
return false;
}
return nts.requiresTunnel();
}
return t.requiresTunnel();
}
private final Integer remotePort;
private final Integer localPort;
private final ServiceProtocolType serviceProtocolType;
public abstract String getAddress();
public abstract DataStoreEntryRef<NetworkTunnelStore> getGateway();
public abstract DataStoreEntryRef<?> getHost();
public boolean licenseRequired() {
return true;
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(remotePort);
Validators.nonNull(serviceProtocolType);
if (getHost() != null) {
getHost().checkComplete();
} else {
Validators.nonNull(getAddress());
}
}
public String getOpenTargetUrl() {
return ServiceAddressRotation.getRotatedAddress(this);
}
public boolean requiresTunnel() {
if (getHost() == null || !(getHost().getStore() instanceof NetworkTunnelStore t)) {
return false;
}
if (!t.isLocallyTunnelable()) {
var parent = t.getNetworkParent();
if (!(parent instanceof NetworkTunnelStore nts)) {
return false;
}
return nts.requiresTunnel();
}
return t.requiresTunnel();
}
@Override
public NetworkTunnelSession newSession() {
if (!(getHost().getStore() instanceof NetworkTunnelStore t)) {
return null;
}
var f = LicenseProvider.get().getFeature("services");
if (licenseRequired() && !f.isSupported()) {
var active = DataStorage.get().getStoreEntries().stream()
@@ -78,13 +118,13 @@ public abstract class AbstractServiceStore implements SingletonSessionStore<Netw
var l = localPort != null ? localPort : HostHelper.findRandomOpenPortOnAllLocalInterfaces();
var parent = getHost().getStore().getNetworkParent();
if (!getHost().getStore().isLocallyTunnelable() && parent instanceof NetworkTunnelStore nts) {
var parent = t.getNetworkParent();
if (!t.isLocallyTunnelable() && parent instanceof NetworkTunnelStore nts) {
return nts.createTunnelSession(
l, remotePort, nts.getTunnelHostName() != null ? nts.getTunnelHostName() : "localhost");
}
return getHost().getStore().createTunnelSession(l, remotePort, "localhost");
return t.createTunnelSession(l, remotePort, "localhost");
}
@Override
@@ -30,9 +30,18 @@ public abstract class AbstractServiceStoreProvider implements SingletonSessionSt
@Override
public boolean supportsSession(SingletonSessionStore<?> s) {
var abs = (AbstractServiceStore) s;
return abs.getHost() == null
|| !abs.getHost().getStore().requiresTunnel()
|| !abs.getHost().getStore().isLocallyTunnelable();
if (abs.getHost() != null && (!(abs.getHost().getStore() instanceof NetworkTunnelStore t)
|| !t.requiresTunnel()
|| !t.isLocallyTunnelable())) {
return false;
}
if (abs.getHost() == null && (abs.getGateway() == null ||
!abs.getGateway().getStore().isLocallyTunnelable() || !abs.getGateway().getStore().requiresTunnel())) {
return false;
}
return true;
}
@Override
@@ -1,22 +1,44 @@
package io.xpipe.ext.base.service;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.NetworkTunnelStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.ext.base.host.AbstractHostStore;
import io.xpipe.ext.base.host.AbstractHostTransformStore;
import io.xpipe.ext.base.host.HostAddressStore;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@SuperBuilder
@SuperBuilder(toBuilder = true)
@Getter
@Jacksonized
@JsonTypeName("customService")
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public final class CustomServiceStore extends AbstractServiceStore {
public final class CustomServiceStore extends AbstractServiceStore implements AbstractHostTransformStore {
private final DataStoreEntryRef<NetworkTunnelStore> host;
private final DataStoreEntryRef<HostAddressStore> host;
private final String address;
private final DataStoreEntryRef<NetworkTunnelStore> gateway;
@Override
public boolean canConvertToAbstractHost() {
return host == null;
}
@Override
public AbstractHostStore createAbstractHostStore() {
return AbstractHostStore.builder().host(address).gateway(gateway).build();
}
@Override
public AbstractHostTransformStore withNewParent(DataStoreEntryRef<AbstractHostStore> newParent) {
return toBuilder().address(null).gateway(null).host(newParent.asNeeded()).build();
}
}
@@ -5,18 +5,44 @@ import io.xpipe.app.ext.DataStoreCreationCategory;
import io.xpipe.app.ext.GuiDialog;
import io.xpipe.app.ext.NetworkTunnelStore;
import io.xpipe.app.hub.comp.StoreChoiceComp;
import io.xpipe.app.hub.comp.StoreComboChoiceComp;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.ext.base.host.AbstractHostStore;
import io.xpipe.ext.base.host.HostAddressStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import java.util.List;
public class CustomServiceStoreProvider extends AbstractServiceStoreProvider {
@Override
public DataStoreEntry getSyntheticParent(DataStoreEntry store) {
var c = (CustomServiceStore) store.getStore();
if (c.getHost() == null || c.getHost().getStore() instanceof AbstractHostStore) {
return null;
}
return super.getSyntheticParent(store);
}
@Override
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
var c = (CustomServiceStore) store.getStore();
if (c.getHost() != null && c.getHost().getStore() instanceof AbstractHostStore) {
return c.getHost().get();
}
return super.getDisplayParent(store);
}
@Override
public int getOrderPriority() {
return -1;
@@ -30,20 +56,42 @@ public class CustomServiceStoreProvider extends AbstractServiceStoreProvider {
@Override
public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
CustomServiceStore st = store.getValue().asNeeded();
var host = new SimpleObjectProperty<>(st.getHost());
var comboHost = new SimpleObjectProperty<>(StoreComboChoiceComp.ComboValue.of(
st.getAddress(),
st.getHost()
));
var gateway = new SimpleObjectProperty<>(st.getGateway());
var hideGateway = BindingsHelper.map(comboHost, c -> c == null || c.getRef() != null);
var localPort = new SimpleObjectProperty<>(st.getLocalPort());
var remotePort = new SimpleObjectProperty<>(st.getRemotePort());
var serviceProtocolType = new SimpleObjectProperty<>(st.getServiceProtocolType());
var hostChoice = new StoreComboChoiceComp<>(
hostStore -> hostStore.getHostAddress().get(),
entry,
comboHost,
NetworkTunnelStore.class,
n -> n.getStore() instanceof AbstractHostStore ||
(n.getStore() instanceof NetworkTunnelStore t && t.isLocallyTunnelable()),
StoreViewState.get().getAllConnectionsCategory()
);
var gatewayChoice = new StoreChoiceComp<>(
StoreChoiceComp.Mode.PROXY,
entry,
gateway,
NetworkTunnelStore.class,
ref -> !ref.get().equals(DataStorage.get().local()),
StoreViewState.get().getAllConnectionsCategory());
var q = new OptionsBuilder()
.nameAndDescription("serviceHost")
.addComp(
StoreChoiceComp.other(
host,
NetworkTunnelStore.class,
n -> n.getStore().isLocallyTunnelable(),
StoreViewState.get().getAllConnectionsCategory()),
host)
.addComp(hostChoice, comboHost)
.nonNull()
.nameAndDescription("gateway")
.addComp(gatewayChoice, gateway)
.hide(hideGateway)
.nameAndDescription("serviceRemotePort")
.addInteger(remotePort)
.nonNull()
@@ -54,7 +102,9 @@ public class CustomServiceStoreProvider extends AbstractServiceStoreProvider {
.bind(
() -> {
return CustomServiceStore.builder()
.host(host.get())
.address(comboHost.get() != null ? comboHost.get().getManualHost() : null)
.host(comboHost.get() != null ? comboHost.get().getRef() : null)
.gateway(gateway.get())
.localPort(localPort.get())
.remotePort(remotePort.get())
.serviceProtocolType(serviceProtocolType.get())
@@ -26,6 +26,16 @@ public class FixedServiceStore extends AbstractServiceStore implements FixedChil
private final DataStoreEntryRef<NetworkTunnelStore> host;
private final DataStoreEntryRef<? extends DataStore> displayParent;
@Override
public String getAddress() {
return "localhost";
}
@Override
public DataStoreEntryRef<NetworkTunnelStore> getGateway() {
return null;
}
@Override
public DataStoreEntryRef<NetworkTunnelStore> getHost() {
return host;
@@ -27,10 +27,11 @@ public class ServiceAddressRotation {
public static String getRotatedAddress(AbstractServiceStore serviceStore) {
var s = serviceStore.getSession();
if (s == null) {
var host = serviceStore.getHost().getStore().getTunnelHostName() != null
? serviceStore.getHost().getStore().getTunnelHostName()
: "localhost";
return getRotatedLocalhost(host + ":" + serviceStore.getRemotePort());
var address = serviceStore.getAddress();
if (address == null) {
address = "localhost";
}
return getRotatedLocalhost(address + ":" + serviceStore.getRemotePort());
}
return getRotatedLocalhost("localhost:" + s.getLocalPort());
+7
View File
@@ -2,6 +2,9 @@ import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.ext.DataStorageExtensionProvider;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.ext.base.desktop.DesktopApplicationStoreProvider;
import io.xpipe.ext.base.host.AbstractHostCreationActionProvider;
import io.xpipe.ext.base.host.AbstractHostStoreProvider;
import io.xpipe.ext.base.host.HostAddressSwitchBranchProvider;
import io.xpipe.ext.base.identity.*;
import io.xpipe.ext.base.script.*;
import io.xpipe.ext.base.service.*;
@@ -14,6 +17,7 @@ open module io.xpipe.ext.base {
exports io.xpipe.ext.base.service;
exports io.xpipe.ext.base.identity;
exports io.xpipe.ext.base.identity.ssh;
exports io.xpipe.ext.base.host;
requires java.desktop;
requires io.xpipe.core;
@@ -28,8 +32,11 @@ open module io.xpipe.ext.base {
requires atlantafx.base;
requires com.sun.jna.platform;
requires com.sun.jna;
requires javafx.base;
provides ActionProvider with
AbstractHostCreationActionProvider,
HostAddressSwitchBranchProvider,
LocalIdentityConvertHubLeafProvider,
RunBackgroundScriptActionProvider,
RunHubBatchScriptActionProvider,
+9 -2
View File
@@ -726,7 +726,7 @@ serviceLocalPortDescription=The local port to forward to, otherwise a random one
serviceRemotePort=Remote port
serviceRemotePortDescription=The port on which the service is running on
serviceHost=Service host
serviceHostDescription=The host the service is running on
serviceHostDescription=The host entry or manual address of the server on which the service is running on
openWebsite=Open website
customServiceGroup.displayName=Service group
customServiceGroup.displayDescription=Group multiple services into one category
@@ -741,7 +741,8 @@ fixedServiceGroup.displayDescription=List the available services on a system
mappedService.displayName=Service
mappedService.displayDescription=Interact with a service exposed by a container
customService.displayName=Service
customService.displayDescription=Automatically tunnel a remote service port to your local machine
#force
customService.displayDescription=Automatically open or tunnel a remote service port on your local machine
fixedService.displayName=Service
fixedService.displayDescription=Use a predefined service
noServices=No available services
@@ -1665,5 +1666,11 @@ abstractHost.displayName=Abstract host
abstractHost.displayDescription=Create an entry for a host that does not support shell connections
abstractHostAddress=Host address
abstractHostAddressDescription=The address of the host
abstractHostGateway=Gateway
abstractHostGatewayDescription=The optional gateway system through which to reach this host
abstractHostConvert=Convert to abstract host
abstractHostNoConnections=No available connections
abstractHostHasConnections=$COUNT$ available connections
abstractHostHasConnection=$COUNT$ available connection
largeFileWarningTitle=Large file edit
largeFileWarningContent=The file you want to edit is quite large with $SIZE$. Do you really want to open this file in your text editor?