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

## Project status

The current source compatibility target is `1.6.156`. It retains the existing Node 24, Ember, Sass,
The current source compatibility target is `1.6.157`. It retains the existing Node 24, Ember, Sass,
dependency, browser-smoke, terminal, console, and test-harness modernization.
It adds a provider-neutral OpenID Connect administration and sign-in flow with
PKCE S256, staged configuration validation, a real test login before
activation, and local-authentication recovery. Product-owned names, logos,
icons, package metadata, and visible text use PastureStack branding. API
models and protocol fields remain compatible.

Release `1.6.156` places authenticated-page notices in the normal layout flow
between the navigation and main content, so an error cannot cover the page
title or right-side actions. Login/MFA notices keep their existing body host.
See the [release note](docs/releases/web-console-1.6.156.md) for focused tests;
packaged Server/browser acceptance remains a separate gate.
Release `1.6.157` mounts authenticated-page notices when their component enters
the DOM. This covers the fresh direct-URL denial missed by `1.6.156`'s route
render callback while keeping login/MFA notices on the body. The focused source
tests pass; packaged Server/browser acceptance is still pending. See the
[release note](docs/releases/web-console-1.6.157.md).

Release `1.6.156` introduced the in-flow authenticated notice layout, but
Server v1.6.490 QA found that a fresh readonly direct create URL still left
the notice fixed on the body and overlapping header actions. An in-app
transition did mount it correctly. See the
[1.6.156 release note](docs/releases/web-console-1.6.156.md).

