diff --git a/.changelog/24168.txt b/.changelog/24168.txt new file mode 100644 index 000000000000..08e5bf4008b7 --- /dev/null +++ b/.changelog/24168.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Add an Edit From Version button as an option when reverting from an older job version +``` diff --git a/ui/app/adapters/job.js b/ui/app/adapters/job.js index 1d195876cdd8..b863ac5079c7 100644 --- a/ui/app/adapters/job.js +++ b/ui/app/adapters/job.js @@ -25,11 +25,11 @@ export default class JobAdapter extends WatchableNamespaceIDs { return this.ajax(url, 'GET'); } - fetchRawSpecification(job) { + fetchRawSpecification(job, version) { const url = addToPath( this.urlForFindRecord(job.get('id'), 'job', null, 'submission'), '', - 'version=' + job.get('version') + 'version=' + (version || job.get('version')) ); return this.ajax(url, 'GET'); } diff --git a/ui/app/components/job-version.js b/ui/app/components/job-version.js index 31606f9844e1..d777cea1cbed 100644 --- a/ui/app/components/job-version.js +++ b/ui/app/components/job-version.js @@ -3,6 +3,8 @@ * 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'; @@ -80,6 +82,11 @@ export default class JobVersion extends Component { this.isOpen = !this.isOpen; } + /** + * @type {'idle' | 'confirming'} + */ + @tracked cloneButtonStatus = 'idle'; + @task(function* () { try { const versionBeforeReversion = this.version.get('job.version'); @@ -88,6 +95,7 @@ export default class JobVersion extends Component { const versionAfterReversion = this.version.get('job.version'); if (versionBeforeReversion === versionAfterReversion) { + // TODO: I don't think this is ever hit, we have template checks against it. this.args.handleError({ level: 'warn', title: 'Reversion Had No Effect', @@ -108,6 +116,48 @@ export default class JobVersion extends Component { }) 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() { + console.log('cloneAsNewJob'); + try { + // TODO: copy the job definition over there. + console.log('Do I have submission info???', this.version); + let job = await this.version.get('job'); + let specification = await job.fetchRawSpecification(this.version.number); + console.log('Do I have specification???', specification); + let specificationSourceString = specification.Source; // TODO: should do some Format checking here, at the very least. + this.router.transitionTo('jobs.run', { + queryParams: { + sourceString: specificationSourceString, + }, + }); + } catch (e) { + this.args.handleError({ + level: 'danger', + title: 'Could Not Clone as New Job', + }); + } + } + @action handleKeydown(event) { if (event.key === 'Escape') { diff --git a/ui/app/models/job.js b/ui/app/models/job.js index eee53d647284..cca22f8fcaac 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -541,8 +541,8 @@ export default class Job extends Model { return this.store.adapterFor('job').fetchRawDefinition(this); } - fetchRawSpecification() { - return this.store.adapterFor('job').fetchRawSpecification(this); + fetchRawSpecification(version) { + return this.store.adapterFor('job').fetchRawSpecification(this, version); } forcePeriodic() { diff --git a/ui/app/routes/jobs/job/definition.js b/ui/app/routes/jobs/job/definition.js index 886683d6bb47..98277cbda2d6 100644 --- a/ui/app/routes/jobs/job/definition.js +++ b/ui/app/routes/jobs/job/definition.js @@ -5,29 +5,59 @@ // @ts-check import Route from '@ember/routing/route'; - +import { inject as service } from '@ember/service'; /** * Route for fetching and displaying a job's definition and specification. */ export default class DefinitionRoute extends Route { + @service notifications; + + queryParams = { + version: { + refreshModel: true, + }, + }; + /** * Fetch the job's definition, specification, and variables from the API. * * @returns {Promise} A promise that resolves to an object containing the job, definition, format, * specification, variableFlags, and variableLiteral. */ - async model() { + async model({ version }) { + version = +version; // query parameter is a string; convert to number + /** @type {import('../../../models/job').default} */ const job = this.modelFor('jobs.job'); if (!job) return; - const definition = await job.fetchRawDefinition(); + let definition; + + if (version) { + try { + const versionResponse = await job.getVersions(); + const versions = versionResponse.Versions; + definition = versions.findBy('Version', version); + if (!definition) { + throw new Error('Version not found'); + } + } catch (e) { + console.error('error fetching job version definition', e); + this.notifications.add({ + title: 'Error Fetching Job Version Definition', + message: `There was an error fetching the versions for this job: ${e.message}`, + color: 'critical', + }); + } + } else { + definition = await job.fetchRawDefinition(); + } let format = 'json'; // default to json in network request errors let specification; let variableFlags; let variableLiteral; try { - const specificationResponse = await job.fetchRawSpecification(); + const specificationResponse = await job.fetchRawSpecification(version); specification = specificationResponse?.Source ?? null; variableFlags = specificationResponse?.VariableFlags ?? null; variableLiteral = specificationResponse?.Variables ?? null; diff --git a/ui/app/routes/jobs/run/index.js b/ui/app/routes/jobs/run/index.js index 95c0bbffd2d2..ff48debc6bdb 100644 --- a/ui/app/routes/jobs/run/index.js +++ b/ui/app/routes/jobs/run/index.js @@ -21,6 +21,9 @@ export default class JobsRunIndexRoute extends Route { template: { refreshModel: true, }, + sourceString: { + refreshModel: true, + }, }; beforeModel(transition) { @@ -33,7 +36,7 @@ export default class JobsRunIndexRoute extends Route { } } - async model({ template }) { + async model({ template, sourceString }) { try { // When jobs are created with a namespace attribute, it is verified against // available namespaces to prevent redirecting to a non-existent namespace. @@ -45,6 +48,12 @@ export default class JobsRunIndexRoute extends Route { return this.store.createRecord('job', { _newDefinition: templateRecord.items.template, }); + } else if (sourceString) { + console.log('elsif', sourceString); + // Add an alert to the page to let the user know that they are submitting a job from a template, and that they should change the name! + return this.store.createRecord('job', { + _newDefinition: sourceString, + }); } else { return this.store.createRecord('job'); } diff --git a/ui/app/templates/components/job-version.hbs b/ui/app/templates/components/job-version.hbs index 587bb18c978b..789e66493357 100644 --- a/ui/app/templates/components/job-version.hbs +++ b/ui/app/templates/components/job-version.hbs @@ -81,6 +81,17 @@
{{#unless this.isCurrent}} {{#if (can "run job" namespace=this.version.job.namespace)}} + {{#if (eq this.cloneButtonStatus 'idle')}} + + + + + {{else if (eq this.cloneButtonStatus 'confirming')}} + {{!-- + Are you sure you want to revert to this version? + --}} + + + + {{/if}} {{else}}