Files
nomad/ui/app/components/job-version.js
Phil Renaud de96c3498b [ui] "Clone and Edit" functionality for versions (#24168)
* Edit from Version functionality

* Reworked as Clone and Revert

* Change name warning for cloning as new job, version 0 checking, and erroring on sourceless clone

* If you try to plan a new version of a job with a different name of the one you're editing, suggest they might want to run a new one instead

* A few code comments and log cleanup

* Scaffolding new acceptance tests

* A whack of fun new tests

* Unit test for version number url passing on fetchRawDef

* Bit of cleanup

* fetchRawDefinition gets version support at adapter layer

* Handle spec-not-available-but-definition-is for clone-as-new-job
2024-11-29 16:12:56 -05:00

263 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
// @ts-check
import Component from '@glimmer/component';
import { action, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import messageForError from 'nomad-ui/utils/message-from-adapter-error';
const changeTypes = ['Added', 'Deleted', 'Edited'];
export default class JobVersion extends Component {
@service store;
@service notifications;
@service router;
@alias('args.version') version;
@alias('args.diff') diff;
@tracked isOpen = false;
@tracked isEditing = false;
@tracked editableTag;
// Passes through to the job-diff component
verbose = true;
constructor() {
super(...arguments);
this.initializeEditableTag();
this.versionsDidUpdate();
}
@action versionsDidUpdate() {
if (this.args.diffsExpanded && this.diff) {
this.isOpen = true;
}
}
initializeEditableTag() {
if (this.version.versionTag) {
this.editableTag = this.store.createRecord('versionTag', {
name: this.version.versionTag.name,
description: this.version.versionTag.description,
});
} else {
this.editableTag = this.store.createRecord('versionTag');
}
this.editableTag.versionNumber = this.version.number;
this.editableTag.jobName = this.version.get('job.plainId');
}
@computed('diff')
get changeCount() {
const diff = this.diff;
const taskGroups = diff.TaskGroups || [];
if (!diff) {
return 0;
}
return (
fieldChanges(diff) +
taskGroups.reduce(arrayOfFieldChanges, 0) +
(taskGroups.mapBy('Tasks') || [])
.reduce(flatten, [])
.reduce(arrayOfFieldChanges, 0)
);
}
@computed('version.{number,job.version}')
get isCurrent() {
return this.version.number === this.version.get('job.version');
}
@action
toggleDiff() {
this.isOpen = !this.isOpen;
}
/**
* @type {'idle' | 'confirming'}
*/
@tracked cloneButtonStatus = 'idle';
@task(function* () {
try {
const versionBeforeReversion = this.version.get('job.version');
yield this.version.revertTo();
yield this.version.get('job').reload();
const versionAfterReversion = this.version.get('job.version');
if (versionBeforeReversion === versionAfterReversion) {
this.args.handleError({
level: 'warn',
title: 'Reversion Had No Effect',
description:
'Reverting to an identical older version doesnt produce a new version',
});
} else {
const job = this.version.get('job');
this.router.transitionTo('jobs.job.index', job.get('idWithNamespace'));
}
} catch (e) {
this.args.handleError({
level: 'danger',
title: 'Could Not Revert',
description: messageForError(e, 'revert'),
});
}
})
revertTo;
@action async cloneAsNewVersion() {
try {
this.router.transitionTo(
'jobs.job.definition',
this.version.get('job.idWithNamespace'),
{
queryParams: {
isEditing: true,
version: this.version.number,
},
}
);
} catch (e) {
this.args.handleError({
level: 'danger',
title: 'Could Not Edit from Version',
});
}
}
@action async cloneAsNewJob() {
const job = await this.version.get('job');
try {
const specification = await job.fetchRawSpecification(
this.version.number
);
this.router.transitionTo('jobs.run', {
queryParams: {
sourceString: specification.Source,
},
});
return;
} catch (specError) {
try {
// If submission info is not available, try to fetch the raw definition
const definition = await job.fetchRawDefinition(this.version.number);
this.router.transitionTo('jobs.run', {
queryParams: {
sourceString: JSON.stringify(definition, null, 2),
},
});
} catch (defError) {
// Both methods failed, show error
this.args.handleError({
level: 'danger',
title: 'Could Not Clone as New Job',
description: messageForError(defError),
});
}
}
}
@action
handleKeydown(event) {
if (event.key === 'Escape') {
this.cancelEditTag();
}
}
@action
toggleEditTag() {
this.isEditing = !this.isEditing;
}
@action
async saveTag(event) {
event.preventDefault();
try {
if (!this.editableTag.name) {
this.notifications.add({
title: 'Error Tagging Job Version',
message: 'Tag name is required',
color: 'critical',
});
return;
}
const savedTag = await this.editableTag.save();
this.version.versionTag = savedTag;
this.version.versionTag.setProperties({
...savedTag.toJSON(),
});
this.initializeEditableTag();
this.isEditing = false;
this.notifications.add({
title: 'Job Version Tagged',
color: 'success',
});
} catch (error) {
console.log('error tagging job version', error);
this.notifications.add({
title: 'Error Tagging Job Version',
message: messageForError(error),
color: 'critical',
});
}
}
@action
cancelEditTag() {
this.isEditing = false;
this.initializeEditableTag();
}
@action
async deleteTag() {
try {
await this.store
.adapterFor('version-tag')
.deleteTag(this.editableTag.jobName, this.editableTag.name);
this.notifications.add({
title: 'Job Version Un-Tagged',
color: 'success',
});
this.version.versionTag = null;
this.initializeEditableTag();
this.isEditing = false;
} catch (error) {
this.notifications.add({
title: 'Error Un-Tagging Job Version',
message: messageForError(error),
color: 'critical',
});
}
}
}
const flatten = (accumulator, array) => accumulator.concat(array);
const countChanges = (total, field) =>
changeTypes.includes(field.Type) ? total + 1 : total;
function fieldChanges(diff) {
return (
(diff.Fields || []).reduce(countChanges, 0) +
(diff.Objects || []).reduce(arrayOfFieldChanges, 0)
);
}
function arrayOfFieldChanges(count, diff) {
if (!diff) {
return count;
}
return count + fieldChanges(diff);
}