diff --git a/e2e/specs/app/dataset_management_actions.spec.ts b/e2e/specs/app/dataset_management_actions.spec.ts index 820c0d3f..a7cd60f0 100644 --- a/e2e/specs/app/dataset_management_actions.spec.ts +++ b/e2e/specs/app/dataset_management_actions.spec.ts @@ -59,6 +59,43 @@ test.describe('Dataset management actions', () => { expect(requestedStates).toContain(null); }); + test('retries a quota allocation failure with visible admin controls and no unrelated ZFS changes', async ({ page }) => { + let updateCalls = 0; + await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' }); + await installHaveApiMock(page, { + user: { id: 1, login: 'admin', level: 99 }, + handlers: { + 'GET datasets/10': () => ownedDataset(), + 'GET transaction_chains': () => ({ transaction_chains: [] }), + 'PUT datasets/10': () => ++updateCalls === 1 + ? failEnvelope('Resource allocation error: no diskspace left') + : { dataset: ownedDataset({ refquota: 20480 }) }, + }, + }); + await page.goto('/admin/datasets/10'); + const override = page.getByTestId('dataset.manage.admin_override'); + await expect(override).toBeVisible(); + await expect(override).not.toBeChecked(); + await expect(page.getByTestId('dataset.manage.sync')).toBeHidden(); + await page.getByTestId('dataset.manage.refquota').fill('20'); + const first = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/datasets/10')); + await page.getByTestId('dataset.manage.edit.submit').click(); + expect((await first).postDataJSON()).toEqual({ dataset: { refquota: 20480 } }); + await expect(page.getByTestId('dataset.manage')).toContainText('no diskspace left'); + await expect(override).not.toBeChecked(); + await override.check(); + await page.getByTestId('dataset.manage.admin_lock_type').selectOption('not_less'); + expect(updateCalls).toBe(1); + const retry = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/datasets/10')); + await page.getByTestId('dataset.manage.edit.submit').click(); + expect((await retry).postDataJSON()).toEqual({ + dataset: { refquota: 20480, admin_override: true, admin_lock_type: 'not_less' }, + }); + await expect(override).not.toBeChecked(); + await expect(page.getByTestId('dataset.manage.admin_lock_type')).toHaveValue(''); + await expect(page.getByTestId('dataset.manage.sync')).toBeHidden(); + }); + test('creates, edits, and deletes a dataset from the overview', async ({ page }) => { await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' }); @@ -277,7 +314,7 @@ test.describe('Dataset management actions', () => { await page.getByTestId('dataset.manage.delete.confirm.confirm').click(); await deleteReq; expect(deleteCalls).toBe(1); - await expect(page).toHaveURL(/\/app\/datasets$/); + await expect(page).toHaveURL(/\/app\/vps\/300\/storage$/); }); test('sends only quota fields and preserves untouched advanced properties on update', async ({ page }) => { diff --git a/e2e/specs/app/vps_detail_tabs_matrix.spec.ts b/e2e/specs/app/vps_detail_tabs_matrix.spec.ts index f5420f05..ab988a06 100644 --- a/e2e/specs/app/vps_detail_tabs_matrix.spec.ts +++ b/e2e/specs/app/vps_detail_tabs_matrix.spec.ts @@ -161,7 +161,7 @@ test('@workflow-matrix @smoke VPS detail tabs expose storage, access, lifecycle, await expect(page.getByText('Autostart priority', { exact: true })).toHaveCount(0); await expect(page.getByText('Change reason', { exact: true })).toHaveCount(0); await expect(page.getByText('Admin lock type', { exact: true })).toHaveCount(0); - await expect(page.getByText('Admin override', { exact: true })).toHaveCount(0); + await expect(page.getByTestId('vps.config.admin_override')).toHaveCount(0); await page.getByRole('link', { name: /^Storage$/ }).click(); await expect(page).toHaveURL(/\/app\/vps\/123\/storage$/); @@ -432,5 +432,5 @@ test('@workflow-matrix @pr-smoke @pr-smoke-mobile VPS admin overview keeps each await expect(page.getByText('Autostart priority', { exact: true })).toBeVisible(); await expect(page.getByText('Change reason', { exact: true })).toBeVisible(); await expect(page.getByText('Admin lock type', { exact: true })).toBeVisible(); - await expect(page.getByText('Admin override', { exact: true })).toBeVisible(); + await expect(page.getByTestId('vps.config.admin_override')).toBeVisible(); }); diff --git a/e2e/specs/app/vps_lifecycle_tab_actions.spec.ts b/e2e/specs/app/vps_lifecycle_tab_actions.spec.ts index c0e7fd62..862298b5 100644 --- a/e2e/specs/app/vps_lifecycle_tab_actions.spec.ts +++ b/e2e/specs/app/vps_lifecycle_tab_actions.spec.ts @@ -119,6 +119,51 @@ async function installLifecycleMock(page: Page, options?: { } test.describe('@pr-smoke VPS lifecycle tab', () => { + test('retries a resource allocation failure only after an admin explicitly enables override', async ({ page }) => { + let updateCalls = 0; + await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' }); + await installLifecycleMock(page, { + updateVps: () => ++updateCalls === 1 + ? failEnvelope('Resource allocation error: no Memory left') + : { vps, _meta: { action_state_id: 506 } }, + }); + await page.goto('/admin/vps/123/config'); + await expect(page.getByTestId('vps.config.admin_override')).toBeVisible(); + await expect(page.getByTestId('vps.config.admin_override')).not.toBeChecked(); + await page.getByRole('spinbutton', { name: /^Memory/ }).fill('8192'); + await page.getByTestId('vps.config.header.save').click(); + const dialog = page.getByTestId('vps.config.confirm'); + const first = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/vpses/123')); + await dialog.getByTestId('vps.config.confirm.confirm').click(); + expect((await first).postDataJSON()).toEqual({ vps: { memory: 8192 } }); + await expect(dialog.getByTestId('vps.config.confirm.error')).toContainText('no Memory left'); + await expect(dialog.getByTestId('vps.config.confirm.admin_override')).not.toBeChecked(); + await dialog.getByTestId('vps.config.confirm.admin_override').check(); + await expect(dialog.getByTestId('vps.config.confirm.error')).toHaveCount(0); + expect(updateCalls).toBe(1); + await page.screenshot({ path: test.info().outputPath('resource-review.png') }); + const retry = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/vpses/123')); + await dialog.getByTestId('vps.config.confirm.confirm').click(); + expect((await retry).postDataJSON()).toEqual({ vps: { memory: 8192, admin_override: true } }); + await expect(dialog).toBeHidden(); + await expect(page.getByTestId('vps.config.admin_override')).not.toBeChecked(); + }); + + test('does not expose resource override to a member', async ({ page }) => { + await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' }); + await installLifecycleMock(page, { user: { id: 7, login: 'owner', level: 1 } }); + await page.goto('/app/vps/123/config'); + await page.getByRole('spinbutton', { name: /^Memory/ }).fill('4096'); + await expect(page.getByTestId('vps.config.admin_override')).toHaveCount(0); + await page.getByTestId('vps.config.header.save').click(); + const dialog = page.getByTestId('vps.config.confirm'); + await expect(dialog).toBeVisible(); + await expect(dialog.getByTestId('vps.config.confirm.admin_override')).toHaveCount(0); + const request = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/vpses/123')); + await dialog.getByTestId('vps.config.confirm.confirm').click(); + expect((await request).postDataJSON()).toEqual({ vps: { memory: 4096 } }); + }); + test('@pr-smoke-mobile keeps rejected VPS configuration saves inside the review dialog and allows retry', async ({ page }) => { let updateCalls = 0; await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' }); diff --git a/e2e/specs/app/vps_storage_tab_mounts.spec.ts b/e2e/specs/app/vps_storage_tab_mounts.spec.ts index 7594ceb4..00674a63 100644 --- a/e2e/specs/app/vps_storage_tab_mounts.spec.ts +++ b/e2e/specs/app/vps_storage_tab_mounts.spec.ts @@ -50,72 +50,77 @@ function mountItemControl(page: Page, mountId: number, control: 'dataset' | 'del } test.describe('@smoke VPS storage tab mounts', () => { - test('lets an admin resize the VPS root SSD live from the configuration entrypoint', async ({ page }) => { - await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' }); - let vpsUpdateCount = 0; - - await installHaveApiMock(page, { - user: { id: 1, login: 'admin', level: 99 }, - handlers: { - 'GET vpses/123': () => ({ vps }), - 'GET datasets/10': () => ({ dataset }), - 'GET ip_addresses': () => ({ ip_addresses: [] }), - 'GET transaction_chains': () => ({ transaction_chains: [] }), - 'GET vpses/123/mounts': () => ({ mounts: [] }), - 'GET dns_resolvers': () => ({ dns_resolvers: [] }), - 'GET user_namespace_maps': () => ({ user_namespace_maps: [] }), - 'PUT vpses/123': () => { - vpsUpdateCount += 1; - return { vps }; - }, - 'PUT datasets/10': () => ({ _meta: { action_state_id: 905 } }), - 'GET action_states/905': () => ({ - action_state: { - id: 905, - label: 'Resize root dataset', - status: true, - finished: false, - current: 0, - total: 1, + for (const override of [false, true]) { + test(`lets an admin resize the VPS root SSD live from the configuration entrypoint (override: ${override})`, async ({ page }) => { + await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' }); + let vpsUpdateCount = 0; + + await installHaveApiMock(page, { + user: { id: 1, login: 'admin', level: 99 }, + handlers: { + 'GET vpses/123': () => ({ vps }), + 'GET datasets/10': () => ({ dataset }), + 'GET ip_addresses': () => ({ ip_addresses: [] }), + 'GET transaction_chains': () => ({ transaction_chains: [] }), + 'GET vpses/123/mounts': () => ({ mounts: [] }), + 'GET dns_resolvers': () => ({ dns_resolvers: [] }), + 'GET user_namespace_maps': () => ({ user_namespace_maps: [] }), + 'PUT vpses/123': () => { + vpsUpdateCount += 1; + return { vps }; }, - }), - }, + 'PUT datasets/10': () => ({ _meta: { action_state_id: 905 } }), + 'GET action_states/905': () => ({ + action_state: { + id: 905, + label: 'Resize root dataset', + status: true, + finished: false, + current: 0, + total: 1, + }, + }), + }, + }); + + await page.goto('/admin/vps/123/config'); + const resizeEntry = page.getByTestId('vps.config.ssd.resize'); + await expect(resizeEntry).toBeVisible(); + await expect(resizeEntry).toHaveAttribute('href', '/admin/vps/123/storage?resize=ssd'); + await expect(resizeEntry.locator('..')).toContainText('20 GiB'); + + await resizeEntry.click(); + await expect(page).toHaveURL(/\/admin\/vps\/123\/storage$/); + const modal = page.getByTestId('vps.storage.resize.modal'); + await expect(modal).toBeVisible(); + await expect(modal).toContainText('No VPS restart'); + await expect(modal).toContainText('Current size: 20 GiB'); + await expect(modal.getByTestId('vps.storage.resize.submit')).toBeDisabled(); + + await modal.getByTestId('vps.storage.resize.size').fill('4'); + await expect(modal).toContainText('cannot be smaller than the currently used space'); + await expect(modal.getByTestId('vps.storage.resize.submit')).toBeDisabled(); + + await modal.getByTestId('vps.storage.resize.size').fill('32'); + await expect(modal.getByTestId('vps.storage.resize.admin_override')).not.toBeChecked(); + if (override) await modal.getByTestId('vps.storage.resize.admin_override').check(); + const requestPromise = page.waitForRequest( + (request) => request.method() === 'PUT' && request.url().includes('/api/v7.0/datasets/10') + ); + await modal.getByTestId('vps.storage.resize.submit').click(); + + expect((await requestPromise).postDataJSON()).toEqual({ dataset: { refquota: 32 * 1024, ...(override ? { admin_override: true } : {}) } }); + expect(vpsUpdateCount).toBe(0); + await expect(modal).toBeHidden(); + await expect(page.getByTestId('vps.storage.root_dataset.resize')).toBeVisible(); + + await page.goto('/app/vps/123/config'); + await expect(page.getByTestId('vps.config.ssd.resize')).toHaveCount(0); + await page.goto('/app/vps/123/storage'); + await expect(page.getByTestId('vps.storage.root_dataset.resize')).toHaveCount(0); }); - await page.goto('/admin/vps/123/config'); - const resizeEntry = page.getByTestId('vps.config.ssd.resize'); - await expect(resizeEntry).toBeVisible(); - await expect(resizeEntry).toHaveAttribute('href', '/admin/vps/123/storage?resize=ssd'); - await expect(resizeEntry.locator('..')).toContainText('20 GiB'); - - await resizeEntry.click(); - await expect(page).toHaveURL(/\/admin\/vps\/123\/storage$/); - const modal = page.getByTestId('vps.storage.resize.modal'); - await expect(modal).toBeVisible(); - await expect(modal).toContainText('No VPS restart'); - await expect(modal).toContainText('Current size: 20 GiB'); - await expect(modal.getByTestId('vps.storage.resize.submit')).toBeDisabled(); - - await modal.getByTestId('vps.storage.resize.size').fill('4'); - await expect(modal).toContainText('cannot be smaller than the currently used space'); - await expect(modal.getByTestId('vps.storage.resize.submit')).toBeDisabled(); - - await modal.getByTestId('vps.storage.resize.size').fill('32'); - const requestPromise = page.waitForRequest( - (request) => request.method() === 'PUT' && request.url().includes('/api/v7.0/datasets/10') - ); - await modal.getByTestId('vps.storage.resize.submit').click(); - - expect((await requestPromise).postDataJSON()).toEqual({ dataset: { refquota: 32 * 1024 } }); - expect(vpsUpdateCount).toBe(0); - await expect(modal).toBeHidden(); - await expect(page.getByTestId('vps.storage.root_dataset.resize')).toBeVisible(); - - await page.goto('/app/vps/123/config'); - await expect(page.getByTestId('vps.config.ssd.resize')).toHaveCount(0); - await page.goto('/app/vps/123/storage'); - await expect(page.getByTestId('vps.storage.root_dataset.resize')).toHaveCount(0); - }); + } test('resets a mount deletion confirm when the VPS route changes', async ({ page }) => { await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' }); diff --git a/e2e/specs/app/vps_support_permissions.spec.ts b/e2e/specs/app/vps_support_permissions.spec.ts index 52126891..fc7b7259 100644 --- a/e2e/specs/app/vps_support_permissions.spec.ts +++ b/e2e/specs/app/vps_support_permissions.spec.ts @@ -129,7 +129,7 @@ test('@pr-smoke support account stays read-only inside the admin VPS shell', asy await expect(page.getByText('Autostart priority', { exact: true })).toHaveCount(0); await expect(page.getByText('Change reason', { exact: true })).toHaveCount(0); await expect(page.getByText('Admin lock type', { exact: true })).toHaveCount(0); - await expect(page.getByText('Admin override', { exact: true })).toHaveCount(0); + await expect(page.getByTestId('vps.config.admin_override')).toHaveCount(0); await expect(page.getByText('You do not have permission to perform this action.')).toBeVisible(); await expect(page.locator('fieldset:disabled')).toBeVisible(); diff --git a/src/i18n/locales/cs/storage.ts b/src/i18n/locales/cs/storage.ts index bc3be733..4fbe49f5 100644 --- a/src/i18n/locales/cs/storage.ts +++ b/src/i18n/locales/cs/storage.ts @@ -170,7 +170,8 @@ export const csStorage = { "dataset.manage.field.atime": "Access time", "dataset.manage.field.relatime": "Relative access time", "dataset.manage.field.sharenfs": "NFS share", - "dataset.manage.field.admin_override": "Nastavit bez ohledu na volné prostředky", + "dataset.manage.field.admin_override": "Povolit překročení prostředků uživatele", + "dataset.manage.help.admin_override": "Povolí navýšení i při nedostatku přidělených prostředků uživatele. Ostatní kontroly zůstávají platné.", "dataset.manage.field.admin_lock_type": "Typ admin locku", "dataset.manage.sync.standard": "Standard", "dataset.manage.sync.always": "Vždy", diff --git a/src/i18n/locales/cs/vps/config.ts b/src/i18n/locales/cs/vps/config.ts index d1c79243..6e9751c7 100644 --- a/src/i18n/locales/cs/vps/config.ts +++ b/src/i18n/locales/cs/vps/config.ts @@ -49,7 +49,7 @@ export const csVps_config = { 'vps.config.field.allow_admin_modifications': 'Povolit admin úpravy', 'vps.config.field.change_reason': 'Důvod změny', 'vps.config.field.admin_lock_type': 'Typ admin locku', - 'vps.config.field.admin_override': 'Admin override', + 'vps.config.field.admin_override': 'Povolit překročení prostředků uživatele', 'vps.config.help.hostname_mode': 'Spravovaný režim zapisuje hostname přes vpsAdmin. Manuální režim vypne správu hostname vpsAdminem.', 'vps.config.help.hostname': 'Použije se pouze při zapnuté správě hostname.', 'vps.config.help.owner': 'Změna vlastníka se před odesláním potvrzuje.', @@ -60,7 +60,7 @@ export const csVps_config = { 'vps.config.help.ssd_unsaved': 'Nejdřív ulož nebo resetuj rozepsané změny CPU, paměti či swapu.', 'vps.config.help.change_reason': 'Volitelná admin poznámka posílaná se změnou prostředků.', 'vps.config.help.admin_lock_type': 'Volitelné vynucení locku použité se změnou prostředků.', - 'vps.config.help.admin_override': 'Pošle admin_override=true se změnou prostředků.', + 'vps.config.help.admin_override': 'Povolí navýšení i při nedostatku přidělených prostředků uživatele. Ostatní kontroly zůstávají platné.', 'vps.config.help.user_namespace_map': 'Změna namespace mapy se před odesláním potvrzuje.', 'vps.config.help.map_mode': 'Určuje, jak se UID/GID mapování použije na připojené datasety. Změna tohoto nastavení restartuje VPS.', 'vps.config.help.autostart_priority': 'Aktivní stav řídí backend. 0 je nejvyšší priorita; vyšší hodnoty startují později.', diff --git a/src/i18n/locales/cs/vps/storage.ts b/src/i18n/locales/cs/vps/storage.ts index 1a899e82..9a825a1f 100644 --- a/src/i18n/locales/cs/vps/storage.ts +++ b/src/i18n/locales/cs/vps/storage.ts @@ -82,8 +82,8 @@ export const csVps_storage = { 'vps.storage.resize.summary': 'Aktuální velikost: {current} · využito: {used}', 'vps.storage.resize.field.size': 'Nová velikost SSD (GiB)', 'vps.storage.resize.field.size_help': 'Zadej celkovou výslednou velikost, ne hodnotu, o kterou se má disk navýšit.', - 'vps.storage.resize.field.admin_override': 'Vynutit bez ohledu na dostupné prostředky', - 'vps.storage.resize.field.admin_override_help': 'Použij jen po ověření kapacity. API dostane parametr admin_override.', + 'vps.storage.resize.field.admin_override': 'Povolit překročení prostředků uživatele', + 'vps.storage.resize.field.admin_override_help': 'Povolí navýšení i při nedostatku přidělených prostředků uživatele. Ostatní kontroly zůstávají platné.', 'vps.storage.resize.error': 'Změnu velikosti SSD se nepodařilo spustit', 'vps.storage.resize.validation.required': 'Zadej novou velikost SSD.', 'vps.storage.resize.validation.invalid': 'Velikost musí být kladné číslo v GiB.', diff --git a/src/i18n/locales/en/storage.ts b/src/i18n/locales/en/storage.ts index 9f91340a..6cf42578 100644 --- a/src/i18n/locales/en/storage.ts +++ b/src/i18n/locales/en/storage.ts @@ -163,7 +163,8 @@ export const enStorage = { "dataset.manage.field.atime": "Access time", "dataset.manage.field.relatime": "Relative access time", "dataset.manage.field.sharenfs": "NFS share", - "dataset.manage.field.admin_override": "Set regardless of available resources", + "dataset.manage.field.admin_override": "Allow exceeding user resource allocation", + "dataset.manage.help.admin_override": "Allow increases even when the user has no allocated resources left. Other checks still apply.", "dataset.manage.field.admin_lock_type": "Admin lock type", "dataset.manage.sync.standard": "Standard", "dataset.manage.sync.always": "Always", diff --git a/src/i18n/locales/en/vps/config.ts b/src/i18n/locales/en/vps/config.ts index 566feb84..0c602ddb 100644 --- a/src/i18n/locales/en/vps/config.ts +++ b/src/i18n/locales/en/vps/config.ts @@ -49,7 +49,7 @@ export const enVps_config = { 'vps.config.field.allow_admin_modifications': 'Allow admin modifications', 'vps.config.field.change_reason': 'Change reason', 'vps.config.field.admin_lock_type': 'Admin lock type', - 'vps.config.field.admin_override': 'Admin override', + 'vps.config.field.admin_override': 'Allow exceeding user resource allocation', 'vps.config.help.hostname_mode': 'Managed mode writes the hostname through vpsAdmin. Manual mode disables vpsAdmin hostname management.', 'vps.config.help.hostname': 'Used only when hostname management is enabled.', 'vps.config.help.owner': 'Changing the owner is confirmed before it is sent.', @@ -60,7 +60,7 @@ export const enVps_config = { 'vps.config.help.ssd_unsaved': 'Save or reset pending CPU, memory or swap changes first.', 'vps.config.help.change_reason': 'Optional admin note sent with resource changes.', 'vps.config.help.admin_lock_type': 'Optional lock enforcement used with resource changes.', - 'vps.config.help.admin_override': 'Send admin_override=true with resource changes.', + 'vps.config.help.admin_override': 'Allow increases even when the user has no allocated resources left. Other checks still apply.', 'vps.config.help.user_namespace_map': 'Changing the namespace map is confirmed before it is sent.', 'vps.config.help.map_mode': 'Controls how UID/GID mappings are applied to mounted datasets. Changing this setting restarts the VPS.', 'vps.config.help.autostart_priority': 'The active state is managed by the backend. 0 is the highest priority; greater values start later.', diff --git a/src/i18n/locales/en/vps/storage.ts b/src/i18n/locales/en/vps/storage.ts index 41f1727d..9d309095 100644 --- a/src/i18n/locales/en/vps/storage.ts +++ b/src/i18n/locales/en/vps/storage.ts @@ -82,8 +82,8 @@ export const enVps_storage = { 'vps.storage.resize.summary': 'Current size: {current} · used: {used}', 'vps.storage.resize.field.size': 'New SSD size (GiB)', 'vps.storage.resize.field.size_help': 'Enter the resulting total size, not the amount to add.', - 'vps.storage.resize.field.admin_override': 'Override available-resource checks', - 'vps.storage.resize.field.admin_override_help': 'Use only after checking capacity. The API receives admin_override.', + 'vps.storage.resize.field.admin_override': 'Allow exceeding user resource allocation', + 'vps.storage.resize.field.admin_override_help': 'Allow increases even when the user has no allocated resources left. Other checks still apply.', 'vps.storage.resize.error': 'The SSD resize could not be started', 'vps.storage.resize.validation.required': 'Enter a new SSD size.', 'vps.storage.resize.validation.invalid': 'Size must be a positive number in GiB.', diff --git a/src/pages/app/datasets/DatasetOverviewPage.tsx b/src/pages/app/datasets/DatasetOverviewPage.tsx index 71acbca3..44a3cfaa 100644 --- a/src/pages/app/datasets/DatasetOverviewPage.tsx +++ b/src/pages/app/datasets/DatasetOverviewPage.tsx @@ -146,11 +146,14 @@ function buildEditablePayload( if (isAdmin) { if (form.sharenfs !== undefined) payload.sharenfs = form.sharenfs.trim(); - if (form.adminOverride !== undefined) payload.admin_override = form.adminOverride; - if (form.adminLockType !== undefined) payload.admin_lock_type = form.adminLockType; } } + if (isAdmin) { + if (form.adminOverride !== undefined) payload.admin_override = form.adminOverride; + if (form.adminLockType !== undefined) payload.admin_lock_type = form.adminLockType; + } + return payload; } @@ -211,13 +214,9 @@ function DatasetManagementCard() { ? ((dataset as any).sync as DatasetEditablePayload['sync']) : undefined, sharenfs: typeof (dataset as any).sharenfs === 'string' ? (dataset as any).sharenfs : undefined, - adminOverride: - typeof (dataset as any).admin_override === 'boolean' ? (dataset as any).admin_override : undefined, - adminLockType: ['no_lock', 'absolute', 'not_less', 'not_more'].includes( - String((dataset as any).admin_lock_type) - ) - ? ((dataset as any).admin_lock_type as DatasetEditForm['adminLockType']) - : undefined, + // Allocation exceptions are explicit per-save options, not dataset properties. + adminOverride: undefined, + adminLockType: undefined, })); const objectLabel = datasetLabel(dataset); @@ -285,6 +284,7 @@ function DatasetManagementCard() { onSuccess: (res) => { track(res.meta, 'action.dataset.update.label'); setEditAdvancedDirty(false); + setEdit((previous) => ({ ...previous, adminOverride: undefined, adminLockType: undefined })); }, onError: (e: any) => { if (e?.code === 'BUSY') chrome.openTasks(); @@ -456,36 +456,6 @@ function DatasetManagementCard() { testId="dataset.manage.sharenfs" /> - + { + setEdit((previous) => ({ ...previous, adminOverride: value })); + }} + label={t('dataset.manage.field.admin_override')} + description={t('dataset.manage.help.admin_override')} + disabled={updateM.isPending} + testId="dataset.manage.admin_override" + /> + + ) : null} + {advancedFields} ); diff --git a/src/pages/app/vps/VpsConfigurationPage.tsx b/src/pages/app/vps/VpsConfigurationPage.tsx index 79b8a7a1..0d828709 100644 --- a/src/pages/app/vps/VpsConfigurationPage.tsx +++ b/src/pages/app/vps/VpsConfigurationPage.tsx @@ -351,6 +351,31 @@ export function VpsConfigurationPage() { patchDraft({ swap: e.target.value })} disabled={saveM.isPending} /> + {canEditAdminConfig ? ( +
+ + patchDraft({ changeReason: e.target.value })} disabled={saveM.isPending} autoComplete="off" /> - -