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
|
targetNamespace: Target Namespace
|
||||||
migrationMode: Migration Mode
|
migrationMode: Migration Mode
|
||||||
coldMigration: Cold Migration
|
coldMigration: Cold Migration
|
||||||
vmsWillShutDown: VMs will shut down
|
|
||||||
virtualMachines: Virtual Machines
|
virtualMachines: Virtual Machines
|
||||||
warningTitle: "Cold Migration Warning"
|
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"
|
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 HarvesterResource from './harvester';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
export default class ForkliftPlan extends HarvesterResource {
|
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.
|
* Deleting a Plan cascades via ownerReferences set at creation time.
|
||||||
* Kubernetes GC will automatically delete: Migration, NetworkMap, StorageMap, Provider (→ Secret).
|
* Kubernetes GC will automatically delete: Migration, NetworkMap, StorageMap, Provider (→ Secret).
|
||||||
@ -9,7 +164,7 @@ export default class ForkliftPlan extends HarvesterResource {
|
|||||||
remove() {
|
remove() {
|
||||||
const opt = { ...arguments };
|
const opt = { ...arguments };
|
||||||
|
|
||||||
opt.params = { propagationPolicy: 'Foreground' };
|
opt.params = { propagationPolicy: 'Background' };
|
||||||
|
|
||||||
return this._remove(opt);
|
return this._remove(opt);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,7 @@ const createdProvider = ref(null);
|
|||||||
const createdSecret = ref(null);
|
const createdSecret = ref(null);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const testPassed = ref(false);
|
const testPassed = ref(false);
|
||||||
|
const testing = ref(false);
|
||||||
|
|
||||||
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
||||||
|
|
||||||
@ -71,6 +72,7 @@ const cancel = async() => {
|
|||||||
const testConnection = async(buttonCb) => {
|
const testConnection = async(buttonCb) => {
|
||||||
testResult.value = null;
|
testResult.value = null;
|
||||||
testError.value = null;
|
testError.value = null;
|
||||||
|
testing.value = true;
|
||||||
|
|
||||||
if (!providerName.value || !url.value || !username.value || !password.value) {
|
if (!providerName.value || !url.value || !username.value || !password.value) {
|
||||||
testError.value = t('harvester.addons.forklift.configureProvider.testMissingFields');
|
testError.value = t('harvester.addons.forklift.configureProvider.testMissingFields');
|
||||||
@ -191,6 +193,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
if (connected) {
|
if (connected) {
|
||||||
testPassed.value = true;
|
testPassed.value = true;
|
||||||
testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess');
|
testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess');
|
||||||
|
testing.value = false;
|
||||||
buttonCb(true);
|
buttonCb(true);
|
||||||
} else {
|
} else {
|
||||||
if (createdProvider.value) {
|
if (createdProvider.value) {
|
||||||
@ -203,6 +206,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout');
|
testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout');
|
||||||
|
testing.value = false;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -220,6 +224,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testError.value = err.message || t('harvester.addons.forklift.configureProvider.testFailed');
|
testError.value = err.message || t('harvester.addons.forklift.configureProvider.testFailed');
|
||||||
|
testing.value = false;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -399,7 +404,7 @@ init();
|
|||||||
{{ t('generic.cancel') }}
|
{{ t('generic.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid || testing"
|
||||||
:action-label="t('harvester.addons.forklift.configureProvider.save')"
|
:action-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||||
:waiting-label="t('harvester.addons.forklift.configureProvider.save')"
|
:waiting-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||||
:success-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.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.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(', ') || '-';
|
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 }`;
|
currentStep = step.name || `Step ${ idx + 1 }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step.error) {
|
if (step.error && !errorMsg) {
|
||||||
errorMsg = (step.error.reasons || []).join('; ') || 'Error';
|
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 {
|
return {
|
||||||
vmName: vm.name || vm.id || 'Unknown',
|
vmName: vm.name || vm.id || 'Unknown',
|
||||||
@ -90,10 +106,11 @@ const rows = computed(() => {
|
|||||||
progress: overallProgress,
|
progress: overallProgress,
|
||||||
currentStep,
|
currentStep,
|
||||||
errorMsg,
|
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;
|
return plan;
|
||||||
});
|
});
|
||||||
@ -209,16 +226,18 @@ init();
|
|||||||
<span class="vm-name">{{ vm.vmName }}</span>
|
<span class="vm-name">{{ vm.vmName }}</span>
|
||||||
<span class="text-muted vm-id">id: {{ vm.vmId }}</span>
|
<span class="text-muted vm-id">id: {{ vm.vmId }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="vm-pct">{{ vm.progress }}%</span>
|
|
||||||
</div>
|
</div>
|
||||||
<PercentageBar
|
<div class="vm-pct-block">
|
||||||
:model-value="vm.progress"
|
<PercentageBar
|
||||||
:color-stops="vm.errorMsg ? { 0: '--error' } : { 99: '--primary', 100: '--success' }"
|
:model-value="vm.progress"
|
||||||
preferred-direction="MORE"
|
:color-stops="vm.errorMsg ? { 100: '--error' } : vm.canceled ? { 100: '--darker' } : vm.progress >= 100 ? { 100: '--success' } : { 100: '--primary' }"
|
||||||
class="vm-bar"
|
preferred-direction="MORE"
|
||||||
/>
|
class="vm-bar"
|
||||||
|
/>
|
||||||
|
<span class="vm-pct text-muted mr-10">{{ vm.progress }}%</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="vm.progress === 100"
|
v-if="vm.progress >= 100"
|
||||||
class="step-label text-muted"
|
class="step-label text-muted"
|
||||||
>
|
>
|
||||||
Finished Successfully
|
Finished Successfully
|
||||||
@ -229,6 +248,12 @@ init();
|
|||||||
>
|
>
|
||||||
{{ vm.errorMsg }}
|
{{ vm.errorMsg }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="vm.canceled"
|
||||||
|
class="step-label text-muted"
|
||||||
|
>
|
||||||
|
Canceled
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="vm.currentStep"
|
v-else-if="vm.currentStep"
|
||||||
class="step-label text-muted"
|
class="step-label text-muted"
|
||||||
@ -252,7 +277,7 @@ init();
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.plan-name-cell {
|
.plan-name-cell {
|
||||||
.plan-name {
|
.plan-name {
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,14 +291,7 @@ init();
|
|||||||
.progress-cells {
|
.progress-cells {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 16px;
|
||||||
}
|
|
||||||
|
|
||||||
.vm-progress {
|
|
||||||
&:not(:last-child) {
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-progress-header {
|
.vm-progress-header {
|
||||||
@ -284,8 +302,7 @@ init();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vm-name {
|
.vm-name {
|
||||||
font-weight: 600;
|
font-size: 14px;
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-name-block {
|
.vm-name-block {
|
||||||
@ -299,8 +316,16 @@ init();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vm-pct {
|
.vm-pct {
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
min-width: 40px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-pct-block {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-bar {
|
.vm-bar {
|
||||||
@ -308,13 +333,12 @@ init();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.step-label {
|
.step-label {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
|
||||||
&.text-error {
|
&.text-error {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user