diff --git a/Makefile b/Makefile index b222c890..66f947b0 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,16 @@ transifex_input = $(i18n)/transifex_input.json # This directory must match .babelrc . transifex_temp = ./temp/babel-plugin-formatjs +build: + rm -rf ./dist + ./node_modules/.bin/fedx-scripts babel src --out-dir dist --source-maps --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js --copy-files + @# --copy-files will bring in everything else that wasn't processed by babel. Remove what we don't want. + @find dist -name '*.test.js*' -delete + rm ./dist/setupTest.js + cp ./package.json ./dist/package.json + cp ./LICENSE ./dist/LICENSE + cp ./README.md ./dist/README.md + precommit: npm run lint npm audit diff --git a/README-template-frontend-app.rst b/README-template-frontend-app.rst deleted file mode 100644 index d528dc0f..00000000 --- a/README-template-frontend-app.rst +++ /dev/null @@ -1,259 +0,0 @@ -frontend-app-[PLACEHOLDER] -########################## - -.. note:: - - This README is a template. As a maintainer, please review its contents and - update all relevant sections. Instructions to you are marked with - "[PLACEHOLDER]" or "[TODO]". Update or remove those sections, and remove this - note when you are done. - -|license-badge| |status-badge| |ci-badge| |codecov-badge| - -.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-[PLACEHOLDER].svg - :target: https://github.com/openedx/frontend-app-[PLACEHOLDER]/blob/main/LICENSE - :alt: License - -.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen - -.. |ci-badge| image:: https://github.com/openedx/frontend-app-[PLACEHOLDER]/actions/workflows/ci.yml/badge.svg - :target: https://github.com/openedx/frontend-app-[PLACEHOLDER]/actions/workflows/ci.yml - :alt: Continuous Integration - -.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-[PLACEHOLDER]/coverage.svg?branch=main - :target: https://codecov.io/github/openedx/frontend-app[PLACEHOLDER]?branch=main - :alt: Codecov - -Purpose -======= - -.. note:: - - [TODO] - - What is this MFE? Add a 2-3 sentence description of what it is and what it - does. - -This is the Awesome MFE. It was built to provide an unmatched learning -experience, with improved tools for both randomized goodness and the ability to -directly reference amaze-blocks in existing courses. This experience is powered -by the new Fantastico storage engine. - -Getting Started -=============== - -Devstack Installation ---------------------- - -.. note:: - - [TODO] - - Describe in detail how this MFE can be installed and set up for development - in a devstack. Include as many screenshots as you can to make your guide - easier to follow! Use the following steps as an example: - -Follow these steps to provision, run, and enable an instance of the -[PLACEHOLDER] MFE for local development via the `devstack`_. - -.. _devstack: https://github.com/openedx/devstack#getting-started - -#. To start, clone the devstack repository as a child of an arbitrary ``~/workspace/`` directory. - - .. code-block:: - - mkdir -p ~/workspace/ - cd ~/workspace/ - git clone https://github.com/openedx/devstack.git - -#. Configure default services and setup devstack - - Create a ``devstack/options.local.mk`` file with only the services required. - Commonly, this will just be the LMS: - - .. code-block:: - - DEFAULT_SERVICES ?= \ - lms - -#. Start the devstack with: - - .. code-block:: - - cd devstack - make dev.pull - make dev.provision - make dev.up - -#. In an LMS shell, enable the ``ENABLE_[PLACEHOLDER]_MICROFRONTEND`` feature flag: - - .. code-block:: - - make lms-shell - vim /edx/etc/lms.yml - --- - FEATURES: - ENABLE_[PLACEHOLDER]_MICROFRONTEND: true - - Exit the shell and restart the LMS so changes take effect: - - .. code-block:: - - make lms-restart - -#. Create and enable the waffle flag required to redirect users to the MFE, - enabling it for everyone: - - .. code-block:: - - make lms-shell - ./manage.py lms waffle_flag --create --everyone [PLACEHOLDER].redirect_to_microfrontend - -#. Start this MFE with: - - .. code-block:: - - cd frontend-app-[PLACEHOLDER] - nvm use - npm ci - npm start - -#. Finally, open the MFE in a browser - - Navigate to `http://localhost:8080 `_ to open the - MFE. This is what it should look like if everything worked: - - .. figure:: ./docs/images/template.jpg - - "Polycon marking template" by mangtronix is licensed under CC BY-SA 2.0. - -Configuration -------------- - -.. note:: - - [TODO] - - Explicitly list anything that this MFE requires to function correctly. This includes: - - * A list of both required and optional .env variables, and how they each - affect the functioning of the MFE - - * A list of edx-platform `feature and waffle flags`_ that are either required - to enable use of this MFE, or affect the behavior of the MFE in some other - way - - * A list of IDAs or other MFEs that this MFE depends on to function correctly - -.. _feature and waffle flags: https://docs.openedx.org/projects/openedx-proposals/en/latest/best-practices/oep-0017-bp-feature-toggles.html - -[PLACEHOLDER: Other Relevant Sections] -====================================== - -.. note:: - - [TODO] - - This is optional, but you might have additional sections you wish to cover. - For instance, architecture documentation, i18n notes, build process, or - more. - -Known Issues -============ - -.. note:: - - [TODO] - - If there are long-standing known issues, list them here as a bulletted list, - linking to the actual issues in the Github repository. - -Development Roadmap -=================== - -.. note:: - - [TODO] - - Include a list of current development targets, in (rough) descending order - of priority. It can be a simple bulleted list of roadmap items with links - to Github issues or wiki pages. - -Getting Help -============ - -.. note:: - - [TODO] - - Use the following as a template, but feel free to add specific places where - this MFE is commonly discussed. - -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-app-[PLACEHOLDER]/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 `_ for details. - -Contributing -============ - -.. note:: - - [TODO] - - Feel free to add contribution details specific to your repository. - -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-app-[PLACEHOLDER] - -Reporting Security Issues -========================= - -Please do not report security issues in public. Email security@openedx.org instead. diff --git a/README.rst b/README.rst index 293567f9..cc1aac1e 100644 --- a/README.rst +++ b/README.rst @@ -1,122 +1,132 @@ -frontend-template-application -############################# +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 -Purpose -******* - -This repository is a template for Open edX micro-frontend applications. It is -flagged as a Template Repository, meaning it can be used as a basis for new -GitHub repositories by clicking the green "Use this template" button above. -The rest of this document describes how to work with your new micro-frontend -**after you've created a new repository from the template.** - -Getting Started -*************** - -After copying the template repository, you'll want to do a find-and-replace to -replace all instances of ``frontend-template-application`` with the name of -your new repository. Also edit index.html to replace "Application Template" -with a friendly name for this application that users will see in their browser -tab. - -Prerequisites -============= - -The `devstack`_ is currently recommended as a development environment for your -new MFE. If you start it with ``make dev.up.lms`` that should give you -everything you need as a companion to this frontend. - -Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer -to the `relevant tutor-mfe documentation`_ to get started using it. - -.. _Devstack: https://github.com/openedx/devstack - -.. _Tutor: https://github.com/overhangio/tutor - -.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development - -Cloning and Startup -=================== - -In the following steps, replace "[PLACEHOLDER]" with the name of the repo you -created when copying this template above. - -1. Clone your new repo: - - ``git clone https://github.com/openedx/frontend-app-[PLACEHOLDER].git`` - -2. Use node v18.x. - - The current version of the micro-frontend build scripts support node 18. - Using other major versions of node *may* work, but this is unsupported. For - convenience, this repository includes an .nvmrc file to help in setting the - correct node version via `nvm `_. - -3. Install npm dependencies: - - ``cd frontend-app-[PLACEHOLDER] && npm install`` - -4. Update the application port to use for local development: - - Default port is 8080. If this does not work for you, update the line - `PORT=8080` to your port in all .env.* files - -5. Start the dev server: - - ``npm start`` - -The dev server is running at `http://localhost:8080 `_ -or whatever port you setup. - -Making Your New Project's README File -===================================== - -Move ``README-template-frontend-app.rst`` to your project's ``README.rst`` -file. Please fill out all the sections - this helps out all developers -understand your MFE, how to install it, and how to use it. - -Developing -********** - -This section concerns development of ``frontend-template-application`` itself, -not the templated copy. - -It should be noted that one of the goals of this repository is for it to -function correctly as an MFE (as in ``npm install && npm start``) even if no -modifications are made. This ensures that developers get a *practical* working -example, not just a theoretical one. +.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen -This also means, of course, that any committed code should be tested and -subject to both CI and branch protection rules. +.. |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 -Project Structure -================= +.. |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 -The source for this project is organized into nested submodules according to -the `Feature-based Application Organization ADR`_. +Purpose +======= -.. _Feature-based Application Organization ADR: https://github.com/openedx/frontend-template-application/blob/master/docs/decisions/0002-feature-based-application-organization.rst +This is the Frontend Plugin Framework library. This framework is designed to allow for any type of plugin to be used when +plugging a child component into a Host MFE. The current plugin that is made available is iFrame-based, which allows +for a component that lives in another MFE (the 'Child MFE') to be plugged into a Plugin Slot that lives in the 'Host MFE'. -Build Process Notes +Getting Started +=============== +1. Add Library Dependency +------------------------- + +Add `@edx/frontend-plugin-framework` to the `package.json` of both Host and Child MFEs. + +Micro-frontend configuration document (JS) +------------------------------------------ + +Micro-frontends that would like to use the Plugin Framework need to be configured via a JavaScript configuration +document and a `plugins` config. Technically, only the Host MFE would require an `env.config.js` file with a `plugins` config. +Keep in mind that since any Child MFE can theoretically also contain its own PluginSlot, it will eventually need its own +JavaScript configuration. + + .. code-block:: + + const config = { + // other existing configuration + plugins: { + sidebar: { + keepDefault: false, // bool to keep default host content + plugins: [ + { + url: 'https://plugin.app/plugin1', + type: IFRAME_PLUGIN, + } + ] + } + } + } + +Host Micro-frontend (JSX) +------------------------- + +Hosts must define PluginSlot components in areas of the UI where they intend to accept extensions. +The Host MFE, and thus the owners of the Host MFE, are responsible for deciding where it is acceptable to mount a plugin. +They also decide the dimensions, responsiveness/scrolling policy, and whether the slot supports passing any additional +data to the plugin as part of its contract. + + .. code-block:: + + + + + + // removed if keepDefault is false + + + + + + + + +Plugin Micro-frontend (JSX) and Fallback Behavior +------------------------------------------------- + +The plugin MFE is no different than any other MFE except that it defines a Plugin component as a child of a route. +This component is responsible for communicating (via postMessage) with the host page and resizing its content to match +the dimensions available in the host’s PluginSlot. + +It’s notoriously difficult to know in the host application 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. +For the fallback content, the Plugin-owning team would pass a fallback component into the Plugin tag that is wrapped around their component, as noted below. Otherwise, a default fallback component would be used. + .. code-block:: + + + + + + + }> + + + + + +Known Issues +============ + +Development Roadmap =================== -**Production Build** - -The production build is created with ``npm run build``. - -Internationalization -==================== +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. -Please see refer to the `frontend-platform i18n howto`_ for documentation on -internationalization. +- The first target is to use this framework in Learner Dashboard MFE to extract the Recommendations panel out of the repo. -.. _frontend-platform i18n howto: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst +- Incorporate other plugin proposals from the Frontend Pluggability Summit in order to provide the most appropriate plugin option for a given component. 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. @@ -129,7 +139,7 @@ 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-template-application/issues +https://github.com/openedx/frontend-plugin-framework/issues For more information about these options, see the `Getting Help`_ page. @@ -139,7 +149,7 @@ For more information about these options, see the `Getting Help`_ page. .. _Getting Help: https://openedx.org/getting-help License -******* +======= The code in this repository is licensed under the AGPLv3 unless otherwise noted. @@ -147,7 +157,7 @@ noted. Please see `LICENSE `_ for details. Contributing -************ +============ Contributions are very welcome. Please read `How To Contribute`_ for details. @@ -161,36 +171,22 @@ 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-template-application +.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-plugin-framework Reporting Security Issues -************************* - -Please do not report security issues in public, and email security@openedx.org instead. - -.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-template-application.svg - :target: https://github.com/openedx/frontend-template-application/blob/main/LICENSE - :alt: License +========================= -.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen - -.. |ci-badge| image:: https://github.com/openedx/frontend-template-application/actions/workflows/ci.yml/badge.svg - :target: https://github.com/openedx/frontend-template-application/actions/workflows/ci.yml - :alt: Continuous Integration - -.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-template-application/coverage.svg?branch=main - :target: https://codecov.io/github/openedx/frontend-template-application?branch=main - :alt: Codecov +Please do not report security issues in public. Email security@openedx.org instead. diff --git a/package-lock.json b/package-lock.json index 1bbd23c8..1bf82b55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@edx/frontend-template-application", + "name": "@edx/frontend-plugin-framework", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@edx/frontend-template-application", + "name": "@edx/frontend-plugin-framework", "version": "0.1.0", "license": "AGPL-3.0", "dependencies": { @@ -19,10 +19,12 @@ "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.0", + "classnames": "^2.3.2", "core-js": "3.33.1", "prop-types": "15.8.1", "react": "17.0.2", "react-dom": "17.0.2", + "react-error-boundary": "^4.0.11", "react-redux": "7.2.9", "react-router": "6.17.0", "react-router-dom": "6.17.0", @@ -32,7 +34,11 @@ "devDependencies": { "@edx/browserslist-config": "^1.1.1", "@edx/frontend-build": "13.0.4", + "@edx/frontend-platform": "5.6.1", "@edx/reactifex": "^2.1.1", + "@testing-library/dom": "^8.20.1", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^12.1.5", "glob": "7.2.3", "husky": "7.0.4", "jest": "29.7.0" @@ -46,6 +52,12 @@ "node": ">=0.10.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", + "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", + "dev": true + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -5621,6 +5633,253 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@testing-library/dom": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.1.4.tgz", + "integrity": "sha512-wpoYrCYwSZ5/AxcrjLxJmCU6I5QAJXslEeSiMQqaWmP2Kzpd1LvF/qxmAIW2qposULGWq2gw30GgVNFLSc2Jnw==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.3.1", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz", + "integrity": "sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.0.0", + "@types/react-dom": "<18.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "<18.0.0", + "react-dom": "<18.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -5637,6 +5896,12 @@ "node": ">=10.13.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", @@ -5901,6 +6166,26 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.23.tgz", + "integrity": "sha512-lnJAZfMEDxfvELeeT24w4rnUYwpzUzQAOTfJQbWYnLcx8AEfz+fXJDCbowIBqNK/Bi4D6j8ovT8Qsda2OtDApA==", + "dev": true, + "dependencies": { + "@types/react": "^17" + } + }, + "node_modules/@types/react-dom/node_modules/@types/react": { + "version": "17.0.70", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.70.tgz", + "integrity": "sha512-yqYMK49/cnqw+T8R9/C+RNjRddYmPDGI5lKHi3bOYceQCBAh8X2ngSbZP0gnVeyvHr0T7wEgIIGKT1usNol08w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, "node_modules/@types/react-redux": { "version": "7.1.25", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", @@ -8330,6 +8615,12 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -8815,6 +9106,12 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -11716,6 +12013,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -15740,6 +16046,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/mailto-link": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mailto-link/-/mailto-link-2.0.0.tgz", @@ -15920,6 +16235,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", @@ -18239,6 +18563,17 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", + "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -18709,6 +19044,19 @@ "node": ">=6.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -20560,6 +20908,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index 80fe1b79..9f639646 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@edx/frontend-template-application", + "name": "@edx/frontend-plugin-framework", "version": "0.1.0", - "description": "Frontend application template", + "description": "Frontend Plugin Framework ", "repository": { "type": "git", - "url": "git+https://github.com/openedx/frontend-template-application.git" + "url": "git+https://github.com/openedx/frontend-plugin-framework.git" }, "browserslist": [ "extends @edx/browserslist-config" @@ -25,12 +25,12 @@ }, "author": "edX", "license": "AGPL-3.0", - "homepage": "https://github.com/openedx/frontend-template-application#readme", + "homepage": "https://github.com/openedx/frontend-plugin-framework#readme", "publishConfig": { "access": "public" }, "bugs": { - "url": "https://github.com/openedx/frontend-template-application/issues" + "url": "https://github.com/openedx/frontend-plugin-framework/issues" }, "dependencies": { "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", @@ -43,10 +43,12 @@ "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.0", + "classnames": "^2.3.2", "core-js": "3.33.1", "prop-types": "15.8.1", "react": "17.0.2", "react-dom": "17.0.2", + "react-error-boundary": "^4.0.11", "react-redux": "7.2.9", "react-router": "6.17.0", "react-router-dom": "6.17.0", @@ -56,7 +58,11 @@ "devDependencies": { "@edx/browserslist-config": "^1.1.1", "@edx/frontend-build": "13.0.4", + "@edx/frontend-platform": "5.6.1", "@edx/reactifex": "^2.1.1", + "@testing-library/dom": "^8.20.1", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^12.1.5", "glob": "7.2.3", "husky": "7.0.4", "jest": "29.7.0" diff --git a/src/plugins/Plugin.jsx b/src/plugins/Plugin.jsx new file mode 100644 index 00000000..34038eeb --- /dev/null +++ b/src/plugins/Plugin.jsx @@ -0,0 +1,101 @@ +'use client'; + +import React, { + useEffect, useMemo, useState, +} from 'react'; +import PropTypes from 'prop-types'; +import { ErrorBoundary } from 'react-error-boundary'; +import { logError } from '@edx/frontend-platform/logging'; +import { + injectIntl, + intlShape, +} from '@edx/frontend-platform/i18n'; + +import { + dispatchMountedEvent, dispatchReadyEvent, dispatchUnmountedEvent, useHostEvent, +} from './data/hooks'; +import { PLUGIN_RESIZE } from './data/constants'; +import messages from './Plugins.messages'; + +// TODO: see example-plugin-app/src/PluginOne.jsx for example of customizing errorFallback +function errorFallbackDefault(intl) { + return ( +
+

