Skip to content

Commit

Permalink
Local micro-frontend setup for alergies UI (#644)
Browse files Browse the repository at this point in the history
* Introduce a generic react micro-frontend for next gen ui and add sample code

* Simplify micro-frontend build by pulling polyfill into shared.min.js

* Fix test failures on adding new micro-frontend

* Add jest setup for mico-frontends

* Add a linter to the micro-frontend code

* Update the pipelines to support the new tools for micro-frontends
  • Loading branch information
bassoGeorge authored Jul 20, 2023
1 parent 402f1b0 commit a5043c6
Show file tree
Hide file tree
Showing 22 changed files with 2,028 additions and 76 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/build_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ jobs:
- run: npm install -g grunt-cli
- run: gem install compass
- run: npm install --global yarn
- name: Build micro-frontends
- name: micro-frontends | install dependencies
working-directory: micro-frontends
run: yarn install --frozen-lock-file && yarn build
run: yarn install --frozen-lock-file
- name: micro-frontends | test
working-directory: micro-frontends
run: yarn test:ci
- name: micro-frontends | build
working-directory: micro-frontends
run: yarn build
- name: Package
run: cd ui && yarn cache clean && /bin/bash ./scripts/package.sh
- name: Set up QEMU
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/validate_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ jobs:
- run: npm install -g grunt-cli
- run: gem install compass
- run: npm install --global yarn
- name: Build micro-frontends
- name: micro-frontends | install dependencies
working-directory: micro-frontends
run: yarn install --frozen-lock-file && yarn build
run: yarn install --frozen-lock-file
- name: micro-frontends | test
working-directory: micro-frontends
run: yarn test:ci
- name: micro-frontends | build
working-directory: micro-frontends
run: yarn build
- name: Package
run : cd ui && yarn cache clean && /bin/bash ./scripts/package.sh
37 changes: 37 additions & 0 deletions micro-frontends/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module.exports = {
root: true,
ignorePatterns: [
"node_modules/*",
"*.config.js",
".*",
"**/__mocks__/*",
"src/setupTests.js",
],
env: {
browser: true,
es2021: true,
},
extends: ["eslint:recommended", "plugin:react/recommended", "prettier"],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["react"],
rules: {},
settings: {
react: {
version: "detect",
},
},
globals: {
angular: "readonly",
},
overrides: [
{
files: ["*.spec.js", "*.spec.jsx"],
plugins: ["jest"],
extends: ["plugin:jest/recommended"],
rules: {},
},
],
};
14 changes: 11 additions & 3 deletions micro-frontends/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,18 @@ main bahmni-apps can reference the built files from there
Here is a description of all the files built

```
shared.min.js // Contains shared JS across microfrontends. should be loaded first
shared.min.css // Contains shared CSS across microfrontends including the carbon stylesheet. should be loaded first
<mfe-name>.min.js // angular module containing components from a single mfe
<mfe-name>.min.css // all the CSS for a given mfe
mfe_polyfills_angular_1_4.min.js // a polyfill required to load any <mfe-name>.min.js
```

Currently, we only have the `ipd.min.js` and `ipd.min.css`;
Currently, we have the following micro-frontends

1. `next-ui.min.js`: A local micro-fontend containing next gen react components to be used by bahmniapps
2. `ipd.min.js`: The IPD micro-frontend talking to the remote IPD repository


### Other notes
1. For every micro-frontend angular module, don't forget to add an entry in the `ui/test/__mocks__/micro-frontends.js` file for mocking it out
9 changes: 9 additions & 0 deletions micro-frontends/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
testEnvironment: "jsdom",
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/src/__mocks__/fileMock.js",
"\\.(css|scss|sass)$": "<rootDir>/src/__mocks__/styleMock.js",
},
setupFilesAfterEnv: ["<rootDir>/src/setupTests.js"],
};
14 changes: 13 additions & 1 deletion micro-frontends/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@
"@babel/preset-react": "^7.22.3",
"@testing-library/react": "12.1.5",
"babel-loader": "^9.1.2",
"babel-jest": "^29.5.0",
"eslint": "^8.42.0",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-config-prettier": "^8.8.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"jest-fetch-mock": "^3.0.3",
"mini-css-extract-plugin": "^2.7.6",
"prettier": "^2.8.8",
"resolve-url-loader": "^3.1.0",
"webpack": "^5.86.0",
"webpack-cli": "^5.1.4",
Expand All @@ -34,6 +43,9 @@
"react2angular": "^4.0.6"
},
"scripts": {
"build": "webpack --mode production"
"build": "webpack --mode production",
"test": "jest",
"test:ci": "yarn test --ci",
"lint": "eslint src/**/*.{js,jsx}"
}
}
1 change: 1 addition & 0 deletions micro-frontends/src/__mocks__/fileMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = "test-file-stub";
1 change: 1 addition & 0 deletions micro-frontends/src/__mocks__/styleMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
3 changes: 1 addition & 2 deletions micro-frontends/src/ipd/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { react2angular } from "react2angular";
import { IpdDashboard } from "./IpdDashboard";
import { DrugChartModal } from "./DrugChartModal";
import "bahmni-carbon-ui/styles.css";

angular.module("bahmni.mfe.ipd", [
"ui.router",
Expand Down Expand Up @@ -35,7 +34,7 @@ function ipdDashboardController($rootScope, $scope, confirmBox) {

// Use hostApi to provide callbacks to the micro-frontend component
$scope.hostApi = {
onConfirm(event) {
onConfirm() {
const dialogScope = {
message:
"This is a dialog triggered on the host in response to an event from IPD ",
Expand Down
9 changes: 9 additions & 0 deletions micro-frontends/src/next-ui/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { react2angular } from "react2angular";
import { PatientAlergiesControl } from "./patientAlergies/PatientAlergiesControl";

const moduleName = "bahmni.mfe.nextUi";
angular.module(moduleName, []);

angular
.module(moduleName)
.component("mfeNextUiPatientAlergiesControl", react2angular(PatientAlergiesControl));
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'carbon-components-react';

/** NOTE: for reasons known only to react2angular,
* any function nested inside one of the prop objects will be undefined at first and then later load up
* so you need to use the conditional operator like props.hostApi?.callback even though it is a mandatory prop
*/

export function PatientAlergiesControl(props) {
return (
<div>
<span>Displaying alergy control from {props.hostData.name}</span>
<Button onClick={props.hostApi?.callback}>Click for callback</Button>
</div>
);
}

PatientAlergiesControl.propTypes = {
hostData: PropTypes.shape({
name: PropTypes.string.isRequired,
}),
hostApi: PropTypes.shape({
callback: PropTypes.func.isRequired,
}),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import { PatientAlergiesControl } from "./PatientAlergiesControl";

it("works as a dummy test", () => {
const hostData = { name: "__test_name__" };
const hostApi = { callback: jest.fn() };
render(<PatientAlergiesControl hostData={hostData} hostApi={hostApi}/>);

expect(screen.getByText('Displaying alergy control from __test_name__')).toBeTruthy();

const button = screen.getByRole('button', { name: 'Click for callback' });
expect(button).toBeTruthy();
fireEvent.click(button);

expect(hostApi.callback).toHaveBeenCalled();

});
2 changes: 2 additions & 0 deletions micro-frontends/src/setupTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// mock the fetch global using jest
require("jest-fetch-mock").enableMocks();
5 changes: 5 additions & 0 deletions micro-frontends/src/shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// polyfill the angular.module.component function for angular v1.4
import 'angular-component';

// import the carbon-ui's stylesheet
import "bahmni-carbon-ui/styles.css";
18 changes: 9 additions & 9 deletions micro-frontends/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ module.exports = {
extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
alias: {
react: path.resolve(__dirname, "./src/__mocks__/globalReact.js"),
'react-dom': path.resolve(__dirname, "./src/__mocks__/globalReactDom.js"),
}
"react-dom": path.resolve(__dirname, "./src/__mocks__/globalReactDom.js"),
},
},
entry: {
ipd: "./src/ipd/index.js",
mfe_polyfills_angular_1_4: "./src/polyfill.js",
"next-ui": "./src/next-ui/index.js",
shared: "./src/shared.js",
},
output: {
path: path.resolve(__dirname, "../ui/app/micro-frontends-dist"),
Expand All @@ -31,7 +32,7 @@ module.exports = {
name: "bahmni_mfe_host",
filename: "remoteEntry.js",
remotes: {
"@openmrs-mf/ipd": dynamicRemote('bahmni_ipd', 'ipd'),
"@openmrs-mf/ipd": dynamicRemote("bahmni_ipd", "ipd"),
},
exposes: {},
shared: {
Expand Down Expand Up @@ -96,15 +97,14 @@ module.exports = {
},
};


/**
* An alternative to providing build time URLs
* We need to do this string promise stuff because we need to resolve the host at run-time.
* This is the way, as documented here: https://webpack.js.org/concepts/module-federation/#promise-based-dynamic-remotes
*
*
* @param {string} name The name of the remote, as given in it's ModuleFederationPlugin configuration
* @param {string} subPath The sub-path of the remote, as set-up in the proxy configuration
*
*
* @returns {string} A string that can be evaluated to a promise that resolves to the remote
*/
function dynamicRemote(name, subPath) {
Expand Down Expand Up @@ -132,5 +132,5 @@ function dynamicRemote(name, subPath) {
}
// inject this script with the src set to the resolved remoteEntry.js
document.head.appendChild(script);
})`
}
})`;
}
Loading

0 comments on commit a5043c6

Please sign in to comment.