diff --git a/src/config/HeaderConfig.ts b/src/config/HeaderConfig.ts index 2d2c61849..ce9543929 100644 --- a/src/config/HeaderConfig.ts +++ b/src/config/HeaderConfig.ts @@ -62,7 +62,7 @@ export const HeaderSubLinks: Record = { "Model Navigator": DataCommons.map((dc) => ({ id: `model-navigator-${dc.name}`, name: `${dc.name}${dc.name.indexOf("Model") === -1 ? " Model" : ""}`, - link: `/model-navigator/${dc.name}`, + link: `/model-navigator/${dc.name}/latest`, className: "navMobileSubItem", })), diff --git a/src/content/ModelNavigator/Controller.tsx b/src/content/ModelNavigator/Controller.tsx index 012baaeab..4789b9894 100644 --- a/src/content/ModelNavigator/Controller.tsx +++ b/src/content/ModelNavigator/Controller.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useParams } from "react-router-dom"; +import { Navigate, useParams } from "react-router-dom"; import NavigatorView from "./NavigatorView"; import ErrorBoundary from "../../components/ErrorBoundary"; import { DataCommonProvider } from "../../components/Contexts/DataCommonContext"; @@ -8,12 +8,15 @@ import usePageTitle from "../../hooks/usePageTitle"; const ModelNavigatorController: React.FC = () => { usePageTitle("Model Navigator"); - const { dataCommon } = useParams<{ dataCommon: DataCommon["name"] }>(); + const { model, version } = useParams<{ model: string; version?: string }>(); + if (!version) { + return ; + } return ( - + - + ); diff --git a/src/content/ModelNavigator/NavigatorView.tsx b/src/content/ModelNavigator/NavigatorView.tsx index a68289913..5336e757e 100644 --- a/src/content/ModelNavigator/NavigatorView.tsx +++ b/src/content/ModelNavigator/NavigatorView.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import { FC } from "react"; import { Box } from "@mui/material"; // eslint-disable-next-line import/no-extraneous-dependencies -- Required to use legacy version from DMN import { Provider } from "react-redux"; @@ -7,6 +7,13 @@ import SuspenseLoader from "../../components/SuspenseLoader"; import { Status, useDataCommonContext } from "../../components/Contexts/DataCommonContext"; import useBuildReduxStore from "../../hooks/useBuildReduxStore"; +type ModelNavigatorProps = { + /** + * The version of the model to display + */ + version: string; +}; + /** * Encapsulates the Data Model Navigator component * @@ -15,9 +22,9 @@ import useBuildReduxStore from "../../hooks/useBuildReduxStore"; * - Building the Redux store for the Data Model Navigator * - Rendering the Data Model Navigator * - * @returns {JSX.Element} + * @returns The Model Navigator view */ -const ModelNavigator: FC = () => { +const ModelNavigator: FC = ({ version = "latest" }) => { const { status, DataCommon } = useDataCommonContext(); const [{ status: buildStatus, store }, , populate] = useBuildReduxStore(); @@ -26,12 +33,12 @@ const ModelNavigator: FC = () => { } if (status === Status.LOADED && buildStatus === "waiting") { - populate(DataCommon); + populate(DataCommon, version); return ; } if (!DataCommon || status === Status.ERROR || buildStatus === "error") { - throw new Error("Unable to build Model Navigator for the selected Data Common"); + throw new Error("Oops! Unable to show the requested data model or model version."); } return ( diff --git a/src/hooks/useBuildReduxStore.ts b/src/hooks/useBuildReduxStore.ts index 463fe4d65..6829d371f 100644 --- a/src/hooks/useBuildReduxStore.ts +++ b/src/hooks/useBuildReduxStore.ts @@ -23,7 +23,7 @@ export type ReduxStoreStatus = "waiting" | "loading" | "error" | "success"; export type ReduxStoreResult = [ { status: ReduxStoreStatus; store: Store }, () => void, - (assets: DataCommon) => void, + (assets: DataCommon, modelVersion: string) => void, ]; const makeStore = (): Store => { @@ -70,8 +70,9 @@ const useBuildReduxStore = (): ReduxStoreResult => { * Injects the Data Model into the store * * @param datacommon The Data Model to inject assets from + * @param modelVersion The version of the Data Model to inject */ - const populateStore = async (datacommon: DataCommon) => { + const populateStore = async (datacommon: DataCommon, modelVersion: string) => { if ( !datacommon?.name || !datacommon?.assets || @@ -84,7 +85,7 @@ const useBuildReduxStore = (): ReduxStoreResult => { setStatus("loading"); - const assets = buildAssetUrls(datacommon); + const assets = buildAssetUrls(datacommon, modelVersion); const response = await getModelExploreData(...assets.model_files)?.catch((e) => { Logger.error(e); return null; diff --git a/src/router.tsx b/src/router.tsx index 71fc39589..56c3e5ad8 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -14,7 +14,7 @@ const Login = LazyLoader(lazy(() => import("./content/Login/Controller"))); const Questionnaire = LazyLoader(lazy(() => import("./content/questionnaire/Controller"))); const DataSubmissions = LazyLoader(lazy(() => import("./content/dataSubmissions/Controller"))); const Users = LazyLoader(lazy(() => import("./content/users/Controller"))); -const DMN = LazyLoader(lazy(() => import("./content/modelNavigator/Controller"))); +const ModelNavigator = LazyLoader(lazy(() => import("./content/ModelNavigator/Controller"))); const ReleaseNotes = LazyLoader(lazy(() => import("./content/ReleaseNotes/Controller"))); const Organizations = LazyLoader(lazy(() => import("./content/organizations/Controller"))); const Studies = LazyLoader(lazy(() => import("./content/studies/Controller"))); @@ -37,8 +37,8 @@ const routes: RouteObject[] = [ element: , }, { - path: "/model-navigator/:dataCommon", - element: , + path: "/model-navigator/:model/:version?", + element: , }, { path: "/release-notes", diff --git a/src/types/DataCommon.d.ts b/src/types/DataCommon.d.ts index 6dd3f368a..6b453930a 100644 --- a/src/types/DataCommon.d.ts +++ b/src/types/DataCommon.d.ts @@ -84,5 +84,5 @@ type ManifestAssets = { * * @example ["1.0", "1.1", "1.3"] */ - versions: string[] | number[]; + versions: string[]; }; diff --git a/src/utils/dataModelUtils.test.ts b/src/utils/dataModelUtils.test.ts index f66650b83..4e5c67789 100644 --- a/src/utils/dataModelUtils.test.ts +++ b/src/utils/dataModelUtils.test.ts @@ -118,7 +118,7 @@ describe("buildAssetUrls cases", () => { } as ManifestAssets, } as DataCommon; - const result = utils.buildAssetUrls(dc); + const result = utils.buildAssetUrls(dc, "latest"); expect(result).toEqual({ model_files: [ @@ -142,7 +142,7 @@ describe("buildAssetUrls cases", () => { } as ManifestAssets, } as DataCommon; - const result = utils.buildAssetUrls(dc); + const result = utils.buildAssetUrls(dc, "latest"); expect(result.model_files).toEqual([ `${MODEL_FILE_REPO}prod/cache/test-name/1.0/model-file`, @@ -163,7 +163,7 @@ describe("buildAssetUrls cases", () => { } as ManifestAssets, } as DataCommon; - const result = utils.buildAssetUrls(dc); + const result = utils.buildAssetUrls(dc, "latest"); expect(result.model_files).toEqual([]); }); @@ -179,7 +179,7 @@ describe("buildAssetUrls cases", () => { } as ManifestAssets, } as DataCommon; - const result = utils.buildAssetUrls(dc); + const result = utils.buildAssetUrls(dc, "latest"); expect(result.readme).toEqual(null); }); @@ -196,7 +196,7 @@ describe("buildAssetUrls cases", () => { } as ManifestAssets, } as DataCommon; - const result = utils.buildAssetUrls(dc); + const result = utils.buildAssetUrls(dc, "latest"); expect(result.navigator_icon).toEqual("genericLogo.png"); }); @@ -213,7 +213,7 @@ describe("buildAssetUrls cases", () => { } as ManifestAssets, } as DataCommon; - const result = utils.buildAssetUrls(dc); + const result = utils.buildAssetUrls(dc, "latest"); expect(result.navigator_icon).toEqual("genericLogo.png"); }); @@ -230,7 +230,7 @@ describe("buildAssetUrls cases", () => { } as ManifestAssets, } as DataCommon; - const result = utils.buildAssetUrls(dc); + const result = utils.buildAssetUrls(dc, "latest"); expect(result.navigator_icon).toEqual( `${MODEL_FILE_REPO}prod/cache/test-name/1.0/custom-logo.png` @@ -238,9 +238,9 @@ describe("buildAssetUrls cases", () => { }); it("should not throw an exception if dealing with invalid data", () => { - expect(() => utils.buildAssetUrls(null)).not.toThrow(); - expect(() => utils.buildAssetUrls({} as DataCommon)).not.toThrow(); - expect(() => utils.buildAssetUrls(undefined)).not.toThrow(); + expect(() => utils.buildAssetUrls(null, "latest")).not.toThrow(); + expect(() => utils.buildAssetUrls({} as DataCommon, "latest")).not.toThrow(); + expect(() => utils.buildAssetUrls(undefined, "latest")).not.toThrow(); }); it("should not throw an exception if `model_files` is not defined", () => { @@ -253,8 +253,40 @@ describe("buildAssetUrls cases", () => { } as ManifestAssets, } as DataCommon; - expect(() => utils.buildAssetUrls(dc)).not.toThrow(); - expect(utils.buildAssetUrls(dc)).toEqual(expect.objectContaining({ model_files: [] })); + expect(() => utils.buildAssetUrls(dc, "latest")).not.toThrow(); + expect(utils.buildAssetUrls(dc, "latest")).toEqual( + expect.objectContaining({ model_files: [] }) + ); + }); + + it("should use the provided modelVersion if it is not 'latest'", () => { + const dc: DataCommon = { + name: "test-name", + assets: { + "current-version": "1.0", + "model-files": ["model-file", "prop-file"], + "readme-file": "readme-file", + "loading-file": "loading-file-zip-name", + } as ManifestAssets, + } as DataCommon; + + const result = utils.buildAssetUrls(dc, "2.1"); + expect(result.model_files).toEqual([ + `${MODEL_FILE_REPO}prod/cache/test-name/2.1/model-file`, + `${MODEL_FILE_REPO}prod/cache/test-name/2.1/prop-file`, + ]); + + const result2 = utils.buildAssetUrls(dc, "1.0"); + expect(result2.model_files).toEqual([ + `${MODEL_FILE_REPO}prod/cache/test-name/1.0/model-file`, + `${MODEL_FILE_REPO}prod/cache/test-name/1.0/prop-file`, + ]); + + const result3 = utils.buildAssetUrls(dc, "latest"); + expect(result3.model_files).toEqual([ + `${MODEL_FILE_REPO}prod/cache/test-name/1.0/model-file`, + `${MODEL_FILE_REPO}prod/cache/test-name/1.0/prop-file`, + ]); }); }); diff --git a/src/utils/dataModelUtils.ts b/src/utils/dataModelUtils.ts index 18970875d..df0ac34e7 100644 --- a/src/utils/dataModelUtils.ts +++ b/src/utils/dataModelUtils.ts @@ -31,33 +31,31 @@ export const fetchManifest = async (): Promise => { /** * Builds the asset URLs for the Data Model Navigator to import from * - * @param dc The data common to build the asset URLs for + * @param model The Data Model (DataCommon) to build asset URLs for + * @param modelVersion The version of the Data Model to build asset URLs for * @returns ModelAssetUrls */ -export const buildAssetUrls = (dc: DataCommon): ModelAssetUrls => ({ - model_files: - dc?.assets?.["model-files"]?.map( - (file) => - `${MODEL_FILE_REPO}${env.REACT_APP_DEV_TIER || "prod"}/cache/${dc?.name}/${dc?.assets?.[ - "current-version" - ]}/${file}` - ) || [], - readme: dc?.assets?.["readme-file"] - ? `${MODEL_FILE_REPO}${env.REACT_APP_DEV_TIER || "prod"}/cache/${dc?.name}/${dc?.assets?.[ - "current-version" - ]}/${dc?.assets?.["readme-file"]}` - : null, - loading_file: dc?.assets?.["loading-file"] - ? `${MODEL_FILE_REPO}${env.REACT_APP_DEV_TIER || "prod"}/cache/${dc?.name}/${dc?.assets?.[ - "current-version" - ]}/${dc?.assets?.["loading-file"]}` - : null, - navigator_icon: dc?.assets?.["model-navigator-logo"] - ? `${MODEL_FILE_REPO}${env.REACT_APP_DEV_TIER || "prod"}/cache/${dc?.name}/${dc?.assets?.[ - "current-version" - ]}/${dc?.assets?.["model-navigator-logo"]}` - : GenericModelLogo, -}); +export const buildAssetUrls = (model: DataCommon, modelVersion): ModelAssetUrls => { + const { name, assets } = model || {}; + const version = modelVersion === "latest" ? assets?.["current-version"] : modelVersion; + const tier = env.REACT_APP_DEV_TIER || "prod"; + + return { + model_files: + assets?.["model-files"]?.map( + (file) => `${MODEL_FILE_REPO}${tier}/cache/${name}/${version}/${file}` + ) || [], + readme: assets?.["readme-file"] + ? `${MODEL_FILE_REPO}${tier}/cache/${name}/${version}/${assets?.["readme-file"]}` + : null, + loading_file: assets?.["loading-file"] + ? `${MODEL_FILE_REPO}${tier}/cache/${name}/${version}/${assets?.["loading-file"]}` + : null, + navigator_icon: assets?.["model-navigator-logo"] + ? `${MODEL_FILE_REPO}${tier}/cache/${name}/${version}/${assets?.["model-navigator-logo"]}` + : GenericModelLogo, + }; +}; /** * Helper function to SAFELY build a set of base filter containers for the Data Model Navigator