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
11 changes: 11 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ Web Console preserves compatible API paths, schema and resource names, action na

Visible branding, product-owned assets, icon identifiers, package metadata, and operator documentation use PastureStack. Historical identifiers remain only where they are server data or protocol contracts and must not be mechanically replaced.

Web Console `1.6.164` is a source-only Receiver validation-label candidate.
Required-field and numeric errors reuse the visible labels from the Receiver,
scale-service, scale-host and service-upgrade forms. Embedded schema scopes,
model-specific translation precedence and unknown-field fallback are retained.
The existing scale minimum/maximum condition is unchanged; only its message
uses the existing numeric translation. No required, numeric or save guard,
API/schema, permission, payload or driver action changes. Focused local source
validation passed 12/12 tests and 61/61 assertions; full CI, component
publication and packaged-browser acceptance remain
separate. Published `1.6.163` and earlier HOLD evidence are not overwritten.

Published Web Console `1.6.163` localizes state badges only from seven recognized
`model.displayState` labels using existing translation keys. Unknown labels and
health/connection display overrides retain their original text; a machine state
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

## Project status

Source candidate `1.6.164` reuses visible Receiver form labels for shared
required-field and numeric validation errors. The three supported driver
configurations retain their own label scopes; model-specific translations still
take precedence and unknown fields keep their existing fallback. The scale
models' existing minimum-greater-than-maximum checks use the existing localized
numeric error. No validation rule, API, schema, permission or driver behavior is
changed. Focused Chrome 153 source validation passed 12/12 tests with 61/61
assertions (eight new Receiver cases and four adjacent existing cases); no
`1.6.164` archive, full CI,
Server packaging, deployment or complete-language/matrix acceptance is claimed.
Published `1.6.163` and historical HOLDs remain unchanged. See the
[candidate note](docs/releases/web-console-1.6.164.md).

Published release `1.6.163` contains two shared display-locale fixes.
Relative dates recompute when the selected language changes and use a Moment
instance locale, without changing the global locale during formatting. State
Expand Down
28 changes: 28 additions & 0 deletions app/mixins/cattle-transitioning-resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,34 @@ const formValidationLabels = {
description: 'formNameDescription.description.label',
value: 'newSecret.value.label',
},
receiver: {
name: 'generic.name',
driver: 'newReceiver.driver.label',
},
scaleservice: {
action: 'newReceiver.action.label',
serviceId: 'newReceiver.service.label',
amount: 'newReceiver.amount.label',
min: 'newReceiver.min.label',
max: 'newReceiver.max.label',
},
scalehost: {
action: 'newReceiver.action.label',
hostSelector: 'newReceiver.hostSelector.label',
amount: 'newReceiver.amount.label',
min: 'newReceiver.min.label',
max: 'newReceiver.max.label',
deleteOption: 'newReceiver.deleteOption.label',
},
serviceupgrade: {
payloadFormat: 'newReceiver.payloadFormat.label',
addressType: 'newReceiver.addressType.label',
tag: 'newReceiver.tag.label',
serviceSelector: 'newReceiver.serviceSelector.label',
batchSize: 'formUpgrade.size',
intervalMillis: 'formUpgrade.interval',
startFirst: 'formUpgrade.behavior',
},
};

