Skip to content

Commit

Permalink
EPMRPP-90318 || Remote plugins support
Browse files Browse the repository at this point in the history
  • Loading branch information
AmsterGet committed May 16, 2024
1 parent 0dfd0c3 commit 53dea4a
Show file tree
Hide file tree
Showing 24 changed files with 419 additions and 157 deletions.
47 changes: 27 additions & 20 deletions app/src/components/extensionLoader/extensionLoader.jsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
/*
* Copyright 2024 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 React from 'react';
import PropTypes from 'prop-types';
import { BubblesLoader } from '@reportportal/ui-kit';
import { ErrorBoundary } from 'components/containers/errorBoundary';
import { createImportProps } from 'controllers/plugins/uiExtensions/createImportProps';
import { ExtensionError } from './extensionError';
import { extensionType } from './extensionTypes';
import { useFederatedComponent } from './hooks';
import { getExtensionUrl } from './utils';
import { FederatedExtensionLoader } from './federatedExtensionLoader';
import { StandaloneExtensionLoader } from './standaloneExtensionLoader';

function ExtensionLoader({ extension, withPreloader, ...componentProps }) {
const { moduleName, scope, pluginName } = extension;
const url = getExtensionUrl(extension);

const { failed, Component } = useFederatedComponent(scope, moduleName, url);

if (failed) {
return <h2>Failed to load extension: {moduleName}</h2>;
}

// TODO: remove legacy extensions when all existing plugins will be migrated to the new engine
const extensionImportProps = createImportProps(pluginName);

return (
<React.Suspense fallback={withPreloader ? <BubblesLoader /> : null}>
{Component ? <Component {...extensionImportProps} {...componentProps} /> : null}
</React.Suspense>
return extension.pluginType === 'remote' ? (
<StandaloneExtensionLoader extension={extension} />
) : (
<FederatedExtensionLoader
extension={extension}
withPreloader={withPreloader}
{...componentProps}
/>
);
}
ExtensionLoader.propTypes = {
Expand All @@ -44,6 +50,7 @@ export function ExtensionLoaderWrapper({
}) {
return (
<ErrorBoundary getFallback={silentOnError ? undefined : () => <ExtensionError />}>
{/* TODO: remove legacy extensions when all existing plugins will be migrated to the new engine */}
{extension.component ? (
<extension.component {...componentProps} />
) : (
Expand Down
39 changes: 37 additions & 2 deletions app/src/components/extensionLoader/extensionTypes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
/*
* Copyright 2024 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 PropTypes from 'prop-types';
import { PLUGIN_TYPE_REMOTE } from 'controllers/plugins/uiExtensions/constants';

/* TODO: remove legacy extensions when all existing plugins will be migrated to the new engine
and within next major version release */
Expand All @@ -11,13 +28,31 @@ const oldExtensionType = PropTypes.shape({

/* New plugins mechanism related code below */

const newExtensionType = PropTypes.shape({
const embeddedExtensionType = PropTypes.shape({
name: PropTypes.string.isRequired,
title: PropTypes.string,
// TODO: describe this field more specifically
type: PropTypes.string.isRequired,
moduleName: PropTypes.string,
scope: PropTypes.string,
pluginName: PropTypes.string.isRequired,
});

export const extensionType = PropTypes.oneOfType([oldExtensionType, newExtensionType]);
const standaloneExtensionType = PropTypes.shape({
pluginName: PropTypes.string.isRequired,
pluginType: PropTypes.oneOf([PLUGIN_TYPE_REMOTE]),
// TODO: describe this field more specifically
type: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
internalRoute: PropTypes.string,
icon: PropTypes.shape({
url: PropTypes.string,
svg: PropTypes.string,
}),
});

export const extensionType = PropTypes.oneOfType([
oldExtensionType,
embeddedExtensionType,
standaloneExtensionType,
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2024 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 React from 'react';
import PropTypes from 'prop-types';
import { BubblesLoader } from '@reportportal/ui-kit';
import { createImportProps } from 'controllers/plugins/uiExtensions/createImportProps';
import { getExtensionUrl } from '../utils';
import { useFederatedComponent } from '../hooks';
import { extensionType } from '../extensionTypes';

export function FederatedExtensionLoader({ extension, withPreloader, ...componentProps }) {
const { moduleName, scope, pluginName } = extension;
const url = getExtensionUrl(extension);

const { failed, Component } = useFederatedComponent(scope, moduleName, url);

// TODO: replace with proper failed state
if (failed) {
return <h2>Failed to load extension: {moduleName}</h2>;
}

// TODO: Provide extensionImportProps via React Context
const extensionImportProps = createImportProps(pluginName);

return (
<React.Suspense fallback={withPreloader ? <BubblesLoader /> : null}>
{Component ? <Component {...extensionImportProps} {...componentProps} /> : null}
</React.Suspense>
);
}
FederatedExtensionLoader.propTypes = {
extension: extensionType,
withPreloader: PropTypes.bool,
};
FederatedExtensionLoader.defaultProps = {
extension: {},
withPreloader: false,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2024 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.
*/

export { FederatedExtensionLoader } from './federatedExtensionLoader';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2024 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.
*/

export { StandaloneExtensionLoader } from './standaloneExtensionLoader';
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2024 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 React, { useRef, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { userInfoSelector } from 'controllers/user';
import { projectInfoSelector } from 'controllers/project';
import { extensionType } from '../extensionTypes';

// http://localhost:3000/#superadmin_personal/plugin/BrowserKube
// TODO: add loader while loading the iframe
// TODO: configure sandbox for iframe
function StandaloneExtensionLoader({ extension, userInfo, projectInfo }) {
const [loaded, setLoaded] = useState(false);
const ref = useRef();

const onLoad = () => {
setLoaded(true);
};

const sendRpContext = () => {
const consumerOrigin = new URL(extension.url).origin;
const data = {
user: userInfo,
project: projectInfo,
};
ref?.current?.contentWindow.postMessage(data, consumerOrigin);
};

useEffect(() => {
if (loaded) {
sendRpContext();
}
}, [loaded, userInfo, projectInfo]);

Check warning on line 48 in app/src/components/extensionLoader/standaloneExtensionLoader/standaloneExtensionLoader.jsx

View workflow job for this annotation

GitHub Actions / build (20)

React Hook useEffect has a missing dependency: 'sendRpContext'. Either include it or remove the dependency array

Check warning on line 48 in app/src/components/extensionLoader/standaloneExtensionLoader/standaloneExtensionLoader.jsx

View workflow job for this annotation

GitHub Actions / build (20)

React Hook useEffect has a missing dependency: 'sendRpContext'. Either include it or remove the dependency array

return (
<iframe
ref={ref}
name={extension.pluginName}
title={extension.pluginName}
src={extension.url}
style={{ width: '100%', height: '100%' }}
onLoad={onLoad}
seamless
/>
);
}
StandaloneExtensionLoader.propTypes = {
extension: extensionType,
userInfo: PropTypes.object.isRequired,
projectInfo: PropTypes.object.isRequired,
};
StandaloneExtensionLoader.defaultProps = {
extension: {},
};

const withConnect = connect((state) => ({
userInfo: userInfoSelector(state),
projectInfo: projectInfoSelector(state),
}));

const ConnectedStandaloneExtensionLoader = withConnect(StandaloneExtensionLoader);

export { ConnectedStandaloneExtensionLoader as StandaloneExtensionLoader };
6 changes: 5 additions & 1 deletion app/src/controllers/pages/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 EPAM Systems
* Copyright 2024 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 @@ -60,6 +60,9 @@ export const REGISTRATION_PAGE = 'REGISTRATION_PAGE';
export const HOME_PAGE = 'HOME_PAGE';
export const ACCOUNT_REMOVED_PAGE = 'ACCOUNT_REMOVED_PAGE';

// extensions
export const PROJECT_PLUGIN_PAGE = 'PROJECT_PLUGIN_PAGE';

export const pageNames = {
[NOT_FOUND]: NOT_FOUND,
ADMINISTRATE_PAGE,
Expand Down Expand Up @@ -95,6 +98,7 @@ export const pageNames = {
PROJECT_USERDEBUG_LOG_PAGE,
OAUTH_SUCCESS,
PLUGIN_UI_EXTENSION_ADMIN_PAGE,
PROJECT_PLUGIN_PAGE,
};

export const adminPageNames = {
Expand Down
3 changes: 2 additions & 1 deletion app/src/controllers/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 EPAM Systems
* Copyright 2024 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 @@ -83,6 +83,7 @@ export {
HOME_PAGE,
CLEAR_PAGE_STATE,
PLUGIN_UI_EXTENSION_ADMIN_PAGE,
PROJECT_PLUGIN_PAGE,
} from './constants';
export { NOT_FOUND } from 'redux-first-router';
export { pageSagas } from './sagas';
3 changes: 1 addition & 2 deletions app/src/controllers/plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 EPAM Systems
* Copyright 2024 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 @@ -60,7 +60,6 @@ export { pluginSagas } from './sagas';
export {
uiExtensionSettingsTabsSelector,
uiExtensionAdminPagesSelector,
uiExtensionPagesSelector,
extensionsLoadedSelector,
uiExtensionSidebarComponentsSelector,
uiExtensionLaunchItemComponentsSelector,
Expand Down
4 changes: 3 additions & 1 deletion app/src/controllers/plugins/uiExtensions/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export const EXTENSION_TYPE_SETTINGS_TAB = 'uiExtension:settingsTab';
export const EXTENSION_TYPE_PAGE = 'uiExtension:page';
export const EXTENSION_TYPE_ADMIN_SIDEBAR_COMPONENT = 'uiExtension:adminSidebarComponent';
export const EXTENSION_TYPE_ADMIN_PAGE = 'uiExtension:adminPage';
export const EXTENSION_TYPE_MODAL = 'uiExtension:modal';
Expand All @@ -21,6 +20,7 @@ export const EXTENSION_TYPE_MAKE_DECISION_DEFECT_TYPE_ADDON =
'uiExtension:makeDecisionDefectTypeAddon';
export const EXTENSION_TYPE_LOG_STACKTRACE_ADDON = 'uiExtension:logStacktraceAddon';
export const EXTENSION_TYPE_TEST_ITEM_DETAILS_ADDON = 'uiExtension:testItemDetailsAddon';
export const EXTENSION_TYPE_PROJECT_PAGE = 'uiExtension:projectPage';

export const COMMAND_GET_FILE = 'getFile';
export const COMMAND_GET_ISSUE_TYPES = 'getIssueTypes';
Expand All @@ -39,3 +39,5 @@ export const MAIN_FILE_KEY = 'main';

export const FETCH_EXTENSIONS_METADATA_SUCCESS = 'fetchExtensionsMetadataSuccess';
export const UPDATE_EXTENSION_METADATA = 'updateExtensionMetadata';

export const PLUGIN_TYPE_REMOTE = 'remote';
15 changes: 15 additions & 0 deletions app/src/controllers/plugins/uiExtensions/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { pluginPageSelector } from 'controllers/pages';

export const useActivePluginPageExtension = (extensionsSelector) => {
const extensions = useSelector(extensionsSelector);
const activePluginPage = useSelector(pluginPageSelector);

const extension = React.useMemo(
() => extensions.find((ex) => ex.internalRoute === activePluginPage),
[extensions, activePluginPage],
);

return extension;
};
2 changes: 1 addition & 1 deletion app/src/controllers/plugins/uiExtensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export { fetchUiExtensions, fetchExtensionsMetadata } from './sagas';
export {
uiExtensionSettingsTabsSelector,
uiExtensionAdminPagesSelector,
uiExtensionPagesSelector,
extensionsLoadedSelector,
uiExtensionSidebarComponentsSelector,
uiExtensionAdminSidebarComponentsSelector,
Expand All @@ -19,5 +18,6 @@ export {
makeDecisionDefectTypeAddonSelector,
logStackTraceAddonSelector,
testItemDetailsAddonSelector,
uiExtensionProjectPagesSelector,
} from './selectors';
export { uiExtensionsReducer } from './reducer';
Loading

0 comments on commit 53dea4a

Please sign in to comment.