diff --git a/client/packages/components/src/components/app-card/components/FavoriteCard.tsx b/client/packages/components/src/components/app-card/components/FavoriteCard.tsx index f1b66a642..9ea71e455 100644 --- a/client/packages/components/src/components/app-card/components/FavoriteCard.tsx +++ b/client/packages/components/src/components/app-card/components/FavoriteCard.tsx @@ -76,7 +76,7 @@ export const FavoriteCard = ({ app, onClick, loading, colorsStyle }: FavoriteCar return ( onClick(app)} > diff --git a/client/packages/components/src/components/app-group/AppGroup.tsx b/client/packages/components/src/components/app-group/AppGroup.tsx index 73d3d40e1..e79b87b6e 100644 --- a/client/packages/components/src/components/app-group/AppGroup.tsx +++ b/client/packages/components/src/components/app-group/AppGroup.tsx @@ -66,7 +66,7 @@ type AppGroupProps = { export const AppGroup = ({ group, onFavorite, dark, onClick }: AppGroupProps) => { const { appKey } = useParams(); - const isGroupActive = !!group.apps.find((a) => a.key === appKey); + const isGroupActive = !!group.apps.find((a) => a.appKey === appKey); return ( @@ -81,7 +81,7 @@ export const AppGroup = ({ group, onFavorite, dark, onClick }: AppGroupProps) => {group.apps.map((app) => ( { const alteredAsDropdownItems = apps ? apps.map((app) => { return { - id: app.key, - title: app.name, + id: app.appKey, + title: app.displayName, subTitle: app.category?.name, graphic: getSearchAppIcon(app), graphicType: 'inline-svg', diff --git a/client/packages/core/src/app/hooks/use-app-loader.ts b/client/packages/core/src/app/hooks/use-app-loader.ts index 28d768041..c46b45f98 100644 --- a/client/packages/core/src/app/hooks/use-app-loader.ts +++ b/client/packages/core/src/app/hooks/use-app-loader.ts @@ -11,7 +11,13 @@ import { AppConfig } from '@equinor/fusion-framework-app'; import { ConfigEnvironment } from '@equinor/fusion-framework-module-app'; import { Client } from '@portal/types'; -export const useAppLoader = (appKey: string) => { +// Todo Move to utils naming of baseName +function cleanBaseName(baseName?: string): string | undefined { + return `/${baseName?.replace('/*', '')}`; +} + +export const useAppLoader = (args: { appKey: string; baseName?: string }) => { + const { appKey } = args; const [loading, setLoading] = useState(false); const [error, setError] = useState(); @@ -33,7 +39,7 @@ export const useAppLoader = (appKey: string) => { // Generate basename for application regex extracts /apps/:appKey const [basename] = window.location.pathname.match(/\/?apps\/[a-z|-]+\//g) ?? [ - window.location.pathname, + cleanBaseName(args.baseName) ?? window.location.pathname, ]; try { @@ -90,7 +96,6 @@ export const useAppLoader = (appKey: string) => { } } catch (error) { console.error('App loading Error: ', error); - setError(error as Error); } }, diff --git a/client/packages/core/src/modules/portal-config/hooks/use-current-app-group.ts b/client/packages/core/src/modules/portal-config/hooks/use-current-app-group.ts index 619eaabf6..ed02d1abd 100644 --- a/client/packages/core/src/modules/portal-config/hooks/use-current-app-group.ts +++ b/client/packages/core/src/modules/portal-config/hooks/use-current-app-group.ts @@ -8,7 +8,7 @@ export const useCurrentAppGroup = (key?: string) => { const { appCategories, isLoading } = usePortalApps(); const currentAppGroup = useMemo(() => { - const nextAppGroup = appCategories?.find((app) => !!app.apps?.find((a) => a.key === key)); + const nextAppGroup = appCategories?.find((app) => !!app.apps?.find((a) => a.appKey === key)); return nextAppGroup ? nextAppGroup : undefined; }, [key, appCategories]); diff --git a/client/packages/core/src/modules/portal-config/hooks/use-favorites.ts b/client/packages/core/src/modules/portal-config/hooks/use-favorites.ts index 1664be450..abd813b16 100644 --- a/client/packages/core/src/modules/portal-config/hooks/use-favorites.ts +++ b/client/packages/core/src/modules/portal-config/hooks/use-favorites.ts @@ -15,9 +15,10 @@ export const useFavorites = () => { const favorite$ = useMemo( () => - combineLatest([app?.getAppManifests(), menuFavoritesController.favorites$]).pipe( - map(([apps, favorites]) => apps.filter((app) => favorites.includes(app.key ?? ''))) - ), + combineLatest([ + app?.getAppManifests({ filterByCurrentUser: true }), + menuFavoritesController.favorites$, + ]).pipe(map(([apps, favorites]) => apps.filter((app) => favorites.includes(app.appKey)))), [apps] ) as Observable; @@ -32,7 +33,7 @@ export const useFavorites = () => { const enabledApps = (appCategories?.map((group) => group.apps) ?? []).flat(); return getDisabledApps(enabledApps, favorites ?? []) .filter((app) => app.isDisabled) - .map((app) => app.key); + .map((app) => app.appKey); }, [appCategories, favorites]); const isPinned = useCallback( @@ -51,13 +52,13 @@ export const useFavorites = () => { ); const favoritesWithDisabled = - useMemo(() => favorites.map((p) => ({ ...p, isDisabled: isDisabled(p.key ?? '') })), [favorites, isDisabled]) || + useMemo(() => favorites.map((p) => ({ ...p, isDisabled: isDisabled(p.appKey) })), [favorites, isDisabled]) || []; const appGroupsWithPinned = useMemo(() => { return (appCategories || []).map((group) => ({ ...group, - apps: group.apps.map((app) => ({ ...app, isPinned: isPinned(app.key ?? '') })), + apps: group.apps.map((app) => ({ ...app, isPinned: isPinned(app.appKey) })), })) as AppCategory[]; }, [isPinned, appCategories]); diff --git a/client/packages/core/src/modules/portal-config/hooks/use-portal-config.ts b/client/packages/core/src/modules/portal-config/hooks/use-portal-config.ts index 3cfc2a77d..cb1e08cae 100644 --- a/client/packages/core/src/modules/portal-config/hooks/use-portal-config.ts +++ b/client/packages/core/src/modules/portal-config/hooks/use-portal-config.ts @@ -1,73 +1,19 @@ import { useFramework } from '@equinor/fusion-framework-react'; import { PortalConfig } from '../module'; -import { useQuery } from 'react-query'; - import { useObservableState } from '@equinor/fusion-observable/react'; -import { useAppProvider } from '@equinor/fusion-framework-react/app'; -import { useMemo, useRef } from 'react'; -import { combineLatest, combineLatestAll, combineLatestWith, filter, firstValueFrom, map, merge } from 'rxjs'; -import { IContextProvider } from '@equinor/fusion-framework-module-context'; -import { IPortalConfigProvider } from '../provider'; -import { AppModule } from '@equinor/fusion-framework-module-app'; -import { AppManifest } from '../types'; - -export const usePortalConfig2 = () => { - const { portalConfig, context, app } = useFramework<[PortalConfig, AppModule]>().modules; - - const portal = useObservableState(portalConfig.state$).value?.portal; +import { useMemo } from 'react'; +import { combineLatestWith, map } from 'rxjs'; - const { value, complete, error } = useObservableState( - useMemo( - () => - app.getAppManifests({ filterByCurrentUser: true }).pipe( - combineLatestWith(portalConfig.getApps$({ contextId: context.currentContext?.id })), - map(([apps, portalAppKeys]) => apps.filter((app) => portalAppKeys.includes(app.appKey))) - ), - [context.currentContext?.id] - ) - ); - - return { - portal, - apps: { - apps: value, - error, - isLoading: !complete, - }, - - queryRoutes: useQuery({ - queryFn: async () => await portalConfig.getRoutesAsync(), - queryKey: ['portal', 'routes'], - }), - queryPortal: useQuery({ - queryFn: async () => await portalConfig.getPortalAsync(), - queryKey: ['portal'], - }), - // queryApps: useQuery({ - // queryFn: async () => { - // return await firstValueFrom( - // app.getAppManifests({ filterByCurrentUser: true }).pipe( - // combineLatestWith(portalConfig.getApps$({ contextId: context.currentContext?.id })), - // map(([apps, portalAppKeys]) => apps.filter((app) => portalAppKeys.includes(app.appKey))) - // ) - // ); - // }, - // queryKey: ['portal', 'apps', context.currentContext?.id || 'app-portal'], - // enabled: Boolean( - // (contextsActivated && context.currentContext && portal?.id) || - // (contextsActivated === false && portal?.id) - // ), - // }), - }; -}; +import { AppModule } from '@equinor/fusion-framework-module-app'; export const usePortalConfig = () => { const { portalConfig } = useFramework<[PortalConfig]>().modules; - const { value, error, complete } = useObservableState(portalConfig.state$); + const { value, error, complete } = useObservableState(useMemo(() => portalConfig.portal$, [portalConfig])); + return { - portal: value?.portal, + portal: value, error, isLoading: !complete, }; @@ -94,7 +40,7 @@ export const usePortalAppsConfig = () => { }; }; -export const usePortalRouterConfig = () => { +export const usePortalRouter = () => { const { portalConfig } = useFramework<[PortalConfig]>().modules; const { value, error, complete } = useObservableState(useMemo(() => portalConfig.routes$, [portalConfig])); diff --git a/client/packages/core/src/modules/portal-config/provider.ts b/client/packages/core/src/modules/portal-config/provider.ts index 0fb8fc520..aeceb460c 100644 --- a/client/packages/core/src/modules/portal-config/provider.ts +++ b/client/packages/core/src/modules/portal-config/provider.ts @@ -1,14 +1,14 @@ import { - AppManifest, Extensions, GetAppsParameters, GetPortalParameters, Portal, PortalConfiguration, + PortalRequest, PortalRouter, PortalState, } from './types'; -import { Observable, OperatorFunction, Subscription, catchError, filter, firstValueFrom, map } from 'rxjs'; +import { Observable, OperatorFunction, Subscription, catchError, filter, firstValueFrom, from, map, take } from 'rxjs'; import { Query } from '@equinor/fusion-query'; import { PortalLoadError } from './errors/portal'; import { HttpResponseError } from '@equinor/fusion-framework-module-http'; @@ -18,15 +18,11 @@ import { createState } from './state/create-state'; import { AppsLoadError } from './errors/apps'; export interface IPortalConfigProvider { - getPortalById$(portalId: string): Observable; - getPortalStateAsync(): Promise; - getPortalAsync(): Promise; - getRoutesAsync(): Promise; - getAppsAsync(arg?: { contextId?: string }): Promise; + getPortalConfigById$(portalId: string): Observable; getApps$(arg?: { contextId?: string }): Observable; - initialize(): Promise; state: PortalState; state$: Observable; + portal$: Observable; routes$: Observable; apps$: Observable; complete(): void; @@ -53,7 +49,8 @@ export class PortalConfigProvider implements IPortalConfigProvider { get routes$(): Observable { return this.#state.pipe( map(({ routes }) => routes), - filterEmpty() + filterEmpty(), + take(1) ); } @@ -74,7 +71,8 @@ export class PortalConfigProvider implements IPortalConfigProvider { get portal$(): Observable { return this.#state.pipe( map(({ portal }) => portal), - filterEmpty() + filterEmpty(), + take(1) ); } @@ -91,54 +89,23 @@ export class PortalConfigProvider implements IPortalConfigProvider { public getApps$ = (args?: { contextId?: string }): Observable => { if (args?.contextId) { - return this.getAppsByContextId$(this.#state.value.portal.id, args.contextId); + return this._AppsByContext$({ portalId: this.#state.value.portal.id, contextId: args.contextId }); } return this.apps$; }; - public getPortalStateAsync = async (): Promise => { - return await firstValueFrom(this.state$); - }; - - public getPortalAsync = async (): Promise => { - return await firstValueFrom(this.portal$); - }; - - public getRoutesAsync = async (): Promise => { - return await firstValueFrom(this.routes$); - }; - - public getExtensionsAsync = async (): Promise => { - return await firstValueFrom(this.routes$); - }; - - public getAppsAsync = async (args?: { contextId?: string }): Promise => { - if (args?.contextId) { - return await firstValueFrom(this.getAppsByContextId$(this.#state.value.portal.id, args.contextId)); - } - return await firstValueFrom(this.apps$); - }; + protected async initialize(): Promise { + this.#state.next(actions.fetchPortal(this.#config.base)); + } - public getPortalById$ = (portalId: string): Observable => { - if (this.#state.value.portal) { - return new Observable((sub) => sub.next(this.#state.value.portal)); + public getPortalConfigById$ = (portalId: string): Observable => { + if (this.#state.value.req) { + return new Observable((sub) => sub.next(this.#state.value.req)); } return this._getPortal$({ portalId }); }; - public getAppsByContextId$ = (portalId: string, contextId: string): Observable => { - return this._AppsByContext$({ portalId, contextId }); - }; - - public getPortalRoutesById$ = (_portalId?: string): Observable => { - return new Observable((sub) => sub.next(this.#config.portalConfig.routes)); - }; - - public async initialize(): Promise { - this.#state.next(actions.fetchPortal(this.#config.base)); - } - - protected _getPortal$(params: GetPortalParameters): Observable { + protected _getPortal$(params: GetPortalParameters): Observable { // Create a new query using the configured client const client = new Query(this.#config.client.getPortal); this.#subscription.add(() => client.complete()); diff --git a/client/packages/core/src/modules/portal-config/state/actions.ts b/client/packages/core/src/modules/portal-config/state/actions.ts index d14da75f7..930a9ccf8 100644 --- a/client/packages/core/src/modules/portal-config/state/actions.ts +++ b/client/packages/core/src/modules/portal-config/state/actions.ts @@ -1,9 +1,7 @@ import { ActionInstanceMap, ActionTypes, createAction, createAsyncAction } from '@equinor/fusion-observable'; import { PortalRequest } from '../types'; -import { AppManifest } from '@equinor/fusion-framework-module-app'; const createActions = () => ({ - /** Portal loading */ setPortal: createAction('set_portal', (portal: PortalRequest, update?: boolean) => ({ payload: portal, meta: { @@ -26,10 +24,10 @@ const createActions = () => ({ payload, meta: { update }, }), - (apps: AppManifest[]) => ({ payload: apps }), + (apps: string[]) => ({ payload: apps }), (error: unknown) => ({ payload: error }) ), - setApps: createAction('set_apps', (apps: AppManifest[], update?: boolean) => ({ + setApps: createAction('set_apps', (apps: string[], update?: boolean) => ({ payload: apps, meta: { created: Date.now(), diff --git a/client/packages/core/src/modules/portal-config/state/create-reducer.ts b/client/packages/core/src/modules/portal-config/state/create-reducer.ts index 91bc413ff..5d225ce5f 100644 --- a/client/packages/core/src/modules/portal-config/state/create-reducer.ts +++ b/client/packages/core/src/modules/portal-config/state/create-reducer.ts @@ -19,13 +19,13 @@ const portalMapper = (portalRequest: PortalRequest): Portal => ({ export const createReducer = (value: PortalStateInitial) => makeReducer({ ...value, status: new Set() } as PortalState, (builder) => { builder.addCase(actions.setPortal, (state, action) => { + state.req = action.payload; state.portal = portalMapper(action.payload); - state.apps = action.payload.apps; - if (action.payload.routes) { - state.routes = action.payload.routes; + if (action.payload.configuration.router) { + state.routes = JSON.parse(action.payload.configuration.router); } - if (action.payload.extensions) { - state.extensions = action.payload.extensions; + if (action.payload.configuration.extension) { + state.extensions = JSON.parse(action.payload.configuration.extension); } }); builder.addCase(actions.setApps, (state, action) => { diff --git a/client/packages/core/src/modules/portal-config/state/flows.ts b/client/packages/core/src/modules/portal-config/state/flows.ts index 4a458c7fe..b4d372b20 100644 --- a/client/packages/core/src/modules/portal-config/state/flows.ts +++ b/client/packages/core/src/modules/portal-config/state/flows.ts @@ -20,7 +20,7 @@ export const handleFetchPortal = meta: { update }, } = action; - const subject = from(provider.getPortalById$(portalId)).pipe( + const subject = from(provider.getPortalConfigById$(portalId)).pipe( filter((x) => !!x), share() ); @@ -50,7 +50,7 @@ export const handleFetchAppsByContext = meta: { update }, } = action; - const subject = from(provider.getAppsByContextId$(portalId, contextId)).pipe( + const subject = from(provider.getApps$({ contextId })).pipe( filter((x) => !!x), share() ); diff --git a/client/packages/core/src/modules/portal-config/types/index.ts b/client/packages/core/src/modules/portal-config/types/index.ts index 5bd76fbc3..c906551f0 100644 --- a/client/packages/core/src/modules/portal-config/types/index.ts +++ b/client/packages/core/src/modules/portal-config/types/index.ts @@ -26,8 +26,14 @@ export type PortalRequest = { subtext?: string; contexts?: ContextType[]; apps?: string[]; + // todo remove routes and extensions routes?: PortalRouter; extensions?: Extensions; + configuration: { + environment: string | null; + extension: string | null; + router: string | null; + }; }; export type PortalRouter = { @@ -109,6 +115,7 @@ export type PortalState = { routes: PortalRouter; apps?: string[]; extensions?: Extensions; + req?: PortalRequest; error?: { message: string; type: string; @@ -138,6 +145,6 @@ export type GetAppsParameters = { }; export type IClient = { - getPortal: QueryCtorOptions; + getPortal: QueryCtorOptions; getPortalApps: QueryCtorOptions; }; diff --git a/client/packages/portal-client/src/components/app-element-provider/AppElementProvider.tsx b/client/packages/portal-client/src/components/app-element-provider/AppElementProvider.tsx index fb2459bdd..29e2323fc 100644 --- a/client/packages/portal-client/src/components/app-element-provider/AppElementProvider.tsx +++ b/client/packages/portal-client/src/components/app-element-provider/AppElementProvider.tsx @@ -4,6 +4,7 @@ import { useAppLoader } from '@portal/core'; interface CurrentAppLoaderProps { appKey: string; + baseName?: string; } const Wrapper = styled.section` @@ -20,10 +21,10 @@ const StyledAppSection = styled.section` max-width: 100%; `; -export const AppElementProvider: FC> = ({ appKey, children }) => { +export const AppElementProvider: FC> = ({ appKey, baseName, children }) => { const ref = useRef(null); - const { loading, error, appRef } = useAppLoader(appKey); + const { loading, error, appRef } = useAppLoader({ appKey, baseName }); useEffect(() => { const refEl = ref.current; diff --git a/client/packages/portal-client/src/components/app-provider/AppProvider.tsx b/client/packages/portal-client/src/components/app-provider/AppProvider.tsx index 8032fadcd..75a772162 100644 --- a/client/packages/portal-client/src/components/app-provider/AppProvider.tsx +++ b/client/packages/portal-client/src/components/app-provider/AppProvider.tsx @@ -9,7 +9,7 @@ export const AppProvider = ({ hasContext, appKey }: { hasContext: boolean; appKe if (isAppAvailable(hasContext) && appKey) { return ( - + ); } diff --git a/client/packages/portal-client/src/components/portal-router/GetConfiguredRoutes.tsx b/client/packages/portal-client/src/components/portal-router/GetConfiguredRoutes.tsx new file mode 100644 index 000000000..fa04d91b8 --- /dev/null +++ b/client/packages/portal-client/src/components/portal-router/GetConfiguredRoutes.tsx @@ -0,0 +1,18 @@ +import { PortalMessagePage } from '@equinor/portal-ui'; +import { PortalRouteWithChildren } from '@portal/core'; +import { PortalPage } from './PortalPage'; + +export const getConfiguredRoutes = (routes: PortalRouteWithChildren[]) => { + return routes?.map((route) => ({ + path: route.path, + element: , + errorElement: , + children: route.children?.map((childRoute) => ({ + path: childRoute.path, + element: , + errorElement: ( + + ), + })), + })); +}; diff --git a/client/packages/portal-client/src/components/portal-router/LoadPage.tsx b/client/packages/portal-client/src/components/portal-router/LoadPage.tsx new file mode 100644 index 000000000..29fb1e2c9 --- /dev/null +++ b/client/packages/portal-client/src/components/portal-router/LoadPage.tsx @@ -0,0 +1,14 @@ +import { PortalRoute } from '@portal/core'; + +import { AppElementProvider } from '../app-element-provider/AppElementProvider'; +import { PortalMessagePage, PortalProgressLoader } from '@equinor/portal-ui'; + +export const LoadPage = ({ pageKey, messages, path }: Partial) => { + if (!pageKey) return ; + + return ( + + + + ); +}; diff --git a/client/packages/portal-client/src/components/portal-router/PortalPage.tsx b/client/packages/portal-client/src/components/portal-router/PortalPage.tsx index eb9702f28..fc1e6d44b 100644 --- a/client/packages/portal-client/src/components/portal-router/PortalPage.tsx +++ b/client/packages/portal-client/src/components/portal-router/PortalPage.tsx @@ -1,9 +1,8 @@ import { PortalRoute } from '@portal/core'; import { FacilityPage, ProjectPage, ProjectPortalPage } from '@equinor/portal-pages'; -import { AppElementProvider } from '../app-element-provider/AppElementProvider'; -import { PortalMessagePage, PortalProgressLoader } from '@equinor/portal-ui'; +import { LoadPage } from './LoadPage'; -export const PortalPage = (prop: { route?: Omit }) => { +export const PortalPage = (prop: { route?: Partial }) => { switch (prop.route?.pageKey) { case 'project-portal': return ; @@ -12,15 +11,6 @@ export const PortalPage = (prop: { route?: Omit }) => { case 'facility': return ; default: - return ; + return ; } }; - -const LoadPage = ({ pageKey, message }: { pageKey?: string; message?: string }) => { - if (!pageKey) return ; - return ( - - - - ); -}; diff --git a/client/packages/portal-client/src/components/portal-router/PortalProvider.tsx b/client/packages/portal-client/src/components/portal-router/PortalProvider.tsx new file mode 100644 index 000000000..fefe4459e --- /dev/null +++ b/client/packages/portal-client/src/components/portal-router/PortalProvider.tsx @@ -0,0 +1,37 @@ +import { MessagePage, PortalProgressLoader } from '@equinor/portal-ui'; + +import { usePortalConfig, usePortalRouter } from '@portal/core'; +import PeopleResolverProvider from '@equinor/fusion-framework-react-components-people-provider'; +import { getRoutes } from './Routes'; +import { PortalRouter } from './PortalRouter'; +import { Typography } from '@equinor/eds-core-react'; + +export function PortalProvider() { + const { portal } = usePortalConfig(); + const { router, isLoading } = usePortalRouter(); + + if (isLoading) { + return ; + } + + return ( + + {router ? ( + + ) : ( +
+ + + Router configuration for {portal?.name} is not provided. + + + Portal could not be configured properly, please contact portal administrator + + +
+ )} +
+ ); +} + +export default PortalProvider; diff --git a/client/packages/portal-client/src/components/portal-router/PortalRouter.tsx b/client/packages/portal-client/src/components/portal-router/PortalRouter.tsx index 510e263ee..e15f2453c 100644 --- a/client/packages/portal-client/src/components/portal-router/PortalRouter.tsx +++ b/client/packages/portal-client/src/components/portal-router/PortalRouter.tsx @@ -1,102 +1,10 @@ +import { useMemo } from 'react'; +import { RouteObject, RouterProvider } from 'react-router-dom'; import { useFramework } from '@equinor/fusion-framework-react'; - -import { PortalMessagePage, PortalProgressLoader } from '@equinor/portal-ui'; import { NavigationModule } from '@equinor/fusion-framework-module-navigation'; -import { useMemo } from 'react'; -import { Navigate, RouteObject, RouterProvider } from 'react-router-dom'; - -import { PortalFrame } from '../portal-frame/PortalFrame'; -import { AppPage } from '../../pages/AppPage/AppPage'; -import { PortalRouter as PortalRouterType, usePortalConfig, usePortalRouterConfig } from '@portal/core'; -import { PortalPage } from './PortalPage'; -import PeopleResolverProvider from '@equinor/fusion-framework-react-components-people-provider'; - -const routes = (portalRoutes: PortalRouterType | undefined): RouteObject[] => { - const pages = - portalRoutes?.routes?.map((route) => ({ - path: route.path, - element: , - errorElement: ( - - ), - children: route.children?.map((childRoute) => ({ - path: childRoute.path, - element: , - errorElement: ( - - ), - })), - })) || []; - - return [ - { - path: '/', - element: , - errorElement: , - children: [ - { - path: '/', - element: , - errorElement: , - }, - ...pages, - { - path: '/apps/:appKey', - element: , - children: [ - { - path: ':contextId/*', - element: , - errorElement: , - }, - ], - errorElement: , - }, - { - path: `/aka/*`, - element: , - errorElement: , - }, - { - path: `/*`, - element: , - errorElement: , - }, - ], - }, - ]; -}; -function PortalRouter({ routes }: { routes: RouteObject[] }) { +export function PortalRouter({ routes }: { routes: RouteObject[] }) { const { navigation } = useFramework<[NavigationModule]>().modules; const router = useMemo(() => navigation.createRouter(routes), []); return ; } - -export function PortalProvider() { - const { router: portalRoutes, isLoading: routesLoading } = usePortalRouterConfig(); - const { portal, isLoading: portalLoading } = usePortalConfig(); - console.log(routesLoading, portalLoading); - - if (routesLoading || portalLoading) { - return ; - } - - return ( - - {portalRoutes ? ( - - ) : ( - <> -

Could not find configuration for {portal?.name}

-

Portal could not be configured

- - )} -
- ); -} - -export default PortalProvider; diff --git a/client/packages/portal-client/src/components/portal-router/Routes.tsx b/client/packages/portal-client/src/components/portal-router/Routes.tsx new file mode 100644 index 000000000..633470059 --- /dev/null +++ b/client/packages/portal-client/src/components/portal-router/Routes.tsx @@ -0,0 +1,50 @@ +import { PortalMessagePage } from '@equinor/portal-ui'; +import { Navigate, RouteObject } from 'react-router-dom'; + +import { PortalFrame } from '../portal-frame/PortalFrame'; +import { AppPage } from '../../pages/AppPage/AppPage'; +import { PortalRouter } from '@portal/core'; +import { PortalPage } from './PortalPage'; +import { getConfiguredRoutes } from './GetConfiguredRoutes'; + +export const getRoutes = (portalRoutes: PortalRouter): RouteObject[] => { + const pages = getConfiguredRoutes(portalRoutes.routes); + + return [ + { + path: '/', + element: , + errorElement: , + children: [ + { + path: '/', + element: , + errorElement: , + }, + ...pages, + { + path: '/apps/:appKey', + element: , + children: [ + { + path: ':contextId/*', + element: , + errorElement: , + }, + ], + errorElement: , + }, + { + path: `/aka/*`, + element: , + errorElement: , + }, + { + path: `/*`, + element: , + errorElement: , + }, + ], + }, + ]; +}; diff --git a/client/packages/portal-client/src/lib/feature-logger.ts b/client/packages/portal-client/src/lib/feature-logger.ts index 0d80a52d6..d3df85460 100644 --- a/client/packages/portal-client/src/lib/feature-logger.ts +++ b/client/packages/portal-client/src/lib/feature-logger.ts @@ -57,7 +57,7 @@ const getPayloadByType = ( context?: ContextItem | null ) => { if (type === 'App selected') { - return JSON.stringify(getSelectedAppPayload({ key: app?.key || null, name: app?.name || null })); + return JSON.stringify(getSelectedAppPayload({ key: app?.appKey || null, name: app?.displayName || null })); } else if (type === 'Context selected') { return JSON.stringify({ selectedContext: context }); } @@ -85,7 +85,7 @@ const getEntry = ( context?: ContextItem ): FeatureLogEntryRequest | undefined => { return { - appKey: app?.key || null, + appKey: app?.appKey || null, contextId: context?.id || null, feature: type, featureVersion: '0.0.1', diff --git a/client/packages/portal-client/src/lib/portal-framework-config.tsx b/client/packages/portal-client/src/lib/portal-framework-config.tsx index ecf938d06..790b55e6a 100644 --- a/client/packages/portal-client/src/lib/portal-framework-config.tsx +++ b/client/packages/portal-client/src/lib/portal-framework-config.tsx @@ -50,63 +50,6 @@ export function createPortalFramework(portalConfig: PortalConfig) { portalId: portalConfig.portalId, portalEnv: portalConfig.fusionLegacyEnvIdentifier, }); - - builder.setRoutes({ - root: { - pageKey: 'project-portal', - }, - - routes: [ - { - path: 'project/*', - pageKey: 'project', - messages: { - errorMessage: 'Fail to load project page', - }, - children: [ - { - messages: { - errorMessage: 'Fail to load project page', - }, - path: ':contextId', - pageKey: 'project', - }, - ], - }, - { - path: 'facility/*', - pageKey: 'facility', - messages: { - errorMessage: 'Fail to load facility page', - }, - children: [ - { - messages: { - errorMessage: 'Fail to load facility page', - }, - path: ':contextId', - pageKey: 'facility', - }, - ], - }, - { - path: 'admin/*', - pageKey: 'portal-administration', - messages: { - errorMessage: 'Fail to load portal administration page', - }, - children: [ - { - messages: { - errorMessage: 'Fail to load portal administration page', - }, - path: ':portalId', - pageKey: 'portal-administration', - }, - ], - }, - ], - }); }); enableContext(config); @@ -268,6 +211,10 @@ export function createPortalFramework(portalConfig: PortalConfig) { config.onInitialized<[NavigationModule, TelemetryModule, AppModule, PortalConfigModule]>(async (fusion) => { new FeatureLogger(fusion); + fusion.portalConfig.portal$.subscribe((portal) => { + document.title = portal?.name || `Fusion`; + }); + // Todo: should be moved to context module fusion.portalConfig.state$.subscribe((state) => { diff --git a/client/packages/portal-client/src/main.tsx b/client/packages/portal-client/src/main.tsx index f116e4dbf..18e604a91 100644 --- a/client/packages/portal-client/src/main.tsx +++ b/client/packages/portal-client/src/main.tsx @@ -4,7 +4,7 @@ import { QueryClientProvider } from 'react-query'; import Framework from '@equinor/fusion-framework-react'; import { PortalProgressLoader } from '@equinor/portal-ui'; -import { PortalProvider } from './components/portal-router/PortalRouter'; +import { PortalProvider } from './components/portal-router/PortalProvider'; import { queryClient } from './utils/queryClient/query-client'; import { createPortalFramework } from './lib'; import { configureDebug } from '@equinor/portal-core'; @@ -15,8 +15,6 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) const portalConfig = window['_config_']; -document.title = `Project Portal | Fusion`; - /* fusion core is spamming the console form module this will remove it in production */ configureDebug(); diff --git a/client/packages/portal-core/src/context-selector/ContextSelector.tsx b/client/packages/portal-core/src/context-selector/ContextSelector.tsx index 72b36d72a..23f10e9aa 100644 --- a/client/packages/portal-core/src/context-selector/ContextSelector.tsx +++ b/client/packages/portal-core/src/context-selector/ContextSelector.tsx @@ -1,13 +1,8 @@ import FusionContextSelector, { ContextResult, ContextSelectEvent } from '@equinor/fusion-react-context-selector'; import { NavigateFunction } from 'react-router-dom'; import { useFrameworkContext } from '../hooks'; -import { getContextPageURL } from '../utils'; import { useOnboardedContexts } from '../hooks/use-onboarded-contexts'; -import { useEffect } from 'react'; -import { NavigationModule } from '@equinor/fusion-framework-module-navigation'; -import { useFramework } from '@equinor/fusion-framework-react'; - interface ContextSelectorProps { variant?: string; navigate?: NavigateFunction; @@ -15,19 +10,8 @@ interface ContextSelectorProps { export const ContextSelector = ({ variant }: ContextSelectorProps) => { const contextProvider = useFrameworkContext(); - const { modules } = useFramework<[NavigationModule]>(); const { currentContext } = useOnboardedContexts(); - useEffect(() => { - //TODO: this should be configurable! - modules.event.addEventListener('onCurrentContextChanged', (event) => { - const url = new URL(getContextPageURL(event.detail.next), location.origin); - - // Do not navigate if we are on a application route - if (location.href !== url.href && !location.href.includes('apps/')) modules.navigation.replace(url); - }); - }, []); - return ( { - test('it should return url with context', () => { - const url = getContextPageUrl('1234'); - expect(url).toEqual('/context-page/1234'); - }); - test('it should return url with no context', () => { - const url = getContextPageUrl(undefined); - expect(url).toEqual('/context-page/'); - }); -}); diff --git a/client/packages/portal-core/src/utils/utils.ts b/client/packages/portal-core/src/utils/utils.ts index 0a1551138..e4f893809 100644 --- a/client/packages/portal-core/src/utils/utils.ts +++ b/client/packages/portal-core/src/utils/utils.ts @@ -19,11 +19,6 @@ export function getPathUrl(path: string, contextId?: string): string { return `${path}/${contextId}`; } -export function getContextPageUrl(contextId?: string): string { - if (!contextId) return '/context-page/'; - return `/context-page/${contextId}`; -} - export function getContextFormUrl() { const match = window.location.pathname.match( /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/ diff --git a/client/packages/portal-pages/src/pages/facility-page/FacilityPage.tsx b/client/packages/portal-pages/src/pages/facility-page/FacilityPage.tsx index 77fd12d42..6ad3f2035 100644 --- a/client/packages/portal-pages/src/pages/facility-page/FacilityPage.tsx +++ b/client/packages/portal-pages/src/pages/facility-page/FacilityPage.tsx @@ -15,6 +15,7 @@ import InfoBox from '../sheared/components/InfoBox/InfoBox'; import { PageHeader } from '../sheared/components/header/Header'; import { platform } from '@equinor/eds-icons'; import { FacilityDetails } from './components/FacilityDetails'; +import { useNavigateOnContextChange } from '../sheared/hooks/useNavigateOnContextChange'; export const Styles = { Wrapper: styled.main` @@ -82,8 +83,8 @@ const SEARCH_PARM_TAB = 'tab'; export const FacilityPage = () => { const { contextId } = useParams(); - - const [searchParams, setSearchparams] = useSearchParams(); + useNavigateOnContextChange(); + const [_, setSearchparams] = useSearchParams(); const [activeTab, setActiveTab] = useState(0); diff --git a/client/packages/portal-pages/src/pages/project-page/ProjectPage.tsx b/client/packages/portal-pages/src/pages/project-page/ProjectPage.tsx index 82f8af187..d69fbd2eb 100644 --- a/client/packages/portal-pages/src/pages/project-page/ProjectPage.tsx +++ b/client/packages/portal-pages/src/pages/project-page/ProjectPage.tsx @@ -22,6 +22,7 @@ import { ProjectMaster } from '../sheared/types'; import { PageHeader } from '../sheared/components/header/Header'; import { ProjectDetails } from './components/ProjectDetails'; import { assignment } from '@equinor/eds-icons'; +import { useNavigateOnContextChange } from '../sheared/hooks/useNavigateOnContextChange'; export const Styles = { Wrapper: styled.main` @@ -84,6 +85,7 @@ const SEARCH_PARM_TAB = 'tab'; export const ProjectPage = () => { const { contextId } = useParams(); + useNavigateOnContextChange(); const [searchParams, setSearchparams] = useSearchParams(); const { feature } = useFrameworkFeature('cc-tab'); diff --git a/client/packages/portal-pages/src/pages/project-portal-page/ProjectPortalPage.tsx b/client/packages/portal-pages/src/pages/project-portal-page/ProjectPortalPage.tsx index 32006924a..b17429399 100644 --- a/client/packages/portal-pages/src/pages/project-portal-page/ProjectPortalPage.tsx +++ b/client/packages/portal-pages/src/pages/project-portal-page/ProjectPortalPage.tsx @@ -12,6 +12,7 @@ import { Checkbox, LinearProgress, Typography } from '@equinor/eds-core-react'; import { useState } from 'react'; import { useFeature } from '@equinor/fusion-framework-react-app/feature-flag'; +import { useNavigateOnContextChange } from '../sheared/hooks/useNavigateOnContextChange'; const styles = { contentSection: css` @@ -117,7 +118,7 @@ export const ProjectPortalPage = (): JSX.Element => { const [value, setValue] = useState(false); const { data, isLoading } = useUserOrgDetails(value); const { feature } = useFeature('project-prediction'); - + useNavigateOnContextChange(); return ( @@ -151,9 +152,7 @@ export const ProjectPortalPage = (): JSX.Element => { }} /> - - {value ? 'All' : 'Current'}{' '} projects: - + {value ? 'All' : 'Current'} projects: {isLoading ? ( diff --git a/client/packages/portal-pages/src/pages/sheared/hooks/useNavigateOnContextChange.ts b/client/packages/portal-pages/src/pages/sheared/hooks/useNavigateOnContextChange.ts new file mode 100644 index 000000000..20a310830 --- /dev/null +++ b/client/packages/portal-pages/src/pages/sheared/hooks/useNavigateOnContextChange.ts @@ -0,0 +1,15 @@ +import { NavigationModule } from '@equinor/fusion-framework-module-navigation'; +import { useFramework } from '@equinor/fusion-framework-react'; +import { getContextPageURL } from '../utils'; +import { useEffect } from 'react'; + +export const useNavigateOnContextChange = () => { + const { modules } = useFramework<[NavigationModule]>(); + useEffect(() => { + modules.event.addEventListener('onCurrentContextChanged', (event) => { + const url = new URL(getContextPageURL(event.detail.next), location.origin); + + modules.navigation.replace(url); + }); + }, []); +}; diff --git a/client/packages/portal-pages/src/pages/sheared/utils.ts b/client/packages/portal-pages/src/pages/sheared/utils.ts new file mode 100644 index 000000000..7cb69f06d --- /dev/null +++ b/client/packages/portal-pages/src/pages/sheared/utils.ts @@ -0,0 +1,15 @@ +import { ContextItem } from '@equinor/fusion-framework-module-context'; + +const CONTEXT_TYPE_TO_ROUTE_MAP: Record = { + Facility: 'Facility', + ProjectMaster: 'Project', +}; + +export function getContextTypeName(contextTypeId?: string | null) { + return contextTypeId ? CONTEXT_TYPE_TO_ROUTE_MAP[contextTypeId] || '' : ''; +} + +export function getContextPageURL(context?: ContextItem | null) { + if (!context) return `/`; + return `${getContextTypeName(context.type.id).toLowerCase()}/${context.id}`; +} diff --git a/client/packages/portal-ui/src/header/breadcrumbs/AppBreadcrumb.tsx b/client/packages/portal-ui/src/header/breadcrumbs/AppBreadcrumb.tsx index ac9f0b237..749d145e3 100644 --- a/client/packages/portal-ui/src/header/breadcrumbs/AppBreadcrumb.tsx +++ b/client/packages/portal-ui/src/header/breadcrumbs/AppBreadcrumb.tsx @@ -15,7 +15,7 @@ export const AppBreadcrumb: FC = ({ appCategory, isMenuOpen, const { appKey } = useParams(); const { dispatchEvent } = useTelemetry(); - const currentApp = appCategory?.apps.find((a) => a.key === appKey); + const currentApp = appCategory?.apps.find((a) => a.appKey === appKey); const ref = useRef(null); const hasApps = Boolean(appCategory?.apps.length); @@ -46,19 +46,19 @@ export const AppBreadcrumb: FC = ({ appCategory, isMenuOpen, {appCategory.apps.map((app) => ( { toggleMenuOpen(false); dispatchEvent( { name: 'onAppNavigation', }, - { appKey: app.key, source: 'top-bar-navigation' } + { appKey: app.appKey, source: 'top-bar-navigation' } ); }} > - {currentApp?.key === app.key ? {app.name} : app.name} + {currentApp?.appKey === app.appKey ? {app.displayName} : app.displayName} ))} diff --git a/client/packages/portal-ui/src/portal-message-page/PortalMessagePage.tsx b/client/packages/portal-ui/src/portal-message-page/PortalMessagePage.tsx index abea97577..869a529c1 100644 --- a/client/packages/portal-ui/src/portal-message-page/PortalMessagePage.tsx +++ b/client/packages/portal-ui/src/portal-message-page/PortalMessagePage.tsx @@ -5,85 +5,102 @@ import { useRouteError } from 'react-router-dom'; import styled from 'styled-components'; const StylesWrapper = styled.div` - width: 100vw; - height: 90vh; - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; - justify-content: center; + width: 100vw; + height: 90vh; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + justify-content: center; `; - const StylesContentWrapper = styled.div` - padding-top: 1rem; + padding-top: 1rem; `; - -type PortalMessageType = "Error" | "Info" | "Warning" | "NoContent" +type PortalMessageType = 'Error' | 'Info' | 'Warning' | 'NoContent'; interface PortalErrorPageProps { - title: string; - body?: React.FC | string; - type?: PortalMessageType - icon?: string; - color?: string; + title: string; + body?: React.FC | string; + type?: PortalMessageType; + icon?: string; + color?: string; } const getPortalMessageType = (type?: PortalMessageType) => { - switch (type) { - case "Error": - return { color: tokens.colors.interactive.danger__resting.hex, icon: "error_outlined" }; - case "Info": - return { color: tokens.colors.interactive.primary__resting.hex, icon: "error_outlined" }; - case "Warning": - return { color: tokens.colors.interactive.warning__resting.hex, icon: "error_outlined" }; - case "NoContent": - return { color: tokens.colors.text.static_icons__default.hex, icon: "file_description" }; - default: - return undefined; - } -} + switch (type) { + case 'Error': + return { color: tokens.colors.interactive.danger__resting.hex, icon: 'error_outlined' }; + case 'Info': + return { color: tokens.colors.interactive.primary__resting.hex, icon: 'error_outlined' }; + case 'Warning': + return { color: tokens.colors.interactive.warning__resting.hex, icon: 'error_outlined' }; + case 'NoContent': + return { color: tokens.colors.text.static_icons__default.hex, icon: 'file_description' }; + default: + return undefined; + } +}; export function PortalMessagePage({ - title, - icon = 'error_outlined', - type, - color, - children + title, + icon = 'error_outlined', + type, + color, + children, }: PropsWithChildren) { - const error = useRouteError() as Error; + const error = useRouteError() as Error; + const currentType = getPortalMessageType(type); + return ( + + + + {title} + - const currentType = getPortalMessageType(type) - return ( - - - - {title} - + {children && children} - - {children && children} - + {error && ( +
+

{error.name}

+

{error.message}

+
+ )} +
+ ); +} +export function MessagePage({ + title, + icon = 'error_outlined', + type, + color, + children, +}: PropsWithChildren) { + const currentType = getPortalMessageType(type); + return ( + + + + {title} + - { - error &&
-

- {error.name} -

-

- {error.message} -

-
- } -
- ); + {children && children} +
+ ); }