const stateColorSortMap = {
Expand Down
3 changes: 2 additions & 1 deletion app/models/scalehost.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export default Resource.extend({
let min = parseInt(this.get('min'),10);
let max = parseInt(this.get('max'),10);
if ( min && max && min > max ) {
errors.push('"Minimum Scale" cannot be greater than "Maximum Scale"');
let intl = this.get('intl');
errors.push(intl.t('validation.number.max', {key: intl.t('newReceiver.min.label'), val: max}));
}

return errors;
Expand Down
3 changes: 2 additions & 1 deletion app/models/scaleservice.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export default Resource.extend({
let min = parseInt(this.get('min'),10);
let max = parseInt(this.get('max'),10);
if ( min && max && min > max ) {
errors.push('"Minimum Scale" cannot be greater than "Maximum Scale"');
let intl = this.get('intl');
errors.push(intl.t('validation.number.max', {key: intl.t('newReceiver.min.label'), val: max}));
}

return errors;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.163",
"version": "1.6.164",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.163",
"version": "1.6.164",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
52 changes: 52 additions & 0 deletions docs/releases/web-console-1.6.164.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Web Console 1.6.164

SOURCE-CANDIDATE ONLY, not a published component or final Server candidate.
Based on the published `1.6.163` runtime source
`5db737a5e04c4cc15672f97296d5bf521ab9a8da` and its merged publication documents
at `7fbc755379e07e0230f1a5fb76873197924eb56a`. No archive, tag, release,
Server packaging, deployment or complete resource/role matrix PASS is claimed.

## Receiver validation labels

The shared validation-label fallback now uses `generic.name` for Receiver Name
and `newReceiver.service.label` for the scale-service target. It also reuses
the actual visible labels for required or numeric fields in the three supported
Receiver drivers. Service-upgrade batch size and interval use `formUpgrade.size`
and `formUpgrade.interval`, matching the template rather than a similarly named
unused label. The mappings are scoped to normalized Receiver/config types;
model-specific labels and direct model translations retain precedence. Unknown
fields and unrelated resources retain their original fallback.

Scale-service and scale-host retain the existing `min && max && min > max`
condition. Its hardcoded English message is replaced by the existing
`validation.number.max` translation and the visible minimum label. No new
translation keys, validation conditions, schema, API, authorization, driver
action, clone behavior or resource request changes are included. The published
`1.6.163` state-badge and relative-date fixes remain byte-for-byte unchanged.

## Focused evidence and pending acceptance

Focused local QUnit validation passed 12/12 tests and 61/61 assertions in native
Chrome `153.0.8010.53`, with zero failures, skipped tests or todos. Eight new
Receiver tests and four adjacent existing validation tests ran; this used the
existing dependency installation, not a fresh locked official CI run.
The new tests call the actual shared
`validationErrors` implementation and real scale models with the actual Intl
service and built English/Traditional Chinese translation JSON. They cover
language switching, nested driver scopes, required/numeric labels, model-label
precedence, unknown fallback and unchanged valid/boundary behavior. Adjacent
existing required-field/update-omission tests are included in the same filter.

The first local attempt had two assertion failures because its expected
scale-service amount label incorrectly bypassed the existing model-specific
translation. The fixture was corrected to retain that precedence; runtime
semantics were not changed to satisfy the test. The original failure evidence
is retained, separate from the corrected successful run.

The numeric package bump changes only package/lock root versions and directly
dependent gate version literals; dependency graphs and security thresholds are
unchanged. Full official CI, deterministic production archives and native
packaged-browser acceptance have not been run for this candidate. This does not
promote the earlier native zero-write Receiver receipt to localized-error PASS,
complete all-language acceptance or resolve the broader incomplete matrix.
Historical HOLD evidence remains HOLD.
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.163",
"version": "1.6.164",
"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.163" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.163"
if [[ "$version" != "1.6.164" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.164"
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.163 browser-session broker-broadcast
1.6.164 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.163":
if package.get("version") != "1.6.164":
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
175 changes: 175 additions & 0 deletions tests/unit/mixins/receiver-validation-labels-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import EmberObject from '@ember/object';
import { run } from '@ember/runloop';
import { setupContext, teardownContext } from '@ember/test-helpers';
import { module, test } from 'qunit';

import CattleTransitioningResource from 'ui/mixins/cattle-transitioning-resource';
import ScaleService from 'ui/models/scaleservice';
import ScaleHost from 'ui/models/scalehost';
import { initialize as initializeResource } from 'ui/initializers/extend-resource';
import { initialize as initializeIntl } from 'ui/instance-initializers/intl';
import resolver from '../../helpers/resolver';

const visibleLabels = {
receiver: {name: 'generic.name', driver: 'newReceiver.driver.label'},
scaleService: {
action: 'newReceiver.action.label', serviceId: 'newReceiver.service.label',
amount: 'newReceiver.amount.label', min: 'newReceiver.min.label', max: 'newReceiver.max.label',
},
scaleHost: {
action: 'newReceiver.action.label', hostSelector: 'newReceiver.hostSelector.label',
amount: 'newReceiver.amount.label', min: 'newReceiver.min.label', max: 'newReceiver.max.label',
deleteOption: 'newReceiver.deleteOption.label',
},
serviceUpgrade: {
payloadFormat: 'newReceiver.payloadFormat.label', addressType: 'newReceiver.addressType.label',
tag: 'newReceiver.tag.label', serviceSelector: 'newReceiver.serviceSelector.label',
batchSize: 'formUpgrade.size', intervalMillis: 'formUpgrade.interval', startFirst: 'formUpgrade.behavior',
},
};

function expectedLabel(intl, type, key) {
let modelKey = `model.${type}.${key}`;
if (intl.exists(`${modelKey}.label`)) {
return intl.t(`${modelKey}.label`);
}
if (intl.exists(modelKey)) {
return intl.t(modelKey);
}
return intl.t(visibleLabels[type][key]);
}

module('Unit | Mixin | cattle transitioning resource validation | receiver labels', function(hooks) {
hooks.beforeEach(async function() {
await setupContext(this, {resolver});
initializeIntl(this.owner);
initializeResource();
this.intl = this.owner.lookup('service:intl');
this.subjects = [];
for (let locale of ['en-us', 'zh-tw']) {
let response = await fetch(`/translations/${locale}.json`);
if (!response.ok) {
throw new Error(`Local translation fixture failed: ${locale} ${response.status}`);
}
this.intl.addTranslations(locale, await response.json());
}
this.intl.setLocale(['en-us']);
this.subject = (type, fields, values = {}, Factory) => {
let Subject = Factory || EmberObject.extend(CattleTransitioningResource);
let subject = Subject.extend({trimValues() {}}).create({
type, ...values, intl: this.intl,
store: EmberObject.create({
getById(schemaType, schemaId) {
return schemaType === 'schema' && schemaId === type.toLowerCase() ? {resourceFields: fields} : null;
},
}),
});
this.subjects.push(subject);
return subject;
};
});

hooks.afterEach(async function() {
run(() => this.subjects.forEach((subject) => subject.destroy()));
await teardownContext(this);
});

test('the actual empty Receiver and target-service errors follow en to zh-tw to en', function(assert) {
let config = this.subject('scaleService', {serviceId: {type: 'string', required: true}}, {serviceId: ''});
let receiver = this.subject('receiver', {
name: {type: 'string', required: true}, scaleServiceConfig: {type: 'scaleService'},
}, {name: '', scaleServiceConfig: config});

assert.deepEqual(receiver.validationErrors(), ['"Name" is required', '"Target Service" is required']);
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
assert.deepEqual(receiver.validationErrors(), ['"名稱" 必須設定', '"目標服務" 必須設定']);
run(() => this.intl.setLocale(['en-us']));
assert.deepEqual(receiver.validationErrors(), ['"Name" is required', '"Target Service" is required']);
});

test('all three nested driver scopes reuse their own visible required-field labels', function(assert) {
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
for (let type of ['scaleService', 'scaleHost', 'serviceUpgrade']) {
let labels = visibleLabels[type];
let fields = Object.fromEntries(Object.keys(labels).map((key) => [key, {type: 'string', required: true}]));
let config = this.subject(type, fields);
let receiver = this.subject('receiver', {[`${type}Config`]: {type}}, {[`${type}Config`]: config});
assert.deepEqual(receiver.validationErrors(), Object.keys(labels).map((key) =>
this.intl.t('validation.required', {key: expectedLabel(this.intl, type, key)})), `${type} labels remain scoped to that embedded schema`);
}
let receiver = this.subject('receiver', {driver: {type: 'enum', required: true}});
assert.deepEqual(receiver.validationErrors(), ['"類型" 必須設定']);
});

test('scale numeric errors use By, Minimum Scale and Maximum Scale in each selected locale', function(assert) {
for (let locale of ['en-us', 'zh-tw']) {
run(() => this.intl.setLocale([locale, 'en-us']));
for (let type of ['scaleService', 'scaleHost']) {
let fields = {amount: {type: 'int', min: 1}, min: {type: 'int', min: 1}, max: {type: 'int', min: 1}};
let subject = this.subject(type, fields, {amount: -1, min: -1, max: -1});
assert.deepEqual(subject.validationErrors(), ['amount', 'min', 'max'].map((key) =>
this.intl.t('validation.number.min', {key: expectedLabel(this.intl, type, key), val: 1})), `${locale} ${type}`);
}
}
});

test('upgrade numeric errors match the formUpgrade labels actually displayed by its template', function(assert) {
let config = this.subject('serviceUpgrade', {batchSize: {type: 'int', min: 1}, intervalMillis: {type: 'int', min: 1}},
{batchSize: -1, intervalMillis: -1});
assert.deepEqual(config.validationErrors(), ['"Batch Size" should be at least 1', '"Batch Interval" should be at least 1']);
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
assert.deepEqual(config.validationErrors(), ['"批次大小" 必須至少為 1', '"批次間隔" 必須至少為 1']);
});

test('the real scale model min-greater-than-max checks are localized without changing their boundaries', function(assert) {
for (let [type, Factory] of [['scaleService', ScaleService], ['scaleHost', ScaleHost]]) {
let subject = this.subject(type, {min: {type: 'int'}, max: {type: 'int'}}, {min: 3, max: 2}, Factory);
run(() => this.intl.setLocale(['en-us']));
assert.deepEqual(subject.validationErrors(), ['"Minimum Scale" should be at most 2'], `${type} English`);
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
assert.deepEqual(subject.validationErrors(), ['"最小數量" 必須最多為 2'], `${type} Traditional Chinese`);
for (let [min, max] of [[2, 2], [1, 2], [null, 2], [3, null]]) {
subject.setProperties({min, max});
assert.deepEqual(subject.validationErrors(), [], `${type} unchanged boundary ${min}/${max}`);
}
}
});

test('model-specific label then direct model translation retain precedence over the form label', function(assert) {
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
this.intl.addTranslations('zh-tw', {
'model.receiver.name.label': '專用名稱', 'model.receiver.driver': '專用類型',
'model.scaleService.serviceId.label': '專用服務',
});
let receiver = this.subject('receiver', {name: {type: 'string', required: true}, driver: {type: 'enum', required: true}});
let config = this.subject('scaleService', {serviceId: {type: 'string', required: true}});
assert.deepEqual(receiver.validationErrors(), ['"專用名稱" 必須設定', '"專用類型" 必須設定']);
assert.deepEqual(config.validationErrors(), ['"專用服務" 必須設定']);
});

test('unknown fields and same-named fields outside Receiver configs keep the existing English fallback', function(assert) {
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
for (let type of ['receiver', 'scaleService', 'scaleHost', 'serviceUpgrade']) {
let subject = this.subject(type, {unknownField: {type: 'string', required: true}});
assert.deepEqual(subject.validationErrors(), ['"Unknown Field" 必須設定'], type);
}
let unrelated = this.subject('service', {serviceId: {type: 'string', required: true}});
assert.deepEqual(unrelated.validationErrors(), ['"Service Id" 必須設定'], 'no global serviceId relabeling');
});

test('valid nested inputs retain values, schemas and the same empty-error result', function(assert) {
for (let locale of ['en-us', 'zh-tw']) {
run(() => this.intl.setLocale([locale, 'en-us']));
let config = this.subject('scaleService', {
serviceId: {type: 'string', required: true}, amount: {type: 'int', min: 1},
}, {serviceId: 'test-service', amount: 1});
let receiver = this.subject('receiver', {
name: {type: 'string', required: true}, scaleServiceConfig: {type: 'scaleService'},
}, {name: 'test-receiver', scaleServiceConfig: config});
assert.deepEqual(receiver.validationErrors(), [], locale);
assert.strictEqual(config.get('serviceId'), 'test-service');
assert.strictEqual(config.get('amount'), 1);
assert.strictEqual(receiver.get('name'), 'test-receiver');
}
});
});
Loading