Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: overhangio/tutor-mfe
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 14a46cf844fb7d094a3facf286994e8be87937b2
Choose a base ref
..
head repository: overhangio/tutor-mfe
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 6d749fd1d15565ff2617d7a06dc4936292155a27
Choose a head ref
Showing with 139 additions and 129 deletions.
  1. +105 −90 README.rst
  2. +6 −14 tutormfe/plugin.py
  3. +28 −25 tutormfe/templates/mfe/build/mfe/env.config.jsx
195 changes: 105 additions & 90 deletions README.rst
Original file line number Diff line number Diff line change
@@ -317,101 +317,120 @@ It's possible to take advantage of this plugin's hooks to configure frontend plu
from tutormfe.hooks import PLUGIN_SLOTS
PLUGIN_SLOTS.add_item((
"all", "footer_slot",
"""
{
/* Hide the default footer. */
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'default_contents',
},
{
/* Insert a custom footer. */
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1>This is the footer.</h1>
),
},
},"""
))
PLUGIN_SLOTS.add_items([
# Hide the default footer
(
"all",
"footer_slot",
"""
{
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'default_contents',
}"""
),
# Insert a custom footer
(
"all",
"footer_slot",
"""
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1>This is the footer.</h1>
),
},
}"""
)
])
Let's take a closer look at what's happening here. To begin with, we're using tutormfe's own ``PLUGIN_SLOTS`` filter. It's a regular Tutor filter, but you won't find it in the main ``tutor`` package:

.. code-block:: python
from tutormfe.hooks import PLUGIN_SLOTS
PLUGIN_SLOTS.add_item((
On the following line, the first parameter specifies which MFE to apply the slot configuration to; for example: ``"learner-dashboard"``, or ``"learning"``. We're using ``"all"`` here, which is a special case: it means the slot configuration should be applied to all MFEs that actually have that slot. (If a particular MFE doesn't have the slot, it will just ignore its configuration.)
The second parameter is the name of the slot:
.. code-block:: python
"all", "footer_slot",
Next up, we're adding actual slot configuration, starting by hiding the default footer. The first parameter in a filter item specifies which MFE to apply the slot configuration to; for example: ``"learner-dashboard"``, or ``"learning"``. We're using ``"all"`` here, which is a special case: it means the slot configuration should be applied to all MFEs that actually have that slot. (If a particular MFE doesn't have the slot, it will just ignore its configuration.)

The last parameter to ``add_item()`` is a big string with the actual slot configuration, which will be interpreted as JSX. Let's look at the first stanza:
The second parameter, ``"footer_slot"``, is the name of the slot as defined in the code of the MFE itself.

.. code-block:: python
"""
{
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'default_contents',
},
"""
PLUGIN_SLOTS.add_items([
# Hide the default footer
(
"all",
"footer_slot",
"""
{
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'default_contents',
}"""
),
What we're doing here is hiding the default contents of the footer with a ``PLUGIN_OPERATIONS.Hide``. (You can refer to the `frontend-plugin-framework README <https://github.com/openedx/frontend-plugin-framework/#>`_ for a full description of the possible plugin types and operations.) And the ``default_contents`` widget we're targetting always refers to what's in an unconfigured slot by default.
The last parameter to ``add_item()`` is a big string with the actual slot configuration, which will be interpreted as JSX. What we're doing there is hiding the default contents of the footer with a ``PLUGIN_OPERATIONS.Hide``. (You can refer to the `frontend-plugin-framework README <https://github.com/openedx/frontend-plugin-framework/#>`_ for a full description of the possible plugin types and operations.) And the ``default_contents`` widget ID we're targetting always refers to what's in an unconfigured slot by default.
In the last stanza, we use ``PLUGIN_OPERATIONS.Insert`` to add our custom JSX component, comprised of a simple ``<h1>`` message we're defining in the configuration itself. We give it a widgetID of ``custom_footer``:
In the second filter item, we once again target the ``"footer_slot"`` on ``"all"`` MFEs. This time, we use ``PLUGIN_OPERATIONS.Insert`` to add our custom JSX component, comprised of a simple ``<h1>`` message we're defining in an anonymous function. We give it a widgetID of ``custom_footer``:
.. code-block:: python
"""
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1>This is the footer.</h1>
),
},
"""
# Insert a custom footer
(
"all",
"footer_slot",
"""
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1>This is the footer.</h1>
),
},
}"""
)
That's it! If you rebuild the ``mfe`` image after enabling the plugin (via ``tutor images build mfe`` or ``tutor local launch``), "This is the footer." should appear at the bottom of every MFE.
It's also possible to target a specific MFE's footer. For instance:
.. code-block:: python
PLUGIN_SLOTS.add_item((
"profile", "footer_slot",
"""
{
/* Hide the global footer we defined earlier. */
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'custom_footer',
},
{
/* Insert a custom footer specific to the Profile MFE. */
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_profile_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1>This is the Profile MFE's footer.</h1>
),
},
},"""
))
PLUGIN_SLOTS.add_items([
# Hide the custom footer
(
"profile",
"footer_slot",
"""
{
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'custom_footer',
}"""
),
# Insert a footer just for the Profile MFE
(
"profile",
"footer_slot",
"""
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_profile_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1>This is the Profile MFE's footer.</h1>
),
},
}"""
)
])
Note that here we're assuming you didn't remove the global footer configuration defined by ``"all"``, so you have to hide ``custom_footer`` instead of ``default_contents``. If you were to rebuild the MFE image now, the Profile MFE's footer would say "This is the Profile MFE's footer", whereas all the others would still contain the global "This is the footer." message.
Note that here we're assuming you didn't remove the global footer configuration defined by the filter items targetting ``"all"``, so you have to hide ``custom_footer`` instead of ``default_contents``. If you were to rebuild the MFE image now, the Profile MFE's footer would say "This is the Profile MFE's footer", whereas all the others would still contain the global "This is the footer." message.
For more complex frontend plugins, you should make use of ``mfe-env-config-*`` patches to define your JSX components separately. For instance, you could create an NPM plugin package, install it via ``mfe-dockerfile-post-npm-install``, import the desired components via ``mfe-env-config-static-imports``, then refer to them with the ``PLUGIN_SLOTS`` filter as described above. Refer to the `patch catalog <#template-patch-catalog>`_ below for more details.
For more complex frontend plugins, you should make use of ``mfe-env-config-*`` patches to define your JSX components separately. For instance, you could create an NPM plugin package, install it via ``mfe-dockerfile-post-npm-install``, import the desired components via ``mfe-env-config-buildtime-imports``, then refer to them with the ``PLUGIN_SLOTS`` filter as described above. Refer to the `patch catalog <#template-patch-catalog>`_ below for more details.
Installing from a private npm registry
@@ -517,28 +536,28 @@ This is the list of all patches used across tutor-mfe (outside of any plugin). A
cd tutor-mfe
git grep "{{ patch" -- tutormfe/templates
mfe-env-config-static-imports
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mfe-env-config-buildtime-imports
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use this patch for any additional static imports you need in ``env.config.jsx``. They will be available here if you used the `mfe-docker-post-npm-install patch <#mfe-docker-post-npm-install>`_ to install an NPM package globally.
Use this patch for any static imports you need in ``env.config.jsx``. They will be available here if you used the `mfe-docker-post-npm-install patch <#mfe-docker-post-npm-install>`_ to install an NPM package for all MFEs.
It gets rendered at the very top of the file. You should use normal `ES6 import syntax <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import>`_.
Note that if you want to import a module only on a particular MFE, you can't do it via static imports: you'll have to use the ``mfe-env-config-dynamic-imports-{}`` patch, described below.
Note that if you want to only import a module for a particular MFE, doing it here won't work: you'll probably want to use the ``mfe-env-config-runtime-imports-{}`` patch described below.
File changed: ``tutormfe/templates/mfe/build/mfe/env.config.jsx``
mfe-env-config-head
~~~~~~~~~~~~~~~~~~~
mfe-env-config-buildtime-definitions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use this patch for arbitrary ``env.config.jsx`` javascript code. It gets rendered immediately after static imports, and is particularly useful for defining more complex components for use in plugin slots.
Use this patch for arbitrary ``env.config.jsx`` javascript code that gets evaluated at build time. It is particularly useful for defining slightly more complex components for use in plugin slots.
There's currently no version of this patch that runs per MFE, though one could use the MFE-specific ``mfe-env-config-dynamic-imports-{}`` to achieve the same effect.
There's no version of this patch that runs per MFE. If you want to define MFE-specific code, you should use the MFE-specific ``mfe-env-config-runtime-definitions-{}`` to achieve the same effect.
File changed: ``tutormfe/templates/mfe/build/mfe/env.config.jsx``
mfe-env-config-dynamic-imports
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mfe-env-config-runtime-definitions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This patch gets rendered inside an ``async`` function in ``env.config.jsx`` that runs in the browser, allowing you to define conditional imports for external modules that may only be available at runtime. Just make sure to use `import() function <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import>`_ syntax:
@@ -551,23 +570,19 @@ Note that if any module is not available at runtime, ``env.config.jsx`` executio
File changed: ``tutormfe/templates/mfe/build/mfe/env.config.jsx``
mfe-env-config-dynamic-imports-{}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mfe-env-config-runtime-definitions-{}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
With this patch you can conditionally import a module for specific MFEs in ``env.config.jsx``. This is a useful place to put an import if you're using the ``mfe-docker-post-npm-install-*`` patch to install a plugin that only works on a particular MFE.
With this patch you can conditionally import modules or define code for specific MFEs in ``env.config.jsx``. This is a useful place to put an import if you're using the ``mfe-docker-post-npm-install-*`` patch to install a plugin that only works on a particular MFE.
As above, make sure to use the ``import()`` function.
You can also use this patch to insert arbitrary code that will only run on a specific MFE.
File changed: ``tutormfe/templates/mfe/build/mfe/env.config.jsx``
mfe-env-config-tail
~~~~~~~~~~~~~~~~~~~
At this point, ``env.config.jsx`` is ready to return the ``config`` object to the initialization code. You can use this patch to do anything to the object, including using modules that were imported dynamically earlier.
mfe-env-config-runtime-final
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There's no MFE-specific version of this patch, but you can achieve the same effect by checking the ``process.env.npm_package_name`` variable in the Javascript code you insert. Refer to the ``env.config.jsx`` template in this repository for an example of how to do that.
At this point, ``env.config.jsx`` is ready to return the ``config`` object to the initialization code at runtime. You can use this patch to do anything to the object, including using modules that were imported dynamically earlier.
File changed: ``tutormfe/templates/mfe/build/mfe/env.config.jsx``
20 changes: 6 additions & 14 deletions tutormfe/plugin.py
Original file line number Diff line number Diff line change
@@ -81,28 +81,20 @@ def _add_core_mfe_apps(apps: dict[str, MFE_ATTRS_TYPE]) -> dict[str, MFE_ATTRS_T
return apps


@functools.lru_cache(maxsize=None)
@tutor_hooks.lru_cache
def get_mfes() -> dict[str, MFE_ATTRS_TYPE]:
"""
This function is cached for performance.
"""
return MFE_APPS.apply({})


@functools.lru_cache(maxsize=None)
def get_plugin_slots(mfe_name: str) -> list[tuple[str, str, str]]:
@tutor_hooks.lru_cache
def get_plugin_slots(mfe_name: str) -> list[tuple[str, str]]:
"""
This function is cached for performance.
"""
return [i for i in PLUGIN_SLOTS.apply([]) if i[0] == mfe_name]


@tutor_hooks.Actions.PLUGIN_LOADED.add()
def _clear_get_mfes_cache(_name: str) -> None:
"""
Don't forget to clear cache, or we'll have some strange surprises...
"""
get_mfes.cache_clear()
return [i[-2:] for i in PLUGIN_SLOTS.iterate() if i[0] == mfe_name]


def iter_mfes() -> t.Iterable[tuple[str, MFE_ATTRS_TYPE]]:
@@ -114,11 +106,11 @@ def iter_mfes() -> t.Iterable[tuple[str, MFE_ATTRS_TYPE]]:
yield from get_mfes().items()


def iter_plugin_slots(mfe_name: str) -> t.Iterable[tuple[str, str, str]]:
def iter_plugin_slots(mfe_name: str) -> t.Iterable[tuple[str, str]]:
"""
Yield:
(mfe_name, slot_name, slot_config)
(slot_name, plugin_config)
"""
yield from get_plugin_slots(mfe_name)

53 changes: 28 additions & 25 deletions tutormfe/templates/mfe/build/mfe/env.config.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
{{- patch("mfe-env-config-static-imports") }}
{{- patch("mfe-env-config-buildtime-imports") }}

{{- patch("mfe-env-config-head") }}
function addPlugins(config, slot_name, plugins) {
if (slot_name in config.pluginSlots === false) {
config.pluginSlots[slot_name] = {
keepDefault: true,
plugins: []
};
}

config.pluginSlots[slot_name].plugins.push(...plugins);
}

{{- patch("mfe-env-config-buildtime-definitions") }}

async function getConfig () {
let config = {};
let config = {
pluginSlots: {}
};

try {
/* We can't assume FPF exists, as it's not declared as a dependency in all
@@ -12,34 +25,24 @@ async function getConfig () {
* needs to be inside the `try{}` block.
*/
const { DIRECT_PLUGIN, PLUGIN_OPERATIONS } = await import('@openedx/frontend-plugin-framework');
{{- patch("mfe-env-config-dynamic-imports") }}

config = {
pluginSlots: {
{%- for _, slot_name, slot_config in iter_plugin_slots("all") %}
{{ slot_name }}: {
keepDefault: true,
plugins: [
{{- slot_config }}
]
},
{%- endfor %}
}
};

{%- for app_name, app in iter_mfes() %}
if (process.env.npm_package_name == '@edx/frontend-app-{{ app_name }}') {
{{- patch("mfe-env-config-dynamic-imports-{}".format(app_name)) }}
{{- patch("mfe-env-config-runtime-definitions") }}

{%- for slot_name, plugin_config in iter_plugin_slots("all") %}
addPlugins(config, '{{ slot_name }}', [{{ plugin_config }}]);
{%- endfor %}

{%- for app_name, _ in iter_mfes() %}
if (process.env.APP_ID == '{{ app_name }}') {
{{- patch("mfe-env-config-runtime-definitions-{}".format(app_name)) }}

{%- for _, slot_name, slot_config in iter_plugin_slots(app_name) %}
config.pluginSlots.{{ slot_name }}.plugins.push(
{{- slot_config }}
);
{%- for slot_name, plugin_config in iter_plugin_slots(app_name) %}
addPlugins(config, '{{ slot_name }}', [{{ plugin_config }}]);
{%- endfor %}
}
{%- endfor %}

{{- patch("mfe-env-config-tail") }}
{{- patch("mfe-env-config-runtime-final") }}
} catch { }

return config;