generated from openedx/frontend-template-application
-
Notifications
You must be signed in to change notification settings - Fork 12
/
README.rst
443 lines (336 loc) · 15.6 KB
/
README.rst
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
Frontend Plugin Framework
##########################
|license-badge| |status-badge| |ci-badge| |codecov-badge|
.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-plugin-framework.svg
:target: https://github.com/openedx/frontend-plugin-framework/blob/master/LICENSE
:alt: License
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
.. |ci-badge| image:: https://github.com/openedx/frontend-plugin-framework/actions/workflows/ci.yml/badge.svg
:target: https://github.com/openedx/frontend-plugin-framework/actions/workflows/ci.yml
:alt: Continuous Integration
.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-plugin-framework/coverage.svg?branch=master
:target: https://codecov.io/github/openedx/frontend-plugin-framework?branch=master
:alt: Codecov
Purpose
=======
The Frontend Plugin Framework is designed to be an extension point to customize an Open edX MFE. This framework supports two types of plugins: iFrame-based and "Direct" plugins.
A Direct plugin allows for a component in the Host MFE -- or a React dependency -- to be made into a plugin and inserted in a plugin slot within the Host MFE.
The iFrame-based Plugin allows for a component that lives in another MFE (the Child MFE) to be plugged into a slot in
the Host MFE.
The primary way this is made possible is through JS-based configurations, where the changes to a plugin slot are defined
(see 'Plugin Operations').
Getting Started
===============
Using the Example Apps
----------------------
1. Run ``make requirements`` in the root directory.
2. Run ``npm run start`` in the root directory.
3. Open another terminal and run ``npm run start:example`` to start the example app. You can visit http://localhost:8080 to see the example app.
4. Make change to the existing code, everything should be hot reloaded.
Add Library Dependency
----------------------
Add ``openedx/frontend-plugin-framework`` to the ``package.json`` of both Host and Child MFEs.
Host Micro-frontend (MFE)
-------------------------
Host MFEs define ``PluginSlot`` components in areas of the UI where they intend to accept plugin extensions.
The Host MFE, and thus the maintainers of the Host MFE, are responsible for deciding where it is acceptable to add a
plugin slot.
The slot also determines the dimensions and responsiveness of each plugin, and supports passing any additional
data to the plugin as part of its contract.
.. code-block::
<HostApp>
<Route path="/page1">
<SomeHostContent />
<PluginSlot
id="sidebar" // this `id` is referenced in the JS-based config
pluginProps={{ // these props are passed along to each plugin
className: 'flex-grow-1',
title: 'example plugins',
}}
style={{
height: 700,
}}
>
<SideBar
propExampleA: 'edX Sidebar',
propExampleB: SomeIcon,
>
</PluginSlot >
</Route>
<Route path="/page2">
<OtherRouteContent />
</Route>
</HostApp>
Host MFE JS-based Configuration
-------------------------------
Micro-frontends that would like to use the Plugin Framework need to use a JavaScript-based config named ``env.config``
with either ``.js`` or ``.jsx`` as the extension. Technically, only the Host MFE requires an ``env.config.js`` file
as that is where the plugin slot's configuration is defined.
However, note that any Child MFE can theoretically contain one or more ``PluginSlot`` components themselves,
thereby making it both a Child MFE and a Host MFE. In this instance, the Child MFE would need its own ``env.config.js``
file as well to define its plugin slots.
.. code-block::
// env.config.js
import { DIRECT_PLUGIN, IFRAME_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
// import any additional dependencies or functions to be used for each plugin operation
import Sidebar from './widgets/social/Sidebar';
import SocialMediaLink from './widgets/social/SocialMediaLink';
import { wrapSidebar, modifySidebar } from './widgets/social/utils';
import { SomeIcon } from '@openedx/paragon/icons';
const config = {
// additional environment variables
pluginSlots: {
sidebar: { // plugin slot id
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'social_media_link',
type: DIRECT_PLUGIN,
priority: 10,
RenderWidget: SocialMediaLink,
},
},
{
op: PLUGIN_OPERATIONS.Wrap,
widgetId: 'default_contents',
wrapper: wrapWidget,
},
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'social_media_link',
fn: modifyWidget,
},
]
}
}
}
export default config;
For more information on how JS based configuration works, see:
* `config.js`_ file in Frontend Platform
* Frontend Build ADR on `JavaScript-based environment configuration`_
* Frontend Platform ADR to `Promote JavaScript file configuration and deprecate environment variable configuration`_
.. _config.js: https://github.com/openedx/frontend-platform/blob/master/src/config.js
.. _JavaScript-based environment configuration: https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
.. _Promote JavaScript file configuration and deprecate environment variable configuration: https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
Priority
````````
The priority property determines where the widgets should be placed based on a 1-100 scale. A widget with a priority of 10
will appear above a widget with a priority of 20.
Default Content
```````````````
The component that is wrapped by a Plugin Slot is referred to as the "default content". In order to render this content,
the ``keepDefault`` boolean in the slot should be set to ``true``. For organizations who aren't using the Plugin Slot
(and therefore aren't defining a slot via JS config), ``keepDefault`` will default to ``true``, thus ensuring that the developer
experience isn't affected; the only change to note is that the component is now wrapped in a Plugin Slot.
If you need to use a plugin operation (e.g. Wrap, Hide, Modify) on default content, the ``widgetId`` you would use to refer to the content is ``defaults_contents``.
Note: The default content will have a priority of 50, allowing for any plugins to appear before or after the default content.
Plugin Operations
=================
There are four plugin operations that each require specific properties.
Insert a Direct Plugin
``````````````````````
The Insert operation will add a widget in the plugin slot. The contents required for a Direct Plugin is the same as
is demonstrated in the Default Contents section above, with the ``content`` key being optional.
.. code-block::
/*
* {String} op - Name of plugin operation
* {Object} widget - The component to be inserted into the slot
*/
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'social_media_link',
type: DIRECT_PLUGIN,
priority: 10,
RenderWidget: SocialMediaLink,
}
}
Insert an iFrame Plugin
```````````````````````
The Insert operation will add a widget in the plugin slot. The contents required for an iFrame Plugin is the same as
is demonstrated in the Default Contents section above.
.. code-block::
/*
* {String} op - Name of plugin operation
* {Object} widget - The component to be inserted into the slot
*/
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'enterprise_navbar',
type: IFRAME_PLUGIN,
priority: 30,
url: 'http://{child_mfe_url}/plugin_iframe',
title: 'Login with XYZ',
}
}
Modify
``````
The Modify operation allows us to modify the contents of a widget, including its id, type, content, RenderWidget function,
or its priority. The operation requires the id of the widget that will be modified and a function to make those changes.
.. code-block::
const modifyWidget = (widget) => {
widget.content = {
propExampleA: 'University XYZ Sidebar',
propExampleB: SomeOtherIcon,
};
return widget;
};
/*
* {String} op - Name of plugin operation
* {String} widgetId - The widget id needed for referencing when using Modify/Wrap/Hide
* {Function} fn - The function to call that can modify the widget's contents and properties
*/
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'sidebar_plugin',
fn: modifyWidget,
}
Wrap
````
Unlike Modify, the Wrap operation adds a React component around the widget, and a single widget can receive more than
one wrap operation. Each wrapper function takes in a ``component`` and ``id`` prop.
.. code-block::
const wrapWidget = ({ component, idx }) => (
<div className="bg-warning" data-testid={`wrapper${idx + 1}`} key={idx}>
<p>This is a wrapper component that is placed around the widget.</p>
{component}
<p>With this wrapper, you can add anything before or after the widget.</p>
</div>
);
/*
* {String} op - Name of plugin operation
* {String} widgetId - The widget id needed for referencing when using Modify/Wrap/Hide
* {Function} wrapper - The function to call that can wrap the widget with a React component
*/
{
op: PLUGIN_OPERATIONS.Wrap,
widgetId: 'default_content_in_slot',
wrapper: wrapWidget,
}
Hide
````
The Hide operation will simply hide whatever content is desired. This is generally used for the default content.
.. code-block::
/*
* {String} op - Name of plugin operation
* {String} widgetId - The widget id needed for referencing when using Modify/Wrap/Hide
*/
{
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'some_undesired_plugin',
}
Using a Child Micro-frontend (MFE) for iFrame-based Plugins
-----------------------------------------------------------
The Child MFE is no different than any other MFE except that it can define a `Plugin` component that can then be pass into the Host MFE
as an iFrame-based plugin via a route.
This component communicates (via ``postMessage``) with the Host MFE and resizes its content to match the dimensions
available in the Host's plugin slot.
Fallback Behavior
-----------------
Setting a Fallback component
''''''''''''''''''''''''''''
The two main places to configure a fallback component for a given implementation are in the PluginSlot props and in the JS configuration. The JS configuration fallback will be prioritized over the PluginSlot props fallback.
PluginSlot props
````````````````
This is ideally used when the same fallback should be applied to all of the plugins in the `PluginSlot`. To configure, set the `slotErrorFallbackComponent` prop in the `PluginSlot` to a React component. This will replace the default `<ErrorPage />` component from frontend-platform.
.. code-block::
<PluginSlot
id='my-plugin-slot'
slotErrorFallbackComponent={<MyCustomFallbackComponent />}
/>
JS configuration
````````````````
Can be used when setting a fallback for a specific plugin within a slot. Set the `errorFallbackComponent` field for the specific plugin to the custom fallback component in the JS configuration. This will be prioritized over any other fallback components.
.. code-block::
const config = {
pluginSlots: {
my_plugin_slot: {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'this_is_a_plugin',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: ReactPluginComponent,
errorFallbackComponent: MyCustomFallbackComponent,
},
},
],
},
},
};
iFrame-based Plugins
''''''''''''''''''''
It's notoriously difficult to know in the Host MFE when an iFrame has failed to load.
Because of security sandboxing, the host isn't allowed to know the HTTP status of the request or to inspect what was
loaded, so we have to rely on waiting for a ``postMessage`` event from within the iFrame to know it has successfully loaded.
A fallback component can be provided to the Plugin that is wrapped around the component, as noted below.
Otherwise, the `default Error fallback from Frontend Platform`_ would be used.
.. code-block::
<MyMFE>
<Route path="/mainContent">
<MyMainContent />
</Route>
<Route path="/plugin1">
<Plugin fallbackComponent={<OtherFallback />}>
<MyCustomContent />
</Plugin>
</Route>
</MyMFE>
.. _default Error fallback from Frontend Platform: https://github.com/openedx/frontend-platform/blob/master/src/react/ErrorBoundary.jsx
Known Issues
============
Development Roadmap
===================
The main priority in developing this library is to extract components from a Host MFE to:
#. allow for teams to develop experimental features without impeding on any other team's work or the core functionality of the Host MFE.
#. allow for customizing/extending the functionality of a Host MFE without having org-specific functionality in an open-source project.
Getting Help
============
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a
frontend repository, the best place to discuss it would be in the `#wg-frontend
channel`_.
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-plugin-framework/issues
For more information about these options, see the `Getting Help`_ page.
.. _Slack invitation: https://openedx.org/slack
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/getting-help
License
=======
The code in this repository is licensed under the AGPLv3 unless otherwise
noted.
Please see `LICENSE <LICENSE>`_ for details.
Contributing
============
Contributions are very welcome. Please read `How To Contribute`_ for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.
The Open edX Code of Conduct
============================
All community members are expected to follow the `Open edX Code of Conduct`_.
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
People
======
The assigned maintainers for this component and other project details may be
found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo.
.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-plugin-framework
Reporting Security Issues
=========================
Please do not report security issues in public. Email security@openedx.org instead.