mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 12:49:14 +00:00
feat(forklift): New changes added based on new figma
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
parent
63d726aa91
commit
341f7734a9
@ -1892,7 +1892,6 @@ harvester:
|
||||
targetNamespace: Target Namespace
|
||||
migrationMode: Migration Mode
|
||||
coldMigration: Cold Migration
|
||||
vmsWillShutDown: VMs will shut down
|
||||
virtualMachines: Virtual Machines
|
||||
warningTitle: "Cold Migration Warning"
|
||||
warningMessage: "All source VMs will be powered off, and downtime will occur during migration. Plan your maintenance window accordingly and notify relevant stakeholders"
|
||||
|
||||
@ -1,6 +1,161 @@
|
||||
import HarvesterResource from './harvester';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default class ForkliftPlan extends HarvesterResource {
|
||||
get planFailed() {
|
||||
const conditions = this.status?.conditions || [];
|
||||
|
||||
return conditions.some((c) => c.type === 'Failed' && c.status === 'True');
|
||||
}
|
||||
|
||||
get isMigrating() {
|
||||
const migration = this.status?.migration;
|
||||
|
||||
return !!migration?.started && !migration?.completed;
|
||||
}
|
||||
|
||||
get planCanceled() {
|
||||
const history = this.status?.migration?.history || [];
|
||||
|
||||
if (history.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const latest = history[history.length - 1];
|
||||
const conditions = latest.conditions || [];
|
||||
|
||||
return conditions.some((c) => c.type === 'Canceled' && c.status === 'True');
|
||||
}
|
||||
|
||||
get stateDisplay() {
|
||||
if (this.planFailed) {
|
||||
return 'Error';
|
||||
}
|
||||
|
||||
if (this.planCanceled) {
|
||||
return 'Canceled';
|
||||
}
|
||||
|
||||
if (this.isMigrating) {
|
||||
return 'In Progress';
|
||||
}
|
||||
|
||||
return 'Active';
|
||||
}
|
||||
|
||||
get stateBackground() {
|
||||
if (this.planFailed) {
|
||||
return 'bg-error';
|
||||
}
|
||||
|
||||
if (this.planCanceled) {
|
||||
return 'bg-warning';
|
||||
}
|
||||
|
||||
if (this.isMigrating) {
|
||||
return 'bg-info';
|
||||
}
|
||||
|
||||
return 'bg-success';
|
||||
}
|
||||
|
||||
get _availableActions() {
|
||||
const canStop = this.isMigrating && !this.planCanceled;
|
||||
const canStart = !this.isMigrating || this.planFailed || this.planCanceled;
|
||||
|
||||
const out = super._availableActions;
|
||||
|
||||
if (canStop) {
|
||||
out.unshift({
|
||||
action: 'stopMigration',
|
||||
enabled: true,
|
||||
icon: 'icon icon-pause',
|
||||
label: 'Stop',
|
||||
});
|
||||
}
|
||||
|
||||
if (canStart) {
|
||||
out.unshift({
|
||||
action: 'startMigration',
|
||||
enabled: true,
|
||||
icon: 'icon icon-play',
|
||||
label: 'Start',
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async stopMigration() {
|
||||
const history = this.status?.migration?.history || [];
|
||||
|
||||
if (history.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latest = history[history.length - 1];
|
||||
const migrationName = latest.migration?.name;
|
||||
|
||||
if (migrationName) {
|
||||
const selfUrl = this.linkFor('self');
|
||||
const url = selfUrl.replace('forklift.konveyor.io.plans', 'forklift.konveyor.io.migrations').replace(`/${ this.metadata.name }`, `/${ migrationName }`);
|
||||
|
||||
await this.$dispatch('request', { url, method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
|
||||
async startMigration() {
|
||||
const namespace = this.metadata.namespace;
|
||||
const history = this.status?.migration?.history || [];
|
||||
|
||||
// Delete previous migrations before starting a new one
|
||||
for (const entry of history) {
|
||||
const name = entry.migration?.name;
|
||||
|
||||
if (name) {
|
||||
const selfUrl = this.linkFor('self');
|
||||
const url = selfUrl.replace('forklift.konveyor.io.plans', 'forklift.konveyor.io.migrations').replace(`/${ this.metadata.name }`, `/${ name }`);
|
||||
|
||||
try {
|
||||
await this.$dispatch('request', { url, method: 'DELETE' });
|
||||
} catch (e) {
|
||||
// Migration may already be gone — ignore 404s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const migration = await this.$dispatch('create', {
|
||||
type: HCI.FORKLIFT_MIGRATION,
|
||||
metadata: {
|
||||
name: `${ this.metadata.name }-migration-${ Math.random().toString(36).substring(2, 7) }`,
|
||||
namespace,
|
||||
ownerReferences: [
|
||||
{
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'Plan',
|
||||
name: this.metadata.name,
|
||||
uid: this.metadata.uid,
|
||||
blockOwnerDeletion: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
spec: {
|
||||
plan: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'Plan',
|
||||
name: this.metadata.name,
|
||||
namespace,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await migration.save();
|
||||
}
|
||||
|
||||
async deletePlan() {
|
||||
await this.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleting a Plan cascades via ownerReferences set at creation time.
|
||||
* Kubernetes GC will automatically delete: Migration, NetworkMap, StorageMap, Provider (→ Secret).
|
||||
@ -9,7 +164,7 @@ export default class ForkliftPlan extends HarvesterResource {
|
||||
remove() {
|
||||
const opt = { ...arguments };
|
||||
|
||||
opt.params = { propagationPolicy: 'Foreground' };
|
||||
opt.params = { propagationPolicy: 'Background' };
|
||||
|
||||
return this._remove(opt);
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ const createdProvider = ref(null);
|
||||
const createdSecret = ref(null);
|
||||
const loading = ref(true);
|
||||
const testPassed = ref(false);
|
||||
const testing = ref(false);
|
||||
|
||||
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
||||
|
||||
@ -71,6 +72,7 @@ const cancel = async() => {
|
||||
const testConnection = async(buttonCb) => {
|
||||
testResult.value = null;
|
||||
testError.value = null;
|
||||
testing.value = true;
|
||||
|
||||
if (!providerName.value || !url.value || !username.value || !password.value) {
|
||||
testError.value = t('harvester.addons.forklift.configureProvider.testMissingFields');
|
||||
@ -191,6 +193,7 @@ const testConnection = async(buttonCb) => {
|
||||
if (connected) {
|
||||
testPassed.value = true;
|
||||
testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess');
|
||||
testing.value = false;
|
||||
buttonCb(true);
|
||||
} else {
|
||||
if (createdProvider.value) {
|
||||
@ -203,6 +206,7 @@ const testConnection = async(buttonCb) => {
|
||||
}
|
||||
|
||||
testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout');
|
||||
testing.value = false;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
@ -220,6 +224,7 @@ const testConnection = async(buttonCb) => {
|
||||
}
|
||||
|
||||
testError.value = err.message || t('harvester.addons.forklift.configureProvider.testFailed');
|
||||
testing.value = false;
|
||||
buttonCb(false);
|
||||
}
|
||||
};
|
||||
@ -399,7 +404,7 @@ init();
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
:disabled="!isFormValid"
|
||||
:disabled="!isFormValid || testing"
|
||||
:action-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
:waiting-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
:success-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
|
||||
@ -48,8 +48,6 @@ const rows = computed(() => {
|
||||
|
||||
plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.type === 'pod' ? 'Pod Network' : (e.destination?.name || '-') }`);
|
||||
plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.storageClass || '-' }`);
|
||||
plan.networkDisplay = plan.networkEntries.join(', ') || '-';
|
||||
plan.storageDisplay = plan.storageEntries.join(', ') || '-';
|
||||
|
||||
plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-';
|
||||
|
||||
@ -76,13 +74,31 @@ const rows = computed(() => {
|
||||
currentStep = step.name || `Step ${ idx + 1 }`;
|
||||
}
|
||||
|
||||
if (step.error) {
|
||||
errorMsg = (step.error.reasons || []).join('; ') || 'Error';
|
||||
if (step.error && !errorMsg) {
|
||||
const reasons = (step.error.reasons || []).join('; ') || 'Error';
|
||||
|
||||
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: ${ reasons }`;
|
||||
}
|
||||
|
||||
if (step.phase === 'Failed' && !errorMsg) {
|
||||
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: Failed`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
overallProgress = Math.round(overallProgress);
|
||||
// Fallback to VM-level error if no step-level error found
|
||||
if (!errorMsg && vm.error) {
|
||||
const reasons = (vm.error.reasons || []).join('; ') || 'Failed';
|
||||
|
||||
errorMsg = `${ currentStep || vm.error.phase || 'Migration' }: ${ reasons }`;
|
||||
}
|
||||
|
||||
overallProgress = Math.round(overallProgress * 10) / 10;
|
||||
|
||||
// If no step-level error but the plan itself is failed, surface it
|
||||
if (!errorMsg && plan.planFailed) {
|
||||
errorMsg = `${ currentStep || 'Migration' }: Failed`;
|
||||
}
|
||||
|
||||
return {
|
||||
vmName: vm.name || vm.id || 'Unknown',
|
||||
@ -90,10 +106,11 @@ const rows = computed(() => {
|
||||
progress: overallProgress,
|
||||
currentStep,
|
||||
errorMsg,
|
||||
canceled: plan.planCanceled,
|
||||
};
|
||||
});
|
||||
|
||||
plan.progress = plan.vmProgress.length > 0 ? Math.round(plan.vmProgress.reduce((sum, vm) => sum + vm.progress, 0) / plan.vmProgress.length) : 0;
|
||||
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;
|
||||
});
|
||||
@ -209,16 +226,18 @@ init();
|
||||
<span class="vm-name">{{ vm.vmName }}</span>
|
||||
<span class="text-muted vm-id">id: {{ vm.vmId }}</span>
|
||||
</div>
|
||||
<span class="vm-pct">{{ vm.progress }}%</span>
|
||||
</div>
|
||||
<div class="vm-pct-block">
|
||||
<PercentageBar
|
||||
:model-value="vm.progress"
|
||||
:color-stops="vm.errorMsg ? { 0: '--error' } : { 99: '--primary', 100: '--success' }"
|
||||
:color-stops="vm.errorMsg ? { 100: '--error' } : vm.canceled ? { 100: '--darker' } : vm.progress >= 100 ? { 100: '--success' } : { 100: '--primary' }"
|
||||
preferred-direction="MORE"
|
||||
class="vm-bar"
|
||||
/>
|
||||
<span class="vm-pct text-muted mr-10">{{ vm.progress }}%</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="vm.progress === 100"
|
||||
v-if="vm.progress >= 100"
|
||||
class="step-label text-muted"
|
||||
>
|
||||
Finished Successfully
|
||||
@ -229,6 +248,12 @@ init();
|
||||
>
|
||||
{{ vm.errorMsg }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="vm.canceled"
|
||||
class="step-label text-muted"
|
||||
>
|
||||
Canceled
|
||||
</div>
|
||||
<div
|
||||
v-else-if="vm.currentStep"
|
||||
class="step-label text-muted"
|
||||
@ -252,7 +277,7 @@ init();
|
||||
<style lang="scss" scoped>
|
||||
.plan-name-cell {
|
||||
.plan-name {
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@ -266,14 +291,7 @@ init();
|
||||
.progress-cells {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vm-progress {
|
||||
&:not(:last-child) {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.vm-progress-header {
|
||||
@ -284,8 +302,7 @@ init();
|
||||
}
|
||||
|
||||
.vm-name {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vm-name-block {
|
||||
@ -299,8 +316,16 @@ init();
|
||||
}
|
||||
|
||||
.vm-pct {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
min-width: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.vm-pct-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.vm-bar {
|
||||
@ -308,13 +333,12 @@ init();
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
margin-top: 4px;
|
||||
line-height: 20px;
|
||||
|
||||
&.text-error {
|
||||
color: var(--error);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user