Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

## Project status

Candidate `1.6.173` repairs empty environment-template choice labels. These
choices are native `ProjectTemplate` resources, not Catalog templates: their
authoritative label is `name`, and selection remains bound to the template ID.
The focused regression covers the actual model, rendered choices, renaming and
selection. Official validation, immutable publication, Server packaging and
packaged native acceptance are pending; this candidate does not promote any
historical HOLD or claim the complete permission matrix is finished. See the
[release note](docs/releases/web-console-1.6.173.md).

Published `1.6.172` preserves API-key create-only first delivery when a redacted
subscribe model arrives before POST/201. Only the API-key editor opts into a
request-private, Schema-bound delivery to its detached clone; newer canonical
Expand Down
2 changes: 1 addition & 1 deletion app/components/view-edit-project/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default Component.extend(NewOrEdit, Sortable, {
var active = this.get('project.projectTemplateId');

var choices = this.get('projectTemplates').map((tpl) => {
return {id: tpl.id, name: tpl.get('localizedName'), image: tpl.get('orchestrationIcon')};
return {id: tpl.id, name: tpl.get('name'), image: tpl.get('orchestrationIcon')};
});

if ( !choices.length ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.172",
"version": "1.6.173",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.172",
"version": "1.6.173",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
30 changes: 30 additions & 0 deletions docs/releases/web-console-1.6.173.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Web Console 1.6.173

Candidate; official validation and publication are pending.

## Root cause and minimal repair

The environment-create form maps native `ProjectTemplate` resources to choice
cards. It incorrectly read `localizedName`, a Catalog Template property which
the native model does not implement, leaving labels empty. Read the native
resource's `name` instead. Preserve the existing name-dependent sorting and
selection by exact template ID; do not translate user-defined names or add a
fallback which conceals a missing model contract.

The bounded consumer review found this incorrect native-model lookup only in
`app/components/view-edit-project/component.js`. Catalog templates legitimately
use `localizedName` and are unchanged. No API, permission, membership, save-hook,
OIDC, MFA, session-generation or WebSocket contract changes.

## Verification and release boundaries

Regressions must use the real native model without an invented `localizedName`,
verify rendered labels and sorting, rename reactivity and exact-ID card
selection. Existing environment permission tests remain applicable. Packaged
native creation, owner/member Host acceptance and the fresh API-key delivery
acceptance remain separate gates. Historical HOLD receipts remain HOLD and
the full permission/resource/locale matrix remains INCOMPLETE.

Use a new immutable numeric component tag and a new immutable Server patch
image. Preserve existing Compose options, volumes, HTTPS origin and prior image
for rollback. Do not deploy the company instance as part of this QA repair.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.172",
"version": "1.6.173",
"private": true,
"description": "PastureStack browser console for the compatible control platform.",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-modernization-blockers
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f:
print(json.load(f).get('version', ''))
PY
)
if [[ "$version" != "1.6.172" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.172"
if [[ "$version" != "1.6.173" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.173"
failures=$((failures + 1))
fi

Expand Down
2 changes: 1 addition & 1 deletion scripts/check-ui-console-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,4 @@ if [[ -n ${PASTURESTACK_PRIVATE_MARKER:-} ]] && grep -RInF -- "$PASTURESTACK_PRI
fi

printf 'UI_CONSOLE_WORKSPACE_OK version=%s persistence=%s cross_tab=%s\n' \
1.6.172 browser-session broker-broadcast
1.6.173 browser-session broker-broadcast
2 changes: 1 addition & 1 deletion scripts/check-ui-critical-high-dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ if lock_bytes != baseline_bytes:
lock = json.loads(lock_bytes)
packages = lock.get("packages", {})
root = packages.get("", {})
if package.get("version") != "1.6.172":
if package.get("version") != "1.6.173":
fail(f"unexpected Web Console version: {package.get('version')}")
if root.get("version") != package.get("version"):
fail(f"lock root version differs: {root.get('version')}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { click, find, findAll, render, settled, setupContext, setupRenderingCont
import { module, test } from 'qunit';

import { initialize as initializePodLayouts } from 'ui/initializers/pod-component-layouts';
import ProjectTemplate from 'ui/models/projecttemplate';
import Router from 'ui/router';
import { destroyOwned } from '../../helpers/owned-subject';
import resolver from '../../helpers/resolver';

module('Integration | Component | view edit project permissions', function(hooks) {
Expand Down Expand Up @@ -61,9 +63,65 @@ module('Integration | Component | view edit project permissions', function(hooks

hooks.afterEach(async function() {
await teardownContext(this);
(this.nativeTemplates || []).forEach((template) => destroyOwned(template));
this.testRoot.remove();
});

test('new environment cards render real native names and select the same ID after a rename', async function(assert) {
this.app = EmberObject.create({baseAssets: '/'});
let zulu = ProjectTemplate.create({id: '1pt-zulu', name: 'Zulu environment', stacks: A([]), app: this.app});
let alpha = ProjectTemplate.create({id: '1pt-alpha', name: 'alpha environment', stacks: A([]), app: this.app});
this.nativeTemplates = [zulu, alpha];
this.projectTemplates = A(this.nativeTemplates);
this.project.setProperties({id: null, projectTemplateId: zulu.id});
assert.strictEqual(alpha.get('localizedName'), undefined, 'the real native model supplies no catalog name alias');

await render(precompileTemplate(`{{view-edit-project
project=this.project originalProject=this.originalProject projectTemplates=this.projectTemplates
app=this.app network=this.network policyManager=this.policyManager userStore=this.userStore
showEdit=true editing=false
}}`));

assert.deepEqual(findAll('.orchestration-driver .clip').map((label) => label.textContent.trim()),
['alpha environment', 'Zulu environment'], 'native user-defined labels render in case-insensitive order');
assert.strictEqual(findAll('.orchestration-driver.active').length, 1, 'one native ID is selected');
assert.strictEqual(find('.orchestration-driver.active .clip').textContent.trim(), 'Zulu environment');

await click(findAll('.orchestration-driver')[0]);
assert.strictEqual(this.project.get('projectTemplateId'), alpha.id, 'the real card action stores its exact model ID');
assert.strictEqual(find('.orchestration-driver.active .clip').textContent.trim(), 'alpha environment');

alpha.set('name', 'zz renamed environment');
await settled();
assert.deepEqual(findAll('.orchestration-driver .clip').map((label) => label.textContent.trim()),
['Zulu environment', 'zz renamed environment'], 'renaming updates the rendered label and card order');
assert.strictEqual(this.project.get('projectTemplateId'), alpha.id, 'the selected ID survives reordering');
assert.strictEqual(findAll('.orchestration-driver.active').length, 1);
assert.strictEqual(find('.orchestration-driver.active .clip').textContent.trim(), 'zz renamed environment');
});

test('new environment renders and selects the None card for an empty native template list', async function(assert) {
this.app = EmberObject.create({baseAssets: '/'});
this.projectTemplates = A([]);
this.project.setProperties({id: null, projectTemplateId: '1pt-stale'});

await render(precompileTemplate(`{{view-edit-project
project=this.project originalProject=this.originalProject projectTemplates=this.projectTemplates
app=this.app network=this.network policyManager=this.policyManager userStore=this.userStore
showEdit=true editing=false
}}`));

assert.strictEqual(findAll('.orchestration-driver').length, 1);
assert.strictEqual(find('.orchestration-driver .clip').textContent.trim(), 'None');
assert.strictEqual(find('.orchestration-driver img').getAttribute('src'), '/assets/images/logos/pasturestack-mark.svg');
assert.strictEqual(findAll('.orchestration-driver.active').length, 0, 'a stale ID is not selected');
assert.ok(find('.well .text-center').textContent.includes('viewEditProject.nativeEngine'), 'the empty-list summary stays native');

await click('.orchestration-driver');
assert.strictEqual(this.project.get('projectTemplateId'), null, 'the native fallback action clears the stale ID');
assert.strictEqual(findAll('.orchestration-driver.active').length, 1, 'the null-ID card becomes selected');
});

test('direct edit URL follows member, metadata, and network links independently', async function(assert) {
let cancelled = 0;
this.cancel = () => { cancelled++; };
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/components/view-edit-project-permissions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { resolve } from 'rsvp';
import { module, test } from 'qunit';

import ViewEditProject from 'ui/components/view-edit-project/component';
import ProjectTemplate from 'ui/models/projecttemplate';
import inertRenderer from '../../helpers/inert-renderer';
import { createOwned, destroyOwned } from '../../helpers/owned-subject';

Expand All @@ -25,6 +26,62 @@ function makeComponent(project, network, options = {}) {
}, 'component');
}

test('native project template choices use names, keep IDs, and react to renames', function(assert) {
let app = EmberObject.create({baseAssets: '/'});
let zulu = ProjectTemplate.create({id: '1pt-zulu', name: 'Zulu environment', stacks: A([]), app});
let alpha = ProjectTemplate.create({id: '1pt-alpha', name: 'alpha environment', stacks: A([]), app});
let project = EmberObject.create({id: null, projectTemplateId: zulu.id, projectMembers: A([])});
let component = makeComponent(project, null, {app, editing: false, projectTemplates: A([zulu, alpha])});

try {
assert.strictEqual(zulu.get('localizedName'), undefined, 'the real native model has no catalog localizedName');
assert.strictEqual(alpha.get('localizedName'), undefined, 'no fixture alias hides a missing native name');
assert.deepEqual(component.get('templateChoices').map(({id, name, image}) => ({id, name, image})), [
{id: alpha.id, name: 'alpha environment', image: alpha.get('orchestrationIcon')},
{id: zulu.id, name: 'Zulu environment', image: zulu.get('orchestrationIcon')},
], 'user-defined names are sorted case-insensitively without losing IDs or icons');

component.send('selectTemplate', alpha.id);
assert.strictEqual(project.get('projectTemplateId'), alpha.id, 'the action stores the selected native ID, not its name');
assert.strictEqual(component.get('selectedProjectTemplate'), alpha, 'the selection resolves the actual native model');

alpha.set('name', 'zz renamed environment');
assert.deepEqual(component.get('templateChoices').map(({id, name}) => ({id, name})), [
{id: zulu.id, name: 'Zulu environment'},
{id: alpha.id, name: 'zz renamed environment'},
], 'the existing name dependency refreshes both labels and order');
assert.strictEqual(project.get('projectTemplateId'), alpha.id, 'renaming does not replace the selected ID');
} finally {
destroyOwned(component);
destroyOwned(zulu);
destroyOwned(alpha);
}
});

test('an empty native project template list keeps the None choice', function(assert) {
let project = EmberObject.create({id: null, projectTemplateId: '1pt-stale', projectMembers: A([])});
let component = makeComponent(project, null, {
app: EmberObject.create({baseAssets: '/'}),
editing: false,
projectTemplates: A([]),
});

try {
let choices = component.get('templateChoices');
assert.strictEqual(choices.length, 1);
assert.deepEqual({id: choices[0].id, name: choices[0].name, image: choices[0].image}, {
id: null,
name: 'None',
image: '/assets/images/logos/pasturestack-mark.svg',
}, 'the no-template fallback remains a null-ID native choice');
component.send('selectTemplate', choices[0].id);
assert.strictEqual(project.get('projectTemplateId'), null, 'the fallback clears a stale template ID');
assert.strictEqual(component.get('selectedProjectTemplate'), undefined);
} finally {
destroyOwned(component);
}
});

test('existing environment writes only resources advertised by their own capabilities', async function(assert) {
let cases = [
{name: 'readonly', project: false, members: false, network: false},
Expand Down
Loading