feat(forklift): Added icons, critical state, fix grids, action button

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
Marcelo Fukumoto 2026-05-11 18:15:31 +02:00
parent 740ca94841
commit a0d145ee60
No known key found for this signature in database
GPG Key ID: 1CA12189625C2543
7 changed files with 104 additions and 39 deletions

View File

@ -14,9 +14,9 @@
"@vue-flow/minimap": "^1.4.0",
"cache-loader": "^4.1.0",
"color": "4.2.3",
"elkjs": "^0.11.0",
"ip": "2.0.1",
"node-polyfill-webpack-plugin": "^3.0.0",
"elkjs": "^0.11.0",
"vue-draggable-next": "^2.2.1",
"yaml": "^2.5.1"
},
@ -53,9 +53,9 @@
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
},
"devDependencies": {
"@commitlint/load": "^19.8.1",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@commitlint/load": "^19.8.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^7.1.0",
"husky": "^9.1.7"

View File

@ -18,7 +18,7 @@ defineProps({
:key="'net-' + idx"
class="mapping-entry"
>
<i class="icon icon-cluster" />
<i class="icon icon-network" />
<span>{{ entry }}</span>
</div>
<div
@ -26,7 +26,7 @@ defineProps({
:key="'stor-' + idx"
class="mapping-entry"
>
<i class="icon icon-storage" />
<i class="icon icon-datastore" />
<span>{{ entry }}</span>
</div>
</div>

View File

@ -1,7 +1,18 @@
import HarvesterResource from './harvester';
import { HCI } from '../types';
import { PRODUCT_NAME } from '../config/harvester';
export default class ForkliftPlan extends HarvesterResource {
get listLocation() {
return {
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
params: {
product: this.$rootGetters['productId'],
cluster: this.$rootGetters['clusterId'],
},
};
}
get planFailed() {
const conditions = this.status?.conditions || [];
@ -23,11 +34,22 @@ export default class ForkliftPlan extends HarvesterResource {
}
get stateDescription() {
if (this.planCritical) {
return this.criticalMessages.join('; ');
const messages = [];
const conditions = this.status?.conditions || [];
if (this.planFailed) {
const failedMsg = conditions.find((c) => c.type === 'Failed' && c.status === 'True')?.message;
if (failedMsg) {
messages.push(failedMsg);
}
}
return null;
if (this.planCritical) {
messages.push(...this.criticalMessages);
}
return messages.length > 0 ? messages.join(' ') : null;
}
get stateObj() {
@ -103,31 +125,51 @@ export default class ForkliftPlan extends HarvesterResource {
return 'bg-success';
}
get isForkliftDashboard() {
const route = this.currentRouter()?.currentRoute?.value;
return route?.name?.endsWith('-forklift');
}
get _availableActions() {
const canStop = this.isMigrating && !this.planCanceled;
const canStart = !this.planSucceeded && (!this.isMigrating || this.planFailed || this.planCanceled || this.planCritical);
if (this.isForkliftDashboard) {
const canStop = this.isMigrating && !this.planCanceled;
const canStart = !this.planSucceeded && (!this.isMigrating || this.planFailed || this.planCanceled || this.planCritical);
const out = [];
const out = super._availableActions;
if (canStart) {
out.push({
action: 'startMigration',
enabled: true,
icon: 'icon icon-play',
label: 'Start',
});
}
if (canStop) {
out.unshift({
action: 'stopMigration',
enabled: true,
icon: 'icon icon-pause',
label: 'Stop',
if (canStop) {
out.push({
action: 'stopMigration',
enabled: true,
icon: 'icon icon-pause',
label: 'Stop',
});
}
out.push({
action: 'promptRemove',
altAction: 'remove',
label: this.t('action.remove'),
icon: 'icon icon-trash',
bulkable: true,
enabled: this.canDelete,
bulkAction: 'promptRemove',
weight: -10,
});
return out;
}
if (canStart) {
out.unshift({
action: 'startMigration',
enabled: true,
icon: 'icon icon-play',
label: 'Start',
});
}
return out;
return super._availableActions;
}
async stopMigration() {

View File

@ -448,7 +448,7 @@ init();
<span class="source-detail text-muted">VLAN {{ entry.vlanId || '0' }}</span>
</div>
<div :class="['mapping-arrow', entry.target ? 'text-success' : 'text-muted']">
<i class="icon icon-upgrade-alt" />
<i class="icon icon-right-arrow-alt" />
</div>
<div class="mapping-target">
<LabeledSelect
@ -498,7 +498,7 @@ init();
>{{ formatStorageDetail(entry) }}</span>
</div>
<div :class="['mapping-arrow', entry.target ? 'text-success' : 'text-muted']">
<i class="icon icon-upgrade-alt" />
<i class="icon icon-right-arrow-alt" />
</div>
<div class="mapping-target">
<LabeledSelect
@ -546,7 +546,7 @@ init();
.mappings-columns {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 64px;
}
@ -602,7 +602,6 @@ init();
.mapping-arrow {
font-size: 22px;
flex-shrink: 0;
transform: rotate(90deg);
}
.mapping-target {

View File

@ -110,6 +110,18 @@ const rows = computed(() => {
};
});
// If no migration progress but we have VMs in the spec, show them at 0%
if (plan.vmProgress.length === 0 && plan.spec?.vms?.length > 0) {
plan.vmProgress = plan.spec.vms.map((vm) => ({
vmName: vm.name || vm.id || 'Unknown',
vmId: vm.id || '',
progress: 0,
currentStep: 'Initializing migration',
errorMsg: '',
canceled: false,
}));
}
plan.progress = plan.vmProgress.length > 0 ? Math.round(plan.vmProgress.reduce((sum, vm) => sum + vm.progress, 0) / plan.vmProgress.length * 10) / 10 : 0;
return plan;
@ -129,7 +141,6 @@ const headers = [
{
...NAME_COL,
labelKey: 'harvester.addons.forklift.dashboard.columns.plan',
subLabel: 'VM IDs',
},
{ ...FORKLIFT_PLAN_VM_COUNT, width: 105 },
{
@ -203,9 +214,6 @@ init();
<div class="plan-name">
{{ row.metadata.name }}
</div>
<div class="plan-vms text-muted">
{{ row.vmIdsDisplay }}
</div>
</div>
</template>
<template #cell:vmCount="{ row }">
@ -309,6 +317,7 @@ init();
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
}
.vm-id {

View File

@ -113,7 +113,7 @@ const startMigration = async(buttonCb) => {
const inStore = store.getters['currentProduct'].inStore;
try {
const planName = `${ providerName.value }-plan-${ Math.random().toString(36).substring(2, 7) }`;
const planName = `${ providerName.value }`;
const planSpec = {
provider: {
@ -363,7 +363,7 @@ init();
<div class="vm-card-specs">
<span class="vm-os text-muted">{{ vm.os }}</span>
<span class="vm-resources">
<i class="icon icon-dock" />
<i class="icon icon-disk" />
{{ vm.cpus }} vCPU &bull; {{ vm.memGB }} &bull; {{ vm.diskDisplay }}
</span>
</div>
@ -433,7 +433,7 @@ init();
.details-grid {
display: grid;
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
grid-template-rows: 1fr 1fr;
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
gap: 12px 128px;
width: 747px;
line-height: 20px;

View File

@ -1,6 +1,21 @@
const config = require('@rancher/shell/vue.config');
const path = require('path');
const baseConfig = require('@rancher/shell/vue.config');
module.exports = config(__dirname, {
const shellConfig = baseConfig(__dirname, {
excludes: [],
// excludes: ['harvester']
});
const originalConfigureWebpack = shellConfig.configureWebpack;
shellConfig.configureWebpack = function(config) {
if (originalConfigureWebpack) {
originalConfigureWebpack(config);
}
// Force all @rancher/icons imports (including from @rancher/shell) to use
// the top-level 2.0.60 version so new icons are available everywhere.
config.resolve.alias['@rancher/icons'] = path.resolve(__dirname, 'node_modules', '@rancher/icons');
};
module.exports = shellConfig;