From 200dad541e7c1f602becfbad6d082ef24b719294 Mon Sep 17 00:00:00 2001 From: Julian Wielga Date: Tue, 11 Jun 2024 03:19:44 +0200 Subject: [PATCH] feat(federation): expose routes. refactor. update react-router. --- federation.config.json | 3 +- package-lock.json | 150 ++++++------------------------- package.json | 3 +- src/api.ts | 46 +++++----- src/components/navigationBar.tsx | 2 +- src/components/root.tsx | 10 +-- src/components/rootProviders.tsx | 57 +++++++----- src/components/rootView.tsx | 10 +-- src/components/routes.tsx | 100 +++++++++------------ src/remoteTab.tsx | 35 ++++++-- src/tokenStorage.ts | 11 +++ 11 files changed, 183 insertions(+), 244 deletions(-) create mode 100644 src/tokenStorage.ts diff --git a/federation.config.json b/federation.config.json index e45597b..da63999 100644 --- a/federation.config.json +++ b/federation.config.json @@ -1,6 +1,7 @@ { "name": "petasos", "exposes": { - "./remoteTab": "./src/remoteTab" + "./remoteTab": "./src/remoteTab", + "./routes": "./src/components/routes" } } diff --git a/package-lock.json b/package-lock.json index 8a3add2..9ccd805 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,9 +28,8 @@ "moment": "2.29.4", "react": "17.0.2", "react-dom": "17.0.2", - "react-helmet": "6.1.0", "react-json-tree": "0.16.2", - "react-router-dom": "6.8.1", + "react-router-dom": "6.23.1", "semantic-release": "19.0.3", "url-join": "5.0.0" }, @@ -59,7 +58,6 @@ "@types/react": "17.0.43", "@types/react-dom": "17.0.14", "@types/react-helmet": "6.1.5", - "@types/react-router-dom": "5.3.3", "@types/webpack-dev-server": "4.7.2", "@types/webpack-env": "1.16.4", "@types/webpack-merge": "5.0.0", @@ -3049,11 +3047,11 @@ } }, "node_modules/@remix-run/router": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz", - "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", "engines": { - "node": ">=14" + "node": ">=14.0.0" } }, "node_modules/@semantic-release/changelog": { @@ -3718,12 +3716,6 @@ "@types/node": "*" } }, - "node_modules/@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", - "dev": true - }, "node_modules/@types/html-minifier": { "version": "4.0.0", "dev": true, @@ -3865,26 +3857,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-router": { - "version": "5.1.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/history": "*", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dev": true, - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", @@ -15451,23 +15423,6 @@ "version": "2.0.4", "license": "MIT" }, - "node_modules/react-helmet": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "object-assign": "^4.1.1", - "prop-types": "^15.7.2", - "react-fast-compare": "^3.1.1", - "react-side-effect": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.3.0" - } - }, - "node_modules/react-helmet/node_modules/react-fast-compare": { - "version": "3.2.0", - "license": "MIT" - }, "node_modules/react-is": { "version": "16.13.1", "license": "MIT" @@ -15497,42 +15452,35 @@ } }, "node_modules/react-router": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", - "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", "dependencies": { - "@remix-run/router": "1.3.2" + "@remix-run/router": "1.16.1" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz", - "integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", "dependencies": { - "@remix-run/router": "1.3.2", - "react-router": "6.8.1" + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, - "node_modules/react-side-effect": { - "version": "2.1.1", - "license": "MIT", - "peerDependencies": { - "react": "^16.3.0 || ^17.0.0" - } - }, "node_modules/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -20586,9 +20534,9 @@ "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==" }, "@remix-run/router": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz", - "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==" + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==" }, "@semantic-release/changelog": { "version": "6.0.1", @@ -21095,12 +21043,6 @@ "@types/node": "*" } }, - "@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", - "dev": true - }, "@types/html-minifier": { "version": "4.0.0", "dev": true, @@ -21240,25 +21182,6 @@ "@types/react": "*" } }, - "@types/react-router": { - "version": "5.1.8", - "dev": true, - "requires": { - "@types/history": "*", - "@types/react": "*" - } - }, - "@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dev": true, - "requires": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, "@types/react-transition-group": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", @@ -29268,20 +29191,6 @@ "react-fast-compare": { "version": "2.0.4" }, - "react-helmet": { - "version": "6.1.0", - "requires": { - "object-assign": "^4.1.1", - "prop-types": "^15.7.2", - "react-fast-compare": "^3.1.1", - "react-side-effect": "^2.1.0" - }, - "dependencies": { - "react-fast-compare": { - "version": "3.2.0" - } - } - }, "react-is": { "version": "16.13.1" }, @@ -29303,25 +29212,22 @@ "dev": true }, "react-router": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", - "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", "requires": { - "@remix-run/router": "1.3.2" + "@remix-run/router": "1.16.1" } }, "react-router-dom": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz", - "integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", "requires": { - "@remix-run/router": "1.3.2", - "react-router": "6.8.1" + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" } }, - "react-side-effect": { - "version": "2.1.1" - }, "react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", diff --git a/package.json b/package.json index 338dff8..b3690ff 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-json-tree": "0.16.2", - "react-router-dom": "6.8.1", + "react-router-dom": "6.23.1", "semantic-release": "19.0.3", "url-join": "5.0.0" }, @@ -53,7 +53,6 @@ "@types/react": "17.0.43", "@types/react-dom": "17.0.14", "@types/react-helmet": "6.1.5", - "@types/react-router-dom": "5.3.3", "@types/webpack-dev-server": "4.7.2", "@types/webpack-env": "1.16.4", "@types/webpack-merge": "5.0.0", diff --git a/src/api.ts b/src/api.ts index 97dfd10..7fcb906 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,4 @@ -const ACCESS_TOKEN_NAMESPACE = "accessToken"; +import { tokenStorage } from "./tokenStorage"; const addHeaders = (customHeaders: Record) => @@ -14,16 +14,19 @@ const addTokenHeader = (token: string) => addHeaders({ Authorization: `Bearer ${ const withContentType = addHeaders({ "Content-Type": "application/json" }); -export const fetchSecured = async (url: string, init: RequestInit = {}): Promise => { - const storageToken = localStorage.getItem(ACCESS_TOKEN_NAMESPACE); - const queryParamToken = getQueryParameters()[ACCESS_TOKEN_NAMESPACE]; - const token = queryParamToken ? queryParamToken : storageToken; - if (queryParamToken) { - localStorage.setItem(ACCESS_TOKEN_NAMESPACE, queryParamToken); - window.history.replaceState(null, null, window.location.pathname); +const getAuthHeader = async (init: RequestInit): Promise => { + try { + const token = await tokenStorage.getToken(); + const withAuth = addTokenHeader(token); + return withAuth(init); + } catch { + return init; } - const withAuth = addTokenHeader(token); - return await fetchJson(url, withAuth(init)); +}; + +export const fetchSecured = async (url: string, init: RequestInit = {}): Promise => { + const options = await getAuthHeader(init); + return await fetchJson(url, options); }; export const fetchJson = async (url: string, init: RequestInit = {}): Promise => { @@ -43,15 +46,18 @@ export const fetchJson = async (url: string, init: RequestInit = {}): Promise return json as R; }; -export const getQueryParameters = () => { - const queryStringKeyValue = window.location.search.replace("?", "").split("&"); - return queryStringKeyValue.reduce((acc, curr) => { - const [key, value] = curr.split("="); - return { - ...acc, - [key]: value, - }; - }, {}); +export const localStorageToken = (namespace = "accessToken") => { + let token: string; + const url = new URL(window.location.href); + if (url.searchParams.has(namespace)) { + token = url.searchParams.get(namespace); + localStorage.setItem(namespace, token); + url.searchParams.delete(namespace); + window.history.replaceState(null, null, url.toString()); + } else { + token = localStorage.getItem(namespace); + } + return token ? Promise.resolve(token) : Promise.reject(); }; -export const fetchFn = Object.keys(getQueryParameters()).length ? fetchSecured : fetchJson; +export const fetchFn = fetchSecured; diff --git a/src/components/navigationBar.tsx b/src/components/navigationBar.tsx index 58b7554..bb50503 100644 --- a/src/components/navigationBar.tsx +++ b/src/components/navigationBar.tsx @@ -6,7 +6,7 @@ import { Link as RouterLink, matchPath, PathMatch, useLocation } from "react-rou import { useStore } from "../store/storeProvider"; import { LayoutRow } from "./layout"; import { LinePlaceholder } from "./linePlaceholder"; -import { RootPath } from "./routes"; +import { RootPath } from "./rootProviders"; function LinkRouter( props: PropsWithChildren<{ diff --git a/src/components/root.tsx b/src/components/root.tsx index 9acf611..789b01f 100644 --- a/src/components/root.tsx +++ b/src/components/root.tsx @@ -1,17 +1,17 @@ import { Box, CssBaseline, ThemeProvider } from "@mui/material"; import React from "react"; -import { BrowserRouter as Router } from "react-router-dom"; -import { RootRoutes } from "./routes"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { createRoutes } from "./routes"; import { theme } from "./theme"; +const router = createBrowserRouter(createRoutes()); + export const Root = () => { return ( - - - + ); diff --git a/src/components/rootProviders.tsx b/src/components/rootProviders.tsx index 05b7b22..47b7873 100644 --- a/src/components/rootProviders.tsx +++ b/src/components/rootProviders.tsx @@ -4,33 +4,50 @@ import { LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import { configure } from "mobx"; import * as React from "react"; -import { PropsWithChildren } from "react"; +import { createContext, PropsWithChildren, useEffect } from "react"; +import { Outlet } from "react-router-dom"; import { Options } from "../config"; import { StoreProvider } from "../store/storeProvider"; +import { tokenStorage } from "../tokenStorage"; import { theme } from "./theme"; configure({ enforceActions: "observed", }); -export const RootProviders = ({ children }: PropsWithChildren) => ( - - { - if (!Object.keys(outerTheme).length) { - return theme; - } +export const RootPath = createContext(null); - // Let's set aside the parent form style since we won't be using the same form style throughout the entire app. - const { - components: { MuiFormControl, MuiFormHelperText, MuiFormLabel }, - ...filteredOuterTheme - } = outerTheme; +export type RootProvidersProps = { + tokenGetter: () => Promise; + basepath?: string; +}; - return deepmerge(theme, filteredOuterTheme); - }} - > - {children} - - -); +export const RootProviders = ({ children = , tokenGetter, basepath }: PropsWithChildren) => { + useEffect(() => { + tokenStorage.replaceTokenGetter(tokenGetter); + }, [tokenGetter]); + + return ( + + + { + if (!Object.keys(outerTheme).length) { + return theme; + } + + // Let's set aside the parent form style since we won't be using the same form style throughout the entire app. + const { + components: { MuiFormControl, MuiFormHelperText, MuiFormLabel }, + ...filteredOuterTheme + } = outerTheme; + + return deepmerge(theme, filteredOuterTheme); + }} + > + {children} + + + + ); +}; diff --git a/src/components/rootView.tsx b/src/components/rootView.tsx index 1d8e4f6..d3043e1 100644 --- a/src/components/rootView.tsx +++ b/src/components/rootView.tsx @@ -1,13 +1,9 @@ -import React from "react"; +import React, { PropsWithChildren } from "react"; import { Outlet } from "react-router-dom"; import { useStore } from "../store/storeProvider"; import { EnsureFetched } from "./ensureFetched"; -export const RootView = () => { +export const RootView = ({ children = }: PropsWithChildren) => { const { topics } = useStore(); - return ( - - - - ); + return {children}; }; diff --git a/src/components/routes.tsx b/src/components/routes.tsx index ef5e2ad..6b147aa 100644 --- a/src/components/routes.tsx +++ b/src/components/routes.tsx @@ -1,71 +1,57 @@ import loadable from "@loadable/component"; import { Box, CircularProgress } from "@mui/material"; import * as React from "react"; -import { createContext } from "react"; -import { Outlet, Route, Routes, useLocation, useParams } from "react-router-dom"; +import { RouteObject } from "react-router-dom"; +import { localStorageToken } from "../api"; import { MainLayout } from "./mainLayout"; -import { RootProviders } from "./rootProviders"; +import { RootProviders, RootProvidersProps } from "./rootProviders"; import { RootView } from "./rootView"; -function RouteTester({ path }: { path?: string }) { - const params = useParams(); - const loc = useLocation(); - return ( -
-

matched {path}

-
{JSON.stringify(params)}
-
{JSON.stringify(loc)}
- -
- ); -} - -function NoMatch() { - return ( -
-

no match

- -
- ); -} - const Loading = () => ( ); -const opts = { - fallback: , -}; - -const TopicsListView = loadable(() => import("./topicsListView"), opts); -const SubscriptionView = loadable(() => import("./subscriptionView"), opts); -const TopicDetailsView = loadable(() => import("./topicDetailsView"), opts); -const TopicView = loadable(() => import("./topicView"), opts); +const RootElement = (props: RootProvidersProps) => ( + + + + + +); -export const RootPath = createContext(null); +const TopicsListView = loadable(() => import("./topicsListView"), { fallback: }); +const TopicView = loadable(() => import("./topicView"), { fallback: }); +const TopicDetailsView = loadable(() => import("./topicDetailsView"), { fallback: }); +const SubscriptionView = loadable(() => import("./subscriptionView"), { fallback: }); -export const RootRoutes = ({ basepath }: { basepath?: string }) => ( - - - - - - } - > - }> - } /> - }> - } /> - } /> - - - } /> - - - -); +export function createRoutes(props: Partial = {}): RouteObject[] { + const { basepath, tokenGetter = localStorageToken } = props; + return [ + { + path: "/", + element: , + children: [ + { + index: true, + element: , + }, + { + path: ":topic", + element: , + children: [ + { + index: true, + element: , + }, + { + path: ":subscription", + element: , + }, + ], + }, + ], + }, + ]; +} diff --git a/src/remoteTab.tsx b/src/remoteTab.tsx index 1277b51..6070ed9 100644 --- a/src/remoteTab.tsx +++ b/src/remoteTab.tsx @@ -1,15 +1,32 @@ import { Box } from "@mui/material"; -import React from "react"; -import { RootRoutes } from "./components/routes"; +import * as React from "react"; +import { Route, RouteObject, Routes } from "react-router-dom"; +import { createRoutes } from "./components/routes"; -interface RemoteComponentProps { - basepath: string; +function createElementsFromRoutes(routes: RouteObject[] = []) { + return routes.map(({ children, element, index, path }, i) => + index ? ( + + ) : ( + + {createElementsFromRoutes(children)} + + ), + ); } -const RemoteTab = ({ basepath }: RemoteComponentProps) => ( - - - -); +type RemoteTabProps = { + basepath: string; + tokenGetter?: () => Promise; +}; + +function RemoteTab({ basepath, tokenGetter }: RemoteTabProps) { + const routes = React.useMemo(() => createRoutes({ basepath, tokenGetter }), [basepath, tokenGetter]); + return ( + + {createElementsFromRoutes(routes)} + + ); +} export default RemoteTab; diff --git a/src/tokenStorage.ts b/src/tokenStorage.ts new file mode 100644 index 0000000..63e992c --- /dev/null +++ b/src/tokenStorage.ts @@ -0,0 +1,11 @@ +export class TokenStorage { + getToken = () => this.tokenGetter(); + + constructor(private tokenGetter: () => Promise = Promise.reject) {} + + replaceTokenGetter(newGetter: () => Promise): void { + this.tokenGetter = newGetter; + } +} + +export const tokenStorage = new TokenStorage();