mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
feat: forklift UI (#891)
* feat: forklift poc first commit Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat: add migration and plan detail pages with progress tracking Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): UI based on the FIGMA Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Fixed config based on new figma changes Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): New changes added based on new figma Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Add the succeded state Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * Merge the new APIs Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Applied fix to select the proper data on network. Added condition to show critical Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Added icons, critical state, fix grids, action button Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Change to use the proper provider instead of a hardcoded Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Added pagination UI Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): FIxed the routing for the extension Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): route and router doesnt work properly on extensions Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): fixed to fix the icon on extension Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Added overflow hidden to mapping target list Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Added better handling on the saving and testing Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Remove test flags Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Changed based on review Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): fixed title and button Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Design changes Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Changed to Wizard Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): added the provider wizard Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Fixed some UI changes Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Removed wrong configs used for testing Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): update size Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): small fixes Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): small fixes after Copilot Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): small fixes after Copilot Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): reverted settings.json changes Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): small fixes after Copilot Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Fix the way it is registered Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): Fixed the type for VMware Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat(forklift): small changes before rebase Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat: Added new API for the vms Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat: Fix some labels Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat: Added some final fixes to structure Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat: added loading to provider steps Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * feat: added comments from copilot review Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> --------- Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
import HarvesterResource from './harvester';
|
||||
import { HCI } from '../types';
|
||||
import { PRODUCT_NAME } from '../config/harvester';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
|
||||
export default class ForkliftPlan extends HarvesterResource {
|
||||
get listLocation() {
|
||||
return {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
||||
params: {
|
||||
product: this.$rootGetters['productId'],
|
||||
cluster: this.$rootGetters['clusterId'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get planFailed() {
|
||||
const conditions = this.status?.conditions || [];
|
||||
|
||||
return conditions.some((c) => c.type === 'Failed' && c.status === 'True');
|
||||
}
|
||||
|
||||
get planCritical() {
|
||||
const conditions = this.status?.conditions || [];
|
||||
|
||||
return conditions.some((c) => c.category === 'Critical' && c.status === 'True');
|
||||
}
|
||||
|
||||
get criticalMessages() {
|
||||
const conditions = this.status?.conditions || [];
|
||||
|
||||
return conditions
|
||||
.filter((c) => c.category === 'Critical' && c.status === 'True')
|
||||
.map((c) => c.message);
|
||||
}
|
||||
|
||||
get stateDescription() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.planCritical) {
|
||||
messages.push(...this.criticalMessages);
|
||||
}
|
||||
|
||||
return messages.length > 0 ? messages.join(' ') : null;
|
||||
}
|
||||
|
||||
get stateObj() {
|
||||
return {
|
||||
error: this.planFailed || this.planCritical,
|
||||
transitioning: this.isMigrating,
|
||||
message: this.stateDescription,
|
||||
};
|
||||
}
|
||||
|
||||
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 planSucceeded() {
|
||||
const migration = this.status?.migration;
|
||||
|
||||
return !!migration?.completed && !this.planFailed && !this.planCanceled;
|
||||
}
|
||||
|
||||
get stateDisplay() {
|
||||
if (this.planFailed) {
|
||||
return this.t('harvester.addons.vmMigration.plan.states.error');
|
||||
}
|
||||
|
||||
if (this.planCritical) {
|
||||
return this.t('harvester.addons.vmMigration.plan.states.error');
|
||||
}
|
||||
|
||||
if (this.planCanceled) {
|
||||
return this.t('harvester.addons.vmMigration.plan.states.canceled');
|
||||
}
|
||||
|
||||
if (this.isMigrating) {
|
||||
return this.t('harvester.addons.vmMigration.plan.states.inProgress');
|
||||
}
|
||||
|
||||
return this.t('harvester.addons.vmMigration.plan.states.active');
|
||||
}
|
||||
|
||||
get stateBackground() {
|
||||
if (this.planFailed) {
|
||||
return 'bg-error';
|
||||
}
|
||||
|
||||
if (this.planCritical) {
|
||||
return 'bg-error';
|
||||
}
|
||||
|
||||
if (this.planCanceled) {
|
||||
return 'bg-warning';
|
||||
}
|
||||
|
||||
if (this.isMigrating) {
|
||||
return 'bg-info';
|
||||
}
|
||||
|
||||
return 'bg-success';
|
||||
}
|
||||
|
||||
get isForkliftDashboard() {
|
||||
const route = this.currentRouter()?.currentRoute?.value;
|
||||
|
||||
return route?.name?.endsWith('-vm-migration');
|
||||
}
|
||||
|
||||
get _availableActions() {
|
||||
const canStop = this.isMigrating && !this.planCanceled;
|
||||
const canStart = !this.planSucceeded && (!this.isMigrating || this.planFailed || this.planCanceled || this.planCritical);
|
||||
const out = [];
|
||||
|
||||
if (canStart) {
|
||||
out.push({
|
||||
action: 'startMigration',
|
||||
enabled: true,
|
||||
icon: 'icon icon-play',
|
||||
label: this.isForkliftDashboard ? this.t('harvester.addons.vmMigration.plan.actions.restart') : this.t('harvester.addons.vmMigration.plan.actions.start'),
|
||||
});
|
||||
}
|
||||
|
||||
if (canStop) {
|
||||
out.push({
|
||||
action: 'stopMigration',
|
||||
enabled: true,
|
||||
icon: 'icon icon-pause',
|
||||
label: this.t('harvester.addons.vmMigration.plan.actions.stop'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isForkliftDashboard) {
|
||||
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;
|
||||
} else {
|
||||
out.push(...super._availableActions);
|
||||
|
||||
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-${ randomStr(5).toLowerCase() }`,
|
||||
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).
|
||||
* Use foreground propagation to ensure children are deleted before the parent.
|
||||
*/
|
||||
remove(opt = {}) {
|
||||
opt.params = { ...(opt.params || {}), propagationPolicy: 'Foreground' };
|
||||
|
||||
return this._remove(opt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import HarvesterResource from './harvester';
|
||||
import { PRODUCT_NAME } from '../config/harvester';
|
||||
|
||||
export default class ForkliftProvider extends HarvesterResource {
|
||||
get _createLocation() {
|
||||
return {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-provider-wizard`,
|
||||
params: {
|
||||
product: this.$rootGetters['productId'],
|
||||
cluster: this.$rootGetters['clusterId'],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get _editLocation() {
|
||||
return {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-provider-wizard`,
|
||||
params: {
|
||||
product: this.$rootGetters['productId'],
|
||||
cluster: this.$rootGetters['clusterId'],
|
||||
},
|
||||
query: { providerId: `${ this.metadata.namespace }/${ this.metadata.name }` },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleting a Provider cascades via ownerReferences set at creation time.
|
||||
* Kubernetes GC will automatically delete: Secret, NetworkMap, StorageMap.
|
||||
* Use foreground propagation to ensure children are deleted before the parent.
|
||||
*/
|
||||
remove(opt = {}) {
|
||||
opt.params = { ...(opt.params || {}), propagationPolicy: 'Foreground' };
|
||||
|
||||
return this._remove(opt);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user