Add viewer conlet (#25)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled

This commit is contained in:
Michael N. Lipp 2024-05-27 12:57:01 +02:00 committed by GitHub
parent b6f0299932
commit a6525a2289
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 2642 additions and 250 deletions

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<fileset-config file-format-version="1.2.0" simple-config="false" sync-formatter="false">
<local-check-config name="Project Checks" location="/VM-Operator/checkstyle.xml" type="project" description="">
<additional-data name="protect-config-file" value="false"/>
</local-check-config>
<fileset name="all" enabled="true" check-config-name="Project Checks" local="true">
<file-match-pattern match-pattern="." include-pattern="true"/>
</fileset>
</fileset-config>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<eclipse-pmd xmlns="http://acanda.ch/eclipse-pmd/0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://acanda.ch/eclipse-pmd/0.8 http://acanda.ch/eclipse-pmd/eclipse-pmd-0.8.xsd">
<analysis enabled="true" />
<rulesets>
<ruleset name="Custom Rules" ref="VM-Operator/ruleset.xml" refcontext="workspace" />
</rulesets>
</eclipse-pmd>

View file

@ -0,0 +1 @@
rollup.config.mjs

View file

@ -0,0 +1,15 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": ["./tsconfig.json"] },
"plugins": [
"@typescript-eslint"
],
"rules": {
"constructor-super": "off"
}
}

View file

@ -0,0 +1,4 @@
/bin/
/bin_test/
/generated/
/build/

View file

@ -0,0 +1,10 @@
build.commands=org.eclipse.jdt.core.javabuilder
connection.arguments=
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.java.home=null
connection.jvm.arguments=
connection.project.dir=..
derived.resources=.gradle,generated
eclipse.preferences.version=1
natures=org.eclipse.jdt.groovy.core.groovyNature,org.eclipse.jdt.core.javanature
project.path=\:org.jgrapes.osgi.conlets.services

View file

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View file

@ -0,0 +1,2 @@
eclipse.preferences.version=1
line.separator=\n

View file

@ -0,0 +1,63 @@
eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
formatter_profile=_JGrapes
formatter_settings_version=13
sp_cleanup.add_default_serial_version_id=true
sp_cleanup.add_generated_serial_version_id=false
sp_cleanup.add_missing_annotations=true
sp_cleanup.add_missing_deprecated_annotations=true
sp_cleanup.add_missing_methods=false
sp_cleanup.add_missing_nls_tags=false
sp_cleanup.add_missing_override_annotations=true
sp_cleanup.add_missing_override_annotations_interface_methods=true
sp_cleanup.add_serial_version_id=false
sp_cleanup.always_use_blocks=true
sp_cleanup.always_use_parentheses_in_expressions=false
sp_cleanup.always_use_this_for_non_static_field_access=false
sp_cleanup.always_use_this_for_non_static_method_access=false
sp_cleanup.convert_functional_interfaces=false
sp_cleanup.convert_to_enhanced_for_loop=false
sp_cleanup.correct_indentation=false
sp_cleanup.format_source_code=true
sp_cleanup.format_source_code_changes_only=false
sp_cleanup.insert_inferred_type_arguments=false
sp_cleanup.make_local_variable_final=true
sp_cleanup.make_parameters_final=false
sp_cleanup.make_private_fields_final=true
sp_cleanup.make_type_abstract_if_missing_method=false
sp_cleanup.make_variable_declarations_final=false
sp_cleanup.never_use_blocks=false
sp_cleanup.never_use_parentheses_in_expressions=true
sp_cleanup.on_save_use_additional_actions=false
sp_cleanup.organize_imports=false
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
sp_cleanup.remove_private_constructors=true
sp_cleanup.remove_redundant_type_arguments=false
sp_cleanup.remove_trailing_whitespaces=false
sp_cleanup.remove_trailing_whitespaces_all=true
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
sp_cleanup.remove_unnecessary_casts=true
sp_cleanup.remove_unnecessary_nls_tags=false
sp_cleanup.remove_unused_imports=false
sp_cleanup.remove_unused_local_variables=false
sp_cleanup.remove_unused_private_fields=true
sp_cleanup.remove_unused_private_members=false
sp_cleanup.remove_unused_private_methods=true
sp_cleanup.remove_unused_private_types=true
sp_cleanup.sort_members=false
sp_cleanup.sort_members_all=false
sp_cleanup.use_anonymous_class_creation=false
sp_cleanup.use_blocks=false
sp_cleanup.use_blocks_only_for_return_and_throw=false
sp_cleanup.use_lambda=true
sp_cleanup.use_parentheses_in_expressions=false
sp_cleanup.use_this_for_non_static_field_access=false
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
sp_cleanup.use_this_for_non_static_method_access=false
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
sp_jautodoc.cleanup.add_header=false
sp_jautodoc.cleanup.replace_header=false

