diff --git a/.gitignore b/.gitignore index 2648984f08249b8e80bb5761b63f2af84d050ac5..1d781452cd7f043ab28e9944024cce06bb611799 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ dist-ssr .env.dev *.mjs +**/api.config.json + # Editor directories and files .idea .DS_Store diff --git a/apps/dataset_viewer/index.html b/apps/dataset_viewer/index.html index e4b78eae12304a075fa19675c4047061d6ab920d..c98b5a0e17a1125f4050417bf1c05d3ec26f897b 100644 --- a/apps/dataset_viewer/index.html +++ b/apps/dataset_viewer/index.html @@ -4,7 +4,7 @@ <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>Vite + React + TS</title> + <title>Dataset Viewer</title> </head> <body> <div id="root"></div> diff --git a/apps/dataset_viewer/package.json b/apps/dataset_viewer/package.json index 46127ab4fb0ccd28fad967d204ca32f1b1fe779d..540b08192ff0c57560b74cfbf8ff3edf76078841 100644 --- a/apps/dataset_viewer/package.json +++ b/apps/dataset_viewer/package.json @@ -13,7 +13,8 @@ "lint:tsc": "tsc", "lint:prettier": "prettier . --check", "fix:eslint": "eslint \"**/*.{js,cjs,ts,tsx}\" --fix", - "fix:prettier": "prettier . --write" + "fix:prettier": "prettier . --write", + "configure:standalone": "cp public/config/api.config.json.example public/config/api.config.json" }, "dependencies": { "@babel/eslint-parser": "^7.25.1", @@ -23,6 +24,9 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", "@tanstack/react-query": "^5.52.1", + "ajv": "^8.17.1", + "bootstrap": "^5.3.3", + "bootswatch": "^5.3.3", "lodash": "^4.17.21", "react": "^18.3.1", "react-bootstrap": "^2.10.4", diff --git a/apps/dataset_viewer/public/config/api.config.json.example b/apps/dataset_viewer/public/config/api.config.json.example new file mode 100644 index 0000000000000000000000000000000000000000..fa5cfdd9063ff7a993a82fd05574f4fdf8f65ff4 --- /dev/null +++ b/apps/dataset_viewer/public/config/api.config.json.example @@ -0,0 +1,35 @@ +{ + "icat_url": "https://icatplus.esrf.fr/", + "authentication": { + "anonymous": { + "plugin": "db", + "username": "reader", + "password": "reader" + }, + "autoRefresh": true, + "autoRefreshThresholdMinutes": "60", + "authenticators": [ + { + "name": "OpenID", + "enabled": true, + "title": "ESRF Single Sign On", + "plugin": "esrf", + "message": "Login with ESRF SSO", + "openidLogoutTransferToApp": true, + "refreshToken": true, + "minValidity": 600, + "configuration": { + "authority": "https://websso.esrf.fr/auth/realms/ESRF", + "clientId": "icat" + } + }, + { + "name": "Database", + "title": "Database", + "enabled": true, + "message": "", + "plugin": "db" + } + ] + } +} diff --git a/apps/dataset_viewer/public/config/dataset.viewer.config.json b/apps/dataset_viewer/public/config/dataset.viewer.config.json new file mode 100644 index 0000000000000000000000000000000000000000..64e0a6c3b841cbdf8274c7c7235527caae1c2cca --- /dev/null +++ b/apps/dataset_viewer/public/config/dataset.viewer.config.json @@ -0,0 +1,149 @@ +[ + { + "beamline": "CM01", + "render": { + "details": { + "type": "local", + "remote": { + "name": "cryoet", + "component": "./CryoETDatasetViewer" + } + } + } + }, + { + "beamline": "BM29", + "datasetParameter": { + "SAXS_experiment_type": ["hplc", "HPLC"] + }, + "render": { + "details": { + "type": "local", + "remote": { + "name": "remoteDatasetViewerApp", + "component": "SAXSHPLCDatasetDetails" + } + }, + "snapshot": { + "type": "local", + "remote": { + "name": "remoteDatasetViewerApp", + "component": "SAXSHPLCDatasetSnapshot" + } + } + } + }, + { + "beamline": "BM29", + "datasetParameter": { + "SAXS_experiment_type": ["sampleChanger", "sample-changer"] + }, + "render": { + "details": { + "type": "local", + "remote": { + "name": "remoteDatasetViewerApp", + "component": "SAXSSampleChangerDatasetDetails" + } + }, + "snapshot": { + "type": "local", + "remote": { + "name": "remoteDatasetViewerApp", + "component": "SAXSSampleChangerDatasetSnapshot" + } + } + } + }, + { + "beamline": "BM29", + "render": { + "details": { + "type": "local", + "remote": { + "name": "remoteDatasetViewerApp", + "component": "SAXSSampleChangerDatasetDetails" + } + }, + "snapshot": { + "type": "local", + "remote": { + "name": "remoteDatasetViewerApp", + "component": "SAXSSampleChangerDatasetSnapshot" + } + } + } + }, + { + "beamline": "ID31", + "render": { + "details": { + "type": "local", + "remote": { + "name": "remoteDatasetViewerApp", + "component": "HTXRPDDataset" + } + } + } + }, + { + "projectName": "The Human Organ Atlas", + "render": { + "details": { + "type": "local", + "remote": { + "name": "remoteDatasetViewerApp", + "component": "./HumanOrganAtlasDataset" + } + } + } + }, + { + "projectName": "paleo", + "render": { + "details": { + "type": "remote", + "remote": { + "name": "remoteDatasetViewerApp", + "component": "./PaleoDataset" + } + } + } + }, + { + "beamline": ["ID24", "ID24-DCM", "ID24-ED"], + "datasetParameter": { + "InstrumentLaser01_energy": null + }, + "render": { + "details": { + "type": "local", + "remote": { + "name": "XASDatasetDetail", + "component": "XASDatasetDetail" + } + }, + "tableCell": { + "type": "local", + "remote": { + "name": "XASDatasetTableCell", + "component": "XASDatasetTableCell" + } + } + } + }, + { + "technique": "TOMO", + "date": "01/01/2022", + "beamline": "BM05", + "render": { + "details": { + "type": "local", + "remote": { + "name": "TOMODataset", + "component": "TOMODataset" + } + } + } + } +] diff --git a/apps/dataset_viewer/public/config/sample.viewer.config.json b/apps/dataset_viewer/public/config/sample.viewer.config.json new file mode 100644 index 0000000000000000000000000000000000000000..1cdc5aba70cd06fa19098f092467abbabc970649 --- /dev/null +++ b/apps/dataset_viewer/public/config/sample.viewer.config.json @@ -0,0 +1,12 @@ +[ + { + "beamline": "BM29", + "render": { + "type": "generic", + "format": "graph", + "props": { + "paginationSize": 10 + } + } + } +] diff --git a/apps/dataset_viewer/public/config/ui.config.json b/apps/dataset_viewer/public/config/ui.config.json new file mode 100644 index 0000000000000000000000000000000000000000..99dc23f62c7078e9a91824dd27a03ef9907a0995 --- /dev/null +++ b/apps/dataset_viewer/public/config/ui.config.json @@ -0,0 +1,169 @@ +{ + "applicationTitle": "Data Portal", + "facilityName": "ESRF", + "homePage": { + "policyMessage": "Public data is accessible to anyone. You need to be logged-in to visualize your data when it is under embargo. See <a href=\"https://www.esrf.fr/fr/home/UsersAndScience/UserGuide/esrf-data-policy.html\" target=\"_blank\" rel=\"noreferrer\">ESRF data policy</a> for more details." + }, + "linkToPreviousVersion": { + "name": "Back to Data Portal V1", + "url": "https://data1.esrf.fr" + }, + "knowledgeBasePage": "https://confluence.esrf.fr/display/DATAPOLWK/Knowledge+Base", + "loginForm": { + "accountCreationLink": "https://smis.esrf.fr/misapps/SMISWebClient/accountManager/searchExistingAccount.do?action=search", + "note": { + "enabled": true, + "title": "Important note", + "notes": [ + { + "text": "In order to login to the Data Portal of the ESRF to send samples or browse embargoed data you need to be declared a member of a proposal on the ESRF User Portal. Once this is done your account will be activated <b>45 days</b> before the first experiment starts. If you need access earlier please contact the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"http://www.esrf.eu/UsersAndScience/UserGuide/Contacts\" >ESRF User Office</a>.<br/>Anonymous login to browse open data is always possible." + }, + { + "text": "During 2019 and according to the General Data Protection Regulation, all portal users who did not consent to the <a href=\"http://www.esrf.fr/GDPR\" rel=\"noopener noreferrer\" target=\"_blank\" > User Portal Privacy Statement</a> have had their account deactivated. Please contact the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"http://www.esrf.eu/UsersAndScience/UserGuide/Contacts\" >User Office</a> if you wish to reactivate it." + } + ] + } + }, + "userPortal": { + "investigationParameterPkName": "Id", + "link": "https://smis.esrf.fr/misapps/SMISWebClient/protected/aform/manageAForm.do?action=view&expSessionVO.pk=" + }, + "doi": { + "link": "https://doi.esrf.fr/", + "minimalAbstractLength": 1000, + "minimalTitleLength": 40, + "facilityPrefix": "10.15151", + "facilitySuffix": "ESRF-DC", + "referenceDoi": "https://doi.esrf.fr/10.15151/ESRF-DC-2011729981" + }, + "fileBrowser": { + "maxFileNb": 1000 + }, + "imageViewer": { + "fileExtensions": [".png", ".jpg", ".jpeg", ".tiff", ".gif"] + }, + "h5Viewer": { + "url": "https://hibou.esrf.fr", + "fileExtensions": [".hdf5", ".h5", ".nexus", ".nx", ".nxs", ".cxi"] + }, + "galleryViewer": { + "fileExtensions": ["jpeg", "jpg", "gif", "png", "svg"] + }, + "textViewer": { + "maxFileSize": 5000000, + "fileExtensions": [ + ".txt", + ".log", + ".json", + ".xml", + ".csv", + ".dat", + ".inp", + ".xds", + ".descr", + ".lp", + ".hkl", + ".site", + ".asc", + ".ini" + ] + }, + "feedback": { + "email": "dataportalrequests@esrf.fr", + "subject": "Feedback", + "body": "Hi,\n\n<< Please provide your feedback here. >>\n<< To report an issue, please include screenshots, reproduction steps, proposal number, beamline, etc. >>\n<< To suggest a new feature, please describe the needs this feature would fulfill. >>\n\nThanks" + }, + "footer": { + "images": [ + { + "src": "/images/esrf.jpg", + "alt": "ESRF", + "href": "https://www.esrf.fr/" + }, + { + "src": "/images/CoreTrustSeal.png", + "alt": "CoreTrustSeal", + "href": "https://www.coretrustseal.org/" + } + ] + }, + "handsonTableLicenseKey": "non-commercial-and-evaluation", + "globus": { + "enabled": true, + "url": "https://app.globus.org/file-manager?", + "collections": [ + { + "root": "/data/visitor/", + "origin": "/data", + "originId": "bfc3eff4-f5ca-4b4d-8532-e9a155f3613f" + }, + { + "root": "/data/projects/hop", + "origin": "/data/projects/hop", + "originId": "340dc883-4b0d-476c-abb0-969e1ddd9dc0" + }, + { + "root": "/data/projects/open-datasets", + "origin": "/data/projects/open-datasets/", + "originId": "8cbe8cdc-048a-48cf-b8a0-046a6af3ba44" + }, + { + "root": "/data/projects/paleo", + "origin": "/data/projects/paleo/public", + "originId": "e8fb6c4b-9ab0-4a1c-a79d-d51cef0b8c3d" + } + ], + "messageAlert": { + "enabled": true, + "message": "For users who want to download large volume of experimental data <strong>(>2GB)</strong>, ESRF users can access the Globus service, please read the <a href=\"https://confluence.esrf.fr/display/SCKB/Globus\" target=\"_blank\">documentation</a> for proceeding." + } + }, + "sample": { + "pageTemplateURL": "https://smis.esrf.fr/misapps/SMISWebClient/protected/samplesheet/view.do?pk=", + "editable": true, + "descriptionParameterName": "Sample_description", + "nonEditableParameterName": "Id", + "notesParameterName": "Sample_notes" + }, + "logbook": { + "help": "https://confluence.esrf.fr/display/DATAPOLWK/Electronic+Logbook", + "defaultMonthPeriodForStaff": 1 + }, + "projects": [ + { + "title": "The Human Organ Atlas", + "key": "The Human Organ Atlas", + "url": "https://human-organ-atlas.esrf.eu/datasets/{datasetId}", + "homepage": "https://human-organ-atlas.esrf.eu" + }, + { + "title": "Paleontology database", + "key": "paleo", + "url": "http://paleo.esrf.fr/datasets/{datasetId}", + "homepage": "http://paleo.esrf.fr" + } + ], + "features": { + "reprocessing": true, + "logbook": true, + "dmp": true, + "logistics": true + }, + "tracking": { + "enabled": true, + "url": "https://matomo-srv-1.esrf.fr/", + "siteId": "14", + "tracker": "piwik.php", + "script": "piwik.js" + }, + "mx": { + "pdb_map_mtz_viewer_url": "https://moorhen.esrf.fr" + }, + "logistics": { + "transportOrganizationEnabled": false, + "facilityReimbursmentEnabled": true, + "facilityForwarderName": "FedEX", + "facilityForwarderAccount": "388310561", + "facilityForwarderNamePickup": ["FEDEX", "DHL express", "UPS"] + } +} diff --git a/apps/dataset_viewer/src/App.tsx b/apps/dataset_viewer/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..51edf11d36181f818f8213415e41436fb9a17269 --- /dev/null +++ b/apps/dataset_viewer/src/App.tsx @@ -0,0 +1,49 @@ +import { + AuthenticatedAPIProvider, + AuthenticatorProvider, + OpenIDProvider, + SideNavProvider, + UnauthenticatedAPIProvider, +} from '@edata-portal/core'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { AppRouter } from 'standalone/routing/AppRouter'; +import { ViewersProvider } from 'standalone/providers/ViewersProvider'; +import { ConfigProvider } from 'standalone/providers/ConfigProvider'; +import { Navigation } from 'standalone/routing/Navigation'; +import 'standalone/scss/main.scss'; + +const queryClient = new QueryClient(); + +function App() { + return ( + <> + <QueryClientProvider client={queryClient}> + <ConfigProvider> + <AppContextProviders> + <ViewersProvider> + <SideNavProvider> + <AppRouter> + <Navigation /> + </AppRouter> + </SideNavProvider> + </ViewersProvider> + </AppContextProviders> + </ConfigProvider> + </QueryClientProvider> + </> + ); +} + +function AppContextProviders({ children }: { children: React.ReactNode }) { + return ( + <UnauthenticatedAPIProvider> + <AuthenticatorProvider> + <OpenIDProvider> + <AuthenticatedAPIProvider>{children}</AuthenticatedAPIProvider> + </OpenIDProvider> + </AuthenticatorProvider> + </UnauthenticatedAPIProvider> + ); +} + +export default App; diff --git a/apps/dataset_viewer/src/main.tsx b/apps/dataset_viewer/src/main.tsx index cf991fc9c95e55439d28f008799856e676bc87eb..8042f52c1a25adcb4f357ecf9d1529f6807cedad 100644 --- a/apps/dataset_viewer/src/main.tsx +++ b/apps/dataset_viewer/src/main.tsx @@ -1,6 +1,9 @@ +import App from 'App'; import React from 'react'; import ReactDOM from 'react-dom/client'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - <React.StrictMode></React.StrictMode>, + <React.StrictMode> + <App /> + </React.StrictMode>, ); diff --git a/apps/dataset_viewer/src/standalone/InvestigationDatasets.tsx b/apps/dataset_viewer/src/standalone/InvestigationDatasets.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8d276531a3ce737854610fb0b4707b6885cc9cbf --- /dev/null +++ b/apps/dataset_viewer/src/standalone/InvestigationDatasets.tsx @@ -0,0 +1,33 @@ +import { usePath, useViewers } from '@edata-portal/core'; +import { + useGetEndpoint, + INVESTIGATION_LIST_ENDPOINT, +} from '@edata-portal/icat-plus-api'; +import { Alert } from 'react-bootstrap'; + +/** + * Fetches and displays the datasets related to a specific investigation. + * + * This component: + * - Retrieves the `investigationId` from the URL path. + * - Fetches the corresponding investigation using `useGetEndpoint`. + * - Renders an alert if no investigation is found. + * - Uses `viewInvestigation` from `useViewers()` to render the investigation details. + * + * @returns {JSX.Element} Investigation details or an error alert. + */ +export default function InvestigationDatasets(): JSX.Element { + const investigationId = usePath('investigationId'); + + const investigations = useGetEndpoint({ + endpoint: INVESTIGATION_LIST_ENDPOINT, + params: { ids: investigationId }, + }); + const { viewInvestigation } = useViewers(); + + if (!investigations || investigations.length === 0) { + return <Alert variant="danger">Could not find investigation</Alert>; + } + + return viewInvestigation(investigations[0]); +} diff --git a/apps/dataset_viewer/src/standalone/InvestigationForm.tsx b/apps/dataset_viewer/src/standalone/InvestigationForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8f3273c4d761f42847eb4bd42197546e5b8aad96 --- /dev/null +++ b/apps/dataset_viewer/src/standalone/InvestigationForm.tsx @@ -0,0 +1,87 @@ +import { useState } from 'react'; +import { Container, Row, Col, Form, Button, ListGroup } from 'react-bootstrap'; + +interface Investigation { + investigationId: string; + groupedBySample: boolean; + name: string; + description: string; +} + +interface InvestigationFormProps { + investigations?: Investigation[]; +} + +export default function InvestigationForm({ + investigations = [], +}: InvestigationFormProps) { + const [investigationId, setInvestigationId] = useState(''); + const [groupBySample, setGroupBySample] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!investigationId) return; + const url = `/investigation/${investigationId}?filters=&groupBySample=${groupBySample}`; + window.open(url, '_blank'); + }; + + const handleQuickJump = (id: string, grouped: boolean) => { + const url = `/investigation/${id}?filters=&groupBySample=${grouped}`; + window.open(url, '_blank'); + }; + + return ( + <Container className="vh-100 d-flex"> + <Row className="w-100"> + <Col md={3} className="d-flex flex-column align-items-start"> + <Form + onSubmit={handleSubmit} + className="p-4 bg-light border rounded shadow-sm w-100" + > + <h2 className="mb-3">Jump to</h2> + <Form.Group className="mb-3"> + <Form.Control + type="text" + placeholder="Enter Investigation ID" + value={investigationId} + onChange={(e) => setInvestigationId(e.target.value)} + /> + </Form.Group> + <Form.Group className="mb-3" controlId="groupBySample"> + <Form.Check + type="checkbox" + label="Group by Sample" + checked={groupBySample} + onChange={() => setGroupBySample(!groupBySample)} + /> + </Form.Group> + <Button type="submit" variant="primary" className="w-100"> + Go + </Button> + </Form> + </Col> + + <Col md={6} className="d-flex flex-column align-items-center"> + <h3 className="mb-3">Quick Access</h3> + <ListGroup className="w-100"> + {investigations.map( + ({ investigationId, groupedBySample, name, description }) => ( + <ListGroup.Item + key={investigationId} + action + onClick={() => + handleQuickJump(investigationId, groupedBySample) + } + > + <h5 className="mb-1">{name}</h5> + <p className="mb-1">{description}</p> + <small>{`Investigation ${investigationId} - Group by Sample: ${groupedBySample}`}</small> + </ListGroup.Item> + ), + )} + </ListGroup> + </Col> + </Row> + </Container> + ); +} diff --git a/apps/dataset_viewer/src/standalone/providers/ConfigProvider.tsx b/apps/dataset_viewer/src/standalone/providers/ConfigProvider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f432065ce47a5d23561909ff0647f135248c0b45 --- /dev/null +++ b/apps/dataset_viewer/src/standalone/providers/ConfigProvider.tsx @@ -0,0 +1,48 @@ +import { ConfigContext } from '@edata-portal/core'; +import { useSuspenseQueries } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +export function ConfigProvider({ children }: { children: React.ReactNode }) { + const [api, ui, techniques] = useSuspenseQueries({ + queries: [ + { + queryKey: ['api.config'], + queryFn: async () => { + const response = await fetch('../config/api.config.json'); + return await response.json(); + }, + }, + { + queryKey: ['ui.config'], + queryFn: async () => { + const response = await fetch('../config/ui.config.json'); + return await response.json(); + }, + }, + { + queryKey: ['techniques.config'], + queryFn: async () => { + return [ + { + name: 'Crystallography', + shortname: 'MX', + }, + ]; + }, + }, + ], + }); + + const value = useMemo( + () => ({ + api: api.data, + ui: ui.data, + techniques: techniques.data, + }), + [api.data, techniques.data, ui.data], + ); + + return ( + <ConfigContext.Provider value={value}>{children}</ConfigContext.Provider> + ); +} diff --git a/apps/dataset_viewer/src/standalone/providers/ViewersProvider.tsx b/apps/dataset_viewer/src/standalone/providers/ViewersProvider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c742b52e1f5057583d027a54c6ee578658298e7e --- /dev/null +++ b/apps/dataset_viewer/src/standalone/providers/ViewersProvider.tsx @@ -0,0 +1,33 @@ +import { ViewersContext } from '@edata-portal/core'; +import { DatasetViewer } from 'standalone/viewers/DatasetViewer'; +import GenericInvestigationViewer from 'standalone/viewers/GenericInvestigationViewer'; +import { SampleViewer } from 'standalone/viewers/SampleViewer'; + +export function ViewersProvider({ children }: { children: React.ReactNode }) { + return ( + <ViewersContext.Provider + value={{ + viewSample: (sample, props) => ( + <SampleViewer key={sample.id} sample={sample} props={props} /> + ), + viewDataset: (dataset, type, props) => ( + <DatasetViewer + key={dataset.id} + dataset={dataset} + type={type} + props={props} + /> + ), + viewInvestigation: (investigation, props) => ( + <GenericInvestigationViewer + key={investigation.id} + investigation={investigation} + {...props} + /> + ), + }} + > + {children} + </ViewersContext.Provider> + ); +} diff --git a/apps/dataset_viewer/src/standalone/routing/AppRouter.tsx b/apps/dataset_viewer/src/standalone/routing/AppRouter.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0a661bfcc59f4aa46678b6feda5febaf6c311701 --- /dev/null +++ b/apps/dataset_viewer/src/standalone/routing/AppRouter.tsx @@ -0,0 +1,18 @@ +import { useMemo } from 'react'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { routes } from 'standalone/routing/Routes'; + +export function AppRouter({ children }: { children: JSX.Element }) { + const router = useMemo(() => { + return createBrowserRouter([ + { + element: <>{children}</>, + children: routes, + id: 'home', + handle: { breadcrumb: 'Home' }, + }, + ]); + }, [children]); + + return <RouterProvider router={router} />; +} diff --git a/apps/dataset_viewer/src/standalone/routing/Navigation.tsx b/apps/dataset_viewer/src/standalone/routing/Navigation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..df81786783e3d283c6607b8e6879b690fe7e92ab --- /dev/null +++ b/apps/dataset_viewer/src/standalone/routing/Navigation.tsx @@ -0,0 +1,38 @@ +import { Container } from 'react-bootstrap'; +import { Outlet } from 'react-router-dom'; +import { SideNavRenderer } from '@edata-portal/core'; + +export function Navigation() { + return ( + <div + style={{ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + }} + > + <Container + style={{ + paddingTop: 20, + overflow: 'auto', + height: '100%', + position: 'relative', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + }} + fluid + className="main" + > + <SideNavRenderer> + <Outlet /> + </SideNavRenderer> + </Container> + </div> + ); +} diff --git a/apps/dataset_viewer/src/standalone/routing/Routes.tsx b/apps/dataset_viewer/src/standalone/routing/Routes.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2a0e079fd0a1c190ddb9d35e8904633f4d44386d --- /dev/null +++ b/apps/dataset_viewer/src/standalone/routing/Routes.tsx @@ -0,0 +1,50 @@ +import InvestigationDatasets from 'standalone/InvestigationDatasets'; +import type { RouteObject } from 'react-router-dom'; + +import InvestigationForm from 'standalone/InvestigationForm'; + +export const routes: RouteObject[] = [ + { + path: '/investigation/:investigationId', + element: <InvestigationDatasets></InvestigationDatasets>, + }, + { + path: '*', + element: ( + <InvestigationForm + investigations={[ + { + name: 'BioSAXS Sample changer', + investigationId: '2010314049', + groupedBySample: true, + description: 'Sample Changer experiment', + }, + { + name: 'BioSAXS Sample HPLC', + investigationId: '2010314049', + groupedBySample: true, + description: 'HPLC experiment', + }, + { + name: 'TOMO', + investigationId: '2103286457', + groupedBySample: true, + description: 'Tomography dataset', + }, + { + name: 'XAS', + investigationId: '1921857835', + groupedBySample: false, + description: 'XAS dataset', + }, + { + name: 'HTXRPD', + investigationId: '2052360584', + groupedBySample: false, + description: 'HTXRPD dataset', + }, + ]} + /> + ), + }, +]; diff --git a/apps/dataset_viewer/src/standalone/scss/colors.scss b/apps/dataset_viewer/src/standalone/scss/colors.scss new file mode 100644 index 0000000000000000000000000000000000000000..210d6faf4d6d5865d5c55e09b542a71294ab1b0a --- /dev/null +++ b/apps/dataset_viewer/src/standalone/scss/colors.scss @@ -0,0 +1,29 @@ +// custom theme colors + +$primary: #2c3e50; +$secondary: #456c74; +$success: #18bc9c; +$info: #108cdf; +$warning: #f39c12; +$danger: #e74c3c; +$light: #ecf0f1; +$dark: #222222; + +$dataset-raw: #b4ccd1; +$dataset-processed: #f9e69e; +$sample: #4d3d5f; + +:root { + --primary: #{$primary}; + --secondary: #{$secondary}; + --success: #{$success}; + --info: #{$info}; + --warning: #{$warning}; + --danger: #{$danger}; + --light: #{$light}; + --dark: #{$dark}; + + --dataset-raw: #{$dataset-raw}; + --dataset-processed: #{$dataset-processed}; + --sample: #{$sample}; +} diff --git a/apps/dataset_viewer/src/standalone/scss/images/esrf-white.png b/apps/dataset_viewer/src/standalone/scss/images/esrf-white.png new file mode 100644 index 0000000000000000000000000000000000000000..a3c925198bc7246316308878f9ffda7079adc867 Binary files /dev/null and b/apps/dataset_viewer/src/standalone/scss/images/esrf-white.png differ diff --git a/apps/dataset_viewer/src/standalone/scss/main.scss b/apps/dataset_viewer/src/standalone/scss/main.scss new file mode 100644 index 0000000000000000000000000000000000000000..7c94a23909253e29305d0ecf0f920abde601c1bf --- /dev/null +++ b/apps/dataset_viewer/src/standalone/scss/main.scss @@ -0,0 +1,60 @@ +@import 'bootswatch/dist/flatly/variables'; +@import './colors.scss'; +@import 'bootstrap/scss/bootstrap'; + +$web-font-path: false; //disable loading of web fonts +@import 'bootswatch/dist/flatly/bootswatch'; + +body { + font-size: 0.9em; +} + +.navbar-brand { + background: url('images/esrf-white.png') no-repeat; + background-size: contain; + padding-left: 70px; +} + +.breadcrumb { + margin: 0px; + padding: 0px; +} + +.table { + --bs-table-bg: 'none'; +} + +.monospace { + font-family: monospace; +} + +.bg-dataset-raw { + background-color: $dataset-raw; + color: color-contrast($dataset-raw); + a { + color: color-contrast($dataset-raw); + } +} + +.bg-dataset-processed { + background-color: $dataset-processed; + color: color-contrast($dataset-processed); + a { + color: color-contrast($dataset-processed); + } +} + +.bg-sample { + background-color: $sample; + color: color-contrast($sample); + a { + color: color-contrast($sample); + } +} + +.bg-secondary { + color: color-contrast($secondary); + a { + color: color-contrast($secondary); + } +} diff --git a/apps/dataset_viewer/src/standalone/viewers/ComponentMap.tsx b/apps/dataset_viewer/src/standalone/viewers/ComponentMap.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6af09f80319c67f8d7ec692444fae113ce1fa111 --- /dev/null +++ b/apps/dataset_viewer/src/standalone/viewers/ComponentMap.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +export const COMPONENT_MAP: Record< + string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + React.LazyExoticComponent<React.ComponentType<any>> +> = { + SAXSSampleChangerDatasetDetails: React.lazy( + () => import('components/technique/saxs/SAXSSampleChangerDatasetDetails'), + ), + SAXSSampleChangerDatasetSnapshot: React.lazy( + () => import('components/technique/saxs/SAXSSampleChangerDatasetSnapshot'), + ), + SAXSHPLCDatasetSnapshot: React.lazy( + () => import('components/technique/saxs/SAXSHPLCDatasetSnapshot'), + ), + SAXSHPLCDatasetDetails: React.lazy( + () => import('components/technique/saxs/SAXSHPLCDatasetDetails'), + ), + HTXRPDDataset: React.lazy( + () => import('components/technique/htxrpd/HTXRPDDataset'), + ), + TOMODataset: React.lazy( + () => import('components/technique/tomo/TOMODataset'), + ), + XASDatasetDetail: React.lazy( + () => import('components/technique/xas/XASDatasetDetail'), + ), + XASDatasetTableCell: React.lazy( + () => import('components/technique/xas/XASDatasetTableCell'), + ), + + // Add more components as needed +}; diff --git a/apps/dataset_viewer/src/standalone/viewers/DatasetViewer.tsx b/apps/dataset_viewer/src/standalone/viewers/DatasetViewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..61c5e8d0e4d7a5ba236438eb1d5e341ae1504016 --- /dev/null +++ b/apps/dataset_viewer/src/standalone/viewers/DatasetViewer.tsx @@ -0,0 +1,158 @@ +import { + getInstrumentNameByInvestigation, + DatasetViewerType, + first, + GenericDatasetDetailsViewer, + getDatasetParamValue, + PROJECT_NAME_PARAM, + DEFINITION_PARAM, +} from '@edata-portal/core'; + +import type { Dataset } from '@edata-portal/icat-plus-api'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import React, { FC } from 'react'; +import { useEffect, useMemo } from 'react'; +import { Alert } from 'react-bootstrap'; +import { COMPONENT_MAP } from 'standalone/viewers/ComponentMap'; +import { + DatasetViewerDefinition, + RENDER_SCHEMA, +} from 'standalone/viewers/RenderSchema'; +import { + useFilterViewers, + validateViewers, +} from 'standalone/viewers/viewerDefinition'; + +function useViewer( + dataset?: Dataset, + viewers?: DatasetViewerDefinition[], + type?: DatasetViewerType, +) { + const investigation = dataset?.investigation; + + const viewersWithType = useMemo(() => { + if (!type) return viewers; + return viewers?.filter((viewer) => type in viewer.render); + }, [viewers, type]); + + const applicableViewers = useFilterViewers( + { + investigationId: investigation?.id?.toString(), + datasetId: dataset?.id?.toString(), + beamline: investigation + ? getInstrumentNameByInvestigation(investigation) + : undefined, + date: dataset?.startDate, + datasetParameters: dataset?.parameters, + technique: dataset + ? getDatasetParamValue(dataset, DEFINITION_PARAM) + : undefined, + project: dataset + ? getDatasetParamValue(dataset, PROJECT_NAME_PARAM) + : undefined, + }, + viewersWithType, + ); + + return first(applicableViewers); +} + +interface DynamicComponentProps { + componentName: string; + dataset: unknown; + [key: string]: unknown; +} +/** + * DynamicComponent dynamically loads and renders a React component based on the given `componentName`. + * + * The component is lazily imported using `React.lazy()`, allowing for code-splitting. + * If the specified component is not found in `componentMap`, an error message is displayed. + * + * @param {string} componentName - The name of the component to be rendered. Must match a key in `componentMap`. + * @param {any} dataset - Data to be passed as a prop to the dynamically loaded component. + * @param {Object} props - Additional props to pass to the dynamically loaded component. + * @returns {JSX.Element} - The dynamically loaded component wrapped in `React.Suspense` or an error message if not found. + */ +const DynamicComponent: FC<DynamicComponentProps> = ( + props: DynamicComponentProps, +) => { + const { componentName, dataset, ...restProps } = props; + const Component = COMPONENT_MAP[componentName]; + + if (!Component) { + return <div>Error: Component '{componentName}' not found.</div>; + } + + return ( + <React.Suspense fallback={<div>Loading...</div>}> + <Component dataset={dataset} {...restProps} /> + </React.Suspense> + ); +}; + +export function DatasetViewer({ + dataset, + props, + type, +}: { + dataset: Dataset; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props?: any; + type: DatasetViewerType; +}) { + const { data: config } = useSuspenseQuery({ + queryKey: ['dataset.viewer.config'], + queryFn: async () => { + const response = await fetch('/config/dataset.viewer.config.json'); + return (await response.json()) as DatasetViewerDefinition[]; + }, + }); + + useEffect(() => { + if (config) { + validateViewers({ + renderSchema: RENDER_SCHEMA, + viewers: config, + }); + } + }, [config]); + + const viewer = useViewer(dataset, config, type); + // This forces the display of the viewer despite its remote configuration + if (type === 'generic') { + return <GenericDatasetDetailsViewer dataset={dataset} {...props} />; + } + + //default viewer + if (type === 'details') { + return ( + <DynamicComponent + componentName={viewer?.render.details?.remote.component || ''} + dataset={dataset} + someProp="value" + /> + ); + //return <GenericDatasetDetailsViewer dataset={dataset} {...props} />; + } + + if (type === 'snapshot') { + return ( + <DynamicComponent + componentName={viewer?.render.snapshot?.remote.component || ''} + dataset={dataset} + someProp="value" + /> + ); + } + + if (type === 'tableCell') { + return ( + <DynamicComponent + componentName={viewer?.render.tableCell?.remote.component || ''} + dataset={dataset} + someProp="value" + /> + ); + } + return <Alert variant="danger">No viewer found</Alert>; +} diff --git a/apps/dataset_viewer/src/standalone/viewers/GenericInvestigationViewer.tsx b/apps/dataset_viewer/src/standalone/viewers/GenericInvestigationViewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3f5218dcfbc03ac408af56be72697d1c0707dbe9 --- /dev/null +++ b/apps/dataset_viewer/src/standalone/viewers/GenericInvestigationViewer.tsx @@ -0,0 +1,64 @@ +import { + DatasetList, + useParam, + WithSideNav, + GenericDatasetsFilter, + useUserPreferences, +} from '@edata-portal/core'; +import type { Investigation } from '@edata-portal/icat-plus-api'; +import { useMemo } from 'react'; + +export default function GenericInvestigationViewer({ + investigation, +}: { + investigation: Investigation; +}) { + const [filters, setFilters] = useParam<string>('filters', ''); + const [isGroupedBySample, setIsGroupedBySample] = useUserPreferences( + 'isGroupedBySample', + true, + ); + + const [sampleChecked, setSampleChecked] = useParam<string>( + 'groupBySample', + isGroupedBySample.toString(), + ); + + const updateSampleChecked = (checked: string) => { + setSampleChecked(checked); + setIsGroupedBySample(checked === 'true'); + }; + + const [sampleId, setSampleId] = useParam<string>('sampleId', ''); + const [search, setSearch] = useParam<string>('search', ''); + const groupBy = useMemo(() => { + return sampleChecked === 'true' ? 'sample' : 'dataset'; + }, [sampleChecked]); + + return ( + <WithSideNav + sideNav={ + <GenericDatasetsFilter + filters={filters} + setFilters={setFilters} + investigation={investigation} + setSampleId={setSampleId} + selectedSampleId={Number(sampleId)} + sampleChecked={sampleChecked} + setSampleChecked={updateSampleChecked} + datasetSearch={search} + setDatasetSearch={setSearch} + /> + } + > + <DatasetList + groupBy={groupBy} + parameterFilter={filters?.length ? filters : undefined} + investigationId={investigation.id.toString()} + sampleIds={sampleId?.length ? [sampleId] : undefined} + sampleId={sampleId} + search={groupBy === 'sample' ? '' : search} + /> + </WithSideNav> + ); +} diff --git a/apps/dataset_viewer/src/standalone/viewers/GenericSampleDatasetGraph.tsx b/apps/dataset_viewer/src/standalone/viewers/GenericSampleDatasetGraph.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bdaab9e835e463d9c570b657a19ae3e356a6d12a --- /dev/null +++ b/apps/dataset_viewer/src/standalone/viewers/GenericSampleDatasetGraph.tsx @@ -0,0 +1,92 @@ +import { + GridGraphDefinition, + GridGraphItem, + GridGraphRender, + HorizontalScroll, + SampleWithDatasetGroups, + first, + sortDatasetsByDate, +} from '@edata-portal/core'; +import type { Dataset, Sample } from '@edata-portal/icat-plus-api'; +import { useMemo } from 'react'; + +export default function GenericSampleDatasetGraph({ + sample, + paginationSize, +}: { + sample: Sample; + paginationSize: number; +}) { + return ( + <SampleWithDatasetGroups + sample={sample} + computeGroups={computeGroupsDataset} + renderGroup={(group) => <RenderDatasetGroup group={group} />} + paginationSize={paginationSize} + /> + ); +} + +function computeGroupsDataset(datasets: Dataset[]) { + return datasets.map((dataset) => [dataset]); +} + +function RenderDatasetGroup({ group }: { group: Dataset[] }) { + const grid = useMemo(() => computeGridGraph(group), [group]); + return ( + <HorizontalScroll> + <GridGraphRender grid={grid} /> + </HorizontalScroll> + ); +} + +function computeGridGraph(datasets: Dataset[]): GridGraphDefinition { + return { + root: getDatasetsGrid(datasets), + }; +} + +function getDatasetsGrid(datasets: Dataset[]): GridGraphItem { + const firstDataset = first(datasets); + + return { + id: (firstDataset?.id || 'empty') + '-list-column', + type: 'column', + items: datasets.map(getDatasetGrid), + collapsible: false, + }; +} + +function getDatasetGrid(dataset: Dataset): GridGraphItem { + const processed = sortDatasetsByDate(dataset.outputDatasets || []); + + const datasetCell: GridGraphItem = { + type: 'cell', + id: dataset.id.toString(), + content: { type: 'dataset', dataset }, + }; + + if (!processed?.length) return datasetCell; + + if (processed.length >= 2) + return { + type: 'row', + id: dataset.id.toString() + '-processed-row', + items: [datasetCell, ...processed.map(getDatasetGrid)], + }; + + return { + type: 'row', + id: dataset.id.toString() + '-processed-row', + items: [ + datasetCell, + { + id: dataset.id.toString() + '-processed-column', + type: 'column', + items: processed.map(getDatasetGrid), + collapsible: true, + collapsedLabel: 'Processed', + }, + ], + }; +} diff --git a/apps/dataset_viewer/src/standalone/viewers/GenericSampleViewer.tsx b/apps/dataset_viewer/src/standalone/viewers/GenericSampleViewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1667edab83ebca7cd3cfcc45fafc6e5080288c63 --- /dev/null +++ b/apps/dataset_viewer/src/standalone/viewers/GenericSampleViewer.tsx @@ -0,0 +1,6 @@ +import { SampleHeaderWithOutputs } from '@edata-portal/core'; +import type { Sample } from '@edata-portal/icat-plus-api'; + +export default function GenericSampleViewer({ sample }: { sample: Sample }) { + return <SampleHeaderWithOutputs sample={sample} />; +} diff --git a/apps/dataset_viewer/src/standalone/viewers/RenderSchema.tsx b/apps/dataset_viewer/src/standalone/viewers/RenderSchema.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ebb3bf040e057dd415d23211816e287aa2e0fb6d --- /dev/null +++ b/apps/dataset_viewer/src/standalone/viewers/RenderSchema.tsx @@ -0,0 +1,64 @@ +import { DATASET_VIEWER_TYPES, DatasetViewerType } from '@edata-portal/core'; +import { JSONSchemaType } from 'ajv'; + +export type GenericViewerDefinition<T> = { + date?: string; + beamline?: string | string[]; + projectName?: string; + technique?: string; + datasetParameter?: { [key: string]: string | string[] | undefined | null }; + render: T; +}; + +export type DatasetViewerDefinition = + GenericViewerDefinition<DatasetViewerRender>; + +export type DatasetViewerRender = { + [K in DatasetViewerType]?: { + type: 'remote'; + remote: RemoteDefinition; + }; +}; + +export type RemoteDefinition = { + name: string; + component: string; +}; + +export const REMOTE_DEFINITION_SCHEMA: JSONSchemaType<RemoteDefinition> = { + type: 'object', + properties: { + name: { type: 'string', nullable: false }, + component: { type: 'string', nullable: false }, + }, + required: ['name', 'component'], + additionalProperties: false, + nullable: true, +}; +export const LOCAL_DEFINITION_SCHEMA: JSONSchemaType<RemoteDefinition> = { + type: 'object', + properties: { + name: { type: 'string', nullable: false }, + component: { type: 'string', nullable: false }, + }, + required: ['name', 'component'], + additionalProperties: false, + nullable: true, +}; + +export const RENDER_SCHEMA: JSONSchemaType<DatasetViewerRender> = { + type: 'object', + properties: DATASET_VIEWER_TYPES.reduce((acc, type) => { + acc[type] = { + type: 'object', + properties: { + type: { type: 'string', enum: ['remote', 'local'], nullable: false }, + remote: REMOTE_DEFINITION_SCHEMA, + }, + required: ['type', 'remote'], + additionalProperties: false, + }; + return acc; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }, {} as any), +}; diff --git a/apps/dataset_viewer/src/standalone/viewers/SampleViewer.tsx b/apps/dataset_viewer/src/standalone/viewers/SampleViewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a5036d46825a2488ed329a78b9e0e4eebc18c1db --- /dev/null +++ b/apps/dataset_viewer/src/standalone/viewers/SampleViewer.tsx @@ -0,0 +1,106 @@ +import type { Sample } from '@edata-portal/icat-plus-api'; +import { first, getInstrumentNameByInvestigation } from '@edata-portal/core'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import type { JSONSchemaType } from 'ajv'; +import { + useFilterViewers, + validateViewers, +} from 'standalone/viewers/viewerDefinition'; +import GenericSampleViewer from 'standalone/viewers/GenericSampleViewer'; +import GenericSampleDatasetGraph from 'standalone/viewers/GenericSampleDatasetGraph'; +import { GenericViewerDefinition } from 'standalone/viewers/RenderSchema'; + +export type SampleViewerDefinition = + GenericViewerDefinition<SampleViewerRender>; + +export type SampleViewerRender = { + type: 'generic'; + format: 'list' | 'graph'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props?: any; +}; + +const RENDER_SCHEMA: JSONSchemaType<SampleViewerRender> = { + anyOf: [ + { + type: 'object', + properties: { + type: { type: 'string', enum: ['generic'], nullable: false }, + format: { type: 'string', enum: ['list', 'graph'], nullable: false }, + props: { type: 'object', nullable: true }, + }, + required: ['type', 'format'], + }, + ], +}; + +function useViewer(sample?: Sample, viewers?: SampleViewerDefinition[]) { + const investigation = sample?.investigation; + + const applicableViewers = useFilterViewers( + { + investigationId: sample?.investigation?.id?.toString(), + beamline: investigation + ? getInstrumentNameByInvestigation(investigation) + : undefined, + date: sample?.investigation?.startDate, + sampleId: sample?.id?.toString(), + }, + viewers, + ); + + return first(applicableViewers); +} + +export function SampleViewer({ + sample, + props, +}: { + sample: Sample; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props?: any; +}) { + const { data: config } = useSuspenseQuery({ + queryKey: ['sample.viewer.config'], + queryFn: async () => { + const response = await fetch('/config/sample.viewer.config.json'); + return (await response.json()) as SampleViewerDefinition[]; + }, + }); + + useEffect(() => { + if (config) { + validateViewers({ + renderSchema: RENDER_SCHEMA, + viewers: config, + }); + } + }, [config]); + + const viewer = useViewer(sample, config); + + if (viewer?.render.type === 'generic') { + if (viewer.render.format === 'graph') { + return ( + <GenericSampleDatasetGraph + sample={sample} + {...viewer.render.props} + {...props} + /> + ); + } + if (viewer.render.format === 'list') { + return ( + <GenericSampleViewer + sample={sample} + {...viewer.render.props} + {...props} + /> + ); + } + } + + //default viewer + return <GenericSampleViewer sample={sample} {...props} />; +} diff --git a/apps/dataset_viewer/src/standalone/viewers/viewerDefinition.ts b/apps/dataset_viewer/src/standalone/viewers/viewerDefinition.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec2e3c2a96ae5c69c242a96915f21a83bd66d68b --- /dev/null +++ b/apps/dataset_viewer/src/standalone/viewers/viewerDefinition.ts @@ -0,0 +1,245 @@ +import { + DEFINITION_PARAM, + parseDate, + PROJECT_NAME_PARAM, +} from '@edata-portal/core'; +import { + Dataset, + DATASET_PARAMETER_VALUE_ENDPOINT, + DatasetParameterValuesResult, + Parameter, + useGetEndpoint, +} from '@edata-portal/icat-plus-api'; +import Ajv, { JSONSchemaType } from 'ajv'; +import { useMemo } from 'react'; +import { GenericViewerDefinition } from 'standalone/viewers/RenderSchema'; + +function getViewerDefinitionSchema<T>( + renderSchema: JSONSchemaType<T>, +): JSONSchemaType<GenericViewerDefinition<T>> { + return { + type: 'object', + properties: { + date: { type: 'string', nullable: true }, + beamline: { + type: ['string', 'array'], + items: { type: 'string' }, + nullable: true, + }, + projectName: { type: 'string', nullable: true }, + technique: { type: 'string', nullable: true }, + datasetParameter: { + type: 'object', + additionalProperties: { + type: ['string', 'array'], + items: { type: 'string' }, + nullable: true, + }, + nullable: true, + }, + render: { ...renderSchema }, + }, + required: ['render'], + additionalProperties: false, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; +} + +export function validateViewers<T>({ + renderSchema, + viewers, +}: { + renderSchema: JSONSchemaType<T>; + viewers: GenericViewerDefinition<T>[]; +}) { + const ajv = new Ajv({ + allowUnionTypes: true, + }); + const schema = getViewerDefinitionSchema(renderSchema); + const validate = ajv.compile(schema); + viewers.forEach((viewer) => { + const valid = validate(viewer); + if (!valid) { + console.error(validate.errors); + throw new Error( + `Invalid sample viewer config:\n\n${ajv.errorsText(validate.errors)}\n\n${JSON.stringify(viewer)}\n\n`, + ); + } + }); +} + +export function useFilterViewers<T>( + data: { + investigationId?: string; + sampleId?: string; + datasetId?: string; + beamline?: string; + date?: string; + datasetParameters?: Dataset['parameters']; + technique?: string; + project?: string; + }, + viewers?: GenericViewerDefinition<T>[], +) { + const { investigationId, sampleId, beamline, date, datasetId } = data; + + const applicableViewers = useMemo(() => { + if (!investigationId || !beamline || !date || !viewers?.length) return []; + return viewers.filter((viewer) => { + //check beamline + if (viewer.beamline !== undefined) { + const beamlineUpper = beamline.toUpperCase(); + if (Array.isArray(viewer.beamline)) { + if ( + !viewer.beamline.map((v) => v.toUpperCase()).includes(beamlineUpper) + ) { + return false; + } + } else { + if (viewer.beamline.toUpperCase() !== beamlineUpper) { + return false; + } + } + } + + //check date + if (viewer.date !== undefined) { + const parsedDate = parseDate(data.date); + const viewerDate = parseDate(viewer.date); + if (!viewerDate || !parsedDate) return false; + if (parsedDate.getTime() < viewerDate.getTime()) { + return false; + } + } + + return true; + }); + }, [investigationId, beamline, date, viewers, data.date]); + + const needsTechniques = useMemo(() => { + if (data.technique || !investigationId || !applicableViewers?.length) + return false; + return applicableViewers.some((v) => !!v.technique); + }, [applicableViewers, data.technique, investigationId]); + + const experimentTechniques = useGetEndpoint({ + endpoint: DATASET_PARAMETER_VALUE_ENDPOINT, + params: { + investigationId: investigationId, + name: DEFINITION_PARAM, + datasetType: 'acquisition', + ...(sampleId ? { sampleId } : {}), + }, + default: [] as DatasetParameterValuesResult[], + skipFetch: !needsTechniques, + }); + + const needsProjects = useMemo(() => { + if (data.project || !investigationId || !applicableViewers?.length) + return false; + return applicableViewers.some((v) => !!v.projectName); + }, [applicableViewers, data.project, investigationId]); + + const experimentProjects = useGetEndpoint({ + endpoint: DATASET_PARAMETER_VALUE_ENDPOINT, + params: { + investigationId: investigationId, + name: PROJECT_NAME_PARAM, + datasetType: 'acquisition', + ...(sampleId ? { sampleId } : {}), + }, + default: [] as DatasetParameterValuesResult[], + skipFetch: !needsProjects, + }); + + const datasetParameterFilters = useMemo(() => { + if ( + data.datasetParameters || + !investigationId || + !applicableViewers?.length + ) + return []; + return applicableViewers + .map((v) => v.datasetParameter) + .flatMap((v) => Object.keys(v || {})) + .filter((v): v is string => !!v); + }, [applicableViewers, data.datasetParameters, investigationId]); + + const experimentDatasetParameters = useGetEndpoint({ + endpoint: DATASET_PARAMETER_VALUE_ENDPOINT, + params: { + investigationId: investigationId, + name: datasetParameterFilters.join(','), + datasetType: 'acquisition', + ...(sampleId ? { sampleId } : {}), + }, + default: [] as DatasetParameterValuesResult[], + skipFetch: !datasetParameterFilters.length, + }); + + return useMemo(() => { + const projectData = data.project + ? [data.project] + : datasetId + ? [] + : experimentProjects.flatMap((p) => p.values); + const techniqueData = data.technique + ? [data.technique] + : datasetId + ? [] + : experimentTechniques.flatMap((p) => p.values); + const datasetParameterData = + data.datasetParameters || experimentDatasetParameters; + + return applicableViewers.filter( + ({ projectName, technique, datasetParameter }) => { + if ( + (projectName && !projectData.length) || + (technique && !techniqueData.length) || + (datasetParameter && !datasetParameterData.length) + ) { + return false; + } + return ( + (!projectName || projectData.includes(projectName)) && + (!technique || techniqueData.includes(technique)) && + (!datasetParameter || + checkDatasetParameters(datasetParameterData, datasetParameter)) + ); + }, + ); + }, [ + applicableViewers, + data.datasetParameters, + data.project, + data.technique, + datasetId, + experimentDatasetParameters, + experimentProjects, + experimentTechniques, + ]); +} + +function checkDatasetParameters( + values: DatasetParameterValuesResult[] | Parameter[], + datasetParameters: { [key: string]: string | string[] | undefined | null }, +) { + function hasValue(key: string, targetValue: string | undefined | null) { + return values.some((value) => { + if (value.name !== key) return false; + if (targetValue === undefined || targetValue === null) return true; + if ('value' in value) return value.value === targetValue; + if ('values' in value) return value.values.includes(targetValue); + return false; + }); + } + + for (const key in datasetParameters) { + const target = datasetParameters[key]; + if (Array.isArray(target)) { + if (!target.some((v) => hasValue(key, v))) return false; + } else if (!hasValue(key, target)) return false; + } + + return true; +} diff --git a/apps/dataset_viewer/tsconfig.json b/apps/dataset_viewer/tsconfig.json index 54f4b84b0d1a9f04e7e7dc24a0f78ad19c34a379..bc6526fe14ebb2d7a79443e1c541210de3dd9f5b 100644 --- a/apps/dataset_viewer/tsconfig.json +++ b/apps/dataset_viewer/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.json", - "include": ["src"], + "include": ["src", "public/config"], "compilerOptions": { "baseUrl": "src", "outDir": "dist", diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index c37b7d5a8d195031c367eed9f6f9ffcb49a2e66b..7bfa99b3b638499ec0cd6030a46850996937841b 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -9,6 +9,7 @@ nav: - Home: index.md - Installation: installation.md - Configuration: configuration.md + - Development: development.md - Run: run.md - Tests: tests.md - Architecture: architecture.md diff --git a/documentation/src/development.md b/documentation/src/development.md new file mode 100644 index 0000000000000000000000000000000000000000..3f596adc37d22884227355d5d11247b67fc5f87b --- /dev/null +++ b/documentation/src/development.md @@ -0,0 +1,44 @@ +# Development + +## Microfront ends + +### MX + +MX microntend contains all the visual components related to the display of crystalography experiments. It can be found under the folder '/apps/mx' + +#### Standalone mode + +You can run the microfrontend in standalone mode by typing: + +``` +pnpm prod +``` + +Example: + +``` +http://localhost:3003/investigation/1405067863 +``` + + + +### Dataset Viewer + +This microfrontend contains the visual display for datasets that require dedicated visualization but are simple enough not to need a specific microfrontend +The MX microfrontend contains all the visual components related to the display of crystallography experiments. It can be found in the '/apps/mx' folder. + +#### Standalone mode + +In order to make development easier, the data_viewer can be run in standalone mode, meaning it does not need to be compiled and run within the portal: + +``` +pnpm configure:standalone +``` + +ad then run: + +``` +pnpm prod +``` + +Once it is running, a welcome page is available, and you can enter the investigation you would like it to display. Example: http://localhost:3005 diff --git a/documentation/src/images/mx_microfrontend_example.jpeg b/documentation/src/images/mx_microfrontend_example.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..34dd6231456837694008ffa9093722dabf7ae1ed Binary files /dev/null and b/documentation/src/images/mx_microfrontend_example.jpeg differ diff --git a/package-lock.json b/package-lock.json index 9b30f7b89b8b5831a16e506326853f15370b671f..27c354fd223cae96cc8f1a3815a4ab0f0649e357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,12 +6,11 @@ "": { "dependencies": { "eslint": "^8.57.0", - "mathjs": "^14.2.0", "prettier": "^3.3.3", "typescript": "5.5.3" }, "devDependencies": { - "@edata-portal/icat-plus-api": "^1.8.12", + "@edata-portal/icat-plus-api": "^1.8.13", "@tanstack/react-query": "^5.52.1", "cypress": "^13.13.3", "dotenv": "^16.4.5", @@ -52,7 +51,7 @@ }, "../../.local/share/pnpm/global/5/.pnpm/@edata-portal+icat-plus-api@1.8.12_@tanstack+react-query@5.52.1_react@18.3.1__react-dom@18.3._duqejkry636hus5cftbbjagihq/node_modules/@edata-portal/icat-plus-api": { "version": "1.8.12", - "dev": true, + "extraneous": true, "dependencies": { "date-fns": "^3.6.0" }, @@ -1304,148 +1303,42 @@ "node": ">=12.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", - "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@edata-portal/icat-plus-api": { - "resolved": "../../.local/share/pnpm/global/5/.pnpm/@edata-portal+icat-plus-api@1.8.12_@tanstack+react-query@5.52.1_react@18.3.1__react-dom@18.3._duqejkry636hus5cftbbjagihq/node_modules/@edata-portal/icat-plus-api", - "link": true - }, - "node_modules/@lambdatest/node-tunnel": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@lambdatest/node-tunnel/-/node-tunnel-4.0.8.tgz", - "integrity": "sha512-IY42aDD4Ryqjug9V4wpCjckKpHjC2zrU/XhhorR5ztX088XITRFKUo8U6+gOjy/V8kAB+EgDuIXfK0izXbt9Ow==", - "license": "ISC", + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/@edata-portal/icat-plus-api/-/icat-plus-api-1.8.15.tgz", + "integrity": "sha512-0gO/NpNUTCDlhtAmxKBvWJ255UrKnpNaOq31hIKpj2QaU3FF8YlRc/BpKt8daPBRnYCzvK5nq4rxXBWjkMbf4A==", + "dev": true, "dependencies": { - "adm-zip": "^0.5.10", - "axios": "^1.6.2", - "get-port": "^1.0.0", - "https-proxy-agent": "^5.0.0", - "split": "^1.0.1" + "date-fns": "^3.6.0" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.28.9", + "react": "^18.2.0", + "react-dom": "^18.2.0" } }, "node_modules/@tanstack/react-query": { "resolved": "../../.local/share/pnpm/global/5/.pnpm/@tanstack+react-query@5.52.1_react@18.3.1/node_modules/@tanstack/react-query", "link": true }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "license": "MIT", - "engines": { - "node": ">=12.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/complex.js": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", - "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/cypress": { "resolved": "../../.local/share/pnpm/global/5/.pnpm/cypress@13.13.3/node_modules/cypress", "link": true }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, "node_modules/dotenv": { "resolved": "../../.local/share/pnpm/global/5/.pnpm/dotenv@16.4.5/node_modules/dotenv", "link": true }, - "node_modules/escape-latex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", - "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", - "license": "MIT" - }, "node_modules/eslint": { "resolved": "../../.local/share/pnpm/global/5/.pnpm/eslint@8.57.0/node_modules/eslint", "link": true @@ -1454,145 +1347,10 @@ "resolved": "../../.local/share/pnpm/global/5/.pnpm/eslint-config-react-app@7.0.1_@babel+plugin-syntax-flow@7.24.7_@babel+core@7.25.2__@babel+plu_jygqjhomjmizk32rm4ampwzu24/node_modules/eslint-config-react-app", "link": true }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.1.tgz", - "integrity": "sha512-Ah6t/7YCYjrPUFUFsOsRLMXAdnYM+aQwmojD2Ayb/Ezr82SwES0vuyQ8qZ3QO8n9j7W14VJuVZZet8U3bhSdQQ==", - "license": "MIT", - "engines": { - "node": ">= 12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/get-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-1.0.0.tgz", - "integrity": "sha512-vg59F3kcXBOtcIijwtdAyCxFocyv/fVkGQvw1kVGrxFO1U4SSGkGjrbASg5DN3TVekVle/jltwOjYRnZWc1YdA==", - "license": "MIT", - "bin": { - "get-port": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/javascript-natural-sort": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", - "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", - "license": "MIT" - }, - "node_modules/mathjs": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.2.0.tgz", - "integrity": "sha512-CcJV1cQwRSrQIAAX3sWejFPUvUsQnTZYisEEuoMBw3gMDJDQzvKQlrul/vjKAbdtW7zaDzPCl04h1sf0wh41TA==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.25.7", - "@lambdatest/node-tunnel": "^4.0.8", - "complex.js": "^2.2.5", - "decimal.js": "^10.4.3", - "escape-latex": "^1.2.0", - "fraction.js": "^5.2.1", - "javascript-natural-sort": "^0.7.1", - "seedrandom": "^3.0.5", - "tiny-emitter": "^2.1.0", - "typed-function": "^4.2.1" - }, - "bin": { - "mathjs": "bin/cli.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/prettier": { "resolved": "../../.local/share/pnpm/global/5/.pnpm/prettier@3.3.3/node_modules/prettier", "link": true }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/react": { "resolved": "../../.local/share/pnpm/global/5/.pnpm/react@18.3.1/node_modules/react", "link": true @@ -1601,51 +1359,6 @@ "resolved": "../../.local/share/pnpm/global/5/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom", "link": true }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, - "node_modules/seedrandom": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "license": "MIT" - }, - "node_modules/split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "license": "MIT", - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "license": "MIT" - }, - "node_modules/tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", - "license": "MIT" - }, - "node_modules/typed-function": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", - "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", - "license": "MIT", - "engines": { - "node": ">= 18" - } - }, "node_modules/typescript": { "resolved": "../../.local/share/pnpm/global/5/.pnpm/typescript@5.5.3/node_modules/typescript", "link": true diff --git a/packages/core/src/components/dataset/generic/GenericDatasetCardViewer.tsx b/packages/core/src/components/dataset/generic/GenericDatasetCardViewer.tsx index 5add1867fe52e4883832c519ce912a80165cef65..65de3a083f6735f6f016aa952bb1752f6e357346 100644 --- a/packages/core/src/components/dataset/generic/GenericDatasetCardViewer.tsx +++ b/packages/core/src/components/dataset/generic/GenericDatasetCardViewer.tsx @@ -19,7 +19,6 @@ export function GenericDatasetCardViewer({ isTableCell: boolean; }) { const viewers = useViewers(); - const tabs = useMemo(() => { if (!viewerConfig) return { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9852425027b80e14969bc784309dda705972683..c30603c354ac6fa796cc6daaef0a36558103ce9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,6 +153,15 @@ importers: '@tanstack/react-query': specifier: ^5.52.1 version: 5.52.1(react@18.3.1) + ajv: + specifier: ^8.17.1 + version: 8.17.1 + bootstrap: + specifier: ^5.3.3 + version: 5.3.3(@popperjs/core@2.11.8) + bootswatch: + specifier: ^5.3.3 + version: 5.3.3 lodash: specifier: ^4.17.21 version: 4.17.21