Feature/web gui2 (#16)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
Add oveview and enhance.
This commit is contained in:
parent
8567a2f052
commit
6f45e7982a
32 changed files with 1382 additions and 250 deletions
|
|
@ -5,7 +5,7 @@ charset = utf-8
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.{md,yml,yaml}]
|
[*.{html,md,yml,yaml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ resources:
|
||||||
- vmop-image-repository-pvc.yaml
|
- vmop-image-repository-pvc.yaml
|
||||||
- vmop-config-map.yaml
|
- vmop-config-map.yaml
|
||||||
- vmop-deployment.yaml
|
- vmop-deployment.yaml
|
||||||
|
- vmop-service.yaml
|
||||||
- vmrunner-role.yaml
|
- vmrunner-role.yaml
|
||||||
- vmrunner-service-account.yaml
|
- vmrunner-service-account.yaml
|
||||||
- vmrunner-role-binding.yaml
|
- vmrunner-role-binding.yaml
|
||||||
|
|
|
||||||
12
deploy/vmop-service.yaml
Normal file
12
deploy/vmop-service.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: vm-operator
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 8080
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: vm-operator
|
||||||
|
app.kubernetes.io/component: manager
|
||||||
|
|
@ -23,15 +23,18 @@
|
||||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||||
"/RoleConfigurator":
|
"/RoleConfigurator":
|
||||||
rolesByUser:
|
rolesByUser:
|
||||||
|
# User admin has role admin
|
||||||
admin:
|
admin:
|
||||||
- admin
|
- admin
|
||||||
|
# All users have role other
|
||||||
"*":
|
"*":
|
||||||
- user
|
- other
|
||||||
replace: false
|
replace: false
|
||||||
"/RoleConletFilter":
|
"/RoleConletFilter":
|
||||||
conletTypesByRole:
|
conletTypesByRole:
|
||||||
user:
|
# Admins can use all conlets
|
||||||
- "!org.jgrapes.webconlet.sysinfo.SysInfoConlet"
|
|
||||||
- "*"
|
|
||||||
admin:
|
admin:
|
||||||
- "*"
|
- "*"
|
||||||
|
# Others cannot use any conlet (except login conlet to log out)
|
||||||
|
other:
|
||||||
|
- org.jgrapes.webconlet.locallogin.LoginConlet
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,37 @@ patches:
|
||||||
"/Reconciler":
|
"/Reconciler":
|
||||||
runnerData:
|
runnerData:
|
||||||
storageClassName: null
|
storageClassName: null
|
||||||
|
"/GuiSocketServer":
|
||||||
|
port: 8888
|
||||||
|
"/GuiHttpServer":
|
||||||
|
# This configures the GUI
|
||||||
|
"/ConsoleWeblet":
|
||||||
|
"/WebConsole":
|
||||||
|
"/LoginConlet":
|
||||||
|
users:
|
||||||
|
admin:
|
||||||
|
fullName: Administrator
|
||||||
|
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||||
|
test:
|
||||||
|
fullName: Test Account
|
||||||
|
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||||
|
"/RoleConfigurator":
|
||||||
|
rolesByUser:
|
||||||
|
# User admin has role admin
|
||||||
|
admin:
|
||||||
|
- admin
|
||||||
|
# All users have role other
|
||||||
|
"*":
|
||||||
|
- other
|
||||||
|
replace: false
|
||||||
|
"/RoleConletFilter":
|
||||||
|
conletTypesByRole:
|
||||||
|
# Admins can use all conlets
|
||||||
|
admin:
|
||||||
|
- "*"
|
||||||
|
# Others cannot use any conlet (except login conlet to log out)
|
||||||
|
other:
|
||||||
|
- org.jgrapes.webconlet.locallogin.LoginConlet
|
||||||
|
|
||||||
- target:
|
- target:
|
||||||
group: apps
|
group: apps
|
||||||
|
|
|
||||||
|
|
@ -22,22 +22,27 @@ import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a VM.
|
* Modifies a VM.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataClass")
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class StartVm extends Event<Void> {
|
public class ModifyVm extends Event<Void> {
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
private final String path;
|
||||||
|
private final Object value;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new start vm event.
|
* Instantiates a new modify vm event.
|
||||||
*
|
*
|
||||||
* @param channels the channels
|
* @param channels the channels
|
||||||
* @param name the name
|
* @param name the name
|
||||||
*/
|
*/
|
||||||
public StartVm(String name, Channel... channels) {
|
public ModifyVm(String name, String path, Object value,
|
||||||
|
Channel... channels) {
|
||||||
super(channels);
|
super(channels);
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.path = path;
|
||||||
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,4 +54,22 @@ public class StartVm extends Event<Void> {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the path.
|
||||||
|
*
|
||||||
|
* @return the path
|
||||||
|
*/
|
||||||
|
public String path() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value.
|
||||||
|
*
|
||||||
|
* @return the value
|
||||||
|
*/
|
||||||
|
public Object value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.manager.events;
|
|
||||||
|
|
||||||
import org.jgrapes.core.Channel;
|
|
||||||
import org.jgrapes.core.Event;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops a VM.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("PMD.DataClass")
|
|
||||||
public class StopVm extends Event<Void> {
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiates a new start vm event.
|
|
||||||
*
|
|
||||||
* @param channels the channels
|
|
||||||
* @param name the name
|
|
||||||
*/
|
|
||||||
public StopVm(String name, Channel... channels) {
|
|
||||||
super(channels);
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the name.
|
|
||||||
*
|
|
||||||
* @return the name
|
|
||||||
*/
|
|
||||||
public String name() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -39,7 +39,7 @@ dependencies {
|
||||||
|
|
||||||
application {
|
application {
|
||||||
applicationName = 'vm-manager'
|
applicationName = 'vm-manager'
|
||||||
applicationDefaultJvmArgs = ['-Xmx128m', '-XX:+UseParallelGC',
|
applicationDefaultJvmArgs = ['-Xmx64m', '-XX:+UseParallelGC',
|
||||||
'-Djava.util.logging.manager=org.jdrupes.vmoperator.util.LongLoggingManager'
|
'-Djava.util.logging.manager=org.jdrupes.vmoperator.util.LongLoggingManager'
|
||||||
]
|
]
|
||||||
// Define the main class for the application.
|
// Define the main class for the application.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<footer>
|
||||||
|
Copyright © Michael N. Lipp 2023
|
||||||
|
</footer>
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* 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.manager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.jgrapes.core.Channel;
|
||||||
|
import org.jgrapes.core.Component;
|
||||||
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
import org.jgrapes.webconsole.base.Conlet;
|
||||||
|
import org.jgrapes.webconsole.base.ConsoleConnection;
|
||||||
|
import org.jgrapes.webconsole.base.events.AddConletRequest;
|
||||||
|
import org.jgrapes.webconsole.base.events.ConsoleConfigured;
|
||||||
|
import org.jgrapes.webconsole.base.events.ConsoleReady;
|
||||||
|
import org.jgrapes.webconsole.base.events.RenderConlet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class AvoidEmptyPolicy extends Component {
|
||||||
|
|
||||||
|
private final String renderedFlagName = getClass().getName() + ".rendered";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new component with its channel set to the given channel.
|
||||||
|
*
|
||||||
|
* @param componentChannel
|
||||||
|
*/
|
||||||
|
public AvoidEmptyPolicy(Channel componentChannel) {
|
||||||
|
super(componentChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On console ready.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param connection the connection
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
public void onConsoleReady(ConsoleReady event,
|
||||||
|
ConsoleConnection connection) {
|
||||||
|
connection.session().put(renderedFlagName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On render conlet.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param connection the connection
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
public void onRenderConlet(RenderConlet event,
|
||||||
|
ConsoleConnection connection) {
|
||||||
|
connection.session().put(renderedFlagName, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On console configured.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param connection the console connection
|
||||||
|
* @throws InterruptedException the interrupted exception
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
public void onConsoleConfigured(ConsoleConfigured event,
|
||||||
|
ConsoleConnection connection) throws InterruptedException,
|
||||||
|
IOException {
|
||||||
|
if ((Boolean) connection.session().getOrDefault(
|
||||||
|
renderedFlagName, false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fire(new AddConletRequest(event.event().event().renderSupport(),
|
||||||
|
"org.jdrupes.vmoperator.vmconlet.VmConlet",
|
||||||
|
Conlet.RenderMode
|
||||||
|
.asSet(Conlet.RenderMode.Preview, Conlet.RenderMode.View)),
|
||||||
|
connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -30,8 +30,7 @@ import java.util.logging.Level;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
||||||
import org.jdrupes.vmoperator.common.K8s;
|
import org.jdrupes.vmoperator.common.K8s;
|
||||||
import org.jdrupes.vmoperator.manager.events.StartVm;
|
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.StopVm;
|
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Component;
|
import org.jgrapes.core.Component;
|
||||||
|
|
@ -152,30 +151,18 @@ public class Controller extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On start vm.
|
* On modify vm.
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
* @throws ApiException the api exception
|
* @throws ApiException the api exception
|
||||||
* @throws IOException Signals that an I/O exception has occurred.
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
public void onStartVm(StartVm event) throws ApiException, IOException {
|
public void onModigyVm(ModifyVm event) throws ApiException, IOException {
|
||||||
patchRunning(event.name(), true);
|
patchVmSpec(event.name(), event.path(), event.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void patchVmSpec(String name, String path, Object value)
|
||||||
* On stop vm.
|
|
||||||
*
|
|
||||||
* @param event the event
|
|
||||||
* @throws ApiException the api exception
|
|
||||||
* @throws IOException Signals that an I/O exception has occurred.
|
|
||||||
*/
|
|
||||||
@Handler
|
|
||||||
public void onStopVm(StopVm event) throws ApiException, IOException {
|
|
||||||
patchRunning(event.name(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void patchRunning(String name, boolean running)
|
|
||||||
throws ApiException, IOException {
|
throws ApiException, IOException {
|
||||||
var crApi = K8s.crApi(Config.defaultClient(), VM_OP_GROUP,
|
var crApi = K8s.crApi(Config.defaultClient(), VM_OP_GROUP,
|
||||||
VM_OP_KIND_VM, namespace, name);
|
VM_OP_KIND_VM, namespace, name);
|
||||||
|
|
@ -188,12 +175,13 @@ public class Controller extends Component {
|
||||||
// Patch running
|
// Patch running
|
||||||
PatchOptions patchOpts = new PatchOptions();
|
PatchOptions patchOpts = new PatchOptions();
|
||||||
patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
|
patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
String valueAsText = value instanceof String
|
||||||
|
? "\"" + value + "\""
|
||||||
|
: value.toString();
|
||||||
var res = crApi.get().patch(namespace, name,
|
var res = crApi.get().patch(namespace, name,
|
||||||
V1Patch.PATCH_FORMAT_JSON_PATCH,
|
V1Patch.PATCH_FORMAT_JSON_PATCH,
|
||||||
new V1Patch("[{\"op\": \"replace\", \"path\": "
|
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/"
|
||||||
+ "\"/spec/vm/state\", "
|
+ path + "\", \"value\": " + valueAsText + "}]"),
|
||||||
+ "\"value\": \"" + (running ? "Running" : "Stopped")
|
|
||||||
+ "\"}]"),
|
|
||||||
patchOpts);
|
patchOpts);
|
||||||
if (!res.isSuccess()) {
|
if (!res.isSuccess()) {
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ public class Manager extends Component {
|
||||||
console.attach(new BrowserLocalBackedKVStore(
|
console.attach(new BrowserLocalBackedKVStore(
|
||||||
console.channel(), consoleWeblet.prefix().getPath()));
|
console.channel(), consoleWeblet.prefix().getPath()));
|
||||||
console.attach(new KVStoreBasedConsolePolicy(console.channel()));
|
console.attach(new KVStoreBasedConsolePolicy(console.channel()));
|
||||||
|
console.attach(new AvoidEmptyPolicy(console.channel()));
|
||||||
console.attach(new RoleConfigurator(console.channel()));
|
console.attach(new RoleConfigurator(console.channel()));
|
||||||
console.attach(new RoleConletFilter(console.channel()));
|
console.attach(new RoleConletFilter(console.channel()));
|
||||||
console.attach(new LoginConlet(console.channel()));
|
console.attach(new LoginConlet(console.channel()));
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
import org.jdrupes.vmoperator.common.Convertions;
|
import org.jdrupes.vmoperator.common.Convertions;
|
||||||
import org.jdrupes.vmoperator.util.Dto;
|
import org.jdrupes.vmoperator.util.Dto;
|
||||||
import org.jdrupes.vmoperator.util.FsdUtils;
|
import org.jdrupes.vmoperator.util.FsdUtils;
|
||||||
|
|
@ -272,7 +273,7 @@ public class Configuration implements Dto {
|
||||||
private boolean checkRuntimeDir() {
|
private boolean checkRuntimeDir() {
|
||||||
// Runtime directory (sockets etc.)
|
// Runtime directory (sockets etc.)
|
||||||
if (runtimeDir == null) {
|
if (runtimeDir == null) {
|
||||||
var appDir = FsdUtils.runtimeDir(Runner.APP_NAME.replace("-", ""));
|
var appDir = FsdUtils.runtimeDir(APP_NAME.replace("-", ""));
|
||||||
if (!Files.exists(appDir) && appDir.toFile().mkdirs()) {
|
if (!Files.exists(appDir) && appDir.toFile().mkdirs()) {
|
||||||
try {
|
try {
|
||||||
// When appDir is derived from XDG_RUNTIME_DIR
|
// When appDir is derived from XDG_RUNTIME_DIR
|
||||||
|
|
@ -288,7 +289,7 @@ public class Configuration implements Dto {
|
||||||
runtimeDir));
|
runtimeDir));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runtimeDir = FsdUtils.runtimeDir(Runner.APP_NAME.replace("-", ""))
|
runtimeDir = FsdUtils.runtimeDir(APP_NAME.replace("-", ""))
|
||||||
.resolve(vm.name);
|
.resolve(vm.name);
|
||||||
runtimeDir.toFile().mkdir();
|
runtimeDir.toFile().mkdir();
|
||||||
swtpmSocket = runtimeDir.resolve("swtpm-sock");
|
swtpmSocket = runtimeDir.resolve("swtpm-sock");
|
||||||
|
|
@ -308,8 +309,8 @@ public class Configuration implements Dto {
|
||||||
private boolean checkDataDir() {
|
private boolean checkDataDir() {
|
||||||
// Data directory
|
// Data directory
|
||||||
if (dataDir == null) {
|
if (dataDir == null) {
|
||||||
dataDir = FsdUtils.dataHome(Runner.APP_NAME.replace("-", ""))
|
dataDir
|
||||||
.resolve(vm.name);
|
= FsdUtils.dataHome(APP_NAME.replace("-", "")).resolve(vm.name);
|
||||||
}
|
}
|
||||||
if (!Files.exists(dataDir)) {
|
if (!Files.exists(dataDir)) {
|
||||||
dataDir.toFile().mkdirs();
|
dataDir.toFile().mkdirs();
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import org.apache.commons.cli.CommandLineParser;
|
||||||
import org.apache.commons.cli.DefaultParser;
|
import org.apache.commons.cli.DefaultParser;
|
||||||
import org.apache.commons.cli.Option;
|
import org.apache.commons.cli.Option;
|
||||||
import org.apache.commons.cli.Options;
|
import org.apache.commons.cli.Options;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont;
|
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
|
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured;
|
import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured;
|
||||||
|
|
@ -178,8 +179,6 @@ import org.jgrapes.util.events.WatchFile;
|
||||||
"PMD.DataflowAnomalyAnalysis" })
|
"PMD.DataflowAnomalyAnalysis" })
|
||||||
public class Runner extends Component {
|
public class Runner extends Component {
|
||||||
|
|
||||||
/** The Constant APP_NAME. */
|
|
||||||
public static final String APP_NAME = "vm-runner";
|
|
||||||
private static final String TEMPLATE_DIR
|
private static final String TEMPLATE_DIR
|
||||||
= "/opt/" + APP_NAME.replace("-", "") + "/templates";
|
= "/opt/" + APP_NAME.replace("-", "") + "/templates";
|
||||||
private static final String DEFAULT_TEMPLATE
|
private static final String DEFAULT_TEMPLATE
|
||||||
|
|
@ -609,7 +608,7 @@ public class Runner extends Component {
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
InputStream props;
|
InputStream props;
|
||||||
var path = FsdUtils.findConfigFile(Runner.APP_NAME.replace("-", ""),
|
var path = FsdUtils.findConfigFile(APP_NAME.replace("-", ""),
|
||||||
"logging.properties");
|
"logging.properties");
|
||||||
if (path.isPresent()) {
|
if (path.isPresent()) {
|
||||||
props = Files.newInputStream(path.get());
|
props = Files.newInputStream(path.get());
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonPrimitive;
|
import com.google.gson.JsonPrimitive;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
|
@ -30,7 +32,7 @@ import java.util.function.Supplier;
|
||||||
* Utility class for pointing to elements on a Gson (Json) tree.
|
* Utility class for pointing to elements on a Gson (Json) tree.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
|
||||||
"PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal" })
|
"PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal", "PMD.GodClass" })
|
||||||
public class GsonPtr {
|
public class GsonPtr {
|
||||||
|
|
||||||
private final JsonElement position;
|
private final JsonElement position;
|
||||||
|
|
@ -209,6 +211,21 @@ public class GsonPtr {
|
||||||
.map(JsonPrimitive::getAsBoolean);
|
.map(JsonPrimitive::getAsBoolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the elements of the selected {@link JsonArray} as list.
|
||||||
|
*
|
||||||
|
* @param <T> the generic type
|
||||||
|
* @param cls the cls
|
||||||
|
* @param selectors the selectors
|
||||||
|
* @return the list
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T extends JsonElement> List<T> getAsListOf(Class<T> cls,
|
||||||
|
Object... selectors) {
|
||||||
|
return get(JsonArray.class, selectors).map(a -> (List<T>) a.asList())
|
||||||
|
.orElse(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the selected value. This pointer must point to a
|
* Sets the selected value. This pointer must point to a
|
||||||
* {@link JsonObject} or {@link JsonArray}. The selector must
|
* {@link JsonObject} or {@link JsonArray}. The selector must
|
||||||
|
|
@ -248,6 +265,18 @@ public class GsonPtr {
|
||||||
return set(selector, new JsonPrimitive(value));
|
return set(selector, new JsonPrimitive(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short for `set(selector, new JsonPrimitive(value))`.
|
||||||
|
*
|
||||||
|
* @param selector the selector
|
||||||
|
* @param value the value
|
||||||
|
* @return the gson ptr
|
||||||
|
* @see #set(Object, JsonElement)
|
||||||
|
*/
|
||||||
|
public GsonPtr set(Object selector, BigInteger value) {
|
||||||
|
return set(selector, new JsonPrimitive(value));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as {@link #set(Object, JsonElement)}, but sets the value
|
* Same as {@link #set(Object, JsonElement)}, but sets the value
|
||||||
* only if it doesn't exist yet, else returns the existing value.
|
* only if it doesn't exist yet, else returns the existing value.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ dependencies {
|
||||||
|
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.2.0,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.2.0,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[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'
|
apply plugin: 'com.github.node-gradle.node'
|
||||||
|
|
@ -22,6 +24,7 @@ task extractDependencies(type: Copy) {
|
||||||
|| it.name.contains('org.jgrapes.webconsole.base')
|
|| it.name.contains('org.jgrapes.webconsole.base')
|
||||||
}
|
}
|
||||||
.collect{ zipTree (it) }
|
.collect{ zipTree (it) }
|
||||||
|
exclude '*.class'
|
||||||
into 'build/unpacked'
|
into 'build/unpacked'
|
||||||
duplicatesStrategy 'include'
|
duplicatesStrategy 'include'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,46 @@
|
||||||
<div class="jdrupes-vmoperator-vmconlet jdrupes-vmoperator-vmconlet-preview"
|
<div class="jdrupes-vmoperator-vmconlet jdrupes-vmoperator-vmconlet-preview"
|
||||||
|
data-conlet-grid-rows="5"
|
||||||
data-jgwc-on-load="orgJDrupesVmOperatorVmConlet.initPreview"
|
data-jgwc-on-load="orgJDrupesVmOperatorVmConlet.initPreview"
|
||||||
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
|
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
|
||||||
<div>Preview</div>
|
|
||||||
|
<form>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{ localize("Period") }}:</legend>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="period" v-model="period" value="day">
|
||||||
|
<span>{{ localize("Last day") }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="period" v-model="period" value="hour">
|
||||||
|
<span>{{ localize("Last hour") }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{ localize("VMsSummary") }}:</td>
|
||||||
|
<td>{{ vmSummary.runningVms }} / {{ vmSummary.totalVms }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ localize("Used CPUs") }}:</td>
|
||||||
|
<td>{{ vmSummary.usedCpus }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ localize("Used RAM") }}:</td>
|
||||||
|
<td>{{ formatMemory(Number(vmSummary.usedRam)) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="vmsChart-wrapper">
|
||||||
|
<canvas class="vmsChart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,10 @@
|
||||||
class="fa fa-check" :title="localize('Yes')"></span>
|
class="fa fa-check" :title="localize('Yes')"></span>
|
||||||
<span v-else-if="key === 'running' && !entry[key]"
|
<span v-else-if="key === 'running' && !entry[key]"
|
||||||
class="fa fa-close" :title="localize('No')"></span>
|
class="fa fa-close" :title="localize('No')"></span>
|
||||||
|
<span v-else-if="key === 'runningConditionSince'"
|
||||||
|
>{{ shortDateTime(entry[key].toString()) }}</span>
|
||||||
<span v-else-if="key === 'currentRam'"
|
<span v-else-if="key === 'currentRam'"
|
||||||
v-html="formatMemory(entry[key])"></span>
|
>{{ formatMemory(entry[key]) }}</span>
|
||||||
<span v-else
|
<span v-else
|
||||||
v-html="controller.breakBeforeDots(entry[key])"></span>
|
v-html="controller.breakBeforeDots(entry[key])"></span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -64,7 +66,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr :id="scopedId(rowIndex)" v-if="$aash.isDisclosed(scopedId(rowIndex))"
|
<tr :id="scopedId(rowIndex)" v-if="$aash.isDisclosed(scopedId(rowIndex))"
|
||||||
:class="[(rowIndex % 2) ? 'odd' : 'even']">
|
:class="[(rowIndex % 2) ? 'odd' : 'even']">
|
||||||
<td colspan="4" class="details">
|
<td colspan="6" class="details">
|
||||||
<table class="table--basic table--basic--autoStriped">
|
<table class="table--basic table--basic--autoStriped">
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ localize("maximumCpus") }}</td>
|
<td>{{ localize("maximumCpus") }}</td>
|
||||||
|
|
@ -72,15 +74,31 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ localize("requestedCpus") }}</td>
|
<td>{{ localize("requestedCpus") }}</td>
|
||||||
<td>{{ entry.spec.vm.maximumCpus }}</td>
|
<td v-if="cic.key !== (entry['name'] + ':cpus')" tabindex="0"
|
||||||
|
v-on:focus="cic.startEdit(entry['name'] + ':cpus', entry.spec.vm.currentCpus)"
|
||||||
|
>{{ entry.spec.vm.currentCpus }}</td>
|
||||||
|
<td v-else><form action="javascript:void();"
|
||||||
|
><input :ref="(el) => { cic.input = el; }"
|
||||||
|
type="number" required :max="entry.spec.vm.maximumCpus"
|
||||||
|
v-on:focusout="cic.endEdit(cic.parseNumber)"
|
||||||
|
v-on:keydown.escape="cic.endEdit()"
|
||||||
|
><span>{{ cic.error }}</span></form></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ localize("maximumRam") }}</td>
|
<td>{{ localize("maximumRam") }}</td>
|
||||||
<td>{{ formatMemory(BigInt(entry.spec.vm.maximumRam)) }}</td>
|
<td>{{ formatMemory(Number(entry.spec.vm.maximumRam)) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ localize("requestedRam") }}</td>
|
<td>{{ localize("requestedRam") }}</td>
|
||||||
<td>{{ formatMemory(BigInt(entry.spec.vm.maximumRam)) }}</td>
|
<td v-if="cic.key !== (entry['name'] + ':ram')" tabindex="0"
|
||||||
|
v-on:focus="cic.startEdit(entry['name'] + ':ram', formatMemory(entry.spec.vm.currentRam))"
|
||||||
|
>{{ formatMemory(entry.spec.vm.currentRam) }}</td>
|
||||||
|
<td v-else><form action="javascript:void(0);"
|
||||||
|
><input :ref="(el) => { cic.input = el; }"
|
||||||
|
type="text" required
|
||||||
|
v-on:focusout="cic.endEdit(parseMemory)"
|
||||||
|
v-on:keydown.escape="cic.endEdit()"
|
||||||
|
><span>{{ cic.error }}</span></form></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
conletName = VM Viewer
|
conletName = VM Viewer
|
||||||
|
|
||||||
|
VMsSummary = VMs (running/total)
|
||||||
|
|
||||||
|
since = Since
|
||||||
currentCpus = Current CPUs
|
currentCpus = Current CPUs
|
||||||
currentRam = Current RAM
|
currentRam = Current RAM
|
||||||
maximumCpus = Maximum CPUs
|
maximumCpus = Maximum CPUs
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
conletName = VM Anzeige
|
conletName = VM Anzeige
|
||||||
|
|
||||||
|
VMsSummary = VMs (gestartet/gesamt)
|
||||||
|
|
||||||
|
Used\ CPUs = Verwendete CPUs
|
||||||
|
Used\ RAM = Verwendetes RAM
|
||||||
|
Period = Zeitraum
|
||||||
|
Last\ hour = Letzte Stunde
|
||||||
|
Last\ day = Letzter Tag
|
||||||
|
|
||||||
running = Gestartet
|
running = Gestartet
|
||||||
|
since = Seit
|
||||||
currentCpus = Aktuelle CPUs
|
currentCpus = Aktuelle CPUs
|
||||||
currentRam = Akuelles RAM
|
currentRam = Akuelles RAM
|
||||||
maximumCpus = Maximale CPUs
|
maximumCpus = Maximale CPUs
|
||||||
|
|
@ -10,6 +19,8 @@ requestedCpus = Angeforderte CPUs
|
||||||
requestedRam = Angefordertes RAM
|
requestedRam = Angefordertes RAM
|
||||||
vmActions = Aktionen
|
vmActions = Aktionen
|
||||||
vmname = Name
|
vmname = Name
|
||||||
|
Value\ is\ above\ maximum = Wert ist zu groß
|
||||||
|
Illegal\ format = Ungültiges Format
|
||||||
|
|
||||||
Start\ VM = VM Starten
|
Start\ VM = VM Starten
|
||||||
Stop\ VM = VM Anhalten
|
Stop\ VM = VM Anhalten
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ let pathsMap = {
|
||||||
"jgconsole": "../../console-base-resource/jgconsole.js",
|
"jgconsole": "../../console-base-resource/jgconsole.js",
|
||||||
"jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js",
|
"jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js",
|
||||||
"l10nBundles": "./" + baseName + "-l10nBundles.ftl.js",
|
"l10nBundles": "./" + baseName + "-l10nBundles.ftl.js",
|
||||||
"vue": "../../page-resource/vue/vue.esm-browser.js"
|
"vue": "../../page-resource/vue/vue.esm-browser.js",
|
||||||
|
"chartjs": "../../page-resource/chart.js/auto.js"
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
* 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.vmconlet;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Class TimeSeries.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
public class TimeSeries {
|
||||||
|
|
||||||
|
private final List<Entry> data = new LinkedList<>();
|
||||||
|
private final Duration period;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new time series.
|
||||||
|
*
|
||||||
|
* @param series the number of series
|
||||||
|
*/
|
||||||
|
public TimeSeries(Duration period) {
|
||||||
|
this.period = period;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds data to the series.
|
||||||
|
*
|
||||||
|
* @param time the time
|
||||||
|
* @param numbers the numbers
|
||||||
|
* @return the time series
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
|
public TimeSeries add(Instant time, Number... numbers) {
|
||||||
|
var newEntry = new Entry(time, numbers);
|
||||||
|
boolean adjust = false;
|
||||||
|
if (data.size() >= 2) {
|
||||||
|
var lastEntry = data.get(data.size() - 1);
|
||||||
|
var lastButOneEntry = data.get(data.size() - 2);
|
||||||
|
adjust = lastEntry.valuesEqual(lastButOneEntry)
|
||||||
|
&& lastEntry.valuesEqual(newEntry);
|
||||||
|
}
|
||||||
|
if (adjust) {
|
||||||
|
data.get(data.size() - 1).adjustTime(time);
|
||||||
|
} else {
|
||||||
|
data.add(new Entry(time, numbers));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purge
|
||||||
|
Instant limit = time.minus(period);
|
||||||
|
while (data.size() > 2
|
||||||
|
&& data.get(0).getTime().isBefore(limit)
|
||||||
|
&& data.get(1).getTime().isBefore(limit)) {
|
||||||
|
data.remove(0);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the entries.
|
||||||
|
*
|
||||||
|
* @return the list
|
||||||
|
*/
|
||||||
|
public List<Entry> entries() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Class Entry.
|
||||||
|
*/
|
||||||
|
public static class Entry {
|
||||||
|
private Instant timestamp;
|
||||||
|
private final Number[] values;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new entry.
|
||||||
|
*
|
||||||
|
* @param time the time
|
||||||
|
* @param numbers the numbers
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.ArrayIsStoredDirectly")
|
||||||
|
public Entry(Instant time, Number... numbers) {
|
||||||
|
timestamp = time;
|
||||||
|
values = numbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the entry's time.
|
||||||
|
*
|
||||||
|
* @param time the time
|
||||||
|
*/
|
||||||
|
public void adjustTime(Instant time) {
|
||||||
|
timestamp = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the entry's time.
|
||||||
|
*
|
||||||
|
* @return the instant
|
||||||
|
*/
|
||||||
|
public Instant getTime() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the values.
|
||||||
|
*
|
||||||
|
* @return the number[]
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.MethodReturnsInternalArray")
|
||||||
|
public Number[] getValues() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if both entries have the same values.
|
||||||
|
*
|
||||||
|
* @param other the other
|
||||||
|
* @return true, if successful
|
||||||
|
*/
|
||||||
|
public boolean valuesEqual(Entry other) {
|
||||||
|
return Arrays.equals(values, other.values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,8 +24,13 @@ import freemarker.template.MalformedTemplateNameException;
|
||||||
import freemarker.template.Template;
|
import freemarker.template.Template;
|
||||||
import freemarker.template.TemplateNotFoundException;
|
import freemarker.template.TemplateNotFoundException;
|
||||||
import io.kubernetes.client.custom.Quantity;
|
import io.kubernetes.client.custom.Quantity;
|
||||||
|
import io.kubernetes.client.custom.Quantity.Format;
|
||||||
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
@ -33,8 +38,7 @@ import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import org.jdrupes.json.JsonBeanDecoder;
|
import org.jdrupes.json.JsonBeanDecoder;
|
||||||
import org.jdrupes.json.JsonDecodeException;
|
import org.jdrupes.json.JsonDecodeException;
|
||||||
import org.jdrupes.vmoperator.manager.events.StartVm;
|
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.StopVm;
|
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
|
||||||
|
|
@ -59,6 +63,7 @@ import org.jgrapes.webconsole.base.events.SetLocale;
|
||||||
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* The Class VmConlet.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
|
|
@ -67,6 +72,14 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
RenderMode.Preview, RenderMode.View);
|
RenderMode.Preview, RenderMode.View);
|
||||||
private final Map<String, DynamicKubernetesObject> vmInfos
|
private final Map<String, DynamicKubernetesObject> vmInfos
|
||||||
= new ConcurrentHashMap<>();
|
= new ConcurrentHashMap<>();
|
||||||
|
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
|
||||||
|
private Summary cachedSummary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The periodically generated update event.
|
||||||
|
*/
|
||||||
|
public static class Update extends Event<Void> {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new component with its channel set to the given channel.
|
* Creates a new component with its channel set to the given channel.
|
||||||
|
|
@ -77,6 +90,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
*/
|
*/
|
||||||
public VmConlet(Channel componentChannel) {
|
public VmConlet(Channel componentChannel) {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
|
setPeriodicRefresh(Duration.ofMinutes(1), () -> new Update());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -116,7 +130,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
ConsoleConnection channel, String conletId, VmsModel conletState)
|
ConsoleConnection channel, String conletId, VmsModel conletState)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
Set<RenderMode> renderedAs = new HashSet<>();
|
Set<RenderMode> renderedAs = new HashSet<>();
|
||||||
boolean sendData = false;
|
boolean sendVmInfos = false;
|
||||||
if (event.renderAs().contains(RenderMode.Preview)) {
|
if (event.renderAs().contains(RenderMode.Preview)) {
|
||||||
Template tpl
|
Template tpl
|
||||||
= freemarkerConfig().getTemplate("VmConlet-preview.ftl.html");
|
= freemarkerConfig().getTemplate("VmConlet-preview.ftl.html");
|
||||||
|
|
@ -127,7 +141,12 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
RenderMode.Preview.addModifiers(event.renderAs()))
|
RenderMode.Preview.addModifiers(event.renderAs()))
|
||||||
.setSupportedModes(MODES));
|
.setSupportedModes(MODES));
|
||||||
renderedAs.add(RenderMode.View);
|
renderedAs.add(RenderMode.View);
|
||||||
sendData = true;
|
channel.respond(new NotifyConletView(type(),
|
||||||
|
conletId, "summarySeries", summarySeries.entries()));
|
||||||
|
var summary = evaluateSummary(false);
|
||||||
|
channel.respond(new NotifyConletView(type(),
|
||||||
|
conletId, "updateSummary", summary));
|
||||||
|
sendVmInfos = true;
|
||||||
}
|
}
|
||||||
if (event.renderAs().contains(RenderMode.View)) {
|
if (event.renderAs().contains(RenderMode.View)) {
|
||||||
Template tpl
|
Template tpl
|
||||||
|
|
@ -139,9 +158,9 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
RenderMode.View.addModifiers(event.renderAs()))
|
RenderMode.View.addModifiers(event.renderAs()))
|
||||||
.setSupportedModes(MODES));
|
.setSupportedModes(MODES));
|
||||||
renderedAs.add(RenderMode.View);
|
renderedAs.add(RenderMode.View);
|
||||||
sendData = true;
|
sendVmInfos = true;
|
||||||
}
|
}
|
||||||
if (sendData) {
|
if (sendVmInfos) {
|
||||||
for (var vmInfo : vmInfos.values()) {
|
for (var vmInfo : vmInfos.values()) {
|
||||||
var def = JsonBeanDecoder.create(vmInfo.getRaw().toString())
|
var def = JsonBeanDecoder.create(vmInfo.getRaw().toString())
|
||||||
.readObject();
|
.readObject();
|
||||||
|
|
@ -158,13 +177,14 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
* @param channel the channel
|
* @param channel the channel
|
||||||
* @throws JsonDecodeException
|
* @throws JsonDecodeException the json decode exception
|
||||||
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
@Handler(namedChannels = "manager")
|
@Handler(namedChannels = "manager")
|
||||||
@SuppressWarnings({ "PMD.ConfusingTernary",
|
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
|
||||||
"PMD.AvoidInstantiatingObjectsInLoops" })
|
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" })
|
||||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||||
throws JsonDecodeException {
|
throws JsonDecodeException, IOException {
|
||||||
if (event.type() == Type.DELETED) {
|
if (event.type() == Type.DELETED) {
|
||||||
var vmName = event.vmDefinition().getMetadata().getName();
|
var vmName = event.vmDefinition().getMetadata().getName();
|
||||||
vmInfos.remove(vmName);
|
vmInfos.remove(vmName);
|
||||||
|
|
@ -175,25 +195,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var vmDef = new DynamicKubernetesObject(
|
var vmDef = prepareForSending(event);
|
||||||
event.vmDefinition().getRaw().deepCopy());
|
|
||||||
GsonPtr.to(vmDef.getRaw()).to("metadata").get(JsonObject.class)
|
|
||||||
.remove("managedFields");
|
|
||||||
var vmSpec = GsonPtr.to(vmDef.getRaw()).to("spec", "vm");
|
|
||||||
vmSpec.set("maximumRam", Quantity.fromString(
|
|
||||||
vmSpec.getAsString("maximumRam").orElse("0")).getNumber()
|
|
||||||
.toBigInteger().toString());
|
|
||||||
vmSpec.set("currentRam", Quantity.fromString(
|
|
||||||
vmSpec.getAsString("currentRam").orElse("0")).getNumber()
|
|
||||||
.toBigInteger().toString());
|
|
||||||
var status = GsonPtr.to(vmDef.getRaw()).to("status");
|
|
||||||
status.set("ram", Quantity.fromString(
|
|
||||||
status.getAsString("ram").orElse("0")).getNumber()
|
|
||||||
.toBigInteger().toString());
|
|
||||||
String vmName = event.vmDefinition().getMetadata().getName();
|
|
||||||
vmInfos.put(vmName, vmDef);
|
|
||||||
|
|
||||||
// Extract running
|
|
||||||
var def = JsonBeanDecoder.create(vmDef.getRaw().toString())
|
var def = JsonBeanDecoder.create(vmDef.getRaw().toString())
|
||||||
.readObject();
|
.readObject();
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
|
|
@ -203,20 +205,165 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var summary = evaluateSummary(true);
|
||||||
|
summarySeries.add(Instant.now(), summary.usedCpus, summary.usedRam);
|
||||||
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
|
for (String conletId : entry.getValue()) {
|
||||||
|
entry.getKey().respond(new NotifyConletView(type(),
|
||||||
|
conletId, "updateSummary", summary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||||
|
private DynamicKubernetesObject prepareForSending(VmDefChanged event) {
|
||||||
|
// Clone and remove managed fields
|
||||||
|
var vmDef = new DynamicKubernetesObject(
|
||||||
|
event.vmDefinition().getRaw().deepCopy());
|
||||||
|
GsonPtr.to(vmDef.getRaw()).to("metadata").get(JsonObject.class)
|
||||||
|
.remove("managedFields");
|
||||||
|
|
||||||
|
// Convert RAM sizes to unitless numbers
|
||||||
|
var vmSpec = GsonPtr.to(vmDef.getRaw()).to("spec", "vm");
|
||||||
|
vmSpec.set("maximumRam", Quantity.fromString(
|
||||||
|
vmSpec.getAsString("maximumRam").orElse("0")).getNumber()
|
||||||
|
.toBigInteger());
|
||||||
|
vmSpec.set("currentRam", Quantity.fromString(
|
||||||
|
vmSpec.getAsString("currentRam").orElse("0")).getNumber()
|
||||||
|
.toBigInteger());
|
||||||
|
var status = GsonPtr.to(vmDef.getRaw()).to("status");
|
||||||
|
status.set("ram", Quantity.fromString(
|
||||||
|
status.getAsString("ram").orElse("0")).getNumber()
|
||||||
|
.toBigInteger());
|
||||||
|
String vmName = event.vmDefinition().getMetadata().getName();
|
||||||
|
vmInfos.put(vmName, vmDef);
|
||||||
|
return vmDef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the periodic update event by sending {@link NotifyConletView}
|
||||||
|
* events.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param connection the console connection
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
|
public void onUpdate(Update event, ConsoleConnection connection) {
|
||||||
|
var summary = evaluateSummary(false);
|
||||||
|
summarySeries.add(Instant.now(), summary.usedCpus, summary.usedRam);
|
||||||
|
for (String conletId : conletIds(connection)) {
|
||||||
|
connection.respond(new NotifyConletView(type(),
|
||||||
|
conletId, "updateSummary", summary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Class Summary.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
|
public static class Summary {
|
||||||
|
|
||||||
|
/** The total vms. */
|
||||||
|
public int totalVms;
|
||||||
|
|
||||||
|
/** The running vms. */
|
||||||
|
public int runningVms;
|
||||||
|
|
||||||
|
/** The used cpus. */
|
||||||
|
public int usedCpus;
|
||||||
|
|
||||||
|
/** The used ram. */
|
||||||
|
public BigInteger usedRam = BigInteger.ZERO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total vms.
|
||||||
|
*
|
||||||
|
* @return the totalVms
|
||||||
|
*/
|
||||||
|
public int getTotalVms() {
|
||||||
|
return totalVms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the running vms.
|
||||||
|
*
|
||||||
|
* @return the runningVms
|
||||||
|
*/
|
||||||
|
public int getRunningVms() {
|
||||||
|
return runningVms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the used cpus.
|
||||||
|
*
|
||||||
|
* @return the usedCpus
|
||||||
|
*/
|
||||||
|
public int getUsedCpus() {
|
||||||
|
return usedCpus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the used ram. Returned as String for Json rendering.
|
||||||
|
*
|
||||||
|
* @return the usedRam
|
||||||
|
*/
|
||||||
|
public String getUsedRam() {
|
||||||
|
return usedRam.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
|
private Summary evaluateSummary(boolean force) {
|
||||||
|
if (!force && cachedSummary != null) {
|
||||||
|
return cachedSummary;
|
||||||
|
}
|
||||||
|
Summary summary = new Summary();
|
||||||
|
for (var vmDef : vmInfos.values()) {
|
||||||
|
summary.totalVms += 1;
|
||||||
|
var status = GsonPtr.to(vmDef.getRaw()).to("status");
|
||||||
|
summary.usedCpus += status.getAsInt("cpus").orElse(0);
|
||||||
|
summary.usedRam = summary.usedRam.add(status.getAsString("ram")
|
||||||
|
.map(BigInteger::new).orElse(BigInteger.ZERO));
|
||||||
|
for (var c : status.getAsListOf(JsonObject.class, "conditions")) {
|
||||||
|
if ("Running".equals(GsonPtr.to(c).getAsString("type")
|
||||||
|
.orElse(null))
|
||||||
|
&& "True".equals(GsonPtr.to(c).getAsString("status")
|
||||||
|
.orElse(null))) {
|
||||||
|
summary.runningVms += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cachedSummary = summary;
|
||||||
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("PMD.AvoidDecimalLiteralsInBigDecimalConstructor")
|
||||||
protected void doUpdateConletState(NotifyConletModel event,
|
protected void doUpdateConletState(NotifyConletModel event,
|
||||||
ConsoleConnection channel, VmsModel conletState)
|
ConsoleConnection channel, VmsModel conletState)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
event.stop();
|
event.stop();
|
||||||
switch (event.method()) {
|
switch (event.method()) {
|
||||||
case "start":
|
case "start":
|
||||||
fire(new StartVm(event.params().asString(0),
|
fire(new ModifyVm(event.params().asString(0), "state", "Running",
|
||||||
new NamedChannel("manager")));
|
new NamedChannel("manager")));
|
||||||
break;
|
break;
|
||||||
case "stop":
|
case "stop":
|
||||||
fire(new StopVm(event.params().asString(0),
|
fire(new ModifyVm(event.params().asString(0), "state", "Stopped",
|
||||||
|
new NamedChannel("manager")));
|
||||||
|
break;
|
||||||
|
case "cpus":
|
||||||
|
fire(new ModifyVm(event.params().asString(0), "currentCpus",
|
||||||
|
new BigDecimal(event.params().asDouble(1)).toBigInteger(),
|
||||||
|
new NamedChannel("manager")));
|
||||||
|
break;
|
||||||
|
case "ram":
|
||||||
|
fire(new ModifyVm(event.params().asString(0), "currentRam",
|
||||||
|
new Quantity(new BigDecimal(event.params().asDouble(1)),
|
||||||
|
Format.BINARY_SI).toSuffixedString(),
|
||||||
new NamedChannel("manager")));
|
new NamedChannel("manager")));
|
||||||
break;
|
break;
|
||||||
default:// ignore
|
default:// ignore
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, Ref, nextTick } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A controller for conditionally shown inputs. "Conditionally shown"
|
||||||
|
* means that the value is usually shown using some display element
|
||||||
|
* (e.g. `span`). Only when that elements gets the focus, it is replaced
|
||||||
|
* with an input element for editing the value.
|
||||||
|
*/
|
||||||
|
export default class ConditionlInputController {
|
||||||
|
|
||||||
|
private submitCallback: (selected: string, value: any) => string | null;
|
||||||
|
private readonly inputKey = ref("");
|
||||||
|
private startValue: any;
|
||||||
|
private inputElement: HTMLInputElement | null = null;
|
||||||
|
private errorMessage = ref("");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new controller.
|
||||||
|
*/
|
||||||
|
constructor(submitCallback: (selected: string, value: string) => string | null) {
|
||||||
|
// this.inputRef = inputRef;
|
||||||
|
this.submitCallback = submitCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
get key() {
|
||||||
|
return this.inputKey.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get error() {
|
||||||
|
return this.errorMessage.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set input(element: HTMLInputElement) {
|
||||||
|
this.inputElement = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
startEdit (key: string, value: any) {
|
||||||
|
if (this.inputKey.value != "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.startValue = value;
|
||||||
|
this.errorMessage.value = "";
|
||||||
|
this.inputKey.value = key;
|
||||||
|
nextTick(() => {
|
||||||
|
this.inputElement!.value = value;
|
||||||
|
this.inputElement!.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
endEdit (converter?: (value: string) => any | null) : boolean {
|
||||||
|
if (typeof converter === 'undefined') {
|
||||||
|
this.inputKey.value = "";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let newValue = converter(this.inputElement!.value);
|
||||||
|
if (newValue === this.startValue) {
|
||||||
|
this.inputKey.value = "";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let submitResult = this.submitCallback (this.inputKey.value, newValue);
|
||||||
|
if (submitResult !== null) {
|
||||||
|
this.errorMessage.value = submitResult;
|
||||||
|
// Neither doing it directly nor doing it with nextTick works.
|
||||||
|
setTimeout(() => this.inputElement!.focus(), 10);
|
||||||
|
} else {
|
||||||
|
this.inputKey.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case it is called by form action
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get parseNumber() {
|
||||||
|
return (value: string): number | null => {
|
||||||
|
if (value.match(/^\d+$/)) {
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Chart } from "chartjs";
|
||||||
|
import TimeSeries from "./TimeSeries";
|
||||||
|
import { formatMemory } from "./MemorySize";
|
||||||
|
import JGConsole from "jgconsole";
|
||||||
|
import l10nBundles from "l10nBundles";
|
||||||
|
import { JGWC } from "jgwc";
|
||||||
|
|
||||||
|
export default class CpuRamChart extends Chart {
|
||||||
|
|
||||||
|
private period = 24 * 3600 * 1000;
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement, series: TimeSeries) {
|
||||||
|
super(canvas.getContext('2d')!, {
|
||||||
|
// The type of chart we want to create
|
||||||
|
type: 'line',
|
||||||
|
|
||||||
|
// The data for our datasets
|
||||||
|
data: {
|
||||||
|
labels: series.getTimes(),
|
||||||
|
datasets: [{
|
||||||
|
// See localize
|
||||||
|
data: series.getSeries(0),
|
||||||
|
yAxisID: 'cpus'
|
||||||
|
}, {
|
||||||
|
// See localize
|
||||||
|
data: series.getSeries(1),
|
||||||
|
yAxisID: 'ram'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configuration options go here
|
||||||
|
options: {
|
||||||
|
animation: false,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: { minUnit: 'minute' },
|
||||||
|
adapters: {
|
||||||
|
date: {
|
||||||
|
// See localize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cpus: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'left',
|
||||||
|
min: 0
|
||||||
|
},
|
||||||
|
ram: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
min: 0,
|
||||||
|
grid: { drawOnChartArea: false },
|
||||||
|
ticks: {
|
||||||
|
stepSize: 1024 * 1024 * 1024,
|
||||||
|
callback: function(value, _index, _values) {
|
||||||
|
return formatMemory(Math.round(Number(value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let css = getComputedStyle(canvas);
|
||||||
|
this.setPropValue("options.plugins.legend.labels.font.family", css.fontFamily);
|
||||||
|
this.setPropValue("options.plugins.legend.labels.color", css.color);
|
||||||
|
this.setPropValue("options.scales.x.ticks.font.family", css.fontFamily);
|
||||||
|
this.setPropValue("options.scales.x.ticks.color", css.color);
|
||||||
|
this.setPropValue("options.scales.cpus.ticks.font.family", css.fontFamily);
|
||||||
|
this.setPropValue("options.scales.cpus.ticks.color", css.color);
|
||||||
|
this.setPropValue("options.scales.ram.ticks.font.family", css.fontFamily);
|
||||||
|
this.setPropValue("options.scales.ram.ticks.color", css.color);
|
||||||
|
|
||||||
|
this.localizeChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
setPeriod(period: number) {
|
||||||
|
this.period = period;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
setPropValue(path: string, value: any) {
|
||||||
|
let ptr: any = this;
|
||||||
|
let segs = path.split(".");
|
||||||
|
let lastSeg = segs.pop()!;
|
||||||
|
for (let seg of segs) {
|
||||||
|
let cur = ptr[seg];
|
||||||
|
if (!cur) {
|
||||||
|
ptr[seg] = {};
|
||||||
|
}
|
||||||
|
// ptr[seg] = ptr[seg] || {}
|
||||||
|
ptr = ptr[seg];
|
||||||
|
}
|
||||||
|
ptr[lastSeg] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
localizeChart() {
|
||||||
|
(<any>this.options.scales?.x).adapters.date.locale = JGWC.lang();
|
||||||
|
this.data.datasets[0].label
|
||||||
|
= JGConsole.localize(l10nBundles, JGWC.lang(), "Used CPUs")
|
||||||
|
this.data.datasets[1].label
|
||||||
|
= JGConsole.localize(l10nBundles, JGWC.lang(), "Used RAM")
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
shift() {
|
||||||
|
this.setPropValue("options.scales.x.max", Date.now());
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.setPropValue("options.scales.x.min", Date.now() - this.period);
|
||||||
|
super.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let unitMap = new Map<string, number>();
|
||||||
|
let unitMappings = new Array<{ key: string; value: number }>();
|
||||||
|
let memorySize = /^(\d+(\.\d+)?)\s*(B|kB|MB|GB|TB|PB|EB|KiB|MiB|GiB|TiB|PiB|EiB)?$/;
|
||||||
|
|
||||||
|
// SI units and common abbreviations
|
||||||
|
let factor = 1;
|
||||||
|
unitMap.set("", factor);
|
||||||
|
let scale = 1000;
|
||||||
|
for (let unit of ["B", "kB", "MB", "GB", "TB", "PB", "EB"]) {
|
||||||
|
unitMap.set(unit, factor);
|
||||||
|
factor = factor * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary units
|
||||||
|
factor = 1024;
|
||||||
|
scale = 1024;
|
||||||
|
for (let unit of ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]) {
|
||||||
|
unitMap.set(unit, factor);
|
||||||
|
factor = factor * scale;
|
||||||
|
}
|
||||||
|
unitMap.forEach((value: number, key: string) => {
|
||||||
|
unitMappings.push({ key, value });
|
||||||
|
});
|
||||||
|
unitMappings.sort((a, b) => a.value < b.value ? 1 : a.value > b.value ? -1 : 0);
|
||||||
|
|
||||||
|
export function formatMemory(size: number): string {
|
||||||
|
for (let mapping of unitMappings) {
|
||||||
|
if (size >= mapping.value
|
||||||
|
&& (size % mapping.value) === 0) {
|
||||||
|
return (size / mapping.value + " " + mapping.key).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMemory(value: string): number | null {
|
||||||
|
let match = value.match(memorySize);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let unit = 1;
|
||||||
|
if (match[3]) {
|
||||||
|
unit = unitMap.get(match[3])!;
|
||||||
|
}
|
||||||
|
return Number(match[1]) * unit;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type OnChangeCallback = ((ts: TimeSeries) => void) | null;
|
||||||
|
|
||||||
|
export default class TimeSeries {
|
||||||
|
private timestamps: Date[] = [];
|
||||||
|
private series: number[][];
|
||||||
|
private period: number;
|
||||||
|
private onChange: OnChangeCallback;
|
||||||
|
|
||||||
|
constructor(nbOfSeries: number, period = 24 * 3600 * 1000,
|
||||||
|
onChange: OnChangeCallback = null) {
|
||||||
|
this.period = period;
|
||||||
|
this.onChange = onChange;
|
||||||
|
this.series = [];
|
||||||
|
while (this.series.length < nbOfSeries) {
|
||||||
|
this.series.push([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.timestamps.length = 0;
|
||||||
|
for (let values of this.series) {
|
||||||
|
values.length = 0;
|
||||||
|
}
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push(time: Date, ...values: number[]) {
|
||||||
|
let adjust = false;
|
||||||
|
if (this.timestamps.length >= 2) {
|
||||||
|
adjust = true;
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
if (values[i] !== this.series[i][this.series[i].length - 1]
|
||||||
|
|| values[i] !== this.series[i][this.series[i].length - 2]) {
|
||||||
|
adjust = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (adjust) {
|
||||||
|
this.timestamps[this.timestamps.length - 1] = time;
|
||||||
|
} else {
|
||||||
|
this.timestamps.push(time);
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
this.series[i].push(values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purge
|
||||||
|
let limit = time.getTime() - this.period;
|
||||||
|
while (this.timestamps.length > 2
|
||||||
|
&& this.timestamps[0].getTime() < limit
|
||||||
|
&& this.timestamps[1].getTime() < limit) {
|
||||||
|
this.timestamps.shift();
|
||||||
|
for (let values of this.series) {
|
||||||
|
values.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimes(): Date[] {
|
||||||
|
return this.timestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSeries(n: number): number[] {
|
||||||
|
return this.series[n];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -16,51 +16,19 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { reactive, ref, createApp, computed, onMounted } from "vue";
|
import {
|
||||||
|
reactive, ref, Ref, createApp, computed, onMounted, watch, nextTick
|
||||||
|
} from "vue";
|
||||||
import JGConsole from "jgconsole";
|
import JGConsole from "jgconsole";
|
||||||
import JgwcPlugin, { JGWC } from "jgwc";
|
import JgwcPlugin, { JGWC } from "jgwc";
|
||||||
import { provideApi, getApi } from "aash-plugin";
|
|
||||||
import l10nBundles from "l10nBundles";
|
import l10nBundles from "l10nBundles";
|
||||||
|
import TimeSeries from "./TimeSeries";
|
||||||
|
import { formatMemory, parseMemory } from "./MemorySize";
|
||||||
|
import CpuRamChart from "./CpuRamChart";
|
||||||
|
import ConditionlInputController from "./ConditionalInputController";
|
||||||
|
|
||||||
import "./VmConlet-style.scss";
|
import "./VmConlet-style.scss";
|
||||||
|
|
||||||
//
|
|
||||||
// Helpers
|
|
||||||
//
|
|
||||||
let unitMap = new Map<string, bigint>();
|
|
||||||
let unitMappings = new Array<{ key: string; value: bigint }>();
|
|
||||||
let memorySize = /^\\s*(\\d+(\\.\\d+)?)\\s*([A-Za-z]*)\\s*/;
|
|
||||||
|
|
||||||
// SI units and common abbreviations
|
|
||||||
let factor = BigInt("1");
|
|
||||||
unitMap.set("", factor);
|
|
||||||
let scale = BigInt("1000");
|
|
||||||
for (let unit of ["B", "kB", "MB", "GB", "TB", "PB", "EB"]) {
|
|
||||||
unitMap.set(unit, factor);
|
|
||||||
factor = factor * scale;
|
|
||||||
}
|
|
||||||
// Binary units
|
|
||||||
factor = BigInt("1024");
|
|
||||||
scale = BigInt("1024");
|
|
||||||
for (let unit of ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]) {
|
|
||||||
unitMap.set(unit, factor);
|
|
||||||
factor = factor * scale;
|
|
||||||
}
|
|
||||||
unitMap.forEach((value: bigint, key: string) => {
|
|
||||||
unitMappings.push({ key, value });
|
|
||||||
});
|
|
||||||
unitMappings.sort((a, b) => a.value < b.value ? 1 : a.value > b.value ? -1 : 0);
|
|
||||||
|
|
||||||
function formatMemory(size: bigint): string {
|
|
||||||
for (let mapping of unitMappings) {
|
|
||||||
if (size >= mapping.value
|
|
||||||
&& (size % mapping.value) === BigInt("0")) {
|
|
||||||
return (size / mapping.value + " " + mapping.key).trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return size.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// For global access
|
// For global access
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
@ -71,14 +39,64 @@ declare global {
|
||||||
window.orgJDrupesVmOperatorVmConlet = {};
|
window.orgJDrupesVmOperatorVmConlet = {};
|
||||||
|
|
||||||
let vmInfos = reactive(new Map());
|
let vmInfos = reactive(new Map());
|
||||||
|
let vmSummary = reactive({
|
||||||
|
totalVms: 0,
|
||||||
|
runningVms: 0,
|
||||||
|
usedCpus: 0,
|
||||||
|
usedRam: ""
|
||||||
|
});
|
||||||
|
|
||||||
window.orgJDrupesVmOperatorVmConlet.initPreview
|
const localize = (key: string) => {
|
||||||
= (previewDom: HTMLElement, _isUpdate: boolean) => {
|
return JGConsole.localize(
|
||||||
const app = createApp({});
|
l10nBundles, JGWC.lang(), key);
|
||||||
app.use(JgwcPlugin, []);
|
};
|
||||||
app.config.globalProperties.window = window;
|
|
||||||
app.mount(previewDom);
|
const shortDateTime = (time: Date) => {
|
||||||
};
|
// https://stackoverflow.com/questions/63958875/why-do-i-get-rangeerror-date-value-is-not-finite-in-datetimeformat-format-w
|
||||||
|
return new Intl.DateTimeFormat(JGWC.lang(),
|
||||||
|
{ dateStyle: "short", timeStyle: "short" }).format(new Date(time));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cannot be reactive, leads to infinite recursion.
|
||||||
|
let chartData = new TimeSeries(2);
|
||||||
|
let chartDateUpdate = ref<Date>(null);
|
||||||
|
|
||||||
|
window.orgJDrupesVmOperatorVmConlet.initPreview = (previewDom: HTMLElement,
|
||||||
|
_isUpdate: boolean) => {
|
||||||
|
const app = createApp({
|
||||||
|
setup(_props: any) {
|
||||||
|
const conletId: string
|
||||||
|
= (<HTMLElement>previewDom.parentNode!).dataset["conletId"]!;
|
||||||
|
|
||||||
|
let chart: CpuRamChart | null = null;
|
||||||
|
onMounted(() => {
|
||||||
|
let canvas: HTMLCanvasElement
|
||||||
|
= previewDom.querySelector(":scope .vmsChart")!;
|
||||||
|
chart = new CpuRamChart(canvas, chartData);
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(chartDateUpdate, (_) => {
|
||||||
|
chart?.update();
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(JGWC.langRef(), (_) => {
|
||||||
|
chart?.localizeChart();
|
||||||
|
})
|
||||||
|
|
||||||
|
const period: Ref<string> = ref<string>("day");
|
||||||
|
|
||||||
|
watch(period, (_) => {
|
||||||
|
let hours = (period.value === "day") ? 24 : 1;
|
||||||
|
chart?.setPeriod(hours * 3600 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { localize, formatMemory, vmSummary, period };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.use(JgwcPlugin, []);
|
||||||
|
app.config.globalProperties.window = window;
|
||||||
|
app.mount(previewDom);
|
||||||
|
};
|
||||||
|
|
||||||
window.orgJDrupesVmOperatorVmConlet.initView = (viewDom: HTMLElement,
|
window.orgJDrupesVmOperatorVmConlet.initView = (viewDom: HTMLElement,
|
||||||
_isUpdate: boolean) => {
|
_isUpdate: boolean) => {
|
||||||
|
|
@ -87,14 +105,10 @@ window.orgJDrupesVmOperatorVmConlet.initView = (viewDom: HTMLElement,
|
||||||
const conletId: string
|
const conletId: string
|
||||||
= (<HTMLElement>viewDom.parentNode!).dataset["conletId"]!;
|
= (<HTMLElement>viewDom.parentNode!).dataset["conletId"]!;
|
||||||
|
|
||||||
const localize = (key: string) => {
|
|
||||||
return JGConsole.localize(
|
|
||||||
l10nBundles, JGWC.lang() || "en", key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const controller = reactive(new JGConsole.TableController([
|
const controller = reactive(new JGConsole.TableController([
|
||||||
["name", "vmname"],
|
["name", "vmname"],
|
||||||
["running", "running"],
|
["running", "running"],
|
||||||
|
["runningConditionSince", "since"],
|
||||||
["currentCpus", "currentCpus"],
|
["currentCpus", "currentCpus"],
|
||||||
["currentRam", "currentRam"],
|
["currentRam", "currentRam"],
|
||||||
["nodeName", "nodeName"]
|
["nodeName", "nodeName"]
|
||||||
|
|
@ -114,12 +128,30 @@ window.orgJDrupesVmOperatorVmConlet.initView = (viewDom: HTMLElement,
|
||||||
|
|
||||||
const idScope = JGWC.createIdScope();
|
const idScope = JGWC.createIdScope();
|
||||||
const detailsByName = reactive(new Set());
|
const detailsByName = reactive(new Set());
|
||||||
|
|
||||||
|
const submitCallback = (selected: string, value: any) => {
|
||||||
|
if (value === null) {
|
||||||
|
return localize("Illegal format");
|
||||||
|
}
|
||||||
|
let vmName = selected.substring(0, selected.lastIndexOf(":"));
|
||||||
|
let property = selected.substring(selected.lastIndexOf(":") + 1);
|
||||||
|
var vmDef = vmInfos.get(vmName);
|
||||||
|
let maxValue = vmDef.spec.vm["maximum"
|
||||||
|
+ property.substring(0, 1).toUpperCase() + property.substring(1)];
|
||||||
|
if (value > maxValue) {
|
||||||
|
return localize("Value is above maximum");
|
||||||
|
}
|
||||||
|
JGConsole.notifyConletModel(conletId, property, vmName, value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cic = new ConditionlInputController(submitCallback);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
controller, vmInfos, filteredData, detailsByName,
|
controller, vmInfos, filteredData, detailsByName, localize,
|
||||||
localize, formatMemory, vmAction,
|
shortDateTime, formatMemory, vmAction, cic, parseMemory,
|
||||||
scopedId: (id: string) => { return idScope.scopedId(id); }
|
scopedId: (id: string) => { return idScope.scopedId(id); }
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.use(JgwcPlugin);
|
app.use(JgwcPlugin);
|
||||||
|
|
@ -132,14 +164,15 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmconlet.VmConlet",
|
||||||
// Add some short-cuts for table controller
|
// Add some short-cuts for table controller
|
||||||
vmDefinition.name = vmDefinition.metadata.name;
|
vmDefinition.name = vmDefinition.metadata.name;
|
||||||
vmDefinition.currentCpus = vmDefinition.status.cpus;
|
vmDefinition.currentCpus = vmDefinition.status.cpus;
|
||||||
vmDefinition.currentRam = BigInt(vmDefinition.status.ram);
|
vmDefinition.currentRam = Number(vmDefinition.status.ram);
|
||||||
for (let condition of vmDefinition.status.conditions) {
|
for (let condition of vmDefinition.status.conditions) {
|
||||||
if (condition.type === "Running") {
|
if (condition.type === "Running") {
|
||||||
vmDefinition.running = condition.status === "True";
|
vmDefinition.running = condition.status === "True";
|
||||||
|
vmDefinition.runningConditionSince
|
||||||
|
= new Date(condition.lastTransitionTime);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vmInfos.set(vmDefinition.name, vmDefinition);
|
vmInfos.set(vmDefinition.name, vmDefinition);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -147,3 +180,22 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmconlet.VmConlet",
|
||||||
"removeVm", function(_conletId: String, vmName: String) {
|
"removeVm", function(_conletId: String, vmName: String) {
|
||||||
vmInfos.delete(vmName);
|
vmInfos.delete(vmName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmconlet.VmConlet",
|
||||||
|
"summarySeries", function(_conletId: String, series: any[]) {
|
||||||
|
chartData.clear();
|
||||||
|
for (let entry of series) {
|
||||||
|
chartData.push(new Date(entry.time.epochSecond * 1000
|
||||||
|
+ entry.time.nano / 1000000),
|
||||||
|
entry.values[0], entry.values[1]);
|
||||||
|
}
|
||||||
|
chartDateUpdate.value = new Date();
|
||||||
|
});
|
||||||
|
|
||||||
|
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmconlet.VmConlet",
|
||||||
|
"updateSummary", function(_conletId: String, summary: any) {
|
||||||
|
chartData.push(new Date(), summary.usedCpus, Number(summary.usedRam));
|
||||||
|
chartDateUpdate.value = new Date();
|
||||||
|
Object.assign(vmSummary, summary);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,23 @@
|
||||||
* Conlet specific styles.
|
* Conlet specific styles.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.jdrupes-vmoperator-vmconlet-preview {
|
||||||
|
form {
|
||||||
|
float: right;
|
||||||
|
padding: 0.15em 0.3em;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: var(--corner-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vmsChart-wrapper {
|
||||||
|
height: 12em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmconlet-view-search {
|
.jdrupes-vmoperator-vmconlet-view-search {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end
|
justify-content: flex-end
|
||||||
|
|
@ -30,7 +47,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmconlet-view-action-list {
|
.jdrupes-vmoperator-vmconlet-view-action-list {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmconlet-view-action-list [role=button]:not(:last-child) {
|
.jdrupes-vmoperator-vmconlet-view-action-list [role=button]:not(:last-child) {
|
||||||
|
|
@ -39,28 +56,51 @@
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmconlet-view td {
|
.jdrupes-vmoperator-vmconlet-view td {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|
||||||
|
&[tabindex] {
|
||||||
|
outline: 1px solid var(--primary);
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmconlet-view td:not([colspan]):first-child {
|
.jdrupes-vmoperator-vmconlet-view td:not([colspan]):first-child {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmconlet-view table td.details {
|
.jdrupes-vmoperator-vmconlet-view table td.details {
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmconlet-view-table {
|
.jdrupes-vmoperator-vmconlet-view-table {
|
||||||
td.column-running {
|
td.column-running {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
&.fa-check {
|
&.fa-check {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.fa-close {
|
&.fa-close {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td.details {
|
||||||
|
table {
|
||||||
|
|
||||||
|
td:nth-child(2) {
|
||||||
|
min-width: 7em;
|
||||||
|
|
||||||
|
input {
|
||||||
|
max-width: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input~span {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
"jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"],
|
"jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"],
|
||||||
"jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"],
|
"jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"],
|
||||||
"l10nBundles": ["./src/org/jdrupes/vmoperator/vmconlet/browser/l10nBundles-stub"],
|
"l10nBundles": ["./src/org/jdrupes/vmoperator/vmconlet/browser/l10nBundles-stub"],
|
||||||
"vue": ["./build/unpacked/org/jgrapes/webconsole/provider/vue/vue/vue"]
|
"vue": ["./build/unpacked/org/jgrapes/webconsole/provider/vue/vue/vue"],
|
||||||
|
"chartjs": ["./build/unpacked/org/jgrapes/webconsole/provider/chartjs/chart.js/auto/auto"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
|
|
|
||||||
236
package-lock.json
generated
236
package-lock.json
generated
|
|
@ -13,7 +13,7 @@
|
||||||
"jsdoc": "^4.0.2",
|
"jsdoc": "^4.0.2",
|
||||||
"node-sass": "^9.0.0",
|
"node-sass": "^9.0.0",
|
||||||
"npm": "^8.11.0",
|
"npm": "^8.11.0",
|
||||||
"rollup": "^3.17.2",
|
"rollup": "^4.1.5",
|
||||||
"rollup-plugin-peer-deps-external": "^2.2.3",
|
"rollup-plugin-peer-deps-external": "^2.2.3",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-typescript2": "^0.36.0",
|
"rollup-plugin-typescript2": "^0.36.0",
|
||||||
|
|
@ -613,13 +613,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/plugin-replace": {
|
"node_modules/@rollup/plugin-replace": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
|
||||||
"integrity": "sha512-je7fu05B800IrMlWjb2wzJcdXzHYW46iTipfChnBDbIbDXhASZs27W1B58T2Yf45jZtJUONegpbce+9Ut2Ti/Q==",
|
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rollup/pluginutils": "^5.0.1",
|
"@rollup/pluginutils": "^5.0.1",
|
||||||
"magic-string": "^0.27.0"
|
"magic-string": "^0.30.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
|
|
@ -677,6 +677,162 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-/fwx6GS8cIbM2rTNyLMxjSCOegHywOdXO+kN9yFy018iCULcKZCyA3xvzw4bxyKbYfdSxQgdhbsl0egNcxerQw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-tmXh7dyEt+JEz/NgDJlB1UeL/1gFV0v8qYzUAU42WZH4lmUJ5rp6/HkR2qUNC5jCgYEwd8/EfbHKtGIEfS4CUg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-lTDmLxdEVhzI3KCesZUrNbl3icBvPrDv/85JasY5gh4P2eAuDFmM4uj9HC5DdH0anLC0fwJ+1Uzasr4qOXcjRQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-v6qEHZyjWnIgcc4oiy8AIeFsUJAx+Kg0sLj+RE7ICwv3u7YC/+bSClxAiBASRjMzqsq0Z+I/pfxj+OD8mjBYxg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-WngCfwPEDUNbZR1FNO2TCROYUwJvRlbvPi3AS85bDUkkoRDBcjUIz42cuB1j4PKilmnZascL5xTMF/yU8YFayA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-Q2A/PEP/UTPTOBwgar3mmCaApahoezai/8e/7f4GCLV6XWCpnU4YwkQQtla7d7nUnc792Ps7g1G0WMovzIknrA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-84aBKNAVzTU/eG3tb2+kR4NGRAtm2YVW/KHwkGGDR4z1k4hyrDbuImsfs/6J74t6y0YLOe9HOSu7ejRjzUBGVQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-mldtP9UEBurIq2+GYMdNeiqCLW1fdgf4KdkMR/QegAeXk4jFHkKQl7p0NITrKFVyVqzISGXH5gR6GSTBH4wszw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-36p+nMcSxjAEzfU47+by102HolUtf/EfgBAidocTKAofJMTqG5QD50qzaFLk4QO+z7Qvg4qd0wr99jGAwnKOig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-5oxhubo0A3J8aF/tG+6jHBg785HF8/88kl1YnfbDKmnqMxz/EFiAQDH9cq6lbnxofjn8tlq5KiTf0crJGOGThg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-uVQyBREKX9ErofL8KAZ4iVlqzSZOXSIG+BOLYuz5FD+Cg6jh1eLIeUa3Q4SgX0QaTRFeeAgSNqCC+8kZrZBpSw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-FQ5qYqRJ2vUBSom3Fos8o/6UvAMOvlus4+HGCAifH1TagbbwVnVVe0o01J1V52EWnQ8kmfpJDJ0FMrfM5yzcSA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@tootallnate/once": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||||
|
|
@ -838,18 +994,6 @@
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc/node_modules/magic-string": {
|
|
||||||
"version": "0.30.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
|
||||||
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.3.4",
|
"version": "3.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
|
||||||
|
|
@ -873,18 +1017,6 @@
|
||||||
"magic-string": "^0.30.0"
|
"magic-string": "^0.30.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity-transform/node_modules/magic-string": {
|
|
||||||
"version": "0.30.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
|
||||||
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.3.4",
|
"version": "3.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
|
||||||
|
|
@ -3093,12 +3225,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.27.0",
|
"version": "0.30.5",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
||||||
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
|
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.13"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
|
|
@ -8770,18 +8902,30 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "3.29.4",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.1.5.tgz",
|
||||||
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
|
"integrity": "sha512-AEw14/q4NHYQkQlngoSae2yi7hDBeT9w84aEzdgCr39+2RL+iTG84lGTkgC1Wp5igtquN64cNzuzZKVz+U6jOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.18.0",
|
"node": ">=18.0.0",
|
||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-android-arm-eabi": "4.1.5",
|
||||||
|
"@rollup/rollup-android-arm64": "4.1.5",
|
||||||
|
"@rollup/rollup-darwin-arm64": "4.1.5",
|
||||||
|
"@rollup/rollup-darwin-x64": "4.1.5",
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": "4.1.5",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "4.1.5",
|
||||||
|
"@rollup/rollup-linux-arm64-musl": "4.1.5",
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.1.5",
|
||||||
|
"@rollup/rollup-linux-x64-musl": "4.1.5",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.1.5",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.1.5",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.1.5",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -9008,9 +9152,9 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.69.3",
|
"version": "1.69.5",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.69.3.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz",
|
||||||
"integrity": "sha512-X99+a2iGdXkdWn1akFPs0ZmelUzyAQfvqYc2P/MPTrJRuIRoTffGzT9W9nFqG00S+c8hXzVmgxhUuHFdrwxkhQ==",
|
"integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": ">=3.0.0 <4.0.0",
|
"chokidar": ">=3.0.0 <4.0.0",
|
||||||
|
|
@ -9517,9 +9661,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.21.0",
|
"version": "5.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.23.0.tgz",
|
||||||
"integrity": "sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==",
|
"integrity": "sha512-Iyy83LN0uX9ZZLCX4Qbu5JiHiWjOCTwrmM9InWOzVeM++KNWEsqV4YgN9U9E8AlohQ6Gs42ztczlWOG/lwDAMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
|
|
@ -9615,9 +9759,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typedoc": {
|
"node_modules/typedoc": {
|
||||||
"version": "0.25.2",
|
"version": "0.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.2.tgz",
|
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.3.tgz",
|
||||||
"integrity": "sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==",
|
"integrity": "sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lunr": "^2.3.9",
|
"lunr": "^2.3.9",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"jsdoc": "^4.0.2",
|
"jsdoc": "^4.0.2",
|
||||||
"node-sass": "^9.0.0",
|
"node-sass": "^9.0.0",
|
||||||
"npm": "^8.11.0",
|
"npm": "^8.11.0",
|
||||||
"rollup": "^3.17.2",
|
"rollup": "^4.1.5",
|
||||||
"rollup-plugin-peer-deps-external": "^2.2.3",
|
"rollup-plugin-peer-deps-external": "^2.2.3",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-typescript2": "^0.36.0",
|
"rollup-plugin-typescript2": "^0.36.0",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue