diff --git a/test/helpers/dashboard-url.helper.spec.js b/test/helpers/dashboard-url.helper.spec.js new file mode 100644 index 000000000..686e71d8e --- /dev/null +++ b/test/helpers/dashboard-url.helper.spec.js @@ -0,0 +1,46 @@ +const should = require('should') // eslint-disable-line no-unused-vars + +describe('dashboard API URL helper', function () { + let getDashboardApiUrl + + before(async function () { + const module = await import('../../ui/src/api/dashboard-url.mjs') + getDashboardApiUrl = module.getDashboardApiUrl + }) + + it('builds the default dashboard API URL', function () { + const url = getDashboardApiUrl( + 'http://localhost:1880/dashboard/page?edit-key=key', + '', + '/dashboard', + 'dashboard-id', + 'flows' + ) + + url.href.should.equal('http://localhost:1880/dashboard/api/v1/dashboard-id/flows') + }) + + it('does not duplicate a shared admin and node root', function () { + const url = getDashboardApiUrl( + 'http://localhost:1880/node-red/dashboard/page?edit-key=key', + '/node-red/', + '/dashboard/', + 'dashboard-id', + 'flows' + ) + + url.href.should.equal('http://localhost:1880/node-red/dashboard/api/v1/dashboard-id/flows') + }) + + it('uses the admin root when the node root is different', function () { + const url = getDashboardApiUrl( + 'http://localhost:1880/node-red/dashboard/page?edit-key=key', + '/admin', + '/dashboard', + 'dashboard-id', + 'flows' + ) + + url.href.should.equal('http://localhost:1880/admin/dashboard/api/v1/dashboard-id/flows') + }) +}) diff --git a/ui/src/api/dashboard-url.mjs b/ui/src/api/dashboard-url.mjs new file mode 100644 index 000000000..81db8436d --- /dev/null +++ b/ui/src/api/dashboard-url.mjs @@ -0,0 +1,22 @@ +function trimPathPart (part) { + return String(part || '').replace(/^\/+|\/+$/g, '') +} + +function getDashboardApiUrl (locationHref, editorPath, dashboardPath, dashboardId, ...path) { + const result = new URL(locationHref) + const pathParts = [ + editorPath, + dashboardPath, + 'api', + 'v1', + dashboardId, + ...path + ].map(trimPathPart).filter(Boolean) + + result.pathname = pathParts.join('/') + result.search = '' + result.hash = '' + return result +} + +export { getDashboardApiUrl } diff --git a/ui/src/api/node-red.js b/ui/src/api/node-red.js index 9cacfccce..d9ef93130 100644 --- a/ui/src/api/node-red.js +++ b/ui/src/api/node-red.js @@ -1,5 +1,7 @@ import axios from 'axios' +import { getDashboardApiUrl } from './dashboard-url.mjs' + function applyAuth (headers, editorPath) { let itemName = 'auth-tokens' if (editorPath) { @@ -18,46 +20,23 @@ function applyAuth (headers, editorPath) { return headers } -/** - * Generates a full dashboard api url based off the current location and the dashboard id - * The path parts are appended to the url - * @example - * // returns a URL object for 'http://localhost:1880/dashboard/api/v1/my-dashboard/flows' - * const url = getDashboardApiUrl('', 'my-dashboard', 'flows') - * @param {String} editorPath - The node-red editor path as provided by httpAdminRoot (e.g. 'editor') (can be empty) - * @param {String} dashboardId - The dashboard id - * @param {...any} path - 1 or more path parts to append to the url - * @returns {String} The full dashboard api url - */ -function getDashboardApiUrl (editorPath, dashboardId, ...path) { - const url = new URL(window.location.href) - const urlBase = url.pathname.split('/').slice(0, -1).join('/') + '/api/v1/' // e.g 'http://localhost:1880/dashboard/api/v1/' - const pathParts = [urlBase, dashboardId, ...(path || [])].map(p => p.replace(/^\/|\/$/g, '')) - if (editorPath) { - pathParts.unshift(editorPath.replace(/\/$/, '').replace(/^\/+/, '')) - } - const result = new URL(url) - result.search = '' - result.pathname = pathParts.join('/') - return result -} - export default { /** * Deploy changes to the Node-RED instance via the dashboards httpAdmin endpoint * @param {Object} options - The options to deploy changes via the http admin endpoint * @param {string} options.dashboard - The dashboard id + * @param {string} options.dashboardPath - The dashboard base path * @param {string} options.page - The page id * @param {string} options.key - The edit key for verification * @param {Array} options.groups - The updated group objects to apply * @param {Array} options.widgets - The updated widget objects to apply * @returns the axios request */ - deployChanges: async function deployChangesViaHttpAdminEndpoint ({ dashboard, page, groups, widgets, key, editorPath }) { + deployChanges: async function deployChangesViaHttpAdminEndpoint ({ dashboard, dashboardPath, page, groups, widgets, key, editorPath }) { const changes = { groups, widgets } return axios.request({ method: 'PATCH', - url: getDashboardApiUrl(editorPath || '', dashboard, 'flows'), + url: getDashboardApiUrl(window.location.href, editorPath || '', dashboardPath, dashboard, 'flows'), headers: applyAuth({ 'Content-type': 'application/json' }, editorPath), data: { dashboard, page, key, changes } }) diff --git a/ui/src/layouts/wysiwyg/index.js b/ui/src/layouts/wysiwyg/index.js index 1791791d9..9dc570769 100644 --- a/ui/src/layouts/wysiwyg/index.js +++ b/ui/src/layouts/wysiwyg/index.js @@ -139,6 +139,7 @@ export default { return NodeRedApi.deployChanges({ dashboard, + dashboardPath: this.$store.getters['ui/dashboards']?.[dashboard]?.path || 'dashboard', page, groups: currentPageGroups, widgets: normalisedWidgets,