View file

@ -0,0 +1,57 @@
plugins {
id 'org.jdrupes.vmoperator.java-library-conventions'
}
dependencies {
implementation project(':org.jdrupes.vmoperator.manager.events')
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.3.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)'
}
apply plugin: 'com.github.node-gradle.node'
node {
download = true
}
task extractDependencies(type: Copy) {
from configurations.compileClasspath
.findAll{ it.name.contains('.provider.')
|| it.name.contains('org.jgrapes.webconsole.base')
}
.collect{ zipTree (it) }
exclude '*.class'
into 'build/unpacked'
duplicatesStrategy 'include'
}
task compileTs(type: NodeTask) {
dependsOn ':npmInstall'
dependsOn extractDependencies
inputs.dir project.file('src')
inputs.file project.file('tsconfig.json')
inputs.file project.file('rollup.config.mjs')
outputs.dir project.file('build/generated/resources')
script = file("${rootProject.rootDir}/node_modules/rollup/dist/bin/rollup")
args = ["-c"]
}
sourceSets {
main {
resources {
srcDir project.file('build/generated/resources')
}
}
}
processResources {
dependsOn compileTs
}
eclipse {
autoBuildTasks compileTs
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
org.jdrupes.vmoperator.vmviewer.VmViewerFactory

View file

@ -0,0 +1,20 @@
<div title="${_("conletName")}" class="jdrupes-vmoperator-vmviewer-edit"
data-jgwc-on-load="orgJDrupesVmOperatorVmViewer.initEdit"
data-jgwc-on-action="orgJDrupesVmOperatorVmViewer.applyEdit"
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
<form :id="formId" ref="formDom" onsubmit="return false;">
<section>
<span>{{ localize("Select VM") }}</span>
<p>
<label>
<span>{{ localize("VM") }}</span>
<select v-model="vmNameInput">
<#list vmNames as name>
<option value="${name}">${name}</option>
</#list>
</select>
</label>
</p>
</section>
</form>
</div>

View file

@ -0,0 +1,31 @@
/*
* VM-Operator
* Copyright (C) 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
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
"use strict";
const l10nBundles = new Map();
let entries = null;
// <#list supportedLanguages() as l>
entries = new Map();
l10nBundles.set("${l.locale.toLanguageTag()}", entries);
// <#list l.l10nBundle.keys as key>
entries.set("${key}", "${l.l10nBundle.getString(key)}");
// </#list>
// </#list>
export default l10nBundles;

View file

@ -0,0 +1,6 @@
<div class="jdrupes-vmoperator-vmviewer jdrupes-vmoperator-vmviewer-preview"
data-conlet-grid-rows="2" data-conlet-grid-columns="2"
data-jgwc-on-load="orgJDrupesVmOperatorVmViewer.initPreview"
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps"
data-conlet-resource-base="${conletResource('')}">
</div>

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="800"
height="533.33331"
viewBox="0 0 24 15.999999"
version="1.1"
id="svg1"
sodipodi:docname="computer-off.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<linearGradient
id="swatch3"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="0"
id="stop3" />
</linearGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.3435029"
inkscape:cx="377.74389"
inkscape:cy="227.01849"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
id="rect1"
style="fill-opacity:1;stroke:#000000;stroke-width:1.97262;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;paint-order:fill markers stroke;fill:#545454"
d="M 3.0038192,0.98808897 H 20.99618 V 13.006705 H 3.0038192 Z" />
<rect
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.00306926;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="rect2"
width="23.995173"
height="2.0017407"
x="0.0039473679"
y="13.998839" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="800"
height="533.33331"
viewBox="0 0 24 15.999999"
version="1.1"
id="svg1"
sodipodi:docname="computer.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<linearGradient
id="swatch3"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="0"
id="stop3" />
</linearGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.3435029"
inkscape:cx="377.74389"
inkscape:cy="227.01849"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
id="rect1"
style="fill-opacity:0;stroke:#000000;stroke-width:1.97262;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;paint-order:fill markers stroke"
d="M 3.0038192,0.98808897 H 20.99618 V 13.006705 H 3.0038192 Z" />
<rect
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.00306926;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="rect2"
width="23.995173"
height="2.0017407"
x="0.0039473679"
y="13.998839" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,3 @@
conletName = VM Console
okayLabel = Apply and Close

View file

@ -0,0 +1,7 @@
conletName = VM-Konsole
okayLabel = Anwenden und Schließen
Select\ VM = VM auswählen
Start\ VM = VM Starten
Stop\ VM = VM Anhalten

View file

@ -0,0 +1,35 @@
import typescript from 'rollup-plugin-typescript2';
import postcss from 'rollup-plugin-postcss';
let packagePath = "org/jdrupes/vmoperator/vmviewer";
let baseName = "VmViewer"
let module = "build/generated/resources/" + packagePath
+ "/" + baseName + "-functions.js";
let pathsMap = {
"aash-plugin": "../../page-resource/aash-vue-components/lib/aash-vue-components.js",
"jgconsole": "../../console-base-resource/jgconsole.js",
"jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js",
"l10nBundles": "./" + baseName + "-l10nBundles.ftl.js",
"vue": "../../page-resource/vue/vue.esm-browser.js"
}
export default {
external: ['aash-plugin', 'jgconsole', 'jgwc', 'l10nBundles', 'vue', 'chartjs'],
input: "src/" + packagePath + "/browser/" + baseName + "-functions.ts",
output: [
{
format: "esm",
file: module,
sourcemap: true,
sourcemapPathTransform: (relativeSourcePath, _sourcemapPath) => {
return relativeSourcePath.replace(/^([^/]*\/){12}/, "./");
},
paths: pathsMap
}
],
plugins: [
typescript(),
postcss()
]
};

View file

@ -0,0 +1,454 @@
/*
* VM-Operator
* Copyright (C) 2023 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
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.vmviewer;
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.JsonPrimitive;
import freemarker.core.ParseException;
import freemarker.template.MalformedTemplateNameException;
import freemarker.template.Template;
import freemarker.template.TemplateNotFoundException;
import io.kubernetes.client.util.Strings;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.logging.Level;
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.manager.events.ChannelCache;
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
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.Event;
import org.jgrapes.core.Manager;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.http.Session;
import org.jgrapes.util.events.ConfigurationUpdate;
import org.jgrapes.util.events.KeyValueStoreQuery;
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.ConsoleUser;
import org.jgrapes.webconsole.base.WebConsoleUtils;
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.ConsoleReady;
import org.jgrapes.webconsole.base.events.DeleteConlet;
import org.jgrapes.webconsole.base.events.NotifyConletModel;
import org.jgrapes.webconsole.base.events.NotifyConletView;
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.freemarker.FreeMarkerConlet;
/**
* The Class VmConlet.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
"PMD.CouplingBetweenObjects" })
public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
private static final Set<RenderMode> MODES = RenderMode.asSet(
RenderMode.Preview, RenderMode.Edit);
private final ChannelCache<String, VmChannel,
K8sDynamicModel> channelManager = new ChannelCache<>();
private static ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule());
private Class<?> preferredIpVersion = Inet4Address.class;
/**
* The periodically generated update event.
*/
public static class Update extends Event<Void> {
}
/**
* Creates a new component with its channel set to the given channel.
*
* @param componentChannel the channel that the component's handlers listen
* on by default and that {@link Manager#fire(Event, Channel...)}
* sends the event to
*/
public VmViewer(Channel componentChannel) {
super(componentChannel);
}
/**
* Configure the component.
*
* @param event the event
*/
@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;
}
});
}
/**
* On {@link ConsoleReady}, fire the {@link AddConletType}.
*
* @param event the event
* @param channel the channel
* @throws TemplateNotFoundException the template not found exception
* @throws MalformedTemplateNameException the malformed template name
* exception
* @throws ParseException the parse exception
* @throws IOException Signals that an I/O exception has occurred.
*/
@Handler
public void onConsoleReady(ConsoleReady event, ConsoleConnection channel)
throws TemplateNotFoundException, MalformedTemplateNameException,
ParseException, IOException {
// Add conlet resources to page
channel.respond(new AddConletType(type())
.setDisplayNames(
localizations(channel.supportedLocales(), "conletName"))
.addRenderMode(RenderMode.Preview)
.addScript(new ScriptResource().setScriptType("module")
.setScriptUri(event.renderSupport().conletResource(
type(), "VmViewer-functions.js"))));
}
private String storagePath(Session session, String conletId) {
return "/" + WebConsoleUtils.userFromSession(session)
.map(ConsoleUser::getName).orElse("")
+ "/" + VmViewer.class.getName() + "/" + conletId;
}
@Override
protected Optional<ViewerModel> createStateRepresentation(Event<?> event,
ConsoleConnection connection, String conletId) throws Exception {
var model = new ViewerModel(conletId);
String jsonState = objectMapper.writeValueAsString(model);
connection.respond(new KeyValueStoreUpdate().update(
storagePath(connection.session(), model.getConletId()), jsonState));
return Optional.of(model);
}
@Override
@SuppressWarnings("PMD.EmptyCatchBlock")
protected Optional<ViewerModel> recreateState(Event<?> event,
ConsoleConnection channel, String conletId) throws Exception {
KeyValueStoreQuery query = new KeyValueStoreQuery(
storagePath(channel.session(), conletId), channel);
newEventPipeline().fire(query, channel);
try {
if (!query.results().isEmpty()) {
var json = query.results().get(0).values().stream().findFirst()
.get();
ViewerModel model
= objectMapper.readValue(json, ViewerModel.class);
return Optional.of(model);
}
} catch (InterruptedException e) {
// Means we have no result.
}
// Fall back to creating default state.
return createStateRepresentation(event, channel, conletId);
}
@Override
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
ConsoleConnection channel, String conletId, ViewerModel conletState)
throws Exception {
ResourceBundle resourceBundle = resourceBundle(channel.locale());
Set<RenderMode> renderedAs = new HashSet<>();
if (event.renderAs().contains(RenderMode.Preview)) {
Template tpl
= freemarkerConfig().getTemplate("VmViewer-preview.ftl.html");
channel.respond(new RenderConlet(type(), conletId,
processTemplate(event, tpl,
fmModel(event, channel, conletId, conletState)))
.setRenderAs(
RenderMode.Preview.addModifiers(event.renderAs()))
.setSupportedModes(MODES));
renderedAs.add(RenderMode.Preview);
if (!Strings.isNullOrEmpty(conletState.vmName())) {
updateConfig(channel, conletState);
}
}
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());
channel.respond(new OpenModalDialog(type(), conletId,
processTemplate(event, tpl, fmModel))
.addOption("cancelable", true)
.addOption("okayLabel",
resourceBundle.getString("okayLabel")));
}
return renderedAs;
}
private void updateConfig(ConsoleConnection channel, ViewerModel model) {
channel.respond(new NotifyConletView(type(),
model.getConletId(), "updateConfig", model.vmName()));
updateVmDef(channel, model);
}
private void updateVmDef(ConsoleConnection channel, ViewerModel model) {
if (Strings.isNullOrEmpty(model.vmName())) {
return;
}
channelManager.associated(model.vmName()).ifPresent(vmDef -> {
try {
var def = JsonBeanDecoder.create(vmDef.data().toString())
.readObject();
channel.respond(new NotifyConletView(type(),
model.getConletId(), "updateVmDefinition", def));
} catch (JsonDecodeException e) {
logger.log(Level.SEVERE, e,
() -> "Failed to serialize VM definition");
}
});
}
@Override
protected void doConletDeleted(ConletDeleted event,
ConsoleConnection channel, String conletId, ViewerModel conletState)
throws Exception {
if (event.renderModes().isEmpty()) {
channel.respond(new KeyValueStoreUpdate().delete(
storagePath(channel.session(), conletId)));
}
}
/**
* Track the VM definitions.
*
* @param event the event
* @param channel the channel
* @throws JsonDecodeException the json decode exception
* @throws IOException
*/
@Handler(namedChannels = "manager")
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
"PMD.ConfusingArgumentToVarargsMethod" })
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
throws JsonDecodeException, IOException {
var vmDef = new K8sDynamicModel(channel.client().getJSON()
.getGson(), event.vmDefinition().data());
var vmName = vmDef.getMetadata().getName();
if (event.type() == K8sObserver.ResponseType.DELETED) {
channelManager.remove(vmName);
} else {
channelManager.put(vmName, channel, vmDef);
}
for (var entry : conletIdsByConsoleConnection().entrySet()) {
var connection = entry.getKey();
for (var conletId : entry.getValue()) {
var model = stateFromSession(connection.session(), conletId);
if (model.isEmpty() || !model.get().vmName().equals(vmName)) {
continue;
}
if (event.type() == K8sObserver.ResponseType.DELETED) {
connection.respond(
new DeleteConlet(conletId, Collections.emptySet()));
} else {
updateVmDef(connection, model.get());
}
}
}
}
@Override
@SuppressWarnings({ "PMD.AvoidDecimalLiteralsInBigDecimalConstructor",
"PMD.ConfusingArgumentToVarargsMethod" })
protected void doUpdateConletState(NotifyConletModel event,
ConsoleConnection channel, ViewerModel model)
throws Exception {
event.stop();
var vmName = event.params().asString(0);
var vmChannel = channelManager.channel(vmName).orElse(null);
if (vmChannel == null) {
return;
}
switch (event.method()) {
case "selectedVm":
model.setVmName(event.params().asString(0));
String jsonState = objectMapper.writeValueAsString(model);
channel.respond(new KeyValueStoreUpdate().update(storagePath(
channel.session(), model.getConletId()), jsonState));
updateConfig(channel, model);
break;
case "start":
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
break;
case "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));
break;
default:// ignore
break;
}
}
private void openConsole(String vmName, ConsoleConnection connection,
ViewerModel model, GetDisplayPassword pwQuery) {
var vmDef = channelManager.associated(vmName).orElse(null);
if (vmDef == null) {
return;
}
var addr = displayIp(vmDef);
if (addr.isEmpty()) {
logger.severe(() -> "Failed to find display IP for " + vmName);
return;
}
var port = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec",
"vm", "display", "spice", "port");
if (port.isEmpty()) {
logger.severe(() -> "No port defined for display of " + vmName);
return;
}
var proxyUrl = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec",
"vm", "display", "spice", "proxyUrl");
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');
});
proxyUrl.map(JsonPrimitive::getAsString).ifPresent(u -> {
if (!Strings.isNullOrEmpty(u)) {
data.append("proxy=").append(u).append('\n');
}
});
connection.respond(new NotifyConletView(type(),
model.getConletId(), "openConsole", "application/x-virt-viewer",
Base64.getEncoder().encodeToString(data.toString().getBytes())));
}
private Optional<InetAddress> displayIp(K8sDynamicModel vmDef) {
var server = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec",
"vm", "display", "spice", "server");
if (server.isPresent()) {
var srv = server.get().getAsString();
try {
var addr = InetAddress.getByName(srv);
logger.fine(() -> "Using IP address from CRD for "
+ vmDef.getMetadata().getName() + ": " + addr);
return Optional.of(addr);
} catch (UnknownHostException e) {
logger.log(Level.SEVERE, e, () -> "Invalid server address "
+ srv + ": " + e.getMessage());
return Optional.empty();
}
}
var addrs = GsonPtr.to(vmDef.data()).getAsListOf(JsonPrimitive.class,
"nodeAddresses").stream().map(JsonPrimitive::getAsString)
.map(a -> {
try {
return InetAddress.getByName(a);
} catch (UnknownHostException e) {
logger.warning(() -> "Invalid IP address: " + a);
return null;
}
}).filter(a -> a != null).toList();
logger.fine(() -> "Known IP addresses for "
+ vmDef.getMetadata().getName() + ": " + addrs);
return addrs.stream()
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
.findFirst().or(() -> addrs.stream().findFirst());
}
@Override
protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
String conletId) throws Exception {
return true;
}
/**
* The Class VmsModel.
*/
public static class ViewerModel extends ConletBaseModel {
private String vmName;
/**
* Instantiates a new vms model.
*
* @param conletId the conlet id
*/
public ViewerModel(@JsonProperty("conletId") String conletId) {
super(conletId);
}
/**
* Gets the vm name.
*
* @return the vmName
*/
@JsonGetter("vmName")
public String vmName() {
return vmName;
}
/**
* Sets the vm name.
*
* @param vmName the vmName to set
*/
public void setVmName(String vmName) {
this.vmName = vmName;
}
}
}

View file

@ -0,0 +1,54 @@
/*
* VM-Operator
* Copyright (C) 2023 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
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.vmviewer;
import java.util.Map;
import java.util.Optional;
import org.jgrapes.core.Channel;
import org.jgrapes.core.ComponentType;
import org.jgrapes.webconsole.base.ConletComponentFactory;
/**
* The factory service for {@link VmViewer}s.
*/
public class VmViewerFactory implements ConletComponentFactory {
/*
* (non-Javadoc)
*
* @see org.jgrapes.core.ComponentFactory#componentType()
*/
@Override
public Class<? extends ComponentType> componentType() {
return VmViewer.class;
}
/*
* (non-Javadoc)
*
* @see org.jgrapes.core.ComponentFactory#create(org.jgrapes.core.Channel,
* java.util.Map)
*/
@Override
public Optional<ComponentType> create(Channel componentChannel,
Map<?, ?> properties) {
return Optional.of(new VmViewer(componentChannel));
}
}

View file

@ -0,0 +1,206 @@
/*
* VM-Operator
* Copyright (C) 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
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
reactive, ref, createApp, computed, watch
} from "vue";
import JGConsole from "jgconsole";
import JgwcPlugin, { JGWC } from "jgwc";
import { provideApi, getApi } from "aash-plugin";
import l10nBundles from "l10nBundles";
import "./VmViewer-style.scss";
// For global access
declare global {
interface Window {
orgJDrupesVmOperatorVmViewer: {
initPreview?: (previewDom: HTMLElement, isUpdate: boolean) => void,
initEdit?: (viewDom: HTMLElement, isUpdate: boolean) => void
applyEdit?: (viewDom: HTMLElement, apply: boolean) => void
}
}
}
window.orgJDrupesVmOperatorVmViewer = {};
interface Api {
/* eslint-disable @typescript-eslint/no-explicit-any */
vmName: string;
vmDefinition: any;
}
const localize = (key: string) => {
return JGConsole.localize(
l10nBundles, JGWC.lang(), key);
};
window.orgJDrupesVmOperatorVmViewer.initPreview = (previewDom: HTMLElement,
_isUpdate: boolean) => {
const app = createApp({
setup(_props: object) {
const conletId = (<HTMLElement>previewDom.closest(
"[data-conlet-id]")!).dataset["conletId"]!;
const resourceBase = (<HTMLElement>previewDom.closest(
"*[data-conlet-resource-base]")!).dataset.conletResourceBase;
const previewApi: Api = reactive({
vmName: "",
vmDefinition: {}
});
const vmDef = computed(() => previewApi.vmDefinition);
watch(() => previewApi.vmName, (name: string) => {
if (name !== "") {
JGConsole.instance.updateConletTitle(conletId, name);
}
});
provideApi(previewDom, previewApi);
const vmAction = (vmName: string, action: string) => {
JGConsole.notifyConletModel(conletId, action, vmName);
};
return { localize, resourceBase, vmDef, vmAction };
},
template: `
<table>
<tbody>
<tr>
<td><img role=button
v-on:click="vmAction(vmDef.name, 'openConsole')"
:src="resourceBase + (vmDef.running
? 'computer.svg' : 'computer-off.svg')"></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')"
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'"
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"
aria-disabled="true" :title="localize('Stop VM')"></span>
</td>
<td v-else>
</td>
</tr>
</tbody>
</table>`
});
app.use(JgwcPlugin, []);
app.config.globalProperties.window = window;
app.mount(previewDom);
};
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmviewer.VmViewer",
"updateConfig", function(conletId: string, vmName: string) {
const conlet = JGConsole.findConletPreview(conletId);
if (!conlet) {
return;
}
const api = getApi<Api>(conlet.element().querySelector(
":scope .jdrupes-vmoperator-vmviewer-preview"))!;
api.vmName = vmName;
});
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmviewer.VmViewer",
"updateVmDefinition", function(conletId: string, vmDefinition: any) {
const conlet = JGConsole.findConletPreview(conletId);
if (!conlet) {
return;
}
const api = getApi<Api>(conlet.element().querySelector(
":scope .jdrupes-vmoperator-vmviewer-preview"))!;
// Add some short-cuts for rendering
vmDefinition.name = vmDefinition.metadata.name;
vmDefinition.currentCpus = vmDefinition.status.cpus;
vmDefinition.currentRam = Number(vmDefinition.status.ram);
for (const condition of vmDefinition.status.conditions) {
if (condition.type === "Running") {
vmDefinition.running = condition.status === "True";
vmDefinition.runningConditionSince
= new Date(condition.lastTransitionTime);
break;
}
}
api.vmDefinition = vmDefinition;
});
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmviewer.VmViewer",
"openConsole", function(_conletId: string, mimeType: string, data: string) {
let target = document.getElementById(
"org.jdrupes.vmoperator.vmviewer.VmViewer.target");
if (!target) {
target = document.createElement("iframe");
target.id = "org.jdrupes.vmoperator.vmviewer.VmViewer.target";
target.setAttribute("name", target.id);
target.setAttribute("style", "display: none;");
document.querySelector("body")!.append(target);
}
const url = "data:" + mimeType + ";base64," + data;
window.open(url, target.id);
});
window.orgJDrupesVmOperatorVmViewer.initEdit = (dialogDom: HTMLElement,
isUpdate: boolean) => {
if (isUpdate) {
return;
}
const app = createApp({
setup() {
const formId = (<HTMLElement>dialogDom
.closest("*[data-conlet-id]")!).id + "-form";
const localize = (key: string) => {
return JGConsole.localize(
l10nBundles, JGWC.lang()!, key);
};
const vmNameInput = ref<string>("");
const conletId = (<HTMLElement>dialogDom.closest(
"[data-conlet-id]")!).dataset["conletId"]!;
const conlet = JGConsole.findConletPreview(conletId);
if (conlet) {
const api = getApi<Api>(conlet.element().querySelector(
":scope .jdrupes-vmoperator-vmviewer-preview"))!;
vmNameInput.value = api.vmName;
}
provideApi(dialogDom, vmNameInput);
return { formId, localize, vmNameInput };
}
});
app.use(JgwcPlugin);
app.mount(dialogDom);
}
window.orgJDrupesVmOperatorVmViewer.applyEdit =
(dialogDom: HTMLElement, apply: boolean) => {
if (!apply) {
return;
}
const conletId = (<HTMLElement>dialogDom.closest("[data-conlet-id]")!)
.dataset["conletId"]!;
const vmName = getApi<ref<string>>(dialogDom!)!.value;
JGConsole.notifyConletModel(conletId, "selectedVm", vmName);
}

