diff --git a/.editorconfig b/.editorconfig index 7e375e1..ad8e2c3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true -[*.{md,yml,yaml}] +[*.{html,md,yml,yaml}] indent_size = 2 indent_style = space diff --git a/deploy/kustomization.yaml b/deploy/kustomization.yaml index a988f88..bc9e17a 100644 --- a/deploy/kustomization.yaml +++ b/deploy/kustomization.yaml @@ -8,6 +8,7 @@ resources: - vmop-image-repository-pvc.yaml - vmop-config-map.yaml - vmop-deployment.yaml +- vmop-service.yaml - vmrunner-role.yaml - vmrunner-service-account.yaml - vmrunner-role-binding.yaml diff --git a/deploy/vmop-service.yaml b/deploy/vmop-service.yaml new file mode 100644 index 0000000..ea5cf66 --- /dev/null +++ b/deploy/vmop-service.yaml @@ -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 diff --git a/dev-example/config.yaml b/dev-example/config.yaml index 26d53d4..4a471a8 100644 --- a/dev-example/config.yaml +++ b/dev-example/config.yaml @@ -23,15 +23,18 @@ password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" "/RoleConfigurator": rolesByUser: + # User admin has role admin admin: - admin + # All users have role other "*": - - user + - other replace: false "/RoleConletFilter": conletTypesByRole: - user: - - "!org.jgrapes.webconlet.sysinfo.SysInfoConlet" - - "*" + # Admins can use all conlets admin: - "*" + # Others cannot use any conlet (except login conlet to log out) + other: + - org.jgrapes.webconlet.locallogin.LoginConlet diff --git a/dev-example/kustomization.yaml b/dev-example/kustomization.yaml index 5bb4a62..ae36fe1 100644 --- a/dev-example/kustomization.yaml +++ b/dev-example/kustomization.yaml @@ -34,6 +34,37 @@ patches: "/Reconciler": runnerData: 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: group: apps diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StartVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java similarity index 67% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StartVm.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java index fb28f0a..8f735da 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StartVm.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java @@ -22,22 +22,27 @@ import org.jgrapes.core.Channel; import org.jgrapes.core.Event; /** - * Starts a VM. + * Modifies a VM. */ @SuppressWarnings("PMD.DataClass") -public class StartVm extends Event { +public class ModifyVm extends Event { 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 name the name */ - public StartVm(String name, Channel... channels) { + public ModifyVm(String name, String path, Object value, + Channel... channels) { super(channels); this.name = name; + this.path = path; + this.value = value; } /** @@ -49,4 +54,22 @@ public class StartVm extends Event { return name; } + /** + * Gets the path. + * + * @return the path + */ + public String path() { + return path; + } + + /** + * Gets the value. + * + * @return the value + */ + public Object value() { + return value; + } + } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StopVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StopVm.java deleted file mode 100644 index a6d6281..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StopVm.java +++ /dev/null @@ -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 . - */ - -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 { - - 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; - } - -} diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle index 33a6fb5..faa87d1 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -39,7 +39,7 @@ dependencies { application { applicationName = 'vm-manager' - applicationDefaultJvmArgs = ['-Xmx128m', '-XX:+UseParallelGC', + applicationDefaultJvmArgs = ['-Xmx64m', '-XX:+UseParallelGC', '-Djava.util.logging.manager=org.jdrupes.vmoperator.util.LongLoggingManager' ] // Define the main class for the application. diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html new file mode 100644 index 0000000..8147dca --- /dev/null +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html @@ -0,0 +1,3 @@ +
+Copyright © Michael N. Lipp 2023 +
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java new file mode 100644 index 0000000..000a21e --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java @@ -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 . + */ + +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); + } + +} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java index 10bb1b2..3f7badb 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java @@ -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_KIND_VM; import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.manager.events.StartVm; -import org.jdrupes.vmoperator.manager.events.StopVm; +import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; @@ -152,30 +151,18 @@ public class Controller extends Component { } /** - * On start vm. + * On modify vm. * * @param event the event * @throws ApiException the api exception * @throws IOException Signals that an I/O exception has occurred. */ @Handler - public void onStartVm(StartVm event) throws ApiException, IOException { - patchRunning(event.name(), true); + public void onModigyVm(ModifyVm event) throws ApiException, IOException { + patchVmSpec(event.name(), event.path(), event.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) + private void patchVmSpec(String name, String path, Object value) throws ApiException, IOException { var crApi = K8s.crApi(Config.defaultClient(), VM_OP_GROUP, VM_OP_KIND_VM, namespace, name); @@ -188,12 +175,13 @@ public class Controller extends Component { // Patch running PatchOptions patchOpts = new PatchOptions(); patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); + String valueAsText = value instanceof String + ? "\"" + value + "\"" + : value.toString(); var res = crApi.get().patch(namespace, name, V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": " - + "\"/spec/vm/state\", " - + "\"value\": \"" + (running ? "Running" : "Stopped") - + "\"}]"), + new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/" + + path + "\", \"value\": " + valueAsText + "}]"), patchOpts); if (!res.isSuccess()) { logger.warning( diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java index f668c21..d39718f 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java @@ -135,6 +135,7 @@ public class Manager extends Component { console.attach(new BrowserLocalBackedKVStore( console.channel(), consoleWeblet.prefix().getPath())); console.attach(new KVStoreBasedConsolePolicy(console.channel())); + console.attach(new AvoidEmptyPolicy(console.channel())); console.attach(new RoleConfigurator(console.channel())); console.attach(new RoleConletFilter(console.channel())); console.attach(new LoginConlet(console.channel())); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java index bbee56e..dda438b 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java @@ -28,6 +28,7 @@ import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import org.jdrupes.vmoperator.common.Convertions; import org.jdrupes.vmoperator.util.Dto; import org.jdrupes.vmoperator.util.FsdUtils; @@ -272,7 +273,7 @@ public class Configuration implements Dto { private boolean checkRuntimeDir() { // Runtime directory (sockets etc.) 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()) { try { // When appDir is derived from XDG_RUNTIME_DIR @@ -288,7 +289,7 @@ public class Configuration implements Dto { runtimeDir)); } } - runtimeDir = FsdUtils.runtimeDir(Runner.APP_NAME.replace("-", "")) + runtimeDir = FsdUtils.runtimeDir(APP_NAME.replace("-", "")) .resolve(vm.name); runtimeDir.toFile().mkdir(); swtpmSocket = runtimeDir.resolve("swtpm-sock"); @@ -308,8 +309,8 @@ public class Configuration implements Dto { private boolean checkDataDir() { // Data directory if (dataDir == null) { - dataDir = FsdUtils.dataHome(Runner.APP_NAME.replace("-", "")) - .resolve(vm.name); + dataDir + = FsdUtils.dataHome(APP_NAME.replace("-", "")).resolve(vm.name); } if (!Files.exists(dataDir)) { dataDir.toFile().mkdirs(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java index 3ace2e5..4e4ba18 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java @@ -50,6 +50,7 @@ import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; 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.events.MonitorCommand; import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured; @@ -178,8 +179,6 @@ import org.jgrapes.util.events.WatchFile; "PMD.DataflowAnomalyAnalysis" }) public class Runner extends Component { - /** The Constant APP_NAME. */ - public static final String APP_NAME = "vm-runner"; private static final String TEMPLATE_DIR = "/opt/" + APP_NAME.replace("-", "") + "/templates"; private static final String DEFAULT_TEMPLATE @@ -609,7 +608,7 @@ public class Runner extends Component { static { try { InputStream props; - var path = FsdUtils.findConfigFile(Runner.APP_NAME.replace("-", ""), + var path = FsdUtils.findConfigFile(APP_NAME.replace("-", ""), "logging.properties"); if (path.isPresent()) { props = Files.newInputStream(path.get()); diff --git a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java index c16b3e0..e3d9fcd 100644 --- a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java +++ b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java @@ -23,6 +23,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import java.math.BigInteger; +import java.util.Collections; +import java.util.List; import java.util.Optional; 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. */ @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", - "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal" }) + "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal", "PMD.GodClass" }) public class GsonPtr { private final JsonElement position; @@ -209,6 +211,21 @@ public class GsonPtr { .map(JsonPrimitive::getAsBoolean); } + /** + * Returns the elements of the selected {@link JsonArray} as list. + * + * @param the generic type + * @param cls the cls + * @param selectors the selectors + * @return the list + */ + @SuppressWarnings("unchecked") + public List getAsListOf(Class cls, + Object... selectors) { + return get(JsonArray.class, selectors).map(a -> (List) a.asList()) + .orElse(Collections.emptyList()); + } + /** * Sets the selected value. This pointer must point to a * {@link JsonObject} or {@link JsonArray}. The selector must @@ -248,6 +265,18 @@ public class GsonPtr { 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 * only if it doesn't exist yet, else returns the existing value. diff --git a/org.jdrupes.vmoperator.vmconlet/build.gradle b/org.jdrupes.vmoperator.vmconlet/build.gradle index 2031651..0adb40e 100644 --- a/org.jdrupes.vmoperator.vmconlet/build.gradle +++ b/org.jdrupes.vmoperator.vmconlet/build.gradle @@ -7,7 +7,9 @@ dependencies { 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.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' @@ -22,6 +24,7 @@ task extractDependencies(type: Copy) { || it.name.contains('org.jgrapes.webconsole.base') } .collect{ zipTree (it) } + exclude '*.class' into 'build/unpacked' duplicatesStrategy 'include' } diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html index 7fa0a7f..11cd882 100644 --- a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html @@ -1,5 +1,46 @@
-
Preview
+ data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps"> + +
+
+ {{ localize("Period") }}: +
    +
  • + +
  • +
  • + +
  • +
+
+
+ + + + + + + + + + + + + + + + +
{{ localize("VMsSummary") }}:{{ vmSummary.runningVms }} / {{ vmSummary.totalVms }}
{{ localize("Used CPUs") }}:{{ vmSummary.usedCpus }}
{{ localize("Used RAM") }}:{{ formatMemory(Number(vmSummary.usedRam)) }}
+
+ +
+
diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html index 83492f4..0e4bb26 100644 --- a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html @@ -44,8 +44,10 @@ class="fa fa-check" :title="localize('Yes')"> + {{ shortDateTime(entry[key].toString()) }} + >{{ formatMemory(entry[key]) }} @@ -64,7 +66,7 @@ - + @@ -72,15 +74,31 @@ - + + - + - + +
{{ localize("maximumCpus") }}
{{ localize("requestedCpus") }}{{ entry.spec.vm.maximumCpus }}{{ entry.spec.vm.currentCpus }}
{{ cic.error }}
{{ localize("maximumRam") }}{{ formatMemory(BigInt(entry.spec.vm.maximumRam)) }}{{ formatMemory(Number(entry.spec.vm.maximumRam)) }}
{{ localize("requestedRam") }}{{ formatMemory(BigInt(entry.spec.vm.maximumRam)) }}{{ formatMemory(entry.spec.vm.currentRam) }}
{{ cic.error }}
diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties index d0ee970..9a0efcf 100644 --- a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties @@ -1,5 +1,8 @@ conletName = VM Viewer +VMsSummary = VMs (running/total) + +since = Since currentCpus = Current CPUs currentRam = Current RAM maximumCpus = Maximum CPUs diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties index bf19f93..d17bab6 100644 --- a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties @@ -1,6 +1,15 @@ 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 +since = Seit currentCpus = Aktuelle CPUs currentRam = Akuelles RAM maximumCpus = Maximale CPUs @@ -10,6 +19,8 @@ requestedCpus = Angeforderte CPUs requestedRam = Angefordertes RAM vmActions = Aktionen vmname = Name +Value\ is\ above\ maximum = Wert ist zu groß +Illegal\ format = Ungültiges Format Start\ VM = VM Starten Stop\ VM = VM Anhalten diff --git a/org.jdrupes.vmoperator.vmconlet/rollup.config.mjs b/org.jdrupes.vmoperator.vmconlet/rollup.config.mjs index 7565030..f1273ad 100644 --- a/org.jdrupes.vmoperator.vmconlet/rollup.config.mjs +++ b/org.jdrupes.vmoperator.vmconlet/rollup.config.mjs @@ -11,7 +11,8 @@ let pathsMap = { "jgconsole": "../../console-base-resource/jgconsole.js", "jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js", "l10nBundles": "./" + baseName + "-l10nBundles.ftl.js", - "vue": "../../page-resource/vue/vue.esm-browser.js" + "vue": "../../page-resource/vue/vue.esm-browser.js", + "chartjs": "../../page-resource/chart.js/auto.js" } export default { diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/TimeSeries.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/TimeSeries.java new file mode 100644 index 0000000..cc17295 --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/TimeSeries.java @@ -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 . + */ + +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 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 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); + } + } +} diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java index 86e152e..4f9c0a9 100644 --- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java @@ -24,8 +24,13 @@ import freemarker.template.MalformedTemplateNameException; import freemarker.template.Template; import freemarker.template.TemplateNotFoundException; import io.kubernetes.client.custom.Quantity; +import io.kubernetes.client.custom.Quantity.Format; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; 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.Map; import java.util.Optional; @@ -33,8 +38,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.jdrupes.json.JsonBeanDecoder; import org.jdrupes.json.JsonDecodeException; -import org.jdrupes.vmoperator.manager.events.StartVm; -import org.jdrupes.vmoperator.manager.events.StopVm; +import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type; @@ -59,6 +63,7 @@ import org.jgrapes.webconsole.base.events.SetLocale; import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; /** + * The Class VmConlet. */ @SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class VmConlet extends FreeMarkerConlet { @@ -67,6 +72,14 @@ public class VmConlet extends FreeMarkerConlet { RenderMode.Preview, RenderMode.View); private final Map vmInfos = 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 { + } /** * Creates a new component with its channel set to the given channel. @@ -77,6 +90,7 @@ public class VmConlet extends FreeMarkerConlet { */ public VmConlet(Channel componentChannel) { super(componentChannel); + setPeriodicRefresh(Duration.ofMinutes(1), () -> new Update()); } /** @@ -116,7 +130,7 @@ public class VmConlet extends FreeMarkerConlet { ConsoleConnection channel, String conletId, VmsModel conletState) throws Exception { Set renderedAs = new HashSet<>(); - boolean sendData = false; + boolean sendVmInfos = false; if (event.renderAs().contains(RenderMode.Preview)) { Template tpl = freemarkerConfig().getTemplate("VmConlet-preview.ftl.html"); @@ -127,7 +141,12 @@ public class VmConlet extends FreeMarkerConlet { RenderMode.Preview.addModifiers(event.renderAs())) .setSupportedModes(MODES)); 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)) { Template tpl @@ -139,9 +158,9 @@ public class VmConlet extends FreeMarkerConlet { RenderMode.View.addModifiers(event.renderAs())) .setSupportedModes(MODES)); renderedAs.add(RenderMode.View); - sendData = true; + sendVmInfos = true; } - if (sendData) { + if (sendVmInfos) { for (var vmInfo : vmInfos.values()) { var def = JsonBeanDecoder.create(vmInfo.getRaw().toString()) .readObject(); @@ -158,13 +177,14 @@ public class VmConlet extends FreeMarkerConlet { * * @param event the event * @param channel the channel - * @throws JsonDecodeException + * @throws JsonDecodeException the json decode exception + * @throws IOException */ @Handler(namedChannels = "manager") - @SuppressWarnings({ "PMD.ConfusingTernary", - "PMD.AvoidInstantiatingObjectsInLoops" }) + @SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" }) public void onVmDefChanged(VmDefChanged event, VmChannel channel) - throws JsonDecodeException { + throws JsonDecodeException, IOException { if (event.type() == Type.DELETED) { var vmName = event.vmDefinition().getMetadata().getName(); vmInfos.remove(vmName); @@ -175,25 +195,7 @@ public class VmConlet extends FreeMarkerConlet { } } } else { - var vmDef = new DynamicKubernetesObject( - 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 vmDef = prepareForSending(event); var def = JsonBeanDecoder.create(vmDef.getRaw().toString()) .readObject(); for (var entry : conletIdsByConsoleConnection().entrySet()) { @@ -203,20 +205,165 @@ public class VmConlet extends FreeMarkerConlet { } } } + + 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 + @SuppressWarnings("PMD.AvoidDecimalLiteralsInBigDecimalConstructor") protected void doUpdateConletState(NotifyConletModel event, ConsoleConnection channel, VmsModel conletState) throws Exception { event.stop(); switch (event.method()) { case "start": - fire(new StartVm(event.params().asString(0), + fire(new ModifyVm(event.params().asString(0), "state", "Running", new NamedChannel("manager"))); break; 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"))); break; default:// ignore diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/ConditionalInputController.ts b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/ConditionalInputController.ts new file mode 100644 index 0000000..e06d245 --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/ConditionalInputController.ts @@ -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 . + */ + +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; + } + } + +} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/CpuRamChart.ts b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/CpuRamChart.ts new file mode 100644 index 0000000..50525f0 --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/CpuRamChart.ts @@ -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 . + */ + +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() { + (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(); + } +} + diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/MemorySize.ts b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/MemorySize.ts new file mode 100644 index 0000000..d3795bf --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/MemorySize.ts @@ -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 . + */ + +let unitMap = new Map(); +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; +} diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/TimeSeries.ts b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/TimeSeries.ts new file mode 100644 index 0000000..284663a --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/TimeSeries.ts @@ -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 . + */ + +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]; + } +} + diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts index 43ab44b..a2cbeec 100644 --- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts @@ -16,51 +16,19 @@ * along with this program. If not, see . */ -import { reactive, ref, createApp, computed, onMounted } from "vue"; +import { + reactive, ref, Ref, createApp, computed, onMounted, watch, nextTick +} from "vue"; import JGConsole from "jgconsole"; import JgwcPlugin, { JGWC } from "jgwc"; -import { provideApi, getApi } from "aash-plugin"; 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"; -// -// Helpers -// -let unitMap = new Map(); -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 declare global { interface Window { @@ -71,14 +39,64 @@ declare global { window.orgJDrupesVmOperatorVmConlet = {}; let vmInfos = reactive(new Map()); +let vmSummary = reactive({ + totalVms: 0, + runningVms: 0, + usedCpus: 0, + usedRam: "" +}); -window.orgJDrupesVmOperatorVmConlet.initPreview - = (previewDom: HTMLElement, _isUpdate: boolean) => { - const app = createApp({}); - app.use(JgwcPlugin, []); - app.config.globalProperties.window = window; - app.mount(previewDom); - }; +const localize = (key: string) => { + return JGConsole.localize( + l10nBundles, JGWC.lang(), key); +}; + +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(null); + +window.orgJDrupesVmOperatorVmConlet.initPreview = (previewDom: HTMLElement, + _isUpdate: boolean) => { + const app = createApp({ + setup(_props: any) { + const conletId: string + = (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 = ref("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, _isUpdate: boolean) => { @@ -87,14 +105,10 @@ window.orgJDrupesVmOperatorVmConlet.initView = (viewDom: HTMLElement, const conletId: string = (viewDom.parentNode!).dataset["conletId"]!; - const localize = (key: string) => { - return JGConsole.localize( - l10nBundles, JGWC.lang() || "en", key); - }; - const controller = reactive(new JGConsole.TableController([ ["name", "vmname"], ["running", "running"], + ["runningConditionSince", "since"], ["currentCpus", "currentCpus"], ["currentRam", "currentRam"], ["nodeName", "nodeName"] @@ -114,12 +128,30 @@ window.orgJDrupesVmOperatorVmConlet.initView = (viewDom: HTMLElement, const idScope = JGWC.createIdScope(); 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 { - controller, vmInfos, filteredData, detailsByName, - localize, formatMemory, vmAction, + controller, vmInfos, filteredData, detailsByName, localize, + shortDateTime, formatMemory, vmAction, cic, parseMemory, scopedId: (id: string) => { return idScope.scopedId(id); } - } + }; } }); app.use(JgwcPlugin); @@ -132,14 +164,15 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmconlet.VmConlet", // Add some short-cuts for table controller vmDefinition.name = vmDefinition.metadata.name; vmDefinition.currentCpus = vmDefinition.status.cpus; - vmDefinition.currentRam = BigInt(vmDefinition.status.ram); + vmDefinition.currentRam = Number(vmDefinition.status.ram); for (let condition of vmDefinition.status.conditions) { if (condition.type === "Running") { vmDefinition.running = condition.status === "True"; + vmDefinition.runningConditionSince + = new Date(condition.lastTransitionTime); break; } } - vmInfos.set(vmDefinition.name, vmDefinition); }); @@ -147,3 +180,22 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmconlet.VmConlet", "removeVm", function(_conletId: String, vmName: String) { 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); +}); + diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss index 349d5be..6ca0333 100644 --- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss @@ -20,6 +20,23 @@ * 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 { display: flex; justify-content: flex-end @@ -30,7 +47,7 @@ } .jdrupes-vmoperator-vmconlet-view-action-list { - white-space: nowrap; + white-space: nowrap; } .jdrupes-vmoperator-vmconlet-view-action-list [role=button]:not(:last-child) { @@ -39,28 +56,51 @@ .jdrupes-vmoperator-vmconlet-view td { vertical-align: top; + + &[tabindex] { + outline: 1px solid var(--primary); + cursor: text; + } } .jdrupes-vmoperator-vmconlet-view td:not([colspan]):first-child { - white-space: nowrap; + white-space: nowrap; } .jdrupes-vmoperator-vmconlet-view table td.details { - padding-left: 1em; + padding-left: 1em; } -.jdrupes-vmoperator-vmconlet-view-table { - td.column-running { - text-align: center; +.jdrupes-vmoperator-vmconlet-view-table { + td.column-running { + text-align: center; - span { - &.fa-check { - color: var(--success); - } + span { + &.fa-check { + color: var(--success); + } - &.fa-close { - color: var(--danger); - } - } + &.fa-close { + 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); + } + } + } } diff --git a/org.jdrupes.vmoperator.vmconlet/tsconfig.json b/org.jdrupes.vmoperator.vmconlet/tsconfig.json index 906e474..5fb2dd7 100644 --- a/org.jdrupes.vmoperator.vmconlet/tsconfig.json +++ b/org.jdrupes.vmoperator.vmconlet/tsconfig.json @@ -15,7 +15,8 @@ "jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"], "jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"], "l10nBundles": ["./src/org/jdrupes/vmoperator/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"], diff --git a/package-lock.json b/package-lock.json index 4eb8aaf..1a31645 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "jsdoc": "^4.0.2", "node-sass": "^9.0.0", "npm": "^8.11.0", - "rollup": "^3.17.2", + "rollup": "^4.1.5", "rollup-plugin-peer-deps-external": "^2.2.3", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.36.0", @@ -613,13 +613,13 @@ } }, "node_modules/@rollup/plugin-replace": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.3.tgz", - "integrity": "sha512-je7fu05B800IrMlWjb2wzJcdXzHYW46iTipfChnBDbIbDXhASZs27W1B58T2Yf45jZtJUONegpbce+9Ut2Ti/Q==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz", + "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.27.0" + "magic-string": "^0.30.3" }, "engines": { "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -838,18 +994,6 @@ "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": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", @@ -873,18 +1017,6 @@ "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": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", @@ -3093,12 +3225,12 @@ "dev": true }, "node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "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.13" + "@jridgewell/sourcemap-codec": "^1.4.15" }, "engines": { "node": ">=12" @@ -8770,18 +8902,30 @@ } }, "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.1.5.tgz", + "integrity": "sha512-AEw14/q4NHYQkQlngoSae2yi7hDBeT9w84aEzdgCr39+2RL+iTG84lGTkgC1Wp5igtquN64cNzuzZKVz+U6jOg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "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" } }, @@ -9008,9 +9152,9 @@ "optional": true }, "node_modules/sass": { - "version": "1.69.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.3.tgz", - "integrity": "sha512-X99+a2iGdXkdWn1akFPs0ZmelUzyAQfvqYc2P/MPTrJRuIRoTffGzT9W9nFqG00S+c8hXzVmgxhUuHFdrwxkhQ==", + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -9517,9 +9661,9 @@ "dev": true }, "node_modules/terser": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.21.0.tgz", - "integrity": "sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.23.0.tgz", + "integrity": "sha512-Iyy83LN0uX9ZZLCX4Qbu5JiHiWjOCTwrmM9InWOzVeM++KNWEsqV4YgN9U9E8AlohQ6Gs42ztczlWOG/lwDAMA==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -9615,9 +9759,9 @@ } }, "node_modules/typedoc": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.2.tgz", - "integrity": "sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.3.tgz", + "integrity": "sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==", "dev": true, "dependencies": { "lunr": "^2.3.9", diff --git a/package.json b/package.json index 42bf2fa..1e01398 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "jsdoc": "^4.0.2", "node-sass": "^9.0.0", "npm": "^8.11.0", - "rollup": "^3.17.2", + "rollup": "^4.1.5", "rollup-plugin-peer-deps-external": "^2.2.3", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.36.0",