+ {intl.formatMessage(messages.unexpectedError)} +

+
+ ); +} + +const Plugin = ({ + children, className, intl, style, ready, errorFallbackProp, +}) => { + const [dimensions, setDimensions] = useState({ + width: null, + height: null, + }); + + const finalStyle = useMemo(() => ({ + ...dimensions, + ...style, + }), [dimensions, style]); + + const errorFallback = errorFallbackProp || errorFallbackDefault; + + // Error logging function + // Need to confirm: When an error is caught here, the logging will be sent to the child MFE's logging service + const logErrorToService = (error, info) => { + logError(error, { stack: info.componentStack }); + }; + + useHostEvent(PLUGIN_RESIZE, ({ payload }) => { + setDimensions({ + width: payload.width, + height: payload.height, + }); + }); + + useEffect(() => { + dispatchMountedEvent(); + + return () => { + dispatchUnmountedEvent(); + }; + }, []); + + useEffect(() => { + if (ready) { + dispatchReadyEvent(); + } + }, [ready]); + + return ( +
+ errorFallback(intl)} + onError={logErrorToService} + > + {children} + +
+ ); +}; + +export default injectIntl(Plugin); + +Plugin.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, + errorFallbackProp: PropTypes.func, + intl: intlShape.isRequired, + ready: PropTypes.bool, + style: PropTypes.object, // eslint-disable-line +}; + +Plugin.defaultProps = { + className: null, + errorFallbackProp: null, + style: {}, + ready: true, +}; diff --git a/src/plugins/Plugin.test.jsx b/src/plugins/Plugin.test.jsx new file mode 100644 index 00000000..38601178 --- /dev/null +++ b/src/plugins/Plugin.test.jsx @@ -0,0 +1,219 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint react/prop-types: off */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { fireEvent } from '@testing-library/dom'; +import '@testing-library/jest-dom'; + +import { initializeMockApp } from '@edx/frontend-platform/testing'; +import { + FormattedMessage, + IntlProvider, +} from '@edx/frontend-platform/i18n'; + +import PluginContainer from './PluginContainer'; +import Plugin from './Plugin'; +import { + IFRAME_PLUGIN, PLUGIN_MOUNTED, PLUGIN_READY, PLUGIN_RESIZE, +} from './data/constants'; +import { IFRAME_FEATURE_POLICY } from './PluginContainerIframe'; + +const iframeConfig = { + url: 'http://localhost/plugin1', + type: IFRAME_PLUGIN, +}; + +// Mock ResizeObserver which is unavailable in the context of a test. +global.ResizeObserver = jest.fn(function mockResizeObserver() { + this.observe = jest.fn(); + this.disconnect = jest.fn(); +}); + +describe('PluginContainer', () => { + it('should return a blank page with a null plugin configuration', () => { + // the URL will be empty and an empty div tag will exist where the iFrame should be + // the iFrame will still take up the space assigned by the host MFE + const component = ( + + ); + + const { container } = render(component); + expect(container.firstChild).toBeNull(); + }); + + it('should render a Plugin iFrame Container when given an iFrame config', async () => { + const title = 'test plugin'; + const component = ( + Loading} /> + ); + + const { container } = render(component); + + const iframeElement = container.querySelector('iframe'); + const fallbackElement = container.querySelector('div'); + + expect(iframeElement).toBeInTheDocument(); + expect(fallbackElement).toBeInTheDocument(); + + expect(fallbackElement.innerHTML).toEqual('Loading'); + + // Ensure the iframe has the proper attributes + expect(iframeElement.attributes.getNamedItem('allow').value).toEqual(IFRAME_FEATURE_POLICY); + expect(iframeElement.attributes.getNamedItem('src').value).toEqual(iframeConfig.url); + expect(iframeElement.attributes.getNamedItem('scrolling').value).toEqual('auto'); + expect(iframeElement.attributes.getNamedItem('title').value).toEqual(title); + // The component isn't ready, since the class has 'd-none' + expect(iframeElement.attributes.getNamedItem('class').value).toEqual('border border-0 d-none'); + + jest.spyOn(iframeElement.contentWindow, 'postMessage'); + + expect(iframeElement.contentWindow.postMessage).not.toHaveBeenCalled(); + + // Dispatch a 'mounted' event manually. + const mountedEvent = new Event('message'); + mountedEvent.data = { + type: PLUGIN_MOUNTED, + }; + mountedEvent.source = iframeElement.contentWindow; + fireEvent(window, mountedEvent); + + expect(iframeElement.contentWindow.postMessage).toHaveBeenCalledWith( + { + type: PLUGIN_RESIZE, + payload: { + width: 0, // There's no width/height here in jsdom-land. + height: 0, + }, + }, + 'http://localhost/plugin1', + ); + + // Dispatch a 'ready' event manually. + const readyEvent = new Event('message'); + readyEvent.data = { + type: PLUGIN_READY, + }; + readyEvent.source = iframeElement.contentWindow; + fireEvent(window, readyEvent); + + expect(iframeElement.attributes.getNamedItem('class').value).toEqual('border border-0'); + }); +}); + +describe('Plugin', () => { + let logError = jest.fn(); + + const error = ( + + ); + + beforeEach(async () => { + // This is a gross hack to suppress error logs in the invalid parentSelector test + jest.spyOn(console, 'error'); + global.console.error.mockImplementation(() => {}); + + const { loggingService } = initializeMockApp(); + logError = loggingService.logError; + }); + + afterEach(() => { + global.console.error.mockRestore(); + jest.clearAllMocks(); + }); + + const PluginPageWrapper = ({ + params, errorFallback, ChildComponent, + }) => ( + + + + + + ); + + const ExplodingComponent = () => { + throw new Error(error); + }; + + const HealthyComponent = () => ( +
+ +
+ ); + + const errorFallbackComponent = () => ( +
+

+ +

+
+
+ ); + + it('should render children if no error', () => { + const component = ( + + ); + const { container } = render(component); + expect(container).toHaveTextContent('Hello World!'); + }); + + it('should throw an error if the child component fails', () => { + const component = ( + + ); + + render(component); + + expect(logError).toHaveBeenCalledTimes(1); + expect(logError).toHaveBeenCalledWith( + new Error(error), + expect.objectContaining({ + stack: expect.stringContaining('ExplodingComponent'), + }), + ); + }); + + it('should render the passed in fallback component when the error boundary receives a React error', () => { + const component = ( + + ); + + const { container } = render(component); + expect(container).toHaveTextContent('Oh geez'); + }); + + it('should render the default fallback component when one is not passed into the Plugin', () => { + const component = ( + + ); + + const { container } = render(component); + expect(container).toHaveTextContent('Oops! An error occurred.'); + }); +}); diff --git a/src/plugins/PluginContainer.jsx b/src/plugins/PluginContainer.jsx new file mode 100644 index 00000000..f19846b8 --- /dev/null +++ b/src/plugins/PluginContainer.jsx @@ -0,0 +1,43 @@ +'use client'; + +import React from 'react'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import PluginContainerIframe from './PluginContainerIframe'; + +import { + IFRAME_PLUGIN, +} from './data/constants'; +import { pluginConfigShape } from './data/shapes'; + +const PluginContainer = ({ config, ...props }) => { + if (config === null) { + return null; + } + + // this will allow for future plugin types to be inserted in the PluginErrorBoundary + let renderer = null; + switch (config.type) { + case IFRAME_PLUGIN: + renderer = ( + + ); + break; + // istanbul ignore next: default isn't meaningful, just satisfying linter + default: + } + + return ( + renderer + ); +}; + +export default PluginContainer; + +PluginContainer.propTypes = { + config: pluginConfigShape, +}; + +PluginContainer.defaultProps = { + config: null, +}; diff --git a/src/plugins/PluginContainerIframe.jsx b/src/plugins/PluginContainerIframe.jsx new file mode 100644 index 00000000..254197e9 --- /dev/null +++ b/src/plugins/PluginContainerIframe.jsx @@ -0,0 +1,101 @@ +import React, { + useEffect, useState, +} from 'react'; +import PropTypes from 'prop-types'; +// eslint-disable-next-line import/no-extraneous-dependencies +import classNames from 'classnames'; + +import { + PLUGIN_MOUNTED, + PLUGIN_READY, + PLUGIN_RESIZE, +} from './data/constants'; +import { + dispatchPluginEvent, + useElementSize, + usePluginEvent, +} from './data/hooks'; +import { pluginConfigShape } from './data/shapes'; + +/** + * Feature policy for iframe, allowing access to certain courseware-related media. + * + * We must use the wildcard (*) origin for each feature, as courseware content + * may be embedded in external iframes. Notably, xblock-lti-consumer is a popular + * block that iframes external course content. + + * This policy was selected in conference with the edX Security Working Group. + * Changes to it should be vetted by them (security@edx.org). + */ +export const IFRAME_FEATURE_POLICY = ( + 'fullscreen; microphone *; camera *; midi *; geolocation *; encrypted-media *' +); + +const PluginContainerIframe = ({ + config, fallback, className, ...props +}) => { + const { url } = config; + const { title, scrolling } = props; + const [mounted, setMounted] = useState(false); + const [ready, setReady] = useState(false); + + const [iframeRef, iframeElement, width, height] = useElementSize(); + + useEffect(() => { + if (mounted) { + dispatchPluginEvent(iframeElement, { + type: PLUGIN_RESIZE, + payload: { + width, + height, + }, + }, url); + } + }, [iframeElement, mounted, width, height, url]); + + usePluginEvent(iframeElement, PLUGIN_MOUNTED, () => { + setMounted(true); + }); + + usePluginEvent(iframeElement, PLUGIN_READY, () => { + setReady(true); + }); + + return ( + <> +