feat: add migration steps tooltip when mouse hover on progress bar (#994)

* feat: add progress bar tooltip

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: copilot review

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2026-07-14 14:37:13 +08:00 committed by GitHub
parent 219f39ae52
commit e0436bff28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -38,6 +38,65 @@ const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORK
const allNetworkMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_NETWORK_MAP));
const allStorageMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_STORAGE_MAP));
const escapeHtml = (value = '') => String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
const getPipelineStepMeta = (step = {}) => {
const phase = String(step.phase || '').toLowerCase();
const hasProgress = typeof step.progress?.completed === 'number' && typeof step.progress?.total === 'number' && step.progress.total > 0;
const progressPct = hasProgress ? Math.round((step.progress.completed / step.progress.total) * 1000) / 10 : null;
if (phase === 'completed' || phase === 'succeeded') {
return {
icon: '✓',
color: 'var(--success)',
text: ''
};
}
if (phase === 'running' || phase === 'inprogress' || phase === 'progressing' || phase === 'started' || (hasProgress && progressPct > 0)) {
return {
icon: '↻',
color: 'var(--primary)',
text: progressPct !== null ? `${ step.phase || t('generic.inProgress') } (${ progressPct }%)` : (step.phase || t('generic.inProgress'))
};
}
if (phase === 'failed' || phase === 'error') {
return {
icon: '✕',
color: 'var(--error)',
text: step.phase || t('harvester.addons.vmMigration.dashboard.progress.failed')
};
}
return {
icon: '○',
color: 'var(--muted)',
text: ''
};
};
const formatPipelineTooltip = (pipeline = []) => {
if (!pipeline.length) {
return `<div style="display:flex;align-items:center;gap:8px;"><span style="color:var(--muted);font-weight:700;">○</span><span>${ escapeHtml(t('harvester.addons.vmMigration.dashboard.progress.initializingMigration')) }</span></div>`;
}
return pipeline
.map((step, idx) => {
const stepName = step.name || t('harvester.addons.vmMigration.dashboard.progress.step', { index: idx + 1 });
const meta = getPipelineStepMeta(step);
const lineText = meta.text ? `${ escapeHtml(stepName) } : ${ escapeHtml(meta.text) }` : escapeHtml(stepName);
return `<div style="display:flex;align-items:center;gap:8px;"><span style="color:${ meta.color };font-weight:700;">${ meta.icon }</span><span>${ lineText }</span></div>`;
})
.join('');
};
const rows = computed(() => {
return allPlans.value.map((plan) => {
const netMapName = plan.spec?.map?.network?.name;
@ -107,6 +166,7 @@ const rows = computed(() => {
vmId: vm.id || '',
progress: overallProgress,
currentStep,
pipelineTooltip: formatPipelineTooltip(pipeline),
errorMsg,
canceled: plan.planCanceled,
};
@ -119,6 +179,7 @@ const rows = computed(() => {
vmId: vm.id || '',
progress: 0,
currentStep: t('harvester.addons.vmMigration.dashboard.progress.initializingMigration'),
pipelineTooltip: formatPipelineTooltip([]),
errorMsg: '',
canceled: false,
}));
@ -266,12 +327,16 @@ init();
</div>
</div>
<div class="vm-pct-block">
<div
v-clean-tooltip="{ content: vm.pipelineTooltip, html: true }"
class="vm-bar"
>
<PercentageBar
:model-value="vm.progress"
:color-stops="vm.errorMsg ? { 100: '--error' } : vm.canceled ? { 100: '--darker' } : vm.progress >= 100 ? { 100: '--success' } : { 100: '--primary' }"
preferred-direction="MORE"
class="vm-bar"
/>
</div>
<span class="vm-pct text-muted mr-10">{{ vm.progress }}%</span>
</div>
<div