mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-05-15 23:41:43 +00:00
Compare commits
No commits in common. "main" and "v1.9.0-dev-20260510" have entirely different histories.
main
...
v1.9.0-dev
@ -1,12 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import { NODE } from '@shell/config/types';
|
import { NODE } from '@shell/config/types';
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
@ -59,46 +62,28 @@ export default {
|
|||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
anyCpuPinning() {
|
vmi() {
|
||||||
return this.resources.some((r) => r.isCpuPinning);
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
},
|
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
|
||||||
|
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
|
||||||
|
|
||||||
vmsByNode() {
|
return resource;
|
||||||
const groups = {};
|
|
||||||
|
|
||||||
for (const r of this.resources) {
|
|
||||||
const node = r.nodeName || '';
|
|
||||||
const name = r.nameDisplay || r.name || r.id;
|
|
||||||
|
|
||||||
if (!groups[node]) {
|
|
||||||
groups[node] = [];
|
|
||||||
}
|
|
||||||
groups[node].push(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cpuPinningAlertMessage() {
|
cpuPinningAlertMessage() {
|
||||||
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
||||||
},
|
},
|
||||||
|
|
||||||
allVmsOnTargetNode() {
|
|
||||||
if (!this.nodeName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.resources.every((r) => r.nodeName === this.nodeName);
|
|
||||||
},
|
|
||||||
|
|
||||||
nodeNameList() {
|
nodeNameList() {
|
||||||
const nodes = this.$store.getters['harvester/all'](NODE);
|
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||||
|
|
||||||
return nodes.filter((n) => {
|
return nodes.filter((n) => {
|
||||||
|
const isNotSelfNode = !!this.availableNodes.includes(n.id);
|
||||||
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
||||||
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
const isCpuPinning = this.actionResource?.isCpuPinning;
|
||||||
|
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||||
|
|
||||||
return isNotWitnessNode && matchingCpuManagerConfig;
|
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
|
||||||
}).map((n) => {
|
}).map((n) => {
|
||||||
let label = n?.metadata?.name;
|
let label = n?.metadata?.name;
|
||||||
const value = n?.metadata?.name;
|
const value = n?.metadata?.name;
|
||||||
@ -141,29 +126,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Filter out VMs already running on the selected node
|
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
||||||
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
|
|
||||||
|
|
||||||
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
|
||||||
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
|
|
||||||
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
|
||||||
|
|
||||||
const failedMigrations = results
|
|
||||||
.map((result, index) => ({ resource: toMigrate[index], result }))
|
|
||||||
.filter(({ result }) => result.status === 'rejected');
|
|
||||||
|
|
||||||
if (failedMigrations.length) {
|
|
||||||
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
|
|
||||||
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
|
|
||||||
const error = result.reason?.data || result.reason;
|
|
||||||
const messages = exceptionToErrorsArray(error);
|
|
||||||
|
|
||||||
return messages.map((message) => `${ vmName }: ${ message }`);
|
|
||||||
});
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close();
|
||||||
@ -183,35 +146,17 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<Card :show-highlight-border="false">
|
<Card :show-highlight-border="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
|
{{ t('harvester.modal.migration.title') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="anyCpuPinning"
|
v-if="actionResource?.isCpuPinning"
|
||||||
color="warning"
|
color="warning"
|
||||||
:label="cpuPinningAlertMessage"
|
:label="cpuPinningAlertMessage"
|
||||||
/>
|
/>
|
||||||
<p>
|
|
||||||
{{ t('harvester.modal.migration.selectedVMs') }}
|
|
||||||
</p>
|
|
||||||
<ul class="vm-list">
|
|
||||||
<li
|
|
||||||
v-for="group in vmsByNode"
|
|
||||||
:key="group.node"
|
|
||||||
>
|
|
||||||
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
|
|
||||||
<span
|
|
||||||
v-if="nodeName && group.node === nodeName"
|
|
||||||
class="already-on-target"
|
|
||||||
>
|
|
||||||
({{ t('harvester.modal.migration.alreadyOnTarget') }})
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="nodeName"
|
v-model:value="nodeName"
|
||||||
class="mt-15"
|
|
||||||
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||||
:options="nodeNameList"
|
:options="nodeNameList"
|
||||||
@ -238,7 +183,7 @@ export default {
|
|||||||
|
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
mode="apply"
|
mode="apply"
|
||||||
:disabled="!nodeName || allVmsOnTargetNode"
|
:disabled="!nodeName"
|
||||||
@click="apply"
|
@click="apply"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -256,16 +201,4 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.already-on-target {
|
|
||||||
color: var(--warning);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-list {
|
|
||||||
list-style: disc;
|
|
||||||
padding-left: 1.5em;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -144,10 +144,6 @@ harvester:
|
|||||||
migration:
|
migration:
|
||||||
failedMessage: Latest migration failed!
|
failedMessage: Latest migration failed!
|
||||||
title: Migration
|
title: Migration
|
||||||
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
|
|
||||||
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
|
|
||||||
unknownNode: (unknown node)
|
|
||||||
alreadyOnTarget: Already on Target
|
|
||||||
fields:
|
fields:
|
||||||
nodeName:
|
nodeName:
|
||||||
label: Target Node
|
label: Target Node
|
||||||
@ -252,7 +248,6 @@ harvester:
|
|||||||
suspendSchedule: Suspend
|
suspendSchedule: Suspend
|
||||||
restoreExistingVM: Replace Existing
|
restoreExistingVM: Replace Existing
|
||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
vmMigrate: Virtual Machine Migration
|
|
||||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
storageMigration: Storage Migration
|
storageMigration: Storage Migration
|
||||||
@ -303,7 +298,6 @@ harvester:
|
|||||||
phase: Phase
|
phase: Phase
|
||||||
attachedVM: Attached Virtual Machine
|
attachedVM: Attached Virtual Machine
|
||||||
cpuManager: CPU Manager
|
cpuManager: CPU Manager
|
||||||
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
|
|
||||||
fingerprint: Fingerprint
|
fingerprint: Fingerprint
|
||||||
value: Value
|
value: Value
|
||||||
actions: Actions
|
actions: Actions
|
||||||
@ -2180,6 +2174,12 @@ typeLabel:
|
|||||||
other { PCI Devices }
|
other { PCI Devices }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
persistentvolumeclaim: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Volume }
|
||||||
|
other { Volumes }
|
||||||
|
}
|
||||||
|
|
||||||
network.harvesterhci.io.clusternetwork: |-
|
network.harvesterhci.io.clusternetwork: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Cluster Network }
|
one { Cluster Network }
|
||||||
|
|||||||
@ -112,7 +112,6 @@ export default {
|
|||||||
value: 'connectivity',
|
value: 'connectivity',
|
||||||
labelKey: 'tableHeaders.routeConnectivity',
|
labelKey: 'tableHeaders.routeConnectivity',
|
||||||
formatter: 'NetworkRouteConnectivity',
|
formatter: 'NetworkRouteConnectivity',
|
||||||
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
|
|
||||||
formatterOpts: { arbitrary: true },
|
formatterOpts: { arbitrary: true },
|
||||||
width: 130,
|
width: 130,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -164,7 +164,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'restartVM',
|
action: 'restartVM',
|
||||||
altAction: 'altRestartVM',
|
|
||||||
enabled: !!this.actions?.restart,
|
enabled: !!this.actions?.restart,
|
||||||
icon: 'icon icon-refresh',
|
icon: 'icon icon-refresh',
|
||||||
label: this.t('harvester.action.restart'),
|
label: this.t('harvester.action.restart'),
|
||||||
@ -172,11 +171,10 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
bulkAction: 'restartVM'
|
bulkAction: 'restartVM'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'softrebootVM',
|
action: 'softrebootVM',
|
||||||
altAction: 'doSoftReboot',
|
enabled: !!this.actions?.softreboot,
|
||||||
enabled: !!this.actions?.softreboot,
|
icon: 'icon icon-pipeline',
|
||||||
icon: 'icon icon-pipeline',
|
label: this.t('harvester.action.softreboot')
|
||||||
label: this.t('harvester.action.softreboot')
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'startVM',
|
action: 'startVM',
|
||||||
@ -228,12 +226,10 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
label: this.t('harvester.action.ejectCDROM')
|
label: this.t('harvester.action.ejectCDROM')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'migrateVM',
|
action: 'migrateVM',
|
||||||
enabled: !!this.actions?.migrate,
|
enabled: !!this.actions?.migrate,
|
||||||
icon: 'icon icon-copy',
|
icon: 'icon icon-copy',
|
||||||
label: this.t('harvester.action.vmMigrate'),
|
label: this.t('harvester.action.migrate')
|
||||||
bulkable: true,
|
|
||||||
bulkAction: 'migrateVM'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'abortMigrationVM',
|
action: 'abortMigrationVM',
|
||||||
@ -373,10 +369,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
altRestartVM() {
|
|
||||||
this.doActionGrowl('restart', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
restartVM(resources = this) {
|
restartVM(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
|
|||||||
31
yarn.lock
31
yarn.lock
@ -3308,7 +3308,17 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
|
||||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||||
|
|
||||||
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33":
|
"@types/express-serve-static-core@*":
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz#1a77faffee9572d39124933259be2523837d7eaa"
|
||||||
|
integrity sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
"@types/qs" "*"
|
||||||
|
"@types/range-parser" "*"
|
||||||
|
"@types/send" "*"
|
||||||
|
|
||||||
|
"@types/express-serve-static-core@^4.17.33":
|
||||||
version "4.19.8"
|
version "4.19.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz#99b960322a4d576b239a640ab52ef191989b036f"
|
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz#99b960322a4d576b239a640ab52ef191989b036f"
|
||||||
integrity sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==
|
integrity sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==
|
||||||
@ -4063,11 +4073,16 @@
|
|||||||
"@vue/compiler-ssr" "3.5.29"
|
"@vue/compiler-ssr" "3.5.29"
|
||||||
"@vue/shared" "3.5.29"
|
"@vue/shared" "3.5.29"
|
||||||
|
|
||||||
"@vue/shared@3.5.29", "@vue/shared@^3.5.18":
|
"@vue/shared@3.5.29":
|
||||||
version "3.5.29"
|
version "3.5.29"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.29.tgz#0fe0d7637b05599d56ca58d83a77c637a1774110"
|
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.29.tgz#0fe0d7637b05599d56ca58d83a77c637a1774110"
|
||||||
integrity sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==
|
integrity sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==
|
||||||
|
|
||||||
|
"@vue/shared@^3.5.18":
|
||||||
|
version "3.5.32"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.32.tgz#dd8ba0d709bf3f758c324a81c8897bad5e1540cf"
|
||||||
|
integrity sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==
|
||||||
|
|
||||||
"@vue/test-utils@2.4.6":
|
"@vue/test-utils@2.4.6":
|
||||||
version "2.4.6"
|
version "2.4.6"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.4.6.tgz#7d534e70c4319d2a587d6a3b45a39e9695ade03c"
|
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.4.6.tgz#7d534e70c4319d2a587d6a3b45a39e9695ade03c"
|
||||||
@ -7148,9 +7163,9 @@ eslint-plugin-node@^11.1.0:
|
|||||||
semver "^6.1.0"
|
semver "^6.1.0"
|
||||||
|
|
||||||
eslint-plugin-promise@^7.1.0:
|
eslint-plugin-promise@^7.1.0:
|
||||||
version "7.3.0"
|
version "7.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz#7c61e117f5db8d7a300bd5143c15d1d828e4c124"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz#a0652195700aea40b926dc3c74b38e373377bfb0"
|
||||||
integrity sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==
|
integrity sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/eslint-utils" "^4.4.0"
|
"@eslint-community/eslint-utils" "^4.4.0"
|
||||||
|
|
||||||
@ -13838,9 +13853,9 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
|
|||||||
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
|
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
|
||||||
|
|
||||||
yaml@^2.5.1:
|
yaml@^2.5.1:
|
||||||
version "2.8.4"
|
version "2.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.4.tgz#4b5f411dd25f9544914d8673d4da7f29248e5e2e"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d"
|
||||||
integrity sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==
|
integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==
|
||||||
|
|
||||||
yargs-parser@^18.1.2:
|
yargs-parser@^18.1.2:
|
||||||
version "18.1.3"
|
version "18.1.3"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user