Skip to content
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
28 changes: 28 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ In addition, this plugin comes with a few MFEs which are enabled by default:
- `Profile <https://github.com/openedx/frontend-app-profile/>`__
- `Catalog <https://github.com/openedx/frontend-app-catalog/>`__

In addition, this plugin bundles a number of "core plugins": frontend plugin packages injected into the MFEs above via the plugin slot framework. The following core plugins are enabled by default:

- `Notifications <https://github.com/openedx/frontend-plugin-notifications/>`__ (a notifications tray in the MFE headers)

Instructions for using each of these MFEs are given below.

Installation
Expand Down Expand Up @@ -182,6 +186,30 @@ To disable an existing MFE, remove the corresponding entry from the ``MFE_APPS``
mfes.pop("profile")
return mfes

Core plugins
~~~~~~~~~~~~

Core plugins are bundled frontend plugin packages that ship with tutor-mfe and are injected into the MFEs via the plugin slot framework. They are enabled by default, but operators can disable any of them by popping the corresponding entry from the ``CORE_PLUGINS`` filter, symmetrically to how ``MFE_APPS`` works.

The following core plugins are currently bundled:

- ``notifications``: the `notifications tray <https://github.com/openedx/frontend-plugin-notifications/>`__, added to the learning and Studio headers. Its behaviour can be tuned with three configuration settings:

- ``NOTIFICATIONS_DEFAULT_FROM_EMAIL`` (default: inherits from ``CONTACT_EMAIL``): the sender address used by notification emails.
- ``NOTIFICATIONS_ENABLE_SHOW_EMAIL_CHANNEL`` (default: ``True``): whether to show the email channel in the notification preferences UI.
- ``NOTIFICATIONS_ENABLE_SHOW_PUSH_CHANNEL`` (default: ``False``): whether to show the push channel in the notification preferences UI.

To disable the notifications tray (or any other core plugin), add a Tutor plugin with:

.. code-block:: python

from tutormfe.hooks import CORE_PLUGINS

@CORE_PLUGINS.add()
def _disable_notifications(plugins):
plugins.pop("notifications", None)
return plugins

Using custom translations to your MFEs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions changelog.d/20260420_core_plugins_notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [Feature] Introduce the ``CORE_PLUGINS`` filter, a new extension point for bundled frontend plugin packages that ship with tutor-mfe. Operators can disable any core plugin by popping its name from the filter, symmetrically to how ``MFE_APPS`` works. (by @arbrandes)
- [Feature] Bundle the notifications tray (``@edx/frontend-plugin-notifications``) as the first core plugin, enabled by default. This replaces the need for a standalone ``tutor-contrib-platform-notifications`` plugin. (by @arbrandes)
4 changes: 4 additions & 0 deletions tutormfe/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

MFE_ATTRS_TYPE = t.Dict[t.Literal["repository", "port", "version"], t.Union["str", int]]

CORE_PLUGIN_ATTRS_TYPE = t.Dict[t.Literal["npm_package", "npm_version"], str]

MFE_APPS: Filter[dict[str, MFE_ATTRS_TYPE], []] = Filter()

CORE_PLUGINS: Filter[dict[str, CORE_PLUGIN_ATTRS_TYPE], []] = Filter()

PLUGIN_SLOTS: Filter[list[tuple[str, str, str]], []] = Filter()
3 changes: 3 additions & 0 deletions tutormfe/patches/lms-env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% if is_core_plugin_enabled("notifications") %}
NOTIFICATIONS_DEFAULT_FROM_EMAIL: '{{ NOTIFICATIONS_DEFAULT_FROM_EMAIL }}'
{% endif %}
3 changes: 3 additions & 0 deletions tutormfe/patches/openedx-common-settings
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% if is_core_plugin_enabled("notifications") %}
NOTIFICATIONS_DEFAULT_FROM_EMAIL = ENV_TOKENS.get("NOTIFICATIONS_DEFAULT_FROM_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
{% endif %}
5 changes: 5 additions & 0 deletions tutormfe/patches/openedx-lms-production-settings
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,8 @@ CSRF_TRUSTED_ORIGINS.append("{% if ENABLE_HTTPS %}https://{% else %}http://{% en

{{ patch("mfe-lms-common-settings") }}
{{ patch("mfe-lms-production-settings") }}

{% if is_core_plugin_enabled("notifications") %}
MFE_CONFIG["SHOW_EMAIL_CHANNEL"] = {{ NOTIFICATIONS_ENABLE_SHOW_EMAIL_CHANNEL }}
MFE_CONFIG["SHOW_PUSH_CHANNEL"] = {{ NOTIFICATIONS_ENABLE_SHOW_PUSH_CHANNEL }}
{% endif %}
60 changes: 59 additions & 1 deletion tutormfe/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
from tutor.types import Config, get_typed

from .__about__ import __version__
from .hooks import MFE_APPS, MFE_ATTRS_TYPE, PLUGIN_SLOTS
from .hooks import (
CORE_PLUGIN_ATTRS_TYPE,
CORE_PLUGINS,
MFE_APPS,
MFE_ATTRS_TYPE,
PLUGIN_SLOTS,
)

# Handle version suffix in main mode, just like tutor core
if __version_suffix__:
Expand Down Expand Up @@ -91,6 +97,25 @@ def _add_core_mfe_apps(apps: dict[str, MFE_ATTRS_TYPE]) -> dict[str, MFE_ATTRS_T
return apps


# Core plugins are bundled frontend plugin packages (and associated wiring) that ship
# enabled by default. Operators can disable any of them by popping the name from the
# CORE_PLUGINS filter, symmetric to how MFE_APPS works.
DEFAULT_CORE_PLUGINS: dict[str, CORE_PLUGIN_ATTRS_TYPE] = {
"notifications": {
"npm_package": "@edx/frontend-plugin-notifications",
"npm_version": "^2.0.3",
},
}


@CORE_PLUGINS.add(priority=tutor_hooks.priorities.HIGH)
def _add_default_core_plugins(
plugins: dict[str, CORE_PLUGIN_ATTRS_TYPE],
) -> dict[str, CORE_PLUGIN_ATTRS_TYPE]:
plugins.update(DEFAULT_CORE_PLUGINS)
return plugins


@tutor_hooks.lru_cache
def get_mfes() -> dict[str, MFE_ATTRS_TYPE]:
"""
Expand All @@ -99,6 +124,27 @@ def get_mfes() -> dict[str, MFE_ATTRS_TYPE]:
return MFE_APPS.apply({})


@tutor_hooks.lru_cache
def get_core_plugins() -> dict[str, CORE_PLUGIN_ATTRS_TYPE]:
"""
This function is cached for performance.
"""
return CORE_PLUGINS.apply({})


def iter_core_plugins() -> t.Iterable[tuple[str, CORE_PLUGIN_ATTRS_TYPE]]:
"""
Yield:

(name, dict)
"""
yield from get_core_plugins().items()


def is_core_plugin_enabled(name: str) -> bool:
return name in get_core_plugins()


class MFEMountData:
"""Stores categorized mounted and unmounted MFEs."""

Expand Down Expand Up @@ -158,6 +204,8 @@ def get_mfe(mfe_name: str) -> t.Union[MFE_ATTRS_TYPE, t.Any]:
("iter_mfes", iter_mfes),
("iter_plugin_slots", iter_plugin_slots),
("is_mfe_enabled", is_mfe_enabled),
("iter_core_plugins", iter_core_plugins),
("is_core_plugin_enabled", is_core_plugin_enabled),
("MFEMountData", MFEMountData),
]
)
Expand Down Expand Up @@ -327,6 +375,16 @@ def _build_3rd_party_dev_mfes_on_launch(
list(config.get("overrides", {}).items())
)

# Notifications core plugin settings. Registered unconditionally: harmless if the
# plugin is disabled, since nothing will consume them.
tutor_hooks.Filters.CONFIG_DEFAULTS.add_items(
[
("NOTIFICATIONS_ENABLE_SHOW_EMAIL_CHANNEL", True),
("NOTIFICATIONS_ENABLE_SHOW_PUSH_CHANNEL", False),
("NOTIFICATIONS_DEFAULT_FROM_EMAIL", "{{ CONTACT_EMAIL }}"),
]
)


# Actions
@tutor_hooks.Actions.CONFIG_LOADED.add()
Expand Down
3 changes: 3 additions & 0 deletions tutormfe/templates/mfe/build/mfe/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ ENV CPPFLAGS=-DPNG_ARM_NEON_OPT=0
{#- We define this environment variable to bypass an issue with the installation of pact https://github.com/pact-foundation/pact-js-core/issues/264 #}
ENV PACT_SKIP_BINARY_INSTALL=true
RUN --mount=type=cache,target=/root/.npm,sharing=shared npm clean-install --no-audit --no-fund --registry=$NPM_REGISTRY
{%- for _plugin_name, plugin in iter_core_plugins() %}
RUN --mount=type=cache,target=/root/.npm,sharing=shared npm install --no-audit --no-fund --registry=$NPM_REGISTRY {{ plugin["npm_package"] }}@{{ plugin["npm_version"] }}
{%- endfor %}
{{ patch("mfe-dockerfile-post-npm-install") }}
{{ patch("mfe-dockerfile-post-npm-install-{}".format(app_name)) }}
COPY --from={{ app_name }}-src / /openedx/app
Expand Down
23 changes: 23 additions & 0 deletions tutormfe/templates/mfe/build/mfe/env.config.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,32 @@ async function setConfig () {
* needs to be inside the `try{}` block.
*/
const { DIRECT_PLUGIN, PLUGIN_OPERATIONS } = await import('@openedx/frontend-plugin-framework');
{%- if is_core_plugin_enabled("notifications") %}
const { NotificationsTray } = await import('@edx/frontend-plugin-notifications');
{%- endif %}
Comment on lines +28 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add this to mfe-env-config-runtime-definitions patch again like it was in tutor-contrib-platform-notifications instead of filling in env.config.jsx?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but not cleanly: the patches tutor-mfe defines (such as mfe-env-config-runtime-definitions) are meant for other plugins to consume. That's what the templates are for: we put the stuff we own in there, and patches are for external use.


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

{%- if is_core_plugin_enabled("notifications") %}
{%- for slot_name in [
"org.openedx.frontend.layout.header_desktop_secondary_menu.v1",
"org.openedx.frontend.layout.header_learning_help.v1",
"org.openedx.frontend.layout.studio_header_search_button_slot.v1",
] %}
addPlugins(config, '{{ slot_name }}', [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'notification-drawer-widget',
priority: 10,
type: DIRECT_PLUGIN,
RenderWidget: NotificationsTray,
},
},
]);
{%- endfor %}
{%- endif %}
Comment on lines +34 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also move this to plugin.py file under notifications section? As I was thinking we might end up adding plugin slots directly to env.config.jsx in future? Although its also fine if we don't do it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually tried, but it doesn't work well: we're consuming the hook (PLUGIN_SLOTS) we're defining ourselves, so there are timing/race conditions that result in the core plugin not being picked up here.

I know it looks funny, but defining this directly in the template is the right place.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are timing/race conditions that result in the core plugin not being picked up here.

I know it looks funny, but defining this directly in the template is the right place.

makes sense,


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