Add pkg/harvester components + shell portings - 1

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia
2024-10-23 17:00:46 +02:00
parent f8408469f7
commit 4f2688f6ab
265 changed files with 42316 additions and 82 deletions
@@ -0,0 +1,95 @@
<script>
import { HCI } from '../types';
import { SCHEMA } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import Banner from '@components/Banner/Banner.vue';
import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink';
import DeviceList from '../edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/DeviceList';
import { ADD_ONS } from '../config/harvester-map';
const schema = {
id: HCI.PCI_DEVICE,
type: SCHEMA,
attributes: {
kind: HCI.PCI_DEVICE,
namespaced: false
},
metadata: { name: HCI.PCI_DEVICE },
};
export default {
name: 'ListPciDevicePage',
components: {
Banner, DeviceList, Loading, MessageLink
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.hasSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.PCI_DEVICE);
this.hasAddonSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
if (this.hasSchema) {
try {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await allHash({
pcidevice: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_DEVICE }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
});
this.enabledPCI = hash.addons.find(addon => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
this.$store.dispatch('type-map/configureType', { match: HCI.PCI_DEVICE, isCreatable: this.enabledPCI });
} catch (e) {}
}
},
data() {
return {
enabledPCI: false,
hasSchema: false,
to: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.PCI_DEVICE_CONTROLLER }?mode=edit`
};
},
computed: {
schema() {
return schema;
},
rows() {
const inStore = this.$store.getters['currentProduct'].inStore;
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
return rows;
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="!hasAddonSchema">
<Banner color="warning">
{{ t('harvester.pci.noPCIPermission') }}
</Banner>
</div>
<DeviceList v-else-if="hasSchema && enabledPCI" :devices="rows" :schema="schema" />
<div v-else>
<Banner color="warning">
<MessageLink
:to="to"
prefix-label="harvester.pci.goSetting.prefix"
middle-label="harvester.pci.goSetting.middle"
suffix-label="harvester.pci.goSetting.suffix"
/>
</Banner>
</div>
</template>
@@ -0,0 +1,166 @@
<script>
import { HCI } from '../types';
import { ADD_ONS } from '../config/harvester-map';
import { NODE } from '@shell/config/types';
import { STATE, AGE, SIMPLE_NAME } from '@shell/config/table-headers';
import { allHash } from '@shell/utils/promise';
import Banner from '@components/Banner/Banner.vue';
import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink';
import ResourceTable from '@shell/components/ResourceTable';
export default {
name: 'ListSriovGpuDevices',
components: {
Banner,
Loading,
MessageLink,
ResourceTable,
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.SR_IOVGPU_DEVICE);
this.hasAddonSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
if (this.hasSchema) {
try {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await allHash({
sriovgpus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE }),
vGpuDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VGPU_DEVICE }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
});
this.hasPCIAddon = hash.addons.find(addon => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
this.hasSriovgpuAddon = hash.addons.find(addon => addon.name === ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER)?.spec?.enabled === true;
} catch (e) {}
}
},
data() {
const inStore = this.$store.getters['currentProduct'].inStore;
return {
hasAddonSchema: false,
hasPCIAddon: false,
hasSriovgpuAddon: false,
schema: null,
hasNode: this.$store.getters[`${ inStore }/schemaFor`](NODE),
toVGpuAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
toPciAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.PCI_DEVICE_CONTROLLER }?mode=edit`
};
},
computed: {
hasSchema() {
return !!this.schema;
},
rows() {
const inStore = this.$store.getters['currentProduct'].inStore;
const rows = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE);
return rows;
},
headers() {
const nodeCol = {
name: 'node',
label: 'Node',
value: 'realNodeName',
sort: ['realNodeName'],
formatter: 'CopyToClipboard',
labelKey: 'tableHeaders.node'
};
const cols = [
STATE,
SIMPLE_NAME,
{
name: 'address',
label: 'Address',
value: 'spec.address',
sort: ['spec.address']
},
{
name: 'vfAddresses',
label: 'VF Addresses',
labelKey: 'harvester.sriovgpu.vfAddresses',
sort: ['status.vfAddresses'],
value: 'status.vfAddresses',
formatter: 'HarvesterVFAddress',
align: 'center',
dashIfEmpty: true,
},
{
name: 'vGpuDevices',
label: 'vGPU Devices',
labelKey: 'harvester.sriovgpu.vGpuDevices',
sort: ['status.vGPUDevices'],
value: 'status.vGPUDevices',
formatter: 'HarvesterVGpuDevices',
align: 'center',
dashIfEmpty: true,
},
{
...AGE,
sort: 'metadata.creationTimestamp:desc',
}
];
if (this.hasNode) {
cols.splice(-1, 0, nodeCol);
}
return cols;
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="!hasAddonSchema">
<Banner color="warning">
{{ t('harvester.sriovgpu.noPermission') }}
</Banner>
</div>
<div v-else-if="!hasSriovgpuAddon || !hasPCIAddon">
<Banner
v-if="!hasSriovgpuAddon"
color="warning"
>
<MessageLink
:to="toVGpuAddon"
prefix-label="harvester.sriovgpu.goSetting.prefix"
middle-label="harvester.sriovgpu.goSetting.middle"
suffix-label="harvester.sriovgpu.goSetting.suffix"
/>
</Banner>
<Banner
v-if="!hasPCIAddon"
color="warning"
>
<MessageLink
:to="toPciAddon"
prefix-label="harvester.pci.goSetting.prefix"
middle-label="harvester.pci.goSetting.middle"
suffix-label="harvester.pci.goSetting.suffix"
/>
</Banner>
</div>
<ResourceTable
v-else-if="hasSchema"
v-bind="$attrs"
:groupable="false"
:namespaced="false"
:headers="headers"
:schema="schema"
:rows="rows"
key-field="_key"
/>
</template>
@@ -0,0 +1,88 @@
<script>
import { STATE, AGE, SIMPLE_NAME } from '@shell/config/table-headers';
import { NODE } from '@shell/config/types';
import ResourceTable from '@shell/components/ResourceTable';
export default {
name: 'ListHarvesterSRIOV',
components: { ResourceTable },
props: {
schema: {
type: Object,
required: true,
},
rows: {
type: Array,
required: true,
},
},
data() {
const inStore = this.$store.getters['currentProduct'].inStore;
return { hasNode: this.$store.getters[`${ inStore }/schemaFor`](NODE) };
},
computed: {
headers() {
const nodeCol = {
name: 'node',
label: 'Node',
value: 'realNodeName',
sort: ['realNodeName'],
formatter: 'CopyToClipboard',
labelKey: 'tableHeaders.node'
};
const cols = [
STATE,
SIMPLE_NAME,
{
name: 'numVFs',
label: 'Num VFs',
sort: ['numVFs'],
value: 'numVFs',
formatter: 'HarvesterVFsNum',
align: 'center',
labelKey: 'harvester.sriov.numVFs',
dashIfEmpty: true,
},
{
name: 'vfAddresses',
label: 'VF Addresses',
labelKey: 'harvester.sriov.vfAddresses',
sort: ['status.vfAddresses'],
value: 'status.vfAddresses',
formatter: 'HarvesterVFAddress',
align: 'center',
dashIfEmpty: true,
},
{
...AGE,
sort: 'metadata.creationTimestamp:desc',
}
];
if (this.hasNode) {
cols.splice(-1, 0, nodeCol);
}
return cols;
},
}
};
</script>
<template>
<ResourceTable
v-bind="$attrs"
:groupable="false"
:namespaced="false"
:headers="headers"
:schema="schema"
:rows="rows"
key-field="_key"
/>
</template>
@@ -0,0 +1,112 @@
<script>
import { HCI } from '../types';
import { allHash } from '@shell/utils/promise';
import Banner from '@components/Banner/Banner.vue';
import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink';
import VGpuDeviceList from '../edit/kubevirt.io.virtualmachine/VirtualMachineVGpuDevices/VGpuDeviceList';
import { ADD_ONS } from '../config/harvester-map';
export default {
name: 'ListVGpuDevices',
components: {
Banner,
Loading,
MessageLink,
VGpuDeviceList
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VGPU_DEVICE);
this.hasAddonSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
if (this.hasSchema) {
try {
const hash = await allHash({
vGpuDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VGPU_DEVICE }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
});
this.hasPCIAddon = hash.addons.find(addon => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
this.hasSriovgpuAddon = hash.addons.find(addon => addon.name === ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER)?.spec?.enabled === true;
this.hasSRIOVGPUSchema = !!this.$store.getters[`${ inStore }/schemaFor`](HCI.SR_IOVGPU_DEVICE);
if (this.hasSRIOVGPUSchema) {
await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE });
}
} catch (e) {}
}
},
data() {
return {
hasAddonSchema: false,
hasPCIAddon: false,
hasSriovgpuAddon: false,
hasSRIOVGPUSchema: false,
schema: null,
toVGpuAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
toPciAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.PCI_DEVICE_CONTROLLER }?mode=edit`
};
},
computed: {
hasSchema() {
return !!this.schema;
},
rows() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.VGPU_DEVICE) || [];
const srioVGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
if (this.hasSRIOVGPUSchema) {
return vGpuDevices.filter(device => !!srioVGpuDevices.find(s => s.isEnabled && s.spec?.nodeName === device.spec?.nodeName));
}
return vGpuDevices;
}
},
typeDisplay() {
return this.t('harvester.vgpu.label');
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="!hasAddonSchema">
<Banner color="warning">
{{ t('harvester.vgpu.noPermission') }}
</Banner>
</div>
<div v-else-if="!hasSriovgpuAddon || !hasPCIAddon">
<Banner
v-if="!hasSriovgpuAddon"
color="warning"
>
<MessageLink
:to="toVGpuAddon"
prefix-label="harvester.vgpu.goSetting.prefix"
middle-label="harvester.vgpu.goSetting.middle"
suffix-label="harvester.vgpu.goSetting.suffix"
/>
</Banner>
<Banner
v-if="!hasPCIAddon"
color="warning"
>
<MessageLink
:to="toPciAddon"
prefix-label="harvester.pci.goSetting.prefix"
middle-label="harvester.pci.goSetting.middle"
suffix-label="harvester.pci.goSetting.suffix"
/>
</Banner>
</div>
<VGpuDeviceList v-else-if="hasSchema" :devices="rows" :schema="schema" />
</template>
@@ -0,0 +1,96 @@
<script>
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
import { STATE, NAME } from '@shell/config/table-headers';
export default {
name: 'ListHarvesterAddons',
components: {
ResourceTable,
Loading,
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }) });
},
computed: {
rows() {
const inStore = this.$store.getters['currentProduct'].inStore;
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
return addons;
},
headers() {
return [
STATE,
NAME,
{
name: 'description',
labelKey: 'tableHeaders.description',
value: 'metadata.name',
align: 'left',
sort: ['status.description'],
formatter: 'Translate',
formatterOpts: { prefix: 'harvester.addons.descriptions' },
},
];
},
schema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
},
},
};
</script>
<template>
<div>
<Loading v-if="$fetchState.pending" />
<ResourceTable
:rows="rows"
:groupable="false"
:namespaced="false"
:schema="schema"
:headers="headers"
>
<template slot="cell:name" slot-scope="scope">
<div class="cell-name">
<LinkDetail v-model="scope.row.displayName" :row="scope.row" />
<a
v-if="scope.row.metadata.name === 'rancher-vcluster' && scope.row.spec.enabled"
v-tooltip="t('harvester.addons.rancherVcluster.accessRancher')"
class="ml-5"
rel="nofollow noopener noreferrer"
target="_blank"
:href="scope.row.rancherHostname"
>
<i class="icon icon-external-link" />
</a>
</div>
</template>
</ResourceTable>
</div>
</template>
<style lang="scss" scoped>
::v-deep .sortable-table TD .badge-state {
max-width: 250px;
text-overflow: clip;
}
::v-deep .cell-name {
white-space: nowrap;
}
</style>
@@ -0,0 +1,81 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { CONFIG_MAP, SCHEMA } from '@shell/config/types';
import { HCI } from '../types';
import { NAME, AGE, NAMESPACE } from '@shell/config/table-headers';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
const schema = {
id: HCI.CLOUD_TEMPLATE,
type: SCHEMA,
attributes: {
kind: HCI.CLOUD_TEMPLATE,
namespaced: true
},
metadata: { name: HCI.CLOUD_TEMPLATE },
};
export default {
name: 'HarvesterListCloudTemplate',
components: { ResourceTable, Loading },
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: CONFIG_MAP });
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](CONFIG_MAP);
if (!configSchema?.collectionMethods.find(x => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.CLOUD_TEMPLATE, isCreatable: false });
}
},
data() {
return { rows: [] };
},
computed: {
headers() {
return [
NAME,
NAMESPACE,
{
name: 'type',
labelKey: 'tableHeaders.type',
value: 'metadata.labels',
formatter: 'CloudInitType',
},
AGE
];
},
filteredRows() {
return this.rows.filter(r => !!r.metadata?.labels?.[HCI_ANNOTATIONS.CLOUD_INIT]);
},
schema() {
return schema;
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:headers="headers"
:groupable="true"
:schema="schema"
:rows="filteredRows"
key-field="_key"
v-on="$listeners"
/>
</template>
@@ -15,6 +15,7 @@ import { REASON } from '@shell/config/table-headers';
import {
EVENT, METRIC, NODE, SERVICE, PVC, LONGHORN, POD, COUNT, NETWORK_ATTACHMENT
} from '@shell/config/types';
import { HCI } from '../types';
import ResourceSummary, { resourceCounts, colorToCountName } from '@shell/components/ResourceSummary';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import HardwareResourceGauge from '@shell/components/HardwareResourceGauge';
@@ -24,8 +25,7 @@ import DashboardMetrics from '@shell/components/DashboardMetrics';
import metricPoller from '@shell/mixins/metric-poller';
import { allDashboardsExist } from '@shell/utils/grafana';
import { isEmpty } from '@shell/utils/object';
import { HCI } from '../types';
import HarvesterUpgrade from '../components/HarvesterUpgrade.vue';
import HarvesterUpgrade from '../components/HarvesterUpgrade';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
dayjs.extend(utc);
@@ -287,8 +287,8 @@ export default {
let errorCount = 0;
(this.nodes || []).map((node) => {
total += node.diskStatusCount?.total;
errorCount += node.diskStatusCount?.errorCount;
total += node.diskStatusCount.total;
errorCount += node.diskStatusCount.errorCount;
});
out[resource.type] = {
+208
View File
@@ -0,0 +1,208 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { STATE, NAME, AGE } from '@shell/config/table-headers';
import {
CAPI, METRIC, NODE, SCHEMA, LONGHORN, POD
} from '@shell/config/types';
import { HCI } from '../types';
import { allHash } from '@shell/utils/promise';
import metricPoller from '@shell/mixins/metric-poller';
const schema = {
id: HCI.HOST,
type: SCHEMA,
attributes: {
kind: HCI.HOST,
namespaced: true
},
metadata: { name: HCI.HOST },
};
export default {
name: 'HarvesterListHost',
components: {
ResourceTable,
Loading,
},
mixins: [metricPoller],
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
};
if (this.$store.getters[`${ inStore }/schemaFor`](METRIC.NODE)) {
_hash.metric = this.$store.dispatch(`${ inStore }/findAll`, { type: METRIC.NODE });
} else {
this.hasMetricSchema = false;
}
if (this.$store.getters[`${ inStore }/schemaFor`](LONGHORN.NODES)) {
_hash.longhornNodes = this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES });
} else {
this.hasLonghornSchema = false;
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.BLOCK_DEVICE)) {
_hash.blockDevices = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BLOCK_DEVICE });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.INVENTORY)) {
_hash.inventories = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.INVENTORY });
}
if (this.$store.getters[`${ inStore }/schemaFor`](CAPI.MACHINE)) {
_hash.machines = this.$store.dispatch(`${ inStore }/findAll`, { type: CAPI.MACHINE });
}
const hash = await allHash(_hash);
this.rows = hash.nodes;
},
data() {
return {
rows: [],
hasMetricSchema: true,
hasLonghornSchema: true,
};
},
computed: {
headers() {
const out = [
{
...STATE,
formatter: 'StateWithPopover',
},
{
...NAME,
width: 130,
},
{
name: 'host-ip',
labelKey: 'tableHeaders.hostIp',
search: ['internalIp'],
value: 'internalIp',
formatter: 'CopyToClipboard',
sort: ['internalIp'],
},
];
if (this.hasMetricSchema) {
const metricCol = [
{
name: 'cpu',
labelKey: 'node.detail.glance.consumptionGauge.cpu',
value: 'id',
formatter: 'HarvesterCPUUsed',
formatterOpts: { showUsed: true },
},
{
name: 'memory',
labelKey: 'node.detail.glance.consumptionGauge.memory',
value: 'id',
formatter: 'HarvesterMemoryUsed',
formatterOpts: { showUsed: true },
},
];
out.splice(-1, 0, ...metricCol);
}
if (this.hasLonghornSchema) {
const storageHeader = {
name: 'storage',
labelKey: 'tableHeaders.storage',
value: 'id',
formatter: 'HarvesterStorageUsed',
formatterOpts: { showReserved: true },
};
out.splice(-1, 0, storageHeader);
}
if (this.hasLonghornSchema) {
out.push({
name: 'diskState',
labelKey: 'tableHeaders.diskState',
value: 'diskState',
formatter: 'HarvesterDiskState',
width: 130,
});
}
out.push(AGE);
out.push({
name: 'console',
label: ' ',
align: 'right',
width: 65,
});
return out;
},
schema() {
return schema;
}
},
methods: {
async loadMetrics() {
const schema = this.$store.getters['harvester/schemaFor'](METRIC.NODE);
if (schema) {
await this.$store.dispatch('harvester/findAll', {
type: METRIC.NODE,
opt: { force: true }
});
this.$forceUpdate();
}
},
goto(row) {
window.open(row.consoleUrl, '_blank');
}
},
typeDisplay() {
const { params:{ resource: type } } = this.$route;
let paramSchema = schema;
if (type !== schema.id) {
paramSchema = this.$store.getters['harvester/schemaFor'](type);
}
return this.$store.getters['type-map/labelFor'](paramSchema, 99);
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<ResourceTable
v-bind="$attrs"
:schema="schema"
:groupable="false"
:headers="headers"
:rows="[...rows]"
:namespaced="false"
key-field="_key"
v-on="$listeners"
>
<template #cell:console="{row}">
<button type="button" class="btn btn-sm role-primary" :disabled="!row.consoleUrl" @click="goto(row)">
{{ t('harvester.host.console') }}
</button>
</template>
</ResourceTable>
</div>
</template>
@@ -0,0 +1,94 @@
<script>
import { allHash } from '@shell/utils/promise';
import ResourceTable from '@shell/components/ResourceTable';
import Banner from '@components/Banner/Banner.vue';
import MessageLink from '@shell/components/MessageLink';
import Loading from '@shell/components/Loading';
import { SCHEMA, LOGGING } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.CLUSTER_FLOW,
type: SCHEMA,
attributes: {
kind: HCI.CLUSTER_FLOW,
namespaced: true
},
metadata: { name: HCI.CLUSTER_FLOW },
};
const LOGGING_ID = 'cattle-logging-system/rancher-logging';
export default {
name: 'ListApps',
components: {
Loading, ResourceTable, Banner, MessageLink
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {};
this.listSchema = this.$store.getters[`${ inStore }/schemaFor`](LOGGING.CLUSTER_FLOW);
if (this.listSchema) {
_hash.clusteroutput = this.$store.dispatch(`${ inStore }/findAll`, { type: LOGGING.CLUSTER_OUTPUT });
_hash.rows = this.$store.dispatch(`${ inStore }/findAll`, { type: LOGGING.CLUSTER_FLOW });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS )) {
_hash.loggingAddon = this.$store.dispatch(`${ inStore }/find`, { type: HCI.ADD_ONS, id: LOGGING_ID });
}
const hash = await allHash(_hash);
this.rows = hash.rows;
this.loggingAddon = hash.loggingAddon;
this.$store.dispatch('type-map/configureType', { match: HCI.CLUSTER_FLOW, isCreatable: this.listSchema && this.listSchema?.collectionMethods.find(x => x.toLowerCase() === 'post') });
},
data() {
return {
rows: [], listSchema: null, loggingAddon: null
};
},
computed: {
schema() {
return schema;
},
to() {
return `${ HCI.ADD_ONS }/cattle-logging-system/rancher-logging?mode=edit#basic`;
},
loggingEnabled() {
return this.loggingAddon?.spec?.enabled;
},
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="listSchema">
<Banner v-if="loggingEnabled === false" color="info">
<MessageLink
:to="to"
prefix-label="harvester.logging.diabledTips.prefix"
middle-label="harvester.logging.diabledTips.middle"
suffix-label="harvester.logging.diabledTips.suffix"
/>
</Banner>
<ResourceTable :schema="schema" :rows="rows" :ignore-filter="true" :groupable="false" />
</div>
<Banner v-else color="warning">
{{ t('harvester.generic.noSchema', {schema: schema.id}) }}
</Banner>
</template>
@@ -0,0 +1,93 @@
<script>
import { allHash } from '@shell/utils/promise';
import ResourceTable from '@shell/components/ResourceTable';
import Banner from '@components/Banner/Banner.vue';
import MessageLink from '@shell/components/MessageLink';
import Loading from '@shell/components/Loading';
import { SCHEMA, LOGGING } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.CLUSTER_OUTPUT,
type: SCHEMA,
attributes: {
kind: HCI.CLUSTER_OUTPUT,
namespaced: true
},
metadata: { name: HCI.CLUSTER_OUTPUT },
};
const LOGGING_ID = 'cattle-logging-system/rancher-logging';
export default {
name: 'ListApps',
components: {
Loading, ResourceTable, Banner, MessageLink
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {};
this.listSchema = this.$store.getters[`${ inStore }/schemaFor`](LOGGING.CLUSTER_OUTPUT);
if (this.listSchema) {
_hash.rows = this.$store.dispatch(`${ inStore }/findAll`, { type: LOGGING.CLUSTER_OUTPUT });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS )) {
_hash.loggingAddon = this.$store.dispatch(`${ inStore }/find`, { type: HCI.ADD_ONS, id: LOGGING_ID });
}
const hash = await allHash(_hash);
this.rows = hash.rows;
this.loggingAddon = hash.loggingAddon;
this.$store.dispatch('type-map/configureType', { match: HCI.CLUSTER_OUTPUT, isCreatable: this.listSchema && this.listSchema?.collectionMethods.find(x => x.toLowerCase() === 'post') });
},
data() {
return {
rows: [], listSchema: null, loggingAddon: null
};
},
computed: {
schema() {
return schema;
},
to() {
return `${ HCI.ADD_ONS }/cattle-logging-system/rancher-logging?mode=edit#basic`;
},
loggingEnabled() {
return this.loggingAddon?.spec?.enabled;
},
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="listSchema">
<Banner v-if="loggingEnabled === false" color="info">
<MessageLink
:to="to"
prefix-label="harvester.logging.diabledTips.prefix"
middle-label="harvester.logging.diabledTips.middle"
suffix-label="harvester.logging.diabledTips.suffix"
/>
</Banner>
<ResourceTable :schema="schema" :rows="rows" :ignore-filter="true" :groupable="false" />
</div>
<Banner v-else color="warning">
{{ t('harvester.generic.noSchema', {schema: schema.id}) }}
</Banner>
</template>
@@ -0,0 +1,99 @@
<script>
import { allHash } from '@shell/utils/promise';
import ResourceTable from '@shell/components/ResourceTable';
import Banner from '@components/Banner/Banner.vue';
import MessageLink from '@shell/components/MessageLink';
import Loading from '@shell/components/Loading';
import { SCHEMA, LOGGING } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.FLOW,
type: SCHEMA,
attributes: {
kind: HCI.FLOW,
namespaced: true
},
metadata: { name: HCI.FLOW },
};
const LOGGING_ID = 'cattle-logging-system/rancher-logging';
export default {
name: 'ListApps',
components: {
Loading, ResourceTable, Banner, MessageLink
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {};
this.listSchema = this.$store.getters[`${ inStore }/schemaFor`](LOGGING.FLOW);
this.clusteroutputSchema = this.$store.getters[`${ inStore }/schemaFor`](LOGGING.CLUSTER_OUTPUT);
if (this.listSchema) {
_hash.output = this.$store.dispatch(`${ inStore }/findAll`, { type: LOGGING.OUTPUT });
_hash.rows = this.$store.dispatch(`${ inStore }/findAll`, { type: LOGGING.FLOW });
}
if (this.clusteroutputSchema) {
_hash.clusteroutput = this.$store.dispatch(`${ inStore }/findAll`, { type: LOGGING.CLUSTER_OUTPUT });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS )) {
_hash.loggingAddon = this.$store.dispatch(`${ inStore }/find`, { type: HCI.ADD_ONS, id: LOGGING_ID });
}
const hash = await allHash(_hash);
this.rows = hash.rows;
this.loggingAddon = hash.loggingAddon;
this.$store.dispatch('type-map/configureType', { match: HCI.FLOW, isCreatable: this.listSchema && this.listSchema?.collectionMethods.find(x => x.toLowerCase() === 'post') });
},
data() {
return {
rows: [], listSchema: null, loggingAddon: null
};
},
computed: {
schema() {
return schema;
},
to() {
return `${ HCI.ADD_ONS }/cattle-logging-system/rancher-logging?mode=edit#basic`;
},
loggingEnabled() {
return this.loggingAddon?.spec?.enabled;
},
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="listSchema">
<Banner v-if="loggingEnabled === false" color="info">
<MessageLink
:to="to"
prefix-label="harvester.logging.diabledTips.prefix"
middle-label="harvester.logging.diabledTips.middle"
suffix-label="harvester.logging.diabledTips.suffix"
/>
</Banner>
<ResourceTable :schema="schema" :rows="rows" :ignore-filter="true" :groupable="false" />
</div>
<Banner v-else color="warning">
{{ t('harvester.generic.noSchema', {schema: schema.id}) }}
</Banner>
</template>
@@ -0,0 +1,93 @@
<script>
import { allHash } from '@shell/utils/promise';
import ResourceTable from '@shell/components/ResourceTable';
import Banner from '@components/Banner/Banner.vue';
import MessageLink from '@shell/components/MessageLink';
import Loading from '@shell/components/Loading';
import { SCHEMA, LOGGING } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.OUTPUT,
type: SCHEMA,
attributes: {
kind: HCI.OUTPUT,
namespaced: true
},
metadata: { name: HCI.OUTPUT },
};
const LOGGING_ID = 'cattle-logging-system/rancher-logging';
export default {
name: 'ListApps',
components: {
Loading, ResourceTable, Banner, MessageLink
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {};
this.listSchema = this.$store.getters[`${ inStore }/schemaFor`](LOGGING.OUTPUT);
if (this.listSchema) {
_hash.rows = this.$store.dispatch(`${ inStore }/findAll`, { type: LOGGING.OUTPUT });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS )) {
_hash.loggingAddon = this.$store.dispatch(`${ inStore }/find`, { type: HCI.ADD_ONS, id: LOGGING_ID });
}
const hash = await allHash(_hash);
this.rows = hash.rows;
this.loggingAddon = hash.loggingAddon;
this.$store.dispatch('type-map/configureType', { match: HCI.OUTPUT, isCreatable: this.listSchema && this.listSchema?.collectionMethods.find(x => x.toLowerCase() === 'post') });
},
data() {
return {
rows: [], listSchema: null, loggingAddon: null
};
},
computed: {
schema() {
return schema;
},
to() {
return `${ HCI.ADD_ONS }/cattle-logging-system/rancher-logging?mode=edit#basic`;
},
loggingEnabled() {
return this.loggingAddon?.spec?.enabled;
},
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="listSchema">
<Banner v-if="loggingEnabled === false" color="info">
<MessageLink
:to="to"
prefix-label="harvester.logging.diabledTips.prefix"
middle-label="harvester.logging.diabledTips.middle"
suffix-label="harvester.logging.diabledTips.suffix"
/>
</Banner>
<ResourceTable :schema="schema" :rows="rows" :ignore-filter="true" :groupable="false" />
</div>
<Banner v-else color="warning">
{{ t('harvester.generic.noSchema', {schema: schema.id}) }}
</Banner>
</template>
@@ -0,0 +1,136 @@
<script>
import jsyaml from 'js-yaml';
import { allHash } from '@shell/utils/promise';
import { Banner } from '@components/Banner';
import MessageLink from '@shell/components/MessageLink';
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { SCHEMA, MONITORING } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.ALERTMANAGERCONFIG,
type: SCHEMA,
attributes: {
kind: HCI.ALERTMANAGERCONFIG,
namespaced: true
},
metadata: { name: HCI.ALERTMANAGERCONFIG },
};
const MONITORING_ID = 'cattle-monitoring-system/rancher-monitoring';
export default {
name: 'ListAlertManagerConfigs',
components: {
Banner, Loading, ResourceTable, MessageLink
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = { rows: this.$store.dispatch(`${ inStore }/findAll`, { type: MONITORING.ALERTMANAGERCONFIG }) };
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS )) {
_hash.monitoring = this.$store.dispatch(`${ inStore }/find`, { type: HCI.ADD_ONS, id: MONITORING_ID });
}
const hash = await allHash(_hash);
this.rows = hash.rows;
this.monitoringAddon = hash.monitoring;
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](MONITORING.ALERTMANAGERCONFIG);
this.$store.dispatch('type-map/configureType', { match: HCI.ALERTMANAGERCONFIG, isCreatable: configSchema?.collectionMethods.find(x => x.toLowerCase() === 'post') });
},
data() {
return { rows: null, monitoringAddon: null };
},
computed: {
schema() {
return schema;
},
to() {
return `${ HCI.ADD_ONS }/cattle-monitoring-system/rancher-monitoring?mode=edit#alertmanager`;
},
monitoringEnabled() {
return this.monitoringAddon?.spec?.enabled;
},
alertingEnabled() {
const valueJson = jsyaml.load(this.monitoringAddon?.spec?.valuesContent);
return valueJson?.alertmanager?.enabled;
},
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Banner v-if="monitoringEnabled === false" color="info">
<MessageLink
:to="to"
prefix-label="harvester.monitoring.alertmanagerConfig.diabledMonitoringTips.prefix"
middle-label="harvester.monitoring.alertmanagerConfig.diabledMonitoringTips.middle"
suffix-label="harvester.monitoring.alertmanagerConfig.diabledMonitoringTips.suffix"
/>
</Banner>
<Banner v-if="alertingEnabled === false" color="info">
<MessageLink
:to="to"
prefix-label="harvester.monitoring.alertmanagerConfig.diabledAlertingTips.prefix"
middle-label="harvester.monitoring.alertmanagerConfig.diabledAlertingTips.middle"
suffix-label="harvester.monitoring.alertmanagerConfig.diabledAlertingTips.suffix"
/>
</Banner>
<Banner color="info">
{{ t('monitoring.alertmanagerConfig.description') }}
</Banner>
<ResourceTable
v-bind="$attrs"
:groupable="true"
:schema="schema"
:rows="rows"
key-field="_key"
v-on="$listeners"
/>
</div>
</template>
<style lang="scss" scoped>
.empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-top: 6em;
min-height: 100%;
}
i {
font-size: 10em;
opacity: 50%;
margin: 0;
}
h2 {
margin: 0;
}
h3 {
margin-top: 2em;
}
</style>
@@ -0,0 +1,208 @@
<script>
import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
import { HCI } from '../types';
import { allHash } from '@shell/utils/promise';
const schema = {
id: HCI.NETWORK_ATTACHMENT,
type: SCHEMA,
attributes: {
kind: HCI.NETWORK_ATTACHMENT,
namespaced: true
},
metadata: { name: HCI.NETWORK_ATTACHMENT },
};
export const NETWORK_HEADERS = [
NAME,
NAMESPACE,
{
name: 'type',
value: 'vlanType',
sort: 'spec.config',
labelKey: 'tableHeaders.networkType'
},
{
name: 'vlan',
value: 'vlanId',
sort: 'spec.config',
labelKey: 'tableHeaders.networkVlan'
},
{
name: 'connectivity',
value: 'connectivity',
labelKey: 'tableHeaders.routeConnectivity',
formatter: 'BadgeStateFormatter',
formatterOpts: { arbitrary: true },
width: 130,
},
AGE
];
export default {
name: 'HarvesterListNetworks',
components: {
ResourceTable,
Banner,
Loading,
BadgeState,
},
async fetch() {
const currentCluster = this.$store.getters['currentCluster'];
const storeName = currentCluster.isHarvester ? 'harvester' : 'cluster';
const _hash = { rows: this.$store.dispatch(`${ storeName }/findAll`, { type: NETWORK_ATTACHMENT }) };
if (this.$store.getters[`${ storeName }/schemaFor`](HCI.NODE_NETWORK)) {
_hash.hostNetworks = this.$store.dispatch(`${ storeName }/findAll`, { type: HCI.NODE_NETWORK });
}
if (this.$store.getters[`${ storeName }/schemaFor`](HCI.CLUSTER_NETWORK)) {
_hash.clusterNetworks = this.$store.dispatch(`${ storeName }/findAll`, { type: HCI.CLUSTER_NETWORK });
}
const hash = await allHash(_hash);
this.rows = hash.rows;
this.hostNetworks = hash.hostNetworks || [];
},
data() {
return {
hash: {},
rows: [],
hosts: [],
hostNetworks: [],
};
},
computed: {
headers() {
return [
STATE,
NAME,
NAMESPACE,
{
name: 'type',
value: 'vlanType',
sort: 'spec.config',
labelKey: 'tableHeaders.networkType'
},
{
name: 'clusterNetwork',
value: 'clusterNetwork',
sort: 'clusterNetwork',
labelKey: 'harvester.network.clusterNetwork.label'
},
{
name: 'vlan',
value: 'vlanId',
sort: 'spec.config',
labelKey: 'tableHeaders.networkVlan'
},
{
name: 'connectivity',
value: 'connectivity',
labelKey: 'tableHeaders.routeConnectivity',
formatter: 'NetworkRouteConnectivity',
formatterOpts: { arbitrary: true },
width: 130,
},
AGE
];
},
schema() {
return schema;
},
abnormalNetwork() {
const notReadyCrd = this.hostNetworks.filter( (O) => !O.isReady);
return notReadyCrd.map( (O) => O.linkMessage);
},
filterRows() {
return this.rows.filter((row) => {
return !row.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_NETWORK];
});
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<template v-if="abnormalNetwork.length">
<Banner
v-for="item in abnormalNetwork"
:key="item.name"
color="error"
>
<router-link :to="item.to">
{{ item.name }}:
</router-link>
{{ item.message }}
</Banner>
</template>
<ResourceTable
v-bind="$attrs"
:headers="headers"
default-sort-by="age"
:schema="schema"
:groupable="true"
:rows="filterRows"
key-field="_key"
v-on="$listeners"
>
<template #cell:state="{row}">
<div class="state">
<BadgeState
:row="row"
/>
<v-popover
v-if="row.clusterNetworkErrorMessage"
trigger="hover"
offset="16"
>
<span class="tooltip-target">
<i class="icon icon-warning icon-lg text-warning" />
</span>
<template slot="popover">
<p>
{{ row.clusterNetworkErrorMessage }}
</p>
</template>
</v-popover>
</div>
</template>
</ResourceTable>
</div>
</template>
<style lang="scss" scoped>
.state {
display: flex;
justify-content: space-between;
.icon-warning {
margin-top: 2px;
}
}
</style>
@@ -0,0 +1,60 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { SECRET, SCHEMA } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.SECRET,
type: SCHEMA,
attributes: {
kind: HCI.SECRET,
namespaced: true
},
metadata: { name: HCI.SECRET },
};
export default {
name: 'HarvesterSecret',
components: { ResourceTable, Loading },
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](SECRET);
if (!configSchema?.collectionMethods.find(x => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.SECRET, isCreatable: false });
}
},
data() {
return { rows: [] };
},
computed: {
schema() {
return schema;
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:groupable="true"
:schema="schema"
:rows="rows"
key-field="_key"
v-on="$listeners"
/>
</template>
@@ -0,0 +1,176 @@
<script>
import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading';
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
import { MANAGEMENT } from '@shell/config/types';
import { HCI } from '../types';
import { allHash } from '@shell/utils/promise';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING } from '../config/settings';
import Tabbed from '@shell/components/Tabbed/index.vue';
import Tab from '@shell/components/Tabbed/Tab.vue';
import Settings from '@pkg/harvester/components/SettingList.vue';
export default {
components: {
Banner,
Loading,
Tabbed,
Tab,
Settings,
},
async fetch() {
let isDev;
try {
isDev = this.$store.getters['prefs/get'](VIEW_IN_API);
} catch {
isDev = this.$store.getters['prefs/get'](DEV);
}
const isSingleProduct = !!this.$store.getters['isSingleProduct'];
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = { harvesterSettings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) };
if (isSingleProduct) {
hash.settings = this.$store.dispatch('management/findAll', { type: MANAGEMENT.SETTING });
}
const rows = await allHash(hash);
let allRows = [];
allRows.push(...rows.harvesterSettings);
if (isSingleProduct) {
allRows = [...rows.settings, ...allRows];
}
// Map settings from array to object keyed by id
const settingsMap = allRows.reduce((res, s) => {
res[s.id] = s;
return res;
}, {});
const initSettings = [];
let SETTINGS = HCI_ALLOWED_SETTINGS;
if (this.isStandaloneHarvester) {
SETTINGS = {
...SETTINGS,
...HCI_SINGLE_CLUSTER_ALLOWED_SETTING,
};
}
Object.keys(SETTINGS).forEach((setting) => {
if (!settingsMap[setting]) {
return;
}
const realSetting = SETTINGS[setting]?.alias || setting;
const s = {
...SETTINGS[setting],
id: realSetting,
data: settingsMap[setting],
};
s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline' || s.customFormatter === 'json' || s.data.customFormatter === 'json');
s.hasActions = !s.readOnly || isDev;
initSettings.push(s);
});
this.initSettings = initSettings.sort((a, b) => {
if (a.id < b.id) {
return -1;
}
if (a.id > b.id) {
return 1;
}
return 0;
});
},
data() {
return { initSettings: [] };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
...mapGetters(['isStandaloneHarvester']),
settings() {
return this.initSettings.map((setting) => {
const s = setting;
const isHarvester = s.data?.type?.includes('harvesterhci');
if (s.kind === 'json') {
try {
s.json = JSON.stringify(JSON.parse(s.data.value || s.data.default || '{}'), null, 2);
} catch (e) {
console.error(`${ s.data.id }: wrong format`); // eslint-disable-line no-console
s.json = {};
}
} else if (s.kind === 'enum') {
const v = s.data.value || s.data.default;
s.enum = isHarvester ? `advancedSettings.enum.harv-${ s.id }.${ v }` : `advancedSettings.enum.${ s.id }.${ v }`;
} else if (s.kind === 'custom') {
s.custom = s.data.customValue;
}
return {
...s,
description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`,
customized: (!s.readOnly && s.data.value && s.data.value !== s.data.default) || s.data.hasCustomized
};
});
}
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Banner color="warning" class="settings-banner">
<div>
{{ t('advancedSettings.subtext') }}
</div>
</Banner>
<Tabbed
class="mt-30"
>
<Tab
name="advanced"
:label="t('harvester.setting.tabs.advanced')"
:weight="99"
>
<Settings
:settings="settings"
category="advanced"
/>
</Tab>
<Tab
name="ui"
:label="t('harvester.setting.tabs.ui')"
:weight="89"
>
<Settings
:settings="settings"
category="ui"
/>
</Tab>
</Tabbed>
</div>
</template>
<style lang='scss' scoped>
.settings-banner {
margin-top: 0;
}
</style>
@@ -0,0 +1,88 @@
<script>
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import { allHash } from '@shell/utils/promise';
import { SCHEMA, STORAGE_CLASS } from '@shell/config/types';
import { HCI } from '../types';
import {
STATE, AGE, NAME, STORAGE_CLASS_PROVISIONER, STORAGE_CLASS_DEFAULT
} from '@shell/config/table-headers';
const schema = {
id: HCI.STORAGE,
type: SCHEMA,
attributes: {
kind: HCI.STORAGE,
namespaced: false
},
metadata: { name: HCI.STORAGE },
};
export default {
name: 'ListHarvesterStorages',
components: {
ResourceTable,
Loading,
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }) });
const storageSchema = this.$store.getters[`${ inStore }/schemaFor`](STORAGE_CLASS);
if ( storageSchema && !storageSchema?.collectionMethods.find(x => ['blocked-post', 'post'].includes(x.toLowerCase())) ) {
this.$store.dispatch('type-map/configureType', { match: HCI.STORAGE, isCreatable: false });
}
},
data() {
return { schema };
},
computed: {
rows() {
const inStore = this.$store.getters['currentProduct'].inStore;
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
return storages.filter(s => !s.parameters?.backingImage);
},
headers() {
return [
STATE,
NAME,
STORAGE_CLASS_PROVISIONER,
STORAGE_CLASS_DEFAULT,
{
name: 'numberOfReplicas',
labelKey: 'harvester.storage.numberOfReplicas.label',
value: 'parameters.numberOfReplicas',
sort: ['parameters.numberOfReplicas'],
align: 'center'
},
AGE,
];
},
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
},
};
</script>
<template>
<div>
<Loading v-if="$fetchState.pending" />
<ResourceTable
:rows="rows"
:schema="schema"
:headers="headers"
/>
</div>
</template>
@@ -0,0 +1,236 @@
<script>
import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink';
import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable';
import { HCI } from '../types';
import { allSettled } from '../utils/promise';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import { BACKUP_TYPE } from '../config/types';
export default {
name: 'HarvesterListBackup',
components: {
ResourceTable, Banner, Loading, Masthead, MessageLink
},
props: {
schema: {
type: Object,
required: true,
}
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await allSettled({
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }),
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }),
});
this.rows = hash.rows;
this.settings = hash.settings;
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) {
const backupTargetResource = hash.settings.find( (O) => O.id === 'backup-target');
const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource);
if (backupTargetResource && !isEmpty) {
this.testConnect();
}
}
},
data() {
const params = { ...this.$route.params };
const resource = params.resource;
return {
rows: [],
settings: [],
resource,
to: `${ HCI.SETTING }/backup-target?mode=edit`,
};
},
methods: {
async testConnect() {
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/backuptarget/healthz');
await this.$store.dispatch('harvester/request', { url });
} catch (err) {
if (err?._status === 400 || err?._status === 503) {
this.$store.dispatch('growl/error', {
title: this.t('harvester.notification.title.error'),
message: err.errors[0]
}, { root: true });
}
}
},
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
}
},
computed: {
headers() {
const cols = [
STATE,
NAME,
NAMESPACE,
{
name: 'targetVM',
labelKey: 'tableHeaders.targetVm',
value: 'attachVM',
align: 'left',
formatter: 'AttachVMWithName'
},
{
name: 'backupTarget',
labelKey: 'tableHeaders.backupTarget',
value: 'backupTarget',
align: 'left',
formatter: 'HarvesterBackupTargetValidation'
},
{
name: 'readyToUse',
labelKey: 'tableHeaders.readyToUse',
value: 'status.readyToUse',
align: 'left',
formatter: 'Checked',
},
];
if (this.hasBackupProgresses) {
cols.push({
name: 'backupProgress',
labelKey: 'tableHeaders.progress',
value: 'backupProgress',
align: 'left',
formatter: 'HarvesterBackupProgressBar',
width: 200,
});
}
cols.push(AGE);
return cols;
},
hasBackupProgresses() {
return !!this.rows.find((R) => R.status?.progress !== undefined);
},
filteredRows() {
return this.rows.filter((R) => R.spec?.type !== BACKUP_TYPE.SNAPSHOT);
},
backupTargetResource() {
return this.settings.find( (O) => O.id === 'backup-target');
},
isEmptyValue() {
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
},
canUpdate() {
return this?.backupTargetResource?.canUpdate;
},
errorMessage() {
return this.backupTargetResource?.errMessage;
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Masthead
:schema="schema"
:resource="resource"
:create-button-label="t('harvester.backup.createText')"
/>
<Banner
v-if="(errorMessage || isEmptyValue) && canUpdate"
color="error"
>
<MessageLink
v-if="isEmptyValue"
:to="to"
prefix-label="harvester.backup.message.noSetting.prefix"
middle-label="harvester.backup.message.noSetting.middle"
suffix-label="harvester.backup.message.noSetting.suffix"
/>
<MessageLink
v-else
:to="to"
prefix-label="harvester.backup.message.errorTip.prefix"
middle-label="harvester.backup.message.errorTip.middle"
>
<template v-slot:suffix>
{{ t('harvester.backup.message.errorTip.suffix') }} {{ errorMessage }}
</template>
</MessageLink>
</Banner>
<div v-else-if="canUpdate">
<Banner
color="info"
>
<MessageLink
:to="to"
prefix-label="harvester.backup.message.viewSetting.prefix"
middle-label="harvester.backup.message.viewSetting.middle"
suffix-label="harvester.backup.message.viewSetting.suffix"
/>
</Banner>
</div>
<ResourceTable
v-bind="$attrs"
:headers="headers"
:groupable="true"
:rows="filteredRows"
:schema="schema"
key-field="_key"
default-sort-by="age"
v-on="$listeners"
>
<template #col:name="{row}">
<td>
<span>
<router-link
v-if="row.status && row.status.source"
:to="row.detailLocation"
>
{{ row.nameDisplay }}
</router-link>
<span v-else>
{{ row.nameDisplay }}
</span>
</span>
</td>
</template>
</ResourceTable>
</div>
</template>
@@ -0,0 +1,80 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import { Banner } from '@components/Banner';
import FilterLabel from '../components/FilterLabel';
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
export default {
name: 'ListHarvesterImage',
components: {
ResourceTable,
Banner,
FilterLabel
},
props: {
schema: {
type: Object,
required: true,
},
rows: {
type: Array,
required: true,
},
},
data() {
return {
searchLabels: [],
filterRows: []
};
},
computed: {
uploadingImages() {
return this.$store.getters['harvester-common/uploadingImages'] || [];
},
},
methods: {
changeRows(filterRows, searchLabels) {
this.$set(this, 'filterRows', filterRows);
this.$set(this, 'searchLabels', searchLabels);
},
sortGenerationFn() {
let base = defaultTableSortGenerationFn(this.schema, this.$store);
this.searchLabels.map((label) => {
base += label.key;
base += label.value;
});
return base;
},
}
};
</script>
<template>
<div>
<Banner
v-if="uploadingImages.length > 0"
color="warning"
:label="t('harvester.image.warning.uploading', {count: uploadingImages.length} )"
/>
<ResourceTable
v-bind="$attrs"
:rows="filterRows"
:schema="schema"
:sort-generation-fn="sortGenerationFn"
key-field="_key"
v-on="$listeners"
>
<template #more-header-middle>
<FilterLabel ref="filterLabel" :rows="rows" @changeRows="changeRows" />
</template>
</ResourceTable>
</div>
</template>
@@ -0,0 +1,185 @@
<script>
import Loading from '@shell/components/Loading';
import LiveData from '@shell/components/formatter/LiveDate';
import ResourceTable from '@shell/components/ResourceTable';
import { HCI } from '../types';
import { allHash } from '@shell/utils/promise';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
export default {
name: 'HarvesterListTemplate',
components: {
ResourceTable, LiveData, Loading
},
props: {
schema: {
type: Object,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await allHash({
template: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM_TEMPLATE }),
templateVersion: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM_VERSION }),
});
this.template = hash.template;
this.templateVersion = hash.templateVersion;
},
data() {
return {
template: [],
templateVersion: [],
};
},
computed: {
headers() {
return [
STATE,
NAME,
NAMESPACE,
{
name: 'defaultVersion',
value: 'id',
labelKey: 'tableHeaders.defaultVersion'
},
AGE
];
},
rows() {
return [...this.templateVersion];
},
groupBy() {
return 'spec.templateId';
},
groupTitleBy() {
return HCI.VM_TEMPLATE;
},
},
methods: {
showActions(e, group) {
const template = group.rows[0].template;
this.$store.commit(`action-menu/show`, {
resources: [template],
elem: e.target
});
},
valueFor(group) {
const resource = group?.rows?.[0].template;
return resource?.metadata?.creationTimestamp;
},
templateLabel(group) {
return group.key;
},
templateResource(group) {
return group?.rows?.[0].template;
}
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:headers="headers"
:sub-rows="true"
:groupable="false"
:rows="rows"
:group-title-by="groupTitleBy"
:group-by="groupBy"
:schema="schema"
:group-can-action="true"
key-field="_key"
v-on="$listeners"
>
<template #group-by="group">
<div class="group-bar">
<div class="group-tab">
<div v-clean-html="templateLabel(group.group)" class="project-name" />
</div>
<div class="right">
<div class="age">
<LiveData
:value="valueFor(group.group)"
:row="templateResource(group.group)"
/>
</div>
<button type="button" class="btn btn-sm actions mr-10 role-multi-action" @click="showActions($event, group.group)">
<i class="icon icon-actions" />
</button>
</div>
</div>
</template>
<template #col:defaultVersion="{row}">
<td v-if="row.isDefaultVersion">
<i class="icon icon-checkmark" />
</td>
<td v-else></td>
</template>
</ResourceTable>
</template>
<style lang="scss" scoped>
::v-deep {
.group-name {
line-height: 30px;
}
.group-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
.right {
display: flex;
align-items: center;
.age {
width: 100px;
}
.actions {
padding-right: 7px;
}
}
&.has-description {
.right {
margin-top: 5px;
}
.group-tab {
&, &::after {
height: 50px;
}
&::after {
right: -20px;
}
.description {
margin-top: -20px;
}
}
}
}
}
</style>
@@ -0,0 +1,131 @@
<script>
import Loading from '@shell/components/Loading';
import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable';
import { HCI } from '../types';
import { SCHEMA } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import { BACKUP_TYPE } from '../config/types';
const schema = {
id: HCI.VM_SNAPSHOT,
type: SCHEMA,
attributes: {
kind: HCI.VM_SNAPSHOT,
namespaced: true
},
metadata: { name: HCI.VM_SNAPSHOT },
};
export default {
name: 'HarvesterListVMSnapshot',
components: {
ResourceTable, Loading, Masthead
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await allHash({
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }),
});
const schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.BACKUP);
if (!schema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.VM_SNAPSHOT, isCreatable: false });
}
this.rows = hash.rows;
},
data() {
const params = { ...this.$route.params };
const resource = params.resource;
return {
rows: [],
resource,
};
},
computed: {
headers() {
return [
STATE,
NAME,
NAMESPACE,
{
name: 'targetVM',
labelKey: 'tableHeaders.targetVm',
value: 'attachVM',
align: 'left',
formatter: 'AttachVMWithName'
},
{
name: 'readyToUse',
labelKey: 'tableHeaders.readyToUse',
value: 'status.readyToUse',
align: 'left',
formatter: 'Checked',
},
AGE
];
},
schema() {
return schema;
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
},
filteredRows() {
return this.rows.filter((R) => R.spec?.type !== BACKUP_TYPE.BACKUP);
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Masthead
:schema="null"
:resource="resource"
:type-display="typeDisplay"
:create-button-label="t('harvester.vmSnapshot.createText')"
/>
<ResourceTable
v-bind="$attrs"
:headers="headers"
:groupable="true"
:rows="filteredRows"
:schema="schema"
key-field="_key"
default-sort-by="age"
v-on="$listeners"
>
<template #col:name="{row}">
<td>
<span>
<router-link
v-if="row.status && row.status.source"
:to="row.detailLocation"
>
{{ row.nameDisplay }}
</router-link>
<span v-else>
{{ row.nameDisplay }}
</span>
</span>
</td>
</template>
</resourcetable>
</div>
</template>
@@ -0,0 +1,184 @@
<script>
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
import { allSettled } from '../utils/promise';
import { PV, PVC, SCHEMA, LONGHORN } from '@shell/config/types';
import { HCI, VOLUME_SNAPSHOT } from '../types';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
const schema = {
id: HCI.VOLUME,
type: SCHEMA,
attributes: {
kind: HCI.VOLUME,
namespaced: true
},
metadata: { name: HCI.VOLUME },
};
export default {
name: 'HarvesterListVolume',
components: {
Loading, ResourceTable, HarvesterVolumeState
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {
pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }),
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
};
const volumeSnapshotSchema = this.$store.getters[`${ inStore }/schemaFor`](VOLUME_SNAPSHOT);
if (volumeSnapshotSchema) {
_hash.snapshots = this.$store.dispatch(`${ inStore }/findAll`, { type: VOLUME_SNAPSHOT });
}
if (this.$store.getters[`${ inStore }/schemaFor`](LONGHORN.VOLUMES)) {
_hash.longhornVolumes = this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.VOLUMES });
}
if (this.$store.getters[`${ inStore }/schemaFor`](LONGHORN.ENGINES)) {
_hash.longhornEngines = this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.ENGINES });
}
const hash = await allSettled(_hash);
const pvcSchema = this.$store.getters[`${ inStore }/schemaFor`](PVC);
if (!pvcSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.VOLUME, isCreatable: false });
}
this.rows = hash.pvcs;
},
data() {
return { rows: [] };
},
computed: {
schema() {
return schema;
},
headers() {
return [
STATE,
NAME,
NAMESPACE,
{
name: 'size',
labelKey: 'tableHeaders.size',
value: 'spec.resources.requests.storage',
sort: 'volumeSort',
formatter: 'Si',
formatterOpts: {
opts: {
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
},
needParseSi: true
},
},
{
name: 'storageClass',
labelKey: 'tableHeaders.storageClass',
value: 'spec.storageClassName'
},
{
name: 'AttachedVM',
labelKey: 'tableHeaders.attachedVM',
type: 'attached',
value: 'spec.claimRef',
sort: 'name',
},
{
name: 'VolumeSnapshotCounts',
labelKey: 'harvester.tableHeaders.volumeSnapshotCounts',
value: 'relatedVolumeSnapshotCounts',
formatter: 'RelatedVolumeSnapshotCounts',
sort: 'name',
align: 'center',
},
{
...STATE,
name: 'phase',
labelKey: 'tableHeaders.phase',
formatterOpts: { arbitrary: true },
value: 'phaseState',
},
AGE,
];
},
},
methods: {
goTo(row) {
return row?.attachVM?.detailLocation;
},
getVMName(row) {
return row.attachVM?.metadata?.name || '';
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:headers="headers"
:groupable="true"
default-sort-by="age"
:namespaced="true"
:rows="rows"
:schema="schema"
key-field="_key"
v-on="$listeners"
>
<template
slot="cell:state"
slot-scope="scope"
>
<div class="state">
<HarvesterVolumeState
class="vmstate"
:row="scope.row"
/>
</div>
</template>
<template
slot="cell:AttachedVM"
slot-scope="scope"
>
<div>
<router-link
v-if="getVMName(scope.row)"
:to="goTo(scope.row)"
>
{{ getVMName(scope.row) }}
</router-link>
</div>
</template>
</ResourceTable>
</template>
<style lang="scss" scoped>
.state {
display: flex;
.vmstate {
margin-right: 6px;
}
}
</style>
@@ -0,0 +1,76 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { SCHEMA, PVC } from '@shell/config/types';
import { VOLUME_SNAPSHOT, HCI } from '../types';
import { allHash } from '@shell/utils/promise';
const schema = {
id: HCI.SNAPSHOT,
type: SCHEMA,
attributes: {
kind: HCI.SNAPSHOT,
namespaced: true
},
metadata: { name: HCI.SNAPSHOT },
};
export default {
name: 'HarvesterListSnapshot',
components: { ResourceTable, Loading },
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = {
volumes: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
snapshots: this.$store.dispatch(`${ inStore }/findAll`, { type: VOLUME_SNAPSHOT }),
};
const res = await allHash(hash);
this.rows = res.snapshots;
const snapShotSchema = this.$store.getters[`${ inStore }/schemaFor`](VOLUME_SNAPSHOT);
if (!snapShotSchema?.collectionMethods.find(x => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.SNAPSHOT, isCreatable: false });
}
},
data() {
return { rows: [] };
},
computed: {
filteredRows() {
return this.rows.filter((R) => {
return R.metadata?.ownerReferences?.[0]?.kind === 'PersistentVolumeClaim';
});
},
schema() {
return schema;
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:groupable="true"
:schema="schema"
:rows="filteredRows"
key-field="_key"
v-on="$listeners"
/>
</template>
@@ -0,0 +1,222 @@
<script>
import ConsoleBar from '../components/VMConsoleBar';
import ResourceTable from '@shell/components/ResourceTable';
import LinkDetail from '@shell/components/formatter/LinkDetail';
import HarvesterVmState from '../formatters/HarvesterVmState';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import { NODE, POD } from '@shell/config/types';
import { HCI } from '../types';
import { allHash } from '@shell/utils/promise';
import Loading from '@shell/components/Loading';
import { clone } from '@shell/utils/object';
export const VM_HEADERS = [
STATE,
{
...NAME,
width: 350,
},
NAMESPACE,
{
name: 'CPU',
label: 'CPU',
sort: ['spec.template.spec.domain.cpu.cores'],
value: 'spec.template.spec.domain.cpu.cores',
align: 'center',
dashIfEmpty: true,
},
{
name: 'Memory',
value: 'displayMemory',
sort: ['memorySort'],
align: 'center',
labelKey: 'tableHeaders.memory',
formatter: 'Si',
formatterOpts: {
opts: {
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
},
needParseSi: true
},
},
{
name: 'ip',
label: 'IP Address',
value: 'id',
formatter: 'HarvesterIpAddress',
labelKey: 'tableHeaders.ipAddress',
sort: ['id'],
},
{
...AGE,
sort: 'metadata.creationTimestamp:desc',
}
];
export default {
name: 'HarvesterListVM',
components: {
Loading,
HarvesterVmState,
LinkDetail,
ConsoleBar,
ResourceTable
},
props: {
schema: {
type: Object,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
pod: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }),
};
if (this.$store.getters[`${ inStore }/schemaFor`](NODE)) {
_hash.nodes = this.$store.dispatch(`${ inStore }/findAll`, { type: NODE });
this.hasNode = true;
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK)) {
_hash.nodeNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NODE_NETWORK });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.CLUSTER_NETWORK)) {
_hash.clusterNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK });
}
const hash = await allHash(_hash);
this.allVMs = hash.vms;
this.allNodeNetworks = hash.nodeNetworks || [];
this.allClusterNetworks = hash.clusterNetworks || [];
},
data() {
return {
hasNode: false,
allVMs: [],
allVMIs: [],
allNodeNetworks: [],
allClusterNetworks: [],
HCI
};
},
computed: {
headers() {
const restoreCol = {
name: 'restoreProgress',
labelKey: 'tableHeaders.restore',
value: 'restoreProgress',
align: 'left',
formatter: 'HarvesterBackupProgressBar',
width: 200,
};
const nodeCol = {
name: 'node',
label: 'Node',
value: 'nodeName',
sort: ['realAttachNodeName'],
formatter: 'HarvesterHost',
labelKey: 'tableHeaders.node'
};
const cols = clone(VM_HEADERS);
if (this.hasNode) {
cols.splice(-1, 0, nodeCol);
}
if (this.hasRestoredVMs) {
cols.splice(-1, 0, restoreCol);
}
return cols;
},
rows() {
const matchVMIs = this.allVMIs.filter(VMI => !this.allVMs.find(VM => VM.id === VMI.id));
return [...this.allVMs, ...matchVMIs];
},
hasRestoredVMs() {
return !!this.rows.find(r => !!r.restoreResource);
}
},
async created() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmis = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI });
await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM });
this.$set(this, 'allVMIs', vmis);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<ResourceTable
v-bind="$attrs"
:headers="headers"
default-sort-by="age"
:rows="rows"
:schema="schema"
:groupable="true"
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-node-network="allNodeNetworks" :all-cluster-network="allClusterNetworks" />
</div>
</template>
<template slot="cell:name" slot-scope="scope">
<div class="name-console">
<LinkDetail v-if="scope.row.type !== HCI.VMI" v-model="scope.row.metadata.name" :row="scope.row" />
<span v-else>
{{ scope.row.metadata.name }}
</span>
<ConsoleBar :resource="scope.row" class="console mr-10 ml-10" />
</div>
</template>
</ResourceTable>
</div>
</template>
<style lang="scss" scoped>
.state {
display: flex;
.vmstate {
margin-right: 6px;
}
}
.name-console {
display: flex;
align-items: center;
justify-content: space-between;
span {
padding-right: 4px;
line-height: 26px;
white-space: nowrap;
}
}
</style>
@@ -0,0 +1,295 @@
<script>
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import Masthead from '@shell/components/ResourceList/Masthead';
import { allHash } from '@shell/utils/promise';
import { STATE, AGE, NAME } from '@shell/config/table-headers';
import { mapPref, GROUP_RESOURCES } from '@shell/store/prefs';
import { NODE } from '@shell/config/types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { CLUSTER_NETWORK } from '../config/query-params';
import { HCI } from '../types';
export default {
name: 'ListHarvesterVLANConfigs',
components: {
ResourceTable,
Loading,
Masthead,
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
configs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_CONFIG }),
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
vlanStatuses: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE })
});
},
data() {
return { HCI };
},
computed: {
groupPreference: mapPref(GROUP_RESOURCES),
headers() {
return [
STATE,
{
...NAME,
width: 300,
},
{
name: 'type',
labelKey: 'tableHeaders.type',
value: 'typeDisplay',
getValue: (row) => row.typeDisplay,
sort: ['typeDisplay'],
},
AGE,
];
},
rows() {
const inStore = this.$store.getters['currentProduct'].inStore;
const configs = this.$store.getters[`${ inStore }/all`](HCI.VLAN_CONFIG);
return configs;
},
vlanConfigSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/schemaFor`](HCI.VLAN_CONFIG);
},
clusterNetworkSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/schemaFor`](HCI.CLUSTER_NETWORK);
},
isClusterNetworkCreatable() {
return (this.clusterNetworkSchema?.collectionMethods || []).includes('POST');
},
createClusterNetworkLocation() {
const location = {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: {
product: HARVESTER_PRODUCT,
resource: HCI.CLUSTER_NETWORK,
},
};
return location;
},
clusterNetworkWithoutConfigs() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK);
const out = clusterNetworks.map((network) => {
const hasChild = !!this.rows.find((config) => config?.spec?.clusterNetwork === network.id);
return {
...network,
hasChild
};
});
return out;
},
rowsWithFakeClusterNetworks() {
const fakeRows = this.clusterNetworkWithoutConfigs.map((network) => {
return {
groupByLabel: network.id,
isFake: true,
mainRowKey: network.id,
nameDisplay: network.id,
groupByClusterNetwork: network.id,
availableActions: []
};
});
return [...this.rows, ...fakeRows];
},
},
methods: {
showClusterNetworkActionButton(group) {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetwork = group.key;
const resource = this.$store.getters[`${ inStore }/byId`](HCI.CLUSTER_NETWORK, clusterNetwork);
return !!resource;
},
showClusterNetworkAction(event, group) {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetwork = group.key;
const resource = this.$store.getters[`${ inStore }/byId`](HCI.CLUSTER_NETWORK, clusterNetwork);
this.$store.commit(`action-menu/show`, {
resources: [resource],
elem: event.target
});
},
createVlanConfigLocation(group) {
const clusterNetwork = group.key;
const location = {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: {
product: HARVESTER_PRODUCT,
resource: HCI.VLAN_CONFIG,
},
};
location.query = { [CLUSTER_NETWORK]: clusterNetwork };
return location;
},
slotName(clusterNetwork) {
return `main-row:${ clusterNetwork }`;
},
groupLabel(group) {
const row = group.rows[0];
if (row.isFake) {
return `${ this.t('harvester.network.clusterNetwork.label') }: ${ row.nameDisplay }`;
}
return `${ this.t('harvester.network.clusterNetwork.label') }: ${ group.key }`;
},
},
};
</script>
<template>
<div>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Masthead
:schema="clusterNetworkSchema"
:type-display="t('harvester.clusterNetwork.title')"
:resource="HCI.CLUSTER_NETWORK"
:create-location="createClusterNetworkLocation"
:create-button-label="t('harvester.clusterNetwork.create.button.label')"
/>
<ResourceTable
:rows="rowsWithFakeClusterNetworks"
:headers="headers"
:groupable="true"
:schema="vlanConfigSchema"
group-by="groupByClusterNetwork"
>
<template #header-middle>
<div />
</template>
<template #group-by="{group}">
<div class="group-bar">
<div class="group-tab">
<span>
{{ groupLabel(group) }}
</span>
</div>
<div class="right">
<router-link
v-if="isClusterNetworkCreatable && group.key !== 'mgmt'"
class="btn btn-sm role-secondary mr-5"
:to="createVlanConfigLocation(group)"
>
{{ t('harvester.vlanConfig.createNetworkConfig') }}
</router-link>
<button
type="button"
class="btn btn-sm role-multi-action actions mr-10"
:class="{invisible: !showClusterNetworkActionButton(group)}"
@click="showClusterNetworkAction($event, group)"
>
<i class="icon icon-actions" />
</button>
</div>
</div>
</template>
<template
v-for="clusterNetwork in clusterNetworkWithoutConfigs"
v-slot:[slotName(clusterNetwork.id)]
>
<tr
v-show="!clusterNetwork.hasChild"
:key="clusterNetwork.id"
class="main-row"
>
<td
class="empty text-center"
colspan="12"
>
{{ clusterNetwork.id === 'mgmt' ? t('harvester.clusterNetwork.mgmt') : t('harvester.clusterNetwork.clusterNetwork') }}
</td>
</tr>
</template>
</ResourceTable>
</div>
</div>
</template>
<style lang="scss" scoped>
.group-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
.right {
margin-top: 5px;
margin-bottom: 3px;
}
.group-tab {
&, &::after {
height: 50px;
}
&::after {
right: -20px;
}
SPAN {
color: var(--body-text) !important;
}
}
}
</style>
<style lang="scss">
.col-link-detail {
// Avoid 'Create Network Config' button to overlap the namespace tab when resize table to min length
min-width: 200px;
}
</style>
<style lang="scss">
.group-bar .right {
display: flex;
margin-left: 50px;
a {
height: 30px;
}
}