From 1a3bb326b79ad0917d623db2a41d8b128738e11b Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Fri, 14 Jan 2022 10:11:22 -0500 Subject: [PATCH 01/24] temp: namespace model refact --- ui/app/routes/jobs/job.js | 2 +- ui/app/templates/components/job-row.hbs | 7 +++++-- ui/config/environment.js | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js index a39c1618315..584a85e3cbb 100644 --- a/ui/app/routes/jobs/job.js +++ b/ui/app/routes/jobs/job.js @@ -11,7 +11,7 @@ export default class JobRoute extends Route { @service token; serialize(model) { - return { job_name: model.get('plainId') }; + return { job_name: JSON.parse(model.get('id')).join('@') }; } model(params, transition) { diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index eeeb523e424..ce8b9e73186 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -1,7 +1,7 @@ @@ -47,7 +47,10 @@ {{/if}} {{else}} - + {{/if}} \ No newline at end of file diff --git a/ui/config/environment.js b/ui/config/environment.js index d0bfae30743..4fc81004510 100644 --- a/ui/config/environment.js +++ b/ui/config/environment.js @@ -25,7 +25,7 @@ module.exports = function (environment) { APP: { blockingQueries: true, - mirageScenario: 'topoMedium', + mirageScenario: 'smallCluster', mirageWithNamespaces: false, mirageWithTokens: true, mirageWithRegions: true, From 898cda014eadf73393b3885ca7c6a436a8f0315a Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 19 Jan 2022 11:20:00 -0500 Subject: [PATCH 02/24] temp: csi refactor --- ui/app/controllers/csi/volumes/index.js | 5 +- ui/app/models/volume.js | 7 + ui/app/routes/csi/volumes/volume.js | 12 +- .../allocations/allocation/task/index.hbs | 149 ++++++++--- ui/app/templates/components/task-row.hbs | 70 ++++- ui/app/templates/csi/volumes/index.hbs | 242 ++++++++++------- ui/app/templates/jobs/job/task-group.hbs | 247 +++++++++++++----- ui/config/environment.js | 2 +- ui/tests/acceptance/allocation-detail-test.js | 4 +- ui/tests/acceptance/client-detail-test.js | 2 +- ui/tests/acceptance/volume-detail-test.js | 2 +- ui/tests/helpers/module-for-job.js | 2 +- 12 files changed, 520 insertions(+), 224 deletions(-) diff --git a/ui/app/controllers/csi/volumes/index.js b/ui/app/controllers/csi/volumes/index.js index a7ca5b9efdb..15fe6f9f423 100644 --- a/ui/app/controllers/csi/volumes/index.js +++ b/ui/app/controllers/csi/volumes/index.js @@ -113,10 +113,7 @@ export default class IndexController extends Controller.extend( @action gotoVolume(volume, event) { lazyClick([ - () => - this.transitionToRoute('csi.volumes.volume', volume.get('plainId'), { - queryParams: { volumeNamespace: volume.get('namespace.name') }, - }), + () => this.transitionToRoute('csi.volumes.volume', volume.get('idWithNamespace')), event, ]); } diff --git a/ui/app/models/volume.js b/ui/app/models/volume.js index f7de4b1f0b2..c230cfda833 100644 --- a/ui/app/models/volume.js +++ b/ui/app/models/volume.js @@ -40,6 +40,13 @@ export default class Volume extends Model { @attr('number') controllersHealthy; @attr('number') controllersExpected; + @computed('plainId') + get idWithNamespace() { + // does this handle default namespace -- I think the backend handles this for us + // but the client would always need to recreate that logic + return `${this.plainId}@${this.belongsTo('namespace').id()}`; + } + @computed('controllersHealthy', 'controllersExpected') get controllersHealthyProportion() { return this.controllersHealthy / this.controllersExpected; diff --git a/ui/app/routes/csi/volumes/volume.js b/ui/app/routes/csi/volumes/volume.js index e5722374a0d..dbf0b44504f 100644 --- a/ui/app/routes/csi/volumes/volume.js +++ b/ui/app/routes/csi/volumes/volume.js @@ -21,13 +21,17 @@ export default class VolumeRoute extends Route.extend(WithWatchers) { } serialize(model) { - return { volume_name: model.get('plainId') }; + return { volume_name: JSON.parse(model.get('id')).join('@') }; } - model(params, transition) { - const namespace = transition.to.queryParams.namespace; - const name = params.volume_name; + model(params) { + // Issue with naming collissions + const url = params.volume_name.split('@'); + const namespace = url.pop(); + const name = url.join(''); + const fullId = JSON.stringify([`csi/${name}`, namespace || 'default']); + return RSVP.hash({ volume: this.store.findRecord('volume', fullId, { reload: true }), namespaces: this.store.findAll('namespace'), diff --git a/ui/app/templates/allocations/allocation/task/index.hbs b/ui/app/templates/allocations/allocation/task/index.hbs index e033567c7b8..fb406618f1a 100644 --- a/ui/app/templates/allocations/allocation/task/index.hbs +++ b/ui/app/templates/allocations/allocation/task/index.hbs @@ -5,23 +5,40 @@
-

{{this.error.title}}

-

{{this.error.description}}

+

+ {{this.error.title}} +

+

+ {{this.error.description}} +

- +
{{/if}} -

{{this.model.name}} {{#if this.model.isConnectProxy}} {{/if}} - {{this.model.state}} + + {{this.model.state}} +
{{#if this.model.isRunning}} @@ -30,7 +47,8 @@ @job={{this.model.task.taskGroup.job}} @taskGroup={{this.model.task.taskGroup}} @allocation={{this.model.allocation}} - @task={{this.model.task}} /> + @task={{this.model.task}} + />
+ @onConfirm={{perform this.restartTask}} + /> {{/if}}

-
- Task Details + + Task Details + - Started At + + Started At + {{format-ts this.model.startedAt}} {{#if this.model.finishedAt}} - Finished At + + Finished At + {{format-ts this.model.finishedAt}} {{/if}} - Driver + + Driver + {{this.model.task.driver}} - Lifecycle - {{this.model.task.lifecycleName}} + + Lifecycle + + + {{this.model.task.lifecycleName}} +
-
Resource Utilization @@ -86,13 +115,16 @@
{{else}}
-

Task isn't running

-

Only running tasks utilize resources.

+

+ Task isn't running +

+

+ Only running tasks utilize resources. +

{{/if}}
- {{#if this.model.task.volumeMounts.length}}
@@ -101,52 +133,97 @@
- Name - Destination - Permissions - Client Source + + Name + + + Destination + + + Permissions + + + Client Source + - {{row.model.volume}} - {{row.model.destination}} - {{if row.model.readOnly "Read" "Read/Write"}} + + {{row.model.volume}} + + + + {{row.model.destination}} + + + + {{if row.model.readOnly "Read" "Read/Write"}} + {{#if row.model.isCSI}} - - {{row.model.source}} + + {{row.model.volume}} {{else}} {{row.model.source}} {{/if}} + + + {{row.model.destination}} + + + + {{if row.model.readOnly "Read" "Read/Write"}} + + + {{row.model.source}} +
{{/if}} -
Recent Events
- + - Time - Type - Description + + Time + + + Type + + + Description + - {{format-ts row.model.time}} - {{row.model.type}} + + {{format-ts row.model.time}} + + + {{row.model.type}} + {{#if row.model.message}} {{row.model.message}} {{else}} - No message + + No message + {{/if}} @@ -154,4 +231,4 @@
- + \ No newline at end of file diff --git a/ui/app/templates/components/task-row.hbs b/ui/app/templates/components/task-row.hbs index 151b5cca7c6..521f0b088d0 100644 --- a/ui/app/templates/components/task-row.hbs +++ b/ui/app/templates/components/task-row.hbs @@ -1,34 +1,54 @@ {{#unless this.task.driverStatus.healthy}} - + {{x-icon "alert-triangle" class="is-warning"}} {{/unless}} - + {{this.task.name}} {{#if this.task.isConnectProxy}} {{/if}} -{{this.task.state}} + + {{this.task.state}} + {{#if this.task.events.lastObject.message}} {{this.task.events.lastObject.message}} {{else}} - No message + + No message + {{/if}} -{{format-ts this.task.events.lastObject.time}} + + {{format-ts this.task.events.lastObject.time}} +
    {{#each this.task.task.volumeMounts as |volume|}}
  • - {{volume.volume}}: + + {{volume.volume}} + : + {{#if volume.isCSI}} - + {{volume.source}} {{else}} @@ -43,15 +63,26 @@ {{#if (and (not this.cpu) this.fetchStats.isRunning)}} ... {{else if this.statsError}} - + {{x-icon "alert-triangle" class="is-warning"}} {{else}} -
{{/if}} {{#if this.isForbidden}} - {{else}} - {{#if this.sortedVolumes}} - - - - Name + {{else if this.sortedVolumes}} + + + + + Name + + {{#if this.system.shouldShowNamespaces}} + + Namespace + + {{/if}} + + Volume Health + + + Controller Health + + + Node Health + + + Provider + + + # Allocs + + + + + + + {{row.model.name}} + + {{#if this.system.shouldShowNamespaces}} - Namespace - {{/if}} - Volume Health - Controller Health - Node Health - Provider - # Allocs - - - - - - {{row.model.name}} - + + {{row.model.namespace.name}} - {{#if this.system.shouldShowNamespaces}} - {{row.model.namespace.name}} + {{/if}} + + {{if row.model.schedulable "Schedulable" "Unschedulable"}} + + + {{#if row.model.controllerRequired}} + {{if (gt row.model.controllersHealthy 0) "Healthy" "Unhealthy"}} + ( + {{row.model.controllersHealthy}} + / + {{row.model.controllersExpected}} + ) + {{else if (gt row.model.controllersExpected 0)}} + {{if (gt row.model.controllersHealthy 0) "Healthy" "Unhealthy"}} + ( + {{row.model.controllersHealthy}} + / + {{row.model.controllersExpected}} + ) + {{else}} + + Node Only + {{/if}} - {{if row.model.schedulable "Schedulable" "Unschedulable"}} - - {{#if row.model.controllerRequired}} - {{if (gt row.model.controllersHealthy 0) "Healthy" "Unhealthy"}} - ({{row.model.controllersHealthy}}/{{row.model.controllersExpected}}) - {{else}} - {{#if (gt row.model.controllersExpected 0)}} - {{if (gt row.model.controllersHealthy 0) "Healthy" "Unhealthy"}} - ({{row.model.controllersHealthy}}/{{row.model.controllersExpected}}) - {{else}} - Node Only - {{/if}} - {{/if}} - - - {{if (gt row.model.nodesHealthy 0) "Healthy" "Unhealthy"}} - ({{row.model.nodesHealthy}}/{{row.model.nodesExpected}}) - - {{row.model.provider}} - {{row.model.allocationCount}} - - - -
- - -
-
- {{else}} -
- {{#if (eq this.visibleVolumes.length 0)}} -

No Volumes

-

- This namespace currently has no CSI Volumes. -

- {{else if this.searchTerm}} -

No Matches

-

- No volumes match the term {{this.searchTerm}} -

- {{/if}} + + + {{if (gt row.model.nodesHealthy 0) "Healthy" "Unhealthy"}} + ( + {{row.model.nodesHealthy}} + / + {{row.model.nodesExpected}} + ) + + + {{row.model.provider}} + + + {{row.model.allocationCount}} + + + + +
+ +
- {{/if}} + + {{else}} +
+ {{#if (eq this.visibleVolumes.length 0)}} +

+ No Volumes +

+

+ This namespace currently has no CSI Volumes. +

+ {{else if this.searchTerm}} +

+ No Matches +

+

+ No volumes match the term + + {{this.searchTerm}} + +

+ {{/if}} +
{{/if}} - + \ No newline at end of file diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index ce4468b5342..2402b5df63a 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -2,64 +2,116 @@ {{page-title "Task group " this.model.name " - Job " this.model.job.name}}
    -
  • Overview
  • +
  • + + Overview + +

- {{this.model.name}} + + {{this.model.name}} +
{{#if this.model.scaling}} - - Count - + + Count + {{/if}}

-
- Task Group Details - - # Tasks {{this.model.tasks.length}} - Reserved CPU {{format-scheduled-hertz this.model.reservedCPU}} + + Task Group Details + + + + # Tasks + + {{this.model.tasks.length}} + + + + Reserved CPU + + {{format-scheduled-hertz this.model.reservedCPU}} + - Reserved Memory + + Reserved Memory + {{format-scheduled-bytes this.model.reservedMemory start="MiB"}} {{#if (gt this.model.reservedMemoryMax this.model.reservedMemory)}} - ({{format-scheduled-bytes this.model.reservedMemoryMax start="MiB"}} Max) + ({{format-scheduled-bytes this.model.reservedMemoryMax start="MiB"}}Max) {{/if}} - Reserved Disk {{format-scheduled-bytes this.model.reservedEphemeralDisk start="MiB"}} + + + Reserved Disk + + {{format-scheduled-bytes this.model.reservedEphemeralDisk start="MiB"}} + {{#if this.model.scaling}} - Count Range - {{this.model.scaling.min}} to {{this.model.scaling.max}} + + + Count Range + + {{this.model.scaling.min}} + to + {{this.model.scaling.max}} - Scaling Policy? + + + Scaling Policy? + {{if this.model.scaling.policy "Yes" "No"}} {{/if}}
-
-
Allocation Status {{this.allocations.length}}
+
+ Allocation Status + + {{this.allocations.length}} + +
- +
    {{#each chart.data as |datum index|}} -
  1. +
  2. {{/each}} @@ -100,62 +152,102 @@ @source={{this.sortedAllocations}} @size={{this.pageSize}} @page={{this.currentPage}} - @class="allocations" as |p|> + @class="allocations" as |p| + > + @class="with-foot" as |t| + > - ID - Created - Modified - Status - Version - Client - Volume - CPU - Memory + + ID + + + Created + + + Modified + + + Status + + + Version + + + Client + + + Volume + + + CPU + + + Memory + - +
    - {{else}} - {{#if this.allocations.length}} -
    -
    -

    No Matches

    -

    No allocations match the term {{this.searchTerm}}

    -
    + {{else if this.allocations.length}} +
    +
    +

    + No Matches +

    +

    + No allocations match the term + + {{this.searchTerm}} + +

    - {{else}} -
    -
    -

    No Allocations

    -

    No allocations have been placed.

    -
    +
    + {{else}} +
    +
    +

    + No Allocations +

    +

    + No allocations have been placed. +

    - {{/if}} +
    {{/if}}
    - - {{#if this.model.scaleState.isVisible}} {{#if this.shouldShowScaleEventTimeline}}
    @@ -167,7 +259,6 @@
{{/if}} -
Recent Scaling Events @@ -177,7 +268,6 @@
{{/if}} - {{#if this.model.volumes.length}}
@@ -186,29 +276,46 @@
- Name - Type - Source - Permissions + + Name + + + Type + + + Source + + + Permissions + {{#if row.model.isCSI}} - + {{row.model.name}} {{else}} {{row.model.name}} {{/if}} - {{row.model.type}} - {{row.model.source}} - {{if row.model.readOnly "Read" "Read/Write"}} + + {{row.model.type}} + + + {{row.model.source}} + + + {{if row.model.readOnly "Read" "Read/Write"}} +
{{/if}} -
+ \ No newline at end of file diff --git a/ui/config/environment.js b/ui/config/environment.js index 4fc81004510..5cfb8fc4b08 100644 --- a/ui/config/environment.js +++ b/ui/config/environment.js @@ -26,7 +26,7 @@ module.exports = function (environment) { APP: { blockingQueries: true, mirageScenario: 'smallCluster', - mirageWithNamespaces: false, + mirageWithNamespaces: true, mirageWithTokens: true, mirageWithRegions: true, showStorybookLink: process.env.STORYBOOK_LINK === 'true', diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index a22ff469fa1..733fafcf122 100644 --- a/ui/tests/acceptance/allocation-detail-test.js +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -76,7 +76,7 @@ module('Acceptance | allocation detail', function (hooks) { await Allocation.details.visitJob(); assert.equal( currentURL(), - `/jobs/${job.id}`, + `/jobs/${job.id}@default`, 'Job link navigates to the job' ); @@ -502,7 +502,7 @@ module('Acceptance | allocation detail (preemptions)', function (hooks) { await Allocation.preempter.visitJob(); assert.equal( currentURL(), - `/jobs/${preempterJob.id}`, + `/jobs/${preempterJob.id}@default`, 'Clicking the preempter job link navigates to the preempter job page' ); diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js index ff7827cef0c..9866acd0ea0 100644 --- a/ui/tests/acceptance/client-detail-test.js +++ b/ui/tests/acceptance/client-detail-test.js @@ -305,7 +305,7 @@ module('Acceptance | client detail', function (hooks) { assert.equal( currentURL(), - `/jobs/${job.id}`, + `/jobs/${job.id}@default`, 'Allocation rows link to the job detail page for the allocation' ); }); diff --git a/ui/tests/acceptance/volume-detail-test.js b/ui/tests/acceptance/volume-detail-test.js index fb264110acf..0d98322ad4b 100644 --- a/ui/tests/acceptance/volume-detail-test.js +++ b/ui/tests/acceptance/volume-detail-test.js @@ -47,7 +47,7 @@ module('Acceptance | volume detail', function (hooks) { }); test('/csi/volumes/:id should show the volume name in the title', async function (assert) { - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); assert.equal(document.title, `CSI Volume ${volume.name} - Nomad`); assert.equal(VolumeDetail.title, volume.name); diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 53d373ae812..d5e932ac773 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -145,7 +145,7 @@ export default function moduleForJob( const encodedStatus = encodeURIComponent(JSON.stringify([status])); const expectedURL = new URL( urlWithNamespace( - `/jobs/${job.name}/clients?status=${encodedStatus}`, + `/jobs/${job.name}@default/clients?status=${encodedStatus}`, job.namespace ), window.location From 793f9c26d331159edc33e8ed8482b1113b282948 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Thu, 10 Feb 2022 10:55:32 -0500 Subject: [PATCH 03/24] temp: bug in region selector causing failing test --- ui/app/components/breadcrumbs/job.hbs | 6 ++---- ui/app/components/job-row.js | 4 +--- ui/app/routes/jobs/job.js | 11 ++++++---- .../allocations/allocation/task/index.hbs | 11 ---------- ui/app/templates/components/global-header.hbs | 14 ++++++++++--- ui/app/templates/components/job-row.hbs | 7 +------ ui/app/templates/jobs/index.hbs | 9 ++++++--- ui/tests/acceptance/regions-test.js | 1 + ui/tests/acceptance/volume-detail-test.js | 20 +++++++++---------- ui/tests/acceptance/volumes-list-test.js | 6 +++--- 10 files changed, 42 insertions(+), 47 deletions(-) diff --git a/ui/app/components/breadcrumbs/job.hbs b/ui/app/components/breadcrumbs/job.hbs index 87e5e2a7b81..d3956485f06 100644 --- a/ui/app/components/breadcrumbs/job.hbs +++ b/ui/app/components/breadcrumbs/job.hbs @@ -12,8 +12,7 @@
  • @@ -30,8 +29,7 @@
  • diff --git a/ui/app/components/job-row.js b/ui/app/components/job-row.js index c353813c0bd..b16c8279459 100644 --- a/ui/app/components/job-row.js +++ b/ui/app/components/job-row.js @@ -26,8 +26,6 @@ export default class JobRow extends Component { @action gotoJob() { const { job } = this; - this.router.transitionTo('jobs.job', job.plainId, { - queryParams: { namespace: job.get('namespace.name') }, - }); + this.router.transitionTo('jobs.job.index', job); } } diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js index 584a85e3cbb..68421f3e276 100644 --- a/ui/app/routes/jobs/job.js +++ b/ui/app/routes/jobs/job.js @@ -11,13 +11,16 @@ export default class JobRoute extends Route { @service token; serialize(model) { + debugger; return { job_name: JSON.parse(model.get('id')).join('@') }; } - model(params, transition) { - const namespace = transition.to.queryParams.namespace || 'default'; - const name = params.job_name; - const fullId = JSON.stringify([name, namespace]); + model(params) { + const url = params.job_name.split('@'); + const namespace = url.pop(); + const name = url.join(''); + + const fullId = JSON.stringify([name, namespace || 'default']); return this.store .findRecord('job', fullId, { reload: true }) diff --git a/ui/app/templates/allocations/allocation/task/index.hbs b/ui/app/templates/allocations/allocation/task/index.hbs index fb406618f1a..49e90ec3cac 100644 --- a/ui/app/templates/allocations/allocation/task/index.hbs +++ b/ui/app/templates/allocations/allocation/task/index.hbs @@ -172,17 +172,6 @@ {{row.model.source}} {{/if}} - - - {{row.model.destination}} - - - - {{if row.model.readOnly "Read" "Read/Write"}} - - - {{row.model.source}} - diff --git a/ui/app/templates/components/global-header.hbs b/ui/app/templates/components/global-header.hbs index ac01bde0aa0..b18b49d93ba 100644 --- a/ui/app/templates/components/global-header.hbs +++ b/ui/app/templates/components/global-header.hbs @@ -24,12 +24,20 @@ {{/if}} {{#if this.system.agent.config.UI.Consul.BaseUIURL}} - + Consul {{/if}} {{#if this.system.agent.config.UI.Vault.BaseUIURL}} - + Vault {{/if}} @@ -50,4 +58,4 @@ {{yield}} -
  • + \ No newline at end of file diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index ce8b9e73186..3f936aa30b7 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -1,10 +1,5 @@ - + {{this.job.name}} diff --git a/ui/app/templates/jobs/index.hbs b/ui/app/templates/jobs/index.hbs index 903453591f9..7abf8bca33e 100644 --- a/ui/app/templates/jobs/index.hbs +++ b/ui/app/templates/jobs/index.hbs @@ -1,4 +1,4 @@ -{{page-title "Jobs"}} +{{!-- {{page-title "Jobs"}}
    @@ -44,7 +44,10 @@ @options={{this.optionsNamespaces}} @selection={{this.qpNamespace}} @onSelect={{action - (queue (action this.cacheNamespace) (action this.setFacetQueryParam "qpNamespace")) + (queue + (action this.cacheNamespace) + (action this.setFacetQueryParam "qpNamespace") + ) }} /> {{/if}} @@ -202,4 +205,4 @@ {{/if}}
    {{/if}} -
    \ No newline at end of file + --}} \ No newline at end of file diff --git a/ui/tests/acceptance/regions-test.js b/ui/tests/acceptance/regions-test.js index fc3ba70268f..b5000c72cf2 100644 --- a/ui/tests/acceptance/regions-test.js +++ b/ui/tests/acceptance/regions-test.js @@ -152,6 +152,7 @@ module('Acceptance | regions (many)', function (hooks) { await Allocation.visit({ id: server.db.allocations[0].id }); + await this.pauseTest(); await selectChoose('[data-test-region-switcher-parent]', newRegion); assert.ok(currentURL().includes('/jobs?'), 'Back at the jobs page'); diff --git a/ui/tests/acceptance/volume-detail-test.js b/ui/tests/acceptance/volume-detail-test.js index 0d98322ad4b..ede06242ee3 100644 --- a/ui/tests/acceptance/volume-detail-test.js +++ b/ui/tests/acceptance/volume-detail-test.js @@ -34,12 +34,12 @@ module('Acceptance | volume detail', function (hooks) { }); test('it passes an accessibility audit', async function (assert) { - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); await a11yAudit(assert); }); test('/csi/volumes/:id should have a breadcrumb trail linking back to Volumes and Storage', async function (assert) { - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); assert.equal(Layout.breadcrumbFor('csi.index').text, 'Storage'); assert.equal(Layout.breadcrumbFor('csi.volumes').text, 'Volumes'); @@ -54,7 +54,7 @@ module('Acceptance | volume detail', function (hooks) { }); test('/csi/volumes/:id should list additional details for the volume below the title', async function (assert) { - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); assert.ok( VolumeDetail.health.includes( @@ -75,7 +75,7 @@ module('Acceptance | volume detail', function (hooks) { writeAllocations.forEach((alloc) => assignWriteAlloc(volume, alloc)); readAllocations.forEach((alloc) => assignReadAlloc(volume, alloc)); - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); assert.equal(VolumeDetail.writeAllocations.length, writeAllocations.length); writeAllocations @@ -95,7 +95,7 @@ module('Acceptance | volume detail', function (hooks) { writeAllocations.forEach((alloc) => assignWriteAlloc(volume, alloc)); readAllocations.forEach((alloc) => assignReadAlloc(volume, alloc)); - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); assert.equal(VolumeDetail.readAllocations.length, readAllocations.length); readAllocations @@ -126,7 +126,7 @@ module('Acceptance | volume detail', function (hooks) { 0 ); - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); VolumeDetail.writeAllocations.objectAt(0).as((allocationRow) => { assert.equal( @@ -198,28 +198,28 @@ module('Acceptance | volume detail', function (hooks) { const allocation = server.create('allocation'); assignWriteAlloc(volume, allocation); - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); await VolumeDetail.writeAllocations.objectAt(0).visit(); assert.equal(currentURL(), `/allocations/${allocation.id}`); }); test('when there are no write allocations, the table presents an empty state', async function (assert) { - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); assert.ok(VolumeDetail.writeTableIsEmpty); assert.equal(VolumeDetail.writeEmptyState.headline, 'No Write Allocations'); }); test('when there are no read allocations, the table presents an empty state', async function (assert) { - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); assert.ok(VolumeDetail.readTableIsEmpty); assert.equal(VolumeDetail.readEmptyState.headline, 'No Read Allocations'); }); test('the constraints table shows access mode and attachment mode', async function (assert) { - await VolumeDetail.visit({ id: volume.id }); + await VolumeDetail.visit({ id: `${volume.id}@default` }); assert.equal(VolumeDetail.constraints.accessMode, volume.accessMode); assert.equal( diff --git a/ui/tests/acceptance/volumes-list-test.js b/ui/tests/acceptance/volumes-list-test.js index 0876aa36a1b..7d11ebd1cb9 100644 --- a/ui/tests/acceptance/volumes-list-test.js +++ b/ui/tests/acceptance/volumes-list-test.js @@ -93,7 +93,7 @@ module('Acceptance | volumes list', function (hooks) { assert.equal(volumeRow.controllerHealth, controllerHealthStr); assert.equal( volumeRow.nodeHealth, - `${nodeHealthStr} (${volume.nodesHealthy}/${volume.nodesExpected})` + `${nodeHealthStr} ( ${volume.nodesHealthy} / ${volume.nodesExpected} )` ); assert.equal(volumeRow.provider, volume.provider); assert.equal(volumeRow.allocations, readAllocs.length + writeAllocs.length); @@ -110,7 +110,7 @@ module('Acceptance | volumes list', function (hooks) { await VolumesList.volumes.objectAt(0).clickName(); assert.equal( currentURL(), - `/csi/volumes/${volume.id}?namespace=${secondNamespace.id}` + `/csi/volumes/${volume.id}@${secondNamespace.id}` ); await VolumesList.visit({ namespace: '*' }); @@ -119,7 +119,7 @@ module('Acceptance | volumes list', function (hooks) { await VolumesList.volumes.objectAt(0).clickRow(); assert.equal( currentURL(), - `/csi/volumes/${volume.id}?namespace=${secondNamespace.id}` + `/csi/volumes/${volume.id}@${secondNamespace.id}` ); }); From 498187b2dab7e607835328595df233d812c16e94 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 16 Feb 2022 15:01:25 +0100 Subject: [PATCH 04/24] feat: add `idWithNamespace`-getter job model --- ui/app/models/job.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index b157592efa5..2e921383e86 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -35,6 +35,17 @@ export default class Job extends Model { @attr() periodicDetails; @attr() parameterizedDetails; + @computed('plainId') + get idWithNamespace() { + const namespaceId = this.belongsTo('namespace').id(); + + if (!namespaceId || namespaceId === 'default') { + return this.plainId; + } else { + return `${this.plainId}@${namespaceId}`; + } + } + @computed('periodic', 'parameterized', 'dispatched') get hasChildren() { return this.periodic || (this.parameterized && !this.dispatched); From 87e49559f86ec247266770d6b95bf60f3811fabd Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 16 Feb 2022 15:02:06 +0100 Subject: [PATCH 05/24] refact: render jobs.index template again --- ui/app/templates/jobs/index.hbs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/app/templates/jobs/index.hbs b/ui/app/templates/jobs/index.hbs index 7abf8bca33e..e08aa1335e7 100644 --- a/ui/app/templates/jobs/index.hbs +++ b/ui/app/templates/jobs/index.hbs @@ -1,4 +1,4 @@ -{{!-- {{page-title "Jobs"}} +{{page-title "Jobs"}}
    @@ -112,13 +112,15 @@ @@ -205,4 +207,4 @@ {{/if}}
    {{/if}} -
    --}} \ No newline at end of file + \ No newline at end of file From 12339d718ad2d84927c464a04e06c530d390c693 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 16 Feb 2022 15:02:39 +0100 Subject: [PATCH 06/24] refact: use `idWithNamespace` in job-row links --- ui/app/components/job-row.js | 2 +- ui/app/templates/components/job-row.hbs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/app/components/job-row.js b/ui/app/components/job-row.js index b16c8279459..107d47fc93a 100644 --- a/ui/app/components/job-row.js +++ b/ui/app/components/job-row.js @@ -26,6 +26,6 @@ export default class JobRow extends Component { @action gotoJob() { const { job } = this; - this.router.transitionTo('jobs.job.index', job); + this.router.transitionTo('jobs.job.index', job.idWithNamespace); } } diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index 3f936aa30b7..3146a931a2e 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -1,5 +1,9 @@ - + {{this.job.name}} From 134fdf96821cb91ae66a67026949045faef9619a Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 16 Feb 2022 15:03:02 +0100 Subject: [PATCH 07/24] feat: improve namespace handling job-route --- ui/app/routes/jobs/job.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js index 68421f3e276..85de41c25c1 100644 --- a/ui/app/routes/jobs/job.js +++ b/ui/app/routes/jobs/job.js @@ -11,16 +11,13 @@ export default class JobRoute extends Route { @service token; serialize(model) { - debugger; return { job_name: JSON.parse(model.get('id')).join('@') }; } model(params) { - const url = params.job_name.split('@'); - const namespace = url.pop(); - const name = url.join(''); + const [name, namespace = 'default'] = params.job_name.split('@'); - const fullId = JSON.stringify([name, namespace || 'default']); + const fullId = JSON.stringify([name, namespace]); return this.store .findRecord('job', fullId, { reload: true }) From 6b142f8b0d3abc6051bdb9a0b301e66f89ec64f0 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 16 Feb 2022 15:22:41 +0100 Subject: [PATCH 08/24] fix: job-dispatch tests after namespace changes --- ui/app/components/job-dispatch.js | 9 ++++--- ui/tests/acceptance/job-dispatch-test.js | 31 ++++++++++-------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/ui/app/components/job-dispatch.js b/ui/app/components/job-dispatch.js index d0a5af2d23a..3300c5304cc 100644 --- a/ui/app/components/job-dispatch.js +++ b/ui/app/components/job-dispatch.js @@ -104,9 +104,12 @@ export default class JobDispatch extends Component { const dispatch = yield this.args.job.dispatch(paramValues, this.payload); // Navigate to the newly created instance. - this.router.transitionTo('jobs.job', dispatch.DispatchedJobID, { - queryParams: { namespace: this.args.job.get('namespace.name') }, - }); + const namespaceId = this.args.job.belongsTo('namespace').id(); + const jobId = namespaceId + ? `${dispatch.DispatchedJobID}@${namespaceId}` + : dispatch.DispatchedJobID; + + this.router.transitionTo('jobs.job', jobId); } catch (err) { const error = messageFromAdapterError(err) || 'Could not dispatch job'; this.errors.pushObject(error); diff --git a/ui/tests/acceptance/job-dispatch-test.js b/ui/tests/acceptance/job-dispatch-test.js index ef476431074..e7f0e011370 100644 --- a/ui/tests/acceptance/job-dispatch-test.js +++ b/ui/tests/acceptance/job-dispatch-test.js @@ -54,12 +54,12 @@ function moduleForJobDispatch(title, jobFactory) { }); test('it passes an accessibility audit', async function (assert) { - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.visit({ id: `${job.id}@${namespace.name}` }); await a11yAudit(assert); }); test('the dispatch button is displayed with management token', async function (assert) { - await JobDetail.visit({ id: job.id, namespace: namespace.name }); + await JobDetail.visit({ id: `${job.id}@${namespace.name}` }); assert.notOk(JobDetail.dispatchButton.isDisabled); }); @@ -82,7 +82,7 @@ function moduleForJobDispatch(title, jobFactory) { clientToken.policyIds = [policy.id]; clientToken.save(); - await JobDetail.visit({ id: job.id, namespace: namespace.name }); + await JobDetail.visit({ id: `${job.id}@${namespace.name}` }); assert.notOk(JobDetail.dispatchButton.isDisabled); // Reset clientToken policies. @@ -93,12 +93,12 @@ function moduleForJobDispatch(title, jobFactory) { test('the dispatch button is disabled when not allowed', async function (assert) { window.localStorage.nomadTokenSecret = clientToken.secretId; - await JobDetail.visit({ id: job.id, namespace: namespace.name }); + await JobDetail.visit({ id: `${job.id}@${namespace.name}` }); assert.ok(JobDetail.dispatchButton.isDisabled); }); test('all meta fields are displayed', async function (assert) { - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.visit({ id: `${job.id}@${namespace.name}` }); assert.equal( JobDispatch.metaFields.length, job.parameterizedJob.MetaOptional.length + @@ -107,7 +107,7 @@ function moduleForJobDispatch(title, jobFactory) { }); test('required meta fields are properly indicated', async function (assert) { - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.visit({ id: `${job.id}@${namespace.name}` }); JobDispatch.metaFields.forEach((f) => { const hasIndicator = f.label.includes(REQUIRED_INDICATOR); @@ -136,10 +136,7 @@ function moduleForJobDispatch(title, jobFactory) { }, }); - await JobDispatch.visit({ - id: jobWithoutMeta.id, - namespace: namespace.name, - }); + await JobDispatch.visit({ id: `${jobWithoutMeta.id}@${namespace.name}` }); assert.ok(JobDispatch.dispatchButton.isPresent); }); @@ -147,7 +144,7 @@ function moduleForJobDispatch(title, jobFactory) { job.parameterizedJob.Payload = 'forbidden'; job.save(); - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.visit({ id: `${job.id}@${namespace.name}` }); assert.ok(JobDispatch.payload.emptyMessage.isPresent); assert.notOk(JobDispatch.payload.editor.isPresent); @@ -170,8 +167,7 @@ function moduleForJobDispatch(title, jobFactory) { }); await JobDispatch.visit({ - id: jobPayloadRequired.id, - namespace: namespace.name, + id: `${jobPayloadRequired.id}@${namespace.name}`, }); let payloadTitle = JobDispatch.payload.title; @@ -181,8 +177,7 @@ function moduleForJobDispatch(title, jobFactory) { ); await JobDispatch.visit({ - id: jobPayloadOptional.id, - namespace: namespace.name, + id: `${jobPayloadOptional.id}@${namespace.name}`, }); payloadTitle = JobDispatch.payload.title; @@ -199,7 +194,7 @@ function moduleForJobDispatch(title, jobFactory) { ).length; } - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.visit({ id: `${job.id}@${namespace.name}` }); // Fill form. JobDispatch.metaFields.map((f) => f.field.input('meta value')); @@ -222,7 +217,7 @@ function moduleForJobDispatch(title, jobFactory) { job.parameterizedJob.Payload = 'forbidden'; job.save(); - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.visit({ id: `${job.id}@${namespace.name}` }); // Fill only optional meta params. JobDispatch.optionalMetaFields.map((f) => f.field.input('meta value')); @@ -237,7 +232,7 @@ function moduleForJobDispatch(title, jobFactory) { job.parameterizedJob.Payload = 'required'; job.save(); - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.visit({ id: `${job.id}@${namespace.name}` }); await JobDispatch.dispatchButton.click(); assert.ok(JobDispatch.hasError, 'Dispatch error message is shown'); From 5db6da4c590fa9a4d7dcccc771b1daa415268dc7 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 16 Feb 2022 15:30:51 +0100 Subject: [PATCH 09/24] fix: job-versions-test We need to adapt the test due to recent namespace changes. --- ui/tests/acceptance/job-versions-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/job-versions-test.js b/ui/tests/acceptance/job-versions-test.js index af43ded0a28..596c5df8880 100644 --- a/ui/tests/acceptance/job-versions-test.js +++ b/ui/tests/acceptance/job-versions-test.js @@ -30,7 +30,7 @@ module('Acceptance | job versions', function (hooks) { const managementToken = server.create('token'); window.localStorage.nomadTokenSecret = managementToken.secretId; - await Versions.visit({ id: job.id, namespace: namespace.id }); + await Versions.visit({ id: `${job.id}@${namespace.id}` }); }); test('it passes an accessibility audit', async function (assert) { From 4c387574361b9c5978ee4f5e34bd68a6849d4ace Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 16 Feb 2022 17:42:26 +0100 Subject: [PATCH 10/24] refact: use `idWithNamespace` in serialize hook jobs.job This will give us 'correct' URLs for free when we only pass a `job`-model to a `LinkTo` that links to the `jobs.job.*`-routes. --- ui/app/routes/jobs/job.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js index 85de41c25c1..e5a50d939b2 100644 --- a/ui/app/routes/jobs/job.js +++ b/ui/app/routes/jobs/job.js @@ -11,7 +11,7 @@ export default class JobRoute extends Route { @service token; serialize(model) { - return { job_name: JSON.parse(model.get('id')).join('@') }; + return { job_name: model.get('idWithNamespace') }; } model(params) { From 70f855e5da330ff7405d4d4fcc676450206d8264 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 16 Feb 2022 17:44:16 +0100 Subject: [PATCH 11/24] refact: don't pass namespace as query-param in job-subnav The new ID handling gives us this behavior for free and we don't need to drill the namespace down through all the route-layers anymore. --- ui/app/templates/components/job-subnav.hbs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ui/app/templates/components/job-subnav.hbs b/ui/app/templates/components/job-subnav.hbs index f9c11e8e6f6..13d260d7c79 100644 --- a/ui/app/templates/components/job-subnav.hbs +++ b/ui/app/templates/components/job-subnav.hbs @@ -3,7 +3,6 @@
  • @@ -24,7 +22,6 @@
  • @@ -35,7 +32,6 @@
  • @@ -46,7 +42,6 @@
  • @@ -56,7 +51,6 @@
  • @@ -67,7 +61,6 @@
  • From a4244e63de9616d74ed2a6605225f41f8e514477 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 16 Feb 2022 17:45:31 +0100 Subject: [PATCH 12/24] fix: some test fixes module-for-job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * less clever™ metaprogramming when checking for expectedURL * clicking slices job-client-status-summary needs to change its behavior and not pass the namespace query-param anymore. --- .../parts/job-client-status-summary.js | 1 - ui/tests/helpers/module-for-job.js | 38 +++++++------------ 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/ui/app/components/job-page/parts/job-client-status-summary.js b/ui/app/components/job-page/parts/job-client-status-summary.js index 6aa683ce39c..2464b2dd3c6 100644 --- a/ui/app/components/job-page/parts/job-client-status-summary.js +++ b/ui/app/components/job-page/parts/job-client-status-summary.js @@ -24,7 +24,6 @@ export default class JobClientStatusSummary extends Component { this.router.transitionTo('jobs.job.clients', this.job, { queryParams: { status: JSON.stringify(statusFilter), - namespace: this.job.get('namespace.name'), }, }); } diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index d5e932ac773..8914b5ae720 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -41,7 +41,7 @@ export default function moduleForJob( if (!job.namespace || job.namespace === 'default') { await JobDetail.visit({ id: job.id }); } else { - await JobDetail.visit({ id: job.id, namespace: job.namespace }); + await JobDetail.visit({ id: `${job.id}@${job.namespace}` }); } const hasClientStatus = ['system', 'sysbatch'].includes(job.type); @@ -248,13 +248,12 @@ export function moduleForJobWithClientStatus( test('the subnav links to clients', async function (assert) { await JobDetail.tabFor('clients').visit(); - assert.equal( - currentURL(), - urlWithNamespace( - `/jobs/${encodeURIComponent(job.id)}/clients`, - job.namespace - ) - ); + + const expectedURL = job.namespace + ? `/jobs/${job.id}@${job.namespace}/clients` + : `/jobs/${job.id}/clients`; + + assert.equal(currentURL(), expectedURL); }); test('job status summary is shown in the overview', async function (assert) { @@ -289,23 +288,12 @@ export function moduleForJobWithClientStatus( await slice.click(); const encodedStatus = encodeURIComponent(JSON.stringify([status])); - const expectedURL = new URL( - urlWithNamespace( - `/jobs/${job.name}/clients?status=${encodedStatus}`, - job.namespace - ), - window.location - ); - const gotURL = new URL(currentURL(), window.location); - assert.deepEqual(gotURL.pathname, expectedURL.pathname); - // Sort and compare URL query params. - gotURL.searchParams.sort(); - expectedURL.searchParams.sort(); - assert.equal( - gotURL.searchParams.toString(), - expectedURL.searchParams.toString() - ); + const expectedURL = job.namespace + ? `/jobs/${job.name}@${job.namespace}/clients?status=${encodedStatus}` + : `/jobs/${job.name}/clients?status=${encodedStatus}`; + + assert.deepEqual(currentURL(), expectedURL, 'url is correct'); }); for (var testName in additionalTests) { @@ -368,6 +356,6 @@ async function visitJobDetailPage({ id, namespace }) { if (!namespace || namespace === 'default') { await JobDetail.visit({ id }); } else { - await JobDetail.visit({ id, namespace }); + await JobDetail.visit({ id: `${id}@${namespace}` }); } } From efbf42b36b8d24981262d085cb00518bc4e72205 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 11:56:29 +0100 Subject: [PATCH 13/24] fix: allocations page tests regarding job links Default namespaced jobs don't append the `@default`-id anymore due to recent `jobs.job#serialize` changes. --- .../allocations/allocation/index.hbs | 179 ++++++++++++++---- ui/tests/acceptance/allocation-detail-test.js | 5 +- 2 files changed, 145 insertions(+), 39 deletions(-) diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index e049e7e2443..0069a9fba85 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -5,11 +5,19 @@
    -

    {{this.error.title}}

    +

    {{this.error.title}}

    {{this.error.description}}

    - +
    @@ -17,13 +25,19 @@

    - Allocation {{this.model.name}} - {{this.model.clientStatus}} + Allocation + {{this.model.name}} + {{this.model.clientStatus}}
    {{#if this.model.isRunning}}
    - +
    + @disabled={{or + this.stopAllocation.isRunning + this.restartAllocation.isRunning + }} + @onConfirm={{perform this.stopAllocation}} + /> + @disabled={{or + this.stopAllocation.isRunning + this.restartAllocation.isRunning + }} + @onConfirm={{perform this.restartAllocation}} + /> {{/if}}

    @@ -55,13 +77,24 @@
    -
    +
    Allocation Details Job - {{this.model.job.name}} + {{this.model.job.name}} Client - {{this.model.node.shortId}} + {{this.model.node.shortId}}
    @@ -74,16 +107,26 @@ {{#if this.model.isRunning}}
    - +
    - +
    {{else}}
    -

    Allocation isn't running

    -

    Only running allocations utilize resources.

    +

    Allocation isn't running

    +

    Only running allocations utilize + resources.

    {{/if}}
    @@ -95,13 +138,17 @@
    Tasks
    -
    +
    {{#if this.sortedStates.length}} + @class="is-striped" + as |t| + > Name @@ -116,13 +163,20 @@ + @onClick={{action "taskClick" row.model.allocation row.model}} + /> {{else}}
    -

    No Tasks

    -

    Allocations will not have tasks until they are in a running state.

    +

    No Tasks

    +

    Allocations will not have tasks until they are in a running state.

    {{/if}}
    @@ -144,7 +198,11 @@ {{row.model.label}} - {{row.model.hostIp}}:{{row.model.value}} + {{row.model.hostIp}}:{{row.model.value}} {{row.model.to}} @@ -173,11 +231,21 @@ {{row.model.name}} {{row.model.portLabel}} - {{join ", " row.model.tags}} + {{join + ", " + row.model.tags + }} {{row.model.onUpdate}} - {{if row.model.connect "Yes" "No"}} + {{if + row.model.connect + "Yes" + "No" + }} - {{#each row.model.connect.sidecarService.proxy.upstreams as |upstream|}} + {{#each + row.model.connect.sidecarService.proxy.upstreams + as |upstream| + }} {{upstream.destinationName}}:{{upstream.localBindPort}} {{/each}} @@ -207,48 +275,81 @@
    - + {{this.preempter.clientStatus}} - {{this.preempter.name}} - {{this.preempter.shortId}} + {{this.preempter.name}} + {{this.preempter.shortId}} Job - {{this.preempter.job.name}} + {{this.preempter.job.name}} Priority - {{this.preempter.job.priority}} + {{this.preempter.job.priority}} Client - {{this.preempter.node.shortId}} + {{this.preempter.node.shortId}} Reserved CPU - {{format-scheduled-hertz this.preempter.resources.cpu}} + {{format-scheduled-hertz + this.preempter.resources.cpu + }} Reserved Memory - {{format-scheduled-bytes this.preempter.resources.memory start="MiB"}} + {{format-scheduled-bytes + this.preempter.resources.memory + start="MiB" + }}
    {{else}}

    Allocation is gone

    -

    This allocation has been stopped and garbage collected.

    +

    This allocation has been stopped and + garbage collected.

    {{/if}}
    {{/if}} - {{#if (and this.model.preemptedAllocations.isFulfilled this.model.preemptedAllocations.length)}} + {{#if + (and + this.model.preemptedAllocations.isFulfilled + this.model.preemptedAllocations.length + ) + }}
    Preempted Allocations
    + @class="allocations is-isolated" + as |t| + > ID @@ -262,7 +363,11 @@ Memory - +
    diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index 733fafcf122..ed447d45699 100644 --- a/ui/tests/acceptance/allocation-detail-test.js +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -76,7 +76,7 @@ module('Acceptance | allocation detail', function (hooks) { await Allocation.details.visitJob(); assert.equal( currentURL(), - `/jobs/${job.id}@default`, + `/jobs/${job.id}`, 'Job link navigates to the job' ); @@ -499,10 +499,11 @@ module('Acceptance | allocation detail (preemptions)', function (hooks) { ); await Allocation.visit({ id: allocation.id }); + await Allocation.preempter.visitJob(); assert.equal( currentURL(), - `/jobs/${preempterJob.id}@default`, + `/jobs/${preempterJob.id}`, 'Clicking the preempter job link navigates to the preempter job page' ); From 51775a04b28d338de8070097cc26c1ae614f65e3 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 12:41:33 +0100 Subject: [PATCH 14/24] fix: client-detail-test no default namespace param Recent changes changed the behavior of not adding the `@default` -namespace - we need to adapt the tests accordingly --- ui/tests/acceptance/client-detail-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js index 9866acd0ea0..ff7827cef0c 100644 --- a/ui/tests/acceptance/client-detail-test.js +++ b/ui/tests/acceptance/client-detail-test.js @@ -305,7 +305,7 @@ module('Acceptance | client detail', function (hooks) { assert.equal( currentURL(), - `/jobs/${job.id}@default`, + `/jobs/${job.id}`, 'Allocation rows link to the job detail page for the allocation' ); }); From 31cbd77a7adce97c4d68091529630510180ea346 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 12:42:23 +0100 Subject: [PATCH 15/24] =?UTF-8?q?fix:=20less=20cleverness=E2=84=A2=20when?= =?UTF-8?q?=20checking=20currentURL=20job-details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no need to check the namespace query-param anymore with `urlWithNamespace` but some tests still are using this. We refactor the tests to be less clever and check the URL in a more manual approach by explicitly defining how the URL should look like if a job belongs to a namespace. --- ui/tests/helpers/module-for-job.js | 58 +++++++++++++++--------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 8914b5ae720..327481acc95 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -51,52 +51,52 @@ export default function moduleForJob( }); test('visiting /jobs/:job_id', async function (assert) { - assert.equal( - currentURL(), - urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}`, job.namespace) - ); + const expectedURL = job.namespace + ? `/jobs/${job.name}@${job.namespace}` + : `/jobs/${job.name}`; + + assert.equal(decodeURIComponent(currentURL()), expectedURL); assert.equal(document.title, `Job ${job.name} - Nomad`); }); test('the subnav links to overview', async function (assert) { await JobDetail.tabFor('overview').visit(); - assert.equal( - currentURL(), - urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}`, job.namespace) - ); + + const expectedURL = job.namespace + ? `/jobs/${job.name}@${job.namespace}` + : `/jobs/${job.name}`; + + assert.equal(decodeURIComponent(currentURL()), expectedURL); }); test('the subnav links to definition', async function (assert) { await JobDetail.tabFor('definition').visit(); - assert.equal( - currentURL(), - urlWithNamespace( - `/jobs/${encodeURIComponent(job.id)}/definition`, - job.namespace - ) - ); + + const expectedURL = job.namespace + ? `/jobs/${job.name}@${job.namespace}/definition` + : `/jobs/${job.name}/definition`; + + assert.equal(decodeURIComponent(currentURL()), expectedURL); }); test('the subnav links to versions', async function (assert) { await JobDetail.tabFor('versions').visit(); - assert.equal( - currentURL(), - urlWithNamespace( - `/jobs/${encodeURIComponent(job.id)}/versions`, - job.namespace - ) - ); + + const expectedURL = job.namespace + ? `/jobs/${job.name}@${job.namespace}/versions` + : `/jobs/${job.name}/versions`; + + assert.equal(decodeURIComponent(currentURL()), expectedURL); }); test('the subnav links to evaluations', async function (assert) { await JobDetail.tabFor('evaluations').visit(); - assert.equal( - currentURL(), - urlWithNamespace( - `/jobs/${encodeURIComponent(job.id)}/evaluations`, - job.namespace - ) - ); + + const expectedURL = job.namespace + ? `/jobs/${job.name}@${job.namespace}/evaluations` + : `/jobs/${job.name}/evaluations`; + + assert.equal(decodeURIComponent(currentURL()), expectedURL); }); test('the title buttons are dependent on job status', async function (assert) { From dadbb0e16fad1eae87a765197075218859c8ade1 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 12:45:01 +0100 Subject: [PATCH 16/24] fix: anonymous policy test job-details We need to access job-details differently when they have a namespace due to recent namespace changes - we need to make the tests reflect that. --- ui/tests/acceptance/job-detail-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index e6027ef3c95..c8c8dd06196 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -322,9 +322,9 @@ module('Acceptance | job detail (with namespaces)', function (hooks) { }); await JobDetail.visit({ - id: job.id, - namespace: server.db.namespaces[1].name, + id: `${job.id}@${server.db.namespaces[1].name}`, }); + assert.notOk(JobDetail.execButton.isDisabled); }); From 221311da79d542bf888761fe5473cda6b8a3db10 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 12:59:58 +0100 Subject: [PATCH 17/24] fix: pack-detail test We need to change the way we access `JobDetail`-pages based on recent namespace changes. --- ui/tests/acceptance/job-detail-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index c8c8dd06196..d378545ea65 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -361,7 +361,7 @@ module('Acceptance | job detail (with namespaces)', function (hooks) { }, }); - await JobDetail.visit({ id: jobFromPack.id, namespace }); + await JobDetail.visit({ id: `${jobFromPack.id}@${namespace}` }); assert.ok(JobDetail.packTag, 'Pack tag is present'); assert.equal( JobDetail.packStatFor('name').text, From 550a5c01e8c0f5ea95e0cc0ebdcd5aef6ba56f99 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 13:22:15 +0100 Subject: [PATCH 18/24] fix: use `@` with remaining `JobDetail.visit`s --- ui/tests/acceptance/job-detail-test.js | 28 ++++++++++++++------------ 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index d378545ea65..4dc485cf1b4 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -251,13 +251,16 @@ module('Acceptance | job detail (with namespaces)', function (hooks) { test('it passes an accessibility audit', async function (assert) { const namespace = server.db.namespaces.find(job.namespaceId); - await JobDetail.visit({ id: job.id, namespace: namespace.name }); + await JobDetail.visit({ id: `${job.id}@${namespace.name}` }); await a11yAudit(assert); }); test('when there are namespaces, the job detail page states the namespace for the job', async function (assert) { const namespace = server.db.namespaces.find(job.namespaceId); - await JobDetail.visit({ id: job.id, namespace: namespace.name }); + + await JobDetail.visit({ + id: `${job.id}@${namespace.name}`, + }); assert.ok( JobDetail.statFor('namespace').text, @@ -301,7 +304,8 @@ module('Acceptance | job detail (with namespaces)', function (hooks) { assert.notOk(JobDetail.execButton.isDisabled); const secondNamespace = server.db.namespaces[1]; - await JobDetail.visit({ id: job2.id, namespace: secondNamespace.name }); + await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` }); + assert.ok(JobDetail.execButton.isDisabled); }); @@ -338,14 +342,13 @@ module('Acceptance | job detail (with namespaces)', function (hooks) { }); await JobDetail.visit({ - id: job.id, - namespace: server.db.namespaces[1].name, + id: `${job.id}@${server.db.namespaces[1].name}`, }); + assert.notOk(JobDetail.metaTable, 'Meta table not present'); await JobDetail.visit({ - id: jobWithMeta.id, - namespace: server.db.namespaces[1].name, + id: `${jobWithMeta.id}@${server.db.namespaces[1].name}`, }); assert.ok(JobDetail.metaTable, 'Meta table is present'); }); @@ -362,6 +365,7 @@ module('Acceptance | job detail (with namespaces)', function (hooks) { }); await JobDetail.visit({ id: `${jobFromPack.id}@${namespace}` }); + assert.ok(JobDetail.packTag, 'Pack tag is present'); assert.equal( JobDetail.packStatFor('name').text, @@ -388,8 +392,7 @@ module('Acceptance | job detail (with namespaces)', function (hooks) { window.localStorage.nomadTokenSecret = managementToken.secretId; await JobDetail.visit({ - id: job.id, - namespace: server.db.namespaces[1].name, + id: `${job.id}@${server.db.namespaces[1].name}`, }); const groupsWithRecommendations = job.taskGroups.filter((group) => @@ -439,8 +442,7 @@ module('Acceptance | job detail (with namespaces)', function (hooks) { test('resource recommendations are not fetched when the feature doesn’t exist', async function (assert) { window.localStorage.nomadTokenSecret = managementToken.secretId; await JobDetail.visit({ - id: job.id, - namespace: server.db.namespaces[1].name, + id: `${job.id}@${server.db.namespaces[1].name}`, }); assert.equal(JobDetail.recommendations.length, 0); @@ -518,10 +520,10 @@ module('Acceptance | job detail (with namespaces)', function (hooks) { clientToken.save(); window.localStorage.nomadTokenSecret = clientToken.secretId; - await JobDetail.visit({ id: job.id, namespace: namespace.name }); + await JobDetail.visit({ id: `${job.id}@${namespace.name}` }); assert.notOk(JobDetail.incrementButton.isDisabled); - await JobDetail.visit({ id: job2.id, namespace: secondNamespace.name }); + await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` }); assert.ok(JobDetail.incrementButton.isDisabled); }); }); From 23f1cb54b48ac91887ade6d7643ff3829df3d41a Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 14:38:27 +0100 Subject: [PATCH 19/24] fix: breadcrumbs allocations due to recent namespace changes * change the breadcrumbs generation to use `idWithNamespace` * adapt tests to reflect new URLs for jobs with namespaces --- ui/app/controllers/allocations/allocation.js | 3 +-- ui/app/controllers/jobs/job/task-group.js | 7 +------ ui/tests/acceptance/task-detail-test.js | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/ui/app/controllers/allocations/allocation.js b/ui/app/controllers/allocations/allocation.js index 142c9f3ff68..b6ebcbacd81 100644 --- a/ui/app/controllers/allocations/allocation.js +++ b/ui/app/controllers/allocations/allocation.js @@ -37,9 +37,8 @@ export default class AllocationsAllocationController extends Controller { label: allocation.taskGroupName, args: [ 'jobs.job.task-group', - job.plainId, + job.idWithNamespace, allocation.taskGroupName, - jobQueryParams, ], }, { diff --git a/ui/app/controllers/jobs/job/task-group.js b/ui/app/controllers/jobs/job/task-group.js index 592f1508e0d..30f29fb3a40 100644 --- a/ui/app/controllers/jobs/job/task-group.js +++ b/ui/app/controllers/jobs/job/task-group.js @@ -174,12 +174,7 @@ export default class TaskGroupController extends Controller.extend( return { title: 'Task Group', label: name, - args: [ - 'jobs.job.task-group', - job, - name, - qpBuilder({ jobNamespace: job.get('namespace.name') || 'default' }), - ], + args: ['jobs.job.task-group', job, name], }; } } diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js index b9adbb3d082..ab3f8c16766 100644 --- a/ui/tests/acceptance/task-detail-test.js +++ b/ui/tests/acceptance/task-detail-test.js @@ -375,7 +375,7 @@ module('Acceptance | task detail (different namespace)', function (hooks) { await Layout.breadcrumbFor('jobs.job.index').visit(); assert.equal( currentURL(), - `/jobs/${job.id}?namespace=other-namespace`, + `/jobs/${job.id}@other-namespace`, 'Job breadcrumb links correctly' ); @@ -383,7 +383,7 @@ module('Acceptance | task detail (different namespace)', function (hooks) { await Layout.breadcrumbFor('jobs.job.task-group').visit(); assert.equal( currentURL(), - `/jobs/${job.id}/${taskGroup}?namespace=other-namespace`, + `/jobs/${job.id}@other-namespace/${taskGroup}`, 'Task Group breadcrumb links correctly' ); From 6dae23bc17d42f807e168a7dd8d5c71e69bcb0d1 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 14:50:05 +0100 Subject: [PATCH 20/24] fix: task-group-detail tests due to namespace changes URLs have changed - tests need to reflect that. --- ui/tests/acceptance/task-group-detail-test.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js index 97facf08c54..01d78c9cc0f 100644 --- a/ui/tests/acceptance/task-group-detail-test.js +++ b/ui/tests/acceptance/task-group-detail-test.js @@ -232,25 +232,23 @@ module('Acceptance | task group detail', function (hooks) { window.localStorage.nomadTokenSecret = clientToken.secretId; await TaskGroup.visit({ - id: job.id, + id: `${job.id}@${SCALE_AND_WRITE_NAMESPACE}`, name: scalingGroup.name, - namespace: SCALE_AND_WRITE_NAMESPACE, }); assert.equal( - currentURL(), - `/jobs/${job.id}/scaling?namespace=${SCALE_AND_WRITE_NAMESPACE}` + decodeURIComponent(currentURL()), + `/jobs/${job.id}@${SCALE_AND_WRITE_NAMESPACE}/scaling` ); assert.notOk(TaskGroup.countStepper.increment.isDisabled); await TaskGroup.visit({ - id: job2.id, + id: `${job2.id}@${secondNamespace.name}`, name: scalingGroup2.name, - namespace: secondNamespace.name, }); assert.equal( - currentURL(), - `/jobs/${job2.id}/scaling?namespace=${READ_ONLY_NAMESPACE}` + decodeURIComponent(currentURL()), + `/jobs/${job2.id}@${READ_ONLY_NAMESPACE}/scaling` ); assert.ok(TaskGroup.countStepper.increment.isDisabled); }); From c475e375d030b5c78522a3643867b6a04ca8640e Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 15:19:20 +0100 Subject: [PATCH 21/24] fix: prettier related test-failutre task-group-detail --- ui/tests/acceptance/task-group-detail-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js index 01d78c9cc0f..dc54767564a 100644 --- a/ui/tests/acceptance/task-group-detail-test.js +++ b/ui/tests/acceptance/task-group-detail-test.js @@ -104,7 +104,7 @@ module('Acceptance | task group detail', function (hooks) { totalMemoryMaxAddendum = ` (${formatScheduledBytes( totalMemoryMax, 'MiB' - )} Max)`; + )}Max)`; } assert.equal( From af6da28f7c840c33e900d6d9edf702a866d7623e Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 15:19:45 +0100 Subject: [PATCH 22/24] fix: prettier related volume-list - test --- ui/tests/acceptance/volumes-list-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/volumes-list-test.js b/ui/tests/acceptance/volumes-list-test.js index 7d11ebd1cb9..c7a3fee9b21 100644 --- a/ui/tests/acceptance/volumes-list-test.js +++ b/ui/tests/acceptance/volumes-list-test.js @@ -79,7 +79,7 @@ module('Acceptance | volumes list', function (hooks) { const isHealthy = healthy > 0; controllerHealthStr = `${ isHealthy ? 'Healthy' : 'Unhealthy' - } (${healthy}/${expected})`; + } ( ${healthy} / ${expected} )`; } const nodeHealthStr = volume.nodesHealthy > 0 ? 'Healthy' : 'Unhealthy'; From a1b201312a890292b54762e8f7a628c8730882ce Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 15:20:11 +0100 Subject: [PATCH 23/24] fix: reflect namespace change volume-detail-test --- ui/tests/acceptance/volume-detail-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/volume-detail-test.js b/ui/tests/acceptance/volume-detail-test.js index ede06242ee3..fdb6f291c4b 100644 --- a/ui/tests/acceptance/volume-detail-test.js +++ b/ui/tests/acceptance/volume-detail-test.js @@ -244,7 +244,7 @@ module('Acceptance | volume detail (with namespaces)', function (hooks) { }); test('/csi/volumes/:id detail ribbon includes the namespace of the volume', async function (assert) { - await VolumeDetail.visit({ id: volume.id, namespace: volume.namespaceId }); + await VolumeDetail.visit({ id: `${volume.id}@${volume.namespaceId}` }); assert.ok(VolumeDetail.hasNamespace); assert.ok(VolumeDetail.namespace.includes(volume.namespaceId || 'default')); From 02218c6c7c6c69d26cfc74b1e4b2b91adb9ee4f6 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Thu, 17 Feb 2022 16:06:49 +0100 Subject: [PATCH 24/24] fix: linting issues and remove remainidn pauseTest --- ui/app/controllers/csi/volumes/index.js | 6 +++++- ui/app/controllers/jobs/job/task-group.js | 1 - ui/tests/acceptance/regions-test.js | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/app/controllers/csi/volumes/index.js b/ui/app/controllers/csi/volumes/index.js index 15fe6f9f423..43c30a22695 100644 --- a/ui/app/controllers/csi/volumes/index.js +++ b/ui/app/controllers/csi/volumes/index.js @@ -113,7 +113,11 @@ export default class IndexController extends Controller.extend( @action gotoVolume(volume, event) { lazyClick([ - () => this.transitionToRoute('csi.volumes.volume', volume.get('idWithNamespace')), + () => + this.transitionToRoute( + 'csi.volumes.volume', + volume.get('idWithNamespace') + ), event, ]); } diff --git a/ui/app/controllers/jobs/job/task-group.js b/ui/app/controllers/jobs/job/task-group.js index 30f29fb3a40..d02252e0a5b 100644 --- a/ui/app/controllers/jobs/job/task-group.js +++ b/ui/app/controllers/jobs/job/task-group.js @@ -5,7 +5,6 @@ import Controller from '@ember/controller'; import { action, computed, get } from '@ember/object'; import { scheduleOnce } from '@ember/runloop'; import intersection from 'lodash.intersection'; -import { qpBuilder } from 'nomad-ui/utils/classes/query-params'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; diff --git a/ui/tests/acceptance/regions-test.js b/ui/tests/acceptance/regions-test.js index b5000c72cf2..fc3ba70268f 100644 --- a/ui/tests/acceptance/regions-test.js +++ b/ui/tests/acceptance/regions-test.js @@ -152,7 +152,6 @@ module('Acceptance | regions (many)', function (hooks) { await Allocation.visit({ id: server.db.allocations[0].id }); - await this.pauseTest(); await selectChoose('[data-test-region-switcher-parent]', newRegion); assert.ok(currentURL().includes('/jobs?'), 'Back at the jobs page');