diff --git a/.changelog/25104.txt b/.changelog/25104.txt
new file mode 100644
index 000000000..45051b244
--- /dev/null
+++ b/.changelog/25104.txt
@@ -0,0 +1,3 @@
+```release-note:improvement
+ui: System, Batch and Sysbatch jobs get a "Revert to prev version" button on their main pages
+```
diff --git a/ui/app/models/job.js b/ui/app/models/job.js
index a6aa6f7e1..164ff8dbf 100644
--- a/ui/app/models/job.js
+++ b/ui/app/models/job.js
@@ -351,6 +351,18 @@ export default class Job extends Model {
return this.type === 'system' || this.type === 'sysbatch';
}
+ // version.Stable is determined by having an associated healthy deployment
+ // but System, Sysbatch, and Batch jobs do not have deployments.
+ // Use this as a boolean to determine if we should show the version stability badge
+ @computed('type')
+ get hasVersionStability() {
+ return (
+ this.type !== 'system' &&
+ this.type !== 'sysbatch' &&
+ this.type !== 'batch'
+ );
+ }
+
@belongsTo('job', { inverse: 'children' }) parent;
@hasMany('job', { inverse: 'parent' }) children;
@@ -455,12 +467,17 @@ export default class Job extends Model {
.any((version) => version.get('stable'));
}
- @computed('versions.@each.stable')
+ @computed('versions.@each.stable', 'aggregateAllocStatus.label')
get latestStableVersion() {
return this.versions.filterBy('stable').sortBy('number').reverse().slice(1)
.firstObject;
}
+ @computed('versions.[]', 'aggregateAllocStatus.label')
+ get latestVersion() {
+ return this.versions.sortBy('number').reverse().slice(1).firstObject;
+ }
+
get actions() {
return this.taskGroups.reduce((acc, taskGroup) => {
return acc.concat(
diff --git a/ui/app/templates/components/job-page/parts/title.hbs b/ui/app/templates/components/job-page/parts/title.hbs
index 391af3af6..2b008ad9b 100644
--- a/ui/app/templates/components/job-page/parts/title.hbs
+++ b/ui/app/templates/components/job-page/parts/title.hbs
@@ -101,6 +101,25 @@
@awaitingConfirmation={{this.revertTo.isRunning}}
@onConfirm={{perform this.revertTo this.job.latestStableVersion}}
/>
+ {{else if
+ (and
+ (not this.job.hasVersionStability)
+ this.job.latestVersion
+ )
+ }}
+
+
{{else}}
Version #{{this.version.number}}
-
- Stable
- {{this.version.stable}}
-
+
+ {{#if this.version.job.hasVersionStability}}
+
+ Stable
+ {{this.version.stable}}
+
+ {{else}}
+
+ {{/if}}
Submitted
{{format-ts this.version.submitTime}}
diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js
index 87609668d..ae5059d59 100644
--- a/ui/mirage/scenarios/default.js
+++ b/ui/mirage/scenarios/default.js
@@ -1237,6 +1237,7 @@ export function createRestartableJobs(server) {
shallow: true,
createAllocations: false,
groupAllocCount: 0,
+ type: 'service',
});
const nonRevertableJob = server.create('job', {
@@ -1246,6 +1247,16 @@ export function createRestartableJobs(server) {
shallow: true,
createAllocations: false,
groupAllocCount: 0,
+ type: 'service',
+ });
+
+ const revertableBatchJob = server.create('job', {
+ name: 'revertable-batch-job',
+ stopped: false,
+ status: 'dead',
+ noDeployments: true,
+ shallow: true,
+ type: 'batch',
});
// So it shows up as "Failed" instead of "Scaled Down"
@@ -1272,6 +1283,10 @@ export function createRestartableJobs(server) {
.all()
.filter((v) => v.jobId === nonRevertableJob.id)
.models.forEach((v) => v.destroy());
+ server.schema.jobVersions
+ .all()
+ .filter((v) => v.jobId === revertableBatchJob.id)
+ .models.forEach((v) => v.destroy());
server.create('job-version', {
job: revertableJob,
@@ -1322,6 +1337,22 @@ export function createRestartableJobs(server) {
noActiveDeployment: true,
});
+ server.create('job-version', {
+ job: revertableBatchJob,
+ namespace: revertableBatchJob.namespace,
+ version: 0,
+ stable: false, // <--- ignored by the UI by way of job.hasVersionStability
+ noActiveDeployment: true,
+ });
+
+ server.create('job-version', {
+ job: revertableBatchJob,
+ namespace: revertableBatchJob.namespace,
+ version: 1,
+ stable: false, // <--- ignored by the UI by way of job.hasVersionStability
+ noActiveDeployment: true,
+ });
+
server.schema.jobVersions
.all()
.filter((v) => v.jobId === revertableJob.id)
diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js
index 1896c743c..727e390be 100644
--- a/ui/tests/acceptance/job-detail-test.js
+++ b/ui/tests/acceptance/job-detail-test.js
@@ -864,6 +864,15 @@ module('Job Start/Stop/Revert/Edit and Resubmit', function (hooks) {
assert.ok(JobDetail.editAndResubmit.isPresent);
});
+ test('A batch job with a previous version can be reverted', async function (assert) {
+ const revertableSystemJob = server.db.jobs.findBy(
+ (j) => j.name === 'revertable-batch-job'
+ );
+ await JobDetail.visit({ id: revertableSystemJob.id });
+ assert.ok(JobDetail.revert.isPresent);
+ assert.equal(JobDetail.revert.text, 'Revert to last version (v0)');
+ });
+
test('Clicking the resubmit button navigates to the job definition page in edit mode', async function (assert) {
const job = server.db.jobs.findBy((j) => j.name === 'non-revertable-job');
await JobDetail.visit({ id: job.id });
diff --git a/ui/tests/acceptance/job-versions-test.js b/ui/tests/acceptance/job-versions-test.js
index 56b4ad623..73c07ae72 100644
--- a/ui/tests/acceptance/job-versions-test.js
+++ b/ui/tests/acceptance/job-versions-test.js
@@ -31,6 +31,7 @@ module('Acceptance | job versions', function (hooks) {
namespaceId: namespace.id,
createAllocations: false,
noDeployments: true,
+ type: 'service',
});
// Create some versions