Skip to content

Commit 53dea4a

Browse files
committed
EPMRPP-90318 || Remote plugins support
1 parent 0dfd0c3 commit 53dea4a

File tree

24 files changed

+419
-157
lines changed

24 files changed

+419
-157
lines changed

app/src/components/extensionLoader/extensionLoader.jsx

+27-20
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,36 @@
1+
/*
2+
* Copyright 2024 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
import React from 'react';
218
import PropTypes from 'prop-types';
3-
import { BubblesLoader } from '@reportportal/ui-kit';
419
import { ErrorBoundary } from 'components/containers/errorBoundary';
5-
import { createImportProps } from 'controllers/plugins/uiExtensions/createImportProps';
620
import { ExtensionError } from './extensionError';
721
import { extensionType } from './extensionTypes';
8-
import { useFederatedComponent } from './hooks';
9-
import { getExtensionUrl } from './utils';
22+
import { FederatedExtensionLoader } from './federatedExtensionLoader';
23+
import { StandaloneExtensionLoader } from './standaloneExtensionLoader';
1024

1125
function ExtensionLoader({ extension, withPreloader, ...componentProps }) {
12-
const { moduleName, scope, pluginName } = extension;
13-
const url = getExtensionUrl(extension);
14-
15-
const { failed, Component } = useFederatedComponent(scope, moduleName, url);
16-
17-
if (failed) {
18-
return <h2>Failed to load extension: {moduleName}</h2>;
19-
}
20-
21-
// TODO: remove legacy extensions when all existing plugins will be migrated to the new engine
22-
const extensionImportProps = createImportProps(pluginName);
23-
24-
return (
25-
<React.Suspense fallback={withPreloader ? <BubblesLoader /> : null}>
26-
{Component ? <Component {...extensionImportProps} {...componentProps} /> : null}
27-
</React.Suspense>
26+
return extension.pluginType === 'remote' ? (
27+
<StandaloneExtensionLoader extension={extension} />
28+
) : (
29+
<FederatedExtensionLoader
30+
extension={extension}
31+
withPreloader={withPreloader}
32+
{...componentProps}
33+
/>
2834
);
2935
}
3036
ExtensionLoader.propTypes = {
@@ -44,6 +50,7 @@ export function ExtensionLoaderWrapper({
4450
}) {
4551
return (
4652
<ErrorBoundary getFallback={silentOnError ? undefined : () => <ExtensionError />}>
53+
{/* TODO: remove legacy extensions when all existing plugins will be migrated to the new engine */}
4754
{extension.component ? (
4855
<extension.component {...componentProps} />
4956
) : (

app/src/components/extensionLoader/extensionTypes.js

+37-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
1+
/*
2+
* Copyright 2024 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
import PropTypes from 'prop-types';
18+
import { PLUGIN_TYPE_REMOTE } from 'controllers/plugins/uiExtensions/constants';
219

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

1229
/* New plugins mechanism related code below */
1330

14-
const newExtensionType = PropTypes.shape({
31+
const embeddedExtensionType = PropTypes.shape({
1532
name: PropTypes.string.isRequired,
1633
title: PropTypes.string,
34+
// TODO: describe this field more specifically
1735
type: PropTypes.string.isRequired,
1836
moduleName: PropTypes.string,
1937
scope: PropTypes.string,
2038
pluginName: PropTypes.string.isRequired,
2139
});
2240

23-
export const extensionType = PropTypes.oneOfType([oldExtensionType, newExtensionType]);
41+
const standaloneExtensionType = PropTypes.shape({
42+
pluginName: PropTypes.string.isRequired,
43+
pluginType: PropTypes.oneOf([PLUGIN_TYPE_REMOTE]),
44+
// TODO: describe this field more specifically
45+
type: PropTypes.string.isRequired,
46+
url: PropTypes.string.isRequired,
47+
internalRoute: PropTypes.string,
48+
icon: PropTypes.shape({
49+
url: PropTypes.string,
50+
svg: PropTypes.string,
51+
}),
52+
});
53+
54+
export const extensionType = PropTypes.oneOfType([
55+
oldExtensionType,
56+
embeddedExtensionType,
57+
standaloneExtensionType,
58+
]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2024 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import PropTypes from 'prop-types';
19+
import { BubblesLoader } from '@reportportal/ui-kit';
20+
import { createImportProps } from 'controllers/plugins/uiExtensions/createImportProps';
21+
import { getExtensionUrl } from '../utils';
22+
import { useFederatedComponent } from '../hooks';
23+
import { extensionType } from '../extensionTypes';
24+
25+
export function FederatedExtensionLoader({ extension, withPreloader, ...componentProps }) {
26+
const { moduleName, scope, pluginName } = extension;
27+
const url = getExtensionUrl(extension);
28+
29+
const { failed, Component } = useFederatedComponent(scope, moduleName, url);
30+
31+
// TODO: replace with proper failed state
32+
if (failed) {
33+
return <h2>Failed to load extension: {moduleName}</h2>;
34+
}
35+
36+
// TODO: Provide extensionImportProps via React Context
37+
const extensionImportProps = createImportProps(pluginName);
38+
39+
return (
40+
<React.Suspense fallback={withPreloader ? <BubblesLoader /> : null}>
41+
{Component ? <Component {...extensionImportProps} {...componentProps} /> : null}
42+
</React.Suspense>
43+
);
44+
}
45+
FederatedExtensionLoader.propTypes = {
46+
extension: extensionType,
47+
withPreloader: PropTypes.bool,
48+
};
49+
FederatedExtensionLoader.defaultProps = {
50+
extension: {},
51+
withPreloader: false,
52+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2024 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export { FederatedExtensionLoader } from './federatedExtensionLoader';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2024 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export { StandaloneExtensionLoader } from './standaloneExtensionLoader';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2024 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React, { useRef, useEffect, useState } from 'react';
18+
import PropTypes from 'prop-types';
19+
import { connect } from 'react-redux';
20+
import { userInfoSelector } from 'controllers/user';
21+
import { projectInfoSelector } from 'controllers/project';
22+
import { extensionType } from '../extensionTypes';
23+
24+
// http://localhost:3000/#superadmin_personal/plugin/BrowserKube
25+
// TODO: add loader while loading the iframe
26+
// TODO: configure sandbox for iframe
27+
function StandaloneExtensionLoader({ extension, userInfo, projectInfo }) {
28+
const [loaded, setLoaded] = useState(false);
29+
const ref = useRef();
30+
31+
const onLoad = () => {
32+
setLoaded(true);
33+
};
34+
35+
const sendRpContext = () => {
36+
const consumerOrigin = new URL(extension.url).origin;
37+
const data = {
38+
user: userInfo,
39+
project: projectInfo,
40+
};
41+
ref?.current?.contentWindow.postMessage(data, consumerOrigin);
42+
};
43+
44+
useEffect(() => {
45+
if (loaded) {
46+
sendRpContext();
47+
}
48+
}, [loaded, userInfo, projectInfo]);
49+
50+
return (
51+
<iframe
52+
ref={ref}
53+
name={extension.pluginName}
54+
title={extension.pluginName}
55+
src={extension.url}
56+
style={{ width: '100%', height: '100%' }}
57+
onLoad={onLoad}
58+
seamless
59+
/>
60+
);
61+
}
62+
StandaloneExtensionLoader.propTypes = {
63+
extension: extensionType,
64+
userInfo: PropTypes.object.isRequired,
65+
projectInfo: PropTypes.object.isRequired,
66+
};
67+
StandaloneExtensionLoader.defaultProps = {
68+
extension: {},
69+
};
70+
71+
const withConnect = connect((state) => ({
72+
userInfo: userInfoSelector(state),
73+
projectInfo: projectInfoSelector(state),
74+
}));
75+
76+
const ConnectedStandaloneExtensionLoader = withConnect(StandaloneExtensionLoader);
77+
78+
export { ConnectedStandaloneExtensionLoader as StandaloneExtensionLoader };

app/src/controllers/pages/constants.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 EPAM Systems
2+
* Copyright 2024 EPAM Systems
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -60,6 +60,9 @@ export const REGISTRATION_PAGE = 'REGISTRATION_PAGE';
6060
export const HOME_PAGE = 'HOME_PAGE';
6161
export const ACCOUNT_REMOVED_PAGE = 'ACCOUNT_REMOVED_PAGE';
6262

63+
// extensions
64+
export const PROJECT_PLUGIN_PAGE = 'PROJECT_PLUGIN_PAGE';
65+
6366
export const pageNames = {
6467
[NOT_FOUND]: NOT_FOUND,
6568
ADMINISTRATE_PAGE,
@@ -95,6 +98,7 @@ export const pageNames = {
9598
PROJECT_USERDEBUG_LOG_PAGE,
9699
OAUTH_SUCCESS,
97100
PLUGIN_UI_EXTENSION_ADMIN_PAGE,
101+
PROJECT_PLUGIN_PAGE,
98102
};
99103

100104
export const adminPageNames = {

app/src/controllers/pages/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 EPAM Systems
2+
* Copyright 2024 EPAM Systems
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -83,6 +83,7 @@ export {
8383
HOME_PAGE,
8484
CLEAR_PAGE_STATE,
8585
PLUGIN_UI_EXTENSION_ADMIN_PAGE,
86+
PROJECT_PLUGIN_PAGE,
8687
} from './constants';
8788
export { NOT_FOUND } from 'redux-first-router';
8889
export { pageSagas } from './sagas';

app/src/controllers/plugins/index.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 EPAM Systems
2+
* Copyright 2024 EPAM Systems
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -60,7 +60,6 @@ export { pluginSagas } from './sagas';
6060
export {
6161
uiExtensionSettingsTabsSelector,
6262
uiExtensionAdminPagesSelector,
63-
uiExtensionPagesSelector,
6463
extensionsLoadedSelector,
6564
uiExtensionSidebarComponentsSelector,
6665
uiExtensionLaunchItemComponentsSelector,

app/src/controllers/plugins/uiExtensions/constants.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export const EXTENSION_TYPE_SETTINGS_TAB = 'uiExtension:settingsTab';
2-
export const EXTENSION_TYPE_PAGE = 'uiExtension:page';
32
export const EXTENSION_TYPE_ADMIN_SIDEBAR_COMPONENT = 'uiExtension:adminSidebarComponent';
43
export const EXTENSION_TYPE_ADMIN_PAGE = 'uiExtension:adminPage';
54
export const EXTENSION_TYPE_MODAL = 'uiExtension:modal';
@@ -21,6 +20,7 @@ export const EXTENSION_TYPE_MAKE_DECISION_DEFECT_TYPE_ADDON =
2120
'uiExtension:makeDecisionDefectTypeAddon';
2221
export const EXTENSION_TYPE_LOG_STACKTRACE_ADDON = 'uiExtension:logStacktraceAddon';
2322
export const EXTENSION_TYPE_TEST_ITEM_DETAILS_ADDON = 'uiExtension:testItemDetailsAddon';
23+
export const EXTENSION_TYPE_PROJECT_PAGE = 'uiExtension:projectPage';
2424

2525
export const COMMAND_GET_FILE = 'getFile';
2626
export const COMMAND_GET_ISSUE_TYPES = 'getIssueTypes';
@@ -39,3 +39,5 @@ export const MAIN_FILE_KEY = 'main';
3939

4040
export const FETCH_EXTENSIONS_METADATA_SUCCESS = 'fetchExtensionsMetadataSuccess';
4141
export const UPDATE_EXTENSION_METADATA = 'updateExtensionMetadata';
42+
43+
export const PLUGIN_TYPE_REMOTE = 'remote';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from 'react';
2+
import { useSelector } from 'react-redux';
3+
import { pluginPageSelector } from 'controllers/pages';
4+
5+
export const useActivePluginPageExtension = (extensionsSelector) => {
6+
const extensions = useSelector(extensionsSelector);
7+
const activePluginPage = useSelector(pluginPageSelector);
8+
9+
const extension = React.useMemo(
10+
() => extensions.find((ex) => ex.internalRoute === activePluginPage),
11+
[extensions, activePluginPage],
12+
);
13+
14+
return extension;
15+
};

app/src/controllers/plugins/uiExtensions/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ export { fetchUiExtensions, fetchExtensionsMetadata } from './sagas';
22
export {
33
uiExtensionSettingsTabsSelector,
44
uiExtensionAdminPagesSelector,
5-
uiExtensionPagesSelector,
65
extensionsLoadedSelector,
76
uiExtensionSidebarComponentsSelector,
87
uiExtensionAdminSidebarComponentsSelector,
@@ -19,5 +18,6 @@ export {
1918
makeDecisionDefectTypeAddonSelector,
2019
logStackTraceAddonSelector,
2120
testItemDetailsAddonSelector,
21+
uiExtensionProjectPagesSelector,
2222
} from './selectors';
2323
export { uiExtensionsReducer } from './reducer';

0 commit comments

Comments
 (0)