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

## Project status

The current source compatibility target is `1.6.155`. It retains the existing Node 24, Ember, Sass,
The current source compatibility target is `1.6.156`. 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.155` positions global notices below the navigation bar using the
existing navbar height and keeps them inside narrow viewports. This corrects
the mobile layout overlap found during `1.6.154` acceptance without changing
authorization or API behavior. See the
[release note](docs/releases/web-console-1.6.155.md) for focused visual tests;
packaged Server/browser acceptance must be checked against a new image.
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.155` moved global notices below the navigation bar and bounded
their width. QA 8080 browser acceptance subsequently found that the fixed
notice still covered the mobile page title and desktop header actions. It is
not the accepted layout; see the
[1.6.155 release note](docs/releases/web-console-1.6.155.md).

Release `1.6.154` shows a localized, persistent permission error when a direct
Stack or Service creation URL is denied, then returns to the Stacks list.
Expand Down
10 changes: 10 additions & 0 deletions app/authenticated/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function projectIdFromTransition(transition) {

export default Route.extend(Subscribe, PromiseToCb, {
catalog : service(),
growl : service(),
prefs : service(),
projects : service(),
settings : service(),
Expand Down Expand Up @@ -158,6 +159,12 @@ 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 @@ -177,6 +184,9 @@ 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
1 change: 1 addition & 0 deletions app/authenticated/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
goToPrevious="goToPrevious"
}}
{{/unless}}
<div id="growl-mount"></div>
<main class="clearfix">
{{#if (and this.forceUpgrade this.projects.current.needsUpgrade)}}
{{project-upgrade model=this.projects.current}}
Expand Down
12 changes: 12 additions & 0 deletions app/services/growl.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,20 @@ export default Service.extend({
$.jGrowl.defaults.closerTemplate = '<div><button type="button" class="btn btn-info btn-xs btn-block">Dismiss All Notifications</button></div>';
},

placeContainer(target) {
let destination = target || document.getElementById('growl-mount') || document.body;
let container = document.getElementById('jGrowl');

if (container && container.parentNode !== destination) {
destination.appendChild(container);
}

return destination;
},

raw: function(title, body, opt) {
opt = opt || {};
opt.appendTo = this.placeContainer();

if ( title )
{
Expand Down
15 changes: 15 additions & 0 deletions app/styles/components/_growl.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ $navbar-height: null !default;
z-index: 10;
}

#growl-mount .jGrowl.top-right {
// The authenticated layout places this between the navbar and main.
// Keep notices in flow so page titles and actions start below them.
position: static;
display: flex;
flex-direction: column;
align-items: flex-end;
}

.jGrowl {
.jGrowl-close {
position: absolute;
Expand All @@ -23,6 +32,12 @@ $navbar-height: null !default;
color: white;
}

.jGrowl-notification {
// Anchor each dismiss control to its own notice, including in the
// in-flow mobile layout and when several notices are visible.
position: relative;
}

.jGrowl-notification .jGrowl-header {
font-size: 1em;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.155",
"version": "1.6.156",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.155",
"version": "1.6.156",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
7 changes: 7 additions & 0 deletions docs/releases/web-console-1.6.155.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ The focused Chrome QUnit layout test covers both themes, both directions and
fit, dismiss-control placement and opacity. Local production/development
build and the focused test passed. Official release CI and packaged 8080
browser acceptance are separate gates and must be recorded after publication.

The subsequent isolated v1.6.489 / 1.6.155 packaged-browser run confirmed
navigation clearance but found that the fixed notice still obscured the
375px page title and 1440px right-side sort controls. Thus the layout was not
accepted despite the original focused runner reporting visibility PASS.
Web Console 1.6.156 moves authenticated notices into document flow and adds
content-overlap checks; this note retains the actual 1.6.155 outcome.
25 changes: 25 additions & 0 deletions docs/releases/web-console-1.6.156.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Web Console 1.6.156

Server v1.6.489 packaged Web Console 1.6.155. Its isolated QA 8080 browser
run confirmed that the permission notice was visible and no longer covered
the 45px navigation bar, but screenshots revealed a remaining defect: at
375px the fixed notice covered the page title; at 1440px it covered the
right-side sort controls. The earlier viewport-only test had incorrectly
reported those cases as passing. This release addresses that shared layout
cause, not the Stack or Service authorization rules.

On authenticated pages the existing jGrowl container is moved into a mount
between the navigation and `<main>`. It stays in the normal document flow and
is right-aligned, so visible notices push page titles and header actions down
at desktop and mobile widths. The same container and plugin instance move
back to the body when leaving the authenticated route; login and MFA notices
retain their existing behavior. A deferred mount operation checks that the
destination still belongs to the document before moving the container.

Focused Chrome QUnit tests cover the shared notification service and layout
at 1440px, 375px, and 280px, including both themes and text directions.
Local Playwright screenshots with compiled CSS verify that the notice clears
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.
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.155",
"version": "1.6.156",
"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.155" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.155"
if [[ "$version" != "1.6.156" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.156"
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.155 browser-session broker-broadcast
1.6.156 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.155":
if package.get("version") != "1.6.156":
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
41 changes: 41 additions & 0 deletions tests/unit/services/growl-test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import { module, test } from 'qunit';
import $ from 'jquery';
import Growl from 'ui/services/growl';

module('Unit | Service | growl');

test('notices use the body outside authenticated pages and move with its mount', function(assert) {
let growl = Growl.create();
growl.raw('Login error', 'Try again');

let container = document.getElementById('jGrowl');
assert.ok(container && container.classList.contains('jGrowl'), 'the global plugin starts');
assert.strictEqual(container.parentNode, document.body, 'login notices use the body');
let instance = $(container).data('jGrowl.instance');
assert.ok(instance && instance.interval, 'the plugin timer is running');

let mount = document.createElement('div');
mount.id = 'growl-mount';
document.getElementById('qunit-fixture').appendChild(mount);
growl.placeContainer();
assert.strictEqual(container.parentNode, mount, 'authenticated notices use the content flow');

growl.placeContainer(document.body);
assert.strictEqual(container.parentNode, document.body, 'logout keeps the plugin available');
assert.strictEqual($(container).data('jGrowl.instance'), instance,
'moving the container preserves its running plugin instance');
mount.remove();
assert.strictEqual(growl.placeContainer(), document.body, 'later login notices still target the body');
growl.raw('Login error', 'Try again');
assert.strictEqual(container.parentNode, document.body, 'later login errors still use the body');

$(container).jGrowl('shutdown');
container.remove();

document.getElementById('qunit-fixture').appendChild(mount);
growl.raw('Denied', 'No create permission');
let newContainer = document.getElementById('jGrowl');
assert.strictEqual(newContainer.parentNode, mount,
'the first authenticated notice is created inside the mount');

$(newContainer).jGrowl('shutdown');
newContainer.remove();
mount.remove();
growl.destroy();
});

test('denied deletes and actions use neutral, private-safe copy in all supported write locales', async function(assert) {
for (let locale of ['en-us', 'zh-tw', 'ja-jp']) {
let response = await fetch(`/translations/${locale}.json`);
Expand Down
90 changes: 58 additions & 32 deletions tests/unit/vendor/growl-viewport-layout-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { module, test } from 'qunit';

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

function growlFrame(asset, direction) {
function growlFrame(asset, direction, headingLayout) {
return new Promise((resolve, reject) => {
let frame = document.createElement('iframe');
let timer = setTimeout(() => reject(new Error(`Timed out loading ${asset}`)), 10000);
Expand All @@ -15,47 +15,73 @@ function growlFrame(asset, direction) {
frame.srcdoc = `<!doctype html><html dir="${direction}"><head>
<link rel="stylesheet" href="/assets/vendor.css">
<link rel="stylesheet" href="/assets/${asset}">
</head><body style="margin:0">
</head><body style="margin:0" dir="${direction}">
<header><nav class="navbar"></nav></header>
<div id="jGrowl" class="jGrowl top-right">
<div class="jGrowl-notification error" style="display:block">
<div class="jGrowl-close">×</div>
<div class="jGrowl-header">沒有權限建立服務</div>
<div class="jGrowl-message">您沒有權限在此專案建立服務。</div>
<div id="growl-mount">
<div id="jGrowl" class="jGrowl top-right">
<div class="jGrowl-notification error" style="display:block">
<div class="jGrowl-close">×</div>
<div class="jGrowl-header">沒有權限建立服務</div>
<div class="jGrowl-message">您沒有權限在此專案建立服務。</div>
</div>
</div>
</div></body></html>`;
</div>
<main><section class="${headingLayout}"><h1>應用程式堆疊</h1>
<a class="btn btn-sm btn-primary">新增堆疊</a>
<div class="pull-right"><button class="btn btn-sm btn-default">狀態</button></div>
</section></main>
</body></html>`;
document.getElementById('qunit-fixture').appendChild(frame);
});
}

test('global notices clear the navbar and fit desktop and mobile viewports', async function(assert) {
test('global notices clear the navbar and page titles and fit the viewport', async function(assert) {
for (let theme of ['light', 'dark']) {
for (let direction of ['ltr', 'rtl']) {
let asset = `ui-${theme}${direction === 'rtl' ? '.rtl' : ''}.css`;
let frame = await growlFrame(asset, direction);
let page = frame.contentDocument;
let notice = page.querySelector('.jGrowl-notification');

for (let width of [1440, 375, 280]) {
frame.style.width = `${width}px`;
await new Promise((resolve) => frame.contentWindow.requestAnimationFrame(resolve));

let navbar = page.querySelector('nav.navbar').getBoundingClientRect();
let bounds = notice.getBoundingClientRect();
let close = page.querySelector('.jGrowl-close').getBoundingClientRect();
let context = `${asset} ${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.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.strictEqual(frame.contentWindow.getComputedStyle(notice).opacity, '1',
`${context}: notice remains opaque and readable`);
}
for (let headingLayout of ['header clearfix', 'clearfix']) {
let frame = await growlFrame(asset, direction, headingLayout);
let page = frame.contentDocument;
let notice = page.querySelector('.jGrowl-notification');

for (let width of [1440, 375, 280]) {
frame.style.width = `${width}px`;
await new Promise((resolve) => frame.contentWindow.requestAnimationFrame(resolve));

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`;

frame.remove();
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`);
}

frame.remove();
}
}
}
});
Loading