diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 1b737c7ec7..5ed9b6100c 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -4,6 +4,18 @@ 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.
+Candidate `1.6.178` handles only the exact `inactive` environment state specially,
+after global project and member authorization succeeds. Network and
+policy-manager values remain unavailable (`null`), with a state-specific
+localized explanation; they are not invented empty resource collections.
+Project metadata, membership and removal capabilities remain sourced from the
+API. A stale editable network cannot be persisted from that inactive form.
+All other states retain the existing scoped reads and error semantics. Backend
+authorization, session generation/mutex, MFA, OIDC, workspace lifecycle and
+dependencies are unchanged. Formal publication and deployed native acceptance
+are pending; historical HOLDs and the incomplete full matrix remain unchanged.
+See the [candidate release note](docs/releases/web-console-1.6.178.md).
+
Published `1.6.177` treats an ended workspace entry as terminal across both logs
and terminal components. Late responses and queued socket/timer callbacks are
bound to the original entry, never an explicitly opened replacement. Existing
diff --git a/README.md b/README.md
index de64d5c5d9..251c99fc88 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,19 @@ PastureStack is an independent community effort to preserve, audit, and moderniz
## Project status
+Candidate `1.6.178` fixes viewing and editing an inactive environment. Once the
+globally authorized project and members have loaded, the page does not request
+network or policy-manager data scoped to that inactive environment: the API
+correctly denies those requests. The existing form explains that network
+settings are unavailable until activation, in all thirteen packaged locales.
+Metadata and membership controls still follow their original API action links;
+cached network data cannot enable a network save. Other states and authorization
+failures retain the established error handling. This candidate changes no
+backend, authentication, session ownership or dependency graph. Formal
+publication and deployed native acceptance remain pending; it does not promote
+earlier HOLD results or claim completion of the full matrix. See the
+[candidate release note](docs/releases/web-console-1.6.178.md).
+
Published `1.6.177` prevents ended log and terminal workspace entries from
reconnecting after remount/reload or from late access-ticket, broker, socket
and timer callbacks. Both components share one lifecycle boundary; asynchronous
diff --git a/app/components/view-edit-project/component.js b/app/components/view-edit-project/component.js
index 07f50a298a..ed7d3ff9fc 100644
--- a/app/components/view-edit-project/component.js
+++ b/app/components/view-edit-project/component.js
@@ -20,6 +20,7 @@ export default Component.extend(NewOrEdit, Sortable, {
originalProject: null,
allProjects: null,
policyManager: null,
+ networkUnavailableForInactiveProject: false,
editing: false,
tab: 'access',
@@ -150,8 +151,8 @@ export default Component.extend(NewOrEdit, Sortable, {
}.property('project.id', 'project.actionLinks.setmembers'),
canEditNetwork: function() {
- return !!this.get('network.actionLinks.update') && !this.get('missingManager') && !this.get('hasUnsupportedPolicy');
- }.property('network.actionLinks.update', 'missingManager', 'hasUnsupportedPolicy'),
+ return !this.get('networkUnavailableForInactiveProject') && !!this.get('network.actionLinks.update') && !this.get('missingManager') && !this.get('hasUnsupportedPolicy');
+ }.property('networkUnavailableForInactiveProject', 'network.actionLinks.update', 'missingManager', 'hasUnsupportedPolicy'),
canSave: function() {
return this.get('canEditProject') ||
diff --git a/app/components/view-edit-project/template.hbs b/app/components/view-edit-project/template.hbs
index 8a3548107a..4c6c636167 100644
--- a/app/components/view-edit-project/template.hbs
+++ b/app/components/view-edit-project/template.hbs
@@ -137,7 +137,11 @@
-{{#if this.network}}
+{{#if this.networkUnavailableForInactiveProject}}
+
+ {{t 'viewEditProject.networkPolicy.inactive'}}
+
+{{else if this.network}}
{{t 'viewEditProject.networkPolicy.label'}}
diff --git a/app/settings/projects/detail/route.js b/app/settings/projects/detail/route.js
index 332728df15..60a4a8f273 100644
--- a/app/settings/projects/detail/route.js
+++ b/app/settings/projects/detail/route.js
@@ -50,7 +50,12 @@ export default Route.extend(PromiseToCb, {
}
);
})],
- networks: ['project', this.toCb(() => {
+ networks: ['importMembers', this.toCb((results) => {
+ // Inactive environments remain globally visible to their owners,
+ // but the API correctly rejects requests scoped to them.
+ if ( results.project.get('state') === 'inactive' ) {
+ return null;
+ }
return userStore.find('network', null, {
filter: {accountId: params.project_id},
headers: {[C.HEADER.PROJECT_ID]: params.project_id},
@@ -58,7 +63,10 @@ export default Route.extend(PromiseToCb, {
throw this.environmentLoadError(err, 'viewEditProject.error.relatedUnavailable');
});
})],
- policyManagers: ['project', this.toCb(() => {
+ policyManagers: ['importMembers', this.toCb((results) => {
+ if ( results.project.get('state') === 'inactive' ) {
+ return null;
+ }
return userStore.find('stack', null, policyManagerOpt).then(null, (err) => {
throw this.environmentLoadError(err, 'viewEditProject.error.relatedUnavailable');
});
@@ -75,7 +83,7 @@ export default Route.extend(PromiseToCb, {
}, 'Load all the things');
return promise.then((hash) => {
- let network = hash.networks.find((x) => C.PROJECT.SUPPORTS_NETWORK_POLICY.includes(x.get('name')));
+ let network = hash.networks ? hash.networks.find((x) => C.PROJECT.SUPPORTS_NETWORK_POLICY.includes(x.get('name'))) : null;
if ( network ) {
network = network.clone();
@@ -106,7 +114,8 @@ export default Route.extend(PromiseToCb, {
let out = EmberObject.create({
all: hash.allProjects,
network: network,
- policyManager: hash.policyManagers.objectAt(0),
+ policyManager: hash.policyManagers ? hash.policyManagers.objectAt(0) : null,
+ networkUnavailableForInactiveProject: hash.project.get('state') === 'inactive',
});
if ( params.editing ) {
diff --git a/app/settings/projects/detail/template.hbs b/app/settings/projects/detail/template.hbs
index d2cb7f2848..4d63521636 100644
--- a/app/settings/projects/detail/template.hbs
+++ b/app/settings/projects/detail/template.hbs
@@ -6,6 +6,7 @@
initialStacks=this.model.stacks
serviceChoices=this.model.serviceChoices
policyManager=this.model.policyManager
+ networkUnavailableForInactiveProject=this.model.networkUnavailableForInactiveProject
showEdit=this.editing
editing=true
tab=this.tab
diff --git a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json
index 82eb97fd86..5c2278190e 100644
--- a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json
+++ b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
- "version": "1.6.177",
+ "version": "1.6.178",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
- "version": "1.6.177",
+ "version": "1.6.178",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
diff --git a/docs/releases/web-console-1.6.178.md b/docs/releases/web-console-1.6.178.md
new file mode 100644
index 0000000000..4d7fffbee9
--- /dev/null
+++ b/docs/releases/web-console-1.6.178.md
@@ -0,0 +1,37 @@
+# Web Console 1.6.178 — inactive environment details
+
+Status: source candidate; formal publication and deployed native acceptance
+are pending. No earlier HOLD is promoted to PASS.
+
+## Cause and correction
+
+An inactive environment remains globally visible to an authorized owner, but
+the engine rejects API requests scoped to that inactive environment. The
+details route unconditionally requested networks and policy-manager stacks;
+their 403 responses prevented its model from committing and obscured otherwise
+authorized metadata and membership information.
+
+The route now waits for globally authorized project and membership reads,
+then skips only those two inapplicable scoped reads when the project state is
+exactly `inactive`. It returns `null` for the unavailable data and a dedicated
+reason flag. The existing details component displays a translated explanation
+in all thirteen packaged locales and prevents a cached network from enabling network saves.
+Metadata, member and removal action links are unchanged. Active and transitional
+states, denied global data, expired sessions and other failures keep the
+existing authorization and error behavior.
+
+## Change boundary and verification
+
+Changes are confined to the details route/template, shared details component,
+thirteen translations and their three focused test modules. Release-version metadata
+and the matching reviewed lock baseline move together; dependencies do not
+change. No API, engine, provider, HAProxy, authentication or session contract is
+modified. The existing ended log/terminal and cross-tab session tests are
+retained.
+
+Added regressions cover inactive view/edit, globally denied project/member
+reads, non-inactive scoped denial, the actual details-template reason flag and
+stale-network write prevention. Formal exact-source tests, reproducible static
+archives, immutable publication and isolated native view/edit/reload/removal
+acceptance must be recorded separately. Full role/resource/locale acceptance
+remains incomplete.
diff --git a/package-lock.json b/package-lock.json
index 82eb97fd86..5c2278190e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
- "version": "1.6.177",
+ "version": "1.6.178",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
- "version": "1.6.177",
+ "version": "1.6.178",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
diff --git a/package.json b/package.json
index fa0af56534..648adb0516 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/web-console",
- "version": "1.6.177",
+ "version": "1.6.178",
"private": true,
"description": "PastureStack browser console for the compatible control platform.",
"repository": {
diff --git a/scripts/check-modernization-blockers b/scripts/check-modernization-blockers
index c7e84ecc97..69984e687a 100755
--- a/scripts/check-modernization-blockers
+++ b/scripts/check-modernization-blockers
@@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f:
print(json.load(f).get('version', ''))
PY
)
-if [[ "$version" != "1.6.177" ]]; then
- echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.177"
+if [[ "$version" != "1.6.178" ]]; then
+ echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.178"
failures=$((failures + 1))
fi
diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace
index 8aca861389..c5eb40c6e2 100755
--- a/scripts/check-ui-console-workspace
+++ b/scripts/check-ui-console-workspace
@@ -143,4 +143,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.177 browser-session broker-broadcast
+ 1.6.178 browser-session broker-broadcast
diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies
index 2d6af6ee44..3caafd7317 100755
--- a/scripts/check-ui-critical-high-dependencies
+++ b/scripts/check-ui-critical-high-dependencies
@@ -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.177":
+if package.get("version") != "1.6.178":
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')}")
diff --git a/tests/integration/components/view-edit-project-permissions-test.js b/tests/integration/components/view-edit-project-permissions-test.js
index e1d4ac0839..ecdcfa5d9a 100644
--- a/tests/integration/components/view-edit-project-permissions-test.js
+++ b/tests/integration/components/view-edit-project-permissions-test.js
@@ -1,14 +1,17 @@
import { A } from '@ember/array';
import Component from '@ember/component';
+import EmberRouter from '@ember/routing/router';
import EmberObject, { get } from '@ember/object';
-import Service from '@ember/service';
+import Service, { service } from '@ember/service';
import { precompileTemplate } from '@ember/template-compilation';
import { click, find, findAll, render, select, settled, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { initialize as initializePodLayouts } from 'ui/initializers/pod-component-layouts';
+import ViewEditProject from 'ui/components/view-edit-project/component';
import ProjectTemplate from 'ui/models/projecttemplate';
import Router from 'ui/router';
+import ProjectDetailTemplate from 'ui/settings/projects/detail/template';
import { destroyOwned } from '../../helpers/owned-subject';
import resolver from '../../helpers/resolver';
@@ -218,6 +221,63 @@ module('Integration | Component | view edit project permissions', function(hooks
assert.strictEqual(findAll('.footer-actions button').length, 2);
});
+ for (let editing of [false, true]) {
+ test(`inactive detail ${editing ? 'edit' : 'view'} shows its state reason without scoped controls`, async function(assert) {
+ // Use an isolated route map; the application's extension registry is
+ // intentionally consumed only once by its native Router.
+ let DetailRouter = EmberRouter.extend({location: 'none'});
+ DetailRouter.map(function() {
+ this.route('settings', function() {
+ this.route('projects', {path: '/env'}, function() {
+ this.route('detail', {path: '/:project_id'});
+ });
+ });
+ });
+ this.owner.register('router:main', DetailRouter);
+ this.owner.lookup('router:main').setupRouter();
+ this.owner.register('component:action-menu', Component.extend({
+ layout: precompileTemplate('{{this.model.actionLinks.remove}}'),
+ }));
+ this.owner.register('component:header-state', Component.extend({
+ layout: precompileTemplate('{{this.model.state}}'),
+ }));
+ this.owner.register('component:power-select', Component.extend({
+ layout: precompileTemplate(''),
+ }));
+ let actionLinks = {update: '/projects/1a21', setmembers: '/projects/1a21?action=setmembers', remove: '/projects/1a21'};
+ this.project.setProperties({state: 'inactive', actionLinks});
+ this.network.set('actionLinks', {update: '/networks/1n1'});
+ this.model = EmberObject.create({
+ project: this.project, originalProject: this.originalProject, all: A([this.project]),
+ network: this.network, policyManager: this.policyManager, networkUnavailableForInactiveProject: true,
+ });
+ this.editing = editing;
+ this.done = () => {};
+ this.cancel = () => {};
+ this.owner.register('service:user-store', Service.extend(this.userStore));
+ // Rendering owners do not run the application's store initializer.
+ // Supply its real service injection without replacing component logic.
+ this.owner.register('component:view-edit-project', ViewEditProject.extend({userStore: service('user-store')}));
+ // Render the actual detail template so its reason flag forwarding is tested.
+ await render(ProjectDetailTemplate);
+
+ assert.ok(find('[data-test-inactive-network-notice]').textContent.includes('viewEditProject.networkPolicy.inactive'), 'a state-specific translated explanation is visible');
+ assert.ok(find('[data-test-member-name]'), 'global membership data stays visible');
+ assert.strictEqual(findAll('.radio input').length, 0, 'cached network data cannot expose policy controls');
+ assert.notOk(find('[data-test-network-only-edit]'), 'no scoped network edit link is introduced');
+ assert.strictEqual(this.project.get('actionLinks'), actionLinks, 'existing global capabilities are untouched');
+ if ( editing ) {
+ assert.false(find('input[type="text"]').disabled, 'global metadata capability still enables its fields');
+ assert.ok(find('[data-test-member-add]'), 'global member capability remains available');
+ assert.strictEqual(findAll('table.grid select').length, 1, 'member roles remain editable');
+ assert.strictEqual(findAll('.footer-actions button').length, 2, 'global save and cancel remain available');
+ } else {
+ assert.strictEqual(find('[data-test-header-state]').textContent.trim(), 'inactive', 'the environment state remains visible');
+ assert.strictEqual(find('[data-test-action-menu]').textContent.trim(), actionLinks.remove, 'the existing action menu receives the original global remove link');
+ }
+ });
+ }
+
test('network-only environment can reach its edit form from the detail header', async function(assert) {
this.owner.register('router:main', Router);
this.owner.register('component:action-menu', Component.extend({
diff --git a/tests/unit/components/view-edit-project-permissions-test.js b/tests/unit/components/view-edit-project-permissions-test.js
index ce77e9afab..e4a2aee261 100644
--- a/tests/unit/components/view-edit-project-permissions-test.js
+++ b/tests/unit/components/view-edit-project-permissions-test.js
@@ -163,6 +163,37 @@ test('member actions cannot mutate a readonly membership list', function(assert)
destroyOwned(component);
});
+test('inactive network unavailability preserves global capabilities and prevents stale network saves', async function(assert) {
+ for (let canEdit of [false, true]) {
+ let writes = {project: 0, members: 0, network: 0};
+ let actionLinks = canEdit ? {update: '/projects/1a21', setmembers: '/projects/1a21?action=setmembers', remove: '/projects/1a21'} : {};
+ let project = EmberObject.create({
+ id: '1a21', state: 'inactive', actionLinks,
+ projectMembers: A([EmberObject.create({externalIdType: 'oidc_user', externalId: 'owner-1', role: 'owner'})]),
+ validationErrors() { return A([]); },
+ save() { writes.project++; return resolve(this); },
+ doAction(action) { assert.strictEqual(action, 'setmembers'); writes.members++; return resolve(); },
+ });
+ let network = EmberObject.create({
+ actionLinks: {update: '/networks/1n1'}, policy: A([]),
+ save() { writes.network++; return resolve(this); },
+ });
+ let component = makeComponent(project, network, {networkUnavailableForInactiveProject: true});
+
+ try {
+ assert.strictEqual(component.get('canEditProject'), canEdit, 'metadata still follows the global project link');
+ assert.strictEqual(component.get('canEditMembers'), canEdit, 'members still follow the global setmembers link');
+ assert.false(component.get('canEditNetwork'), 'even a stale editable network cannot enable scoped persistence');
+ assert.strictEqual(component.get('canSave'), canEdit, 'the notice does not grant a save capability');
+ await component.get('actions').save.call(component);
+ assert.deepEqual(writes, {project: Number(canEdit), members: Number(canEdit), network: 0}, 'only the advertised global operations are saved');
+ assert.strictEqual(project.get('actionLinks'), actionLinks, 'remove and other action links are not rewritten');
+ } finally {
+ destroyOwned(component);
+ }
+ }
+});
+
test('member validation errors use translated messages', function(assert) {
let owner = EmberObject.create({externalIdType: 'oidc_user', externalId: 'owner-1', role: 'owner'});
let project = EmberObject.create({
diff --git a/tests/unit/settings/projects/detail/route-test.js b/tests/unit/settings/projects/detail/route-test.js
index 133b943302..fed8132abb 100644
--- a/tests/unit/settings/projects/detail/route-test.js
+++ b/tests/unit/settings/projects/detail/route-test.js
@@ -13,6 +13,7 @@ function fixture(failureAt, failure = new Error(`${failureAt} failed`)) {
let project = EmberObject.create({
id: '1a21',
name: 'QA project',
+ state: 'active',
projectMembers: null,
followLink(name) {
if ( failureAt === 'membersSync' ) {
@@ -30,6 +31,8 @@ function fixture(failureAt, failure = new Error(`${failureAt} failed`)) {
return EmberObject.create({
id: this.get('id'),
name: this.get('name'),
+ state: this.get('state'),
+ actionLinks: this.get('actionLinks'),
projectMembers: this.get('projectMembers'),
});
},
@@ -66,6 +69,83 @@ function fixture(failureAt, failure = new Error(`${failureAt} failed`)) {
return {failure, members, policyManager, project, store, relatedCalls};
}
+for (let editing of [false, true]) {
+ test(`inactive environment ${editing ? 'edit' : 'view'} preserves global data without scoped resource reads`, async function(assert) {
+ let data = fixture();
+ let actionLinks = {update: '/projects/1a21', setmembers: '/projects/1a21?action=setmembers', remove: '/projects/1a21'};
+ data.project.setProperties({state: 'inactive', actionLinks});
+ let route = ProjectDetailRoute.create({userStore: data.store});
+
+ try {
+ let model = await route.model({project_id: '1a21', editing});
+ assert.deepEqual(data.relatedCalls, [], 'neither network nor policy-manager API is requested');
+ assert.strictEqual(model.get('network'), null, 'network data is unavailable, not an empty collection');
+ assert.strictEqual(model.get('policyManager'), null, 'no policy manager is invented');
+ assert.true(model.get('networkUnavailableForInactiveProject'), 'the model exposes the exact state reason');
+ assert.strictEqual(model.get('all').objectAt(0), data.project, 'the global project list is retained');
+ assert.strictEqual(model.get('project.projectMembers'), data.members, 'authorized global memberships are retained');
+ assert.strictEqual(model.get('project.actionLinks'), actionLinks, 'the API remains the source of metadata, member, and remove capabilities');
+ assert.strictEqual(model.get('originalProject'), editing ? data.project : null);
+ assert.strictEqual(model.get('project') === data.project, !editing, 'only the edit form clones the project');
+ } finally {
+ run(() => route.destroy());
+ }
+ });
+}
+
+test('inactive global project and member failures remain errors before any scoped reads', async function(assert) {
+ let intl = EmberObject.create({t(key) { return key; }});
+ for (let task of ['allProjects', 'project', 'members']) {
+ for (let status of [401, 403, 404, 503]) {
+ let failure = {status, message: 'Raw API message'};
+ let data = fixture(task, failure);
+ data.project.set('state', 'inactive');
+ let route = ProjectDetailRoute.create({userStore: data.store, intl});
+
+ try {
+ await route.model({project_id: '1a21', editing: false}).then(
+ () => assert.ok(false, `${task} ${status} must reject`),
+ (error) => {
+ assert.strictEqual(error.status, status === 403 || status === 404 ? 404 : status,
+ `${task} ${status} keeps the established error classification`);
+ if ( status === 401 ) {
+ assert.strictEqual(error, failure, 'expired-session handling is unchanged');
+ }
+ }
+ );
+ assert.deepEqual(data.relatedCalls, [], `${task} ${status} cannot start scoped reads`);
+ } finally {
+ run(() => route.destroy());
+ }
+ }
+ }
+});
+
+test('only exact inactive skips scoped reads; other states still reject related 403s', async function(assert) {
+ let intl = EmberObject.create({t(key) { return key; }});
+ for (let state of ['active', 'deactivating', 'activating', 'removed', 'upgrading', 'updating-active', 'unknown', null, undefined]) {
+ for (let task of ['networks', 'policyManagers']) {
+ let data = fixture(task, {status: 403, message: 'Forbidden'});
+ data.project.set('state', state);
+ let route = ProjectDetailRoute.create({userStore: data.store, intl});
+
+ try {
+ await route.model({project_id: '1a21', editing: false}).then(
+ () => assert.ok(false, `${state} ${task} 403 must reject`),
+ (error) => {
+ assert.strictEqual(error.status, 404, 'the existing denied-resource error is preserved');
+ assert.strictEqual(error.messageKey, 'viewEditProject.error.relatedUnavailable');
+ }
+ );
+ assert.ok(data.relatedCalls.includes(task === 'networks' ? 'network' : 'stack'), 'the applicable request is still sent');
+ assert.strictEqual(data.project.get('projectMembers'), data.members, 'global members were loaded first');
+ } finally {
+ run(() => route.destroy());
+ }
+ }
+ }
+});
+
test('a denied project cannot start unrelated network or policy-manager reads', async function(assert) {
let data = fixture('project', {status: 404, message: 'Environment unavailable'});
let route = ProjectDetailRoute.create({userStore: data.store, intl: EmberObject.create({
@@ -89,6 +169,8 @@ test('loads project members through the supported link contract before cloning f
assert.notStrictEqual(model.get('project'), data.project, 'editing uses a clone');
assert.strictEqual(model.get('project.projectMembers'), data.members, 'the imported members reach the editable clone');
assert.strictEqual(model.get('policyManager'), data.policyManager, 'the policy manager is loaded');
+ assert.false(model.get('networkUnavailableForInactiveProject'), 'an active environment has no inactive notice');
+ assert.deepEqual(data.relatedCalls, ['network', 'stack'], 'both active-environment reads still run');
assert.strictEqual(data.store.get('networkOptions.filter.accountId'), '1a21', 'the network lookup stays in the selected project');
assert.strictEqual(data.store.get('networkOptions.headers.X-Api-Project-Id'), '1a21', 'the network lookup uses its project policy');
assert.strictEqual(data.store.get('policyManagerOptions.headers.X-Api-Project-Id'), '1a21', 'policy manager lookup is scoped to the project');
diff --git a/translations/de-de.yaml b/translations/de-de.yaml
index b13b473c46..11284ed9ee 100644
--- a/translations/de-de.yaml
+++ b/translations/de-de.yaml
@@ -3840,6 +3840,7 @@ viewEditProject:
noMembers: 'Fügen Sie ein oder mehrere Mitglieder hinzu, die diese Umgebung nutzen können.'
networkPolicy:
label: Netzwerkrichtlinie
+ inactive: Diese Umgebung ist deaktiviert; die Netzwerkeinstellungen sind vorübergehend nicht verfügbar. Aktivieren Sie die Umgebung, um sie anzuzeigen oder zu bearbeiten.
description: 'Steuern Sie, welche Container über das verwaltete Netzwerk miteinander kommunizieren dürfen.'
unsupported: >-
Diese Umgebung verfügt über Netzwerkrichtlinienregeln, die von UI noch nicht unterstützt werden. Bitte verwenden
diff --git a/translations/en-us.yaml b/translations/en-us.yaml
index 0302b7e84e..f5ef42be6b 100644
--- a/translations/en-us.yaml
+++ b/translations/en-us.yaml
@@ -4213,6 +4213,7 @@ viewEditProject:
noMembers: Add one or more members who can use this environment.
networkPolicy:
label: Network Policy
+ inactive: This environment is deactivated; network settings are temporarily unavailable. Activate the environment to view or edit them.
description: Control what containers are allowed to communicate with each other over the Managed Network.
unsupported: This environment has network policy rules which are not yet supported by the UI. Please use the API to manage the policy.
noManager: Network Policies are available only for environments using the PastureStack orchestration engine with the Network Policy Manager infrastructure template deployed.
diff --git a/translations/fa-ir.yaml b/translations/fa-ir.yaml
index 7649462fd2..c8092def89 100644
--- a/translations/fa-ir.yaml
+++ b/translations/fa-ir.yaml
@@ -3729,6 +3729,7 @@ viewEditProject:
noMembers: افزودن اعضایی که می توانند از این محیط استفاده کنند.
networkPolicy:
label: خط مشی شبکه
+ inactive: این محیط غیرفعال است؛ تنظیمات شبکه موقتاً در دسترس نیستند. برای مشاهده یا ویرایش آنها، محیط را فعال کنید.
description: کنترل کنید که چه کانتینرهایی مجاز به برقراری ارتباط با یکدیگر از طریق شبکه مدیریت شده هستند.
unsupported: >-
این محیط دارای قوانین خط مشی شبکه است که هنوز توسط UI پشتیبانی نمی شود. لطفاً از API برای مدیریت خط مشی استفاده
diff --git a/translations/fil-ph.yaml b/translations/fil-ph.yaml
index 81daf2abf4..d08aace7d6 100644
--- a/translations/fil-ph.yaml
+++ b/translations/fil-ph.yaml
@@ -3786,6 +3786,7 @@ viewEditProject:
noMembers: Magdagdag ng isa o higit pang miyembro na maaaring gumamit ng environment na ito.
networkPolicy:
label: Patakaran sa Network
+ inactive: Naka-deactivate ang environment na ito; pansamantalang hindi available ang mga setting ng network. I-activate ang environment upang makita o i-edit ang mga ito.
description: Kontrolin kung anong mga container ang pinapayagang makipag-ugnayan sa isa't isa sa Pamamahala ng Network.
unsupported: >-
Ang kapaligirang ito ay may mga panuntunan sa patakaran sa network na hindi pa sinusuportahan ng UI. Pakigamit
diff --git a/translations/fr-fr.yaml b/translations/fr-fr.yaml
index 4d0145d346..623e9a6e8e 100644
--- a/translations/fr-fr.yaml
+++ b/translations/fr-fr.yaml
@@ -3785,6 +3785,7 @@ viewEditProject:
noMembers: Ajouter un ou plusieurs membres qui peuvent utiliser cet environnement.
networkPolicy:
label: Politique de réseau
+ inactive: Cet environnement est désactivé ; les paramètres réseau sont temporairement indisponibles. Activez l’environnement pour les consulter ou les modifier.
description: Contrôlez quels conteneurs sont autorisés à communiquer entre eux sur le réseau géré.
unsupported: >-
Cet environnement possède des règles de politique réseau qui ne sont pas encore prises en charge par UI. Veuillez
diff --git a/translations/hu-hu.yaml b/translations/hu-hu.yaml
index 0eaebccfea..f027a8aff1 100644
--- a/translations/hu-hu.yaml
+++ b/translations/hu-hu.yaml
@@ -3803,6 +3803,7 @@ viewEditProject:
noMembers: Egy vagy több tag hozzáadása ehhez a környezethez.
networkPolicy:
label: Hálózati házirend
+ inactive: Ez a környezet inaktív; a hálózati beállítások átmenetileg nem érhetők el. A megtekintésükhöz vagy módosításukhoz aktiválja a környezetet.
description: 'Szabályozhatod, hogy mely konténerek kommunikálhatnak egymással a kezelt hálózaton keresztül.'
unsupported: >-
Ez a környezet hálózati házirend-szabályokkal rendelkezik, amelyeket a UI még nem támogat. Kérjük, használja a
diff --git a/translations/ja-jp.yaml b/translations/ja-jp.yaml
index c3249d29c2..b354e8d0ed 100644
--- a/translations/ja-jp.yaml
+++ b/translations/ja-jp.yaml
@@ -3991,6 +3991,7 @@ viewEditProject:
noMembers: この環境を利用することができる 1 つまたは複数のメンバーを追加します。
networkPolicy:
label: ネットワークポリシー
+ inactive: この環境は無効です。ネットワーク設定は一時的に利用できません。表示または編集するには環境を有効にしてください。
description: 管理ネットワーク経由での相互通信を許可するコンテナをコントロールします。
unsupported: この環境には UI ではサポートされていない ネットワークポリシー ルール が存在します。ポリシーを管理するには API を利用してください。
noManager: ネットワークポリシーは Network Policy Manager テンプレートがデプロイされており PastureStack オーケストレーションエンジンを利用している環境下でのみ利用できます。
diff --git a/translations/ko-kr.yaml b/translations/ko-kr.yaml
index 999b2efd3d..4f4b85f773 100644
--- a/translations/ko-kr.yaml
+++ b/translations/ko-kr.yaml
@@ -3643,6 +3643,7 @@ viewEditProject:
noMembers: 이 환경을 사용할 수 있는 구성원을 한 명 이상 추가하세요.
networkPolicy:
label: 네트워크 정책
+ inactive: 이 환경은 비활성 상태이며 네트워크 설정을 일시적으로 사용할 수 없습니다. 설정을 보거나 수정하려면 환경을 활성화하세요.
description: 관리형 네트워크를 통해 서로 통신할 수 있는 컨테이너를 제어합니다.
unsupported: 이 환경에는 UI에서 아직 지원되지 않는 네트워크 정책 규칙이 있습니다. 정책을 관리하려면 API를 이용하세요.
noManager: 네트워크 정책은 네트워크 정책 관리자 인프라 템플릿이 배포된 PastureStack 오케스트레이션 엔진을 사용하는 환경에서만 사용할 수 있습니다.
diff --git a/translations/pt-br.yaml b/translations/pt-br.yaml
index c8f97e87db..0edfbf4bb4 100644
--- a/translations/pt-br.yaml
+++ b/translations/pt-br.yaml
@@ -3789,6 +3789,7 @@ viewEditProject:
noMembers: Adicione um ou mais membros que possam usar este ambiente.
networkPolicy:
label: Política de Rede
+ inactive: Este ambiente está desativado; as configurações de rede estão temporariamente indisponíveis. Ative o ambiente para visualizá-las ou editá-las.
description: Controle quais contêineres podem se comunicar entre si pela Rede Gerenciada.
unsupported: >-
Este ambiente possui regras de política de rede que ainda não são suportadas pelo UI. Use o API para gerenciar a
diff --git a/translations/ru-ru.yaml b/translations/ru-ru.yaml
index 14d5d81aa1..6656cba54c 100644
--- a/translations/ru-ru.yaml
+++ b/translations/ru-ru.yaml
@@ -3811,6 +3811,7 @@ viewEditProject:
noMembers: 'Добавьте одиного или несколько участников, которые могут использовать эту среду.'
networkPolicy:
label: Сетевая политика
+ inactive: Эта среда отключена; настройки сети временно недоступны. Активируйте среду, чтобы просмотреть или изменить их.
description: 'Контролируйте, каким контейнерам разрешено взаимодействовать друг с другом через управляемую сеть.'
unsupported: >-
В этой среде есть правила сетевой политики, которые еще не поддерживаются UI. Пожалуйста, используйте API для
diff --git a/translations/uk-ua.yaml b/translations/uk-ua.yaml
index cad66e5dfe..3d6dc9c7c3 100644
--- a/translations/uk-ua.yaml
+++ b/translations/uk-ua.yaml
@@ -3848,6 +3848,7 @@ viewEditProject:
noMembers: 'Додайте одного або декілька учасників, які можуть використовувати це середовище.'
networkPolicy:
label: Політика мережі
+ inactive: Це середовище вимкнено; налаштування мережі тимчасово недоступні. Активуйте середовище, щоб переглянути або змінити їх.
description: 'Ви можете контролювати, контейнери які можуть зв''язуватися один з одним через керовану мережу.'
unsupported: >-
Це середовище має правила мережевої політики, які ще не підтримуються UI. Для керування політикою використовуйте
diff --git a/translations/zh-hans.yaml b/translations/zh-hans.yaml
index 99b8f64242..70d077f736 100644
--- a/translations/zh-hans.yaml
+++ b/translations/zh-hans.yaml
@@ -3686,6 +3686,7 @@ viewEditProject:
noMembers: 添加一个或者多个成员能够使用此环境的成员
networkPolicy:
label: 网络策略
+ inactive: 此环境已停用;网络设置暂不可用。启用环境后可查看或修改。
description: 控制受管网络中哪些容器彼此之间能够进行通讯
unsupported: 此环境中的网络策略设置暂不支持通过UI进行设置,请使用API管理网络策略。
noManager: 网络策略功能仅在使用 PastureStack 编排引擎且部署了Network Policy Manager基础设施模板的环境中可用。
diff --git a/translations/zh-tw.yaml b/translations/zh-tw.yaml
index 0666aedc19..bdd975ab5d 100644
--- a/translations/zh-tw.yaml
+++ b/translations/zh-tw.yaml
@@ -4020,6 +4020,7 @@ viewEditProject:
noMembers: 新增一個或多個成員能夠使用此環境的成員
networkPolicy:
label: 網路策略
+ inactive: 環境已停用;網路設定暫不可用。啟用後可檢視/修改。
description: 控制受管網路中哪些容器彼此之間能夠進行通訊
unsupported: 此環境中的網路策略設定暫不支援透過 UI 進行設定,請使用 API 管理網路策略。
noManager: 網路策略功能僅適用於使用 PastureStack 容器編排服務,且已部署網路策略管理基礎架構範本的環境。