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,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] = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user