Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EPMRPP-98929 || Allow registering UI extensions without plugins installed #4175

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions app/src/components/extensionLoader/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 EPAM Systems
* Copyright 2025 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,12 +20,11 @@ import { MAIN_FILE_KEY } from 'controllers/plugins/uiExtensions/constants';
const DEFAULT_EXTENSION_FILE_NAME = 'remoteEntity.js';

export const getExtensionUrl = (extension) => {
const isDev = process.env.NODE_ENV === 'development';
const { pluginName, url: defaultUrl, binaryData = {} } = extension;
const { pluginName, url: remoteUrl, binaryData = {} } = extension;
const fileName = binaryData[MAIN_FILE_KEY] || DEFAULT_EXTENSION_FILE_NAME;

if (isDev && defaultUrl) {
return `${defaultUrl}/${fileName}`;
if (remoteUrl) {
return `${remoteUrl}/${fileName}`;
}

return URLS.pluginPublicFile(pluginName, fileName);
Expand Down
12 changes: 10 additions & 2 deletions app/src/controllers/plugins/uiExtensions/actions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 EPAM Systems
* Copyright 2025 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,11 @@
* limitations under the License.
*/

import { FETCH_EXTENSION_MANIFESTS_SUCCESS, UPDATE_EXTENSION_MANIFEST } from './constants';
import {
FETCH_EXTENSION_MANIFESTS_SUCCESS,
UPDATE_EXTENSION_MANIFEST,
ADD_EXTENSION_MANIFEST,
} from './constants';

export const fetchExtensionManifestsSuccessAction = (extensionManifests) => ({
type: FETCH_EXTENSION_MANIFESTS_SUCCESS,
Expand All @@ -24,3 +28,7 @@ export const updateExtensionManifestAction = (extensionManifest) => ({
type: UPDATE_EXTENSION_MANIFEST,
payload: extensionManifest,
});
export const addExtensionManifestAction = (extensionManifest) => ({
type: ADD_EXTENSION_MANIFEST,
payload: extensionManifest,
});
6 changes: 5 additions & 1 deletion app/src/controllers/plugins/uiExtensions/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 EPAM Systems
* Copyright 2025 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -53,5 +53,9 @@ export const ICON_FILE_KEY = 'icon';
// redux actions
export const FETCH_EXTENSION_MANIFESTS_SUCCESS = 'fetchExtensionManifestsSuccess';
export const UPDATE_EXTENSION_MANIFEST = 'updateExtensionManifest';
export const ADD_EXTENSION_MANIFEST = 'addExtensionManifest';

// for remotely hosted plugins (e.g. Browser Kube)
export const PLUGIN_TYPE_REMOTE = 'remote';
// for locally hosted plugins (e.g. Test library)
export const PLUGIN_TYPE_CORE = 'core';
43 changes: 37 additions & 6 deletions app/src/controllers/plugins/uiExtensions/overrideExtension.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,51 @@
/*
* Copyright 2025 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { pluginByNameSelector } from 'controllers/plugins';
import { updateExtensionManifestAction } from './actions';
import { updateExtensionManifestAction, addExtensionManifestAction } from './actions';
import { MANIFEST_FILE_KEY } from './constants';

// TODO: restrict access to this function (f.e. only for admins)
const fetchManifest = async (url, manifestFileName) => {
const response = await fetch(`${url}/${manifestFileName}`, {
contentType: 'application/json',
});
return response.json();
};

// TODO: restrict access to this function (e.g. only for admins)
export const createExtensionOverrider = (store) => async (pluginName, url) => {
const plugin = pluginByNameSelector(store.getState(), pluginName);

const manifestFileName =
plugin.details?.binaryData?.[MANIFEST_FILE_KEY] || `${MANIFEST_FILE_KEY}.json`;

const response = await fetch(`${url}/${manifestFileName}`, {
contentType: 'application/json',
});
const manifest = await response.json();
const manifest = await fetchManifest(url, manifestFileName);

store.dispatch(updateExtensionManifestAction({ ...manifest, pluginName, url }));

return manifest;
};

// TODO: restrict access to this function (e.g. only for admins)
export const createExtensionAppender = (store) => async (url) => {
const manifestFileName = `${MANIFEST_FILE_KEY}.json`;

const manifest = await fetchManifest(url, manifestFileName);

store.dispatch(addExtensionManifestAction({ ...manifest, url }));

return manifest;
};
10 changes: 8 additions & 2 deletions app/src/controllers/plugins/uiExtensions/reducer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 EPAM Systems
* Copyright 2025 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,7 +15,11 @@
*/

import { combineReducers } from 'redux';
import { FETCH_EXTENSION_MANIFESTS_SUCCESS, UPDATE_EXTENSION_MANIFEST } from './constants';
import {
FETCH_EXTENSION_MANIFESTS_SUCCESS,
UPDATE_EXTENSION_MANIFEST,
ADD_EXTENSION_MANIFEST,
} from './constants';

const extensionManifestsReducer = (state = [], { type = '', payload = {} }) => {
switch (type) {
Expand All @@ -28,6 +32,8 @@ const extensionManifestsReducer = (state = [], { type = '', payload = {} }) => {
}
return item;
});
case ADD_EXTENSION_MANIFEST:
return [...state, payload];
default:
return state;
}
Expand Down
9 changes: 5 additions & 4 deletions app/src/controllers/plugins/uiExtensions/registerPlugin.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { createExtensionOverrider } from './overrideExtension';
import { createExtensionOverrider, createExtensionAppender } from './overrideExtension';

window.RP = {};

export const initPluginRegistration = (store) => {
// allows overriding plugin UI extensions in favor of separately hosted files (e.g. locally hosted plugin files)
const overrideExtension = createExtensionOverrider(store);
window.RP = {
overrideExtension,
// allows overriding plugin UI extensions in favor of separately hosted files (e.g. locally hosted plugin files)
overrideExtension: createExtensionOverrider(store),
// allows appending UI extensions (e.g. locally or somewhere hosted files)
appendExtension: createExtensionAppender(store),
};
};
6 changes: 3 additions & 3 deletions app/src/controllers/plugins/uiExtensions/sagas.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 EPAM Systems
* Copyright 2025 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -49,7 +49,7 @@ export function* fetchExtensionManifests(action) {

try {
const results = yield Promise.allSettled(calls);
const metadataArray = results.reduce((acc, result, index) => {
const manifestsArray = results.reduce((acc, result, index) => {
if (result.status !== 'fulfilled') {
return acc;
}
Expand All @@ -62,7 +62,7 @@ export function* fetchExtensionManifests(action) {
});
}, []);

yield put(fetchExtensionManifestsSuccessAction(metadataArray));
yield put(fetchExtensionManifestsSuccessAction(manifestsArray));
} catch (error) {
console.error('Plugin manifests load error'); // eslint-disable-line no-console
}
Expand Down
9 changes: 6 additions & 3 deletions app/src/controllers/plugins/uiExtensions/selectors.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 EPAM Systems
* Copyright 2025 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,6 +35,7 @@ import {
EXTENSION_TYPE_TEST_ITEM_DETAILS_ADDON,
EXTENSION_TYPE_PROJECT_PAGE,
PLUGIN_TYPE_REMOTE,
PLUGIN_TYPE_CORE,
} from './constants';
import {
domainSelector,
Expand All @@ -50,11 +51,13 @@ const createExtensionSelectorByType = (type, pluginNamesSelector = enabledPlugin
pluginNamesSelector,
extensionManifestsSelector,
(enabledPluginNames, extensionManifests) => {
// TODO: update 'pluginType' usage once the backend for remote plugins will be ready
// TODO: update 'pluginType' usage once the backend for remote and core plugins will be ready
const uiExtensions = extensionManifests
.filter(
({ pluginName, pluginType }) =>
enabledPluginNames.includes(pluginName) || pluginType === PLUGIN_TYPE_REMOTE,
enabledPluginNames.includes(pluginName) ||
pluginType === PLUGIN_TYPE_REMOTE ||
pluginType === PLUGIN_TYPE_CORE,
)
.reduce(
(acc, { extensions, ...commonManifestProperties }) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,16 @@ export const ProjectSidebar = ({ onClickNavBtn }) => {
icon: SettingsIcon,
message: formatMessage(messages.projectsSettings),
});
projectPageExtensions.forEach(({ icon, internalRoute }) => {
projectPageExtensions.forEach(({ icon, internalRoute, name, title }) => {
if (icon) {
sidebarItems.push({
onClick: onClickNavBtn,
link: {
type: PROJECT_PLUGIN_PAGE,
payload: { organizationSlug, projectSlug, pluginPage: internalRoute },
payload: { organizationSlug, projectSlug, pluginPage: internalRoute || name },
},
icon: icon.svg,
message: icon.title,
message: icon.title || title,
});
}
});
Expand Down
3 changes: 2 additions & 1 deletion app/src/routes/routesMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ const routesMap = {
},
},
[PLUGIN_UI_EXTENSION_ADMIN_PAGE]: '/plugin/:pluginPage/:pluginRoute*',
[PROJECT_PLUGIN_PAGE]: '/:projectId/plugin/:pluginPage/:pluginRoute*',
[PROJECT_PLUGIN_PAGE]:
'/organizations/:organizationSlug/projects/:projectSlug/plugin/:pluginPage/:pluginRoute*',
};

export const onBeforeRouteChange = (dispatch, getState, { action }) => {
Expand Down
10 changes: 6 additions & 4 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ http {
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "object-src 'none'; default-src 'self' data: *.uservoice.com; script-src 'self' 'sha256-3q+Q3HGgk9UiNUdwzAAIEnZ+yR0E/2GaklnqnIzhtwE=' status.reportportal.io www.google-analytics.com www.googletagmanager.com stats.g.doubleclick.net *.saucelabs.com *.epam.com *.uservoice.com *.rawgit.com https://*.clarity.ms https://c.bing.com; worker-src 'self' blob:; font-src 'self' data: fonts.googleapis.com fonts.gstatic.com *.rawgit.com; style-src-elem 'self' data: 'unsafe-inline' *.googleapis.com *.rawgit.com; style-src 'self' 'unsafe-inline' https://tagmanager.google.com; media-src 'self' *.saucelabs.com *.browserstack.com blob:; img-src * 'self' data: blob: http: https: www.google-analytics.com; connect-src 'self' *.google-analytics.com *.analytics.google.com https://stats.g.doubleclick.net https://*.clarity.ms https://c.bing.com; frame-src 'self' https://webto.salesforce.com";
# The CSP need to be updated before release (localhost should be removed in favor of own Service UI files)
# Update other `location`s accordingly
add_header Content-Security-Policy "object-src 'none'; default-src 'self' data: *.uservoice.com; script-src 'self' 'sha256-3q+Q3HGgk9UiNUdwzAAIEnZ+yR0E/2GaklnqnIzhtwE=' status.reportportal.io www.google-analytics.com www.googletagmanager.com stats.g.doubleclick.net *.saucelabs.com *.epam.com *.uservoice.com *.rawgit.com https://*.clarity.ms https://c.bing.com; worker-src 'self' blob:; font-src 'self' data: fonts.googleapis.com fonts.gstatic.com *.rawgit.com; style-src-elem 'self' data: 'unsafe-inline' *.googleapis.com *.rawgit.com; style-src 'self' 'unsafe-inline' https://tagmanager.google.com; media-src 'self' *.saucelabs.com *.browserstack.com blob:; img-src * 'self' data: blob: http: https: www.google-analytics.com; connect-src localhost:* 'self' *.google-analytics.com *.analytics.google.com https://stats.g.doubleclick.net https://*.clarity.ms https://c.bing.com; frame-src 'self' https://webto.salesforce.com";
try_files $uri /index.html;
}

Expand All @@ -46,20 +48,20 @@ http {
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "object-src 'none'; default-src 'self' data: *.uservoice.com; script-src 'self' 'sha256-3q+Q3HGgk9UiNUdwzAAIEnZ+yR0E/2GaklnqnIzhtwE=' status.reportportal.io www.google-analytics.com www.googletagmanager.com stats.g.doubleclick.net *.saucelabs.com *.epam.com *.uservoice.com *.rawgit.com https://*.clarity.ms https://c.bing.com; worker-src 'self' blob:; font-src 'self' data: fonts.googleapis.com fonts.gstatic.com *.rawgit.com; style-src-elem 'self' data: 'unsafe-inline' *.googleapis.com *.rawgit.com; style-src 'self' 'unsafe-inline' https://tagmanager.google.com; media-src 'self' *.saucelabs.com *.browserstack.com blob:; img-src * 'self' data: blob: http: https: www.google-analytics.com; connect-src 'self' *.google-analytics.com *.analytics.google.com https://stats.g.doubleclick.net https://*.clarity.ms https://c.bing.com; frame-src 'self' https://webto.salesforce.com";
add_header Content-Security-Policy "object-src 'none'; default-src 'self' data: *.uservoice.com; script-src 'self' 'sha256-3q+Q3HGgk9UiNUdwzAAIEnZ+yR0E/2GaklnqnIzhtwE=' status.reportportal.io www.google-analytics.com www.googletagmanager.com stats.g.doubleclick.net *.saucelabs.com *.epam.com *.uservoice.com *.rawgit.com https://*.clarity.ms https://c.bing.com; worker-src 'self' blob:; font-src 'self' data: fonts.googleapis.com fonts.gstatic.com *.rawgit.com; style-src-elem 'self' data: 'unsafe-inline' *.googleapis.com *.rawgit.com; style-src 'self' 'unsafe-inline' https://tagmanager.google.com; media-src 'self' *.saucelabs.com *.browserstack.com blob:; img-src * 'self' data: blob: http: https: www.google-analytics.com; connect-src localhost:* 'self' *.google-analytics.com *.analytics.google.com https://stats.g.doubleclick.net https://*.clarity.ms https://c.bing.com; frame-src 'self' https://webto.salesforce.com";
try_files $uri /index.html;
}

# build info
location /info {
add_header Cache-Control "public, must-revalidate";
add_header Content-Security-Policy "object-src 'none'; default-src 'self' data: *.uservoice.com; script-src 'self' 'sha256-3q+Q3HGgk9UiNUdwzAAIEnZ+yR0E/2GaklnqnIzhtwE=' status.reportportal.io www.google-analytics.com www.googletagmanager.com stats.g.doubleclick.net *.saucelabs.com *.epam.com *.uservoice.com *.rawgit.com https://*.clarity.ms https://c.bing.com; worker-src 'self' blob:; font-src 'self' data: fonts.googleapis.com fonts.gstatic.com *.rawgit.com; style-src-elem 'self' data: 'unsafe-inline' *.googleapis.com *.rawgit.com; style-src 'self' 'unsafe-inline' https://tagmanager.google.com; media-src 'self' *.saucelabs.com *.browserstack.com blob:; img-src * 'self' data: blob: http: https: www.google-analytics.com; connect-src 'self' *.google-analytics.com *.analytics.google.com https://stats.g.doubleclick.net https://*.clarity.ms https://c.bing.com; frame-src 'self' https://webto.salesforce.com";
add_header Content-Security-Policy "object-src 'none'; default-src 'self' data: *.uservoice.com; script-src 'self' 'sha256-3q+Q3HGgk9UiNUdwzAAIEnZ+yR0E/2GaklnqnIzhtwE=' status.reportportal.io www.google-analytics.com www.googletagmanager.com stats.g.doubleclick.net *.saucelabs.com *.epam.com *.uservoice.com *.rawgit.com https://*.clarity.ms https://c.bing.com; worker-src 'self' blob:; font-src 'self' data: fonts.googleapis.com fonts.gstatic.com *.rawgit.com; style-src-elem 'self' data: 'unsafe-inline' *.googleapis.com *.rawgit.com; style-src 'self' 'unsafe-inline' https://tagmanager.google.com; media-src 'self' *.saucelabs.com *.browserstack.com blob:; img-src * 'self' data: blob: http: https: www.google-analytics.com; connect-src localhost:* 'self' *.google-analytics.com *.analytics.google.com https://stats.g.doubleclick.net https://*.clarity.ms https://c.bing.com; frame-src 'self' https://webto.salesforce.com";
try_files $uri /buildInfo.json 404;
}

location /ui/info {
add_header Cache-Control "public, must-revalidate";
add_header Content-Security-Policy "object-src 'none'; default-src 'self' data: *.uservoice.com; script-src 'self' 'sha256-3q+Q3HGgk9UiNUdwzAAIEnZ+yR0E/2GaklnqnIzhtwE=' status.reportportal.io www.google-analytics.com www.googletagmanager.com stats.g.doubleclick.net *.saucelabs.com *.epam.com *.uservoice.com *.rawgit.com https://*.clarity.ms https://c.bing.com; worker-src 'self' blob:; font-src 'self' data: fonts.googleapis.com fonts.gstatic.com *.rawgit.com; style-src-elem 'self' data: 'unsafe-inline' *.googleapis.com *.rawgit.com; style-src 'self' 'unsafe-inline' https://tagmanager.google.com; media-src 'self' *.saucelabs.com *.browserstack.com blob:; img-src * 'self' data: blob: http: https: www.google-analytics.com; connect-src 'self' *.google-analytics.com *.analytics.google.com https://stats.g.doubleclick.net https://*.clarity.ms https://c.bing.com; frame-src 'self' https://webto.salesforce.com";
add_header Content-Security-Policy "object-src 'none'; default-src 'self' data: *.uservoice.com; script-src 'self' 'sha256-3q+Q3HGgk9UiNUdwzAAIEnZ+yR0E/2GaklnqnIzhtwE=' status.reportportal.io www.google-analytics.com www.googletagmanager.com stats.g.doubleclick.net *.saucelabs.com *.epam.com *.uservoice.com *.rawgit.com https://*.clarity.ms https://c.bing.com; worker-src 'self' blob:; font-src 'self' data: fonts.googleapis.com fonts.gstatic.com *.rawgit.com; style-src-elem 'self' data: 'unsafe-inline' *.googleapis.com *.rawgit.com; style-src 'self' 'unsafe-inline' https://tagmanager.google.com; media-src 'self' *.saucelabs.com *.browserstack.com blob:; img-src * 'self' data: blob: http: https: www.google-analytics.com; connect-src localhost:* 'self' *.google-analytics.com *.analytics.google.com https://stats.g.doubleclick.net https://*.clarity.ms https://c.bing.com; frame-src 'self' https://webto.salesforce.com";
try_files $uri /buildInfo.json 404;
}

Expand Down
Loading