View file

@ -0,0 +1,42 @@
/*
* VM-Operator
* Copyright (C) 2023 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
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Conlet specific styles.
*/
.jdrupes-vmoperator-vmviewer-preview img {
height: 3em;
padding: 0.25rem;
&:hover {
box-shadow: var(--darkening);
}
}
.jdrupes-vmoperator-vmviewer-preview-action-list {
white-space: nowrap;
[role=button] {
padding: 0.25rem;
&:not([aria-disabled]):hover {
box-shadow: var(--darkening);
}
}
}

View file

@ -0,0 +1 @@
export default new Map<string, Map<string, string>>();

View file

@ -0,0 +1,19 @@
/*
* VM-Operator
* Copyright (C) 2023 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
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.vmviewer;

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"sourceMap": true,
"inlineSources": true,
"declaration": true,
"importHelpers": true,
"strict": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"lib": ["DOM", "ES2020"],
"paths": {
"aash-plugin": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/aash-vue-components/lib/AashPlugin"],
"jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"],
"jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"],
"l10nBundles": ["./src/org/jdrupes/vmoperator/vmviewer/browser/l10nBundles-stub"],
"vue": ["./build/unpacked/org/jgrapes/webconsole/provider/vue/vue/vue"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "l10nBundles-stub.ts"]
}