Skip to content

The "Layout Editor" button for Dashboard Pages (when clicked) redirects to the wrong URL if altered RED.settings.httpNodeRoot #1927

@f0lg0

Description

@f0lg0

Current Behavior

The onclick event when opening the layout editor for a given UI page in nodes/config/ui_base.html

async function showDashboardWysiwygEditor (pageId, baseId) {
    // call to httpadmin endpoint requesting layout editor mode for this group
    const windowUrl = new URL(window.location.href)
    const base = RED.nodes.node(baseId)
    const dashboardBasePath = (base.path || 'dashboard').replace(/\/$/, '').replace(/^\/+/, '')
    const authTokens = RED.settings.get('auth-tokens') || {}
    const headers = {}
    if (authTokens?.access_token) {
        headers.Authorization = `${authTokens.token_type || 'Bearer'} ${authTokens.access_token}`
    }

    // promisify the ajax call so we can await it & return false after opening the popup
    const ajax = () => {
        return new Promise((resolve, reject) => {
            $.ajax({
                url: `${dashboardBasePath}/api/v1/${baseId}/edit/${pageId}`, // e.g. /dashboard/api/v1/123/edit/456
                type: 'PATCH',
                headers,
                data: {
                    mode: 'edit',
                    dashboard: baseId,
                    page: pageId
                },
                success: function (data) {
                    // construct the url to open the layout editor
                    const _url = new URL(`${dashboardBasePath}/${data.path}`.replace(/\/\//g, '/'), windowUrl.origin)
                    _url.searchParams.set('edit-key', data.editKey)
                    const editorPath = windowUrl.pathname?.replace(/\/$/, '').replace(/^\/+/, '') || ''
                    if (editorPath) {
                        _url.searchParams.set('editor-path', editorPath)
                    }
                    resolve(_url)
                },
                error: function (err) {
                    reject(err)
                }
            })
        })
    }
    try {
        const url = await ajax()
        const target = `ff-dashboard-${baseId}` // try to reuse the same window per base
        const newWindow = window.open(url, target)
        // if we have an access_token then send it to the new window once opened
        // This becomes necessary if the user has gotten logged out since opening the editor.
        // Also, this squares up any token mismatch due to iframes/Same-Origin Policy etc.
        if (authTokens?.access_token && newWindow) {
            // Wait 1s then send the local access_token
            // (it is unlikely user will have done any editing and be wanting to deploy within 1s!)
            setTimeout(() => {
                newWindow.postMessage({
                    type: 'authorization',
                    access_token: authTokens.access_token,
                    token_type: authTokens.token_type,
                    expires_in: authTokens.expires_in
                }, url.origin)
            }, 1000)
        }
    } catch (err) {
        console.error('layout mode error', err)
        RED.notify(RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.error.layoutEditor'), 'error')
    }
}

Crafts an URL without considering the RED.settings.httpNodeRoot that can be altered by the user when setting up Nodered

// construct the url to open the layout editor
const _url = new URL(`${dashboardBasePath}/${data.path}`.replace(/\/\//g, '/'), windowUrl.origin)
_url.searchParams.set('edit-key', data.editKey)
const editorPath = windowUrl.pathname?.replace(/\/$/, '').replace(/^\/+/, '') || ''
if (editorPath) {
    _url.searchParams.set('editor-path', editorPath)
}
resolve(_url)

resulting in a wrong redirect causing a 404.

In other words, if Nodered starts at http://localhost:8000/custompath/ instead of simply http://localhost:8000/, clicking on the Layout Editor button results in a redirect to http://localhost:8000/dashboard/page1?edit... causing a 404 instead of http://localhost:8000/custompath/dashboard/page1?edit....

This behavior can be seen by simply deploying any UI node such as a Button.

Expected Behavior

I expect the Layout Editor button to redirect me to the layout editor page listening under the path that considers the RED.settings.httpNodeRoot prefix.

For example, if Nodered starts at http://localhost:8000/custompath/ instead of simply http://localhost:8000/, clicking on the Layout Editor button should redirect me http://localhost:8000/custompath/dashboard/page1?edit... instead of http://localhost:8000/dashboard/page1?edit....

This could be fixed by considering the RED.settings.httpNodeRoot prefix here:

const _url = new URL(`${RED.settings.httpNodeRoot}/${dashboardBasePath}/${data.path}`.replace(/\/\//g, '/'), windowUrl.origin)

instead of

const _url = new URL(`${dashboardBasePath}/${data.path}`.replace(/\/\//g, '/'), windowUrl.origin)

Steps To Reproduce

Alter Nodered settings and set a custom prefix on RED.settings.httpNodeRoot, then deploy any UI node such as a Button and proceed to click on the "Layout editor" button on the Dashboard 2.0 sidebar under the given page, usually labeled as Page 1 if no other configs have been changed. You should encounter a 404 error.

For referece instead, the "Open Dashboard" button works correctly.

Environment

  • Dashboard version: 1.29.0
  • Node-RED version: 4.0.9
  • Node.js version: 22.16.0
  • npm version: 10.9.2
  • Platform/OS: Ubuntu 24.04.3
  • Browser: Firefox

Have you provided an initial effort estimate for this issue?

I am not a FlowFuse team member

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions