Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Cross Imports #4

Merged
merged 2 commits into from
Nov 27, 2020
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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,68 @@ Make sure you have [Heroky CLI](https://devcenter.heroku.com/articles/heroku-cli
- Now you have to configure frame app to use the URL provided by Heroku like `https://<APP-NAME>.herokuapp.com/topcoder-micro-frontends-navbar-app.js` to load this microapp.
- NOTE: Authorization would not work because only predefined list of domain allowed by `accounts-app`.

### Cross microfrontend imports

This app exports functions to be imported by other microapps.
- `login` - redirects to login page
- `logout` - clears session storage and redirects to logout page
- `setAppMenu` - sets sidebar menu for the app by app's `path`
- `getAuthUserTokens` - returns a promise which resolves to object with user tokens `{ tokenV3, tokenV2 }`
- `getAuthUserProfile` - returns a promise which resolves to the user profile object

#### How to export

- To export any function we have to `export` in file [src/topcoder-micro-frontends-navbar-app.js](src/topcoder-micro-frontends-navbar-app.js).
- If we want to prepare some function for exporting, the good place to do so is inside [src/utils/exports.js](src/utils/exports.js).
- We have to bind actions before exporting.
- It's not recommended to export the whole Redux Store to keep only navbar responsible for updating it. It's better to create promises which would return some particular value from the store.

#### How to import

When we want to use methods exported in the navbar microapp in other apps we have to make sure that webpack would not process imports from navbar as it is handled by `importmaps`, see [Cross microfrontend imports](https://single-spa.js.org/docs/recommended-setup/#cross-microfrontend-imports).

##### How to import in React app

For example see https://github.com/topcoder-platform/micro-frontends-react-app

1. Add `@topcoder/micro-frontends-navbar-app` to `externals` in webpack config:
```js
externals: {
"@topcoder/micro-frontends-navbar-app": "@topcoder/micro-frontends-navbar-app",
},
```

2. As `importmaps` only work in browser and don't work in unit test, we have to mock this module in unit tests. For example by creating a file `src/__mocks__/@topcoder/micro-frontends-navbar-app.js` with the content like:
```js
module.exports = {
login: () => {},
logout: () => {},
setAppMenu: () => {},
getAuthUserTokens: () => new Promise(() => {}),
getAuthUserProfile: () => new Promise(() => {}),
};
```

##### How to import in Angular app

For example see https://github.com/topcoder-platform/micro-frontends-angular-app

1. Add `@topcoder/micro-frontends-navbar-app` to `externals` in webpack config:
```js
externals: {
"@topcoder/micro-frontends-navbar-app": "@topcoder/micro-frontends-navbar-app",
},
```

2. Add type definition in `src/typings.d.ts`:
```js
declare module '@topcoder/micro-frontends-navbar-app' {
export const login: any;
export const logout: any;
export const setAppMenu: any;
export const getAuthUserTokens: any;
export const getAuthUserProfile: any;
}
```

3. TODO: How to make e2e tests work for Angular? So far they fail with error `Module not found: Error: Can't resolve '@topcoder/micro-frontends-navbar-app'`
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"jest-cli": "^25.2.7",
"prettier": "^2.0.4",
"pretty-quick": "^2.0.1",
"redux-logger": "^3.0.6",
"single-spa-react": "^2.14.0",
"systemjs-webpack-interop": "^2.1.2",
"webpack": "^4.41.2",
Expand All @@ -46,6 +47,7 @@
},
"dependencies": {
"@reach/router": "^1.3.4",
"tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.1",
"browser-cookies": "^1.2.0",
"classnames": "^2.2.6",
"config": "^3.3.2",
Expand All @@ -56,7 +58,6 @@
"react-outside-click-handler": "^1.3.0",
"react-redux": "^7.2.1",
"react-responsive": "^8.1.0",
"redux": "^4.0.5",
"tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.3"
"redux": "^4.0.5"
}
}
27 changes: 17 additions & 10 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
/**
* Main App component
*/
import React, {useState, useCallback} from "react";
import React, { useState, useCallback, useMemo } from "react";
import _ from "lodash";
import MainMenu from "./components/MainMenu";
import NavBar from "./components/NavBar";
import { Router } from "@reach/router";
import { APPS } from "./constants";
import {
createHistory,
LocationProvider
} from "@reach/router"
import { Router, createHistory, LocationProvider } from "@reach/router";
import { useSelector } from "react-redux";

// History for location provider
let history = createHistory(window)
let history = createHistory(window);

const App = () => {
// all menu options
const menu = useSelector((state) => state.menu);
// flat list of all apps (only updated when menu updated in the Redux store)
const apps = useMemo(() => _.flatMap(menu, "apps"), [menu]);
// Left sidebar collapse state
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
// Toggle left sidebar callback
Expand All @@ -27,8 +28,14 @@ const App = () => {
<NavBar />
<div className="main-menu-wrapper">
<Router>
{APPS.map((app) => (
<MainMenu sidebarCollapsed={sidebarCollapsed} toggleSidebar={toggleSidebar} key={app.path} path={app.path + "/*"} app={app} />
{apps.map((app) => (
<MainMenu
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
key={app.path}
path={app.path + "/*"}
app={app}
/>
))}
</Router>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import auth from "./auth";
import menu from "./menu";

export default {
auth,
menu,
};
16 changes: 16 additions & 0 deletions src/actions/menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ACTIONS } from "../constants";

export default {
/**
* Set menu options for the app in the side-bar.
*
* @param {String} path app path
* @param {Array} menuOptions menu options
*
* @returns {{ type: String, payload: any }} action object
*/
setAppMenu: (path, menuOptions) => ({
type: ACTIONS.MENU.SET_APP_MENU,
payload: { path, menuOptions },
}),
};
5 changes: 3 additions & 2 deletions src/components/AllAppsMenu/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { Link } from "@reach/router";
import cn from "classnames";
import OutsideClickHandler from "react-outside-click-handler";
import AllAppsMenuIcon from "../../assets/images/all-apps-menu.svg";
import { APP_CATEGORIES } from "../../constants";
import "./styles.css";
import { useSelector } from "react-redux";

const AllAppsMenu = () => {
const menu = useSelector((state) => state.menu);
const [isOpenMenu, setIsOpenMenu] = useState(false);

const closeMenu = useCallback(() => {
Expand Down Expand Up @@ -41,7 +42,7 @@ const AllAppsMenu = () => {
<div className="all-apps-menu-popover-content">
<div className="all-apps-menu-list-title">SWITCH TOOLS</div>
<ul className="all-apps-menu-list">
{APP_CATEGORIES.map((appCategory) => (
{menu.map((appCategory) => (
<Fragment>
<div className="switch-category-title">
<div className="menu-divider"></div>
Expand Down
32 changes: 16 additions & 16 deletions src/components/NavBar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,34 @@
*
* Shows global top navigation bar with all apps menu, logo and user menu.
*/
import React, {useState, useCallback, Fragment} from "react";
import React, {useState, useCallback, Fragment, useEffect, useMemo} from "react";
import _ from 'lodash';
import UserMenu from "../UserMenu";
import AllAppsMenu from "../AllAppsMenu";
import { useSelector } from "react-redux";
import { Link, useLocation } from "@reach/router";
import TCLogo from "../../assets/images/tc-logo.svg";
import config from "../../../config";
import { getLoginUrl } from "../../utils";
import "./styles.css";
import { useMediaQuery } from "react-responsive";
import NotificationsMenu from "../NotificationsMenu";
import { useEffect } from "react";
import { APPS } from '../../constants';

const NavBar = () => {
// all menu options
const menu = useSelector((state) => state.menu);
// flat list of all apps
const apps = useMemo(() => _.flatMap(menu, "apps"), [menu]);
// Active app
const [activeApp, setActiveApp] = useState(null);
const auth = useSelector((state) => state.auth);
const loginUrl = `${config.URL.AUTH}/member?retUrl=${encodeURIComponent(
window.location.href.match(/[^?]*/)[0]
)}`;
const isMobile = useMediaQuery({
query: "(max-width: 1023px)",
});

const routerLocation = useLocation();
// Check app title with route activated
useEffect(() => {
const activeApp = APPS.find(f => routerLocation.pathname.indexOf(f.path) !== -1);
const activeApp = apps.find(f => routerLocation.pathname.indexOf(f.path) !== -1);
setActiveApp(activeApp);
}, [routerLocation])

Expand All @@ -42,11 +42,11 @@ const NavBar = () => {
return (
<div className="navbar">
<div className="navbar-left">
{isMobile ?
{isMobile ?
(
<AllAppsMenu/>
)
:
)
:
(
<Fragment>
<Link to="/">
Expand All @@ -57,7 +57,7 @@ const NavBar = () => {
</Fragment>
)
}

</div>

<div className="navbar-center">
Expand All @@ -80,7 +80,7 @@ const NavBar = () => {
</Fragment>
)
) : (
<a href={loginUrl} className="navbar-login">
<a href={getLoginUrl()} className="navbar-login">
Login
</a>
))}
Expand All @@ -98,13 +98,13 @@ const NavBar = () => {
</Fragment>
)
) : (
<a href={loginUrl} className="navbar-login">
<a href={getLoginUrl()} className="navbar-login">
Login
</a>
))}
</Fragment>
)}

</div>
</div>
);
Expand Down
17 changes: 9 additions & 8 deletions src/components/UserMenu/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React, { useState, useCallback, Fragment } from "react";
import Avatar from "../Avatar";
import cn from "classnames";
import OutsideClickHandler from "react-outside-click-handler";
import config from "../../../config";
import { logout, getLogoutUrl } from "../../utils";
import "./styles.css";
import { useMediaQuery } from "react-responsive";

Expand All @@ -17,18 +17,19 @@ const UserMenu = ({ profile }) => {
const closeMenu = useCallback(() => {
setIsOpenMenu(false);
}, [setIsOpenMenu]);

const isMobile = useMediaQuery({
query: "(max-width: 1023px)",
});

const toggleMenu = useCallback(() => {
setIsOpenMenu(!isOpenMenu);
}, [isOpenMenu, setIsOpenMenu]);

const logoutUrl = `${config.URL.AUTH}/logout?retUrl=${encodeURIComponent(
"https://" + window.location.host
)}`;

const onLogoutClick = useCallback((evt) => {
evt.preventDefault();
logout();
}, []);

return (
<OutsideClickHandler onOutsideClick={closeMenu}>
Expand All @@ -43,7 +44,7 @@ const UserMenu = ({ profile }) => {
>
<Avatar profile={profile} />
{isMobile ? (<Fragment></Fragment>) : (<div className="user-menu-handle">{profile.handle}</div>) }

</div>

{isOpenMenu && (
Expand All @@ -59,7 +60,7 @@ const UserMenu = ({ profile }) => {
<div className="user-menu-popover-content">
<ul className="user-menu-list">
<li>
<a href={logoutUrl}>Log Out</a>
<a href={getLogoutUrl()} onClick={onLogoutClick}>Log Out</a>
</li>
</ul>
</div>
Expand Down
Loading