diff --git a/.changelog/12713.txt b/.changelog/12713.txt
new file mode 100644
index 000000000..3094100bf
--- /dev/null
+++ b/.changelog/12713.txt
@@ -0,0 +1,3 @@
+```release-note:bug
+ui: Fixed a bug where volumes were being incorrectly linked when per_alloc=true
+```
\ No newline at end of file
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 000000000..f408cac8c
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1 @@
+{"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]}
\ No newline at end of file
diff --git a/ui/.template-lintrc.js b/ui/.template-lintrc.js
index fdfa6bd90..28057918d 100644
--- a/ui/.template-lintrc.js
+++ b/ui/.template-lintrc.js
@@ -7,5 +7,6 @@ module.exports = {
'no-action': 'off',
'no-invalid-interactive': 'off',
'no-inline-styles': 'off',
+ 'no-curly-component-invocation': { allow: ['format-volume-name'] },
},
};
diff --git a/ui/app/components/task-row.js b/ui/app/components/task-row.js
index 35343205d..64bccf1a5 100644
--- a/ui/app/components/task-row.js
+++ b/ui/app/components/task-row.js
@@ -5,6 +5,7 @@ import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { task, timeout } from 'ember-concurrency';
import { lazyClick } from '../helpers/lazy-click';
+
import {
classNames,
tagName,
diff --git a/ui/app/helpers/format-volume-name.js b/ui/app/helpers/format-volume-name.js
new file mode 100644
index 000000000..5ec96e281
--- /dev/null
+++ b/ui/app/helpers/format-volume-name.js
@@ -0,0 +1,18 @@
+import { helper } from '@ember/component/helper';
+
+/**
+ * Volume Name Formatter
+ *
+ * Usage: {{format-volume-name source=string isPerAlloc=boolean volumeExtension=string}}
+ *
+ * Outputs a title/link for volumes that are per_alloc-aware.
+ * (when a volume is per_alloc, its route location requires an additional extension)
+ */
+export function formatVolumeName(
+ _,
+ { source = '', isPerAlloc, volumeExtension }
+) {
+ return `${source}${isPerAlloc ? volumeExtension : ''}`;
+}
+
+export default helper(formatVolumeName);
diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js
index fb3aa8445..9f9e1a054 100644
--- a/ui/app/models/allocation.js
+++ b/ui/app/models/allocation.js
@@ -119,6 +119,13 @@ export default class Allocation extends Model {
return [];
}
+ // When per_alloc is set to true on a volume, the volumes are duplicated between active allocations.
+ // We differentiate them with a [#] suffix, inferred from a volume's allocation's name property.
+ @computed('name')
+ get volumeExtension() {
+ return this.name.substring(this.name.lastIndexOf('['));
+ }
+
@fragmentArray('task-state') states;
@fragmentArray('reschedule-event') rescheduleEvents;
diff --git a/ui/app/models/volume-definition.js b/ui/app/models/volume-definition.js
index 77e6c0819..20624d796 100644
--- a/ui/app/models/volume-definition.js
+++ b/ui/app/models/volume-definition.js
@@ -11,6 +11,7 @@ export default class VolumeDefinition extends Fragment {
@attr('string') source;
@attr('string') type;
@attr('boolean') readOnly;
+ @attr('boolean') perAlloc;
@equal('type', 'csi') isCSI;
@alias('taskGroup.job.namespace') namespace;
diff --git a/ui/app/templates/allocations/allocation/task/index.hbs b/ui/app/templates/allocations/allocation/task/index.hbs
index e6d62dc20..3191a1601 100644
--- a/ui/app/templates/allocations/allocation/task/index.hbs
+++ b/ui/app/templates/allocations/allocation/task/index.hbs
@@ -163,10 +163,19 @@
{{#if row.model.isCSI}}