Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/28005.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: Fix service detail page not rendering
```
31 changes: 29 additions & 2 deletions ui/app/adapters/watchable.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,36 @@ export default class Watchable extends ApplicationAdapter {
json,
);
if (replace) {
store.unloadAll(relationship.type);
// Capture existing record IDs before pushing new data.
// We must push first so that the relationship's LID references are
// updated before any old records are removed. Calling
// store.unloadAll() first leaves dangling LID references in the
// hasMany relationship state, which causes Ember Data 4.12+ to
// throw "Cannot create a record ... as no resource data exists"
// when the relationship is accessed.
const existingIds = store
.peekAll(relationship.type)
.map((r) => r.id);
const newIds = new Set(
(normalizedData.data || []).map((r) => r.id),
);

store.push(normalizedData);

// Remove records that were not present in the new payload using
// removeRecord, which safely unlinks the record from all of its
// relationships before unloading it from the store.
existingIds.forEach((id) => {
if (!newIds.has(id)) {
const record = store.peekRecord(relationship.type, id);
if (record) {
removeRecord(store, record);
}
}
});
} else {
store.push(normalizedData);
}
store.push(normalizedData);
},
(error) => {
if (error instanceof AbortError || error.name === 'AbortError') {
Expand Down
67 changes: 67 additions & 0 deletions ui/tests/unit/adapters/job-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,73 @@ module('Unit | Adapter | Job', function (hooks) {
);
});

test('reloadRelationship with replace:true pushes new records before removing stale ones, preventing dangling LID references', async function (assert) {
await this.initializeUI();

// Create two services for job-1 in mirage
this.server.create('service', {
id: 'svc-keep',
jobId: 'job-1',
namespace: 'default',
});
this.server.create('service', {
id: 'svc-remove',
jobId: 'job-1',
namespace: 'default',
});

// Load the job and trigger the initial services relationship load
const job = await this.store.findRecord(
'job',
JSON.stringify(['job-1', 'default']),
);
await job.services;
await settled();

assert.strictEqual(
this.store.peekAll('service').length,
2,
'Initially 2 services are in the store',
);

// Simulate a server-side change: svc-remove is gone from the server
this.server.db.services.remove('svc-remove');

// Reload the services relationship with replace: true
await this.store.adapterFor('job').reloadRelationship(job, 'services', {
replace: true,
});
await settled();

assert.notOk(
this.store.peekRecord('service', 'svc-remove'),
'svc-remove is removed from the store after the replace reload',
);
assert.ok(
this.store.peekRecord('service', 'svc-keep'),
'svc-keep is still in the store after the replace reload',
);

// Critically, accessing the job's services relationship must not throw
// the "Cannot create a record ... as no resource data exists" error that
// Ember Data 4.12 raises when a hasMany relationship still holds a dangling
// LID for a record that was removed via store.unloadAll().
let errorThrown = null;
try {
const currentServices = job.hasMany('services').value();
if (currentServices) {
currentServices.forEach((s) => void s.serviceName);
}
} catch (e) {
errorThrown = e;
}
assert.strictEqual(
errorThrown,
null,
'Accessing job.services after a replace reload does not throw',
);
});

test('findAll can be canceled', async function (assert) {
await this.initializeUI();

Expand Down
11 changes: 11 additions & 0 deletions ui/tests/unit/serializers/network-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,15 @@ module('Unit | Serializer | Network', function (hooks) {
undefined,
);
});

test('missing IP does not throw', async function (assert) {
const original = {
ReservedPorts: [{ Label: 'http', Value: 80 }],
};

assert.deepEqual(
this.subject().normalize(NetworkModel, original).data.attributes.ip,
undefined,
);
});
});
2 changes: 1 addition & 1 deletion ui/tests/unit/serializers/port-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright IBM Corp. 2015, 2025
* Copyright IBM Corp. 2015, 2026
* SPDX-License-Identifier: BUSL-1.1
*/

Expand Down
Loading