mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 13:16:05 +08:00
Add pkg/harvester components + shell portings - 1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
<script>
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { LONGHORN, METRIC } from '@shell/config/types';
|
||||
import { Banner } from '@components/Banner';
|
||||
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
|
||||
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
|
||||
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
|
||||
|
||||
const COMPLETE = 'complete';
|
||||
const PROMOTE_RESTART = 'promoteRestart';
|
||||
const PROMOTE_SUCCEED = 'promoteSucceed';
|
||||
|
||||
export default {
|
||||
name: 'BasicNode',
|
||||
|
||||
components: {
|
||||
Banner,
|
||||
LabelValue,
|
||||
HarvesterCPUUsed,
|
||||
HarvesterMemoryUsed,
|
||||
HarvesterStorageUsed,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
metrics: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'view'
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
customName() {
|
||||
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CUSTOM_NAME];
|
||||
},
|
||||
|
||||
consoleUrl() {
|
||||
const consoleUrl = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL];
|
||||
let value = consoleUrl;
|
||||
|
||||
if (!consoleUrl) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!consoleUrl.startsWith('http://') && !consoleUrl.startsWith('https://')) {
|
||||
value = `http://${ consoleUrl }`;
|
||||
}
|
||||
|
||||
return {
|
||||
display: consoleUrl,
|
||||
value
|
||||
};
|
||||
},
|
||||
|
||||
cpuTotal() {
|
||||
let out = 0;
|
||||
|
||||
if (this.metrics) {
|
||||
out = this.metrics.cpuCapacity;
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
cpuUsage() {
|
||||
let out = 0;
|
||||
|
||||
if (this.metrics) {
|
||||
out = this.metrics.cpuUsage;
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
memoryTotal() {
|
||||
let out = 0;
|
||||
|
||||
if (this.metrics) {
|
||||
out = this.metrics.memoryCapacity;
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
memoryUsage() {
|
||||
let out = 0;
|
||||
|
||||
if (this.metrics) {
|
||||
out = this.metrics.memoryUsage;
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
cpuUnits() {
|
||||
return 'C';
|
||||
},
|
||||
|
||||
memoryUnits() {
|
||||
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||
|
||||
return `${ UNITS[exponent] }iB`;
|
||||
},
|
||||
|
||||
nodeType() {
|
||||
if (this.value.isEtcd) {
|
||||
return this.t('harvester.host.detail.etcd');
|
||||
}
|
||||
|
||||
if (this.value.isMaster) {
|
||||
return this.t('harvester.host.detail.management');
|
||||
}
|
||||
|
||||
return this.t('harvester.host.detail.compute');
|
||||
},
|
||||
|
||||
lastUpdateTime() {
|
||||
return this.value.status?.conditions?.[0]?.lastHeartbeatTime;
|
||||
},
|
||||
|
||||
nodeRoleState() {
|
||||
if (!this.value.isEtcd) {
|
||||
const promoteStatus = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.PROMOTE_STATUS];
|
||||
|
||||
if (promoteStatus === COMPLETE) {
|
||||
const isExistRoleStatus = this.value.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_MASTER] !== undefined || this.value.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_CONTROL_PLANE] !== undefined;
|
||||
|
||||
return this.t(`harvester.host.promote.${ isExistRoleStatus ? PROMOTE_SUCCEED : PROMOTE_RESTART }`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
hasMetricNodeSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](METRIC.NODE);
|
||||
},
|
||||
|
||||
hasLonghornSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](LONGHORN.NODES);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
memoryFormatter(value) {
|
||||
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||
|
||||
const formatOptions = {
|
||||
addSuffix: false,
|
||||
increment: 1024,
|
||||
minExponent: exponent
|
||||
};
|
||||
|
||||
return formatSi(value, formatOptions);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="host-detail">
|
||||
<Banner
|
||||
v-if="value.isKVMDisable"
|
||||
color="error"
|
||||
label-key="harvester.host.detail.kvm.disableMessage"
|
||||
/>
|
||||
<h3>{{ t('harvester.host.tabs.overview') }}</h3>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.host.detail.customName')" :value="customName" />
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.host.detail.hostIP')" :value="value.internalIp" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.host.detail.os')" :value="value.status.nodeInfo.osImage" />
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<div class="role">
|
||||
<LabelValue :name="t('harvester.host.detail.role')">
|
||||
<template #value>
|
||||
{{ nodeType }}
|
||||
<span
|
||||
v-if="nodeRoleState"
|
||||
class="text-warning ml-20"
|
||||
>
|
||||
{{ nodeRoleState }}
|
||||
</span>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.host.detail.create')" :value="value.metadata.creationTimestamp" />
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.host.detail.update')" :value="lastUpdateTime" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.host.detail.consoleUrl')" :value="consoleUrl.value">
|
||||
<a slot="value" :href="consoleUrl.value" target="_blank">{{ consoleUrl.display }}</a>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasMetricNodeSchema">
|
||||
<hr class="divider" />
|
||||
<h3>{{ t('harvester.host.tabs.monitor') }}</h3>
|
||||
<div class="row mb-20">
|
||||
<div
|
||||
class="col"
|
||||
:class="{
|
||||
'span-4': hasLonghornSchema,
|
||||
'span-6': !hasLonghornSchema,
|
||||
}"
|
||||
>
|
||||
<HarvesterCPUUsed
|
||||
:row="value"
|
||||
:resource-name="t('node.detail.glance.consumptionGauge.cpu')"
|
||||
:show-used="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="col"
|
||||
:class="{
|
||||
'span-4': hasLonghornSchema,
|
||||
'span-6': !hasLonghornSchema,
|
||||
}"
|
||||
>
|
||||
<HarvesterMemoryUsed
|
||||
:row="value"
|
||||
:resource-name="t('node.detail.glance.consumptionGauge.memory')"
|
||||
:show-used="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasLonghornSchema"
|
||||
class="col span-4"
|
||||
>
|
||||
<HarvesterStorageUsed
|
||||
:row="value"
|
||||
:resource-name="t('harvester.host.detail.storage')"
|
||||
:show-reserved="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider" />
|
||||
<h3>{{ t('harvester.host.detail.more') }}</h3>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.detail.uuid')" :value="value.status.nodeInfo.systemUUID" />
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.detail.kernel')" :value="value.status.nodeInfo.kernelVersion" />
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.detail.containerRuntime')" :value="value.status.nodeInfo.containerRuntimeVersion" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="value.manufacturer || value.serialNumber || value.model"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div
|
||||
v-if="value.manufacturer"
|
||||
class="col span-4"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.host.detail.manufacturer')"
|
||||
:value="value.manufacturer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="value.serialNumber"
|
||||
class="col span-4"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.host.detail.serialNumber')"
|
||||
:value="value.serialNumber"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="value.model"
|
||||
class="col span-4"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.host.detail.model')"
|
||||
:value="value.model"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.role {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,214 @@
|
||||
<script>
|
||||
import Tag from '@shell/components/Tag';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { BadgeState } from '@components/BadgeState';
|
||||
import { Banner } from '@components/Banner';
|
||||
import HarvesterDisk from '../../mixins/harvester-disk';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LabelValue,
|
||||
BadgeState,
|
||||
Banner,
|
||||
Tag
|
||||
},
|
||||
|
||||
mixins: [
|
||||
HarvesterDisk,
|
||||
],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'edit',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
allowSchedulingOptions() {
|
||||
return [{
|
||||
label: this.t('generic.enabled'),
|
||||
value: true,
|
||||
}, {
|
||||
label: this.t('generic.disabled'),
|
||||
value: false,
|
||||
}];
|
||||
},
|
||||
|
||||
evictionRequestedOptions() {
|
||||
return [{
|
||||
label: this.t('generic.yes'),
|
||||
value: true,
|
||||
}, {
|
||||
label: this.t('generic.no'),
|
||||
value: false,
|
||||
}];
|
||||
},
|
||||
|
||||
provisionPhase() {
|
||||
return this.value?.blockDevice?.provisionPhase || {};
|
||||
},
|
||||
|
||||
mountedMessage() {
|
||||
const state = this.value?.blockDevice?.metadata?.state || {};
|
||||
|
||||
if (state?.error) {
|
||||
return state?.message;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.$emit('input', this.value);
|
||||
},
|
||||
|
||||
canEditPath(value) {
|
||||
if (this.mountedMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value.isNew && !!value.originPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="disk" @input="update">
|
||||
<Banner
|
||||
v-if="mountedMessage"
|
||||
color="error"
|
||||
:label="mountedMessage"
|
||||
/>
|
||||
<div v-if="!value.isNew">
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<LabelValue
|
||||
v-if="value.tags.length"
|
||||
:name="t('harvester.host.disk.tags.label')"
|
||||
>
|
||||
<template #value>
|
||||
<div class="mt-5">
|
||||
<Tag v-for="(prop, key) in value.tags" :key="key + prop" class="mr-5">
|
||||
{{ prop }}
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-12">
|
||||
<div class="pull-right">
|
||||
{{ t('harvester.host.disk.conditions') }}:
|
||||
<BadgeState
|
||||
v-clean-tooltip="readyCondition.message"
|
||||
:color="readyCondition.status === 'True' ? 'bg-success' : 'bg-error' "
|
||||
:icon="readyCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
|
||||
label="Ready"
|
||||
class="mr-10 ml-10 state"
|
||||
/>
|
||||
<BadgeState
|
||||
v-clean-tooltip="schedulableCondition.message"
|
||||
:color="schedulableCondition.status === 'True' ? 'bg-success' : 'bg-error' "
|
||||
:icon="schedulableCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
|
||||
label="Schedulable"
|
||||
class="mr-10 state"
|
||||
/>
|
||||
<BadgeState
|
||||
v-if="provisionPhase.label"
|
||||
:color="provisionPhase.color"
|
||||
:icon="provisionPhase.icon"
|
||||
:label="provisionPhase.label"
|
||||
class="mr-10 state"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!value.isNew" class="row mt-30">
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.disk.storageAvailable.label')"
|
||||
:value="value.storageAvailable"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.disk.storageScheduled.label')"
|
||||
:value="value.storageScheduled"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.disk.storageMaximum.label')"
|
||||
:value="value.storageMaximum"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mt-10" />
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('generic.name')"
|
||||
:value="value.displayName"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.disk.path.label')"
|
||||
:value="value.path"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.close {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding:0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.disk {
|
||||
position: relative;
|
||||
|
||||
.secret-name {
|
||||
height: $input-height;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.badge-state {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<script>
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { HCI } from '../../types';
|
||||
import { ksmtunedMode, ksmtunedRunOption } from '../../edit/harvesterhci.io.host/HarvesterKsmtuned.vue';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterKsmtuned',
|
||||
components: { LabelValue },
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.KSTUNED });
|
||||
|
||||
this.ksmtuned = hash.find((node) => {
|
||||
return node.id === this.node.id;
|
||||
});
|
||||
},
|
||||
|
||||
data() {
|
||||
return { ksmtuned: {} };
|
||||
},
|
||||
|
||||
computed: {
|
||||
modeText() {
|
||||
const mode = this.ksmtuned.spec.mode;
|
||||
|
||||
return ksmtunedMode.find(M => M.value === mode).label;
|
||||
},
|
||||
|
||||
thresCoef() {
|
||||
return `${ this.ksmtuned.spec.thresCoef } %`;
|
||||
},
|
||||
|
||||
runText() {
|
||||
const run = this.ksmtuned.spec.run;
|
||||
|
||||
return ksmtunedRunOption.find(M => M.value === run).label;
|
||||
},
|
||||
|
||||
showRunInformation() {
|
||||
return this.ksmtuned.spec.run === 'run';
|
||||
},
|
||||
|
||||
mergeNodesText() {
|
||||
return this.ksmtuned.spec?.mergeAcrossNodes ? this.t('harvester.host.ksmtuned.enable') : this.t('harvester.host.ksmtuned.disable');
|
||||
},
|
||||
|
||||
ksmdPhase() {
|
||||
return this.ksmtuned?.status?.ksmdPhase;
|
||||
},
|
||||
|
||||
ksmdPhaseTextColor() {
|
||||
return this.ksmdPhase === 'Running' ? 'text-success' : 'text-warning';
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="ksmtuned.status">
|
||||
<h2>
|
||||
{{ t('harvester.host.ksmtuned.configure') }}
|
||||
</h2>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.run')" :value="runText" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showRunInformation" class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.thresCoef')" :value="thresCoef" />
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.mode')" :value="modeText" />
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.enableMergeNodes')" :value="mergeNodesText" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showRunInformation">
|
||||
<hr class="divider" />
|
||||
|
||||
<h3>{{ t('harvester.host.ksmtuned.parameters.title') }}</h3>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.parameters.boost')" :value="ksmtuned.spec.ksmtunedParameters.boost" />
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.parameters.decay')" :value="ksmtuned.spec.ksmtunedParameters.decay" />
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.parameters.sleepMsec')" :value="ksmtuned.spec.ksmtunedParameters.sleepMsec" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.parameters.minPages')" :value="ksmtuned.spec.ksmtunedParameters.minPages" />
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.parameters.maxPages')" :value="ksmtuned.spec.ksmtunedParameters.maxPages" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<hr class="divider" />
|
||||
<h3><t k="harvester.host.ksmtuned.statistics.title" :raw="true" /></h3>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.ksmStatus')">
|
||||
<template #value>
|
||||
<span :class="ksmdPhaseTextColor">{{ ksmdPhase }}</span>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.statistics.sharing')" :value="ksmtuned.status.sharing" />
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.statistics.shared')" :value="ksmtuned.status.shared" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.statistics.unshared')" :value="ksmtuned.status.unshared" />
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.statistics.volatile')" :value="ksmtuned.status.volatile" />
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.statistics.fullScans')" :value="ksmtuned.status.fullScans" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.statistics.stableNodeDups')" :value="ksmtuned.status.stableNodeDups" />
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabelValue :name="t('harvester.host.ksmtuned.statistics.stableNodeChains')" :value="ksmtuned.status.stableNodeChains" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script>
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { Banner } from '@components/Banner';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterSeeder',
|
||||
|
||||
components: {
|
||||
RadioGroup,
|
||||
LabelValue,
|
||||
Banner,
|
||||
},
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
inventory: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const enableInventory = !!this.inventory?.id;
|
||||
|
||||
return {
|
||||
enableInventory,
|
||||
value: this.inventory,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
selectedSecret() {
|
||||
const namespace = this.value.spec?.baseboardSpec?.connection?.authSecretRef?.namespace;
|
||||
const name = this.value?.spec?.baseboardSpec?.connection?.authSecretRef?.name;
|
||||
|
||||
if (namespace && name) {
|
||||
return `${ namespace }/${ name }`;
|
||||
} else {
|
||||
return 'N/A';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="inventory.warningMessages.length > 0">
|
||||
<Banner
|
||||
v-for="msg in inventory.warningMessages"
|
||||
:key="msg.text"
|
||||
color="error"
|
||||
:label="msg.text"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="enableInventory">
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.seeder.inventory.host.label')"
|
||||
:value="value.spec.baseboardSpec.connection.host"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.seeder.inventory.port.label')"
|
||||
:value="value.spec.baseboardSpec.connection.port"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.seeder.inventory.insecureTLS.label')"
|
||||
:value="value.spec.baseboardSpec.connection.insecureTLS ? t('generic.yes') : t('generic.no')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.seeder.inventory.secret.label')"
|
||||
:value="selectedSecret"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.seeder.inventory.event.label')"
|
||||
:value="value.spec.events.enabled ? t('generic.enabled') : t('generic.disabled')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="value.spec.events.enabled"
|
||||
class="col span-6"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.seeder.inventory.pollingInterval.label')"
|
||||
:value="value.spec.events.pollingInterval"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model="enableInventory"
|
||||
:options="[
|
||||
{ label: t('generic.enabled'), value: true },
|
||||
{ label: t('generic.disabled'), value: false }
|
||||
]"
|
||||
:mode="mode"
|
||||
name="enableInventory"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script>
|
||||
import { STATE, AGE, NAME } from '@shell/config/table-headers';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import HarvesterVmState from '../../formatters/HarvesterVmState';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../../types';
|
||||
import { HOSTNAME } from '@shell/config/labels-annotations';
|
||||
|
||||
export default {
|
||||
name: 'InstanceNode',
|
||||
|
||||
components: {
|
||||
SortableTable,
|
||||
Loading,
|
||||
HarvesterVmState,
|
||||
},
|
||||
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const hash = await allHash({
|
||||
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
||||
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
|
||||
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
|
||||
});
|
||||
const instanceMap = {};
|
||||
|
||||
(hash.vmis || []).forEach((vmi) => {
|
||||
const vmiUID = vmi?.metadata?.ownerReferences?.[0]?.uid;
|
||||
|
||||
if (vmiUID) {
|
||||
instanceMap[vmiUID] = vmi;
|
||||
}
|
||||
});
|
||||
|
||||
this.allClusterNetwork = hash.allClusterNetwork;
|
||||
this.rows = hash.vms.filter((row) => {
|
||||
return instanceMap[row.metadata?.uid]?.status?.nodeName === this.node?.metadata?.labels?.[HOSTNAME];
|
||||
});
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
rows: [],
|
||||
allClusterNetwork: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
headers() {
|
||||
return [
|
||||
STATE,
|
||||
NAME,
|
||||
{
|
||||
name: 'vmCPU',
|
||||
labelKey: 'tableHeaders.cpu',
|
||||
search: false,
|
||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||
value: 'spec.template.spec.domain.cpu.cores',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
name: 'vmRAM',
|
||||
labelKey: 'glance.memory',
|
||||
search: false,
|
||||
sort: ['memorySort'],
|
||||
value: 'spec.template.spec.domain.resources.limits.memory',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
name: 'ip',
|
||||
label: 'IP Address',
|
||||
labelKey: 'harvester.tableHeaders.vm.ipAddress',
|
||||
value: 'id',
|
||||
formatter: 'HarvesterIpAddress'
|
||||
},
|
||||
{
|
||||
...AGE,
|
||||
sort: 'metadata.creationTimestamp:desc',
|
||||
}
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<div v-else id="host-instances" class="row">
|
||||
<div class="col span-12">
|
||||
<SortableTable
|
||||
v-bind="$attrs"
|
||||
:headers="headers"
|
||||
default-sort-by="age"
|
||||
:rows="rows"
|
||||
key-field="_key"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template slot="cell:state" slot-scope="scope" class="state-col">
|
||||
<div class="state">
|
||||
<HarvesterVmState class="vmstate" :row="scope.row" :all-cluster-network="allClusterNetwork" />
|
||||
</div>
|
||||
</template>
|
||||
</Sortabletable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#host-instances {
|
||||
::v-deep thead th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
::v-deep .state {
|
||||
display: flex;
|
||||
|
||||
.vmstate {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script>
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
|
||||
import { _CREATE } from '@shell/config/query-params';
|
||||
|
||||
export default {
|
||||
name: 'LinkStatus',
|
||||
|
||||
components: { LabelValue },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: _CREATE,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('generic.name')"
|
||||
:value="value.name"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('tableHeaders.state')"
|
||||
:value="value.state"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('tableHeaders.type')"
|
||||
:value="value.type"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.fields.macAddress')"
|
||||
:value="value.mac"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div
|
||||
v-if="value.promiscuous === 'true'"
|
||||
class="col span-6"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.fields.promiscuous')"
|
||||
:value="value.promiscuous"
|
||||
>
|
||||
<template #value>
|
||||
{{ value.promiscuous === 'true' ? t('generic.yes') : t('generic.no') }}
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script>
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
||||
import { BadgeState } from '@components/BadgeState';
|
||||
|
||||
import { _CREATE } from '@shell/config/query-params';
|
||||
import { findBy } from '@shell/utils/array';
|
||||
|
||||
import LinkStatus from './LinkStatus';
|
||||
import { HCI } from '../../../types';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHostNetwork',
|
||||
|
||||
components: {
|
||||
LabelValue,
|
||||
LinkStatus,
|
||||
ArrayListGrouped,
|
||||
BadgeState,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: _CREATE,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
conditions() {
|
||||
return this.value?.status?.conditions || [];
|
||||
},
|
||||
|
||||
readyCondition() {
|
||||
return findBy(this.conditions, 'type', 'ready') || {};
|
||||
},
|
||||
|
||||
linkStatus() {
|
||||
const linkMonitorId = this.value?.status?.linkMonitor;
|
||||
const nodeName = this.value?.status?.node;
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const linkMonitors = this.$store.getters[`${ inStore }/all`](HCI.LINK_MONITOR);
|
||||
const linkMonitor = (linkMonitors.filter(l => l.id === linkMonitorId) || [])[0] || {};
|
||||
|
||||
return linkMonitor?.status?.linkStatus?.[nodeName] || [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<div class="pull-right">
|
||||
{{ t('resourceTabs.conditions.tab') }}:
|
||||
<BadgeState
|
||||
v-clean-tooltip="readyCondition.message"
|
||||
:color="readyCondition.status === 'True' ? 'bg-success' : 'bg-error' "
|
||||
:icon="readyCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
|
||||
:label="t('tableHeaders.ready')"
|
||||
class="mr-10 ml-10 state"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.network.clusterNetwork.label')"
|
||||
:value="value.status.clusterNetwork"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.vlanStatus.vlanConfig.label')"
|
||||
:value="value.status.vlanConfig"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-12">
|
||||
<ArrayListGrouped
|
||||
v-model="linkStatus"
|
||||
:mode="mode"
|
||||
:can-remove="false"
|
||||
>
|
||||
<template #default="props">
|
||||
<LinkStatus
|
||||
:value="props.row.value"
|
||||
:mode="mode"
|
||||
/>
|
||||
</template>
|
||||
</ArrayListGrouped>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,488 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tag from '@shell/components/Tag';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import InfoBox from '@shell/components/InfoBox';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
||||
import Loading from '@shell/components/Loading.vue';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
import Banner from '@components/Banner/Banner.vue';
|
||||
|
||||
import metricPoller from '@shell/mixins/metric-poller';
|
||||
import {
|
||||
METRIC, NODE, LONGHORN, POD, EVENT
|
||||
} from '@shell/config/types';
|
||||
import { HCI } from '../../types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { formatSi } from '@shell/utils/units';
|
||||
import { findBy } from '@shell/utils/array';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
|
||||
import Basic from './HarvesterHostBasic';
|
||||
import Instance from './VirtualMachineInstance';
|
||||
import Disk from './HarvesterHostDisk';
|
||||
import VlanStatus from './VlanStatus';
|
||||
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
||||
import HarvesterSeeder from './HarvesterSeeder';
|
||||
|
||||
const LONGHORN_SYSTEM = 'longhorn-system';
|
||||
|
||||
export default {
|
||||
name: 'DetailHost',
|
||||
|
||||
components: {
|
||||
Tabbed,
|
||||
Tab,
|
||||
Tag,
|
||||
Basic,
|
||||
Instance,
|
||||
ArrayListGrouped,
|
||||
Disk,
|
||||
InfoBox,
|
||||
VlanStatus,
|
||||
LabelValue,
|
||||
HarvesterKsmtuned,
|
||||
Loading,
|
||||
SortableTable,
|
||||
HarvesterSeeder,
|
||||
Banner,
|
||||
},
|
||||
mixins: [metricPoller],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
nodes: this.$store.dispatch('harvester/findAll', { type: NODE }),
|
||||
pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
|
||||
};
|
||||
|
||||
if (this.$store.getters['harvester/schemaFor'](HCI.VLAN_STATUS)) {
|
||||
hash.hostNetworks = this.$store.dispatch('harvester/findAll', { type: HCI.VLAN_STATUS });
|
||||
}
|
||||
|
||||
if (this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE)) {
|
||||
hash.blockDevices = this.$store.dispatch('harvester/findAll', { type: HCI.BLOCK_DEVICE });
|
||||
}
|
||||
|
||||
if (this.$store.getters['harvester/schemaFor'](LONGHORN.NODES)) {
|
||||
hash.longhornNodes = this.$store.dispatch('harvester/findAll', { type: LONGHORN.NODES });
|
||||
}
|
||||
|
||||
if (this.$store.getters['harvester/schemaFor'](HCI.LINK_MONITOR)) {
|
||||
hash.linkMonitors = this.$store.dispatch('harvester/findAll', { type: HCI.LINK_MONITOR });
|
||||
}
|
||||
|
||||
if (this.$store.getters['harvester/schemaFor'](HCI.ADD_ONS)) {
|
||||
hash.addons = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS });
|
||||
}
|
||||
|
||||
if (this.$store.getters['harvester/schemaFor'](HCI.INVENTORY)) {
|
||||
hash.inventories = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.INVENTORY });
|
||||
}
|
||||
|
||||
const res = await allHash(hash);
|
||||
const hostNetworkResource = (res.hostNetworks || []).find( O => this.value.id === O.attachNodeName);
|
||||
|
||||
this.loadMetrics();
|
||||
|
||||
if (hostNetworkResource) {
|
||||
this.hostNetworkResource = hostNetworkResource;
|
||||
}
|
||||
|
||||
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
|
||||
const provisionedBlockDevices = blockDevices.filter((d) => {
|
||||
const provisioned = d?.spec?.fileSystem?.provisioned;
|
||||
const isCurrentNode = d?.spec?.nodeName === this.value.id;
|
||||
const isLonghornMounted = findBy(this.longhornDisks, 'name', d.metadata.name);
|
||||
|
||||
return provisioned && isCurrentNode && !isLonghornMounted;
|
||||
})
|
||||
.map((d) => {
|
||||
return {
|
||||
isNew: true,
|
||||
name: d?.metadata?.name,
|
||||
originPath: d?.spec?.fileSystem?.mountPoint,
|
||||
path: d?.spec?.fileSystem?.mountPoint,
|
||||
blockDevice: d,
|
||||
displayName: d?.displayName,
|
||||
forceFormatted: d?.spec?.fileSystem?.forceFormatted || false,
|
||||
};
|
||||
});
|
||||
|
||||
const disks = [...this.longhornDisks, ...provisionedBlockDevices];
|
||||
|
||||
this.disks = disks;
|
||||
this.newDisks = clone(disks);
|
||||
|
||||
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||
const seeder = addons.find(addon => addon.id === 'harvester-system/harvester-seeder');
|
||||
|
||||
const seederEnabled = seeder ? seeder?.spec?.enabled : false;
|
||||
|
||||
if (seederEnabled) {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const inventories = this.$store.getters[`${ inStore }/all`](HCI.INVENTORY) || [];
|
||||
|
||||
const inventory = inventories.find(inv => inv.id === `harvester-system/${ this.value.id }`);
|
||||
|
||||
if (inventory) {
|
||||
this.inventory = inventory;
|
||||
} else {
|
||||
this.inventory = await this.$store.dispatch(`${ inStore }/create`, {
|
||||
type: HCI.INVENTORY,
|
||||
metadata: {
|
||||
name: this.value.id,
|
||||
namespace: 'harvester-system'
|
||||
},
|
||||
});
|
||||
|
||||
this.inventory.applyDefaults();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
metrics: null,
|
||||
mode: 'view',
|
||||
hostNetworkResource: null,
|
||||
newDisks: [],
|
||||
disks: [],
|
||||
allEvents: [],
|
||||
didLoadEvents: false,
|
||||
inventory: {},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
longhornDisks() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.value.id }`);
|
||||
const diskStatus = longhornNode?.status?.diskStatus || {};
|
||||
const diskSpec = longhornNode?.spec?.disks || {};
|
||||
|
||||
const formatOptions = {
|
||||
increment: 1024,
|
||||
minExponent: 3,
|
||||
maxExponent: 3,
|
||||
maxPrecision: 2,
|
||||
suffix: 'iB',
|
||||
};
|
||||
|
||||
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
||||
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `longhorn-system/${ key }`);
|
||||
|
||||
return {
|
||||
...diskStatus[key],
|
||||
...diskSpec?.[key],
|
||||
name: key,
|
||||
isNew: false,
|
||||
storageReserved: formatSi(diskSpec[key]?.storageReserved, formatOptions),
|
||||
storageAvailable: formatSi(diskStatus[key]?.storageAvailable, formatOptions),
|
||||
storageMaximum: formatSi(diskStatus[key]?.storageMaximum, formatOptions),
|
||||
storageScheduled: formatSi(diskStatus[key]?.storageScheduled, formatOptions),
|
||||
blockDevice,
|
||||
displayName: blockDevice?.displayName || key,
|
||||
forceFormatted: blockDevice?.spec?.fileSystem?.forceFormatted || false,
|
||||
tags: diskSpec?.[key]?.tags || [],
|
||||
};
|
||||
});
|
||||
|
||||
return longhornDisks;
|
||||
},
|
||||
|
||||
hasKsmtunedSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||
},
|
||||
|
||||
hasBlockDevicesSchema() {
|
||||
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
||||
},
|
||||
|
||||
hasHostNetworksSchema() {
|
||||
return !!this.$store.getters['harvester/schemaFor'](HCI.VLAN_STATUS);
|
||||
},
|
||||
|
||||
vlanStatuses() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const nodeId = this.value.id;
|
||||
const vlanStatuses = this.$store.getters[`${ inStore }/all`](HCI.VLAN_STATUS);
|
||||
|
||||
return vlanStatuses.filter(s => s?.status?.node === nodeId) || [];
|
||||
},
|
||||
|
||||
longhornNode() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const longhornNodes = this.$store.getters[`${ inStore }/all`](LONGHORN.NODES);
|
||||
|
||||
return longhornNodes.find(node => node.id === `${ LONGHORN_SYSTEM }/${ this.value.id }`);
|
||||
},
|
||||
|
||||
events() {
|
||||
return this.allEvents.filter((event) => {
|
||||
return event.involvedObject?.uid === this.value?.metadata?.uid &&
|
||||
event.reason !== 'SeederUpdated';
|
||||
}).map((event) => {
|
||||
return {
|
||||
reason: (`${ event.reason || this.t('generic.unknown') }${ event.count > 1 ? ` (${ event.count })` : '' }`).trim(),
|
||||
message: event.message || this.t('generic.unknown'),
|
||||
date: event.lastTimestamp || event.firstTimestamp || event.metadata.creationTimestamp,
|
||||
eventType: event.eventType
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
eventHeaders() {
|
||||
return [
|
||||
{
|
||||
name: 'reason',
|
||||
label: this.t('tableHeaders.reason'),
|
||||
value: 'reason',
|
||||
sort: 'reason',
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
label: this.t('tableHeaders.message'),
|
||||
value: 'message',
|
||||
sort: 'message',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: this.t('tableHeaders.updated'),
|
||||
value: 'date',
|
||||
sort: 'date:desc',
|
||||
formatter: 'LiveDate',
|
||||
formatterOpts: { addSuffix: true },
|
||||
width: 125
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
seederEnabled() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||
const seeder = addons.find(addon => addon.id === 'harvester-system/harvester-seeder');
|
||||
|
||||
return seeder ? seeder?.spec?.enabled : false;
|
||||
},
|
||||
|
||||
ntpSync() {
|
||||
const jsonString = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.NODE_NTP_SYNC_STATUS];
|
||||
let out = null;
|
||||
|
||||
if (!jsonString) {
|
||||
return out;
|
||||
}
|
||||
|
||||
try {
|
||||
out = JSON.parse(jsonString);
|
||||
} catch (err) {
|
||||
this.$store.dispatch('growl/fromError', {
|
||||
title: this.t('generic.notification.title.error', { name: escapeHtml(this.value.metadata.name) }),
|
||||
err,
|
||||
}, { root: true });
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
ntpSyncedStatus() {
|
||||
const status = this.ntpSync?.ntpSyncStatus;
|
||||
|
||||
if (status === 'disabled') {
|
||||
return {
|
||||
status: 'disabled',
|
||||
warning: { key: 'harvester.host.ntp.ntpSyncStatus.isDisabled' }
|
||||
};
|
||||
}
|
||||
|
||||
const current = this.ntpSync?.currentNtpServers || '';
|
||||
|
||||
if (status === 'unsynced') {
|
||||
return {
|
||||
status: 'unsynced',
|
||||
warning: {
|
||||
key: 'harvester.host.ntp.ntpSyncStatus.isUnsynced',
|
||||
current
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadMetrics() {
|
||||
const schema = this.$store.getters['harvester/schemaFor'](METRIC.NODE);
|
||||
|
||||
if (schema) {
|
||||
this.metrics = await this.$store.dispatch('harvester/find', {
|
||||
type: METRIC.NODE,
|
||||
id: this.value.id,
|
||||
opt: { force: true, watch: false }
|
||||
});
|
||||
|
||||
this.$forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
// Ensures we only fetch events and show the table when the events tab has been activated
|
||||
tabChange(neu) {
|
||||
if (!this.didLoadEvents && neu?.selectedName === 'events') {
|
||||
this.$store.dispatch(`harvester/findAll`, { type: EVENT }).then((events) => {
|
||||
this.allEvents = events;
|
||||
this.didLoadEvents = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<div v-else>
|
||||
<Banner
|
||||
v-if="ntpSyncedStatus.status === 'disabled'"
|
||||
color="warning"
|
||||
>
|
||||
<span v-clean-html="t(ntpSyncedStatus.warning.key)"></span>
|
||||
</Banner>
|
||||
<Banner
|
||||
v-if="ntpSyncedStatus.status === 'unsynced'"
|
||||
color="warning"
|
||||
>
|
||||
<span v-clean-html="t(ntpSyncedStatus.warning.key, { current: ntpSyncedStatus.warning.current }, true)"></span>
|
||||
</Banner>
|
||||
<Tabbed
|
||||
v-bind="$attrs"
|
||||
class="mt-15"
|
||||
:side-tabs="true"
|
||||
@changed="tabChange"
|
||||
>
|
||||
<Tab name="basics" :label="t('harvester.host.tabs.basics')" :weight="4" class="bordered-table">
|
||||
<Basic
|
||||
v-model="value"
|
||||
:metrics="metrics"
|
||||
:mode="mode"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab name="instance" :label="t('harvester.host.tabs.instance')" :weight="3" class="bordered-table">
|
||||
<Instance :node="value" />
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="hasHostNetworksSchema && vlanStatuses.length > 0"
|
||||
name="network"
|
||||
:label="t('harvester.host.tabs.network')"
|
||||
:weight="2"
|
||||
class="bordered-table"
|
||||
>
|
||||
<InfoBox
|
||||
v-for="vlan in vlanStatuses"
|
||||
:key="vlan.id"
|
||||
>
|
||||
<VlanStatus
|
||||
:value="vlan"
|
||||
:mode="mode"
|
||||
/>
|
||||
</InfoBox>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="hasBlockDevicesSchema"
|
||||
name="disk"
|
||||
:weight="1"
|
||||
:label="t('harvester.host.tabs.storage')"
|
||||
>
|
||||
<div
|
||||
v-if="longhornNode"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<LabelValue
|
||||
v-if="longhornNode.spec.tags.length"
|
||||
:name="t('harvester.host.tags.label')"
|
||||
>
|
||||
<template #value>
|
||||
<div class="mt-5">
|
||||
<Tag v-for="(prop, key) in longhornNode.spec.tags" :key="key + prop" class="mr-5">
|
||||
{{ prop }}
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
<ArrayListGrouped
|
||||
v-model="newDisks"
|
||||
:mode="mode"
|
||||
:can-remove="false"
|
||||
:initial-empty-row="false"
|
||||
>
|
||||
<template #default="props">
|
||||
<Disk
|
||||
v-model="props.row.value"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:disks="disks"
|
||||
/>
|
||||
</template>
|
||||
</ArrayListGrouped>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="hasKsmtunedSchema"
|
||||
name="ksmtuned"
|
||||
:weight="0"
|
||||
:show-header="false"
|
||||
:label="t('harvester.host.tabs.ksmtuned')"
|
||||
>
|
||||
<HarvesterKsmtuned :mode="mode" :node="value" />
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="seederEnabled"
|
||||
name="seeder"
|
||||
:weight="-1"
|
||||
:label="t('harvester.host.tabs.seeder')"
|
||||
>
|
||||
<HarvesterSeeder
|
||||
:mode="mode"
|
||||
:node="value"
|
||||
:inventory="inventory"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
label-key="harvester.virtualMachine.detail.tabs.events"
|
||||
name="events"
|
||||
:weight="-99"
|
||||
>
|
||||
<SortableTable
|
||||
:rows="events"
|
||||
:headers="eventHeaders"
|
||||
key-field="id"
|
||||
:search="false"
|
||||
:table-actions="false"
|
||||
:row-actions="false"
|
||||
default-sort-by="date"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,287 @@
|
||||
<script>
|
||||
import { SECRET_TYPES as TYPES } from '@shell/config/secret';
|
||||
import { base64Decode } from '@shell/utils/crypto';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||
import DetailText from '@shell/components/DetailText';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
|
||||
const types = [
|
||||
TYPES.OPAQUE,
|
||||
TYPES.DOCKER_JSON,
|
||||
TYPES.TLS,
|
||||
TYPES.SSH,
|
||||
TYPES.BASIC,
|
||||
];
|
||||
const registryAddresses = [
|
||||
'DockerHub', 'Quay.io', 'Artifactory', 'Custom'
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ResourceTabs,
|
||||
DetailText,
|
||||
Tab,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
let username;
|
||||
let password;
|
||||
let registryUrl;
|
||||
let registryProvider = 'Custom';
|
||||
let key;
|
||||
let crt;
|
||||
|
||||
if (this.value._type === TYPES.DOCKER_JSON) {
|
||||
const json = base64Decode(this.value.data['.dockerconfigjson']);
|
||||
|
||||
const { auths } = JSON.parse(json);
|
||||
|
||||
registryUrl = Object.keys(auths)[0];
|
||||
|
||||
if (registryUrl === 'index.docker.io/v1/') {
|
||||
registryProvider = 'DockerHub';
|
||||
} else if (registryUrl === 'quay.io') {
|
||||
registryProvider = 'Quay.io';
|
||||
} else if (registryUrl.includes('artifactory')) {
|
||||
registryProvider = 'Artifactory';
|
||||
}
|
||||
|
||||
username = auths[registryUrl].username;
|
||||
password = auths[registryUrl].password;
|
||||
}
|
||||
|
||||
const data = this.value?.data || {};
|
||||
|
||||
if (this.value._type === TYPES.TLS) {
|
||||
// do not show existing key when editing
|
||||
key = this.mode === 'edit' ? '' : base64Decode(data['tls.key']);
|
||||
|
||||
crt = base64Decode(data['tls.crt']);
|
||||
}
|
||||
|
||||
if (this.value._type === TYPES.SERVICE_ACCT) {
|
||||
key = base64Decode(data['token']);
|
||||
crt = base64Decode(data['ca.crt']);
|
||||
}
|
||||
|
||||
if ( this.value._type === TYPES.BASIC ) {
|
||||
username = base64Decode(data.username || '');
|
||||
password = base64Decode(data.password || '');
|
||||
}
|
||||
|
||||
if ( this.value._type === TYPES.SSH ) {
|
||||
username = base64Decode(data['ssh-publickey'] || '');
|
||||
password = base64Decode(data['ssh-privatekey'] || '');
|
||||
}
|
||||
|
||||
if (!this.value._type) {
|
||||
this.$set(this.value, '_type', TYPES.OPAQUE);
|
||||
}
|
||||
|
||||
return {
|
||||
types,
|
||||
registryAddresses,
|
||||
registryProvider,
|
||||
username,
|
||||
password,
|
||||
registryUrl,
|
||||
key,
|
||||
crt,
|
||||
relatedServices: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isCertificate() {
|
||||
return this.value._type === TYPES.TLS;
|
||||
},
|
||||
|
||||
isSvcAcctToken() {
|
||||
return this.value._type === TYPES.SERVICE_ACCT;
|
||||
},
|
||||
|
||||
isRegistry() {
|
||||
return this.value._type === TYPES.DOCKER_JSON;
|
||||
},
|
||||
|
||||
isSsh() {
|
||||
return this.value._type === TYPES.SSH;
|
||||
},
|
||||
|
||||
isBasicAuth() {
|
||||
return this.value._type === TYPES.BASIC;
|
||||
},
|
||||
|
||||
parsedRows() {
|
||||
const rows = [];
|
||||
const { data = {} } = this.value;
|
||||
|
||||
Object.keys(data).forEach((key) => {
|
||||
const value = base64Decode(data[key]);
|
||||
|
||||
rows.push({
|
||||
key,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
return rows;
|
||||
},
|
||||
|
||||
dataLabel() {
|
||||
switch (this.value._type) {
|
||||
case TYPES.TLS:
|
||||
return this.t('secret.certificate.certificate');
|
||||
case TYPES.SSH:
|
||||
return this.t('secret.ssh.keys');
|
||||
case TYPES.BASIC:
|
||||
return this.t('secret.authentication');
|
||||
default:
|
||||
return this.t('secret.data');
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ResourceTabs
|
||||
v-model="value"
|
||||
:need-events="false"
|
||||
:need-related="false"
|
||||
:mode="mode"
|
||||
>
|
||||
<Tab
|
||||
name="data"
|
||||
:label="dataLabel"
|
||||
>
|
||||
<template v-if="isRegistry || isBasicAuth">
|
||||
<div
|
||||
v-if="isRegistry"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<DetailText
|
||||
:value="registryUrl"
|
||||
label-key="secret.registry.domainName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<DetailText
|
||||
:value="username"
|
||||
label-key="secret.registry.username"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<DetailText
|
||||
:value="password"
|
||||
label-key="secret.registry.password"
|
||||
:conceal="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-else-if="isCertificate"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-6">
|
||||
<DetailText
|
||||
:value="key"
|
||||
label-key="secret.certificate.privateKey"
|
||||
:conceal="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<DetailText
|
||||
:value="crt"
|
||||
label-key="secret.certificate.certificate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="isSvcAcctToken"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-6">
|
||||
<DetailText
|
||||
:value="crt"
|
||||
label-key="secret.serviceAcct.ca"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<DetailText
|
||||
:value="key"
|
||||
label-key="secret.serviceAcct.token"
|
||||
:conceal="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="isSsh"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-6">
|
||||
<DetailText
|
||||
:value="username"
|
||||
label-key="secret.ssh.public"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<DetailText
|
||||
:value="password"
|
||||
label-key="secret.ssh.private"
|
||||
:conceal="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(row,idx) in parsedRows"
|
||||
:key="idx"
|
||||
class="entry"
|
||||
>
|
||||
<DetailText
|
||||
:value="row.value"
|
||||
:label="row.key"
|
||||
:conceal="true"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!parsedRows.length">
|
||||
<div
|
||||
v-t="'sortableTable.noRows'"
|
||||
class="m-20 text-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.entry {
|
||||
margin-top: 10px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,221 @@
|
||||
<script>
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import VM_MIXIN from '../../mixins/harvester-vm';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { HCI } from '../../types';
|
||||
import CpuMemory from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
||||
|
||||
import OverviewKeypairs from '../kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineKeypairs';
|
||||
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||
const UNDEFINED = 'n/a';
|
||||
|
||||
export default {
|
||||
name: 'BackupDetail',
|
||||
|
||||
components: {
|
||||
Volume,
|
||||
Network,
|
||||
CruResource,
|
||||
Tabbed,
|
||||
Loading,
|
||||
LabelValue,
|
||||
Tab,
|
||||
CloudConfig,
|
||||
Checkbox,
|
||||
CpuMemory,
|
||||
OverviewKeypairs,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, VM_MIXIN],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await allHash({ allImages: this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE }) });
|
||||
},
|
||||
|
||||
data() {
|
||||
return { vm: null };
|
||||
},
|
||||
|
||||
computed: {
|
||||
name() {
|
||||
return this.value?.metadata?.name || UNDEFINED;
|
||||
},
|
||||
|
||||
hostname() {
|
||||
return this?.spec?.template?.spec?.hostname;
|
||||
},
|
||||
|
||||
imageName() {
|
||||
const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || [];
|
||||
|
||||
const image = imageList.find( I => this.imageId === I.id);
|
||||
|
||||
return image?.spec?.displayName || '-';
|
||||
},
|
||||
|
||||
disks() {
|
||||
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||
|
||||
return disks.filter((disk) => {
|
||||
return !!disk.bootOrder;
|
||||
}).sort((a, b) => {
|
||||
if (a.bootOrder < b.bootOrder) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
},
|
||||
|
||||
cdroms() {
|
||||
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||
|
||||
return disks.filter((disk) => {
|
||||
return !!disk.cdrom;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDeviceType(o) {
|
||||
if (o.disk) {
|
||||
return 'Disk';
|
||||
} else {
|
||||
return 'CD-ROM';
|
||||
}
|
||||
},
|
||||
isEmpty(o) {
|
||||
return o !== undefined && Object.keys(o).length === 0;
|
||||
},
|
||||
onTabChanged({ tab }) {
|
||||
if (tab.name === 'advanced') {
|
||||
this.$refs.yamlEditor?.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<CruResource
|
||||
v-else
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
>
|
||||
<Tabbed v-if="spec" :side-tabs="true" @changed="onTabChanged">
|
||||
<Tab name="Basics" :label="t('harvester.virtualMachine.detail.tabs.basics')">
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.name')" :value="name" />
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.fields.image')" :value="imageName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.hostname')" :value="hostname" />
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.input.MachineType')" :value="machineType" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CpuMemory :cpu="cpu" :mode="mode" :memory="memory" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.bootOrder')">
|
||||
<template #value>
|
||||
<div>
|
||||
<ul>
|
||||
<li v-for="(disk) in disks" :key="disk.bootOrder">
|
||||
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.CDROMs')">
|
||||
<template #value>
|
||||
<div>
|
||||
<ul v-if="cdroms.length > 0">
|
||||
<li v-for="(rom) in cdroms" :key="rom.name">
|
||||
{{ rom.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<span v-else>
|
||||
{{ t("harvester.virtualMachine.detail.notAvailable") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="volume"
|
||||
:label="t('harvester.tab.volume')"
|
||||
:weight="-1"
|
||||
>
|
||||
<Volume v-model="diskRows" :mode="mode" />
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="network"
|
||||
:label="t('harvester.tab.network')"
|
||||
:weight="-2"
|
||||
>
|
||||
<Network v-model="networkRows" :mode="mode" />
|
||||
</Tab>
|
||||
|
||||
<Tab name="keypairs" :label="t('harvester.virtualMachine.detail.tabs.keypairs')" class="bordered-table" :weight="-3">
|
||||
<OverviewKeypairs v-if="vm" v-model="vm" />
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="advanced"
|
||||
:label="t('harvester.tab.advanced')"
|
||||
:weight="-4"
|
||||
>
|
||||
<CloudConfig
|
||||
ref="yamlEditor"
|
||||
:user-script="userScript"
|
||||
:mode="mode"
|
||||
:network-script="networkScript"
|
||||
/>
|
||||
|
||||
<div class="spacer"></div>
|
||||
<Checkbox v-model="installUSBTablet" :mode="mode" class="check" type="checkbox" :label="t('harvester.virtualMachine.enableUsb')" />
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script>
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
},
|
||||
|
||||
components: { LabelValue },
|
||||
|
||||
computed: {
|
||||
migratable() {
|
||||
return this.value.migratable === 'true' ? this.t('generic.yes') : this.t('generic.no');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="value.nodeSelector"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<LabelValue
|
||||
:name="t('harvester.storage.nodeSelector.label')"
|
||||
:value="value.nodeSelector"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="value.diskSelector"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<LabelValue
|
||||
:name="t('harvester.storage.diskSelector.label')"
|
||||
:value="value.diskSelector"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.storage.migratable.label')"
|
||||
:value="migratable"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.storage.numberOfReplicas.label')"
|
||||
:value="value.numberOfReplicas"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.storage.staleReplicaTimeout.label')"
|
||||
:value="value.staleReplicaTimeout"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script>
|
||||
import CopyToClipboardText from '@shell/components/CopyToClipboardText';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { DESCRIPTION } from '@shell/config/labels-annotations';
|
||||
import { HCI } from '@pkg/harvester/config/labels-annotations';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { findBy } from '@shell/utils/array';
|
||||
import { get } from '@shell/utils/object';
|
||||
|
||||
import Storage from './Storage';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CopyToClipboardText,
|
||||
Tab,
|
||||
Tabbed,
|
||||
LabelValue,
|
||||
Storage,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
formattedValue() {
|
||||
return this.value?.downSize;
|
||||
},
|
||||
|
||||
url() {
|
||||
return this.value?.spec?.url || '-';
|
||||
},
|
||||
|
||||
description() {
|
||||
return this.value?.metadata?.annotations?.[DESCRIPTION] || '-';
|
||||
},
|
||||
|
||||
errorMessage() {
|
||||
const conditions = get(this.value, 'status.conditions');
|
||||
|
||||
return findBy(conditions, 'type', 'Imported')?.message || '-';
|
||||
},
|
||||
|
||||
isUpload() {
|
||||
return this.value?.spec?.sourceType === 'upload';
|
||||
},
|
||||
|
||||
imageName() {
|
||||
return this.value?.metadata?.annotations?.[HCI.IMAGE_NAME] || '-';
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||
<Tab
|
||||
name="detail"
|
||||
:label="t('harvester.virtualMachine.detail.tabs.basics')"
|
||||
class="bordered-table"
|
||||
:weight="99"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<LabelValue
|
||||
v-if="isUpload"
|
||||
:name="t('harvester.image.fileName')"
|
||||
:value="imageName"
|
||||
class="mb-20"
|
||||
/>
|
||||
<LabelValue
|
||||
v-else
|
||||
:name="t('harvester.image.url')"
|
||||
:value="url"
|
||||
class="mb-20"
|
||||
>
|
||||
<template #value>
|
||||
<div v-if="url !== '-'">
|
||||
<CopyToClipboardText :text="url" />
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ url }}
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<LabelValue :name="t('harvester.image.size')" :value="formattedValue" class="mb-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<LabelValue :name="t('nameNsDescription.description.label')" :value="description" class="mb-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage !== '-'" class="row">
|
||||
<div class="col span-12">
|
||||
<div>
|
||||
{{ t('tableHeaders.message') }}
|
||||
</div>
|
||||
<div :class="{ 'error': errorMessage !== '-' }">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="storage"
|
||||
:label="t('harvester.storage.label')"
|
||||
:weight="89"
|
||||
class="bordered-table"
|
||||
>
|
||||
<Storage
|
||||
v-model="value.spec.storageClassParameters"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,221 @@
|
||||
<script>
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import VM_MIXIN from '../../mixins/harvester-vm';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { HCI } from '../../types';
|
||||
import CpuMemory from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
||||
|
||||
import OverviewKeypairs from '../kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineKeypairs';
|
||||
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||
const UNDEFINED = 'n/a';
|
||||
|
||||
export default {
|
||||
name: 'VMSnapshotDetail',
|
||||
|
||||
components: {
|
||||
Volume,
|
||||
Network,
|
||||
CruResource,
|
||||
Tabbed,
|
||||
Loading,
|
||||
LabelValue,
|
||||
Tab,
|
||||
CloudConfig,
|
||||
Checkbox,
|
||||
CpuMemory,
|
||||
OverviewKeypairs,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, VM_MIXIN],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await allHash({ allImages: this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE }) });
|
||||
},
|
||||
|
||||
data() {
|
||||
return { vm: null };
|
||||
},
|
||||
|
||||
computed: {
|
||||
name() {
|
||||
return this.value?.metadata?.name || UNDEFINED;
|
||||
},
|
||||
|
||||
hostname() {
|
||||
return this?.spec?.template?.spec?.hostname;
|
||||
},
|
||||
|
||||
imageName() {
|
||||
const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || [];
|
||||
|
||||
const image = imageList.find( I => this.imageId === I.id);
|
||||
|
||||
return image?.spec?.displayName || '-';
|
||||
},
|
||||
|
||||
disks() {
|
||||
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||
|
||||
return disks.filter((disk) => {
|
||||
return !!disk.bootOrder;
|
||||
}).sort((a, b) => {
|
||||
if (a.bootOrder < b.bootOrder) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
},
|
||||
|
||||
cdroms() {
|
||||
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||
|
||||
return disks.filter((disk) => {
|
||||
return !!disk.cdrom;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDeviceType(o) {
|
||||
if (o.disk) {
|
||||
return 'Disk';
|
||||
} else {
|
||||
return 'CD-ROM';
|
||||
}
|
||||
},
|
||||
isEmpty(o) {
|
||||
return o !== undefined && Object.keys(o).length === 0;
|
||||
},
|
||||
onTabChanged({ tab }) {
|
||||
if (tab.name === 'advanced') {
|
||||
this.$refs.yamlEditor?.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<CruResource
|
||||
v-else
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
>
|
||||
<Tabbed v-if="spec" :side-tabs="true" @changed="onTabChanged">
|
||||
<Tab name="Basics" :label="t('harvester.virtualMachine.detail.tabs.basics')">
|
||||
<div class="row mb-10">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.name')" :value="name" />
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.fields.image')" :value="imageName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-10">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.hostname')" :value="hostname" />
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.input.MachineType')" :value="machineType" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CpuMemory :cpu="cpu" :mode="mode" :memory="memory" />
|
||||
|
||||
<div class="row mb-10">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.bootOrder')">
|
||||
<template #value>
|
||||
<div>
|
||||
<ul>
|
||||
<li v-for="(disk) in disks" :key="disk.bootOrder">
|
||||
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.CDROMs')">
|
||||
<template #value>
|
||||
<div>
|
||||
<ul v-if="cdroms.length > 0">
|
||||
<li v-for="(rom) in cdroms" :key="rom.name">
|
||||
{{ rom.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<span v-else>
|
||||
{{ t("harvester.virtualMachine.detail.notAvailable") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="volume"
|
||||
:label="t('harvester.tab.volume')"
|
||||
:weight="-1"
|
||||
>
|
||||
<Volume v-model="diskRows" :mode="mode" />
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="network"
|
||||
:label="t('harvester.tab.network')"
|
||||
:weight="-2"
|
||||
>
|
||||
<Network v-model="networkRows" :mode="mode" />
|
||||
</Tab>
|
||||
|
||||
<Tab name="keypairs" :label="t('harvester.virtualMachine.detail.tabs.keypairs')" class="bordered-table" :weight="-3">
|
||||
<OverviewKeypairs v-if="vm" v-model="vm" />
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="advanced"
|
||||
:label="t('harvester.tab.advanced')"
|
||||
:weight="-4"
|
||||
>
|
||||
<CloudConfig
|
||||
ref="yamlEditor"
|
||||
:user-script="userScript"
|
||||
:mode="mode"
|
||||
:network-script="networkScript"
|
||||
/>
|
||||
|
||||
<div class="spacer"></div>
|
||||
<Checkbox v-model="installUSBTablet" :mode="mode" class="check" type="checkbox" :label="t('harvester.virtualMachine.enableUsb')" />
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
<script>
|
||||
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
||||
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { HCI } from '../../../types';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
|
||||
const UNDEFINED = 'n/a';
|
||||
|
||||
export default {
|
||||
name: 'VMDetailsBasics',
|
||||
|
||||
components: {
|
||||
VMConsoleBar,
|
||||
HarvesterIpAddress,
|
||||
LabelValue,
|
||||
InputOrDisplay
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
vmi: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
creationTimestamp() {
|
||||
const date = new Date(this.value?.metadata?.creationTimestamp);
|
||||
|
||||
if (!date.getMonth) {
|
||||
return UNDEFINED;
|
||||
}
|
||||
|
||||
return `${ date.getMonth() + 1 }/${ date.getDate() }/${ date.getUTCFullYear() }`;
|
||||
},
|
||||
|
||||
node() {
|
||||
const node = this.vmi?.status?.nodeName || UNDEFINED;
|
||||
|
||||
return this.isDown ? this.t('harvester.virtualMachine.detail.details.down') : node;
|
||||
},
|
||||
|
||||
hostname() {
|
||||
const hostName = this.vmi?.spec?.hostname || this.vmi?.status?.guestOSInfo?.hostname || this.t('harvester.virtualMachine.detail.GuestAgentNotInstalled');
|
||||
|
||||
return this.isDown ? this.t('harvester.virtualMachine.detail.details.down') : hostName;
|
||||
},
|
||||
|
||||
imageName() {
|
||||
const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || [];
|
||||
|
||||
const image = imageList.find( (I) => {
|
||||
return this.value.rootImageId === I.id;
|
||||
});
|
||||
|
||||
return image?.spec?.displayName || 'N/A';
|
||||
},
|
||||
|
||||
disks() {
|
||||
const disks = this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||
|
||||
return disks.filter((disk) => {
|
||||
return !!disk.bootOrder;
|
||||
}).sort((a, b) => {
|
||||
if (a.bootOrder < b.bootOrder) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
},
|
||||
|
||||
cdroms() {
|
||||
const disks = this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||
|
||||
return disks.filter((disk) => {
|
||||
return !!disk.cdrom;
|
||||
});
|
||||
},
|
||||
|
||||
flavor() {
|
||||
const domain = this.value?.spec?.template?.spec?.domain;
|
||||
|
||||
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||
},
|
||||
|
||||
kernelRelease() {
|
||||
const kernelRelease = this.vmi?.status?.guestOSInfo?.kernelRelease || this.t('harvester.virtualMachine.detail.GuestAgentNotInstalled');
|
||||
|
||||
return this.isDown ? this.t('harvester.virtualMachine.detail.details.down') : kernelRelease;
|
||||
},
|
||||
|
||||
operatingSystem() {
|
||||
const operatingSystem = this.vmi?.status?.guestOSInfo?.prettyName || this.t('harvester.virtualMachine.detail.GuestAgentNotInstalled');
|
||||
|
||||
return this.isDown ? this.t('harvester.virtualMachine.detail.details.down') : operatingSystem;
|
||||
},
|
||||
|
||||
isDown() {
|
||||
return this.isEmpty(this.vmi);
|
||||
},
|
||||
|
||||
machineType() {
|
||||
return this.value?.spec?.template?.spec?.domain?.machine?.type || undefined;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDeviceType(o) {
|
||||
if (o.disk) {
|
||||
return 'Disk';
|
||||
} else {
|
||||
return 'CD-ROM';
|
||||
}
|
||||
},
|
||||
isEmpty(o) {
|
||||
return o !== undefined && Object.keys(o).length === 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VMConsoleBar :resource="value" class="consoleBut" />
|
||||
<div class="overview-basics">
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.name')" :value="value.nameDisplay">
|
||||
<template #value>
|
||||
<div class="smart-row">
|
||||
<div class="console">
|
||||
{{ value.nameDisplay }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.fields.image')" :value="imageName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.hostname')" :value="hostname">
|
||||
<template #value>
|
||||
<div>
|
||||
{{ hostname }}
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.node')" :value="node">
|
||||
<template #value>
|
||||
<div>
|
||||
{{ node }}
|
||||
</div>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.ipAddress')">
|
||||
<template #value>
|
||||
<HarvesterIpAddress v-model="value.id" :row="value" />
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.created')" :value="creationTimestamp" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider" />
|
||||
|
||||
<h2>{{ t('harvester.virtualMachine.detail.tabs.configurations') }}</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<InputOrDisplay :name="t('harvester.virtualMachine.detail.details.bootOrder')" :value="disks" :mode="mode">
|
||||
<template #value>
|
||||
<ul>
|
||||
<li v-for="(disk) in disks" :key="disk.bootOrder">
|
||||
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<InputOrDisplay :name="t('harvester.virtualMachine.detail.details.CDROMs')" :value="cdroms" :mode="mode">
|
||||
<template #value>
|
||||
<div>
|
||||
<ul v-if="cdroms.length > 0">
|
||||
<li v-for="(rom) in cdroms" :key="rom.name">
|
||||
{{ rom.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<span v-else>
|
||||
{{ t("harvester.virtualMachine.detail.notAvailable") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.operatingSystem')" :value="operatingSystem" />
|
||||
</div>
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.flavor')" :value="flavor" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.kernelRelease')" :value="kernelRelease" />
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.input.MachineType')" :value="machineType" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.consoleBut {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.overview-basics {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: auto;
|
||||
grid-row-gap: 15px;
|
||||
|
||||
.badge-state {
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.smart-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.console {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__ssh-key {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<script>
|
||||
import { REASON } from '@shell/config/table-headers';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
|
||||
export default {
|
||||
name: 'VirtualMachineEvents',
|
||||
|
||||
components: { SortableTable },
|
||||
|
||||
props: {
|
||||
events: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const reason = {
|
||||
...REASON,
|
||||
canBeVariable: true,
|
||||
width: 180
|
||||
};
|
||||
|
||||
const eventHeaders = [
|
||||
reason,
|
||||
{
|
||||
name: 'resource',
|
||||
label: 'Resource',
|
||||
labelKey: 'clusterIndexPage.sections.events.resource.label',
|
||||
value: 'displayInvolvedObject',
|
||||
sort: ['involvedObject.kind', 'involvedObject.name'],
|
||||
canBeVariable: true,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
name: 'date',
|
||||
label: 'Date',
|
||||
labelKey: 'clusterIndexPage.sections.events.date.label',
|
||||
value: 'lastTimestamp',
|
||||
sort: 'lastTimestamp:desc',
|
||||
formatter: 'LiveDate',
|
||||
formatterOpts: { addSuffix: true },
|
||||
width: 125,
|
||||
defaultSort: true,
|
||||
},
|
||||
];
|
||||
|
||||
return { eventHeaders };
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SortableTable
|
||||
:rows="events"
|
||||
:headers="eventHeaders"
|
||||
key-field="id"
|
||||
:search="false"
|
||||
:table-actions="false"
|
||||
:row-actions="false"
|
||||
:paging="true"
|
||||
:rows-per-page="10"
|
||||
default-sort-by="date"
|
||||
>
|
||||
<template #cell:resource="{row, value}">
|
||||
<div class="text-info">
|
||||
{{ value }}
|
||||
</div>
|
||||
<div v-if="row.message">
|
||||
{{ row.displayMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</SortableTable>
|
||||
</template>
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<script>
|
||||
import isString from 'lodash/isString';
|
||||
import { HCI } from '../../../types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import impl from '../../../mixins/harvester-vm/impl';
|
||||
|
||||
export default {
|
||||
mixins: [impl],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const hash = await allHash({ allSSHs: this.$store.dispatch('harvester/findAll', { type: HCI.SSH }) });
|
||||
|
||||
this.allSSHs = hash.allSSHs;
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
allSSHs: [],
|
||||
sshKeys: []
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleShow(idx) {
|
||||
const ssh = this.sshKeys[idx];
|
||||
|
||||
this.$set(this.sshKeys, idx, {
|
||||
...ssh,
|
||||
showKey: !ssh.showKey
|
||||
});
|
||||
},
|
||||
|
||||
getKeys() {
|
||||
return this.mergeAllSSHs(this.value?.spec);
|
||||
},
|
||||
|
||||
isShow(id = '') {
|
||||
const ssh = this.sshKeys.find(O => O?.data?.id === id) || {};
|
||||
|
||||
return ssh.showKey || false;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
allSSHs(neu) {
|
||||
const out = this.getKeys().map((ssh) => {
|
||||
return {
|
||||
id: ssh.id,
|
||||
publicKey: isString(ssh.data) ? ssh.data : ssh.data?.spec?.publicKey,
|
||||
showKey: this.isShow(ssh.id)
|
||||
};
|
||||
});
|
||||
|
||||
this.$set(this, 'sshKeys', out);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overview-sshKeys">
|
||||
<div v-for="(ssh, index) in sshKeys" :key="index" class="row overview-sshKeys__item">
|
||||
<div class="col span-4">
|
||||
{{ ssh.id }}
|
||||
</div>
|
||||
<div class="col span-7 offset-1">
|
||||
<div v-if="ssh.showKey" class="key-display">
|
||||
{{ ssh.publicKey }}
|
||||
<button class="btn btn-sm role-link hide-bar" @click="toggleShow(index)">
|
||||
<i class="icon icon-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button v-else class="btn btn-sm role-link" @click="toggleShow(index)">
|
||||
*******<i class="icons icon-show"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.overview-sshKeys {
|
||||
text-align: left;
|
||||
max-height: 700px;
|
||||
overflow: auto;
|
||||
|
||||
&__item {
|
||||
margin-bottom: 15px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.key-display {
|
||||
position: relative;
|
||||
padding-right: 30px;
|
||||
word-break: break-word;
|
||||
|
||||
.hide-bar {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<script>
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
|
||||
export default {
|
||||
name: 'VirtualMachineMigration',
|
||||
|
||||
components: { LabelValue },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
vmiResource: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { localResource: this.vmiResource };
|
||||
},
|
||||
|
||||
computed: {
|
||||
migrationState() {
|
||||
return this.localResource?.status?.migrationState;
|
||||
},
|
||||
sourceNode() {
|
||||
return this.migrationState?.sourceNode || 'N/A';
|
||||
},
|
||||
targetNode() {
|
||||
return this.migrationState?.targetNode || 'N/A';
|
||||
},
|
||||
started() {
|
||||
return this.migrationState?.startTimestamp || 'N/A';
|
||||
},
|
||||
ended() {
|
||||
return this.migrationState?.endTimestamp || 'N/A';
|
||||
},
|
||||
message() {
|
||||
return 'N/A';
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
vmiResource: {
|
||||
handler(neu) {
|
||||
this.localResource = neu;
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.sourceNode')" :value="sourceNode" />
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.targetNode')" :value="targetNode" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.started')" :value="started" />
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.ended')" :value="ended" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,271 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { EVENT, SERVICE, POD } from '@shell/config/types';
|
||||
import { HCI } from '../../types';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import VM_MIXIN from '../../mixins/harvester-vm';
|
||||
import DashboardMetrics from '@shell/components/DashboardMetrics';
|
||||
import { allHash, setPromiseResult } from '@shell/utils/promise';
|
||||
import { allDashboardsExist } from '@shell/utils/grafana';
|
||||
|
||||
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
||||
import PodAffinity from '@shell/components/form/PodAffinity';
|
||||
import AccessCredentials from '../../edit/kubevirt.io.virtualmachine/VirtualMachineAccessCredentials';
|
||||
import Events from './VirtualMachineTabs/VirtualMachineEvents';
|
||||
import Migration from './VirtualMachineTabs/VirtualMachineMigration';
|
||||
import OverviewBasics from './VirtualMachineTabs/VirtualMachineBasics';
|
||||
import OverviewKeypairs from './VirtualMachineTabs/VirtualMachineKeypairs';
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
import Labels from '@shell/components/form/Labels';
|
||||
|
||||
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
|
||||
|
||||
export default {
|
||||
name: 'VMIDetailsPage',
|
||||
|
||||
components: {
|
||||
Tab,
|
||||
Tabbed,
|
||||
Events,
|
||||
OverviewBasics,
|
||||
Volume,
|
||||
Network,
|
||||
OverviewKeypairs,
|
||||
CloudConfig,
|
||||
Migration,
|
||||
DashboardMetrics,
|
||||
AccessCredentials,
|
||||
NodeScheduling,
|
||||
PodAffinity,
|
||||
KeyValue,
|
||||
Labels,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, VM_MIXIN],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
switchToCloud: false,
|
||||
VM_METRICS_DETAIL_URL,
|
||||
showVmMetrics: false,
|
||||
};
|
||||
},
|
||||
|
||||
async created() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
|
||||
services: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE }),
|
||||
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
|
||||
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
|
||||
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
|
||||
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
||||
};
|
||||
|
||||
await allHash(hash);
|
||||
|
||||
setPromiseResult(
|
||||
allDashboardsExist(this.$store, this.currentCluster.id, [VM_METRICS_DETAIL_URL], 'harvester'),
|
||||
this,
|
||||
'showVmMetrics',
|
||||
'Determine vm metrics'
|
||||
);
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['currentCluster']),
|
||||
|
||||
vmi() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const vmiList = this.$store.getters[`${ inStore }/all`](HCI.VMI) || [];
|
||||
const vmi = vmiList.find( (VMI) => {
|
||||
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.value?.metadata?.uid;
|
||||
});
|
||||
|
||||
return vmi;
|
||||
},
|
||||
|
||||
allEvents() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/all`](EVENT);
|
||||
},
|
||||
|
||||
events() {
|
||||
return this.allEvents.filter((e) => {
|
||||
const { name, creationTimestamp } = this.value?.metadata || {};
|
||||
const podName = this.value.podResource?.metadata?.name;
|
||||
const pvcName = this.value.persistentVolumeClaimName || [];
|
||||
|
||||
const involvedName = e?.involvedObject?.name;
|
||||
|
||||
const matchPVC = pvcName.find(name => name === involvedName);
|
||||
|
||||
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
|
||||
}).sort((a, b) => {
|
||||
if (a.lastTimestamp > b.lastTimestamp) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
},
|
||||
|
||||
graphVars() {
|
||||
return {
|
||||
namespace: this.value.namespace,
|
||||
vm: this.value.name
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTabChanged({ tab }) {
|
||||
if (tab.name === 'cloudConfig') {
|
||||
this.$refs.yamlEditor?.refresh();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: {
|
||||
handler(neu) {
|
||||
const diskRows = this.getDiskRows(neu);
|
||||
|
||||
this.$set(this, 'diskRows', diskRows);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true" @changed="onTabChanged">
|
||||
<Tab name="basics" :label="t('harvester.virtualMachine.detail.tabs.basics')" class="bordered-table" :weight="7">
|
||||
<OverviewBasics v-model="value" :vmi="vmi" mode="view" />
|
||||
</Tab>
|
||||
|
||||
<Tab name="disks" :label="t('harvester.tab.volume')" class="bordered-table" :weight="6">
|
||||
<Volume
|
||||
v-model="diskRows"
|
||||
mode="view"
|
||||
:namespace="value.metadata.namespace"
|
||||
:vm="value"
|
||||
:resource-type="value.type"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab name="networks" :label="t('harvester.virtualMachine.detail.tabs.networks')" class="bordered-table" :weight="5">
|
||||
<Network v-model="networkRows" mode="view" />
|
||||
</Tab>
|
||||
|
||||
<Tab name="keypairs" :label="t('harvester.virtualMachine.detail.tabs.keypairs')" class="bordered-table" :weight="3">
|
||||
<OverviewKeypairs v-model="value" />
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="showVmMetrics"
|
||||
name="vm-metrics"
|
||||
:label="t('harvester.virtualMachine.detail.tabs.metrics')"
|
||||
:weight="2.5"
|
||||
>
|
||||
<template #default="props">
|
||||
<DashboardMetrics
|
||||
v-if="props.active"
|
||||
:detail-url="VM_METRICS_DETAIL_URL"
|
||||
graph-height="550px"
|
||||
:has-summary-and-detail="false"
|
||||
:vars="graphVars"
|
||||
/>
|
||||
</template>
|
||||
</Tab>
|
||||
|
||||
<Tab name="nodeScheduling" :label="t('workload.container.titles.nodeScheduling')" :weight="2.4">
|
||||
<template #default="{active}">
|
||||
<NodeScheduling v-if="spec" :key="active" :mode="mode" :value="spec.template.spec" :nodes="nodesIdOptions" />
|
||||
</template>
|
||||
</Tab>
|
||||
|
||||
<Tab :label="t('harvester.tab.vmScheduling')" name="vmScheduling" :weight="2.3">
|
||||
<template #default="{active}">
|
||||
<PodAffinity
|
||||
v-if="spec"
|
||||
:key="active"
|
||||
:mode="mode"
|
||||
:value="spec.template.spec"
|
||||
:nodes="nodes"
|
||||
:all-namespaces-option-available="true"
|
||||
:namespaces="filteredNamespaces"
|
||||
:overwrite-labels="affinityLabels"
|
||||
/>
|
||||
</template>
|
||||
</Tab>
|
||||
|
||||
<Tab :label="t('harvester.tab.accessCredentials')" class="bordered-table" name="accessCredentials" :weight="2.2">
|
||||
<AccessCredentials mode="view" :value="accessCredentials" :resource="value" />
|
||||
</Tab>
|
||||
|
||||
<Tab name="cloudConfig" :label="t('harvester.virtualMachine.detail.tabs.cloudConfig')" class="bordered-table" :weight="2">
|
||||
<CloudConfig
|
||||
ref="yamlEditor"
|
||||
mode="view"
|
||||
:user-script="userScript"
|
||||
:network-script="networkScript"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab name="event" :label="t('harvester.virtualMachine.detail.tabs.events')" :weight="1">
|
||||
<Events :resource="vmi" :events="events" />
|
||||
</Tab>
|
||||
|
||||
<Tab name="migration" :label="t('harvester.virtualMachine.detail.tabs.migration')">
|
||||
<Migration v-model="value" :vmi-resource="vmi" />
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="instanceLabel"
|
||||
:label="t('harvester.tab.instanceLabel')"
|
||||
:weight="-99"
|
||||
>
|
||||
<Labels
|
||||
:default-container-class="'labels-and-annotations-container'"
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:display-side-by-side="false"
|
||||
:show-annotations="false"
|
||||
:show-label-title="false"
|
||||
>
|
||||
<template #labels="{toggler}">
|
||||
<KeyValue
|
||||
key="labels"
|
||||
:value="value.instanceLabels"
|
||||
:protected-keys="value.systemLabels || []"
|
||||
:toggle-filter="toggler"
|
||||
:add-label="t('labels.addLabel')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@input="value.setInstanceLabels($event)"
|
||||
/>
|
||||
</template>
|
||||
</Labels>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script>
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { NETWORK_HEADERS } from '@pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ResourceTabs,
|
||||
Tab,
|
||||
SortableTable,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = { ipPools: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }) };
|
||||
|
||||
await allHash(hash);
|
||||
},
|
||||
|
||||
computed: {
|
||||
networks() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const networks = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT);
|
||||
|
||||
return networks.filter(n => n?.id === this.value?.spec?.selector?.network);
|
||||
},
|
||||
|
||||
networkHeaders() {
|
||||
return NETWORK_HEADERS;
|
||||
},
|
||||
|
||||
ranges() {
|
||||
return this.value.spec.ranges;
|
||||
},
|
||||
|
||||
rangeHeaders() {
|
||||
return [{
|
||||
name: 'subnet',
|
||||
label: this.t('harvester.ipPool.subnet.label'),
|
||||
value: 'subnet',
|
||||
}, {
|
||||
name: 'gateway',
|
||||
label: this.t('harvester.ipPool.gateway.label'),
|
||||
value: 'gateway',
|
||||
}, {
|
||||
name: 'startIP',
|
||||
label: this.t('harvester.ipPool.startIP.label'),
|
||||
value: 'rangeStart',
|
||||
}, {
|
||||
name: 'endIP',
|
||||
label: this.t('harvester.ipPool.endIP.label'),
|
||||
value: 'rangeEnd',
|
||||
}];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ResourceTabs
|
||||
v-model="value"
|
||||
:need-related="false"
|
||||
>
|
||||
<Tab
|
||||
name="network"
|
||||
label-key="harvester.ipPool.network.label"
|
||||
:weight="99"
|
||||
>
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="networkHeaders"
|
||||
:rows="networks"
|
||||
:row-actions="false"
|
||||
:table-actions="false"
|
||||
:search="false"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="range"
|
||||
label-key="harvester.ipPool.tabs.range"
|
||||
:weight="89"
|
||||
>
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="rangeHeaders"
|
||||
:rows="ranges"
|
||||
:row-actions="false"
|
||||
:table-actions="false"
|
||||
:search="false"
|
||||
/>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</template>
|
||||
@@ -0,0 +1,190 @@
|
||||
<script>
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
import { HCI } from '@pkg/harvester/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { KEY, VALUE } from '@shell/config/table-headers';
|
||||
import { VM_HEADERS } from '@pkg/harvester/list/kubevirt.io.virtualmachine';
|
||||
import { matching } from '@shell/utils/selector';
|
||||
import { IP_POOL_HEADERS } from '../product';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ResourceTabs,
|
||||
Tab,
|
||||
SortableTable,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
ipPools: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IP_POOL }),
|
||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||
};
|
||||
|
||||
await allHash(hash);
|
||||
},
|
||||
|
||||
computed: {
|
||||
ipPools() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const ipPools = this.$store.getters[`${ inStore }/all`](HCI.IP_POOL);
|
||||
|
||||
return ipPools.filter(i => i.id === this.value.status.allocatedAddress.ipPool);
|
||||
},
|
||||
|
||||
ipPoolHeaders() {
|
||||
return IP_POOL_HEADERS;
|
||||
},
|
||||
|
||||
listeners() {
|
||||
const listeners = this.value?.spec?.listeners;
|
||||
|
||||
return listeners;
|
||||
},
|
||||
|
||||
listenerHeaders() {
|
||||
return [
|
||||
{
|
||||
name: 'name',
|
||||
label: this.t('tableHeaders.name'),
|
||||
value: 'name',
|
||||
sort: 'name:desc',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
label: this.t('tableHeaders.port'),
|
||||
value: 'port',
|
||||
sort: 'port:desc',
|
||||
},
|
||||
{
|
||||
name: 'protocol',
|
||||
label: this.t('tableHeaders.protocol'),
|
||||
value: 'protocol',
|
||||
sort: 'protocol:desc',
|
||||
},
|
||||
{
|
||||
name: 'backendPort',
|
||||
label: this.t('harvester.loadBalancer.listeners.backendPort.label'),
|
||||
value: 'backendPort',
|
||||
sort: 'backendPort:desc',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
backendServerSelectors() {
|
||||
return Object.keys(this.value.spec?.backendServerSelector || {}).map(key => ({
|
||||
key,
|
||||
value: this.value.spec.backendServerSelector[key],
|
||||
}));
|
||||
},
|
||||
|
||||
serviceSelectorInfoHeaders() {
|
||||
return [
|
||||
{
|
||||
...KEY,
|
||||
width: 200,
|
||||
},
|
||||
VALUE,
|
||||
];
|
||||
},
|
||||
|
||||
vmHeaders() {
|
||||
const filterNames = ['state', 'ip', 'node'];
|
||||
|
||||
return VM_HEADERS.filter(h => !filterNames.includes(h.name));
|
||||
},
|
||||
|
||||
vms() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vms = this.$store.getters[`${ inStore }/all`](HCI.VM).filter(vm => vm.metadata.namespace === this.value.metadata.namespace);
|
||||
const match = matching(vms, this.value?.spec?.backendServerSelector, 'spec.template.metadata.labels');
|
||||
|
||||
return match;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ResourceTabs
|
||||
v-model="value"
|
||||
:need-related="false"
|
||||
>
|
||||
<Tab
|
||||
v-if="value.spec.ipam === 'pool'"
|
||||
name="ipPool"
|
||||
label-key="harvester.loadBalancer.ipPool.label"
|
||||
:weight="99"
|
||||
>
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="ipPoolHeaders"
|
||||
:rows="ipPools"
|
||||
:row-actions="false"
|
||||
:table-actions="false"
|
||||
:search="false"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="value.spec.workloadType === 'vm'"
|
||||
name="vm"
|
||||
:label="t('harvester.loadBalancer.backendServers.label')"
|
||||
class="bordered-table"
|
||||
:weight="98"
|
||||
>
|
||||
<SortableTable
|
||||
:rows="vms"
|
||||
:headers="vmHeaders"
|
||||
key-field="id"
|
||||
:row-actions="false"
|
||||
:table-actions="false"
|
||||
:search="vms.length > 10 ? true : false"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="value.spec.workloadType === 'vm'"
|
||||
name="listeners"
|
||||
:label="t('harvester.loadBalancer.tabs.listeners')"
|
||||
class="bordered-table"
|
||||
:weight="89"
|
||||
>
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="listenerHeaders"
|
||||
:rows="listeners"
|
||||
:row-actions="false"
|
||||
:table-actions="false"
|
||||
:search="false"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="value.spec.workloadType === 'vm'"
|
||||
name="selector"
|
||||
:label="t('harvester.loadBalancer.tabs.backendServer')"
|
||||
class="bordered-table"
|
||||
:weight="79"
|
||||
>
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="serviceSelectorInfoHeaders"
|
||||
:rows="backendServerSelectors"
|
||||
:row-actions="false"
|
||||
:table-actions="false"
|
||||
:show-headers="true"
|
||||
:search="false"
|
||||
/>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</template>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script>
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { STATE, NAME, AGE } from '@shell/config/table-headers';
|
||||
import { matching } from '@shell/utils/selector';
|
||||
import { NODE } from '@shell/config/types';
|
||||
import { isEmpty } from '@shell/utils/object';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ResourceTabs,
|
||||
Tab,
|
||||
SortableTable,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.$store.dispatch('harvester/findAll', { type: NODE });
|
||||
|
||||
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
|
||||
|
||||
await allHash(hash);
|
||||
},
|
||||
|
||||
computed: {
|
||||
nodeHeaders() {
|
||||
return [
|
||||
STATE,
|
||||
NAME,
|
||||
{
|
||||
name: 'host-ip',
|
||||
labelKey: 'tableHeaders.hostIp',
|
||||
search: ['internalIp'],
|
||||
value: 'internalIp',
|
||||
sort: ['internalIp'],
|
||||
},
|
||||
AGE,
|
||||
];
|
||||
},
|
||||
|
||||
nodes() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
||||
const selector = this.value?.spec?.nodeSelector;
|
||||
|
||||
if (!isEmpty(selector)) {
|
||||
return matching(nodes, selector);
|
||||
} else {
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ResourceTabs
|
||||
v-model="value"
|
||||
:need-related="false"
|
||||
>
|
||||
<Tab
|
||||
name="node"
|
||||
label-key="harvester.vlanConfig.titles.host"
|
||||
:weight="99"
|
||||
>
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="nodeHeaders"
|
||||
:rows="nodes"
|
||||
:row-actions="false"
|
||||
:table-actions="false"
|
||||
:search="false"
|
||||
/>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</template>
|
||||
Reference in New Issue
Block a user