Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • icat/data-portal
1 result
Show changes
Commits on Source (18)
Showing
with 598 additions and 281 deletions
......@@ -18,7 +18,7 @@
"dependencies": {
"@babel/eslint-parser": "^7.25.1",
"@edata-portal/core": "workspace:*",
"@edata-portal/icat-plus-api": "^1.8.12",
"@edata-portal/icat-plus-api": "^1.8.13",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
......
......@@ -18,7 +18,7 @@
"dependencies": {
"@babel/eslint-parser": "^7.25.1",
"@edata-portal/core": "workspace:*",
"@edata-portal/icat-plus-api": "^1.8.12",
"@edata-portal/icat-plus-api": "^1.8.13",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
......
......@@ -4,6 +4,7 @@ import {
SnashoptConfigItem,
getDatasetName,
getDatasetParamValue,
getParameterByName,
isProcessed,
} from '@edata-portal/core';
import type { Dataset } from '@edata-portal/icat-plus-api';
......@@ -12,14 +13,28 @@ const isBuffer = (dataset: Dataset) => {
return getDatasetParamValue(dataset, 'SAXS_sample_type') === 'buffer';
};
const isIntegration = (dataset: Dataset): boolean => {
return getDatasetName(dataset).indexOf('integrate') !== -1;
};
const checkScanType = (
dataset: Dataset,
scanType: string,
keyword: string,
): boolean => {
// Retrieve the scanType parameter from the dataset
const parameter = getParameterByName('scanType', dataset.parameters);
const isSubtraction = (dataset: Dataset): boolean => {
return getDatasetName(dataset).indexOf('subtract') !== -1;
// Return true if scanType exists and matches the provided type
if (parameter?.value === scanType) {
return true;
}
// Check if the dataset name contains the specified keyword
return getDatasetName(dataset).includes(keyword);
};
const isIntegration = (dataset: Dataset): boolean =>
checkScanType(dataset, 'integration', 'integrate');
const isSubtraction = (dataset: Dataset): boolean =>
checkScanType(dataset, 'subtraction', 'subtract');
function sampleParameters() {
return [
{
......@@ -144,7 +159,7 @@ export function getSnapshotConfigByProcesssedDataset(
processedDataset: Dataset,
): SnashoptConfigItem[] {
/** if subtractiong */
if (processedDataset.name.indexOf('subtract') !== -1) {
if (isSubtraction(processedDataset)) {
return [
{
type: 'metadata',
......@@ -196,7 +211,7 @@ export function getSnapshotConfigByProcesssedDataset(
];
}
if (processedDataset.name.indexOf('integrate') !== -1) {
if (isIntegration(processedDataset)) {
return [
{
type: 'metadata',
......
......@@ -4,26 +4,15 @@ import {
formatDateToDayAndTime,
Gallery,
getDatasetParamValue,
MetadataCard,
MetadataTable,
AttenuatorsWidget,
type MetadataTableParameter,
MetadataTableParameters,
} from '@edata-portal/core';
import { InstrumentSlitWidget } from '@edata-portal/core/src/components/metadata/InstrumentSlitWidget';
import type { Dataset } from '@edata-portal/icat-plus-api';
import { Card, Col, Container, Row } from 'react-bootstrap';
const addParam = (
params: MetadataTableParameter[],
dataset: Dataset,
key: string,
caption: string,
digits?: number,
) => {
const value = getDatasetParamValue(dataset, key) || undefined;
if (value) {
params.push({ caption, value, digits });
}
};
import { Col, Container, Row } from 'react-bootstrap';
const presetSummary = (dataset: Dataset): MetadataTableParameter[] => {
const params: MetadataTableParameter[] = [];
......@@ -34,10 +23,18 @@ const presetSummary = (dataset: Dataset): MetadataTableParameter[] => {
? formatDateToDayAndTime(getDatasetParamValue(dataset, 'endDate'))
: undefined;
if (startTime) {
params.push({ caption: 'Start', value: startTime });
params.push({
caption: 'Start',
value: startTime,
parameterName: 'startDate',
});
}
if (endTime) {
params.push({ caption: 'End', value: endTime });
params.push({
caption: 'End',
value: endTime,
parameterName: 'endDate',
});
}
return params;
};
......@@ -82,6 +79,7 @@ const beamParameters = (dataset: Dataset) => [
)
? `${convertUnitDatasetParameter(dataset, 'TOMOAcquisition_source_sample_distance', 'm', 1)} m`
: '',
parameterName: 'TOMOAcquisition_source_sample_distance',
},
{
caption: 'Ring current at start',
......@@ -93,26 +91,6 @@ const beamParameters = (dataset: Dataset) => [
},
];
const attenuatorsParameters = (dataset: Dataset): MetadataTableParameter[] => {
const params: MetadataTableParameter[] = [];
for (let i = 0; i < 15; i++) {
const index = (i + 1).toString().padStart(2, '0');
const type = getDatasetParamValue(
dataset,
`InstrumentAttenuator${index}_type`,
);
const thickness = getDatasetParamValue(
dataset,
`InstrumentAttenuator${index}_thickness`,
);
if (type && type.toLowerCase() !== 'empty') {
params.push({ caption: `Att${i + 1}`, value: `${type}, ${thickness}` });
}
}
return params;
};
const formatImagesDimensions = (dataset: Dataset) => {
const value = getDatasetParamValue(dataset, `InstrumentDetector01Rois_value`);
const dimensions = value?.split(',');
......@@ -135,6 +113,7 @@ const cameraParameters = (dataset: Dataset) => [
{
caption: 'Images dimensions (HxV)',
value: formatImagesDimensions(dataset),
parameterName: 'InstrumentDetector01Rois_value',
},
{
caption: 'Sample pixel size',
......@@ -155,6 +134,7 @@ const cameraParameters = (dataset: Dataset) => [
value: getDatasetParamValue(dataset, 'TOMOAcquisition_latency_time')
? `${convertUnitDatasetParameter(dataset, 'TOMOAcquisition_latency_time', 'ms', 1)} ms`
: '',
parameterName: 'TOMOAcquisition_latency_time',
},
{
caption: 'Acq mode',
......@@ -169,6 +149,7 @@ const cameraParameters = (dataset: Dataset) => [
value: getDatasetParamValue(dataset, 'TOMOAcquisition_magnification')
? `${convertToFixed(getDatasetParamValue(dataset, 'TOMOAcquisition_magnification'), 2)}x`
: '',
parameterName: 'TOMOAcquisition_magnification',
},
{
caption: 'Optic type',
......@@ -176,145 +157,7 @@ const cameraParameters = (dataset: Dataset) => [
},
];
const primarySlitsParameters = (dataset: Dataset): MetadataTableParameter[] => {
const params: MetadataTableParameter[] = [];
addParam(
params,
dataset,
'InstrumentSlitPrimary_horizontal_gap',
'Horizontal gap',
2,
);
addParam(
params,
dataset,
'InstrumentSlitPrimary_horizontal_offset',
'Horizontal offset',
2,
);
addParam(
params,
dataset,
'InstrumentSlitPrimary_vertical_gap',
'Vertical gap',
2,
);
addParam(
params,
dataset,
'InstrumentSlitPrimary_vertical_offset',
'Vertical offset',
2,
);
return params;
};
const secondarySlitsParameters = (
dataset: Dataset,
): MetadataTableParameter[] => {
const params: MetadataTableParameter[] = [];
addParam(
params,
dataset,
'InstrumentSlitSecondary_horizontal_gap',
'Horizontal gap',
2,
);
addParam(
params,
dataset,
'InstrumentSlitSecondary_horizontal_offset',
'Horizontal offset',
2,
);
addParam(
params,
dataset,
'InstrumentSlitSecondary_vertical_gap',
'Vertical gap',
2,
);
addParam(
params,
dataset,
'InstrumentSlitSecondary_vertical_offset',
'Vertical offset',
2,
);
return params;
};
const otherSlits = (
dataset: Dataset,
): { slitsTitles: string[]; otherSlitsData: MetadataTableParameter[][] } => {
const paramKeys = [
{ key: 'InstrumentSlits_horizontal_gap', caption: 'Horizontal gap' },
{ key: 'InstrumentSlits_horizontal_offset', caption: 'Horizontal offset' },
{ key: 'InstrumentSlits_vertical_gap', caption: 'Vertical gap' },
{ key: 'InstrumentSlits_vertical_offset', caption: 'Vertical offset' },
];
const values = paramKeys.map(({ key }) => getDatasetParamValue(dataset, key));
if (values.every((val) => val === null || val === undefined)) {
return { slitsTitles: ['Slits'], otherSlitsData: [] };
}
const splitArrays = values.map((val) =>
typeof val === 'string'
? val.split(/\s+/).filter(Boolean) // Split on spaces and remove empty entries
: val !== null && val !== undefined
? [String(val)]
: [],
);
// Ensure all split arrays have the same length
const slitsNumber = Math.max(...splitArrays.map((arr) => arr.length));
const allHaveSameLength = splitArrays.every(
(arr) => arr.length === slitsNumber,
);
if (allHaveSameLength) {
// Slit names if available, otherwise "Slits"
const slitNames = getDatasetParamValue(dataset, 'InstrumentSlits_name');
const slitTitles = slitNames
? slitNames
.split(/\s+/)
.slice(0, slitsNumber)
.map((name) => name.trim())
: Array(slitsNumber).fill('Slits');
// Data into separate rows per slit configuration
const slitData = Array.from({ length: slitsNumber }, (_, i) =>
paramKeys.map(({ caption }, j) => ({
caption,
value: splitArrays[j][i] || '',
digits: 2,
})),
);
return {
slitsTitles: slitTitles,
otherSlitsData: slitData,
};
}
return {
slitsTitles: ['Slits'],
otherSlitsData: [
paramKeys
.map(({ caption }, i) =>
values[i] !== null && values[i] !== undefined
? { caption, value: String(values[i]).trim(), digits: 2 }
: null,
)
.filter(Boolean) as MetadataTableParameter[],
],
};
};
export default function TOMOSummaryView({ dataset }: { dataset: Dataset }) {
const { slitsTitles, otherSlitsData } = otherSlits(dataset);
return (
<Container fluid>
<Row className="g-3">
......@@ -327,75 +170,28 @@ export default function TOMOSummaryView({ dataset }: { dataset: Dataset }) {
</Col>
)}
<Col xs="auto">
<TOMOMetadataCard
<MetadataCard
title="Acquisition parameters"
dataset={dataset}
parameters={acquisitionParameters()}
entity={dataset}
content={acquisitionParameters()}
/>
</Col>
<Col xs="auto">
<TOMOMetadataCard
<MetadataCard
title="Beam"
dataset={dataset}
parameters={beamParameters(dataset)}
entity={dataset}
content={beamParameters(dataset)}
/>
</Col>
{attenuatorsParameters(dataset).length > 0 && (
<Col xs="auto">
<TOMOMetadataCard
title="Attenuators"
dataset={dataset}
parameters={attenuatorsParameters(dataset)}
/>
</Col>
)}
<AttenuatorsWidget dataset={dataset} />
<Col xs="auto">
<TOMOMetadataCard
<MetadataCard
title="Camera and optics"
dataset={dataset}
parameters={cameraParameters(dataset)}
entity={dataset}
content={cameraParameters(dataset)}
/>
</Col>
{(primarySlitsParameters(dataset).length > 0 ||
secondarySlitsParameters(dataset).length > 0 ||
otherSlitsData.length > 0) && (
<Col xs="auto">
<Card>
<Card.Header className="text-center">Slits</Card.Header>
<Card.Body>
<Row>
{primarySlitsParameters(dataset).length > 0 && (
<Col xs="auto">
<TOMOMetadataCard
title="Primary Slits"
dataset={dataset}
parameters={primarySlitsParameters(dataset)}
/>
</Col>
)}
{secondarySlitsParameters(dataset).length > 0 && (
<Col xs="auto">
<TOMOMetadataCard
title="Secondary Slits"
dataset={dataset}
parameters={secondarySlitsParameters(dataset)}
/>
</Col>
)}
{otherSlitsData.map((params, index) => (
<Col xs="auto" key={index}>
<TOMOMetadataCard
title={slitsTitles[index] || 'Slits'}
dataset={dataset}
parameters={params}
/>
</Col>
))}
</Row>
</Card.Body>
</Card>
</Col>
)}
<InstrumentSlitWidget dataset={dataset} />
<Col xs="auto">
<Gallery dataset={dataset.outputDatasets?.[0]}></Gallery>
</Col>
......@@ -403,20 +199,3 @@ export default function TOMOSummaryView({ dataset }: { dataset: Dataset }) {
</Container>
);
}
function TOMOMetadataCard({
title,
dataset,
parameters,
}: {
title: string;
dataset: Dataset;
parameters: MetadataTableParameters;
}) {
return (
<Card>
<Card.Header className="text-center">{title}</Card.Header>
<MetadataTable entity={dataset} parameters={parameters}></MetadataTable>
</Card>
);
}
......@@ -18,7 +18,7 @@
"dependencies": {
"@babel/eslint-parser": "^7.25.1",
"@edata-portal/core": "workspace:*",
"@edata-portal/icat-plus-api": "^1.8.12",
"@edata-portal/icat-plus-api": "^1.8.13",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
......
......@@ -18,7 +18,7 @@
"dependencies": {
"@babel/eslint-parser": "^7.25.1",
"@edata-portal/core": "workspace:*",
"@edata-portal/icat-plus-api": "^1.8.12",
"@edata-portal/icat-plus-api": "^1.8.13",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
......
......@@ -13,6 +13,8 @@ import {
} from 'react-bootstrap';
import * as XLSX from 'xlsx';
const HEADER_SEPARATOR = ' - ';
export function SpreadSheetImportExport({
onImport,
data,
......@@ -26,7 +28,12 @@ export function SpreadSheetImportExport({
}) {
return (
<Col xs={'auto'}>
<div className="bg-secondary rounded border p-3 mb-2">
<div
className="bg-gradient-secondary rounded border p-3 mb-2"
style={{
borderColor: 'var(--bs-secondary)',
}}
>
<Row className="g-1">
<Import onImport={onImport} columns={columns} />
<Export data={data} columns={columns} name={name} />
......@@ -41,16 +48,18 @@ function HandsOnTableHelpMessage() {
return (
<Col>
<Row>
<small>
<strong>Supported formats: CSV, XLS, XLSX</strong>
<br />
<i>
Columns should match the table below. You can download the template
file to fill in the data.
<div className="bg-secondary rounded" style={{ marginTop: '4px' }}>
<small>
<strong>Supported formats: CSV, XLS, XLSX</strong>
<br />
Import will overwrite the current table data.
</i>
</small>
<i>
Columns should match the table below. You can download the
template file to fill in the data.
<br />
Import will overwrite the current table data.
</i>
</small>
</div>
</Row>
</Col>
);
......@@ -75,7 +84,7 @@ function Import({
const firstRowIsHeader = firstRow.every((cell, i) => {
const column: InputColumnDescription = columns[i];
return (
String(cell).toLowerCase().trim() ===
String(cell).toLowerCase().split(HEADER_SEPARATOR)[0].trim() ===
(column.header || column.name || '').toLowerCase().trim()
);
});
......@@ -155,11 +164,33 @@ function Export({
const hasData = cleanData.length > 0;
const exportToXLSX = (exporting: any[][], name: string) => {
const headerCounts = columns.reduce(
(acc, column) => {
const key = column.header || column.name || '';
acc[key] = (acc[key] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
// return the columns and data to be exported
// if a column header is duplicated, we add the name
const getColumnsToExport = (exporting: any[][]) => {
const toExport = [
columns.map((c) => c.header || c.name || ''),
columns.map((c) => {
const header = c.header || c.name || '';
return headerCounts[header] > 1 &&
header.toLowerCase() !== c.name.toLowerCase()
? `${header}${HEADER_SEPARATOR}${c.name}`
: header;
}),
...exporting,
];
return toExport;
};
const exportToXLSX = (exporting: any[][], name: string) => {
const toExport = getColumnsToExport(exporting);
const sheet = XLSX.utils.aoa_to_sheet(toExport);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, sheet, name.slice(0, 31));
......@@ -167,10 +198,7 @@ function Export({
};
const exportToCSV = (exporting: any[][], name: string) => {
const toExport = [
columns.map((c) => c.header || c.name || ''),
...exporting,
];
const toExport = getColumnsToExport(exporting);
const csv = Papa.unparse(toExport);
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
......
......@@ -16,7 +16,7 @@
"fix:prettier": "prettier . --write"
},
"dependencies": {
"@edata-portal/icat-plus-api": "^1.8.12",
"@edata-portal/icat-plus-api": "^1.8.13",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
......
<!doctype html>
<html lang="en"></html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Data Portal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
......@@ -19,11 +19,14 @@
"@babel/eslint-parser": "^7.25.1",
"@edata-portal/core": "workspace:*",
"@edata-portal/h5": "workspace:*",
"@edata-portal/icat-plus-api": "^1.8.12",
"@edata-portal/icat-plus-api": "^1.8.13",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@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",
"papaparse": "^5.4.1",
"react": "^18.3.1",
......
{
"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"
}
]
}
}
{
"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>(&gt;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"]
}
}
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 (
<>
<h1>MX Microfrontend Development</h1>
<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;
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from 'App';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
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]);
}
import { ConfigContext } from '@edata-portal/core';
import { useSuspenseQueries } from '@tanstack/react-query';
import { useMemo } from 'react';
declare global {
interface Window {
remoteUrls: any;
}
}
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>
);
}
import { ViewersContext } from '@edata-portal/core';
import { DatasetViewer } from 'standalone/viewers/DatasetViewer';
import { InvestigationViewer } from 'standalone/viewers/InvestigationViewer';
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) => (
<InvestigationViewer
key={investigation.id}
investigation={investigation}
props={props}
/>
),
}}
>
{children}
</ViewersContext.Provider>
);
}
import InvestigationDatasets from 'standalone/InvestigationDatasets';
import { useMemo } from 'react';
import {
createBrowserRouter,
RouteObject,
RouterProvider,
} from 'react-router-dom';
export const routes: RouteObject[] = [
{
path: '/investigation/:investigationId',
element: <InvestigationDatasets></InvestigationDatasets>,
},
{
path: '*',
element: <div>Cheers</div>,
},
];
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} />;
}
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>
);
}
// 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};
}