Release `1.6.155` moved global notices below the navigation bar and bounded
their width. QA 8080 browser acceptance subsequently found that the fixed
Expand Down
10 changes: 0 additions & 10 deletions app/authenticated/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export function projectIdFromTransition(transition) {

export default Route.extend(Subscribe, PromiseToCb, {
catalog : service(),
growl : service(),
prefs : service(),
projects : service(),
settings : service(),
Expand Down Expand Up @@ -159,12 +158,6 @@ export default Route.extend(Subscribe, PromiseToCb, {
let app = this.controllerFor('application');

this._super();
scheduleOnce('afterRender', this, function() {
let mount = document.getElementById('growl-mount');
if (mount && mount.isConnected) {
this.get('growl').placeContainer(mount);
}
});
if ( !this.controllerFor('application').get('isPopup') && this.get('projects.current') )
{
this.connectSubscribe();
Expand All @@ -184,9 +177,6 @@ export default Route.extend(Subscribe, PromiseToCb, {
},

deactivate() {
// Keep the plugin's container alive when Ember removes this route's mount.
this.get('growl').placeContainer(document.body);

this._super();
this.disconnectSubscribe();
cancel(this.get('testTimer'));
Expand Down
2 changes: 1 addition & 1 deletion app/authenticated/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
goToPrevious="goToPrevious"
}}
{{/unless}}
<div id="growl-mount"></div>
{{growl-mount}}
<main class="clearfix">
{{#if (and this.forceUpgrade this.projects.current.needsUpgrade)}}
{{project-upgrade model=this.projects.current}}
Expand Down
20 changes: 20 additions & 0 deletions app/components/growl-mount/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Component from '@ember/component';
import { service } from '@ember/service';

export default Component.extend({
elementId: 'growl-mount',
growl: service(),

didInsertElement() {
this._super(...arguments);
this.get('growl').placeContainer(this.element);
},

willDestroyElement() {
let container = document.getElementById('jGrowl');
if (container && container.parentNode === this.element) {
this.get('growl').placeContainer(document.body);
}
this._super(...arguments);
},
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.156",
"version": "1.6.157",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.156",
"version": "1.6.157",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
9 changes: 9 additions & 0 deletions docs/releases/web-console-1.6.156.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ the navigation, title, and header actions at those widths. These are source
and local-browser checks; official CI, the immutable Server image, and a new
QA 8080 packaged-browser receipt must be verified separately. No API,
Engine, permission, or request-payload contract changes in this release.

The subsequent isolated Server v1.6.490 / Web Console 1.6.156 QA run did not
accept the layout. On a fresh readonly direct Stack create URL at 1440px, the
denial notice remained fixed under `BODY` despite an empty `#growl-mount` and
overlapped the right-side header actions. A same-page client-side transition
did move the notice into the mount and clear the actions. The route's deferred
mount callback therefore missed the fresh-load case; `1.6.157` moves the
placement to the mount component's insertion lifecycle. This note records the
packaged result separately from the earlier source and local-browser checks.
25 changes: 25 additions & 0 deletions docs/releases/web-console-1.6.157.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Web Console 1.6.157

Server v1.6.490 packaged Web Console 1.6.156. In isolated 8080 browser QA, a
readonly user opening `/apps/stacks/add` directly on a fresh page received
the intended permission denial, but the notice remained fixed under `BODY`.
The authenticated template contained an empty `#growl-mount`, and the notice
overlapped the right-side header actions at 1440px (notice y55–136; actions
y65–97). A client-side transition on the same page moved the notice into the
mount and cleared the actions.
Thus `1.6.156` did not pass packaged browser acceptance for this path.

The route's deferred render callback could miss the mount on a fresh load.
The authenticated template now renders a `growl-mount` component that moves
the existing jGrowl container when its element enters the DOM. On teardown it
returns the container to `BODY` only if that component still owns it. This
preserves the running plugin instance and keeps login/MFA notices on their
existing body host. The change does not alter route permission checks, API
authorization, notice copy, or request payloads.

`npm test -- --filter='growl'` passed 7/7 Chrome 153 QUnit tests, covering
notice placement, component insertion and teardown, delete notices, and layout.
The layout test covers light and dark themes, LTR and RTL, English, Traditional
Chinese, and Japanese, at 1440px, 375px, and 280px. These checks use the source
build. A newly packaged Server image and fresh direct-URL QA 8080 acceptance
remain pending.
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.156",
"version": "1.6.157",
"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.156" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.156"
if [[ "$version" != "1.6.157" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.157"
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.156 browser-session broker-broadcast
1.6.157 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 @@ -66,7 +66,7 @@ if lock_bytes != baseline_bytes:
lock = json.loads(lock_bytes)
packages = lock.get("packages", {})
root = packages.get("", {})
if package.get("version") != "1.6.156":
if package.get("version") != "1.6.157":
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
73 changes: 73 additions & 0 deletions tests/integration/components/growl-mount-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { module, test } from 'qunit';
import $ from 'jquery';
import { clearRender, render, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers';
import { precompileTemplate } from '@ember/template-compilation';
import resolver from '../../helpers/resolver';
import { initialize as initializePodLayouts } from 'ui/initializers/pod-component-layouts';

module('Integration | Component | growl mount', function(hooks) {
hooks.beforeEach(async function() {
this.testRoot = document.createElement('div');
this.testRoot.id = 'ember-testing';
document.body.appendChild(this.testRoot);
await setupContext(this, {resolver});
initializePodLayouts();
await setupRenderingContext(this);
});

hooks.afterEach(async function() {
await teardownContext(this);
let container = document.getElementById('jGrowl');
if (container) {
$(container).jGrowl('shutdown');
container.remove();
}
this.testRoot.remove();
});

test('moves an early denial into the rendered mount and back for login or MFA', async function(assert) {
let growl = this.owner.lookup('service:growl');
growl.error('Action unavailable', 'No create permission');
let container = document.getElementById('jGrowl');
let instance = $(container).data('jGrowl.instance');
assert.strictEqual(container.parentNode, document.body,
'a denial raised before the authenticated template renders starts on the body');

await render(precompileTemplate('{{growl-mount}}'));
let mount = document.getElementById('growl-mount');
assert.strictEqual(container.parentNode, mount,
'the component moves the existing notice when the mount actually enters the DOM');
assert.strictEqual(getComputedStyle(container).position, 'static',
'authenticated notices reserve space above main content');
assert.strictEqual($(container).data('jGrowl.instance'), instance,
'the running plugin instance is preserved');

await clearRender();
assert.strictEqual(container.parentNode, document.body,
'leaving the authenticated view returns the notice to the body');
assert.strictEqual(getComputedStyle(container).position, 'fixed',
'login and MFA retain the global notice position');
growl.raw('Login error', 'Try again');
assert.strictEqual(container.parentNode, document.body,
'login and MFA notices remain outside the authenticated mount');
assert.strictEqual($(container).data('jGrowl.instance'), instance,
'the same plugin instance remains active');
});

test('destroying an old mount does not reclaim a container moved elsewhere', async function(assert) {
let growl = this.owner.lookup('service:growl');
growl.error('Action unavailable', 'No create permission');
await render(precompileTemplate('{{growl-mount}}'));
let container = document.getElementById('jGrowl');
let nextMount = document.createElement('div');
document.body.appendChild(nextMount);

growl.placeContainer(nextMount);
await clearRender();
assert.strictEqual(container.parentNode, nextMount,
'the old mount leaves another destination in control of the plugin');

growl.placeContainer(document.body);
nextMount.remove();
});
});
81 changes: 53 additions & 28 deletions tests/unit/vendor/growl-viewport-layout-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import { module, test } from 'qunit';

module('Unit | Vendor | Growl viewport layout');

const copy = {
'zh-tw': {title: '使用者堆疊', action: '狀態',
header: '無法執行此動作', message: '您沒有權限在此環境中建立此資源。'},
'en-us': {title: 'User Stacks', action: 'State',
header: 'Action unavailable',
message: 'You do not have permission to create this resource in this environment.'},
'ja-jp': {title: 'ユーザースタック', action: '状態',
header: '操作できません',
message: 'この環境でこのリソースを作成する権限がありません。'},
};

function growlFrame(asset, direction, headingLayout) {
return new Promise((resolve, reject) => {
let frame = document.createElement('iframe');
Expand Down Expand Up @@ -47,37 +58,51 @@ test('global notices clear the navbar and page titles and fit the viewport', asy
for (let width of [1440, 375, 280]) {
frame.style.width = `${width}px`;
await new Promise((resolve) => frame.contentWindow.requestAnimationFrame(resolve));
for (let locale of Object.keys(copy)) {
page.documentElement.lang = locale;
page.querySelector('.jGrowl-header').textContent = copy[locale].header;
page.querySelector('.jGrowl-message').textContent = copy[locale].message;
page.querySelector('main h1').textContent = copy[locale].title;
page.querySelector('main .pull-right button').textContent = copy[locale].action;

let navbar = page.querySelector('nav.navbar').getBoundingClientRect();
let bounds = notice.getBoundingClientRect();
let close = page.querySelector('.jGrowl-close').getBoundingClientRect();
let main = page.querySelector('main').getBoundingClientRect();
let title = page.querySelector('main h1').getBoundingClientRect();
let action = page.querySelector('main .btn').getBoundingClientRect();
let headerActions = page.querySelector('main .pull-right').getBoundingClientRect();
let context = `${asset} ${headingLayout} ${width}px`;
let navbar = page.querySelector('nav.navbar').getBoundingClientRect();
let mount = page.querySelector('#growl-mount').getBoundingClientRect();
let bounds = notice.getBoundingClientRect();
let close = page.querySelector('.jGrowl-close').getBoundingClientRect();
let main = page.querySelector('main').getBoundingClientRect();
let title = page.querySelector('main h1').getBoundingClientRect();
let action = page.querySelector('main .btn').getBoundingClientRect();
let headerActions = page.querySelector('main .pull-right').getBoundingClientRect();
let context = `${asset} ${headingLayout} ${locale} ${width}px`;

assert.ok(bounds.top >= navbar.bottom, `${context}: notice starts below the navbar`);
assert.ok(bounds.left >= -1 && bounds.right <= width + 1,
`${context}: notice stays within the viewport`);
assert.ok(bounds.bottom <= frame.contentWindow.innerHeight + 1,
`${context}: notice stays above the viewport bottom`);
assert.ok(bounds.width > 0 && bounds.height > 0, `${context}: notice remains visible`);
assert.ok(close.left >= bounds.left && close.right <= bounds.right,
`${context}: dismiss control remains inside the notice`);
assert.ok(close.top >= bounds.top && close.bottom <= bounds.bottom,
`${context}: dismiss control stays within the notice vertically`);
assert.strictEqual(frame.contentWindow.getComputedStyle(notice).opacity, '1',
`${context}: notice remains opaque and readable`);
assert.strictEqual(notice.parentElement.parentElement.id, 'growl-mount',
`${context}: notice is inside the authenticated mount`);
assert.strictEqual(frame.contentWindow.getComputedStyle(notice.parentElement).position,
'static', `${context}: the notice container participates in page layout`);
assert.ok(mount.bottom >= bounds.bottom,
`${context}: the mount reserves the notice height`);
assert.ok(bounds.top >= navbar.bottom, `${context}: notice starts below the navbar`);
assert.ok(bounds.left >= -1 && bounds.right <= width + 1,
`${context}: notice stays within the viewport`);
assert.ok(bounds.bottom <= frame.contentWindow.innerHeight + 1,
`${context}: notice stays above the viewport bottom`);
assert.ok(bounds.width > 0 && bounds.height > 0, `${context}: notice remains visible`);
assert.ok(close.left >= bounds.left && close.right <= bounds.right,
`${context}: dismiss control remains inside the notice`);
assert.ok(close.top >= bounds.top && close.bottom <= bounds.bottom,
`${context}: dismiss control stays within the notice vertically`);
assert.strictEqual(frame.contentWindow.getComputedStyle(notice).opacity, '1',
`${context}: notice remains opaque and readable`);

assert.ok(main.top >= bounds.bottom,
`${context}: main content starts below the notice`);
assert.ok(title.top >= bounds.bottom,
`${context}: page title starts below the notice`);
assert.ok(action.top >= bounds.bottom,
`${context}: page action starts below the notice`);
assert.ok(headerActions.top >= bounds.bottom && headerActions.right <= width + 1,
`${context}: header actions clear the notice and fit the viewport`);
assert.ok(main.top >= bounds.bottom,
`${context}: main content starts below the notice`);
assert.ok(title.top >= bounds.bottom,
`${context}: page title starts below the notice`);
assert.ok(action.top >= bounds.bottom,
`${context}: page action starts below the notice`);
assert.ok(headerActions.top >= bounds.bottom && headerActions.right <= width + 1,
`${context}: header actions clear the notice and fit the viewport`);
}
}

frame.remove();
Expand Down
Loading