Rename conlet.
This commit is contained in:
parent
4ceaaa9fa2
commit
e839f7b3b2
34 changed files with 42 additions and 42 deletions
10
org.jdrupes.vmoperator.vmmgmt/.checkstyle
Normal file
10
org.jdrupes.vmoperator.vmmgmt/.checkstyle
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<fileset-config file-format-version="1.2.0" simple-config="false" sync-formatter="false">
|
||||
<local-check-config name="Project Checks" location="/VM-Operator/checkstyle.xml" type="project" description="">
|
||||
<additional-data name="protect-config-file" value="false"/>
|
||||
</local-check-config>
|
||||
<fileset name="all" enabled="true" check-config-name="Project Checks" local="true">
|
||||
<file-match-pattern match-pattern="." include-pattern="true"/>
|
||||
</fileset>
|
||||
</fileset-config>
|
||||
7
org.jdrupes.vmoperator.vmmgmt/.eclipse-pmd
Normal file
7
org.jdrupes.vmoperator.vmmgmt/.eclipse-pmd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<eclipse-pmd xmlns="http://acanda.ch/eclipse-pmd/0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://acanda.ch/eclipse-pmd/0.8 http://acanda.ch/eclipse-pmd/eclipse-pmd-0.8.xsd">
|
||||
<analysis enabled="true" />
|
||||
<rulesets>
|
||||
<ruleset name="Custom Rules" ref="VM-Operator/ruleset.xml" refcontext="workspace" />
|
||||
</rulesets>
|
||||
</eclipse-pmd>
|
||||
1
org.jdrupes.vmoperator.vmmgmt/.eslintignore
Normal file
1
org.jdrupes.vmoperator.vmmgmt/.eslintignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
rollup.config.mjs
|
||||
15
org.jdrupes.vmoperator.vmmgmt/.eslintrc.json
Normal file
15
org.jdrupes.vmoperator.vmmgmt/.eslintrc.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": { "project": ["./tsconfig.json"] },
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"constructor-super": "off"
|
||||
}
|
||||
}
|
||||
|
||||
4
org.jdrupes.vmoperator.vmmgmt/.gitignore
vendored
Normal file
4
org.jdrupes.vmoperator.vmmgmt/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/bin/
|
||||
/bin_test/
|
||||
/generated/
|
||||
/build/
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
build.commands=org.eclipse.jdt.core.javabuilder
|
||||
connection.arguments=
|
||||
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
||||
connection.java.home=null
|
||||
connection.jvm.arguments=
|
||||
connection.project.dir=..
|
||||
derived.resources=.gradle,generated
|
||||
eclipse.preferences.version=1
|
||||
natures=org.eclipse.jdt.groovy.core.groovyNature,org.eclipse.jdt.core.javanature
|
||||
project.path=\:org.jgrapes.osgi.conlets.services
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
eclipse.preferences.version=1
|
||||
encoding/<project>=UTF-8
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
eclipse.preferences.version=1
|
||||
line.separator=\n
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
eclipse.preferences.version=1
|
||||
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
|
||||
formatter_profile=_JGrapes
|
||||
formatter_settings_version=13
|
||||
sp_cleanup.add_default_serial_version_id=true
|
||||
sp_cleanup.add_generated_serial_version_id=false
|
||||
sp_cleanup.add_missing_annotations=true
|
||||
sp_cleanup.add_missing_deprecated_annotations=true
|
||||
sp_cleanup.add_missing_methods=false
|
||||
sp_cleanup.add_missing_nls_tags=false
|
||||
sp_cleanup.add_missing_override_annotations=true
|
||||
sp_cleanup.add_missing_override_annotations_interface_methods=true
|
||||
sp_cleanup.add_serial_version_id=false
|
||||
sp_cleanup.always_use_blocks=true
|
||||
sp_cleanup.always_use_parentheses_in_expressions=false
|
||||
sp_cleanup.always_use_this_for_non_static_field_access=false
|
||||
sp_cleanup.always_use_this_for_non_static_method_access=false
|
||||
sp_cleanup.convert_functional_interfaces=false
|
||||
sp_cleanup.convert_to_enhanced_for_loop=false
|
||||
sp_cleanup.correct_indentation=false
|
||||
sp_cleanup.format_source_code=true
|
||||
sp_cleanup.format_source_code_changes_only=false
|
||||
sp_cleanup.insert_inferred_type_arguments=false
|
||||
sp_cleanup.make_local_variable_final=true
|
||||
sp_cleanup.make_parameters_final=false
|
||||
sp_cleanup.make_private_fields_final=true
|
||||
sp_cleanup.make_type_abstract_if_missing_method=false
|
||||
sp_cleanup.make_variable_declarations_final=false
|
||||
sp_cleanup.never_use_blocks=false
|
||||
sp_cleanup.never_use_parentheses_in_expressions=true
|
||||
sp_cleanup.on_save_use_additional_actions=false
|
||||
sp_cleanup.organize_imports=false
|
||||
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
|
||||
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
|
||||
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
|
||||
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
|
||||
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
|
||||
sp_cleanup.remove_private_constructors=true
|
||||
sp_cleanup.remove_redundant_type_arguments=false
|
||||
sp_cleanup.remove_trailing_whitespaces=false
|
||||
sp_cleanup.remove_trailing_whitespaces_all=true
|
||||
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
|
||||
sp_cleanup.remove_unnecessary_casts=true
|
||||
sp_cleanup.remove_unnecessary_nls_tags=false
|
||||
sp_cleanup.remove_unused_imports=false
|
||||
sp_cleanup.remove_unused_local_variables=false
|
||||
sp_cleanup.remove_unused_private_fields=true
|
||||
sp_cleanup.remove_unused_private_members=false
|
||||
sp_cleanup.remove_unused_private_methods=true
|
||||
sp_cleanup.remove_unused_private_types=true
|
||||
sp_cleanup.sort_members=false
|
||||
sp_cleanup.sort_members_all=false
|
||||
sp_cleanup.use_anonymous_class_creation=false
|
||||
sp_cleanup.use_blocks=false
|
||||
sp_cleanup.use_blocks_only_for_return_and_throw=false
|
||||
sp_cleanup.use_lambda=true
|
||||
sp_cleanup.use_parentheses_in_expressions=false
|
||||
sp_cleanup.use_this_for_non_static_field_access=false
|
||||
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
|
||||
sp_cleanup.use_this_for_non_static_method_access=false
|
||||
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
|
||||
sp_jautodoc.cleanup.add_header=false
|
||||
sp_jautodoc.cleanup.replace_header=false
|
||||
57
org.jdrupes.vmoperator.vmmgmt/build.gradle
Normal file
57
org.jdrupes.vmoperator.vmmgmt/build.gradle
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
plugins {
|
||||
id 'org.jdrupes.vmoperator.java-library-conventions'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':org.jdrupes.vmoperator.manager.events')
|
||||
|
||||
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.1.0,3)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)'
|
||||
|
||||
}
|
||||
|
||||
apply plugin: 'com.github.node-gradle.node'
|
||||
|
||||
node {
|
||||
download = true
|
||||
}
|
||||
|
||||
task extractDependencies(type: Copy) {
|
||||
from configurations.compileClasspath
|
||||
.findAll{ it.name.contains('.provider.')
|
||||
|| it.name.contains('org.jgrapes.webconsole.base')
|
||||
}
|
||||
.collect{ zipTree (it) }
|
||||
exclude '*.class'
|
||||
into 'build/unpacked'
|
||||
duplicatesStrategy 'include'
|
||||
}
|
||||
|
||||
task compileTs(type: NodeTask) {
|
||||
dependsOn ':npmInstall'
|
||||
dependsOn extractDependencies
|
||||
inputs.dir project.file('src')
|
||||
inputs.file project.file('tsconfig.json')
|
||||
inputs.file project.file('rollup.config.mjs')
|
||||
outputs.dir project.file('build/generated/resources')
|
||||
script = file("${rootProject.rootDir}/node_modules/rollup/dist/bin/rollup")
|
||||
args = ["-c"]
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
resources {
|
||||
srcDir project.file('build/generated/resources')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processResources {
|
||||
dependsOn compileTs
|
||||
}
|
||||
|
||||
eclipse {
|
||||
autoBuildTasks compileTs
|
||||
}
|
||||
1
org.jdrupes.vmoperator.vmmgmt/package.json
Normal file
1
org.jdrupes.vmoperator.vmmgmt/package.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
org.jdrupes.vmoperator.vmmgmt.VmMgmtFactory
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Moodle Tools Console
|
||||
* Copyright (C) 2022 Michael N. Lipp
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU 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 General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const l10nBundles = new Map();
|
||||
let entries = null;
|
||||
// <#list supportedLanguages() as l>
|
||||
entries = new Map();
|
||||
l10nBundles.set("${l.locale.toLanguageTag()}", entries);
|
||||
// <#list l.l10nBundle.keys as key>
|
||||
entries.set("${key}", "${l.l10nBundle.getString(key)}");
|
||||
// </#list>
|
||||
// </#list>
|
||||
|
||||
export default l10nBundles;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<div class="jdrupes-vmoperator-vmmgmt jdrupes-vmoperator-vmmgmt-preview"
|
||||
data-conlet-grid-rows="5"
|
||||
data-jgwc-on-load="orgJDrupesVmOperatorVmMgmt.initPreview"
|
||||
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
|
||||
|
||||
<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("currentCpus") }}:</td>
|
||||
<td>{{ vmSummary.usedCpus }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize("currentRam") }}:</td>
|
||||
<td>{{ formatMemory(Number(vmSummary.usedRam)) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="vmsChart-wrapper">
|
||||
<canvas class="vmsChart"></canvas>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<div class="jdrupes-vmoperator-vmmgmt jdrupes-vmoperator-vmmgmt-view"
|
||||
data-jgwc-on-load="orgJDrupesVmOperatorVmMgmt.initView"
|
||||
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
|
||||
<div class="jdrupes-vmoperator-vmmgmt-view-search">
|
||||
<form>
|
||||
<label class="form__label--horizontal">
|
||||
<span>{{ localize("Filter") }}</span>
|
||||
<input type="text" class="form__input-text--with-remove"
|
||||
v-on:input="controller.updateFilter($event)">
|
||||
<span role="button" tabindex="0" class="fa fa-remove"
|
||||
v-on:click="controller.clearFilter($event)"></span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<table
|
||||
class="table--basic--striped jdrupes-vmoperator-vmmgmt-view-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="key in controller.keys"
|
||||
class="sortable" v-on:click="controller.sortBy(key)">
|
||||
{{ localize(controller.label(key)) }}<span v-if="controller.sortedByAsc(key)"
|
||||
role="button" tabindex="0">▲</span><span
|
||||
v-if="controller.sortedByDesc(key)" role="button"
|
||||
tabindex="0">▼</span>
|
||||
</th>
|
||||
<th>
|
||||
{{ localize("vmActions") }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(entry, rowIndex) in filteredData">
|
||||
<tr :class="[(rowIndex % 2) ? 'odd' : 'even']"
|
||||
:aria-expanded="(entry.name in detailsByName) ? 'true' : 'false'">
|
||||
<td v-for="key in controller.keys"
|
||||
v-bind:class="'column-' + key"
|
||||
v-bind:title="key == 'name' ? entry['name']: false"
|
||||
v-bind:rowspan="(key == 'name') && $aash.isDisclosed(scopedId(rowIndex)) ? 2 : false">
|
||||
<aash-disclosure-button v-if="key === 'name'" :type="'div'"
|
||||
:id-ref="scopedId(rowIndex)">
|
||||
<span v-html="controller.breakBeforeDots(entry[key])"></span>
|
||||
</aash-disclosure-button>
|
||||
<span v-else-if="key === 'running' && entry[key]"
|
||||
class="fa fa-check" :title="localize('Yes')"></span>
|
||||
<span v-else-if="key === 'running' && !entry[key]"
|
||||
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'"
|
||||
>{{ formatMemory(entry[key]) }}</span>
|
||||
<span v-else
|
||||
v-html="controller.breakBeforeDots(entry[key])"></span>
|
||||
</td>
|
||||
<td class="jdrupes-vmoperator-vmmgmt-view-action-list">
|
||||
<span role="button"
|
||||
v-if="entry.spec.vm.state != 'Running' && !entry['running']"
|
||||
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
|
||||
v-on:click="vmAction(entry.name, 'start')"></span>
|
||||
<span role="button" v-else class="fa fa-play"
|
||||
aria-disabled="true" :title="localize('Start VM')"></span>
|
||||
<span role="button"
|
||||
v-if="entry.spec.vm.state != 'Stopped' && entry['running']"
|
||||
tabindex="0" class="fa fa-stop" :title="localize('Stop VM')"
|
||||
v-on:click="vmAction(entry.name, 'stop')"></span>
|
||||
<span role="button" v-else class="fa fa-stop"
|
||||
aria-disabled="true" :title="localize('Stop VM')"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr :id="scopedId(rowIndex)" v-if="$aash.isDisclosed(scopedId(rowIndex))"
|
||||
:class="[(rowIndex % 2) ? 'odd' : 'even']">
|
||||
<td colspan="8" class="details">
|
||||
<table class="table--basic table--basic--autoStriped">
|
||||
<tr>
|
||||
<td>{{ localize("maximumCpus") }}</td>
|
||||
<td>{{ maximumCpus(entry) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize("requestedCpus") }}</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>
|
||||
<td>{{ localize("maximumRam") }}</td>
|
||||
<td>{{ formatMemory(Number(entry.spec.vm.maximumRam)) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ localize("requestedRam") }}</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>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
conletName = VM Management
|
||||
|
||||
VMsSummary = VMs (running/total)
|
||||
|
||||
since = Since
|
||||
currentCpus = Current CPUs
|
||||
currentRam = Current RAM
|
||||
maximumCpus = Maximum CPUs
|
||||
maximumRam = Maximum RAM
|
||||
nodeName = Node
|
||||
requestedCpus = Requested CPUs
|
||||
requestedRam = Requested RAM
|
||||
running = Running
|
||||
usedBy = Used by
|
||||
usedFrom = Used from
|
||||
vmActions = Actions
|
||||
vmname = Name
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
conletName = VM-Management
|
||||
|
||||
VMsSummary = VMs (gestartet/gesamt)
|
||||
|
||||
Period = Zeitraum
|
||||
Last\ hour = Letzte Stunde
|
||||
Last\ day = Letzter Tag
|
||||
|
||||
running = Gestartet
|
||||
since = Seit
|
||||
currentCpus = Aktuelle CPUs
|
||||
currentRam = Akuelles RAM
|
||||
maximumCpus = Maximale CPUs
|
||||
maximumRam = Maximales RAM
|
||||
nodeName = Knoten
|
||||
requestedCpus = Angeforderte CPUs
|
||||
requestedRam = Angefordertes RAM
|
||||
usedBy = Benutzt durch
|
||||
usedFrom = Benutzt von
|
||||
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
|
||||
|
||||
Yes = Ja
|
||||
No = Nein
|
||||
36
org.jdrupes.vmoperator.vmmgmt/rollup.config.mjs
Normal file
36
org.jdrupes.vmoperator.vmmgmt/rollup.config.mjs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import typescript from 'rollup-plugin-typescript2';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
|
||||
let packagePath = "org/jdrupes/vmoperator/vmmgmt";
|
||||
let baseName = "VmMgmt"
|
||||
let module = "build/generated/resources/" + packagePath
|
||||
+ "/" + baseName + "-functions.js";
|
||||
|
||||
let pathsMap = {
|
||||
"aash-plugin": "../../page-resource/aash-vue-components/lib/aash-vue-components.js",
|
||||
"jgconsole": "../../console-base-resource/jgconsole.js",
|
||||
"jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js",
|
||||
"l10nBundles": "./" + baseName + "-l10nBundles.ftl.js",
|
||||
"vue": "../../page-resource/vue/vue.esm-browser.js",
|
||||
"chartjs": "../../page-resource/chart.js/auto.js"
|
||||
}
|
||||
|
||||
export default {
|
||||
external: ['aash-plugin', 'jgconsole', 'jgwc', 'l10nBundles', 'vue', 'chartjs'],
|
||||
input: "src/" + packagePath + "/browser/" + baseName + "-functions.ts",
|
||||
output: [
|
||||
{
|
||||
format: "esm",
|
||||
file: module,
|
||||
sourcemap: true,
|
||||
sourcemapPathTransform: (relativeSourcePath, _sourcemapPath) => {
|
||||
return relativeSourcePath.replace(/^([^/]*\/){12}/, "./");
|
||||
},
|
||||
paths: pathsMap
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
typescript(),
|
||||
postcss()
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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.vmmgmt;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The Class TimeSeries.
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||
public class TimeSeries {
|
||||
|
||||
@SuppressWarnings("PMD.LooseCoupling")
|
||||
private final LinkedList<Entry> data = new LinkedList<>();
|
||||
private final Duration period;
|
||||
|
||||
/**
|
||||
* Instantiates a new time series.
|
||||
*
|
||||
* @param period the period
|
||||
*/
|
||||
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",
|
||||
"PMD.AvoidSynchronizedStatement" })
|
||||
public TimeSeries add(Instant time, Number... numbers) {
|
||||
var newEntry = new Entry(time, numbers);
|
||||
boolean nothingNew = false;
|
||||
synchronized (data) {
|
||||
if (data.size() >= 2) {
|
||||
var lastEntry = data.get(data.size() - 1);
|
||||
var lastButOneEntry = data.get(data.size() - 2);
|
||||
nothingNew = lastEntry.valuesEqual(lastButOneEntry)
|
||||
&& lastEntry.valuesEqual(newEntry);
|
||||
}
|
||||
if (nothingNew) {
|
||||
data.removeLast();
|
||||
}
|
||||
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.removeFirst();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entries.
|
||||
*
|
||||
* @return the list
|
||||
*/
|
||||
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
|
||||
public List<Entry> entries() {
|
||||
synchronized (data) {
|
||||
return new ArrayList<>(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Class Entry.
|
||||
*/
|
||||
public static class Entry {
|
||||
private final 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,399 @@
|
|||
/*
|
||||
* 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.vmmgmt;
|
||||
|
||||
import freemarker.core.ParseException;
|
||||
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 java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||
import org.jdrupes.vmoperator.manager.events.ChannelTracker;
|
||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
import org.jdrupes.vmoperator.util.DataPath;
|
||||
import org.jgrapes.core.Channel;
|
||||
import org.jgrapes.core.Event;
|
||||
import org.jgrapes.core.Manager;
|
||||
import org.jgrapes.core.annotation.Handler;
|
||||
import org.jgrapes.webconsole.base.Conlet.RenderMode;
|
||||
import org.jgrapes.webconsole.base.ConletBaseModel;
|
||||
import org.jgrapes.webconsole.base.ConsoleConnection;
|
||||
import org.jgrapes.webconsole.base.events.AddConletRequest;
|
||||
import org.jgrapes.webconsole.base.events.AddConletType;
|
||||
import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
|
||||
import org.jgrapes.webconsole.base.events.ConsoleReady;
|
||||
import org.jgrapes.webconsole.base.events.NotifyConletModel;
|
||||
import org.jgrapes.webconsole.base.events.NotifyConletView;
|
||||
import org.jgrapes.webconsole.base.events.RenderConlet;
|
||||
import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
|
||||
import org.jgrapes.webconsole.base.events.SetLocale;
|
||||
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
||||
|
||||
/**
|
||||
* The Class {@link VmMgmt}.
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
|
||||
"PMD.CouplingBetweenObjects" })
|
||||
public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
||||
|
||||
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||
RenderMode.Preview, RenderMode.View);
|
||||
private final ChannelTracker<String, VmChannel,
|
||||
VmDefinition> channelTracker = new ChannelTracker<>();
|
||||
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.
|
||||
*
|
||||
* @param componentChannel the channel that the component's handlers listen
|
||||
* on by default and that {@link Manager#fire(Event, Channel...)}
|
||||
* sends the event to
|
||||
*/
|
||||
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||
public VmMgmt(Channel componentChannel) {
|
||||
super(componentChannel);
|
||||
setPeriodicRefresh(Duration.ofMinutes(1), () -> new Update());
|
||||
}
|
||||
|
||||
/**
|
||||
* On {@link ConsoleReady}, fire the {@link AddConletType}.
|
||||
*
|
||||
* @param event the event
|
||||
* @param channel the channel
|
||||
* @throws TemplateNotFoundException the template not found exception
|
||||
* @throws MalformedTemplateNameException the malformed template name
|
||||
* exception
|
||||
* @throws ParseException the parse exception
|
||||
* @throws IOException Signals that an I/O exception has occurred.
|
||||
*/
|
||||
@Handler
|
||||
public void onConsoleReady(ConsoleReady event, ConsoleConnection channel)
|
||||
throws TemplateNotFoundException, MalformedTemplateNameException,
|
||||
ParseException, IOException {
|
||||
// Add conlet resources to page
|
||||
channel.respond(new AddConletType(type())
|
||||
.setDisplayNames(
|
||||
localizations(channel.supportedLocales(), "conletName"))
|
||||
.addRenderMode(RenderMode.Preview)
|
||||
.addScript(new ScriptResource().setScriptType("module")
|
||||
.setScriptUri(event.renderSupport().conletResource(
|
||||
type(), "VmMgmt-functions.js"))));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<VmsModel> createNewState(AddConletRequest event,
|
||||
ConsoleConnection connection, String conletId) throws Exception {
|
||||
return Optional.of(new VmsModel(conletId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
||||
ConsoleConnection channel, String conletId, VmsModel conletState)
|
||||
throws Exception {
|
||||
Set<RenderMode> renderedAs = EnumSet.noneOf(RenderMode.class);
|
||||
boolean sendVmInfos = false;
|
||||
if (event.renderAs().contains(RenderMode.Preview)) {
|
||||
Template tpl
|
||||
= freemarkerConfig().getTemplate("VmMgmt-preview.ftl.html");
|
||||
channel.respond(new RenderConlet(type(), conletId,
|
||||
processTemplate(event, tpl,
|
||||
fmModel(event, channel, conletId, conletState)))
|
||||
.setRenderAs(
|
||||
RenderMode.Preview.addModifiers(event.renderAs()))
|
||||
.setSupportedModes(MODES));
|
||||
renderedAs.add(RenderMode.Preview);
|
||||
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
|
||||
= freemarkerConfig().getTemplate("VmMgmt-view.ftl.html");
|
||||
channel.respond(new RenderConlet(type(), conletId,
|
||||
processTemplate(event, tpl,
|
||||
fmModel(event, channel, conletId, conletState)))
|
||||
.setRenderAs(
|
||||
RenderMode.View.addModifiers(event.renderAs()))
|
||||
.setSupportedModes(MODES));
|
||||
renderedAs.add(RenderMode.View);
|
||||
sendVmInfos = true;
|
||||
}
|
||||
if (sendVmInfos) {
|
||||
for (var item : channelTracker.values()) {
|
||||
channel.respond(new NotifyConletView(type(),
|
||||
conletId, "updateVm",
|
||||
simplifiedVmDefinition(item.associated())));
|
||||
}
|
||||
}
|
||||
|
||||
return renderedAs;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||
private Map<String, Object> simplifiedVmDefinition(VmDefinition vmDef) {
|
||||
// Convert RAM sizes to unitless numbers
|
||||
var spec = DataPath.deepCopy(vmDef.spec());
|
||||
var vmSpec = DataPath.<Map<String, Object>> get(spec, "vm").get();
|
||||
vmSpec.put("maximumRam", Quantity.fromString(
|
||||
DataPath.<String> get(vmSpec, "maximumRam").orElse("0")).getNumber()
|
||||
.toBigInteger());
|
||||
vmSpec.put("currentRam", Quantity.fromString(
|
||||
DataPath.<String> get(vmSpec, "currentRam").orElse("0")).getNumber()
|
||||
.toBigInteger());
|
||||
var status = DataPath.deepCopy(vmDef.status());
|
||||
status.put("ram", Quantity.fromString(
|
||||
DataPath.<String> get(status, "ram").orElse("0")).getNumber()
|
||||
.toBigInteger());
|
||||
|
||||
// Build result
|
||||
return Map.of("metadata",
|
||||
Map.of("namespace", vmDef.namespace(),
|
||||
"name", vmDef.name()),
|
||||
"spec", spec,
|
||||
"status", status,
|
||||
"nodeName", vmDef.extra("nodeName"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Track the VM definitions.
|
||||
*
|
||||
* @param event the event
|
||||
* @param channel the channel
|
||||
* @throws IOException
|
||||
*/
|
||||
@Handler(namedChannels = "manager")
|
||||
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
|
||||
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
|
||||
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||
throws IOException {
|
||||
var vmName = event.vmDefinition().name();
|
||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||
channelTracker.remove(vmName);
|
||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||
for (String conletId : entry.getValue()) {
|
||||
entry.getKey().respond(new NotifyConletView(type(),
|
||||
conletId, "removeVm", vmName));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var vmDef = event.vmDefinition();
|
||||
channelTracker.put(vmName, channel, vmDef);
|
||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||
for (String conletId : entry.getValue()) {
|
||||
entry.getKey().respond(new NotifyConletView(type(),
|
||||
conletId, "updateVm", simplifiedVmDefinition(vmDef)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 long runningVms;
|
||||
|
||||
/** The used cpus. */
|
||||
public long 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 long getRunningVms() {
|
||||
return runningVms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the used cpus.
|
||||
*
|
||||
* @return the usedCpus
|
||||
*/
|
||||
public long 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",
|
||||
"PMD.LambdaCanBeMethodReference" })
|
||||
private Summary evaluateSummary(boolean force) {
|
||||
if (!force && cachedSummary != null) {
|
||||
return cachedSummary;
|
||||
}
|
||||
Summary summary = new Summary();
|
||||
for (var vmDef : channelTracker.associated()) {
|
||||
summary.totalVms += 1;
|
||||
summary.usedCpus += vmDef.<Number> fromStatus("cpus")
|
||||
.map(Number::intValue).orElse(0);
|
||||
summary.usedRam = summary.usedRam
|
||||
.add(vmDef.<String> fromStatus("ram")
|
||||
.map(r -> Quantity.fromString(r).getNumber().toBigInteger())
|
||||
.orElse(BigInteger.ZERO));
|
||||
summary.runningVms
|
||||
+= vmDef.<List<Map<String, Object>>> fromStatus("conditions")
|
||||
.orElse(Collections.emptyList()).stream()
|
||||
.filter(cond -> DataPath.get(cond, "type")
|
||||
.map(t -> "Running".equals(t)).orElse(false)
|
||||
&& DataPath.get(cond, "status")
|
||||
.map(s -> "True".equals(s)).orElse(false))
|
||||
.count();
|
||||
}
|
||||
cachedSummary = summary;
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PMD.AvoidDecimalLiteralsInBigDecimalConstructor")
|
||||
protected void doUpdateConletState(NotifyConletModel event,
|
||||
ConsoleConnection channel, VmsModel conletState)
|
||||
throws Exception {
|
||||
event.stop();
|
||||
String vmName = event.param(0);
|
||||
var vmChannel = channelTracker.channel(vmName).orElse(null);
|
||||
if (vmChannel == null) {
|
||||
return;
|
||||
}
|
||||
switch (event.method()) {
|
||||
case "start":
|
||||
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||
break;
|
||||
case "stop":
|
||||
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||
break;
|
||||
case "cpus":
|
||||
fire(new ModifyVm(vmName, "currentCpus",
|
||||
new BigDecimal(event.param(1).toString()).toBigInteger(),
|
||||
vmChannel));
|
||||
break;
|
||||
case "ram":
|
||||
fire(new ModifyVm(vmName, "currentRam",
|
||||
new Quantity(new BigDecimal(event.param(1).toString()),
|
||||
Format.BINARY_SI).toSuffixedString(),
|
||||
vmChannel));
|
||||
break;
|
||||
default:// ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
|
||||
String conletId) throws Exception {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Class VmsModel.
|
||||
*/
|
||||
public class VmsModel extends ConletBaseModel {
|
||||
|
||||
/**
|
||||
* Instantiates a new vms model.
|
||||
*
|
||||
* @param conletId the conlet id
|
||||
*/
|
||||
public VmsModel(String conletId) {
|
||||
super(conletId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* VM-Operator
|
||||
* Copyright (C) 2023 Michael N. Lipp
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jdrupes.vmoperator.vmmgmt;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.jgrapes.core.Channel;
|
||||
import org.jgrapes.core.ComponentType;
|
||||
import org.jgrapes.webconsole.base.ConletComponentFactory;
|
||||
|
||||
/**
|
||||
* The factory service for {@link VmMgmt}s.
|
||||
*/
|
||||
public class VmMgmtFactory implements ConletComponentFactory {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.jgrapes.core.ComponentFactory#componentType()
|
||||
*/
|
||||
@Override
|
||||
public Class<? extends ComponentType> componentType() {
|
||||
return VmMgmt.class;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.jgrapes.core.ComponentFactory#create(org.jgrapes.core.Channel,
|
||||
* java.util.Map)
|
||||
*/
|
||||
@Override
|
||||
public Optional<ComponentType> create(Channel componentChannel,
|
||||
Map<?, ?> properties) {
|
||||
return Optional.of(new VmMgmt(componentChannel));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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, 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: number | null)
|
||||
=> string | null;
|
||||
private readonly inputKey = ref("");
|
||||
private startValue: string | null = null;
|
||||
private inputElement: HTMLInputElement | null = null;
|
||||
private errorMessage = ref("");
|
||||
|
||||
/**
|
||||
* Creates a new controller.
|
||||
*/
|
||||
constructor(submitCallback: (selected: string, value: number | null)
|
||||
=> 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: string) {
|
||||
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) => number | null) : boolean {
|
||||
if (typeof converter === 'undefined') {
|
||||
this.inputKey.value = "";
|
||||
return false;
|
||||
}
|
||||
const newValue = converter(this.inputElement!.value);
|
||||
if (newValue === this.startValue) {
|
||||
this.inputKey.value = "";
|
||||
return false;
|
||||
}
|
||||
const 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,140 @@
|
|||
/*
|
||||
* 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";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const 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) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
let ptr: any = this;
|
||||
const segs = path.split(".");
|
||||
const lastSeg = segs.pop()!;
|
||||
for (const seg of segs) {
|
||||
const 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/>.
|
||||
*/
|
||||
|
||||
const unitMap = new Map<string, number>();
|
||||
const unitMappings = new Array<{ key: string; value: number }>();
|
||||
const 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 (const unit of ["B", "kB", "MB", "GB", "TB", "PB", "EB"]) {
|
||||
unitMap.set(unit, factor);
|
||||
factor = factor * scale;
|
||||
}
|
||||
|
||||
// Binary units
|
||||
factor = 1024;
|
||||
scale = 1024;
|
||||
for (const 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 (const 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 {
|
||||
const 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 (const 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
|
||||
const limit = time.getTime() - this.period;
|
||||
while (this.timestamps.length > 2
|
||||
&& this.timestamps[0].getTime() < limit
|
||||
&& this.timestamps[1].getTime() < limit) {
|
||||
this.timestamps.shift();
|
||||
for (const 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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* 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 {
|
||||
reactive, ref, Ref, createApp, computed, onMounted, watch
|
||||
} from "vue";
|
||||
import JGConsole from "jgconsole";
|
||||
import JgwcPlugin, { JGWC } from "jgwc";
|
||||
import l10nBundles from "l10nBundles";
|
||||
import TimeSeries from "./TimeSeries";
|
||||
import { formatMemory, parseMemory } from "./MemorySize";
|
||||
import CpuRamChart from "./CpuRamChart";
|
||||
import ConditionlInputController from "./ConditionalInputController";
|
||||
|
||||
import "./VmMgmt-style.scss";
|
||||
|
||||
// For global access
|
||||
declare global {
|
||||
interface Window {
|
||||
orgJDrupesVmOperatorVmMgmt: {
|
||||
initPreview?: (previewDom: HTMLElement, isUpdate: boolean) => void,
|
||||
initView?: (viewDom: HTMLElement, isUpdate: boolean) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.orgJDrupesVmOperatorVmMgmt = {};
|
||||
|
||||
const vmInfos = reactive(new Map());
|
||||
const vmSummary = reactive({
|
||||
totalVms: 0,
|
||||
runningVms: 0,
|
||||
usedCpus: 0,
|
||||
usedRam: ""
|
||||
});
|
||||
|
||||
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.
|
||||
const chartData = new TimeSeries(2);
|
||||
const chartDateUpdate = ref<Date>(null);
|
||||
|
||||
window.orgJDrupesVmOperatorVmMgmt.initPreview = (previewDom: HTMLElement,
|
||||
_isUpdate: boolean) => {
|
||||
const app = createApp({
|
||||
setup(_props: object) {
|
||||
let chart: CpuRamChart | null = null;
|
||||
onMounted(() => {
|
||||
const 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, (_) => {
|
||||
const 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.orgJDrupesVmOperatorVmMgmt.initView = (viewDom: HTMLElement,
|
||||
_isUpdate: boolean) => {
|
||||
const app = createApp({
|
||||
setup(_props: object) {
|
||||
const conletId: string
|
||||
= (<HTMLElement>viewDom.parentNode!).dataset["conletId"]!;
|
||||
|
||||
const controller = reactive(new JGConsole.TableController([
|
||||
["name", "vmname"],
|
||||
["running", "running"],
|
||||
["runningConditionSince", "since"],
|
||||
["currentCpus", "currentCpus"],
|
||||
["currentRam", "currentRam"],
|
||||
["nodeName", "nodeName"],
|
||||
["usedFrom", "usedFrom"],
|
||||
["usedBy", "usedBy"]
|
||||
], {
|
||||
sortKey: "name",
|
||||
sortOrder: "up"
|
||||
}));
|
||||
|
||||
const filteredData = computed(() => {
|
||||
const infos = Array.from(vmInfos.values());
|
||||
return controller.filter(infos);
|
||||
});
|
||||
|
||||
const vmAction = (vmName: string, action: string) => {
|
||||
JGConsole.notifyConletModel(conletId, action, vmName);
|
||||
};
|
||||
|
||||
const idScope = JGWC.createIdScope();
|
||||
const detailsByName = reactive(new Set());
|
||||
|
||||
const submitCallback = (selected: string, value: number | null) => {
|
||||
if (value === null) {
|
||||
return localize("Illegal format");
|
||||
}
|
||||
const vmName = selected.substring(0, selected.lastIndexOf(":"));
|
||||
const property = selected.substring(selected.lastIndexOf(":") + 1);
|
||||
const vmDef = vmInfos.get(vmName);
|
||||
const 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);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const maximumCpus = (vmDef: any) => {
|
||||
if (vmDef.spec.vm["maximumCpus"]) {
|
||||
return vmDef.spec.vm.maximumCpus;
|
||||
}
|
||||
const topo = vmDef.spec.vm.cpuTopology;
|
||||
return Math.max(1, topo.coresPerDie)
|
||||
* Math.max(1, topo.diesPerSocket)
|
||||
* Math.max(1, topo.sockets)
|
||||
* Math.max(1, topo.threadsPerCore);
|
||||
}
|
||||
|
||||
return {
|
||||
controller, vmInfos, filteredData, detailsByName, localize,
|
||||
shortDateTime, formatMemory, vmAction, cic, parseMemory,
|
||||
maximumCpus,
|
||||
scopedId: (id: string) => { return idScope.scopedId(id); }
|
||||
};
|
||||
}
|
||||
});
|
||||
app.use(JgwcPlugin);
|
||||
app.config.globalProperties.window = window;
|
||||
app.mount(viewDom);
|
||||
};
|
||||
|
||||
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
"updateVm", function(_conletId: string, vmDefinition: any) {
|
||||
// Add some short-cuts for table controller
|
||||
vmDefinition.name = vmDefinition.metadata.name;
|
||||
vmDefinition.currentCpus = vmDefinition.status.cpus;
|
||||
vmDefinition.currentRam = Number(vmDefinition.status.ram);
|
||||
vmDefinition.usedFrom = vmDefinition.status.consoleClient || "";
|
||||
vmDefinition.usedBy = vmDefinition.status.consoleUser || "";
|
||||
for (const 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);
|
||||
});
|
||||
|
||||
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt",
|
||||
"removeVm", function(_conletId: string, vmName: string) {
|
||||
vmInfos.delete(vmName);
|
||||
});
|
||||
|
||||
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
"summarySeries", function(_conletId: string, series: any[]) {
|
||||
chartData.clear();
|
||||
for (const entry of series) {
|
||||
chartData.push(new Date(entry.time * 1000),
|
||||
entry.values[0], entry.values[1]);
|
||||
}
|
||||
chartDateUpdate.value = new Date();
|
||||
});
|
||||
|
||||
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
"updateSummary", function(_conletId: string, summary: any) {
|
||||
chartData.push(new Date(), summary.usedCpus, Number(summary.usedRam));
|
||||
chartDateUpdate.value = new Date();
|
||||
Object.assign(vmSummary, summary);
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* VM-Operator
|
||||
* Copyright (C) 2023 Michael N. Lipp
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Conlet specific styles.
|
||||
*/
|
||||
|
||||
.jdrupes-vmoperator-vmmgmt-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-vmmgmt-view-search {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
form {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.jdrupes-vmoperator-vmmgmt-view-table {
|
||||
td {
|
||||
vertical-align: top;
|
||||
|
||||
&[tabindex] {
|
||||
outline: 1px solid var(--primary);
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&:not([colspan]):first-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.column-running {
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
&.fa-check {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.fa-close {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td.details {
|
||||
padding-left: 1em;
|
||||
|
||||
table {
|
||||
td:nth-child(2) {
|
||||
min-width: 7em;
|
||||
|
||||
input {
|
||||
max-width: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
input~span {
|
||||
margin-left: 0.5em;
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jdrupes-vmoperator-vmmgmt-view-action-list {
|
||||
white-space: nowrap;
|
||||
|
||||
[role=button] {
|
||||
padding: 0.25rem;
|
||||
|
||||
&:not([aria-disabled]):hover, &[aria-disabled='false']:hover {
|
||||
box-shadow: var(--darkening);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export default new Map<string, Map<string, string>>();
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* VM-Operator
|
||||
* Copyright (C) 2023 Michael N. Lipp
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jdrupes.vmoperator.vmmgmt;
|
||||
24
org.jdrupes.vmoperator.vmmgmt/tsconfig.json
Normal file
24
org.jdrupes.vmoperator.vmmgmt/tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "es2015",
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"declaration": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["DOM", "ES2020"],
|
||||
"paths": {
|
||||
"aash-plugin": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/aash-vue-components/lib/AashPlugin"],
|
||||
"jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"],
|
||||
"jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"],
|
||||
"l10nBundles": ["./src/org/jdrupes/vmoperator/vmmgmt/browser/l10nBundles-stub"],
|
||||
"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"],
|
||||
"exclude": ["node_modules", "l10nBundles-stub.ts"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue