Provide ACLs (together with general improvements) for the viewer conlet.
This commit is contained in:
parent
a6525a2289
commit
659463b3b4
42 changed files with 1664 additions and 679 deletions
|
|
@ -3,5 +3,6 @@ conletName = VM-Konsole
|
|||
okayLabel = Anwenden und Schließen
|
||||
Select\ VM = VM auswählen
|
||||
|
||||
Start\ VM = VM Starten
|
||||
Stop\ VM = VM Anhalten
|
||||
Start\ VM = VM starten
|
||||
Stop\ VM = VM anhalten
|
||||
Open\ console = Konsole anzeigen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* VM-Operator
|
||||
* Copyright (C) 2023 Michael N. Lipp
|
||||
* Copyright (C) 2023,2024 Michael N. Lipp
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
|
|
@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonGetter;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import freemarker.core.ParseException;
|
||||
import freemarker.template.MalformedTemplateNameException;
|
||||
|
|
@ -33,18 +34,23 @@ import java.net.Inet4Address;
|
|||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import org.bouncycastle.util.Objects;
|
||||
import org.jdrupes.json.JsonBeanDecoder;
|
||||
import org.jdrupes.json.JsonDecodeException;
|
||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||
import org.jdrupes.vmoperator.common.VmDefinitionModel.Permission;
|
||||
import org.jdrupes.vmoperator.manager.events.ChannelCache;
|
||||
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||
|
|
@ -52,6 +58,7 @@ import org.jdrupes.vmoperator.manager.events.VmChannel;
|
|||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||
import org.jgrapes.core.Channel;
|
||||
import org.jgrapes.core.Components;
|
||||
import org.jgrapes.core.Event;
|
||||
import org.jgrapes.core.Manager;
|
||||
import org.jgrapes.core.annotation.Handler;
|
||||
|
|
@ -62,11 +69,15 @@ import org.jgrapes.util.events.KeyValueStoreUpdate;
|
|||
import org.jgrapes.webconsole.base.Conlet.RenderMode;
|
||||
import org.jgrapes.webconsole.base.ConletBaseModel;
|
||||
import org.jgrapes.webconsole.base.ConsoleConnection;
|
||||
import org.jgrapes.webconsole.base.ConsoleRole;
|
||||
import org.jgrapes.webconsole.base.ConsoleUser;
|
||||
import org.jgrapes.webconsole.base.WebConsoleUtils;
|
||||
import org.jgrapes.webconsole.base.events.AddConletRequest;
|
||||
import org.jgrapes.webconsole.base.events.AddConletType;
|
||||
import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
|
||||
import org.jgrapes.webconsole.base.events.ConletDeleted;
|
||||
import org.jgrapes.webconsole.base.events.ConsoleConfigured;
|
||||
import org.jgrapes.webconsole.base.events.ConsolePrepared;
|
||||
import org.jgrapes.webconsole.base.events.ConsoleReady;
|
||||
import org.jgrapes.webconsole.base.events.DeleteConlet;
|
||||
import org.jgrapes.webconsole.base.events.NotifyConletModel;
|
||||
|
|
@ -75,22 +86,32 @@ import org.jgrapes.webconsole.base.events.OpenModalDialog;
|
|||
import org.jgrapes.webconsole.base.events.RenderConlet;
|
||||
import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
|
||||
import org.jgrapes.webconsole.base.events.SetLocale;
|
||||
import org.jgrapes.webconsole.base.events.UpdateConletType;
|
||||
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
||||
|
||||
/**
|
||||
* The Class VmConlet.
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
|
||||
"PMD.CouplingBetweenObjects" })
|
||||
"PMD.CouplingBetweenObjects", "PMD.GodClass" })
|
||||
public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||
|
||||
private static final String VM_NAME_PROPERTY = "vmName";
|
||||
private static final String RENDERED
|
||||
= VmViewer.class.getName() + ".rendered";
|
||||
private static final String PENDING
|
||||
= VmViewer.class.getName() + ".pending";
|
||||
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||
RenderMode.Preview, RenderMode.Edit);
|
||||
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
|
||||
RenderMode.Preview, RenderMode.StickyPreview);
|
||||
private final ChannelCache<String, VmChannel,
|
||||
K8sDynamicModel> channelManager = new ChannelCache<>();
|
||||
VmDefinitionModel> channelManager = new ChannelCache<>();
|
||||
private static ObjectMapper objectMapper
|
||||
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
private Class<?> preferredIpVersion = Inet4Address.class;
|
||||
private final Set<String> syncUsers = new HashSet<>();
|
||||
private final Set<String> syncRoles = new HashSet<>();
|
||||
|
||||
/**
|
||||
* The periodically generated update event.
|
||||
|
|
@ -114,24 +135,47 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
*
|
||||
* @param event the event
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Handler
|
||||
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
||||
event.structured(componentPath()).ifPresent(c -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
var dispRes = (Map<String, Object>) c
|
||||
.getOrDefault("displayResource", Collections.emptyMap());
|
||||
switch ((String) dispRes.getOrDefault("preferredIpVersion", "")) {
|
||||
case "ipv6":
|
||||
preferredIpVersion = Inet6Address.class;
|
||||
break;
|
||||
case "ipv4":
|
||||
default:
|
||||
preferredIpVersion = Inet4Address.class;
|
||||
break;
|
||||
try {
|
||||
var dispRes = (Map<String, Object>) c
|
||||
.getOrDefault("displayResource", Collections.emptyMap());
|
||||
switch ((String) dispRes.getOrDefault("preferredIpVersion",
|
||||
"")) {
|
||||
case "ipv6":
|
||||
preferredIpVersion = Inet6Address.class;
|
||||
break;
|
||||
case "ipv4":
|
||||
default:
|
||||
preferredIpVersion = Inet4Address.class;
|
||||
break;
|
||||
}
|
||||
|
||||
// Sync
|
||||
for (var entry : (List<Map<String, String>>) c.getOrDefault(
|
||||
"syncPreviewsFor", Collections.emptyList())) {
|
||||
if (entry.containsKey("user")) {
|
||||
syncUsers.add(entry.get("user"));
|
||||
} else if (entry.containsKey("role")) {
|
||||
syncRoles.add(entry.get("role"));
|
||||
}
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
logger.config("Malformed configuration: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean syncPreviews(Session session) {
|
||||
return WebConsoleUtils.userFromSession(session)
|
||||
.filter(u -> syncUsers.contains(u.getName())).isPresent()
|
||||
|| WebConsoleUtils.rolesFromSession(session).stream()
|
||||
.filter(cr -> syncRoles.contains(cr.getName())).findAny()
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* On {@link ConsoleReady}, fire the {@link AddConletType}.
|
||||
*
|
||||
|
|
@ -155,6 +199,61 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
.addScript(new ScriptResource().setScriptType("module")
|
||||
.setScriptUri(event.renderSupport().conletResource(
|
||||
type(), "VmViewer-functions.js"))));
|
||||
channel.session().put(RENDERED, new HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* On console configured.
|
||||
*
|
||||
* @param event the event
|
||||
* @param connection the console connection
|
||||
* @throws InterruptedException the interrupted exception
|
||||
*/
|
||||
@Handler
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
public void onConsoleConfigured(ConsoleConfigured event,
|
||||
ConsoleConnection connection) throws InterruptedException,
|
||||
IOException {
|
||||
@SuppressWarnings("unchecked")
|
||||
final var rendered = (Set<String>) connection.session().get(RENDERED);
|
||||
connection.session().remove(RENDERED);
|
||||
if (!syncPreviews(connection.session())) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean foundMissing = false;
|
||||
for (var vmName : accessibleVms(connection)) {
|
||||
if (rendered.contains(vmName)) {
|
||||
continue;
|
||||
}
|
||||
if (!foundMissing) {
|
||||
// Suspending to allow rendering of conlets to be noticed
|
||||
var failSafe = Components.schedule(t -> event.resumeHandling(),
|
||||
Duration.ofSeconds(1));
|
||||
event.suspendHandling(failSafe::cancel);
|
||||
connection.setAssociated(PENDING, event);
|
||||
foundMissing = true;
|
||||
}
|
||||
fire(new AddConletRequest(event.event().event().renderSupport(),
|
||||
VmViewer.class.getName(),
|
||||
RenderMode.asSet(RenderMode.Preview))
|
||||
.addProperty(VM_NAME_PROPERTY, vmName),
|
||||
connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On console prepared.
|
||||
*
|
||||
* @param event the event
|
||||
* @param connection the connection
|
||||
*/
|
||||
@Handler
|
||||
public void onConsolePrepared(ConsolePrepared event,
|
||||
ConsoleConnection connection) {
|
||||
if (syncPreviews(connection.session())) {
|
||||
connection.respond(new UpdateConletType(type()));
|
||||
}
|
||||
}
|
||||
|
||||
private String storagePath(Session session, String conletId) {
|
||||
|
|
@ -163,6 +262,20 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
+ "/" + VmViewer.class.getName() + "/" + conletId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<ViewerModel> createNewState(AddConletRequest event,
|
||||
ConsoleConnection connection, String conletId) throws Exception {
|
||||
var model = new ViewerModel(conletId);
|
||||
model.vmName = (String) event.properties().get(VM_NAME_PROPERTY);
|
||||
if (model.vmName != null) {
|
||||
model.setGenerated(true);
|
||||
}
|
||||
String jsonState = objectMapper.writeValueAsString(model);
|
||||
connection.respond(new KeyValueStoreUpdate().update(
|
||||
storagePath(connection.session(), model.getConletId()), jsonState));
|
||||
return Optional.of(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<ViewerModel> createStateRepresentation(Event<?> event,
|
||||
ConsoleConnection connection, String conletId) throws Exception {
|
||||
|
|
@ -197,32 +310,57 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", "unchecked" })
|
||||
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
||||
ConsoleConnection channel, String conletId, ViewerModel conletState)
|
||||
ConsoleConnection channel, String conletId, ViewerModel model)
|
||||
throws Exception {
|
||||
ResourceBundle resourceBundle = resourceBundle(channel.locale());
|
||||
Set<RenderMode> renderedAs = new HashSet<>();
|
||||
if (event.renderAs().contains(RenderMode.Preview)) {
|
||||
channel.associated(PENDING, Event.class)
|
||||
.ifPresent(e -> {
|
||||
e.resumeHandling();
|
||||
channel.setAssociated(PENDING, null);
|
||||
});
|
||||
|
||||
// Remove conlet if definition has been removed
|
||||
if (model.vmName() != null
|
||||
&& !channelManager.associated(model.vmName()).isPresent()) {
|
||||
channel.respond(
|
||||
new DeleteConlet(conletId, Collections.emptySet()));
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
// Don't render if user has not at least one permission
|
||||
if (model.vmName() != null
|
||||
&& channelManager.associated(model.vmName())
|
||||
.map(d -> permissions(d, channel.session()).isEmpty())
|
||||
.orElse(true)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
// Render
|
||||
Template tpl
|
||||
= freemarkerConfig().getTemplate("VmViewer-preview.ftl.html");
|
||||
channel.respond(new RenderConlet(type(), conletId,
|
||||
processTemplate(event, tpl,
|
||||
fmModel(event, channel, conletId, conletState)))
|
||||
fmModel(event, channel, conletId, model)))
|
||||
.setRenderAs(
|
||||
RenderMode.Preview.addModifiers(event.renderAs()))
|
||||
.setSupportedModes(MODES));
|
||||
.setSupportedModes(
|
||||
model.isGenerated() ? MODES_FOR_GENERATED : MODES));
|
||||
renderedAs.add(RenderMode.Preview);
|
||||
if (!Strings.isNullOrEmpty(conletState.vmName())) {
|
||||
updateConfig(channel, conletState);
|
||||
if (!Strings.isNullOrEmpty(model.vmName())) {
|
||||
Optional.ofNullable(channel.session().get(RENDERED))
|
||||
.ifPresent(s -> ((Set<String>) s).add(model.vmName()));
|
||||
updateConfig(channel, model);
|
||||
}
|
||||
}
|
||||
if (event.renderAs().contains(RenderMode.Edit)) {
|
||||
Template tpl = freemarkerConfig()
|
||||
.getTemplate("VmViewer-edit.ftl.html");
|
||||
var fmModel = fmModel(event, channel, conletId, conletState);
|
||||
fmModel.put("vmNames",
|
||||
channelManager.keys().stream().sorted().toList());
|
||||
var fmModel = fmModel(event, channel, conletId, model);
|
||||
fmModel.put("vmNames", accessibleVms(channel));
|
||||
channel.respond(new OpenModalDialog(type(), conletId,
|
||||
processTemplate(event, tpl, fmModel))
|
||||
.addOption("cancelable", true)
|
||||
|
|
@ -232,6 +370,21 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
return renderedAs;
|
||||
}
|
||||
|
||||
private List<String> accessibleVms(ConsoleConnection channel) {
|
||||
return channelManager.associated().stream()
|
||||
.filter(d -> !permissions(d, channel.session()).isEmpty())
|
||||
.map(d -> d.getMetadata().getName()).sorted().toList();
|
||||
}
|
||||
|
||||
private Set<Permission> permissions(VmDefinitionModel vmDef,
|
||||
Session session) {
|
||||
var user = WebConsoleUtils.userFromSession(session)
|
||||
.map(ConsoleUser::getName).orElse(null);
|
||||
var roles = WebConsoleUtils.rolesFromSession(session)
|
||||
.stream().map(ConsoleRole::getName).toList();
|
||||
return vmDef.permissionsFor(user, roles);
|
||||
}
|
||||
|
||||
private void updateConfig(ConsoleConnection channel, ViewerModel model) {
|
||||
channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "updateConfig", model.vmName()));
|
||||
|
|
@ -246,6 +399,9 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
try {
|
||||
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
||||
.readObject();
|
||||
def.setField("userPermissions",
|
||||
permissions(vmDef, channel.session()).stream()
|
||||
.map(Permission::toString).toList());
|
||||
channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "updateVmDefinition", def));
|
||||
} catch (JsonDecodeException e) {
|
||||
|
|
@ -279,8 +435,10 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||
throws JsonDecodeException, IOException {
|
||||
var vmDef = new K8sDynamicModel(channel.client().getJSON()
|
||||
var vmDef = new VmDefinitionModel(channel.client().getJSON()
|
||||
.getGson(), event.vmDefinition().data());
|
||||
GsonPtr.to(vmDef.data()).to("metadata").get(JsonObject.class)
|
||||
.remove("managedFields");
|
||||
var vmName = vmDef.getMetadata().getName();
|
||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||
channelManager.remove(vmName);
|
||||
|
|
@ -291,7 +449,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
var connection = entry.getKey();
|
||||
for (var conletId : entry.getValue()) {
|
||||
var model = stateFromSession(connection.session(), conletId);
|
||||
if (model.isEmpty() || !model.get().vmName().equals(vmName)) {
|
||||
if (model.isEmpty()
|
||||
|| !Objects.areEqual(model.get().vmName(), vmName)) {
|
||||
continue;
|
||||
}
|
||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||
|
|
@ -311,11 +470,15 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
ConsoleConnection channel, ViewerModel model)
|
||||
throws Exception {
|
||||
event.stop();
|
||||
var vmName = event.params().asString(0);
|
||||
var vmChannel = channelManager.channel(vmName).orElse(null);
|
||||
if (vmChannel == null) {
|
||||
var both = Optional.ofNullable(event.params().asString(0))
|
||||
.flatMap(vm -> channelManager.both(vm));
|
||||
if (both.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var vmChannel = both.get().channel;
|
||||
var vmDef = both.get().associated;
|
||||
var vmName = vmDef.metadata().getName();
|
||||
var perms = permissions(vmDef, channel.session());
|
||||
switch (event.method()) {
|
||||
case "selectedVm":
|
||||
model.setVmName(event.params().asString(0));
|
||||
|
|
@ -325,15 +488,22 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
updateConfig(channel, model);
|
||||
break;
|
||||
case "start":
|
||||
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||
if (perms.contains(Permission.START)) {
|
||||
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||
}
|
||||
break;
|
||||
case "stop":
|
||||
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||
if (perms.contains(Permission.STOP)) {
|
||||
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||
}
|
||||
break;
|
||||
case "openConsole":
|
||||
channelManager.channel(vmName).ifPresent(
|
||||
vc -> fire(Event.onCompletion(new GetDisplayPassword(vmName),
|
||||
ds -> openConsole(vmName, channel, model, ds)), vc));
|
||||
if (perms.contains(Permission.ACCESS_CONSOLE)) {
|
||||
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef),
|
||||
e -> e.password().ifPresent(
|
||||
pw -> openConsole(vmName, channel, model, pw)));
|
||||
fire(pwQuery, vmChannel);
|
||||
}
|
||||
break;
|
||||
default:// ignore
|
||||
break;
|
||||
|
|
@ -341,7 +511,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
}
|
||||
|
||||
private void openConsole(String vmName, ConsoleConnection connection,
|
||||
ViewerModel model, GetDisplayPassword pwQuery) {
|
||||
ViewerModel model, String password) {
|
||||
var vmDef = channelManager.associated(vmName).orElse(null);
|
||||
if (vmDef == null) {
|
||||
return;
|
||||
|
|
@ -362,10 +532,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
StringBuffer data = new StringBuffer(100)
|
||||
.append("[virt-viewer]\ntype=spice\nhost=")
|
||||
.append(addr.get().getHostAddress()).append("\nport=")
|
||||
.append(Integer.toString(port.get().getAsInt())).append('\n');
|
||||
pwQuery.password().ifPresent(p -> {
|
||||
data.append("password=").append(p).append('\n');
|
||||
});
|
||||
.append(Integer.toString(port.get().getAsInt()))
|
||||
.append("\npassword=").append(password).append('\n');
|
||||
proxyUrl.map(JsonPrimitive::getAsString).ifPresent(u -> {
|
||||
if (!Strings.isNullOrEmpty(u)) {
|
||||
data.append("proxy=").append(u).append('\n');
|
||||
|
|
@ -418,9 +586,11 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
/**
|
||||
* The Class VmsModel.
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataClass")
|
||||
public static class ViewerModel extends ConletBaseModel {
|
||||
|
||||
private String vmName;
|
||||
private boolean generated;
|
||||
|
||||
/**
|
||||
* Instantiates a new vms model.
|
||||
|
|
@ -450,5 +620,23 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
|||
this.vmName = vmName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if is generated.
|
||||
*
|
||||
* @return the generated
|
||||
*/
|
||||
public boolean isGenerated() {
|
||||
return generated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the generated.
|
||||
*
|
||||
* @param generated the generated to set
|
||||
*/
|
||||
public void setGenerated(boolean generated) {
|
||||
this.generated = generated;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,17 +84,22 @@ window.orgJDrupesVmOperatorVmViewer.initPreview = (previewDom: HTMLElement,
|
|||
<tbody>
|
||||
<tr>
|
||||
<td><img role=button
|
||||
:aria-disabled="!vmDef.running || !vmDef.userPermissions
|
||||
|| !vmDef.userPermissions.includes('accessConsole')"
|
||||
v-on:click="vmAction(vmDef.name, 'openConsole')"
|
||||
:src="resourceBase + (vmDef.running
|
||||
? 'computer.svg' : 'computer-off.svg')"></td>
|
||||
? 'computer.svg' : 'computer-off.svg')"
|
||||
:title="localize('Open console')"></td>
|
||||
<td v-if="vmDef.spec"
|
||||
class="jdrupes-vmoperator-vmviewer-preview-action-list">
|
||||
<span role="button" v-if="vmDef.spec.vm.state != 'Running'"
|
||||
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
|
||||
<span role="button" v-if="vmDef.spec.vm.state != 'Running'"
|
||||
:aria-disabled="!vmDef.userPermissions.includes('start')"
|
||||
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
|
||||
v-on:click="vmAction(vmDef.name, 'start')"></span>
|
||||
<span role="button" v-else class="fa fa-play"
|
||||
aria-disabled="true" :title="localize('Start VM')"></span>
|
||||
<span role="button" v-if="vmDef.spec.vm.state != 'Stopped'"
|
||||
:aria-disabled="!vmDef.userPermissions.includes('stop')"
|
||||
tabindex="0" class="fa fa-stop" :title="localize('Stop VM')"
|
||||
v-on:click="vmAction(vmDef.name, 'stop')"></span>
|
||||
<span role="button" v-else class="fa fa-stop"
|
||||
|
|
|
|||
|
|
@ -19,24 +19,27 @@
|
|||
/*
|
||||
* Conlet specific styles.
|
||||
*/
|
||||
.jdrupes-vmoperator-vmviewer-preview img {
|
||||
height: 3em;
|
||||
padding: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--darkening);
|
||||
.jdrupes-vmoperator-vmviewer-preview {
|
||||
|
||||
[role=button] {
|
||||
padding: 0.25rem;
|
||||
|
||||
&:not([aria-disabled]):hover, &[aria-disabled='false']:hover {
|
||||
box-shadow: var(--darkening);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
height: 3em;
|
||||
padding: 0.25rem;
|
||||
|
||||
&[aria-disabled=''], &[aria-disabled='true'] {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jdrupes-vmoperator-vmviewer-preview-action-list {
|
||||
white-space: nowrap;
|
||||
|
||||
[role=button] {
|
||||
padding: 0.25rem;
|
||||
|
||||
&:not([aria-disabled]):hover {
|
||||
box-shadow: var(--darkening);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue