From baf0a91973ae04cd4a0aab2f2437bff6920c0eaa Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Wed, 21 Oct 2026 16:11:42 +0200 Subject: [PATCH] console: Fix schedule any time delay hints --- CHANGELOG.md | 1 + cypress/e2e/console/gateways/edit.spec.js | 78 +++++++++++++++++++ .../components/unit-input/common/duration.js | 2 +- pkg/webui/components/unit-input/unit-input.js | 4 + pkg/webui/console/constants/delays.js | 4 +- .../lorawan-settings-form/index.js | 66 +++------------- .../validation-schema.js | 8 +- pkg/webui/lib/shared-messages.js | 4 +- pkg/webui/locales/en.json | 3 +- pkg/webui/locales/ja.json | 3 +- 10 files changed, 105 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beefbc9534..f56facb86a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ For details about compatibility between different releases, see the **Commitment - Parsing of the `rctx` field in the `upinfo` object of upstream messages received via the LoRa Basics Station LNS protocol. The field name was misspelled as `rtcx`, so the radio context reported by gateways was ignored. The antenna index in the uplink metadata now reflects the reported radio context and is echoed back in class A downlinks, instead of always being 0. - The antenna gain sent to LoRa Basics Station gateways in the router configuration. Each board's radio configuration now uses the gain of its own antenna instead of applying the first antenna's gain to all boards, and fractional gains are no longer truncated. - Prefilling the end device model, hardware version, firmware version and band in the Console when scanning an end device QR code that contains a LoRa Alliance vendor ID and vendor profile ID that are known in the Device Repository. Previously, only the brand was prefilled. +- Misleading schedule any time delay hints in the gateway LoRaWAN settings in the Console. The Console showed a "delay too short" warning for gateways without a delay, even though the Gateway Server then uses the default delay. The Console now shows the minimum (132.5ms) in a single description and shows the default delay (532.5ms) as placeholder when no delay is set. ## [3.36.2] - 2026-08-17 diff --git a/cypress/e2e/console/gateways/edit.spec.js b/cypress/e2e/console/gateways/edit.spec.js index 77022e35eb..adbea02df7 100644 --- a/cypress/e2e/console/gateways/edit.spec.js +++ b/cypress/e2e/console/gateways/edit.spec.js @@ -19,6 +19,8 @@ describe('Gateway general settings', () => { let gateway let gateway2 let gateway3 + let gateway4 + let gateway5 const collabUserId = 'test-collab-user' const collabUser = { ids: { user_id: collabUserId }, @@ -69,9 +71,26 @@ describe('Gateway general settings', () => { gateway_server_address: 'localhost', frequency_plan_ids: ['EU_863_870'], } + gateway4 = { + ids: { gateway_id: 'test-gateway-default-delay', eui: '0000000000000003' }, + name: 'Test Gateway Default Delay', + description: 'Gateway for testing the default schedule any time delay', + gateway_server_address: 'localhost', + frequency_plan_ids: ['EU_863_870'], + } + gateway5 = { + ids: { gateway_id: 'test-gateway-reset-delay', eui: '0000000000000004' }, + name: 'Test Gateway Reset Delay', + description: 'Gateway for testing resetting the schedule any time delay', + schedule_anytime_delay: '523ms', + gateway_server_address: 'localhost', + frequency_plan_ids: ['EU_863_870'], + } cy.createGateway(gateway, user.ids.user_id) cy.createGateway(gateway2, user.ids.user_id) cy.createGateway(gateway3, user.ids.user_id) + cy.createGateway(gateway4, user.ids.user_id) + cy.createGateway(gateway5, user.ids.user_id) }) it('displays newly created gateway values', () => { @@ -152,6 +171,65 @@ describe('Gateway general settings', () => { .and('have.attr', 'value', '0.523') }) + it('displays the default schedule any time delay when no delay is set', () => { + cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) + cy.visit( + `${Cypress.config('consoleRootPath')}/gateways/${gateway4.ids.gateway_id}/general-settings`, + ) + + cy.findByText('LoRaWAN options', { selector: 'h3' }) + .closest('[data-test-id="collapsible-section"]') + .within(() => { + cy.findByRole('button', { name: 'Expand' }).click() + cy.findByLabelText('Schedule any time delay') + .should('have.value', '') + .and('have.attr', 'placeholder', '532.5ms') + cy.findByText( + 'Configure gateway delay. If the delay is too short, the minimum of 132.5ms will be used.', + ).should('be.visible') + }) + }) + + it('succeeds resetting the schedule any time delay to the default', () => { + cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) + cy.visit( + `${Cypress.config('consoleRootPath')}/gateways/${gateway5.ids.gateway_id}/general-settings`, + ) + cy.intercept('PUT', `/api/v3/gateways/${gateway5.ids.gateway_id}`).as('update-gateway') + + cy.findByText('LoRaWAN options', { selector: 'h3' }) + .closest('[data-test-id="collapsible-section"]') + .within(() => { + cy.findByRole('button', { name: 'Expand' }).click() + cy.findByLabelText('Schedule any time delay').should('have.value', '0.523') + cy.findByLabelText('Schedule any time delay').clear() + cy.findByRole('button', { name: 'Save changes' }).click() + }) + + cy.wait('@update-gateway') + .its('request.body') + .should(body => { + expect(body.gateway.schedule_anytime_delay).to.equal('0s') + expect(body.field_mask.paths).to.include('schedule_anytime_delay') + }) + cy.findByTestId('error-notification').should('not.exist') + cy.findByTestId('toast-notification-success').findByText('Gateway updated').should('be.visible') + cy.findByLabelText('Schedule any time delay') + .should('have.value', '') + .and('have.attr', 'placeholder', '532.5ms') + + cy.reload() + + cy.findByText('LoRaWAN options', { selector: 'h3' }) + .closest('[data-test-id="collapsible-section"]') + .within(() => { + cy.findByRole('button', { name: 'Expand' }).click() + cy.findByLabelText('Schedule any time delay') + .should('have.value', '') + .and('have.attr', 'placeholder', '532.5ms') + }) + }) + it('succeeds changing gateway information', () => { cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) cy.visit( diff --git a/pkg/webui/components/unit-input/common/duration.js b/pkg/webui/components/unit-input/common/duration.js index 926a236aaf..5f22b58b7d 100644 --- a/pkg/webui/components/unit-input/common/duration.js +++ b/pkg/webui/components/unit-input/common/duration.js @@ -39,7 +39,7 @@ const decoder = (rawValue = '') => { return { value: '', unit: null } } const value = rawValue.split(unitRegexp)[0] - const unit = rawValue.split(value)[1] || null + const unit = rawValue.slice(value.length) || null return { value: value ? Number(value) : undefined, unit, diff --git a/pkg/webui/components/unit-input/unit-input.js b/pkg/webui/components/unit-input/unit-input.js index 95563237e1..82b866580b 100644 --- a/pkg/webui/components/unit-input/unit-input.js +++ b/pkg/webui/components/unit-input/unit-input.js @@ -38,6 +38,7 @@ const UnitInput = props => { inputWidth, onInputChange, onUnitChange, + placeholder, } = props const maskedUnits = unitSelector ? units.filter(u => unitSelector.includes(u.value)) : units return ( @@ -57,6 +58,7 @@ const UnitInput = props => { disabled={disabled} error={error} inputWidth={inputWidth} + placeholder={placeholder} />