From ed00e5c110a7b80e4b7fcdc57480227336bf8235 Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Wed, 21 Sep 2022 08:55:42 +0200 Subject: [PATCH 001/117] Fixed wrong entry link creation on overview page. #1045 --- gui/src/components/Quantity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/components/Quantity.js b/gui/src/components/Quantity.js index d2c427c32..45f8f05d6 100644 --- a/gui/src/components/Quantity.js +++ b/gui/src/components/Quantity.js @@ -387,7 +387,7 @@ const quantityPresets = { render: (data) => ( - {data.entry_id} + {data.entry_id} ) -- GitLab From bda222fdcec11f8ef30fed9c83664bfd027fa998 Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Wed, 21 Sep 2022 08:21:20 +0000 Subject: [PATCH 002/117] Resolve "Displaying upload timestamp in local timezone" --- .vscode/settings.json | 3 +- gui/.vscode/settings.json | 16 ++++- gui/src/components/archive/ArchiveBrowser.js | 6 +- gui/src/components/dataset/DatasetsPage.js | 5 +- gui/src/components/entry/OverviewView.js | 62 +++++++++++++------ gui/src/components/entry/OverviewView.spec.js | 4 +- gui/src/components/uploads/ProcessingTable.js | 4 +- gui/src/components/uploads/UploadOverview.js | 3 +- gui/src/components/uploads/UploadsPage.js | 3 +- gui/src/utils.js | 13 +++- nomad/app/v1/routers/datasets.py | 2 +- nomad/archive/required.py | 14 ++++- nomad/doi.py | 2 +- nomad/metainfo/metainfo.py | 19 +++++- tests/metainfo/test_references.py | 17 +++-- 15 files changed, 128 insertions(+), 45 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4e19f47f1..633d893f8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -117,5 +117,6 @@ "tests" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "jest.autoRun": "off" } diff --git a/gui/.vscode/settings.json b/gui/.vscode/settings.json index 928d3d308..3a86e53cf 100644 --- a/gui/.vscode/settings.json +++ b/gui/.vscode/settings.json @@ -1,9 +1,19 @@ -{ +{ + "editor.rulers": [ + 90 + ], + "editor.renderWhitespace": "all", + "editor.tabSize": 4, + "files.trimTrailingWhitespace": true, + "[javascript]": { + "editor.tabSize": 2 + }, "files.exclude": { "**/node_modules": true }, "files.watcherExclude": { "**": true, "**/**": true - } -} \ No newline at end of file + }, + "jest.autoRun": "off" +} diff --git a/gui/src/components/archive/ArchiveBrowser.js b/gui/src/components/archive/ArchiveBrowser.js index f5a611a32..e52cffab8 100644 --- a/gui/src/components/archive/ArchiveBrowser.js +++ b/gui/src/components/archive/ArchiveBrowser.js @@ -57,7 +57,7 @@ import DeleteIcon from '@material-ui/icons/Delete' import XYPlot from './XYPlot' import { titleCase, createUploadUrl, parseNomadUrl, refType, - resolveNomadUrl, resolveInternalRef, systemMetainfoUrl} from '../../utils' + resolveNomadUrl, resolveInternalRef, systemMetainfoUrl, formatTimestamp} from '../../utils' import {EntryButton} from '../nav/Routes' import NavigateIcon from '@material-ui/icons/MoreHoriz' import ReloadIcon from '@material-ui/icons/Replay' @@ -609,7 +609,7 @@ function QuantityItemPreview({value, def}) { } else { - let finalValue = (def.type.type_data === 'nomad.metainfo.metainfo._Datetime' ? new Date(value).toLocaleString() : value) + let finalValue = (def.type.type_data === 'nomad.metainfo.metainfo._Datetime' ? formatTimestamp(value) : value) let finalUnit if (def.unit) { const a = new Q(finalValue, def.unit).toSystem(units) @@ -631,7 +631,7 @@ const QuantityValue = React.memo(function QuantityValue({value, def}) { const units = useUnits() const getRenderValue = useCallback(value => { - let finalValue = (def.type.type_data === 'nomad.metainfo.metainfo._Datetime' ? new Date(value).toLocaleString() : value) + let finalValue = (def.type.type_data === 'nomad.metainfo.metainfo._Datetime' ? formatTimestamp(value) : value) let finalUnit if (def.unit) { const a = new Q(finalValue, def.unit).toSystem(units) diff --git a/gui/src/components/dataset/DatasetsPage.js b/gui/src/components/dataset/DatasetsPage.js index 552f3bc92..ee5f4d80d 100644 --- a/gui/src/components/dataset/DatasetsPage.js +++ b/gui/src/components/dataset/DatasetsPage.js @@ -33,6 +33,7 @@ import Quantity from '../Quantity' import DialogContentText from '@material-ui/core/DialogContentText' import DialogActions from '@material-ui/core/DialogActions' import { SourceApiCall, SourceApiDialogButton } from '../buttons/SourceDialogButton' +import { formatTimestamp } from '../../utils' export const help = ` NOMAD allows you to create *datasets* from your data. A dataset is like a tag that you @@ -63,12 +64,12 @@ const columns = [ { key: 'dataset_create_time', label: 'Create time', - render: dataset => new Date(dataset.dataset_create_time).toLocaleString() + render: dataset => formatTimestamp(dataset.dataset_create_time) }, { key: 'dataset_modified_time', label: 'Modify time', - render: dataset => (dataset.dataset_modified_time ? new Date(dataset.dataset_modified_time).toLocaleString() : '') + render: dataset => (dataset.dataset_modified_time ? formatTimestamp(dataset.dataset_modified_time) : '') } ] diff --git a/gui/src/components/entry/OverviewView.js b/gui/src/components/entry/OverviewView.js index b1ea5404b..286f298d5 100644 --- a/gui/src/components/entry/OverviewView.js +++ b/gui/src/components/entry/OverviewView.js @@ -42,6 +42,7 @@ import { } from '../archive/ArchiveBrowser' import { useErrors } from '../errors' import DefinitionsCard from './properties/DefinitionsCard' +import { ErrorHandler } from '../ErrorHandler' function MetadataSection({title, children}) { return @@ -223,27 +224,50 @@ const OverviewView = React.memo((props) => { )} {sections .map((section, index) => ( - + + + )) } - - - {index?.results?.material?.topology - ? - : - } - - - - - - - - + + + + + + + + {index?.results?.material?.topology + ? + : } + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/src/components/entry/OverviewView.spec.js b/gui/src/components/entry/OverviewView.spec.js index a62e8fa90..b9eefcc14 100644 --- a/gui/src/components/entry/OverviewView.spec.js +++ b/gui/src/components/entry/OverviewView.spec.js @@ -66,8 +66,8 @@ test('correctly renders metadata and all properties', async () => { expectQuantity(undefined, `${index.nomad_version}/${index.nomad_commit}`, 'processing version', 'Version used in the last processing') // TODO: add the following to the state for testing. // expectQuantity('datasets', index.datasets[0].dataset_name) - // expectQuantity('upload_create_time', new Date(index.upload_create_time).toLocaleString()) - // expectQuantity('last_processing_time', new Date(index.last_processing_time).toLocaleString()) + // expectQuantity('upload_create_time', formatTimestamp(index.upload_create_time)) + // expectQuantity('last_processing_time', formatTimestamp(index.last_processing_time)) // Check if all material data is shown (on the right, in the materials card) expectComposition(index) diff --git a/gui/src/components/uploads/ProcessingTable.js b/gui/src/components/uploads/ProcessingTable.js index c65efb050..93bfe137f 100644 --- a/gui/src/components/uploads/ProcessingTable.js +++ b/gui/src/components/uploads/ProcessingTable.js @@ -28,7 +28,7 @@ import EntryDownloadButton from '../entry/EntryDownloadButton' import DeleteEntriesButton from './DeleteEntriesButton' import Quantity from '../Quantity' import EditMetaDataDialog from './EditMetaDataDialog' -import {pluralize} from '../../utils' +import {formatTimestamp, pluralize} from '../../utils' import { useUploadPageContext } from './UploadPageContext' const columns = [ @@ -58,7 +58,7 @@ const columns = [ key: 'complete_time', align: 'left', sortable: false, - render: entry => new Date(entry.complete_time).toLocaleString() + render: entry => formatTimestamp(entry.complete_time) }, {key: 'comment', sortable: false, align: 'left'}, { diff --git a/gui/src/components/uploads/UploadOverview.js b/gui/src/components/uploads/UploadOverview.js index ee6557551..888486475 100644 --- a/gui/src/components/uploads/UploadOverview.js +++ b/gui/src/components/uploads/UploadOverview.js @@ -48,6 +48,7 @@ import CreateEntry from './CreateEntry' import { useUploadPageContext } from './UploadPageContext' import { useApi } from '../api' import ReloadIcon from '@material-ui/icons/Replay' +import { formatTimestamp } from '../../utils' const useDropButtonStyles = makeStyles(theme => ({ dropzone: { @@ -543,7 +544,7 @@ function UploadOverview(props) { Publish {isPublished && - {upload?.with_embargo ? `This upload has been published under embargo with a period of ${upload?.embargo_length} months from ${new Date(upload?.publish_time).toLocaleString()}.` + {upload?.with_embargo ? `This upload has been published under embargo with a period of ${upload?.embargo_length} months from ${formatTimestamp(upload?.publish_time)}.` : `This upload has already been published.`} } {!isPublished && } diff --git a/gui/src/components/uploads/UploadsPage.js b/gui/src/components/uploads/UploadsPage.js index 276fb180b..d712b36d1 100644 --- a/gui/src/components/uploads/UploadsPage.js +++ b/gui/src/components/uploads/UploadsPage.js @@ -45,6 +45,7 @@ import Quantity from '../Quantity' import { SourceApiCall, SourceApiDialogButton } from '../buttons/SourceDialogButton' import { useHistory } from 'react-router-dom' import DeleteUploadsButton from './DeleteUploadsButton' +import { formatTimestamp } from '../../utils' export const help = ` NOMAD allows you to upload data. After upload, NOMAD will process your data: it will @@ -131,7 +132,7 @@ export function useUploadsPageContext() { const columns = [ { key: 'upload_create_time', - render: upload => new Date(upload.upload_create_time).toLocaleString() + render: upload => formatTimestamp(upload.upload_create_time) }, {key: 'upload_name'}, { diff --git a/gui/src/utils.js b/gui/src/utils.js index 97adae0db..2fe2bd1a0 100644 --- a/gui/src/utils.js +++ b/gui/src/utils.js @@ -375,7 +375,18 @@ export function formatInteger(value) { * @return {str} The timestamp with new formatting */ export function formatTimestamp(value) { - return value && new Date(value).toLocaleString() + if (value.search(/(\+|Z)/) === -1) { // search for timezone information + try { + // assume UTC timestamp from server and attempt to manually add UTC timezone, + // new Date will wrongly assume local timezone. + const result = new Date(`${value}Z`).toLocaleString() + if (result !== 'Invalid Date') { + return result + } + } catch {} + } + // create string for timestamp with correct timezone information or fallback + return new Date(value).toLocaleString() } /** diff --git a/nomad/app/v1/routers/datasets.py b/nomad/app/v1/routers/datasets.py index 5069b9b9a..f9490d7b6 100644 --- a/nomad/app/v1/routers/datasets.py +++ b/nomad/app/v1/routers/datasets.py @@ -259,7 +259,7 @@ async def post_datasets( Create a new dataset. ''' - now = datetime.now() + now = datetime.utcnow() dataset_type = create.dataset_type if create.dataset_type is not None else DatasetType.owned # check if name already exists diff --git a/nomad/archive/required.py b/nomad/archive/required.py index d511bb86b..e71f6d3d7 100644 --- a/nomad/archive/required.py +++ b/nomad/archive/required.py @@ -261,8 +261,13 @@ class RequiredReader: result[prop] = value.to_list() if isinstance(value, ArchiveList) else value continue - result[prop] = [handle_item(item) for item in value] if isinstance( - value, (list, ArchiveList)) else handle_item(value) + try: + result[prop] = [handle_item(item) for item in value] if isinstance( + value, (list, ArchiveList)) else handle_item(value) + except ArchiveError as e: + # We continue just logging the error. Unresolvable references + # will appear as unset references in the returned archive. + utils.get_logger(__name__).error('archive error', exc_info=e) return result @@ -416,6 +421,11 @@ class RequiredReader: result[prop] = [self._apply_required(val, item, dataset) for item in archive_child] else: result[prop] = self._apply_required(val, archive_child, dataset) + except ArchiveError as e: + # We continue just logging the error. Unresolvable references + # will appear as unset references in the returned archive. + utils.get_logger(__name__).error('archive error', exc_info=e) + continue except (KeyError, IndexError): continue diff --git a/nomad/doi.py b/nomad/doi.py index 5f71a0957..c12af60a9 100644 --- a/nomad/doi.py +++ b/nomad/doi.py @@ -112,7 +112,7 @@ class DOI(Document): # to create new DOIs based on a counter per day until we find a non existing DOI. # This might be bad if many DOIs per day are to be expected. counter = 1 - create_time = datetime.datetime.now() + create_time = datetime.datetime.utcnow() while True: doi_str = '%s/NOMAD/%s-%d' % ( diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index 3b3713856..790f9b84d 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -103,6 +103,8 @@ validElnComponents = { 'reference': ['ReferenceEditQuantity'] } +_unset_value = '__UNSET__' + def _default_hash(): return hashlib.new(_hash_method) @@ -969,6 +971,12 @@ class QuantityReference(Reference): def serialize_proxy_value(self, proxy): return f'{proxy.m_proxy_value}/{self.target_quantity_def.name}' + def set_normalize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: + if not value.m_is_set(self.target_quantity_def): + return _unset_value + + return super().set_normalize(section, quantity_def, value) + def get_normalize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: section = super().get_normalize(section, quantity_def, value) return getattr(section, self.target_quantity_def.name) @@ -1700,12 +1708,21 @@ class MSection(metaclass=MObjectMeta): # TODO find a way to make this a subclas if dimensions == 0: value = self.__set_normalize(quantity_def, value) + if value == _unset_value: + return + elif dimensions == 1: if type(value) == str or not isinstance(value, IterableABC): raise TypeError( f'The shape of {quantity_def} requires an iterable value, but {value} is not iterable.') - value = list(self.__set_normalize(quantity_def, item) for item in value) + list_value = list() + for item in value: + item_value = self.__set_normalize(quantity_def, item) + if item_value == _unset_value: + continue + list_value.append(item_value) + value = list_value def __check_shape(shape): if not isinstance(shape, str) or shape == '*': diff --git a/tests/metainfo/test_references.py b/tests/metainfo/test_references.py index f6bf0e81c..812ae575c 100644 --- a/tests/metainfo/test_references.py +++ b/tests/metainfo/test_references.py @@ -128,12 +128,11 @@ def test_section_proxy(example_data): def test_quantity_proxy(example_data): - example_data.referencing.quantity_reference = MProxy( - 'doesnotexist', - m_proxy_section=example_data.referencing, - m_proxy_type=Referencing.section_reference.type) with pytest.raises(MetainfoReferenceError): - example_data.referencing.quantity_reference + example_data.referencing.quantity_reference = MProxy( + 'doesnotexist', + m_proxy_section=example_data.referencing, + m_proxy_type=Referencing.section_reference.type) example_data.referencing.quantity_reference = MProxy( '/referenced', @@ -187,6 +186,14 @@ def test_quantity_references_serialize(): assert source == root.m_to_dict() +def test_quantity_references_unset_serialize(): + referencing = Referencing() + referencing.quantity_reference = Referenced() + + assert not referencing.m_is_set(Referencing.quantity_reference) + assert 'quantity_reference' not in referencing.m_to_dict() + + def test_section_reference_serialize(): class TargetSection(MSection): test_quantity = Quantity(type=str) -- GitLab From a7bbd8291e30801ddc781c7c99f5cc56faf05b0a Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Wed, 21 Sep 2022 12:39:25 +0200 Subject: [PATCH 003/117] Added docus for using gitlab. Refactored develop documentation. --- docs/develop/gitlab.md | 157 ++++++++++++ docs/develop/guides.md | 194 ++++++++++++++ docs/{ => develop}/normalizers.md | 0 docs/{ => develop}/parser.md | 0 docs/{ => develop}/search.md | 0 docs/{developers.md => develop/setup.md} | 306 +---------------------- docs/schema/python.md | 4 +- mkdocs.yml | 10 +- 8 files changed, 369 insertions(+), 302 deletions(-) create mode 100644 docs/develop/gitlab.md create mode 100644 docs/develop/guides.md rename docs/{ => develop}/normalizers.md (100%) rename docs/{ => develop}/parser.md (100%) rename docs/{ => develop}/search.md (100%) rename docs/{developers.md => develop/setup.md} (56%) diff --git a/docs/develop/gitlab.md b/docs/develop/gitlab.md new file mode 100644 index 000000000..c2d904422 --- /dev/null +++ b/docs/develop/gitlab.md @@ -0,0 +1,157 @@ +--- +title: Using Git/GitLab +--- + +## Branches + +We use *protected* and *feature* branches. You must not (even if you have +the rights) commit to it directly. + +- `master`, a *protected* branch and also the *default* branch. Represents the latest stable +release (i.e. what the current official NOMAD runs on). +- `develop`, a quasi *protected* branch. It contains the latest, potentially +unreleased, features and fixes. +- *feature branches*, this is where you work. Typically they are automatically +named after issues: `-`. +- `vX.X.X`, tags for releases. + + +## Flow: From issue to merge + +### Create issues + +Everyone with an MPCDF GitLab account can create issues. + +- Use descriptive short titles. Specific words over general works. Try to describe +the problem and do not assume causes. +- You do not need to greet us or say thx and goodbye. Let's keep it purely technically. +- For bugs: think about how to re-produce the problem and provide examples when applicable. +- For features: in addition to the feature descriptive, try to provide a use-case that +might help us understand the feature and its scope. +- You can label the issue if you know the label system. + +### Labeling issues + +There are three main categories for labels. Ideally each issue gets one of each category: + +- The *state* labels (grey). Those are used to manage when and how the issue is addressed. +This should be given the NOMAD team member who is currently responsible to moderate the +development. If its a simple fix and you want to do it yourself, assign yourself and use +"bugfixes". + +- The *component* label (purple). This denote the part of the NOMAD software that is +most likely effected. Multiple purple labels are possible. + +- The *kind* label (key). Wether this is a bug, feature, refactoring, or documentation issue. + +Unlabeled issues will get labeled by the NOMAD team as soon as possible. + + +### Working on an issue + +Within the development team and during development meetings we decide who is acting on +an issue. The person is assigned to the issue on GitLab. It typically switches its grey *state* label +from *backlog* to *current* or *bugfixes*. The assigned person is responsible to further +discuss the problem with the issue author and involve more people if necessary. + +To contribute code changes, you should create a branch and merge request (MR) from the +GitLab issue via the button offered by GitLab. This will create a branch of *develop* and +create a merge request that targets *develop*. + +You can work on this branch and push as often as you like. Each push will cause a pipeline +to run. The solution can be discussed in the merge request. + + +### Code review and merge + +When you are satisfied with your solution, and your CI/CD pipeline passes you can mark your MR as *ready*. +To review GUI changes, you should deploy your branch to the dev-cluster via CI/CD actions. +Find someone on the NOMAD developer team to review your MR and request a review through +GitLab. The review should be performed shortly and should not stall the MR longer than +two full work days. + +The reviewer will open *threads* that need to be solved by the MR author. If all +threads are resolved, you can re-request a review. The reviewer should eventually merge +the MR. Typically we squash MRs to keep the revision history short. +This will typically auto-close the issue. + +## Clean version history + +It is often necessary to consider code history to reason about potential problems in +our code. This can only be done, if we keep a "clean" history. + +- Use descriptive commit messages. Use simple verbs (*added*, *removed*, *refactored*, etc.) +name features and changed components. [Include issue numbers](https://docs.gitlab.com/ee/user/project/issues/crosslinking_issues.html) +to create links in gitlab. + +- Learn how to amend to avoid lists of small related commits. + +- Learn how to rebase. Only merging feature-branches should create merge commits. + +- Squash commits when merging. + +- Some videos on more advanced git usage: https://youtube.be/Uszj_k0DGsg, https://youtu.be/qsTthZi23VE + +### amend +While working on a feature, there are certain practices that will help us to create +a clean history with coherent commits, where each commit stands on its own. + +```sh + git commit --amend +``` + +If you committed something to your own feature branch and then realize by CI that you have +some tiny error in it that you need to fix, try to amend this fix to the last commit. +This will avoid unnecessary tiny commits and foster more coherent single commits. With *amend* +you are basically adding changes to the last commit, i.e. editing the last commit. If +you push, you need to force it `git push origin feature-branch --force-with-lease`. So be careful, and +only use this on your own branches. + +### rebase +```sh + git rebase +``` + +Lets assume you work on a bigger feature that takes more time. You might want to merge +the version branch into your feature branch from time to time to get the recent changes. +In these cases, use rebase and not merge. Rebase puts your branch commits in front of the +merged commits instead of creating a new commit with two ancestors. It basically moves the +point where you initially branched away from the version branch to the current position in +the version branch. This will avoid merges, merge commits, and generally leave us with a +more consistent history. You can also rebase before creating a merge request, which basically +allows no-op merges. Ideally the only real merges that we ever have, are between +version branches. + + +### squash +```sh + git merge --squash +``` + +When you need multiple branches to implement a feature and merge between them, try to +use *squash*. Squashing basically puts all commits of the merged branch into a single commit. +It basically allows you to have many commits and then squash them into one. This is useful +if these commits were made just to synchronize between workstations, due to +unexpected errors in CI/CD, because you needed a save point, etc. Again the goal is to have +coherent commits, where each commits makes sense on its own. + + +## Submodules + + +The main NOMAD GitLab-project (`nomad-fair`) uses Git-submodules to maintain its +parsers and other dependencies. All these submodules are places in the `/dependencies` +directory. There are helper scripts to install (`./dependencies.sh`) and +commit changes to all submodules (`./dependencies-git.sh`). After merging or checking out, +you have to make sure that the modules are updated to not accidentally commit old +submodule commits again. Usually you do the following to check if you really have a +clean working directory. + +```sh + git checkout something-with-changes + git submodule update --init --recursive + git status +``` + +We typically use the `master`/`main` branch on all dependencies. Of course feature branches +can be used on dependencies to manage work in progress. diff --git a/docs/develop/guides.md b/docs/develop/guides.md new file mode 100644 index 000000000..b45e93e80 --- /dev/null +++ b/docs/develop/guides.md @@ -0,0 +1,194 @@ +--- +title: Code guidelines +--- + +NOMAD has a long history and many people are involved in its development. These +guidelines are set out to keep the code quality high and consistent. Please read +them carefully. + +## Principles and rules + +- simple first, complicated only when necessary +- search and adopt generic established 3rd party solutions before implementing specific solutions +- only uni directional dependencies between components/modules, no circles +- only one language: Python (except, GUI of course) + +The are some *rules* or better strong *guidelines* for writing code. The following +applies to all python code (and were applicable, also to JS and other code): + +- Use an IDE (e.g. [vscode](https://code.visualstudio.com/) or otherwise automatically + enforce code ([formatting and linting](https://code.visualstudio.com/docs/python/linting)). + Use `nomad qa` before committing. This will run all tests, static type checks, linting, etc. + +- There is a style guide to python. Write [pep-8](https://www.python.org/dev/peps/pep-0008/) + compliant python code. An exception is the line cap at 79, which can be broken but keep it 90-ish. + +- Test the public interface of each sub-module (i.e. python file) + +- Be [pythonic](https://docs.python-guide.org/writing/style/) and watch + [this](https://www.youtube.com/watch?v=wf-BqAjZb8M). + +- Add doc-strings to the *public* interface of each sub-module (e.g. python file). Public meaning API that + is exposed to other sub-modules (i.e. other python files). + +- Use google [docstrings](http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) and + use Markdown. + +- The project structure is according to [this guide](https://docs.python-guide.org/writing/structure/). + Keep it! + +- Write tests for all contributions. + +- Adopt *Clean Code* practices. Here is a [good introduction](https://youtu.be/7EmboKQH8lM). + + +## Enforcing Rules with CI/CD + +These *guidelines* are partially enforced by CI/CD. As part of CI all tests are run on all +branches; further we run a *linter*, *pep8* checker, and *mypy* (static type checker). You can +run `nomad qa` to run all these tests and checks before committing. + +See [git/gitlab](./gitlab.md) for more details on how to work with issues, branches, merge +requests, and CI/CD. + +## Names and identifiers + +There is a certain terminology consistently used in this documentation and the source +code. Use this terminology for identifiers. + +Do not use abbreviations. There are (few) exceptions: `proc` (processing); `exc`, `e` (exception); +`calc` (calculation), `repo` (repository), `utils` (utilities), and `aux` (auxiliary). +Other exceptions are `f` for file-like streams and `i` for index running variables. +Btw., the latter is almost never necessary in python. + +Terms: + +- upload: A logical unit that comprises a collection of files uploaded by a user, organized + in a directory structure. +- entry: An archive item, created by parsing a *mainfile*. Each entry belongs to an upload and + is associated with various metadata (an upload may have many entries). +- child entry: Some parsers generate multiple entries - a *main* entry plus some number of + *child* entries. Child entries are identified by the mainfile plus a *mainfile_key* + (string value). +- calculation: denotes the results of a theoretical computation, created by CMS code. + Note that entries do not have to be based on calculations; they can also be based on + experimental results. +- raw file: A user uploaded file, located somewhere in the upload's directory structure. +- mainfile: A raw file identified as parseable, defining an entry of the upload in question. +- aux file: Additional files the user uploaded within an upload. +- entry metadata: Some quantities of an entry that are searchable in NOMAD. +- archive data: The normalized data of an entry in nomad's meta-info-based format. + +Throughout nomad, we use different ids. If something +is called *id*, it is usually a random uuid and has no semantic connection to the entity +it identifies. If something is called a *hash* then it is a hash generated based on the +entity it identifies. This means either the whole thing or just some properties of +this entities. + +- The most common hashes is the `entry_hash` based on mainfile and auxfile contents. +- The `upload_id` is a UUID assigned to the upload on creation. It never changes. +- The `mainfile` is a path within an upload that points to a file identified as parseable. + This also uniquely identifies an entry within the upload. +- The `entry_id` (previously called `calc_id`) uniquely identifies an entry. It is a hash + over the `mainfile` and respective `upload_id`. **NOTE:** For backward compatibility, + `calc_id` is also still supported in the api, but using it is strongly discouraged. +- We often use pairs of `upload_id/entry_id`, which in many contexts allow to resolve an entry-related + file on the filesystem without having to ask a database about it. +- The `pid` or (`coe_calc_id`) is a legacy sequential interger id, previously used to identify + entries. We still store the `pid` on these older entries for historical purposes. +- Calculation `handle` or `handle_id` are created based on those `pid`. + To create hashes we use :py:func:`nomad.utils.hash`. + + +## Logging + +There are three important prerequisites to understand about nomad-FAIRDI's logging: + +- All log entries are recorded in a central elastic search database. To make this database + useful, log entries must be sensible in size, frequence, meaning, level, and logger name. + Therefore, we need to follow some rules when it comes to logging. +- We use an *structured* logging approach. Instead of encoding all kinds of information + in log messages, we use key-value pairs that provide context to a log *event*. In the + end all entries are stored as JSON dictionaries with `@timestamp`, `level`, + `logger_name`, `event` plus custom context data. Keep events very short, most + information goes into the context. +- We use logging to inform about the state of nomad-FAIRDI, not about user + behavior, input, or data. Do not confuse this when determining the log-level for an event. + For example, a user providing an invalid upload file should never be an error. + +Please follow the following rules when logging: + +- If a logger is not already provided, only use + :py:func:`nomad.utils.get_logger` to acquire a new logger. Never use the + build-in logging directly. These logger work like the system loggers, but + allow you to pass keyword arguments with additional context data. See also + the [structlog docs](https://structlog.readthedocs.io/en/stable/). +- In many context, a logger is already provided (e.g. api, processing, parser, normalizer). + This provided logger has already context information bounded. So it is important to + use those instead of acquiring your own loggers. Have a look for methods called + `get_logger` or attributes called `logger`. +- Keep events (what usually is called *message*) very short. Examples are: *file uploaded*, + *extraction failed*, etc. +- Structure the keys for context information. When you analyse logs in ELK, you will + see that the set of all keys over all log entries can be quit large. Structure your + keys to make navigation easier. Use keys like `nomad.proc.parser_version` instead of + `parser_version`. Use module names as prefixes. +- Don't log everything. Try to anticipate, how you would use the logs in case of bugs, + error scenarios, etc. +- Don't log sensitive data. +- Think before logging data (especially dicts, list, numpy arrays, etc.). +- Logs should not be abused as a *printf*-style debugging tool. + +The following keys are used in the final logs that are piped to Logstash. +Notice that the key name is automatically formed by a separate formatter and +may differ from the one used in the actual log call. + +Keys that are autogenerated for all logs: + + - `@timestamp`: Timestamp for the log + - `@version`: Version of the logger + - `host`: The host name from which the log originated + - `path`: Path of the module from which the log was created + - `tags`: Tags for this log + - `type`: The *message_type* as set in the LogstashFormatter + - `level`: The log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` + - `logger_name`: Name of the logger + - `nomad.service`: The service name as configured in `config.py` + - `nomad.release`: The release name as configured in `config.py` + +Keys that are present for events related to processing an entry: + + - `nomad.upload_id`: The id of the currently processed upload + - `nomad.entry_id`: The id of the currently processed entry + - `nomad.mainfile`: The mainfile of the currently processed entry + +Keys that are present for events related to exceptions: + + - `exc_info`: Stores the full python exception that was encountered. All + uncaught exceptions will be stored automatically here. + - `digest`: If an exception was raised, the last 256 characters of the message + are stored automatically into this key. If you wish to search for exceptions + in Kibana, you will want to use this value as it will be indexed unlike the + full exception object. + + +## Copyright Notices + +We follow this [recommendation](https://www.linuxfoundation.org/blog/2020/01/copyright-notices-in-open-source-software-projects/) +of the Linux Foundation for the copyright notice that is placed on top of each source +code file. + +It is intended to provide a broad generic statement that allows all authors/contributors +of the NOMAD project to claim their copyright, independent of their organization or +individual ownership. + +You can simply copy the notice from another file. From time to time we can use a tool +like [licenseheaders](https://pypi.org/project/licenseheaders/) to ensure correct +notices. In addition we keep an purely informative AUTHORS file. + + +## Git submodules and other "in-house" dependencies + +As the NOMAD eco-systems grows, you might develop libraries that are used by NOMAD instead +of being part of its main code-base. The same guide-lines should apply. You can +use git-hub actions if you library is hosted on github to ensure automated linting and tests. diff --git a/docs/normalizers.md b/docs/develop/normalizers.md similarity index 100% rename from docs/normalizers.md rename to docs/develop/normalizers.md diff --git a/docs/parser.md b/docs/develop/parser.md similarity index 100% rename from docs/parser.md rename to docs/develop/parser.md diff --git a/docs/search.md b/docs/develop/search.md similarity index 100% rename from docs/search.md rename to docs/develop/search.md diff --git a/docs/developers.md b/docs/develop/setup.md similarity index 56% rename from docs/developers.md rename to docs/develop/setup.md index c1ecfe47c..5e214a3a8 100644 --- a/docs/developers.md +++ b/docs/develop/setup.md @@ -1,7 +1,15 @@ --- title: Getting started --- -# Getting started with developing NOMAD +# Setup a dev environment, run the app, and run test + +This is a step-by-step guide to get started with NOMAD development. You will clone +all sources, set-up a *Python* and *node* environment, install all necessary dependency, +run the infrastructure in development mode, learn to run out test-suites, and setup-up +*Visual Studio Code* for NOMAD development. + +This is not about working with the NOMAD Python package. You can find the `nomad-lab` +documentation [here](../pythonlib.md). ## Clone the sources If not already done, you should clone nomad. If you have a gitlab@MPCDF account, you can clone with git URL: @@ -521,298 +529,4 @@ this folder into vscode extensions folder `~/.vscode/extensions/` or create an i vsce package ``` -then install the extension by drag the file `nomad-0.0.x.vsix` and drop it into the extension panel of the vscode. - -## Code guidelines - -### Principles and rules - -- simple first, complicated only when necessary -- adopting generic established 3rd party solutions before implementing specific solutions -- only uni directional dependencies between components/modules, no circles -- only one language: Python (except, GUI of course) - -The are some *rules* or better strong *guidelines* for writing code. The following -applies to all python code (and were applicable, also to JS and other code): - -- Use an IDE (e.g. [vscode](https://code.visualstudio.com/) or otherwise automatically - enforce code ([formatting and linting](https://code.visualstudio.com/docs/python/linting)). - Use `nomad qa` before committing. This will run all tests, static type checks, linting, etc. - -- There is a style guide to python. Write [pep-8](https://www.python.org/dev/peps/pep-0008/) - compliant python code. An exception is the line cap at 79, which can be broken but keep it 90-ish. - -- Test the public API of each sub-module (i.e. python file) - -- Be [pythonic](https://docs.python-guide.org/writing/style/) and watch - [this](https://www.youtube.com/watch?v=wf-BqAjZb8M). - -- Document any *public* API of each sub-module (e.g. python file). Public meaning API that - is exposed to other sub-modules (i.e. other python files). - -- Use google [docstrings](http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). - -- Add your doc-strings to the sphinx documentation in `docs`. Use .md, follow the example. - Markdown in sphinx is supported via [recommonmark](https://recommonmark.readthedocs.io/en/latest/index.html#autostructify) - and [AutoStructify](http://recommonmark.readthedocs.io/en/latest/auto_structify.html) - -- The project structure is according to [this guide](https://docs.python-guide.org/writing/structure/). - Keep it! - -- Write tests for all contributions. - - -### Enforcing Rules with CI/CD - -These *guidelines* are partially enforced by CI/CD. As part of CI all tests are run on all -branches; further we run a *linter*, *pep8* checker, and *mypy* (static type checker). You can -run `nomad qa` to run all these tests and checks before committing. - -The CI/CD will run on all refs that do not start with `dev-`. The CI/CD will -not release or deploy anything automatically, but it can be manually triggered after the -build and test stage completed successfully. - -### Names and identifiers - -There is a certain terminology consistently used in this documentation and the source -code. Use this terminology for identifiers. - -Do not use abbreviations. There are (few) exceptions: `proc` (processing); `exc`, `e` (exception); -`calc` (calculation), `repo` (repository), `utils` (utilities), and `aux` (auxiliary). -Other exceptions are `f` for file-like streams and `i` for index running variables. -Btw., the latter is almost never necessary in python. - -Terms: - -- upload: A logical unit that comprises a collection of files uploaded by a user, organized - in a directory structure. -- entry: An archive item, created by parsing a *mainfile*. Each entry belongs to an upload and - is associated with various metadata (an upload may have many entries). -- child entry: Some parsers generate multiple entries - a *main* entry plus some number of - *child* entries. Child entries are identified by the mainfile plus a *mainfile_key* - (string value). -- calculation: denotes the results of a theoretical computation, created by CMS code. - Note that entries do not have to be based on calculations; they can also be based on - experimental results. -- raw file: A user uploaded file, located somewhere in the upload's directory structure. -- mainfile: A raw file identified as parseable, defining an entry of the upload in question. -- aux file: Additional files the user uploaded within an upload. -- entry metadata: Some quantities of an entry that are searchable in NOMAD. -- archive data: The normalized data of an entry in nomad's meta-info-based format. - -Throughout nomad, we use different ids. If something -is called *id*, it is usually a random uuid and has no semantic connection to the entity -it identifies. If something is called a *hash* then it is a hash generated based on the -entity it identifies. This means either the whole thing or just some properties of -this entities. - -- The most common hashes is the `entry_hash` based on mainfile and auxfile contents. -- The `upload_id` is a UUID assigned to the upload on creation. It never changes. -- The `mainfile` is a path within an upload that points to a file identified as parseable. - This also uniquely identifies an entry within the upload. -- The `entry_id` (previously called `calc_id`) uniquely identifies an entry. It is a hash - over the `mainfile` and respective `upload_id`. **NOTE:** For backward compatibility, - `calc_id` is also still supported in the api, but using it is strongly discouraged. -- We often use pairs of `upload_id/entry_id`, which in many contexts allow to resolve an entry-related - file on the filesystem without having to ask a database about it. -- The `pid` or (`coe_calc_id`) is a legacy sequential interger id, previously used to identify - entries. We still store the `pid` on these older entries for historical purposes. -- Calculation `handle` or `handle_id` are created based on those `pid`. - To create hashes we use :py:func:`nomad.utils.hash`. - - -### Logging - -There are three important prerequisites to understand about nomad-FAIRDI's logging: - -- All log entries are recorded in a central elastic search database. To make this database - useful, log entries must be sensible in size, frequence, meaning, level, and logger name. - Therefore, we need to follow some rules when it comes to logging. -- We use an *structured* logging approach. Instead of encoding all kinds of information - in log messages, we use key-value pairs that provide context to a log *event*. In the - end all entries are stored as JSON dictionaries with `@timestamp`, `level`, - `logger_name`, `event` plus custom context data. Keep events very short, most - information goes into the context. -- We use logging to inform about the state of nomad-FAIRDI, not about user - behavior, input, or data. Do not confuse this when determining the log-level for an event. - For example, a user providing an invalid upload file should never be an error. - -Please follow the following rules when logging: - -- If a logger is not already provided, only use - :py:func:`nomad.utils.get_logger` to acquire a new logger. Never use the - build-in logging directly. These logger work like the system loggers, but - allow you to pass keyword arguments with additional context data. See also - the [structlog docs](https://structlog.readthedocs.io/en/stable/). -- In many context, a logger is already provided (e.g. api, processing, parser, normalizer). - This provided logger has already context information bounded. So it is important to - use those instead of acquiring your own loggers. Have a look for methods called - `get_logger` or attributes called `logger`. -- Keep events (what usually is called *message*) very short. Examples are: *file uploaded*, - *extraction failed*, etc. -- Structure the keys for context information. When you analyse logs in ELK, you will - see that the set of all keys over all log entries can be quit large. Structure your - keys to make navigation easier. Use keys like `nomad.proc.parser_version` instead of - `parser_version`. Use module names as prefixes. -- Don't log everything. Try to anticipate, how you would use the logs in case of bugs, - error scenarios, etc. -- Don't log sensitive data. -- Think before logging data (especially dicts, list, numpy arrays, etc.). -- Logs should not be abused as a *printf*-style debugging tool. - -The following keys are used in the final logs that are piped to Logstash. -Notice that the key name is automatically formed by a separate formatter and -may differ from the one used in the actual log call. - -Keys that are autogenerated for all logs: - - - `@timestamp`: Timestamp for the log - - `@version`: Version of the logger - - `host`: The host name from which the log originated - - `path`: Path of the module from which the log was created - - `tags`: Tags for this log - - `type`: The *message_type* as set in the LogstashFormatter - - `level`: The log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` - - `logger_name`: Name of the logger - - `nomad.service`: The service name as configured in `config.py` - - `nomad.release`: The release name as configured in `config.py` - -Keys that are present for events related to processing an entry: - - - `nomad.upload_id`: The id of the currently processed upload - - `nomad.entry_id`: The id of the currently processed entry - - `nomad.mainfile`: The mainfile of the currently processed entry - -Keys that are present for events related to exceptions: - - - `exc_info`: Stores the full python exception that was encountered. All - uncaught exceptions will be stored automatically here. - - `digest`: If an exception was raised, the last 256 characters of the message - are stored automatically into this key. If you wish to search for exceptions - in Kibana, you will want to use this value as it will be indexed unlike the - full exception object. - - -### Copyright Notices - -We follow this [recommendation](https://www.linuxfoundation.org/blog/2020/01/copyright-notices-in-open-source-software-projects/) -of the Linux Foundation for the copyright notice that is placed on top of each source -code file. - -It is intended to provide a broad generic statement that allows all authors/contributors -of the NOMAD project to claim their copyright, independent of their organization or -individual ownership. - -You can simply copy the notice from another file. From time to time we can use a tool -like [licenseheaders](https://pypi.org/project/licenseheaders/) to ensure correct -notices. In addition we keep an purely informative AUTHORS file. - - -## Git/GitLab - -### Branches and clean version history - -The `master` branch of our repository is *protected*. You must not (even if you have -the rights) commit to it directly. The `master` branch references the latest official -release (i.e. what the current NOMAD runs on). The current development is represented by -*version* branches, named `vx.x.x`. Usually there are two or more of these branched, -representing the development on *minor/bugfix* versions and the next *major* version(s). -Ideally these *version* branches are also not manually push to. - -Instead you develop -on *feature* branches. These are branches that are dedicated to implement a single feature. -They are short lived and only exist to implement a single feature. - -The lifecycle of a *feature* branch should look like this: - -- create the *feature* branch from the last commit on the respective *version* branch that passes CI - -- do your work and push until you are satisfied and the CI passes - -- create a merge request on GitLab - -- discuss the merge request on GitLab - -- continue to work (with the open merge request) until all issues from the discussion are resolved - -- the maintainer performs the merge and the *feature* branch gets deleted - -### Submodules - -We currently use git submodules to manage NOMAD internal dependencies (e.g. parsers). -All dependencies are python packages and installed via pip to your python environement. - -This allows us to target (e.g. install) individual commits. More importantly, we can address commit -hashes to identify exact parser/normalizer versions. On the downside, common functions -for all dependencies (e.g. the python-common package, or nomad_meta_info) cannot be part -of the nomad-FAIRDI project. In general, it is hard to simultaneously develop nomad-FAIRDI -and NOMAD-coe dependencies. - -Another approach is to integrate the NOMAD-coe sources with nomad-FAIRDI. The lacking -availability of individual commit hashes, could be replaces with hashes of source-code -files. - -We use the `master` branch on all dependencies. Of course feature branches can be used on -dependencies to manage work in progress. - -### Keep a clean history - -While working on a feature, there are certain practices that will help us to create -a clean history with coherent commits, where each commit stands on its own. - -```sh - git commit --amend -``` - -If you committed something to your own feature branch and then realize by CI that you have -some tiny error in it that you need to fix, try to amend this fix to the last commit. -This will avoid unnecessary tiny commits and foster more coherent single commits. With *amend* -you are basically adding changes to the last commit, i.e. editing the last commit. If -you push, you need to force it `git push origin feature-branch --force-with-lease`. So be careful, and -only use this on your own branches. - -```sh - git rebase -``` - -Lets assume you work on a bigger feature that takes more time. You might want to merge -the version branch into your feature branch from time to time to get the recent changes. -In these cases, use rebase and not merge. Rebase puts your branch commits in front of the -merged commits instead of creating a new commit with two ancestors. It basically moves the -point where you initially branched away from the version branch to the current position in -the version branch. This will avoid merges, merge commits, and generally leave us with a -more consistent history. You can also rebase before creating a merge request, which basically -allows no-op merges. Ideally the only real merges that we ever have, are between -version branches. - -```sh - git merge --squash -``` - -When you need multiple branches to implement a feature and merge between them, try to -use *squash*. Squashing basically puts all commits of the merged branch into a single commit. -It basically allows you to have many commits and then squash them into one. This is useful -if these commits were made just to synchronize between workstations, due to -unexpected errors in CI/CD, because you needed a save point, etc. Again the goal is to have -coherent commits, where each commits makes sense on its own. - -Often a feature is also represented by an *issue* on GitLab. Please mention the respective -issues in your commits by adding the issue id at the end of the commit message: *My message. #123*. - -We tag releases with `vX.X.X` according to the regular semantic versioning practices. -After releasing and tagging the *version* branch is removed. Do not confuse tags with *version* branches. -Remember that tags and branches are both Git references and you can accidentally pull/push/checkout a tag. - -The main NOMAD GitLab-project (`nomad-fair`) uses Git-submodules to maintain its -parsers and other dependencies. All these submodules are places in the `/dependencies` -directory. There are helper scripts to install (`./dependencies.sh`) and -commit changes to all submodules (`./dependencies-git.sh`). After merging or checking out, -you have to make sure that the modules are updated to not accidentally commit old -submodule commits again. Usually you do the following to check if you really have a -clean working directory. - -```sh - git checkout something-with-changes - git submodule update - git status -``` +then install the extension by drag the file `nomad-0.0.x.vsix` and drop it into the extension panel of the vscode. \ No newline at end of file diff --git a/docs/schema/python.md b/docs/schema/python.md index b2a7809b5..6472a3a7e 100644 --- a/docs/schema/python.md +++ b/docs/schema/python.md @@ -249,7 +249,7 @@ followed? ### Schema super structure -You should follow the basic [developer's getting started](../developers.md) to setup a development environment. This will give you all the necessary libraries and allows you +You should follow the basic [developer's getting started](../develop/setup.md) to setup a development environment. This will give you all the necessary libraries and allows you to place your modules into the NOMAD code. The `EntryArchive` section definition sets the root of the archive for each entry in @@ -257,7 +257,7 @@ NOMAD. It therefore defines the top level sections: - `metadata`, all "administrative" metadata (ids, permissions, publish state, uploads, user metadata, etc.) - `results`, a summary with copies and references to data from method specific sections. This also -presents the [searchable metadata](../search.md). +presents the [searchable metadata](../develop/search.md). - `workflows`, all workflow metadata - Method specific sub-sections, e.g. `run`. This is were all parsers are supposed to add the parsed data. diff --git a/mkdocs.yml b/mkdocs.yml index 70a691c73..954519926 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,10 +17,12 @@ nav: - schema/elns.md # - Using the AI Toolkit and other remote tools: aitoolkit.md - Developing NOMAD: - - developers.md - - search.md - - parser.md - - normalizers.md + - develop/setup.md + - develop/guides.md + - develop/gitlab.md + - develop/search.md + - develop/parser.md + - develop/normalizers.md - Operating NOMAD (Oasis): oasis.md theme: name: material -- GitLab From fb64c3694220db21bde4d725e9d2cff6ce68fa58 Mon Sep 17 00:00:00 2001 From: Mohammad Nakhaee Date: Thu, 22 Sep 2022 07:01:24 +0000 Subject: [PATCH 004/117] referenceEditQuantity api call --- gui/src/components/editQuantity/ReferenceEditQuantity.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/gui/src/components/editQuantity/ReferenceEditQuantity.js b/gui/src/components/editQuantity/ReferenceEditQuantity.js index 3583cf20f..5265e97b6 100644 --- a/gui/src/components/editQuantity/ReferenceEditQuantity.js +++ b/gui/src/components/editQuantity/ReferenceEditQuantity.js @@ -59,12 +59,11 @@ const ReferenceEditQuantity = React.memo(function ReferenceEditQuantity(props) { if (input !== '') { query['entry_name.prefix'] = input } - const sections = referencedSectionQualifiedNames?.map(qualifiedName => ({'sections': qualifiedName})) + const sections = referencedSectionQualifiedNames?.map(qualifiedName => ({'sections': qualifiedName, ...query})) api.post('entries/query', { 'owner': 'visible', 'query': { - 'or': sections, - ...query + 'or': sections }, 'required': { 'include': [ @@ -144,11 +143,9 @@ const ReferenceEditQuantity = React.memo(function ReferenceEditQuantity(props) { }, [onChange]) const createNewEntry = useCallback((uploadId, fileName) => { - const template = {name: 'noname'} const archive = { data: { - m_def: quantityDef.type._referencedSection._url || quantityDef.type._referencedSection._qualifiedName, - ...template + m_def: quantityDef.type._referencedSection._url || quantityDef.type._referencedSection._qualifiedName } } return new Promise((resolve, reject) => { -- GitLab From ae8f54e0274a0406220644a082e3f941a81c006f Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Thu, 22 Sep 2022 07:04:34 +0000 Subject: [PATCH 005/117] Resolve issues with missing quantity references to BS reciprocal cell. --- gui/src/components/ErrorHandler.js | 3 +- gui/src/components/entry/conftest.spec.js | 6 +- .../properties/ElectronicPropertiesCard.js | 36 +++++++--- gui/src/components/visualization/BandGap.js | 24 +++++-- .../components/visualization/BandGap.spec.js | 30 ++++++++ .../components/visualization/BandStructure.js | 6 +- .../components/visualization/BrillouinZone.js | 27 +++++-- .../visualization/BrillouinZone.spec.js | 28 ++++++++ .../visualization/ElectronicProperties.js | 43 +++++------ .../GeometryOptimization.spec.js | 11 ++- gui/src/components/visualization/Plot.js | 7 -- .../RadialDistributionFunction.spec.js | 15 ++-- .../visualization/Trajectory.spec.js | 11 ++- .../components/visualization/conftest.spec.js | 50 ++++++++++--- nomad/normalizing/band_structure.py | 28 +++----- tests/normalizing/conftest.py | 36 +++------- tests/normalizing/test_band_structure.py | 71 ++++++++----------- 17 files changed, 256 insertions(+), 176 deletions(-) create mode 100644 gui/src/components/visualization/BandGap.spec.js create mode 100644 gui/src/components/visualization/BrillouinZone.spec.js diff --git a/gui/src/components/ErrorHandler.js b/gui/src/components/ErrorHandler.js index 0068b13fb..6cff72171 100644 --- a/gui/src/components/ErrorHandler.js +++ b/gui/src/components/ErrorHandler.js @@ -66,6 +66,7 @@ withErrorHandler.propTypes = ({ message: PropTypes.oneOfType([PropTypes.string, PropTypes.func]) // Provide either a fixed error message or a callback that will receive the error details. }) +export const webGlError = 'Could not display the visualization as your browser does not support WebGL content.' export const withWebGLErrorHandler = WrappedComponent => props => { const hasWebGL = useState(hasWebGLSupport())[0] @@ -76,7 +77,7 @@ export const withWebGLErrorHandler = WrappedComponent => props => { return - Could not display the visualization as your browser does not support WebGL content. + {webGlError} } } diff --git a/gui/src/components/entry/conftest.spec.js b/gui/src/components/entry/conftest.spec.js index c08d2256c..a8cd7e3e4 100644 --- a/gui/src/components/entry/conftest.spec.js +++ b/gui/src/components/entry/conftest.spec.js @@ -98,9 +98,9 @@ export function expectTrajectory(index, root = screen) { /** * Tests that an methodology itme is displayed correctly. * - * @param {PlotState} state The expected plot state. - * @param {str} placeholderTestID The test id for the placeholder. - * @param {str} errorMsg The expected error message. + * @param {str} title The methodology title. + * @param {object} data The data that should be shown. + * @param {str} path The path for the methodology information. * @param {object} container The root element to perform the search on. */ export async function expectMethodologyItem( diff --git a/gui/src/components/entry/properties/ElectronicPropertiesCard.js b/gui/src/components/entry/properties/ElectronicPropertiesCard.js index 529338892..ca81caf2c 100644 --- a/gui/src/components/entry/properties/ElectronicPropertiesCard.js +++ b/gui/src/components/entry/properties/ElectronicPropertiesCard.js @@ -18,13 +18,11 @@ import React from 'react' import PropTypes from 'prop-types' import { PropertyCard } from './PropertyCard' -import { useUnits } from '../../../units' import { getLocation, resolveInternalRef } from '../../../utils' import ElectronicProperties from '../../visualization/ElectronicProperties' import { refPath } from '../../archive/metainfo' const ElectronicPropertiesCard = React.memo(({index, properties, archive}) => { - const units = useUnits() const urlPrefix = `${getLocation()}/data` // Find out which properties are present @@ -37,7 +35,7 @@ const ElectronicPropertiesCard = React.memo(({index, properties, archive}) => { } // Resolve DOS data - let dos = hasDos ? null : false + let dos = hasDos ? undefined : false const dosData = archive?.results?.properties?.electronic?.dos_electronic if (dosData) { dos = {} @@ -49,22 +47,40 @@ const ElectronicPropertiesCard = React.memo(({index, properties, archive}) => { dos.m_path = `${urlPrefix}/${refPath(dosData.energies.split('/').slice(0, -1).join('/'))}` } - // Resolve band structure data - let bs = hasBs ? null : false + // Resolve data for band structure, brillouin zone and band gaps. + let bs = hasBs ? undefined : false + let band_gap = hasBs ? undefined : false const bsData = archive?.results?.properties?.electronic?.band_structure_electronic + let brillouin_zone = bsData ? undefined : false if (bsData) { - bs = {} - bs.reciprocal_cell = resolveInternalRef(bsData.reciprocal_cell, archive) - bs.segment = resolveInternalRef(bsData.segment, archive) + const segments = resolveInternalRef(bsData.segment, archive) + bs = { + segment: segments, + m_path: `${urlPrefix}/${refPath(bsData.segment[0].split('/').slice(0, -2).join('/'))}` + } + if (bsData.band_gap) { bs.energy_highest_occupied = Math.max(...bsData.band_gap.map(x => x.energy_highest_occupied)) bs.band_gap = bsData.band_gap + band_gap = bsData.band_gap } - bs.m_path = `${urlPrefix}/${refPath(bsData.reciprocal_cell.split('/').slice(0, -1).join('/'))}` + + const reciprocal_cell = resolveInternalRef(bsData.reciprocal_cell, archive) + brillouin_zone = reciprocal_cell + ? { + reciprocal_cell: reciprocal_cell, + segment: segments + } + : false } return - + }) diff --git a/gui/src/components/visualization/BandGap.js b/gui/src/components/visualization/BandGap.js index d37612394..6187bb59c 100644 --- a/gui/src/components/visualization/BandGap.js +++ b/gui/src/components/visualization/BandGap.js @@ -19,9 +19,12 @@ import React, { } from 'react' import PropTypes from 'prop-types' import { SectionTable } from '../Quantity' import { useUnits } from '../../units' +import { withErrorHandler } from '../ErrorHandler' import NoData from './NoData' import Placeholder from './Placeholder' +export const bandGapError = "Could not load band gap." + // Band gap quantities to show. Saved as const object to prevent re-renders const columns = { index: {label: 'Ch.', align: 'left'}, @@ -33,7 +36,7 @@ const columns = { * Shows a summary of all band gap values, each displayed in a separate row of a * table. */ -const BandGap = React.memo(({data, section}) => { +const BandGap = React.memo(({data, section, 'data-testid': testID}) => { const units = useUnits() return data !== false ? data @@ -44,13 +47,24 @@ const BandGap = React.memo(({data, section}) => { data={{data: data}} units={units} /> - : + : : }) BandGap.propTypes = { - data: PropTypes.any, - section: PropTypes.string + data: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.shape({ + index: PropTypes.number, + value: PropTypes.number, + type: PropTypes.string + })), + PropTypes.oneOf([false, undefined]) // False for NoData, undefined for Placeholder, + ]), + section: PropTypes.string, + 'data-testid': PropTypes.string +} +BandGap.defaultProps = { + 'data-testid': 'band-gap' } -export default BandGap +export default withErrorHandler(bandGapError)(BandGap) diff --git a/gui/src/components/visualization/BandGap.spec.js b/gui/src/components/visualization/BandGap.spec.js new file mode 100644 index 000000000..bec0380dd --- /dev/null +++ b/gui/src/components/visualization/BandGap.spec.js @@ -0,0 +1,30 @@ +/* + * Copyright The NOMAD Authors. + * + * This file is part of NOMAD. See https://nomad-lab.eu for further info. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react' +import { render } from '../conftest.spec' +import { expectVisualization, VisualizationState } from './conftest.spec' +import BandGap, { bandGapError } from './BandGap' + +test.each([ + ['no data', VisualizationState.NoData, false], + ['loading', VisualizationState.Loading, undefined], + ['error: invalid data', VisualizationState.Error, "invalid data"] +])('band gap: %s', async (id, state, data) => { + render() + await expectVisualization(state, 'band-gap-placeholder', bandGapError) +}) diff --git a/gui/src/components/visualization/BandStructure.js b/gui/src/components/visualization/BandStructure.js index 40f55dae5..89e0cd7de 100644 --- a/gui/src/components/visualization/BandStructure.js +++ b/gui/src/components/visualization/BandStructure.js @@ -17,6 +17,7 @@ */ import React, {useState, useEffect, useMemo} from 'react' import PropTypes from 'prop-types' +import { isFinite } from 'lodash' import { useTheme } from '@material-ui/core/styles' import Plot from '../visualization/Plot' import { add, distance, mergeObjects } from '../../utils' @@ -28,7 +29,6 @@ const BandStructure = React.memo(({ data, layout, className, - placeholderStyle, units, type, ...other @@ -46,7 +46,7 @@ const BandStructure = React.memo(({ if (type === 'vibrational') { return [0, true] } else { - if (!data.energy_highest_occupied === undefined) { + if (!isFinite(data.energy_highest_occupied)) { return [0, false] } else { return [new Quantity(data.energy_highest_occupied, energyUnit).toSystem(units).value(), true] @@ -297,7 +297,6 @@ const BandStructure = React.memo(({ layout={finalLayout} floatTitle={'Band structure'} warning={normalized ? null : msgNormalizationWarning} - placeholderStyle={placeholderStyle} metaInfoLink={data?.m_path} className={className} {...other} @@ -316,7 +315,6 @@ BandStructure.propTypes = { ]), layout: PropTypes.object, className: PropTypes.string, - placeholderStyle: PropTypes.any, units: PropTypes.object, // Contains the unit configuration type: PropTypes.string // Type of band structure: electronic or vibrational } diff --git a/gui/src/components/visualization/BrillouinZone.js b/gui/src/components/visualization/BrillouinZone.js index e056bd9ec..006a44bbc 100644 --- a/gui/src/components/visualization/BrillouinZone.js +++ b/gui/src/components/visualization/BrillouinZone.js @@ -28,6 +28,7 @@ import { } from '@material-ui/icons' import { BrillouinZoneViewer } from '@lauri-codes/materia' import Floatable from './Floatable' +import NoData from './NoData' import Placeholder from '../visualization/Placeholder' import { scale, distance } from '../../utils' import { withErrorHandler, withWebGLErrorHandler } from '../ErrorHandler' @@ -36,7 +37,7 @@ import { Actions, Action } from '../Actions' /** * Interactive 3D Brillouin zone viewer based on the 'materia'-library. */ - +export const bzError = 'Could not load Brillouin zone.' const fitMargin = 0.06 const useStyles = makeStyles((theme) => { return { @@ -92,8 +93,11 @@ const BrillouinZone = React.memo(({ // Run only on first render to initialize the viewer. const theme = useTheme() useEffect(() => { + if (!data || refViewer.current) { + return + } refViewer.current = new BrillouinZoneViewer(undefined) - }, []) + }, [data]) // Called only on first render to load the given structure. useEffect(() => { @@ -199,9 +203,15 @@ const BrillouinZone = React.memo(({ refViewer.current.render() }, []) + // If data is set explicitly to false, we show the NoData component. + if (data === false) { + return + } + if (loading) { return } @@ -233,10 +243,13 @@ const BrillouinZone = React.memo(({ }) BrillouinZone.propTypes = { - data: PropTypes.shape({ - reciprocal_cell: PropTypes.array.isRequired, // Reciprocal cell in SI units - segment: PropTypes.array.isRequired // Array of section_k_band_segments in SI units - }), + data: PropTypes.oneOfType([ + PropTypes.shape({ + reciprocal_cell: PropTypes.array.isRequired, // Reciprocal cell in SI units + segment: PropTypes.array.isRequired // Array of section_k_band_segments in SI units + }), + PropTypes.oneOf([false, undefined]) // False for NoData, undefined for Placeholder + ]), captureName: PropTypes.string, // Name of the file that the user can download classes: PropTypes.object, className: PropTypes.string, @@ -246,4 +259,4 @@ BrillouinZone.defaultProps = { captureName: 'brillouin_zone' } -export default withWebGLErrorHandler(withErrorHandler('Could not load Brillouin zone.')(BrillouinZone)) +export default withWebGLErrorHandler(withErrorHandler(bzError)(BrillouinZone)) diff --git a/gui/src/components/visualization/BrillouinZone.spec.js b/gui/src/components/visualization/BrillouinZone.spec.js new file mode 100644 index 000000000..750fb618f --- /dev/null +++ b/gui/src/components/visualization/BrillouinZone.spec.js @@ -0,0 +1,28 @@ +/* + * Copyright The NOMAD Authors. + * + * This file is part of NOMAD. See https://nomad-lab.eu for further info. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react' +import { render } from '../conftest.spec' +import { expectVisualization, VisualizationState } from './conftest.spec' +import BrillouinZone from './BrillouinZone' + +test.each([ + ['no webgl', VisualizationState.NoWebGL, false] +])('brillouin zone: %s', async (id, state, data) => { + render() + await expectVisualization(state) +}) diff --git a/gui/src/components/visualization/ElectronicProperties.js b/gui/src/components/visualization/ElectronicProperties.js index 70e4a426a..70c044a05 100644 --- a/gui/src/components/visualization/ElectronicProperties.js +++ b/gui/src/components/visualization/ElectronicProperties.js @@ -18,7 +18,7 @@ import React, { useCallback, useMemo } from 'react' import { Subject } from 'rxjs' import PropTypes from 'prop-types' -import { Quantity } from '../../units' +import { Quantity, useUnits } from '../../units' import DOS from './DOS' import BandStructure from './BandStructure' import BrillouinZone from './BrillouinZone' @@ -32,14 +32,10 @@ const useStyles = makeStyles((theme) => { return { nodata: { top: theme.spacing(0.7), - left: theme.spacing(2), - right: theme.spacing(2), bottom: '3.55rem' }, placeholder: { top: theme.spacing(0.7), - left: theme.spacing(2), - right: theme.spacing(2), bottom: theme.spacing(2) } } @@ -48,9 +44,11 @@ const useStyles = makeStyles((theme) => { const ElectronicProperties = React.memo(({ bs, dos, - classes, - units + brillouin_zone, + band_gap, + classes }) => { + const units = useUnits() const range = useMemo(() => new Quantity(electronicRange, 'electron_volt').toSystem(units).value(), [units]) const bsLayout = useMemo(() => ({yaxis: {autorange: false, range: range}}), [range]) const dosLayout = useMemo(() => ({yaxis: {autorange: false, range: range}}), [range]) @@ -77,8 +75,6 @@ const ElectronicProperties = React.memo(({ { bsYSubject.next({yaxis: {range: electronicRange}}) }} @@ -99,25 +95,24 @@ const ElectronicProperties = React.memo(({ data-testid="dos-electronic" /> - {bs !== false && <> - - - - - - - } + + + + + + }) ElectronicProperties.propTypes = { - dos: PropTypes.any, // Set to false if not available, set to other falsy value to show placeholder. - bs: PropTypes.any, // Set to false if not available, set to other falsy value to show placeholder. - classes: PropTypes.object, - units: PropTypes.object // Contains the unit configuration + dos: PropTypes.any, + bs: PropTypes.any, + brillouin_zone: PropTypes.any, + band_gap: PropTypes.any, + classes: PropTypes.object } export default ElectronicProperties diff --git a/gui/src/components/visualization/GeometryOptimization.spec.js b/gui/src/components/visualization/GeometryOptimization.spec.js index 6e5635287..e8706172d 100644 --- a/gui/src/components/visualization/GeometryOptimization.spec.js +++ b/gui/src/components/visualization/GeometryOptimization.spec.js @@ -17,16 +17,15 @@ */ import React from 'react' import { render } from '../conftest.spec' -import { expectPlot } from './conftest.spec' +import { expectPlot, VisualizationState } from './conftest.spec' import GeometryOptimization from './GeometryOptimization' -import { PlotState } from './Plot' const errorMsg = 'Could not load geometry optimization data.' test.each([ - [PlotState.NoData, false, undefined], - [PlotState.Loading, undefined, 'geometry-optimization-plot-placeholder'], - [PlotState.Error, {invalid: 'data'}, undefined], - [PlotState.Success, [0, 1, 2, 3, 4], undefined] + [VisualizationState.NoData, false, undefined], + [VisualizationState.Loading, undefined, 'geometry-optimization-plot-placeholder'], + [VisualizationState.Error, {invalid: 'data'}, undefined], + [VisualizationState.Success, [0, 1, 2, 3, 4], undefined] ])('%s', async (state, energies, placeholderTestID) => { const {container} = render() await expectPlot(state, placeholderTestID, errorMsg, container) diff --git a/gui/src/components/visualization/Plot.js b/gui/src/components/visualization/Plot.js index 53ed232cc..b095df817 100644 --- a/gui/src/components/visualization/Plot.js +++ b/gui/src/components/visualization/Plot.js @@ -493,10 +493,3 @@ Plot.defaultProps = { } export default Plot - -export const PlotState = { - NoData: 'NoData', - Loading: 'Loading', - Success: 'Success', - Error: 'Error' -} diff --git a/gui/src/components/visualization/RadialDistributionFunction.spec.js b/gui/src/components/visualization/RadialDistributionFunction.spec.js index 5c2640817..2df5d340b 100644 --- a/gui/src/components/visualization/RadialDistributionFunction.spec.js +++ b/gui/src/components/visualization/RadialDistributionFunction.spec.js @@ -18,17 +18,16 @@ import React from 'react' import { render } from '../conftest.spec' import { expectMethodologyItem } from '../entry/conftest.spec' -import { expectPlot } from './conftest.spec' -import { PlotState } from './Plot' +import { expectPlot, VisualizationState } from './conftest.spec' import RadialDistributionFunction, { rdfError, rdfPath } from './RadialDistributionFunction' test.each([ - ['no data', PlotState.NoData, {molecular: {'MOL-MOL': false}}, undefined], - ['loading', PlotState.Loading, {molecular: {'MOL-MOL': undefined}}, 'radial-distribution-function-molecular-mol-mol-placeholder'], - ['error: data cannot be false', PlotState.Error, false, undefined], - ['error: data cannot be undefined', PlotState.Error, undefined, undefined], - ['error: invalid data layout', PlotState.Error, {invalid: "data"}, undefined], - ['valid', PlotState.Success, {molecular: {'MOL-MOL': [{bins: [0, 1], value: [0, 1]}]}}, undefined] + ['no data', VisualizationState.NoData, {molecular: {'MOL-MOL': false}}, undefined], + ['loading', VisualizationState.Loading, {molecular: {'MOL-MOL': undefined}}, 'radial-distribution-function-molecular-mol-mol-placeholder'], + ['error: data cannot be false', VisualizationState.Error, false, undefined], + ['error: data cannot be undefined', VisualizationState.Error, undefined, undefined], + ['error: invalid data layout', VisualizationState.Error, {invalid: "data"}, undefined], + ['valid', VisualizationState.Success, {molecular: {'MOL-MOL': [{bins: [0, 1], value: [0, 1]}]}}, undefined] ])('rdf plot: %s', async (id, state, data, placeholderTestID) => { render() await expectPlot(state, placeholderTestID, rdfError) diff --git a/gui/src/components/visualization/Trajectory.spec.js b/gui/src/components/visualization/Trajectory.spec.js index bb7732c1f..ac87c7cd6 100644 --- a/gui/src/components/visualization/Trajectory.spec.js +++ b/gui/src/components/visualization/Trajectory.spec.js @@ -18,15 +18,14 @@ import React from 'react' import { render } from '../conftest.spec' import { expectMethodologyItem } from '../entry/conftest.spec' -import { expectPlot } from './conftest.spec' +import { expectPlot, VisualizationState } from './conftest.spec' import Trajectory, { trajectoryError, trajectoryPath } from './Trajectory' -import { PlotState } from './Plot' test.each([ - ['no data', PlotState.NoData, false, false, false, undefined], - ['loading', PlotState.Loading, undefined, false, false, 'trajectory-placeholder'], - ['error', PlotState.Error, {invalid: 'data'}, false, false, undefined], - ['valid', PlotState.Success, {time: [0, 1, 2], value: [0, 1, 2]}, false, false, undefined] + ['no data', VisualizationState.NoData, false, false, false, undefined], + ['loading', VisualizationState.Loading, undefined, false, false, 'trajectory-placeholder'], + ['error', VisualizationState.Error, {invalid: 'data'}, false, false, undefined], + ['valid', VisualizationState.Success, {time: [0, 1, 2], value: [0, 1, 2]}, false, false, undefined] ])('trajectory plot: %s', async (id, state, temperature, pressure, energyPotential, placeholderTestID) => { render( None: diff --git a/tests/normalizing/conftest.py b/tests/normalizing/conftest.py index a2250afce..1d37530fd 100644 --- a/tests/normalizing/conftest.py +++ b/tests/normalizing/conftest.py @@ -32,7 +32,7 @@ from nomad.datamodel.metainfo.simulation.method import ( Method, BasisSet, Electronic, DFT, XCFunctional, Functional, Electronic, Smearing, Scf, XCFunctional, Functional, GW) from nomad.datamodel.metainfo.simulation.system import ( - AtomsGroup, System, Atoms as AtomsMethod) + AtomsGroup, System, Atoms as AtomsSystem) from nomad.datamodel.metainfo.simulation.calculation import ( Calculation, Energy, EnergyEntry, Dos, DosValues, BandStructure, BandEnergies) from nomad.datamodel.metainfo.workflow import ( @@ -109,7 +109,7 @@ def get_template_dft() -> EntryArchive: xc_functional = XCFunctional(exchange=[Functional(name='GGA_X_PBE')]) method.dft = DFT(xc_functional=xc_functional) system = run.m_create(System) - system.atoms = AtomsMethod( + system.atoms = AtomsSystem( lattice_vectors=[ [5.76372622e-10, 0.0, 0.0], [0.0, 5.76372622e-10, 0.0], @@ -186,7 +186,7 @@ def get_template_for_structure(atoms: Atoms) -> EntryArchive: def get_section_system(atoms: Atoms): system = System() - system.atoms = AtomsMethod( + system.atoms = AtomsSystem( positions=atoms.get_positions() * 1E-10, labels=atoms.get_chemical_symbols(), lattice_vectors=atoms.get_cell() * 1E-10, @@ -260,7 +260,8 @@ def get_template_band_structure( band_gaps: List = None, type: str = 'electronic', has_references: bool = True, - normalize: bool = True) -> EntryArchive: + normalize: bool = True, + has_reciprocal_cell: bool = True) -> EntryArchive: '''Used to create a test data for band structures. Args: @@ -271,10 +272,13 @@ def get_template_band_structure( type: 'electronic' or 'vibrational' has_references: Whether the band structure has energy references or not. normalize: Whether the returned value is already normalized or not. + has_reciprocal_cell: Whether the reciprocal cell is available or not ''' if band_gaps is None: band_gaps = [None] template = get_template_dft() + if not has_reciprocal_cell: + template.run[0].system[0].atoms = None scc = template.run[0].calculation[0] if type == 'electronic': bs = scc.m_create(BandStructure, Calculation.band_structure_electronic) @@ -717,26 +721,6 @@ def dos_electronic() -> EntryArchive: return run_normalize(template) -@pytest.fixture(scope='session') -def bands_unpolarized_gap_indirect() -> EntryArchive: - return get_template_band_structure([(1, 'indirect')]) - - -@pytest.fixture(scope='session') -def bands_polarized_no_gap() -> EntryArchive: - return get_template_band_structure([None, None]) - - -@pytest.fixture(scope='session') -def bands_unpolarized_no_gap() -> EntryArchive: - return get_template_band_structure([None]) - - -@pytest.fixture(scope='session') -def bands_polarized_gap_indirect() -> EntryArchive: - return get_template_band_structure([(1, 'indirect'), (0.8, 'indirect')]) - - @pytest.fixture(scope='session') def dos_si_vasp() -> EntryArchive: parser_name = 'parsers/vasp' @@ -786,8 +770,8 @@ def hash_exciting() -> EntryArchive: @pytest.fixture(scope='session') -def hash_vasp(bands_unpolarized_gap_indirect) -> EntryArchive: - return bands_unpolarized_gap_indirect +def hash_vasp() -> EntryArchive: + return get_template_band_structure([(1, 'indirect')]) @pytest.fixture(scope='session') diff --git a/tests/normalizing/test_band_structure.py b/tests/normalizing/test_band_structure.py index 3be9298a4..306ee49f4 100644 --- a/tests/normalizing/test_band_structure.py +++ b/tests/normalizing/test_band_structure.py @@ -21,10 +21,7 @@ import pytest from tests.normalizing.conftest import ( # pylint: disable=unused-import phonon, - bands_unpolarized_no_gap, - bands_polarized_no_gap, - bands_unpolarized_gap_indirect, - bands_polarized_gap_indirect, + get_template_band_structure, band_path_cF, band_path_tP, band_path_hP, @@ -35,48 +32,36 @@ from tests.normalizing.conftest import ( # pylint: disable=unused-import from nomad.units import ureg -def test_band_gaps(bands_unpolarized_no_gap, bands_polarized_no_gap, bands_unpolarized_gap_indirect, bands_polarized_gap_indirect): +@pytest.mark.parametrize('gaps,has_reciprocal_cell', [ + pytest.param([None], True, id="unpolarized, no gap"), + pytest.param([(1, 'indirect')], True, id="unpolarized, finite gap"), + pytest.param([None, None], True, id="polarized, no gap"), + pytest.param([(1, 'indirect'), (0.8, 'indirect')], True, id="polarized, different gaps"), + pytest.param([(1, 'indirect')], False, id="no reciprocal cell"), +]) +def test_band_gaps(gaps, has_reciprocal_cell): """Tests that band gaps are correctly identified for different cases. """ - def test_generic(bs): - """Generic tests for band structure data.""" + bs = get_template_band_structure(gaps, has_reciprocal_cell=has_reciprocal_cell).run[0].calculation[0].band_structure_electronic[0] + if has_reciprocal_cell: assert bs.reciprocal_cell.shape == (3, 3) - - # Unpolarized, no gaps - bs = bands_unpolarized_no_gap.run[0].calculation[0].band_structure_electronic[0] - test_generic(bs) - assert len(bs.band_gap) == 1 - assert bs.band_gap[0].value == 0 - - # Polarized, no gaps - bs = bands_polarized_no_gap.run[0].calculation[0].band_structure_electronic[0] - test_generic(bs) - assert len(bs.band_gap) == 2 - assert bs.band_gap[0].value == 0 - assert bs.band_gap[1].value == 0 - - # Unpolarized, finite gap, indirect - bs = bands_unpolarized_gap_indirect.run[0].calculation[0].band_structure_electronic[0] - test_generic(bs) - assert len(bs.band_gap) == 1 - info = bs.band_gap[0] - gap_joule = info.value - gap_ev = gap_joule.to(ureg.eV).magnitude - assert gap_ev == pytest.approx(1, 0.001) - assert info.type == "indirect" - - # Polarized, finite gap, indirect - bs = bands_polarized_gap_indirect.run[0].calculation[0].band_structure_electronic[0] - test_generic(bs) - assert len(bs.band_gap) == 2 - channel_up = bs.band_gap[0] - channel_down = bs.band_gap[1] - gap_up_ev = channel_up.value.to(ureg.eV).magnitude - gap_down_ev = channel_down.value.to(ureg.eV).magnitude - assert channel_up.type == "indirect" - assert channel_down.type == "indirect" - assert gap_up_ev == pytest.approx(1, 0.01) - assert gap_down_ev == pytest.approx(0.8, 0.01) + else: + assert bs.reciprocal_cell is None + assert len(bs.band_gap) == len(gaps) + for index, gap in enumerate(gaps): + channel_info = bs.band_gap[index] + value = channel_info.value + if gap is not None and has_reciprocal_cell: + assert channel_info.type == gap[1] + else: + assert channel_info.type is None + if gap is None: + assert value == 0 + else: + gap_ev = value.to(ureg.eV).magnitude + assert gap_ev == pytest.approx(gap[0], 0.001) + eho_ev = channel_info.energy_highest_occupied.to(ureg.eV).magnitude + assert eho_ev == pytest.approx(1 if gap else 0, 0.001) def test_paths(band_path_cF, band_path_tP, band_path_hP): -- GitLab From e7e19c6efab6c9c7f0ff4657663dbb57fc3eae34 Mon Sep 17 00:00:00 2001 From: Pepe Marquez Date: Wed, 21 Sep 2022 12:14:20 +0200 Subject: [PATCH 006/117] Removing solarcell section instance from the Results root --- nomad/datamodel/results.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nomad/datamodel/results.py b/nomad/datamodel/results.py index 49bc5d7ed..f41a0895b 100644 --- a/nomad/datamodel/results.py +++ b/nomad/datamodel/results.py @@ -2383,7 +2383,6 @@ class Results(MSection): method = SubSection(sub_section=Method.m_def, repeats=False) properties = SubSection(sub_section=Properties.m_def, repeats=False) eln = SubSection(sub_section=ELN.m_def, repeats=False) - solarcell = SubSection(sub_section=SolarCell.m_def, repeats=False) m_package.__init_metainfo__() -- GitLab From 5e7ca4ac9e64d84a7b0a077b7d00adae776206f9 Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 19 Sep 2022 12:03:53 +0200 Subject: [PATCH 007/117] Show the default value in archive browser --- gui/src/components/archive/ArchiveBrowser.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gui/src/components/archive/ArchiveBrowser.js b/gui/src/components/archive/ArchiveBrowser.js index e52cffab8..61fb80ff0 100644 --- a/gui/src/components/archive/ArchiveBrowser.js +++ b/gui/src/components/archive/ArchiveBrowser.js @@ -484,7 +484,7 @@ class SectionAdaptor extends ArchiveAdaptor { async itemAdaptor(key) { const [name, index] = key.split(':') const property = this.def._properties[name] - const value = this.obj[name] + const value = property?.default || this.obj[name] if (!property) { return super.itemAdaptor(key) } else if (property.m_def === SubSectionMDef) { @@ -794,7 +794,8 @@ function Section({section, def, parentRelation, entryIsEditable}) { const renderQuantity = useCallback(quantityDef => { const key = quantityDef.name - const disabled = section[key] === undefined + const value = quantityDef.default || section[key] + const disabled = value === undefined if (!disabled && quantityDef.type.type_kind === 'reference' && quantityDef.shape.length === 1) { return } @@ -808,7 +809,7 @@ function Section({section, def, parentRelation, entryIsEditable}) { {!disabled &&  =  -- GitLab From 96f526c2322a1b6310a0e32aeaea2a648de74fdc Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Thu, 22 Sep 2022 11:46:49 +0200 Subject: [PATCH 008/117] Support YAML files for ELNs. --- gui/src/components/DataStore.js | 12 +++++++++++- gui/src/components/api.js | 5 +++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/gui/src/components/DataStore.js b/gui/src/components/DataStore.js index c0d71ab1f..7cc72fd23 100644 --- a/gui/src/components/DataStore.js +++ b/gui/src/components/DataStore.js @@ -23,6 +23,7 @@ import { apiBase } from '../config' import { refType, parseNomadUrl, createEntryUrl, systemMetainfoUrl } from '../utils' import { Metainfo } from './archive/metainfo' import currentSystemMetainfoData from '../metainfo' +import YAML from 'yaml' function addSubscription(storeObj, cb, options) { storeObj._subscriptions.push({cb, ...options}) @@ -480,8 +481,17 @@ const DataStore = React.memo(({children}) => { delete newArchive.metadata delete newArchive.results delete newArchive.processing_logs + + const config = {} + let stringifiedArchive + if (fileName.endsWith('yaml') || fileName.endsWith('yml')) { + config.headers = { + 'Content-Type': 'application/yaml' + } + stringifiedArchive = YAML.stringify(newArchive) + } return new Promise((resolve, reject) => { - api.put(`/uploads/${uploadId}/raw/${path}?file_name=${fileName}&wait_for_processing=true&entry_hash=${archive.metadata.entry_hash}`, newArchive) + api.put(`/uploads/${uploadId}/raw/${path}?file_name=${fileName}&wait_for_processing=true&entry_hash=${archive.metadata.entry_hash}`, stringifiedArchive || newArchive, config) .then(response => { requestRefreshEntry(installationUrl, entryId) }) diff --git a/gui/src/components/api.js b/gui/src/components/api.js index 7f41a0b0f..eb9f3ff20 100644 --- a/gui/src/components/api.js +++ b/gui/src/components/api.js @@ -277,9 +277,10 @@ class Api { const auth = await this.authHeaders() config = config || {} config.params = config.params || {} - config.headers = config.headers || { + config.headers = { accept: 'application/json', - ...auth.headers + ...auth.headers, + ...(config?.headers || {}) } try { const results = await method(path, body, config) -- GitLab From 030bd018ae7962011c04160758655b4d23c88bab Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Thu, 22 Sep 2022 11:47:51 +0200 Subject: [PATCH 009/117] Improved polymorph subsection select. Refactored browser section in different editable and eln contexts. #1032 --- .../with-default-values.archive.yaml | 34 ++++++++++++++ gui/src/components/archive/ArchiveBrowser.js | 45 ++++++++++--------- gui/src/components/archive/Browser.js | 4 ++ .../components/entry/ArchiveEntryView.spec.js | 18 ++++---- 4 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 examples/data/custom-schema/with-default-values.archive.yaml diff --git a/examples/data/custom-schema/with-default-values.archive.yaml b/examples/data/custom-schema/with-default-values.archive.yaml new file mode 100644 index 000000000..33ffd598a --- /dev/null +++ b/examples/data/custom-schema/with-default-values.archive.yaml @@ -0,0 +1,34 @@ +definitions: + sections: + PlainSection: + quantities: + with_default: + type: str + default: 'default value' + without_default: + type: str + ElnSection: + base_section: PlainSection + quantities: + with_default_eln: + type: str + default: 'default value' + m_annotations: + eln: + component: StringEditQuantity + without_default_eln: + type: str + m_annotations: + eln: + component: StringEditQuantity + RootSection: + base_section: nomad.datamodel.EntryData + m_annotations: + eln: + sub_sections: + eln: + section: ElnSection + plain: + section: PlainSection +data: + m_def: RootSection diff --git a/gui/src/components/archive/ArchiveBrowser.js b/gui/src/components/archive/ArchiveBrowser.js index 61fb80ff0..4a4cfa25e 100644 --- a/gui/src/components/archive/ArchiveBrowser.js +++ b/gui/src/components/archive/ArchiveBrowser.js @@ -27,7 +27,7 @@ import { FormHelperText} from '@material-ui/core' import { useRouteMatch, useHistory } from 'react-router-dom' import Autocomplete from '@material-ui/lab/Autocomplete' -import Browser, { Item, Content, Compartment, Adaptor, formatSubSectionName, laneContext, useLane, browserContext } from './Browser' +import Browser, { Item, Content, Compartment, Adaptor, formatSubSectionName, laneContext, useLane, browserContext, ItemChip } from './Browser' import { RawFileAdaptor } from './FileBrowser' import { isEditable, PackageMDef, QuantityMDef, removeSubSection, SectionMDef, SubSectionMDef, @@ -405,7 +405,7 @@ class ArchiveAdaptor extends Adaptor { * @param {*} obj A data object, located somewhere in the archive specified by baseUrl * @param {*} def The metainfo definition of obj */ - constructor(baseUrl, obj, def) { + constructor(baseUrl, obj, def, isInEln) { super() this.baseUrl = baseUrl this.parsedBaseUrl = parseNomadUrl(baseUrl) @@ -415,6 +415,7 @@ class ArchiveAdaptor extends Adaptor { this.api = undefined this.dataStore = undefined this.entryIsEditable = undefined + this.isInEln = isInEln === undefined && def.m_def === SectionMDef ? isEditable(def) : isInEln this.obj = obj // The data in the archive tree to display this.def = def this.unsubscriberFunctions = [] @@ -455,7 +456,8 @@ class ArchiveAdaptor extends Adaptor { const newDefUrl = resolveNomadUrl(obj.m_def, baseUrl) def = await this.dataStore.getMetainfoDefAsync(newDefUrl) } - return new SectionAdaptor(baseUrl, obj, def) + const isInEln = this.isInEln || isEditable(def) + return new SectionAdaptor(baseUrl, obj, def, isInEln) } if (def.m_def === QuantityMDef) { @@ -552,7 +554,8 @@ class SectionAdaptor extends ArchiveAdaptor { section={this.obj} def={this.def} parentRelation={this.parentRelation} - entryIsEditable={this.entryIsEditable} /> + sectionIsInEln={this.isInEln} + sectionIsEditable={this.entryIsEditable && this.isInEln}/> } } @@ -688,32 +691,35 @@ QuantityValue.propTypes = ({ const InheritingSections = React.memo(function InheritingSections({def, section, lane}) { const browser = useContext(browserContext) - const selection = useMemo(() => { - return section?.m_def || null - }, [section]) const handleInheritingSectionsChange = useCallback((e) => { section.m_def = e.target.value browser.invalidateLanesFromIndex(lane.index) }, [section, browser, lane]) + if (section.m_def) { + return '' + } + return (def._allInheritingSections?.length > 0 && - Select an m_def from the list + Multiple specific sections are available - + {def.name} {def._allInheritingSections.map((inheritingSection, i) => { - const val = inheritingSection._url || inheritingSection._qualifiedName + const sectionValue = inheritingSection._url || inheritingSection._qualifiedName return ( - + {inheritingSection.name} ) @@ -729,7 +735,7 @@ InheritingSections.propTypes = ({ lane: PropTypes.object }) -function Section({section, def, parentRelation, entryIsEditable}) { +function Section({section, def, parentRelation, sectionIsEditable, sectionIsInEln}) { const {handleArchiveChanged} = useEntryPageContext() || {} const config = useRecoilValue(configState) const [showJson, setShowJson] = useState(false) @@ -740,10 +746,6 @@ function Section({section, def, parentRelation, entryIsEditable}) { return lane?.adaptor?.parsedBaseUrl?.entryId }, [lane]) - const sectionIsEditable = useMemo(() => { - return entryIsEditable && isEditable(def) - }, [entryIsEditable, def]) - const actions = useMemo(() => { const navButton = navEntryId && ( @@ -795,6 +797,7 @@ function Section({section, def, parentRelation, entryIsEditable}) { const renderQuantity = useCallback(quantityDef => { const key = quantityDef.name const value = quantityDef.default || section[key] + const isDefault = value && !section[key] const disabled = value === undefined if (!disabled && quantityDef.type.type_kind === 'reference' && quantityDef.shape.length === 1) { return @@ -815,6 +818,7 @@ function Section({section, def, parentRelation, entryIsEditable}) { } + {isDefault && } ) }, [section]) @@ -874,7 +878,7 @@ function Section({section, def, parentRelation, entryIsEditable}) { {subSectionCompartment} {quantities - .filter(quantityDef => section[quantityDef.name] !== undefined || config.showAllDefined) + .filter(quantityDef => section[quantityDef.name] !== undefined || config.showAllDefined || sectionIsInEln) .filter(filter) .map(renderQuantity) } @@ -899,7 +903,8 @@ Section.propTypes = ({ section: PropTypes.object.isRequired, def: PropTypes.object.isRequired, parentRelation: PropTypes.object, - entryIsEditable: PropTypes.bool.isRequired + sectionIsEditable: PropTypes.bool, + sectionIsInEln: PropTypes.bool }) function SubSection({subSectionDef, section, editable}) { diff --git a/gui/src/components/archive/Browser.js b/gui/src/components/archive/Browser.js index 0c699700d..749d785c0 100644 --- a/gui/src/components/archive/Browser.js +++ b/gui/src/components/archive/Browser.js @@ -602,3 +602,7 @@ Title.propTypes = ({ PropTypes.node ]) }) + +export function ItemChip(props) { + return +} diff --git a/gui/src/components/entry/ArchiveEntryView.spec.js b/gui/src/components/entry/ArchiveEntryView.spec.js index 3092e4ae1..d0697473c 100644 --- a/gui/src/components/entry/ArchiveEntryView.spec.js +++ b/gui/src/components/entry/ArchiveEntryView.spec.js @@ -19,11 +19,13 @@ import React from 'react' import { join } from 'path' import { waitFor } from '@testing-library/dom' import userEvent from '@testing-library/user-event' -import { render, screen, within, startAPI, closeAPI, blockConsoleOutput, unblockConsoleOutput, waitForGUI } from '../conftest.spec' +import { render, screen, within, startAPI, closeAPI, blockConsoleOutput, unblockConsoleOutput } from '../conftest.spec' import { getLane, navigateTo, browseRecursively } from '../archive/conftest.spec' import EntryPageContext from './EntryPageContext' import ArchiveEntryView from './ArchiveEntryView' import { minutes } from '../../setupTests' +import { act } from 'react-dom/test-utils' +import {fireEvent} from '@testing-library/react' beforeEach(() => { blockConsoleOutput() @@ -107,23 +109,23 @@ test.each([ test('inheriting sections', async () => { await startAPI('tests.states.uploads.archive_browser_test', 'tests/data/uploads/archive_browser_test_inheriting_sectins', 'test', 'password') - - render() + await act(async () => render()) expect(await screen.findByText('Entry')).toBeVisible() const path = 'data' const sectionName = '../uploads/archive_browser_test/raw/inheriting-schema.archive.yaml#definitions/section_definitions/1' await navigateTo(path) - userEvent.click(await screen.findByTestId('subsection:C1')) - expect(await screen.findByText('Select an m_def from the list')).toBeInTheDocument() + await userEvent.click(await screen.findByTestId('subsection:C1')) + const selectLabel = await screen.findByText('Select a section') + expect(selectLabel).toBeInTheDocument() const dropDown = await screen.findByTestId(`inheriting:SubSectionBase1`) expect(dropDown).toBeInTheDocument() const selectInput = within(dropDown).getByRole('textbox', { hidden: true }) - - await waitForGUI() - await waitFor(() => expect(selectInput.value).toEqual(`${sectionName}`)) + await waitFor(() => expect(selectInput.value).toEqual('')) + await fireEvent.change(selectInput, {target: {value: sectionName}}) + await waitFor(() => expect(selectLabel).not.toBeInTheDocument()) }) test.each([ -- GitLab From a7927873e05a655072e0e0f81e2daa1ea9543203 Mon Sep 17 00:00:00 2001 From: Mohammad Nakhaee Date: Thu, 22 Sep 2022 13:55:44 +0000 Subject: [PATCH 010/117] Resolve "api does not return entries with empty sections" --- nomad/datamodel/datamodel.py | 3 +++ nomad/metainfo/metainfo.py | 10 +++++++++- nomad/parsing/file_parser/basic_parser.py | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/nomad/datamodel/datamodel.py b/nomad/datamodel/datamodel.py index 654040e30..7daf80e16 100644 --- a/nomad/datamodel/datamodel.py +++ b/nomad/datamodel/datamodel.py @@ -577,6 +577,9 @@ class EntryMetadata(metainfo.MSection): for section, property_def, _ in archive.m_traverse(): sections.add(section.m_def) + if property_def is None: + continue + section_path = get_section_path(section) quantity_path = f'{section_path}.{property_def.name}' if section_path else property_def.name quantities.add(quantity_path) diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index 790f9b84d..beded0de5 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -2533,13 +2533,18 @@ class MSection(metaclass=MObjectMeta): # TODO find a way to make this a subclas def m_traverse(self): ''' Performs a depth-first traversal and yield tuples of section, property def, - parent index for all set properties. + parent index for all set properties. If the section has no property the empty + section is returned. ''' + + empty = True for key in self.__dict__: property_def = self.m_def.all_properties.get(key) if property_def is None: continue + empty = False + if isinstance(property_def, SubSection): for sub_section in self.m_get_sub_sections(property_def): for i in sub_section.m_traverse(): @@ -2550,6 +2555,9 @@ class MSection(metaclass=MObjectMeta): # TODO find a way to make this a subclas else: yield self, property_def, -1 + if empty: + yield self, None, -1 + def m_pretty_print(self, indent=None): ''' Pretty prints the containment hierarchy ''' if indent is None: diff --git a/nomad/parsing/file_parser/basic_parser.py b/nomad/parsing/file_parser/basic_parser.py index d890de089..cc8cefd5c 100644 --- a/nomad/parsing/file_parser/basic_parser.py +++ b/nomad/parsing/file_parser/basic_parser.py @@ -120,7 +120,9 @@ class BasicParser: def remove_empty_section(sections, definition): for n in range(len(sections) - 1, -1, -1): empty = True - for _ in sections[n].m_traverse(): + for _, property_def, _ in sections[n].m_traverse(): + if property_def is None: + continue empty = False break if empty: -- GitLab From bc5fbde56c86b82f3917440527a389ac04cdd3f3 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Fri, 23 Sep 2022 07:11:41 +0000 Subject: [PATCH 011/117] Resolve "Search input component tests" --- gui/src/components/About.js | 2 +- gui/src/components/api.js | 2 +- gui/src/components/archive/ArchiveBrowser.js | 6 +++--- gui/src/components/archive/FileBrowser.spec.js | 10 +++++----- gui/src/components/archive/MetainfoBrowser.js | 4 ++-- gui/src/components/archive/SectionEditor.js | 4 ++-- gui/src/components/archive/conftest.spec.js | 4 ++-- gui/src/components/dataset/DatasetsPage.js | 2 +- gui/src/components/datatable/Datatable.js | 4 ++-- gui/src/components/entry/ArchiveEntryView.spec.js | 4 ++-- gui/src/components/entry/ArchiveLogView.spec.js | 6 +++--- gui/src/components/entry/EntryDetails.js | 2 +- gui/src/components/entry/OverviewView.spec.js | 6 +++--- gui/src/components/entry/conftest.spec.js | 4 ++-- gui/src/components/entry/properties/DefinitionsCard.js | 2 +- gui/src/components/entry/properties/NexusCard.js | 2 +- gui/src/components/entry/properties/SectionCard.js | 4 ++-- gui/src/components/nav/Navigation.js | 4 ++-- gui/src/components/north/NorthLaunchButton.js | 2 +- .../components/search/input/InputPeriodicTable.spec.js | 4 ++-- gui/src/components/uploads/UploadProcessingStatus.js | 2 +- 21 files changed, 40 insertions(+), 40 deletions(-) diff --git a/gui/src/components/About.js b/gui/src/components/About.js index d5a74561c..6aa3c03c2 100644 --- a/gui/src/components/About.js +++ b/gui/src/components/About.js @@ -43,7 +43,7 @@ import { pluralize } from '../utils' function CodeInfo({code, ...props}) { if (!code) { - return '' + return null } const metadata = parserMetadata[code] diff --git a/gui/src/components/api.js b/gui/src/components/api.js index eb9f3ff20..39692b7a3 100644 --- a/gui/src/components/api.js +++ b/gui/src/components/api.js @@ -454,7 +454,7 @@ function VerifyGlobalLogin({children}) { }, [api, setVerified]) if (verified === null) { - return '' + return null } if (!verified) { diff --git a/gui/src/components/archive/ArchiveBrowser.js b/gui/src/components/archive/ArchiveBrowser.js index 4a4cfa25e..85a655db0 100644 --- a/gui/src/components/archive/ArchiveBrowser.js +++ b/gui/src/components/archive/ArchiveBrowser.js @@ -825,7 +825,7 @@ function Section({section, def, parentRelation, sectionIsEditable, sectionIsInEl if (!section) { console.error('section is not available') - return '' + return null } const filter = config.showCodeSpecific ? def => !def.virtual : def => !def.virtual && !def.name.startsWith('x_') @@ -1101,7 +1101,7 @@ export const SectionPlots = React.memo(function SectionPlots({section, sectionDe }, [plots.length]) if (plots.length < 1 || selected.find(index => index >= plots.length)) { - return '' + return null } return @@ -1210,7 +1210,7 @@ export function Meta({def}) { const classes = useMetaStyles() const config = useRecoilValue(configState) if (!config.showMeta) { - return '' + return null } return
diff --git a/gui/src/components/archive/FileBrowser.spec.js b/gui/src/components/archive/FileBrowser.spec.js index 1d6200d18..bdba953f1 100644 --- a/gui/src/components/archive/FileBrowser.spec.js +++ b/gui/src/components/archive/FileBrowser.spec.js @@ -96,7 +96,7 @@ async function testBrowseAround(editable) { fireEvent.error(within(getLane(3)).getByRole('img')) await within(getLane(3)).findByText('Failed to open with image viewer. Bad file format?') expect(within(getLane(3)).getByButtonText('Open with text viewer')).toBeEnabled() - userEvent.click(within(getLane(3)).getByButtonText('Open with text viewer')) + await userEvent.click(within(getLane(3)).getByButtonText('Open with text viewer')) await within(getLane(3)).findByText('this is not an image!') // text content of the file // json @@ -197,10 +197,10 @@ test('delete files', async () => { for (const fileName of ['vasp.xml', '1.aux']) { await navigateTo(`test_entry/${fileName}`, browserConfig) - userEvent.click(within(getLane(2)).getByButtonText('delete this file')) + await userEvent.click(within(getLane(2)).getByButtonText('delete this file')) await screen.findByText(/Really delete the file/) expect(screen.queryAllByText(fileName).length).toBeGreaterThan(1) - userEvent.click(screen.getByButtonText('OK')) + await userEvent.click(screen.getByButtonText('OK')) await waitFor(() => { expect(screen.queryAllByText(fileName).length).toEqual(0) }) @@ -229,10 +229,10 @@ test('delete folder', async () => { const parentPath = (segments.slice(0, segments.length - 1)).join('/') // Navigate to path const lane = await navigateTo(path, browserConfig) - userEvent.click(within(lane).getByButtonText('delete this folder')) + await userEvent.click(within(lane).getByButtonText('delete this folder')) await screen.findByText(/Really delete the directory/) expect(screen.queryAllByText(folderName).length).toBeGreaterThan(1) - userEvent.click(screen.getByButtonText('OK')) + await userEvent.click(screen.getByButtonText('OK')) await waitFor(() => { expect(screen.queryAllByText(folderName).length).toEqual(0) }) diff --git a/gui/src/components/archive/MetainfoBrowser.js b/gui/src/components/archive/MetainfoBrowser.js index f1d274b77..e1ad42052 100644 --- a/gui/src/components/archive/MetainfoBrowser.js +++ b/gui/src/components/archive/MetainfoBrowser.js @@ -549,7 +549,7 @@ function DefinitionProperties({def, children}) { const hasSearchAnnotations = searchAnnotations && searchAnnotations.length > 0 if (!(children || def.aliases?.length || def.deprecated || (def.more && Object.keys(def.more).length) || hasSearchAnnotations)) { - return '' + return null } return @@ -770,7 +770,7 @@ DefinitionLabel.propTypes = ({ const Annotations = React.memo(function Annotations({def}) { if (!def.m_annotations) { - return '' + return null } return ( diff --git a/gui/src/components/archive/SectionEditor.js b/gui/src/components/archive/SectionEditor.js index 099fbcd86..ace97aa6c 100644 --- a/gui/src/components/archive/SectionEditor.js +++ b/gui/src/components/archive/SectionEditor.js @@ -73,7 +73,7 @@ const PropertyEditor = React.memo(function PropertyEditor({quantityDef, value, o const componentName = editAnnotation?.component const component = componentName && editQuantityComponents[componentName] if (!component) { - return '' + return null } const props = { quantityDef: quantityDef, @@ -92,7 +92,7 @@ const PropertyEditor = React.memo(function PropertyEditor({quantityDef, value, o /> } else { console.log('Unsupported quantity shape ', shape) - return '' + return null } }) PropertyEditor.propTypes = { diff --git a/gui/src/components/archive/conftest.spec.js b/gui/src/components/archive/conftest.spec.js index b91faa211..60319b20c 100644 --- a/gui/src/components/archive/conftest.spec.js +++ b/gui/src/components/archive/conftest.spec.js @@ -146,7 +146,7 @@ export async function selectItemAndWaitForRender(lane, laneIndex, itemKey, item if (!item) { item = within(lane).getByTestId(`item:${itemKey}`) } - userEvent.click(item) + await userEvent.click(item) await waitFor(() => { const nextLane = getLane(laneIndex + 1, itemKey) expect(nextLane).not.toBeNull() @@ -207,7 +207,7 @@ export async function browseRecursively(lane, laneIndex, path, itemFilter, filte for (const itemList of within(lane).queryAllByRole('item-list')) { const label = itemList.textContent try { - userEvent.click(itemList) + await userEvent.click(itemList) await within(lane).findByTestId(`item-list:${label}`) expectNoConsoleOutput() } catch (error) { diff --git a/gui/src/components/dataset/DatasetsPage.js b/gui/src/components/dataset/DatasetsPage.js index ee5f4d80d..ff55c0ffd 100644 --- a/gui/src/components/dataset/DatasetsPage.js +++ b/gui/src/components/dataset/DatasetsPage.js @@ -54,7 +54,7 @@ const columns = [ data={{datasets: dataset}} /> } - return '' + return null } }, { diff --git a/gui/src/components/datatable/Datatable.js b/gui/src/components/datatable/Datatable.js index 1ef903e51..bc6536773 100644 --- a/gui/src/components/datatable/Datatable.js +++ b/gui/src/components/datatable/Datatable.js @@ -168,7 +168,7 @@ export const DatatableLoadMorePagination = React.memo(function DatatableLoadMore
} - return '' + return null }) DatatableLoadMorePagination.propTypes = {} @@ -521,7 +521,7 @@ export const DatatableToolbarActions = React.memo(function DatatableToolbarActio } - return '' + return null }) DatatableToolbarActions.propTypes = { /** If true, this component is only shown if the surrounding table as an active selection. */ diff --git a/gui/src/components/entry/ArchiveEntryView.spec.js b/gui/src/components/entry/ArchiveEntryView.spec.js index d0697473c..4757d3042 100644 --- a/gui/src/components/entry/ArchiveEntryView.spec.js +++ b/gui/src/components/entry/ArchiveEntryView.spec.js @@ -94,12 +94,12 @@ test.each([ if (withDefinition) { // Click the definitions checkbox - userEvent.click(screen.getByRoleAndText('checkbox', 'definitions')) + await userEvent.click(screen.getByRoleAndText('checkbox', 'definitions')) expect(await within(getLane(0)).findByText('meta')).toBeVisible() } if (withAll) { // Click the metainfo definition - userEvent.click(screen.getByRoleAndText('checkbox', 'all defined')) + await userEvent.click(screen.getByRoleAndText('checkbox', 'all defined')) expect(await within(getLane(0)).findByText('processing_logs')).toBeVisible() } const lane = await navigateTo(path) diff --git a/gui/src/components/entry/ArchiveLogView.spec.js b/gui/src/components/entry/ArchiveLogView.spec.js index a2bd90a40..67589c178 100644 --- a/gui/src/components/entry/ArchiveLogView.spec.js +++ b/gui/src/components/entry/ArchiveLogView.spec.js @@ -45,12 +45,12 @@ test('Correctly renders the page', async () => { expect(infoBox).toBeChecked() // Unchecking the checkbox INFO should re-paint the DOM with only one log and no seeMore button - userEvent.click(infoBox) + await userEvent.click(infoBox) await waitFor(() => expect(screen.queryAllByTestId('Accordions')).toHaveLength(1)) expect(seeMoreButton).not.toBeInTheDocument() // Re-checking the INFO button should repaint the DOM with the seeMore Button as well as the 10 logs - userEvent.click(infoBox) + await userEvent.click(infoBox) expect(await screen.findByText(/see more/i)).toBeInTheDocument() expect(screen.queryAllByTestId('Accordions')).toHaveLength(10) @@ -60,7 +60,7 @@ test('Correctly renders the page', async () => { const butt = within(view).getByRole('button') await userEvent.click(butt) await waitFor(() => expect(screen.queryByTestId('system_size')).toBeInTheDocument()) - userEvent.click(screen.getByTestId('system_size')) + await userEvent.click(screen.getByTestId('system_size')) expect(await screen.findByText(/debug: parsers\/vasp \| undefined/i)) closeAPI() diff --git a/gui/src/components/entry/EntryDetails.js b/gui/src/components/entry/EntryDetails.js index ecf5b251b..aa3a7f6b6 100644 --- a/gui/src/components/entry/EntryDetails.js +++ b/gui/src/components/entry/EntryDetails.js @@ -175,7 +175,7 @@ export const VisitEntryAction = React.memo(function VisitEntryAction({data, ...p const {user} = useApi() const hide = (data.with_embargo && !user && !data.viewers.find(viewer => viewer.user_id === user.sub)) || data.process_running if (hide) { - return '' + return null } // The portal is disabled for this tooltip because this button causes a diff --git a/gui/src/components/entry/OverviewView.spec.js b/gui/src/components/entry/OverviewView.spec.js index b9eefcc14..035091e25 100644 --- a/gui/src/components/entry/OverviewView.spec.js +++ b/gui/src/components/entry/OverviewView.spec.js @@ -291,7 +291,7 @@ test.each([ const deleteButton1 = screen1.queryByTitle('Delete archive').closest('button') expect(deleteButton1).toBeEnabled() - fireEvent.click(deleteButton1) + await userEvent.click(deleteButton1) await waitForGUI() const deleteButtons = screen1.queryAllByText(/delete mainfile/i) const deleteButton = deleteButtons[0] @@ -312,11 +312,11 @@ test.each([ const inputTextField2 = within(cardSample2).queryAllByRole('textbox', { hidden: true }) fireEvent.change(inputTextField2[0], { target: { value: 'new text 2' } }) - userEvent.click(deleteButton) + await userEvent.click(deleteButton) await waitForGUI() expect(saveButton2).toBeEnabled() - fireEvent.click(saveButton2) + await userEvent.click(saveButton2) await waitForGUI() expect(await screen2.queryByText('The changes cannot be saved. The content has been modified by someone else.')).toBeInTheDocument() diff --git a/gui/src/components/entry/conftest.spec.js b/gui/src/components/entry/conftest.spec.js index a8cd7e3e4..864fa3fd4 100644 --- a/gui/src/components/entry/conftest.spec.js +++ b/gui/src/components/entry/conftest.spec.js @@ -75,10 +75,10 @@ export async function expectStructure(index, root = screen) { expect(prim).not.toBeInTheDocument() // After clicking all the options should be shown - userEvent.click(select) + await userEvent.click(select) expect(await root.findByText('Conventional')).toBeInTheDocument() expect(await root.findByText('Primitive')).toBeInTheDocument() - userEvent.click(select) + await userEvent.click(select) } export function expectNoStructure(index, root = screen) { diff --git a/gui/src/components/entry/properties/DefinitionsCard.js b/gui/src/components/entry/properties/DefinitionsCard.js index 020815462..deaf5fa19 100644 --- a/gui/src/components/entry/properties/DefinitionsCard.js +++ b/gui/src/components/entry/properties/DefinitionsCard.js @@ -27,7 +27,7 @@ const DefinitionsCard = React.memo(function DefinitionsCard({index, archive}) { const {entryId, uploadId} = useEntryPageContext() if (!index?.quantities?.includes('definitions')) { - return '' + return null } const actions = ( diff --git a/gui/src/components/entry/properties/NexusCard.js b/gui/src/components/entry/properties/NexusCard.js index 3fd6b0c73..75000d58d 100644 --- a/gui/src/components/entry/properties/NexusCard.js +++ b/gui/src/components/entry/properties/NexusCard.js @@ -24,7 +24,7 @@ import { Card } from '@material-ui/core' const NexusCard = React.memo(function NexusCard({index}) { if (index.parser_name !== 'parsers/nexus') { - return '' + return null } return ( diff --git a/gui/src/components/entry/properties/SectionCard.js b/gui/src/components/entry/properties/SectionCard.js index 0caf5a1a7..bdbe32b15 100644 --- a/gui/src/components/entry/properties/SectionCard.js +++ b/gui/src/components/entry/properties/SectionCard.js @@ -53,7 +53,7 @@ const PropertyPreview = React.memo(({quantityDef, section}) => { const {entryId, uploadId} = useEntryPageContext() const maxPreviewLength = 5 if (!quantityDef.type) { - return '' + return null } const shape = quantityDef.shape || [] @@ -187,7 +187,7 @@ const SectionCard = React.memo(({archivePath, sectionDef, section, readOnly, ... if (!sectionDef) { console.error('SectionCard: section definition is not available') - return '' + return null } return diff --git a/gui/src/components/nav/Navigation.js b/gui/src/components/nav/Navigation.js index 5fc4f81a9..dc033d5b2 100644 --- a/gui/src/components/nav/Navigation.js +++ b/gui/src/components/nav/Navigation.js @@ -105,11 +105,11 @@ function BetaSnack() { if (!version) { console.warn('no version data available') - return '' + return null } if (!version.isBeta && !version.isTest) { - return '' + return null } return ({name: key, title: key, ...northTools[key]})) if (toolsData.length === 0) { - return '' + return null } return ( diff --git a/gui/src/components/search/input/InputPeriodicTable.spec.js b/gui/src/components/search/input/InputPeriodicTable.spec.js index b97d2df41..45bb2f4dc 100644 --- a/gui/src/components/search/input/InputPeriodicTable.spec.js +++ b/gui/src/components/search/input/InputPeriodicTable.spec.js @@ -58,12 +58,12 @@ describe('', () => { // Test that after selecting C, only H and C are selectable. const cButton = screen.getByText('C') - userEvent.click(cButton) + await userEvent.click(cButton) await expectInputPeriodicTableItems(['C', 'H']) // Test that after enabling exclusive search, only C is selectable const exclusiveCheckbox = screen.getByRole('checkbox') - userEvent.click(exclusiveCheckbox) + await userEvent.click(exclusiveCheckbox) await expectInputPeriodicTableItems(['C']) }) }) diff --git a/gui/src/components/uploads/UploadProcessingStatus.js b/gui/src/components/uploads/UploadProcessingStatus.js index 264ab4905..c07609b1a 100644 --- a/gui/src/components/uploads/UploadProcessingStatus.js +++ b/gui/src/components/uploads/UploadProcessingStatus.js @@ -38,7 +38,7 @@ const UploadProcessingStatus = React.memo(function ProcessingStatus() { const classes = useStyles() if (!upload) { - return '' + return null } return ( -- GitLab From 4629f43ce5b8e882d3d2160ada912fafed3cdc32 Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Fri, 23 Sep 2022 10:07:30 +0200 Subject: [PATCH 012/117] Increased version numbers for next release. --- gui/package.json | 2 +- gui/public/env.js | 2 +- nomad/config.py | 2 +- ops/kubernetes/deployments/prod-develop-values.yaml | 2 +- ops/kubernetes/deployments/prod-staging-values.yaml | 2 +- ops/kubernetes/deployments/prod-test-values.yaml | 2 +- ops/kubernetes/deployments/prod-util-values.yaml | 2 +- ops/kubernetes/deployments/prod-values.yaml | 2 +- ops/kubernetes/nomad/Chart.yaml | 4 ++-- ops/kubernetes/nomad/values.yaml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gui/package.json b/gui/package.json index cbcc0473b..d59ff785b 100644 --- a/gui/package.json +++ b/gui/package.json @@ -1,6 +1,6 @@ { "name": "nomad-fair-gui", - "version": "1.1.3", + "version": "1.1.4", "commit": "e98694e", "private": true, "workspaces": [ diff --git a/gui/public/env.js b/gui/public/env.js index b3b6058d7..7ec89862f 100644 --- a/gui/public/env.js +++ b/gui/public/env.js @@ -12,7 +12,7 @@ window.nomadEnv = { 'northBase': 'http://localhost:9000/fairdi/nomad/latest/north', 'debug': false, 'version': { - 'label': '1.1.3', + 'label': '1.1.4', 'isBeta': false, 'isTest': true, 'usesBetaData': true, diff --git a/nomad/config.py b/nomad/config.py index 1f1604b13..1ea2cae1d 100644 --- a/nomad/config.py +++ b/nomad/config.py @@ -342,7 +342,7 @@ datacite = NomadConfig( ) meta = NomadConfig( - version='1.1.3', + version='1.1.4', commit=gitinfo.commit, deployment='devel', label=None, diff --git a/ops/kubernetes/deployments/prod-develop-values.yaml b/ops/kubernetes/deployments/prod-develop-values.yaml index 80550bf60..c398c61e3 100644 --- a/ops/kubernetes/deployments/prod-develop-values.yaml +++ b/ops/kubernetes/deployments/prod-develop-values.yaml @@ -1,5 +1,5 @@ version: - label: "1.1.3" + label: "1.1.4" isBeta: true usesBetaData: false officialUrl: "https://nomad-lab.eu/prod/v1/gui" diff --git a/ops/kubernetes/deployments/prod-staging-values.yaml b/ops/kubernetes/deployments/prod-staging-values.yaml index 7d53c628a..8a71a1a27 100644 --- a/ops/kubernetes/deployments/prod-staging-values.yaml +++ b/ops/kubernetes/deployments/prod-staging-values.yaml @@ -1,5 +1,5 @@ version: - label: "1.1.3" + label: "1.1.4" isBeta: true usesBetaData: false officialUrl: "https://nomad-lab.eu/prod/v1/gui" diff --git a/ops/kubernetes/deployments/prod-test-values.yaml b/ops/kubernetes/deployments/prod-test-values.yaml index c9a530f3e..72d38a017 100644 --- a/ops/kubernetes/deployments/prod-test-values.yaml +++ b/ops/kubernetes/deployments/prod-test-values.yaml @@ -1,5 +1,5 @@ version: - label: "1.1.3" + label: "1.1.4" isBeta: true usesBetaData: true officialUrl: "https://nomad-lab.eu/prod/v1/gui" diff --git a/ops/kubernetes/deployments/prod-util-values.yaml b/ops/kubernetes/deployments/prod-util-values.yaml index 015dea59b..6c7b8c214 100644 --- a/ops/kubernetes/deployments/prod-util-values.yaml +++ b/ops/kubernetes/deployments/prod-util-values.yaml @@ -1,5 +1,5 @@ version: - label: "1.1.3" + label: "1.1.4" isBeta: true usesBetaData: false officialUrl: "https://nomad-lab.eu/prod/v1/gui" diff --git a/ops/kubernetes/deployments/prod-values.yaml b/ops/kubernetes/deployments/prod-values.yaml index 196236df3..215a2cfc0 100644 --- a/ops/kubernetes/deployments/prod-values.yaml +++ b/ops/kubernetes/deployments/prod-values.yaml @@ -1,5 +1,5 @@ version: - label: "1.1.3" + label: "1.1.4" isBeta: true usesBetaData: false officialUrl: "https://nomad-lab.eu/prod/v1/gui" diff --git a/ops/kubernetes/nomad/Chart.yaml b/ops/kubernetes/nomad/Chart.yaml index 0907397f4..82a91c12a 100644 --- a/ops/kubernetes/nomad/Chart.yaml +++ b/ops/kubernetes/nomad/Chart.yaml @@ -1,9 +1,9 @@ apiVersion: v2 name: nomad description: A Helm chart for Kubernetes that only runs nomad services and uses externally hosted databases. -appVersion: "1.1.3" +appVersion: "1.1.4" type: application -version: 1.1.3 +version: 1.1.4 dependencies: - name: rabbitmq version: "8.30.1" diff --git a/ops/kubernetes/nomad/values.yaml b/ops/kubernetes/nomad/values.yaml index 2e7225863..c632523ab 100644 --- a/ops/kubernetes/nomad/values.yaml +++ b/ops/kubernetes/nomad/values.yaml @@ -1,6 +1,6 @@ ## Default values for nomad@FAIRDI version: - label: "1.1.3" + label: "1.1.4" isTest: false isBeta: false usesBetaData: false -- GitLab From 41782686a3a3fcb154f2c5f99e1228c3bb292d04 Mon Sep 17 00:00:00 2001 From: Mohammad Nakhaee Date: Fri, 23 Sep 2022 10:25:35 +0000 Subject: [PATCH 013/117] Resolve "quantity type "string" failing in yaml schema" --- .../editQuantity/EditQuantityExamples.js | 15 +++---- .../editQuantity/EnumEditQuantity.js | 7 ++-- nomad/metainfo/metainfo.py | 7 +++- tests/metainfo/test_yaml_schema.py | 41 ++++++++++++++++++- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/gui/src/components/editQuantity/EditQuantityExamples.js b/gui/src/components/editQuantity/EditQuantityExamples.js index 5bf3bcf20..87ba0ff28 100644 --- a/gui/src/components/editQuantity/EditQuantityExamples.js +++ b/gui/src/components/editQuantity/EditQuantityExamples.js @@ -151,7 +151,7 @@ export function EditQuantityExamples() { m_annotations: eln: component: NumberEditQuantity - defaultDisplayUnit="fs"`} + defaultDisplayUnit: "fs"`} > @@ -165,7 +165,7 @@ export function EditQuantityExamples() { m_annotations: eln: component: NumberEditQuantity - defaultDisplayUnit="eV"`} + defaultDisplayUnit: "eV"`} > @@ -179,7 +179,7 @@ export function EditQuantityExamples() { m_annotations: eln: component: NumberEditQuantity - defaultDisplayUnit="milliampere / ms^2 * cm"`} + defaultDisplayUnit: "milliampere / ms^2 * cm"`} > @@ -304,8 +304,9 @@ export function EditQuantityExamples() { autocomplete_enum: type: type_kind: enum - type_data: + type_data: [ ... + ] m_annotations: eln: component: AutocompleteEditQuantity`} @@ -370,13 +371,13 @@ export function EditQuantityExamples() { - + diff --git a/gui/src/components/editQuantity/EnumEditQuantity.js b/gui/src/components/editQuantity/EnumEditQuantity.js index e12c6704a..e0c64e781 100644 --- a/gui/src/components/editQuantity/EnumEditQuantity.js +++ b/gui/src/components/editQuantity/EnumEditQuantity.js @@ -22,7 +22,8 @@ import {getFieldProps, TextFieldWithHelp} from './StringEditQuantity' import AutoComplete from '@material-ui/lab/Autocomplete' export const EnumEditQuantity = React.memo((props) => { - const {quantityDef, value, onChange, suggestions, ...otherProps} = props + const {quantityDef, value, onChange, ...otherProps} = props + const {suggestions, ...fieldProps} = getFieldProps(quantityDef) const handleChange = useCallback(value => { if (onChange) { @@ -42,7 +43,7 @@ export const EnumEditQuantity = React.memo((props) => { )} @@ -53,7 +54,7 @@ export const EnumEditQuantity = React.memo((props) => { select variant='filled' size='small' withOtherAdornment fullWidth value={value || ''} onChange={event => handleChange(event.target.value)} - {...getFieldProps(quantityDef)} + {...fieldProps} {...otherProps} > {quantityDef.type?.type_data.map(item => ( diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index beded0de5..02c94fec4 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -63,10 +63,13 @@ _primitive_types = { bool: bool, np.bool_: bool} +primitive_type_aliases = {'string': str, 'boolean': bool} _primitive_type_names = { primitive_type.__name__: primitive_type for primitive_type in _primitive_types} +_primitive_type_names.update(primitive_type_aliases) + _types_int_numpy = {np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32, np.uint64} _types_int_python = {int} _types_int = _types_int_python | _types_int_numpy @@ -82,8 +85,8 @@ _types_numpy = _types_num_numpy | _types_str_numpy | _types_bool_numpy _delta_symbols = {'delta_', 'Δ'} validElnTypes = { - 'str': ['str'], - 'bool': ['bool'], + 'str': ['str', 'string'], + 'bool': ['bool', 'boolean'], 'number': [x.__name__ for x in _types_num_python] + [f'np.{x.__name__}' for x in _types_num_numpy], 'datetime': ['Datetime'], 'enum': ['{type_kind: Enum, type_data: [Operator, Responsible_person]}'], diff --git a/tests/metainfo/test_yaml_schema.py b/tests/metainfo/test_yaml_schema.py index cbaeabe7a..8ce002fca 100644 --- a/tests/metainfo/test_yaml_schema.py +++ b/tests/metainfo/test_yaml_schema.py @@ -3,7 +3,7 @@ import pytest import yaml from nomad.datamodel.data import UserReference, AuthorReference -from nomad.metainfo.metainfo import validElnComponents, validElnTypes +from nomad.metainfo.metainfo import validElnComponents, validElnTypes, primitive_type_aliases from nomad.utils import strip from nomad.metainfo import Package, MSection, Quantity, Reference, SubSection, Section, MProxy, MetainfoError @@ -129,6 +129,43 @@ def test_yaml_deserialization(): des_m_package.m_to_dict() +yaml_schema_example_extended_types = strip(''' +m_def: 'nomad.metainfo.metainfo.Package' +sections: + Sample: + base_section: 'nomad.datamodel.metainfo.measurements.Sample' + quantities: + method: + type: string + m_annotations: + eln: + component: StringEditQuantity + spin: + type: boolean + m_annotations: + eln: + component: BoolEditQuantity +''') + + +def test_yaml_extended_types_deserialization(): + des_m_package = yaml_to_package(yaml_schema_example_extended_types) + + des_sample = des_m_package['section_definitions'][0] + + assert des_sample.name == "Sample" + + method = des_sample['quantities'][0] + assert method.name == 'method' + assert method.type == str + + spin = des_sample['quantities'][1] + assert spin.name == 'spin' + assert spin.type == bool + + des_m_package.m_to_dict() + + @pytest.mark.parametrize('yaml_schema, expected_error', [ pytest.param(strip(''' m_def: 'nomad.metainfo.metainfo.Package' @@ -229,6 +266,8 @@ def test_datatype_component_annotations(eln_type, eln_component): quantity = process['quantities'][0] if type(quantity.type).__name__ != 'type': type_name = type(quantity.type).__name__ + if type_name in primitive_type_aliases.keys(): + type_name = primitive_type_aliases[type_name].__name__ package.__init_metainfo__() assert isinstance(exception.value, MetainfoError) assert exception.value.args[0] == 'One constraint was violated: The component `%s` is not compatible with the quantity `%s` of the type `%s`. Accepted components: %s (there are 0 more violations)' \ -- GitLab From e328e747b45599b6bf7f2ca19769eb13b88d6b67 Mon Sep 17 00:00:00 2001 From: David Sikter Date: Mon, 26 Sep 2022 06:56:51 +0000 Subject: [PATCH 014/117] Resolve "Loading custom metainfos updates the global metainfo (through the `_allInheritingSections`)" --- gui/src/components/About.js | 5 +- gui/src/components/DataStore.js | 27 +++- gui/src/components/archive/ArchiveBrowser.js | 17 +- gui/src/components/archive/Browser.spec.js | 2 +- gui/src/components/archive/MetainfoBrowser.js | 47 ++++-- .../archive/MetainfoBrowser.spec.js | 16 +- gui/src/components/archive/conftest.spec.js | 35 ++-- gui/src/components/archive/metainfo.js | 153 ++++++++++++------ gui/src/components/archive/metainfo.spec.js | 2 +- gui/src/components/conftest.spec.js | 10 +- .../editQuantity/ReferenceEditQuantity.js | 17 +- .../components/entry/ArchiveEntryView.spec.js | 12 +- gui/src/components/search/conftest.spec.js | 6 +- .../search/menus/FilterSubMenuArchive.js | 6 +- gui/src/components/uploads/CreateEntry.js | 9 +- gui/src/utils.js | 38 +++++ 16 files changed, 267 insertions(+), 135 deletions(-) diff --git a/gui/src/components/About.js b/gui/src/components/About.js index 6aa3c03c2..bdb3bfef4 100644 --- a/gui/src/components/About.js +++ b/gui/src/components/About.js @@ -164,7 +164,10 @@ export const CodeList = React.memo(({withUploadInstructions}) => { }, []) return - {codeshtml} + {codeshtml.map((html, index) => + + {html} + )} setSelected(null)} /> }) diff --git a/gui/src/components/DataStore.js b/gui/src/components/DataStore.js index 7cc72fd23..bb7b344d0 100644 --- a/gui/src/components/DataStore.js +++ b/gui/src/components/DataStore.js @@ -21,7 +21,7 @@ import { useApi, DoesNotExist } from './api' import { useErrors } from './errors' import { apiBase } from '../config' import { refType, parseNomadUrl, createEntryUrl, systemMetainfoUrl } from '../utils' -import { Metainfo } from './archive/metainfo' +import { getUrlFromDefinition, Metainfo } from './archive/metainfo' import currentSystemMetainfoData from '../metainfo' import YAML from 'yaml' @@ -72,6 +72,7 @@ const DataStore = React.memo(({children}) => { const uploadStore = useRef({}) // The upload store objects const entryStore = useRef({}) // The entry store objects const metainfoDataStore = useRef({}) // The metainfo data store objects + const externalInheritanceCache = useRef({}) // Used to keep track of inheritance between metainfos /** * Gets an upload object from the store, creating it if it doesn't exist (in which case @@ -539,7 +540,7 @@ const DataStore = React.memo(({children}) => { // If needed, create metainfo (which will also initiate parsing) if (!metainfoData._metainfo) { const parent = metainfoBaseUrl === systemMetainfoUrl ? undefined : await getMetainfoAsync(systemMetainfoUrl) - metainfoData._metainfo = new Metainfo(parent, metainfoData, getMetainfoAsync) + metainfoData._metainfo = new Metainfo(parent, metainfoData, getMetainfoAsync, externalInheritanceCache.current) } // Returned object after parsing is completed. return await metainfoData._metainfo._result @@ -629,6 +630,25 @@ const DataStore = React.memo(({children}) => { } } + /** + * Gets all the inheriting sections of the provided section definition. That is: all inheriting + * sections *currently known to the store*. The result is returned as a list of definitions. + */ + function getAllInheritingSections(definition) { + const rv = [] + // Add subclasses (i.e. *directly* inheriting from this section) + const url = getUrlFromDefinition(definition) + rv.push(...(definition._allInternalInheritingSections || [])) + rv.push(...(externalInheritanceCache.current[url] || [])) + // Add recursively (sub-sub classes, sub-sub-sub classes etc.) + const recursive = [] + for (const inheritingSection of rv) { + recursive.push(...getAllInheritingSections(inheritingSection)) + } + rv.push(...recursive) + return rv + } + const contextValue = { getUpload, getUploadAsync, @@ -641,7 +661,8 @@ const DataStore = React.memo(({children}) => { selectedEntry, getMetainfoAsync, getMetainfoDefAsync, - subscribeToMetainfo + subscribeToMetainfo, + getAllInheritingSections } return diff --git a/gui/src/components/archive/ArchiveBrowser.js b/gui/src/components/archive/ArchiveBrowser.js index 85a655db0..fe2fdb32b 100644 --- a/gui/src/components/archive/ArchiveBrowser.js +++ b/gui/src/components/archive/ArchiveBrowser.js @@ -31,7 +31,7 @@ import Browser, { Item, Content, Compartment, Adaptor, formatSubSectionName, lan import { RawFileAdaptor } from './FileBrowser' import { isEditable, PackageMDef, QuantityMDef, removeSubSection, SectionMDef, SubSectionMDef, - useMetainfo, getMetainfoFromDefinition + useMetainfo, getMetainfoFromDefinition, getUrlFromDefinition } from './metainfo' import { ArchiveTitle, metainfoAdaptorFactory, DefinitionLabel } from './MetainfoBrowser' import { Matrix, Number } from './visualizations' @@ -44,7 +44,7 @@ import grey from '@material-ui/core/colors/grey' import classNames from 'classnames' import { useApi } from '../api' import { useErrors } from '../errors' -import { useEntryStoreObj } from '../DataStore' +import { useDataStore, useEntryStoreObj } from '../DataStore' import { SourceApiCall, SourceApiDialogButton, SourceJsonDialogButton } from '../buttons/SourceDialogButton' import DownloadIcon from '@material-ui/icons/CloudDownload' import { Download } from '../entry/Download' @@ -62,6 +62,7 @@ import {EntryButton} from '../nav/Routes' import NavigateIcon from '@material-ui/icons/MoreHoriz' import ReloadIcon from '@material-ui/icons/Replay' import UploadIcon from '@material-ui/icons/CloudUpload' +import { apiBase } from '../../config' export const configState = atom({ key: 'config', @@ -690,6 +691,7 @@ QuantityValue.propTypes = ({ }) const InheritingSections = React.memo(function InheritingSections({def, section, lane}) { + const dataStore = useDataStore() const browser = useContext(browserContext) const handleInheritingSectionsChange = useCallback((e) => { section.m_def = e.target.value @@ -697,10 +699,11 @@ const InheritingSections = React.memo(function InheritingSections({def, section, }, [section, browser, lane]) if (section.m_def) { - return '' + return null } - return (def._allInheritingSections?.length > 0 && + const allInheritingSections = dataStore.getAllInheritingSections(def) + return (allInheritingSections.length > 0 && Multiple specific sections are available @@ -713,11 +716,11 @@ const InheritingSections = React.memo(function InheritingSections({def, section, size="small" select > - + {def.name} - {def._allInheritingSections.map((inheritingSection, i) => { - const sectionValue = inheritingSection._url || inheritingSection._qualifiedName + {allInheritingSections.map((inheritingSection, i) => { + const sectionValue = getUrlFromDefinition(inheritingSection, {installationUrl: apiBase}, true) return ( {inheritingSection.name} diff --git a/gui/src/components/archive/Browser.spec.js b/gui/src/components/archive/Browser.spec.js index 540c3d48a..934e104c0 100644 --- a/gui/src/components/archive/Browser.spec.js +++ b/gui/src/components/archive/Browser.spec.js @@ -92,7 +92,7 @@ test('Test browser lane error boundry', async () => { await navigateTo('dir1/success', browserConfig) expectNoConsoleOutput() // Call lane which fails to render - await expect(selectItemAndWaitForRender(getLane(1), 1, 'fail')).rejects.toThrow() + await expect(selectItemAndWaitForRender(1, 'fail')).rejects.toThrow() expect(within(getLane(2)).queryByText(laneErrorBoundryMessage)).not.toBeNull() expect(filteredConsoleOutput().length).not.toBe(0) } finally { diff --git a/gui/src/components/archive/MetainfoBrowser.js b/gui/src/components/archive/MetainfoBrowser.js index e1ad42052..2dbd9a764 100644 --- a/gui/src/components/archive/MetainfoBrowser.js +++ b/gui/src/components/archive/MetainfoBrowser.js @@ -205,6 +205,10 @@ class MetainfoAdaptor extends Adaptor { const metainfoBaseUrl = getMetainfoFromDefinition(this.def)._url await dataStore.getMetainfoAsync(metainfoBaseUrl) this.unsubscriber = dataStore.subscribeToMetainfo(metainfoBaseUrl, () => {}) + + if (this.def.m_def === SectionMDef || this.def.m_def === SubSectionMDef) { + this.inheritingSections = dataStore.getAllInheritingSections(this.def.sub_section || this.def) + } } } @@ -216,7 +220,7 @@ class MetainfoAdaptor extends Adaptor { export class MetainfoRootAdaptor extends MetainfoAdaptor { async itemAdaptor(key) { - const rootSection = getMetainfoFromDefinition(this.def).getRootSectionDefinitions().find(def => def.name === key) + const rootSection = getMetainfoFromDefinition(this.def).getRootSectionDefinitions().find(def => def._qualifiedName === key) if (rootSection) { return metainfoAdaptorFactory(rootSection) } else { @@ -311,13 +315,13 @@ const Metainfo = React.memo(function Metainfo(props) { const globalMetainfo = useGlobalMetainfo() return - + Entry {globalMetainfo.getRootSectionDefinitions().filter(def => def.name !== 'EntryArchive').map((def, i) => ( - + {def.more?.label || def.name} @@ -342,7 +346,7 @@ export class SectionDefAdaptor extends MetainfoAdaptor { return metainfoAdaptorFactory(innerSectionDef) } } else if (type === '_inheritingSectionDef') { - const inheritingSectionDef = this.def._allInheritingSections.find(inheritingSection => inheritingSection.name === name) + const inheritingSectionDef = this.inheritingSections.find(inheritingSection => inheritingSection._qualifiedName === name) if (inheritingSectionDef) { return metainfoAdaptorFactory(inheritingSectionDef) } @@ -362,7 +366,7 @@ export class SectionDefAdaptor extends MetainfoAdaptor { return super.itemAdaptor(key) } render() { - return + return } } @@ -371,11 +375,17 @@ class SubSectionDefAdaptor extends MetainfoAdaptor { super(def) this.sectionDefAdaptor = new SectionDefAdaptor(this.def.sub_section) } + async initialize(api, dataStore) { + await this.sectionDefAdaptor.initialize(api, dataStore) + } + cleanup() { + this.sectionDefAdaptor.cleanup() + } async itemAdaptor(key) { return this.sectionDefAdaptor.itemAdaptor(key) } render() { - return + return } } @@ -394,7 +404,7 @@ class CategoryDefAdaptor extends MetainfoAdaptor { } } -function SectionDefContent({def}) { +function SectionDefContent({def, inheritingSections}) { const config = useRecoilValue(configState) const metainfoConfig = useRecoilValue(metainfoConfigState) const filter = def.extends_base_section ? () => true : def => { @@ -437,10 +447,10 @@ function SectionDefContent({def}) { })} } - {def._allInheritingSections?.length > 0 && + {inheritingSections.length > 0 && - {def._allInheritingSections.map((inheritingSection, index) => { - const key = `_inheritingSectionDef@${inheritingSection.name}` + {inheritingSections.map((inheritingSection, index) => { + const key = `_inheritingSectionDef@${inheritingSection._qualifiedName}` const categories = inheritingSection.categories const unused = categories?.find(c => c.name === 'Unused') return @@ -511,33 +521,36 @@ function SectionDefContent({def}) { } SectionDefContent.propTypes = ({ - def: PropTypes.object + def: PropTypes.object, + inheritingSections: PropTypes.array }) -function SectionDef({def}) { +function SectionDef({def, inheritingSections}) { return - + } SectionDef.propTypes = ({ - def: PropTypes.object + def: PropTypes.object, + inheritingSections: PropTypes.array }) -function SubSectionDef({def}) { +function SubSectionDef({def, inheritingSections}) { const sectionDef = def.sub_section return - + } SubSectionDef.propTypes = ({ - def: PropTypes.object + def: PropTypes.object, + inheritingSections: PropTypes.array }) function DefinitionProperties({def, children}) { diff --git a/gui/src/components/archive/MetainfoBrowser.spec.js b/gui/src/components/archive/MetainfoBrowser.spec.js index f3f10c6f9..0e65f7a59 100644 --- a/gui/src/components/archive/MetainfoBrowser.spec.js +++ b/gui/src/components/archive/MetainfoBrowser.spec.js @@ -39,19 +39,19 @@ function rand() { /** * Lower values -> visits fewer locations. */ -const visitProbabilityDecayFactor = 0.5 +const visitProbabilityDecayFactor = 0.1 -function metainfoItemFilter(parentPath, items) { +function metainfoItemFilter(parentPath, itemKeys) { // The metainfo tree is very big, so we need to limit the crawling. This method is used // to make the selection. const parentSegments = parentPath.split('/').length - 1 if (parentSegments === 0) { // Root - filter nothing - return Object.keys(items) + return itemKeys } const rv = [] const categoryCache = {} - for (const itemKey of Object.keys(items)) { + for (const itemKey of itemKeys) { // Compute a "category" for the item, which is a string based on certain properties of // the itemKey. When selecting items to visit, we try to cover as many categories as possible let category = itemKey.startsWith('_') ? '1' : '0' @@ -67,7 +67,7 @@ function metainfoItemFilter(parentPath, items) { } cache.push(itemKey) } - const visitProbability = visitProbabilityDecayFactor ** (parentSegments - 1) + const visitProbability = visitProbabilityDecayFactor ** parentSegments for (const category of Object.keys(categoryCache).sort()) { if (rand() < visitProbability) { // Include one of the itemKeys in this category, selected at random @@ -89,9 +89,9 @@ test('Browse metainfo pseudorandomly', async () => { }) const path = '' - const lane = await navigateTo(path) + await navigateTo(path) const laneIndex = path ? path.split('/').length : 0 - const {count} = await browseRecursively(lane, laneIndex, join('*MetaInfoBrowser*', path), metainfoItemFilter, 2) + const {count} = await browseRecursively(laneIndex, join('*MetaInfoBrowser*', path), metainfoItemFilter, 2) // Currently we do not test that the visited items are the same using the hash // returned by browseRecursively. This is because any change in the metainfo @@ -101,6 +101,6 @@ test('Browse metainfo pseudorandomly', async () => { // Check that the tested number of paths is enough, but also not too high. Adjust // visitProbabilityDecayFactor if the number is not in this range. - expect(count).toBeGreaterThan(500) + expect(count).toBeGreaterThan(100) expect(count).toBeLessThan(700) }, 20 * minutes) // NOTE!!! Do not increase this timeout! Rather, adjust the visitProbabilityDecayFactor diff --git a/gui/src/components/archive/conftest.spec.js b/gui/src/components/archive/conftest.spec.js index 60319b20c..7347fde76 100644 --- a/gui/src/components/archive/conftest.spec.js +++ b/gui/src/components/archive/conftest.spec.js @@ -142,10 +142,9 @@ export async function checkLanes(path, browserConfig) { * as the last argument (for optimization purposes), otherwise it will be fetched from the * itemKey. */ -export async function selectItemAndWaitForRender(lane, laneIndex, itemKey, item = null) { - if (!item) { - item = within(lane).getByTestId(`item:${itemKey}`) - } +export async function selectItemAndWaitForRender(laneIndex, itemKey) { + const lane = getLane(laneIndex) + const item = within(lane).getByTestId(`item:${itemKey}`) await userEvent.click(item) await waitFor(() => { const nextLane = getLane(laneIndex + 1, itemKey) @@ -171,14 +170,14 @@ export async function navigateTo(path, browserConfig) { const segments = path.split('/') let subpath = '' let laneIndex = 0 - let lane = getLane(0) + let lane let nextLane = getLane(1) for (const segment of segments) { subpath = join(subpath, segment) const selectedItemKey = nextLane ? getLaneKey(nextLane) : null if (selectedItemKey !== segment) { // Need to select a (different) item in this lane - lane = await selectItemAndWaitForRender(lane, laneIndex, segment) + lane = await selectItemAndWaitForRender(laneIndex, segment) if (browserConfig?.browserTree) { await checkLanes(subpath, browserConfig) } @@ -196,12 +195,13 @@ export async function navigateTo(path, browserConfig) { * itemFilter is provided, all items will be visited. This method provides an easy way to * verify that the browser renders all (or at least a lot of) paths correctly. */ -export async function browseRecursively(lane, laneIndex, path, itemFilter, filterKeyLength = 2, filterMemory = null) { +export async function browseRecursively(laneIndex, path, itemFilter, filterKeyLength = 2, filterMemory = null) { + const lane = getLane(laneIndex) let count = 0 const hash = crypto.createHash('sha512') if (filterMemory === null) { - filterMemory = {} + filterMemory = new Set() } // Click on all discovered item-lists to open them for (const itemList of within(lane).queryAllByRole('item-list')) { @@ -215,26 +215,27 @@ export async function browseRecursively(lane, laneIndex, path, itemFilter, filte throw error } } - const items = {} + let itemKeys = [] for (const item of within(lane).queryAllByTestId(/^item:/)) { const itemKey = getItemKey(item) - items[itemKey] = item + itemKeys.push(itemKey) hash.update(itemKey) } - const itemKeys = itemFilter ? itemFilter(path, items) : Object.keys(items) + if (itemFilter) { + itemKeys = itemFilter(path, itemKeys) + } for (const itemKey of itemKeys) { const itemPath = join(path, itemKey) const segments = itemPath.split('/') const filterKey = segments.slice(segments.length - filterKeyLength).join('/') - if (!filterMemory[filterKey]) { - filterMemory[filterKey] = true - const item = items[itemKey] + if (!filterMemory.has(filterKey)) { + filterMemory.add(filterKey) const nextPath = `${path}/${itemKey}` // Uncomment line below if you want to show the paths visited. // process.stdout.write(`next path: ${nextPath}\n`) - let nextLane + // process.stdout.write(`mem: ${process.memoryUsage().heapTotal / 1e9}\n`) try { - nextLane = await selectItemAndWaitForRender(lane, laneIndex, itemKey, item) + await selectItemAndWaitForRender(laneIndex, itemKey) expectNoConsoleOutput() } catch (error) { process.stdout.write(`ERROR encountered when browsing to: ${nextPath}\n`) @@ -242,7 +243,7 @@ export async function browseRecursively(lane, laneIndex, path, itemFilter, filte } // new lane rendered successfully count++ - const rv = await browseRecursively(nextLane, laneIndex + 1, nextPath, itemFilter, filterKeyLength, filterMemory) + const rv = await browseRecursively(laneIndex + 1, nextPath, itemFilter, filterKeyLength, filterMemory) count += rv.count hash.update(rv.hash) } diff --git a/gui/src/components/archive/metainfo.js b/gui/src/components/archive/metainfo.js index 13f78b1ee..3d947b09f 100644 --- a/gui/src/components/archive/metainfo.js +++ b/gui/src/components/archive/metainfo.js @@ -21,7 +21,9 @@ import PropTypes from 'prop-types' import { useErrors } from '../errors' import { useApi } from '../api' import { useDataStore } from '../DataStore' -import { parseNomadUrl, resolveNomadUrl, resolveInternalRef, systemMetainfoUrl, createEntryUrl } from '../../utils' +import { + parseNomadUrl, resolveNomadUrl, resolveInternalRef, systemMetainfoUrl, createEntryUrl, + urlEncodePath, urlJoin, relativizeNomadUrl } from '../../utils' import { apiBase } from '../../config' const metainfoContext = React.createContext() @@ -180,10 +182,11 @@ export class Metainfo { /** * Constructs a Metainfo object. Note, this should only be invoked by the store. */ - constructor(parent, data, getMetainfoAsync) { + constructor(parent, data, getMetainfoAsync, externalInheritanceCache) { this._parent = parent this._data = data this._getMetainfoAsync = getMetainfoAsync + this._externalInheritanceCache = externalInheritanceCache this._url = data._url // the data url, always string this._parsedUrl = parseNomadUrl(data._url) @@ -294,14 +297,14 @@ export class Metainfo { async _parse() { // Parse data if (this._data.packages) { - await this._addPackages(this._data.packages) + let pkgIndex = 0 + for (const pkg of this._data.packages) { + await this._addPackage(pkg, null, this._data, 'packages', pkgIndex++) + } } if (this._data.definitions) { - const entryId = this._data?.metadata?.entry_id // TODO: which format to use? - const mainfile = this._data?.metadata?.mainfile - const uploadId = this._data?.metadata?.upload_id - const url = mainfile && uploadId && `../uploads/${uploadId}/raw/${mainfile}#definitions` - await this._addPackages([this._data.definitions], entryId ? `entry_id:${entryId}` : null, url) + const entryId = this._parsedUrl.entryId + await this._addPackage(this._data.definitions, `entry_id:${entryId}`, this._data, 'definitions') } this._isParsed = true return this @@ -323,31 +326,43 @@ export class Metainfo { return sectionDef } - sectionDef.base_sections = await this.resolveDefinitionList(sectionDef.base_sections || []) + sectionDef.base_sections = sectionDef.base_sections || [] sectionDef.quantities = sectionDef.quantities || [] sectionDef.sub_sections = sectionDef.sub_sections || [] sectionDef.inner_section_definitions = sectionDef.inner_section_definitions || [] sectionDef._allBaseSections = [] - sectionDef._allInheritingSections = [] + sectionDef._allInternalInheritingSections = [] sectionDef._parentSections = [] sectionDef._parentSubSections = [] - if (!sectionDef.extends_base_section) { - for (const baseSection of sectionDef.base_sections) { + const resolvedBaseSections = [] + for (const baseSectionRef of sectionDef.base_sections) { + const baseSection = await this.resolveDefinition(baseSectionRef) + resolvedBaseSections.push(baseSection) + if (!sectionDef.extends_base_section) { await this._initSection(baseSection) sectionDef._allBaseSections.push(baseSection) baseSection._allBaseSections.forEach(baseBaseSection => sectionDef._allBaseSections.push(baseBaseSection)) - if (!baseSection._allInheritingSections.includes(sectionDef)) { - baseSection._allInheritingSections.push(sectionDef) - sectionDef._allInheritingSections.forEach(inheritingInheritingSection => { - if (!baseSection._allInheritingSections.includes(inheritingInheritingSection)) { - baseSection._allInheritingSections.push(inheritingInheritingSection) - } - }) + if (typeof baseSectionRef === 'string' && (baseSectionRef.startsWith('#') || baseSectionRef.startsWith('/'))) { + // Internal base section link (both this and the base section defined in the same metainfo data) + if (!baseSection._allInternalInheritingSections.includes(sectionDef)) { + baseSection._allInternalInheritingSections.push(sectionDef) + } + } else { + // External base section link. These are stored in a separate cache in the store. + const baseSectionUrl = getUrlFromDefinition(baseSection) + let externalInheritingSections = this._externalInheritanceCache[baseSectionUrl] + if (!externalInheritingSections) { + externalInheritingSections = [] + this._externalInheritanceCache[baseSectionUrl] = externalInheritingSections + } + if (!externalInheritingSections.includes(sectionDef)) { + externalInheritingSections.push(sectionDef) + } } } } - + sectionDef.base_sections = resolvedBaseSections return sectionDef } @@ -382,9 +397,6 @@ export class Metainfo { pkg._sections[sectionDef.name] = sectionDef await this._initSection(sectionDef) sectionDef._qualifiedName = parentDef ? `${parentDef._qualifiedName || parentDef._unique_id || parentDef.name}.${sectionDef.name}` : sectionDef.name - if (parentDef?._url) { - sectionDef._url = `${parentDef._url}/section_definitions/${parentIndex}` - } sectionDef._package = pkg let index = 0 @@ -412,17 +424,15 @@ export class Metainfo { sectionDef._allProperties = await this._getAllProperties(sectionDef) sectionDef._properties = {} - const addProperty = async property => { - sectionDef._properties[property.name] = property - if (!sectionDef.extends_base_section) { - property._section = sectionDef - property._qualifiedName = `${sectionDef._qualifiedName}.${property.name}` - } + + // Do for new properties (i.e. defined in THIS section, not inherited from base sections) + const addNewProperty = async (property, parentProperty, index) => { + property._section = sectionDef + property._parentProperty = parentProperty + property._parentIndex = index + property._qualifiedName = `${sectionDef._qualifiedName}.${property.name}` property._package = pkg await this._addDef(property) - } - for (const property of sectionDef._allProperties) { - await addProperty(property) if (property.m_def === QuantityMDef) { property.shape = property.shape || [] if (isReference(property)) { @@ -438,19 +448,32 @@ export class Metainfo { property._section = sectionDef } } + for (const def of sectionDef.quantities) { + await addNewProperty(def, 'quantities', index) + } + for (const def of sectionDef.sub_sections) { + await addNewProperty(def, 'sub_sections', index) + } + + // Do for all properties (new + inherited) + for (const property of sectionDef._allProperties) { + sectionDef._properties[property.name] = property + } } - async _addPackage(pkg, unique_id, url) { + async _addPackage(pkg, unique_id, pkgParentData, parentProperty, parentIndex) { this._packagePrefixCache = null pkg.m_def = PackageMDef if (unique_id) { pkg._unique_id = unique_id } - pkg._url = url const packageName = pkg.name || '*' this._packageDefs[packageName] = pkg await this._addDef(pkg) + pkg._pkgParentData = pkgParentData + pkg._parentProperty = parentProperty + pkg._parentIndex = parentIndex pkg._sections = {} pkg.category_definitions = pkg.category_definitions || [] pkg.section_definitions = pkg.section_definitions || [] @@ -465,14 +488,6 @@ export class Metainfo { for (const sectionDef of pkg.section_definitions) { await this._addSection(pkg, sectionDef, pkg, 'section_definitions', index++) } - - pkg._metainfo = this // Allows us to, from a pkg definition, get the metainfo object containing it. - } - - async _addPackages(packages, unique_id, url) { - for (const pkg of packages) { - await this._addPackage(pkg, unique_id, url) - } } path(nameOrDef) { @@ -608,14 +623,54 @@ export function removeSubSection(section, subSectionDef, index) { } /** - * @param {*} definition The section definition to create a reference for. - * @returns The reference fragment for the given section definition. + * Given a definition, compute its url (string). Optionally, you can specify relativeTo, an + * object of the form {installationUrl, uploadId, entryId} (containing the first, the two first, + * or all three atributes, depending on what you want the url to be relative to). If relativeTo + * is left out, we return an absolute url. You may also specify preferMainfile = true if you + * want the url to use the mainfile rather than the entryId when possible (more humanly readable). */ -export function getSectionReference(definition) { - if (!definition._parent) { - return '' + export function getUrlFromDefinition(definition, relativeTo = null, preferMainfile = false) { + const pkg = definition.m_def === PackageMDef ? definition : definition._package + const metainfo = pkg._pkgParentData._metainfo + if (!metainfo._parsedUrl.entryId && relativeTo?.installationUrl === metainfo._parsedUrl.installationUrl) { + return definition._qualifiedName + } + let parentUrl + switch (definition.m_def) { + case PackageMDef: { + const entryId = metainfo._parsedUrl.entryId + if (entryId) { + // Custom metainfo + let rv + if (relativeTo) { + rv = relativizeNomadUrl( + metainfo._parsedUrl, relativeTo.installationUrl, relativeTo.uploadId, relativeTo.entryId) + } else { + rv = metainfo._url + } + if (preferMainfile) { + const mainfile = metainfo._data.metadata.mainfile + rv = rv.replace(`/archive/${entryId}`, `/raw/${urlEncodePath(mainfile)}`) + } + return rv + } + // System metainfo + return urlJoin(metainfo._url, 'packages', definition._parentIndex) + } + case SectionMDef: + parentUrl = getUrlFromDefinition(definition._parent, relativeTo, preferMainfile) + break + case SubSectionMDef: + case QuantityMDef: + parentUrl = getUrlFromDefinition(definition._section, relativeTo, preferMainfile) + break + case CategoryMDef: + parentUrl = getUrlFromDefinition(definition._package, relativeTo, preferMainfile) + break + default: + throw new Error('Could not get url from definition: bad m_def') } - const ref = `${getSectionReference(definition._parent)}/${definition._parentProperty}` + const ref = `${parentUrl}/${definition._parentProperty}` if (!isNaN(definition._parentIndex)) { return `${ref}/${definition._parentIndex}` } @@ -626,7 +681,7 @@ export function getSectionReference(definition) { * Given a definition, gets the metainfo object in which it is defined. */ export function getMetainfoFromDefinition(definition) { - if (definition._metainfo) return definition._metainfo + if (definition._pkgParentData) return definition._pkgParentData._metainfo return getMetainfoFromDefinition(definition._package || definition._parent || definition._section) } diff --git a/gui/src/components/archive/metainfo.spec.js b/gui/src/components/archive/metainfo.spec.js index aa391f949..aea4aa006 100644 --- a/gui/src/components/archive/metainfo.spec.js +++ b/gui/src/components/archive/metainfo.spec.js @@ -21,7 +21,7 @@ import { systemMetainfoUrl } from '../../utils' async function createMetainfo(data, parent, url = systemMetainfoUrl) { data._url = url - data._metainfo = new Metainfo(parent, data, null) + data._metainfo = new Metainfo(parent, data, null, {}) return await data._metainfo._result } diff --git a/gui/src/components/conftest.spec.js b/gui/src/components/conftest.spec.js index 9368715e6..fcd45b924 100644 --- a/gui/src/components/conftest.spec.js +++ b/gui/src/components/conftest.spec.js @@ -43,9 +43,7 @@ import DataStore from './DataStore' import searchQuantities from '../searchQuantities' import { keycloakBase } from '../config' import { useKeycloak } from '@react-keycloak/web' -import { GlobalMetainfo, Metainfo } from './archive/metainfo' -import metainfoData from '../metainfo' -import { systemMetainfoUrl } from '../utils' +import { GlobalMetainfo } from './archive/metainfo' beforeEach(async () => { // For some strange reason, the useKeycloak mock gets reset if we set it earlier @@ -77,12 +75,6 @@ beforeEach(async () => { disconnect() {} } window.ResizeObserver = ResizeObserver - - // TODO: Hacky: resetting the global metainfo, since loading custom metainfos update it, and it - // creates effects across tests. Need a better solution. - metainfoData._url = systemMetainfoUrl - metainfoData._metainfo = new Metainfo(null, metainfoData, null) - await metainfoData._metainfo._result }) const fs = require('fs') diff --git a/gui/src/components/editQuantity/ReferenceEditQuantity.js b/gui/src/components/editQuantity/ReferenceEditQuantity.js index 5265e97b6..c23df94ad 100644 --- a/gui/src/components/editQuantity/ReferenceEditQuantity.js +++ b/gui/src/components/editQuantity/ReferenceEditQuantity.js @@ -27,6 +27,8 @@ import { ItemButton } from '../archive/Browser' import { getFieldProps } from './StringEditQuantity' import { isWaitingForUpdateTestId, refType, resolveNomadUrl } from '../../utils' import AddIcon from '@material-ui/icons/AddCircle' +import { getUrlFromDefinition } from '../archive/metainfo' +import { useDataStore } from '../DataStore' const filter = createFilterOptions() @@ -36,7 +38,8 @@ const useStyles = makeStyles(theme => ({ const ReferenceEditQuantity = React.memo(function ReferenceEditQuantity(props) { const styles = useStyles() - const {uploadId, archive, url} = useEntryPageContext('*') + const dataStore = useDataStore() + const {installationUrl, uploadId, archive, url} = useEntryPageContext('*') const {quantityDef, value, onChange, index} = props const [entry, setEntry] = useState(null) const {api} = useApi() @@ -47,8 +50,8 @@ const ReferenceEditQuantity = React.memo(function ReferenceEditQuantity(props) { const fetchedSuggestionsFor = useRef() const referencedSectionQualifiedNames = useMemo(() => { - return [...quantityDef.type._referencedSection._allInheritingSections.map(section => section._qualifiedName), quantityDef.type._referencedSection._qualifiedName] - }, [quantityDef]) + return [...dataStore.getAllInheritingSections(quantityDef.type._referencedSection).map(section => section._qualifiedName), quantityDef.type._referencedSection._qualifiedName] + }, [dataStore, quantityDef]) const fetchSuggestions = useCallback(input => { if (fetchedSuggestionsFor.current === input) { return // We've already fetched suggestions for this search string @@ -142,10 +145,10 @@ const ReferenceEditQuantity = React.memo(function ReferenceEditQuantity(props) { } }, [onChange]) - const createNewEntry = useCallback((uploadId, fileName) => { + const createNewEntry = useCallback((fileName) => { const archive = { data: { - m_def: quantityDef.type._referencedSection._url || quantityDef.type._referencedSection._qualifiedName + m_def: getUrlFromDefinition(quantityDef.type._referencedSection, {installationUrl, uploadId}, true) } } return new Promise((resolve, reject) => { @@ -166,12 +169,12 @@ const ReferenceEditQuantity = React.memo(function ReferenceEditQuantity(props) { reject(new Error(error)) }) }) - }, [api, quantityDef.type._referencedSection._qualifiedName, quantityDef.type._referencedSection._url]) + }, [api, quantityDef.type._referencedSection, installationUrl, uploadId]) const handleValueChange = useCallback((event, value) => { if (value?.createNewEntry) { value.entry_name = `${value.createNewEntry}.archive.json` - createNewEntry(value.upload_id, value.createNewEntry) + createNewEntry(value.createNewEntry) .then(response => { setInputValue(response.processing.entry.mainfile) changeValue({ diff --git a/gui/src/components/entry/ArchiveEntryView.spec.js b/gui/src/components/entry/ArchiveEntryView.spec.js index 4757d3042..be3841d6e 100644 --- a/gui/src/components/entry/ArchiveEntryView.spec.js +++ b/gui/src/components/entry/ArchiveEntryView.spec.js @@ -35,13 +35,13 @@ afterEach(() => { closeAPI() }) -function archiveItemFilter(parentPath, items) { +function archiveItemFilter(parentPath, itemKeys) { // The archive tree is very big and contains referential cycles, so we need to limit the crawling. // This method is used to make the selection. const segments = parentPath.split('/') if (segments.length === 1) { // Root - filter nothing - return Object.keys(items) + return itemKeys } if (segments[segments.length - 2] === '_metainfo' || segments[segments.length - 2] === '_baseSectionDef@0') { // Never step deeper than one level into metainfo definitions, these are tested elsewhere @@ -50,7 +50,7 @@ function archiveItemFilter(parentPath, items) { } const rv = [] const itemLists = {} - for (const itemKey of Object.keys(items)) { + for (const itemKey of itemKeys) { const parts = itemKey.split(':') if (parts.length === 2) { const [label, index] = parts @@ -102,9 +102,9 @@ test.each([ await userEvent.click(screen.getByRoleAndText('checkbox', 'all defined')) expect(await within(getLane(0)).findByText('processing_logs')).toBeVisible() } - const lane = await navigateTo(path) + await navigateTo(path) const laneIndex = path ? path.split('/').length : 0 - await browseRecursively(lane, laneIndex, join(`*ArchiveBrowser ${name}*`, path), archiveItemFilter, filterKeyLength) + await browseRecursively(laneIndex, join(`*ArchiveBrowser ${name}*`, path), archiveItemFilter, filterKeyLength) }, 20 * minutes) test('inheriting sections', async () => { @@ -113,7 +113,7 @@ test('inheriting sections', async () => { expect(await screen.findByText('Entry')).toBeVisible() const path = 'data' - const sectionName = '../uploads/archive_browser_test/raw/inheriting-schema.archive.yaml#definitions/section_definitions/1' + const sectionName = '../uploads/archive_browser_test/raw/inheriting-schema.archive.yaml#/definitions/section_definitions/1' await navigateTo(path) await userEvent.click(await screen.findByTestId('subsection:C1')) diff --git a/gui/src/components/search/conftest.spec.js b/gui/src/components/search/conftest.spec.js index c1e1d534f..cba7c14e0 100644 --- a/gui/src/components/search/conftest.spec.js +++ b/gui/src/components/search/conftest.spec.js @@ -19,7 +19,7 @@ import React from 'react' import PropTypes from 'prop-types' import assert from 'assert' -import { waitFor, waitForElementToBeRemoved } from '@testing-library/dom' +import { waitFor } from '@testing-library/dom' import elementData from '../../elementData.json' import { screen, WrapperDefault, WrapperNoAPI } from '../conftest.spec' import { render } from '@testing-library/react' @@ -145,7 +145,9 @@ export async function expectInputRange(quantity, loaded, histogram, anchored, mi // Check histogram if (histogram) { // Check that placeholder disappears - !loaded && await waitForElementToBeRemoved(() => root.getByTestId('inputrange-histogram-placeholder')) + if (!loaded) { + await waitFor(() => expect(root.queryByTestId('inputrange-histogram-placeholder')).toBe(null)) + } } // Test text elements if the component is not anchored diff --git a/gui/src/components/search/menus/FilterSubMenuArchive.js b/gui/src/components/search/menus/FilterSubMenuArchive.js index b2cab4881..2095613b1 100644 --- a/gui/src/components/search/menus/FilterSubMenuArchive.js +++ b/gui/src/components/search/menus/FilterSubMenuArchive.js @@ -28,6 +28,7 @@ import { useSearchContext } from '../SearchContext' import InputItem from '../input/InputItem' import { InputTextQuantity } from '../input/InputText' import { useErrors } from '../../errors' +import { useDataStore } from '../../DataStore' const filterProperties = def => !(def.name.startsWith('x_') || def.virtual) @@ -148,6 +149,7 @@ const FilterSubMenuArchive = React.memo(({ value, ...rest }) => { + const dataStore = useDataStore() const globalMetainfo = useGlobalMetainfo() const [[options, tree], setOptions] = useState([[], {}]) const {raiseError} = useErrors() @@ -180,7 +182,7 @@ const FilterSubMenuArchive = React.memo(({ } if (def.m_def === SectionMDef) { def._allProperties.filter(filterProperties).forEach(def => addDef(def, fullName, node)) - def._allInheritingSections.forEach(def => { + dataStore.getAllInheritingSections(def).forEach(def => { def._allProperties.filter(filterProperties).forEach(def => addDef(def, fullName, node)) }) } @@ -189,7 +191,7 @@ const FilterSubMenuArchive = React.memo(({ setOptions([options, tree]) }) .catch(raiseError) - }, [raiseError, globalMetainfo, setOptions]) + }, [raiseError, dataStore, globalMetainfo, setOptions]) return diff --git a/gui/src/components/uploads/CreateEntry.js b/gui/src/components/uploads/CreateEntry.js index d5a3201c0..f1033aeb1 100644 --- a/gui/src/components/uploads/CreateEntry.js +++ b/gui/src/components/uploads/CreateEntry.js @@ -5,11 +5,11 @@ import { useApi } from '../api' import { useErrors } from '../errors' import { getUrl } from '../nav/Routes' import { useHistory, useLocation } from 'react-router-dom' -import { getSectionReference, SectionMDef, useGlobalMetainfo } from '../archive/metainfo' +import { getUrlFromDefinition, SectionMDef, useGlobalMetainfo } from '../archive/metainfo' import { useUploadPageContext } from './UploadPageContext' const CreateEntry = React.memo(function CreateEntry(props) { - const {uploadId, isProcessing} = useUploadPageContext() + const {installationUrl, uploadId, isProcessing} = useUploadPageContext() const {api} = useApi() const {raiseError} = useErrors() const globalMetainfo = useGlobalMetainfo() @@ -72,8 +72,7 @@ const CreateEntry = React.memo(function CreateEntry(props) { const newTemplates = getTemplatesFromDefinitions( archive.definitions.section_definitions, archive.metadata.entry_id, archive, section => { - const fragment = getSectionReference(section) - return `../uploads/${archive.metadata.upload_id}/raw/${archive.metadata.mainfile}#/definitions${fragment}` + return getUrlFromDefinition(section, {installationUrl, uploadId}, true) }) newTemplates.forEach(template => { templates.push(template) @@ -87,7 +86,7 @@ const CreateEntry = React.memo(function CreateEntry(props) { } getTemplates().then(setTemplates).catch(raiseError) - }, [api, raiseError, setTemplates, globalMetainfo, isProcessing]) + }, [api, raiseError, setTemplates, globalMetainfo, isProcessing, installationUrl, uploadId]) const handleAdd = useCallback(() => { api.put(`uploads/${uploadId}/raw/?file_name=${name}.archive.json&wait_for_processing=true`, selectedTemplate.archive) diff --git a/gui/src/utils.js b/gui/src/utils.js index 2fe2bd1a0..7baecb23a 100644 --- a/gui/src/utils.js +++ b/gui/src/utils.js @@ -1060,6 +1060,44 @@ export function resolveNomadUrlNoThrow(url, baseUrl) { } } +/** + * Relativizes a url with respect to the provided installationUrl, uploadId, entryId, and + * returns the shortest possible relative url, as a string. + * This method thus basically does the *opposite* of resolveNomadUrl. You can specify either none, + * the first, the two first, or all three arguments of the (installationUrl, uploadId, entryId) + * tuple. The url provided must be absolute. + */ +export function relativizeNomadUrl(url, installationUrl, uploadId, entryId) { + const parsedUrl = parseNomadUrl(url) + if (!parsedUrl.isResolved) { + throw new Error(`Absolute url required, got ${url}.`) + } + if (!parsedUrl.uploadId) { + throw new Error(`Expected url to specify an upload, got ${url}`) + } + if (parsedUrl.installationUrl !== installationUrl) { + // Nothing to relativize + return normalizeNomadUrl(parsedUrl) + } + if (parsedUrl.entryId === entryId) { + // Same installation and entry + return '#' + (parsedUrl.path || '/') + } + // Same installation, possibly also same upload + let rv = parsedUrl.uploadId === uploadId ? '../upload' : `../uploads/${parsedUrl.uploadId}` + if (parsedUrl.type === refType.archive || parsedUrl.type === refType.metainfo) { + rv = `${rv}/archive/${parsedUrl.entryId}` + if (parsedUrl.path) { + rv = `${rv}#${parsedUrl.path}` + } + } else if (parsedUrl.type === refType.upload) { + if (parsedUrl.path) { + rv = `${rv}/raw/${parsedUrl.path}` + } + } + return rv +} + /** * Returns a url string which is "normalized", i.e. it is absolute and have the preferred * url form for referencing the resource/data. Normalized urls always prefer identifying -- GitLab From 6517d7792499549efbd3ce4424b7c5936eec192b Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Fri, 23 Sep 2022 14:49:38 +0200 Subject: [PATCH 015/117] Fixed displayed error message for unavailable API. --- gui/src/components/api.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/gui/src/components/api.js b/gui/src/components/api.js index 39692b7a3..1d44de321 100644 --- a/gui/src/components/api.js +++ b/gui/src/components/api.js @@ -67,7 +67,10 @@ function handleApiError(e) { } let error = null - if (e.response) { + if (e?.message === 'Failed to fetch' || e?.message === 'Network Error') { + error = new ApiError(e.message) + error.status = 400 + } else if (e.response) { const body = e.response.body const message = (body && (body.message || body.description)) || e.response?.data?.detail || e.response.statusText const errorMessage = `${message} (${e.response.status})` @@ -85,13 +88,8 @@ function handleApiError(e) { error.status = e.response.status error.apiMessage = message } else { - if (e.message === 'Failed to fetch' || e.message === 'Network Error') { - error = new ApiError(e.message) - error.status = 400 - } else { - const errorMessage = e.status ? `${e} (${e.status})` : '' + e - error = new Error(errorMessage) - } + const errorMessage = e.status ? `${e} (${e.status})` : '' + e + error = new Error(errorMessage) } throw error } -- GitLab From 79c5ce89afe64ee1cb153a21df313b023305a4ca Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Fri, 23 Sep 2022 14:50:00 +0200 Subject: [PATCH 016/117] Fixed missing key prop warning for code list on about page. --- gui/src/components/About.js | 43 ++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/gui/src/components/About.js b/gui/src/components/About.js index bdb3bfef4..42538fa3f 100644 --- a/gui/src/components/About.js +++ b/gui/src/components/About.js @@ -94,28 +94,26 @@ export const CodeList = React.memo(({withUploadInstructions}) => { const [selected, setSelected] = useState(null) // Create lists containing code name and category - const codes = [] const categorySizes = {} - Object.entries(parserMetadata).forEach(([code, metadata]) => { - const name = metadata.codeLabel || code - const category = metadata.codeCategory - if (categorySizes[category]) { - categorySizes[category] += 1 - } else { - categorySizes[category] = 1 - } - if (!metadata || code === 'example') { - return - } - - const link = withUploadInstructions - ? [code, category, setSelected(code)}>{name}] - : metadata.codeUrl - ? [code, category, {name}] - : [code, category, name] + const codes = Object.entries(parserMetadata) + .filter(([code, metadata]) => metadata && code !== 'example') + .map(([code, metadata], index) => { + const name = metadata.codeLabel || code + const category = metadata.codeCategory + if (categorySizes[category]) { + categorySizes[category] += 1 + } else { + categorySizes[category] = 1 + } - codes.push(link) - }) + if (withUploadInstructions) { + return [code, category, setSelected(code)}>{name}] + } else if (metadata.codeUrl) { + return [code, category, {name}] + } else { + return [code, category, name] + } + }) // Sort by category size, then by program name. Codes without category go to // the end. @@ -164,10 +162,7 @@ export const CodeList = React.memo(({withUploadInstructions}) => { }, []) return - {codeshtml.map((html, index) => - - {html} - )} + {codeshtml.map((html, index) => {html})} setSelected(null)} /> }) -- GitLab From 3857c72cd420e066250aa098baf2757385dccd7e Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Fri, 23 Sep 2022 14:50:49 +0200 Subject: [PATCH 017/117] Fixed error message on 404 for upload file view. --- gui/src/components/uploads/UploadFilesView.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gui/src/components/uploads/UploadFilesView.js b/gui/src/components/uploads/UploadFilesView.js index 8c9239cea..1fda79a73 100644 --- a/gui/src/components/uploads/UploadFilesView.js +++ b/gui/src/components/uploads/UploadFilesView.js @@ -21,9 +21,17 @@ import FileBrowser from '../archive/FileBrowser' import Page from '../Page' import { useUploadPageContext } from './UploadPageContext' import { createUploadUrl } from '../../utils' +import { Typography } from '@material-ui/core' const UploadFilesView = React.memo(function UploadFilesView() { - const {installationUrl, uploadId} = useUploadPageContext() + const {installationUrl, uploadId, error, hasUpload} = useUploadPageContext() + + if (!hasUpload) { + return + {(error ? {error.apiMessage || error.message || 'Failed to load'} : loading ...)} + + } + return Date: Fri, 23 Sep 2022 14:51:12 +0200 Subject: [PATCH 018/117] Fixed error on browsing files of pubilshed entry. --- nomad/app/v1/routers/uploads.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nomad/app/v1/routers/uploads.py b/nomad/app/v1/routers/uploads.py index 8635a9e2b..e07f90ca6 100644 --- a/nomad/app/v1/routers/uploads.py +++ b/nomad/app/v1/routers/uploads.py @@ -507,12 +507,12 @@ async def get_upload( upload_id: str = Path( ..., description='The unique id of the upload to retrieve.'), - user: User = Depends(create_user_dependency(required=True))): + user: User = Depends(create_user_dependency())): ''' Fetches a specific upload by its upload_id. ''' # Get upload (or throw exception if nonexistent/no access) - upload = _get_upload_with_read_access(upload_id, user) + upload = _get_upload_with_read_access(upload_id, user, include_others=True) return UploadProcDataResponse( upload_id=upload_id, -- GitLab From 852779c0aafadb9b4bd961ad3813c94219772771 Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Fri, 23 Sep 2022 14:51:32 +0200 Subject: [PATCH 019/117] Fixed delete upload button error handling. --- gui/src/components/DataStore.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gui/src/components/DataStore.js b/gui/src/components/DataStore.js index bb7b344d0..e5cb5900f 100644 --- a/gui/src/components/DataStore.js +++ b/gui/src/components/DataStore.js @@ -172,6 +172,9 @@ const DataStore = React.memo(({children}) => { if (dataToUpdate.upload || dataToUpdate.entries) { dataToUpdate.error = undefined // Updating upload or entries -> reset error } + if (dataToUpdate?.upload?.current_process === 'delete_upload') { + newStoreObj.deletionRequested = true // Will treat subsequent 404 errors + } const viewers = newStoreObj.upload?.viewers const writers = newStoreObj.upload?.writers newStoreObj.isViewer = user && viewers?.includes(user.sub) @@ -217,12 +220,9 @@ const DataStore = React.memo(({children}) => { : api.get(`/uploads/${uploadId}`) apiCall.then(apiData => { - const upload = requireEntriesPage ? apiData.response?.upload : apiData.data - let dataToUpdate = requireEntriesPage - ? {error: undefined, isRefreshing: false, upload: upload, entries: apiData.response?.data, apiData, pagination: currentPagination, refreshOptions} - : {error: undefined, isRefreshing: false, upload: upload, entries: undefined, apiData: undefined, refreshOptions} - const deletionRequested = upload?.current_process === 'delete_upload' && (upload?.process_status === 'PENDING' || upload?.process_status === 'RUNNING') - if (deletionRequested) dataToUpdate = {...dataToUpdate, deletionRequested} + const dataToUpdate = requireEntriesPage + ? {error: undefined, isRefreshing: false, upload: apiData.response?.upload, entries: apiData.response?.data, apiData, pagination: currentPagination, refreshOptions} + : {error: undefined, isRefreshing: false, upload: apiData.data, entries: undefined, apiData: undefined, refreshOptions} updateUpload(installationUrl, uploadId, dataToUpdate) }).catch((error) => { if (requireEntriesPage && error.apiMessage === 'Page out of range requested.') { -- GitLab From 02f7da55ec8ae81c5830da1575a32664b81cd108 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 020/117] Resolve "Search customization" --- .gitlab-ci.yml | 2 + generate_gui_artifacts.sh | 3 +- gui/public/env.js | 37 +- gui/src/components/UserdataPage.js | 15 +- gui/src/components/UserdataPage.spec.js | 33 + gui/src/components/conftest.spec.js | 12 +- gui/src/components/dataset/DatasetPage.js | 48 +- gui/src/components/nav/Routes.js | 83 +- gui/src/components/search/FilterRegistry.js | 175 ++- gui/src/components/search/SearchContext.js | 102 +- .../search/{Search.js => SearchPage.js} | 8 +- gui/src/components/search/SearchPage.spec.js | 49 + .../components/search/SearchPageEntries.js | 60 - .../components/search/SearchPageMaterials.js | 66 - gui/src/components/search/SearchResults.js | 85 ++ gui/src/components/search/conftest.spec.js | 58 +- .../components/search/menus/FilterMainMenu.js | 154 +-- gui/src/components/search/menus/FilterMenu.js | 28 +- .../search/menus/FilterSubMenuAccess.js | 6 +- .../search/menus/FilterSubMenuArchive.js | 6 +- .../search/menus/FilterSubMenuAuthor.js | 8 +- .../search/menus/FilterSubMenuDFT.js | 8 +- .../search/menus/FilterSubMenuDataset.js | 8 +- .../search/menus/FilterSubMenuEELS.js | 8 +- .../search/menus/FilterSubMenuELN.js | 8 +- .../search/menus/FilterSubMenuElectronic.js | 8 +- .../search/menus/FilterSubMenuElements.js | 8 +- .../search/menus/FilterSubMenuGW.js | 8 +- .../FilterSubMenuGeometryOptimization.js | 8 +- .../search/menus/FilterSubMenuIDs.js | 8 +- .../search/menus/FilterSubMenuMaterial.js | 8 +- .../search/menus/FilterSubMenuMechanical.js | 8 +- .../search/menus/FilterSubMenuMethod.js | 8 +- .../search/menus/FilterSubMenuOptimade.js | 13 +- .../menus/FilterSubMenuOptoelectronic.js | 14 +- .../search/menus/FilterSubMenuQuantities.js | 8 +- .../search/menus/FilterSubMenuSimulation.js | 8 +- .../search/menus/FilterSubMenuSpectroscopy.js | 8 +- .../search/menus/FilterSubMenuSymmetry.js | 8 +- .../menus/FilterSubMenuThermodynamic.js | 8 +- .../search/menus/FilterSubMenuVibrational.js | 8 +- .../search/results/SearchResults.js | 50 - .../search/results/SearchResultsEntries.js | 140 --- .../search/results/SearchResultsMaterials.js | 83 -- gui/src/config.js | 7 +- gui/src/setupTests.js | 24 +- gui/tests/data/search/searchpage.json | 648 ++++++++++ gui/tests/data/search/userdatapage.json | 1088 +++++++++++++++++ nomad/cli/admin/admin.py | 18 +- nomad/cli/dev.py | 38 +- nomad/config.py | 193 ++- 51 files changed, 2628 insertions(+), 870 deletions(-) create mode 100644 gui/src/components/UserdataPage.spec.js rename gui/src/components/search/{Search.js => SearchPage.js} (96%) create mode 100644 gui/src/components/search/SearchPage.spec.js delete mode 100644 gui/src/components/search/SearchPageEntries.js delete mode 100644 gui/src/components/search/SearchPageMaterials.js create mode 100644 gui/src/components/search/SearchResults.js delete mode 100644 gui/src/components/search/results/SearchResults.js delete mode 100644 gui/src/components/search/results/SearchResultsEntries.js delete mode 100644 gui/src/components/search/results/SearchResultsMaterials.js create mode 100644 gui/tests/data/search/searchpage.json create mode 100644 gui/tests/data/search/userdatapage.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a6f901745..27959d665 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -76,6 +76,7 @@ gui linting: - git submodule sync - git submodule update --init -- gui/materia gui/crystcif-parse - docker run --rm $TEST_IMAGE python -m nomad.cli dev search-quantities > gui/src/searchQuantities.json + - docker run --rm $TEST_IMAGE python -m nomad.cli dev gui-config > gui/public/env.js - docker run --rm $TEST_IMAGE python -m nomad.cli dev units > gui/src/unitsData.js - docker run --rm $TEST_IMAGE python -m nomad.cli dev toolkit-metadata > gui/src/toolkitMetadata.json - docker run --rm $TEST_IMAGE python -m nomad.cli dev metainfo > gui/src/metainfo.json @@ -132,6 +133,7 @@ gui tests: - git submodule sync - git submodule update --init -- gui/materia gui/crystcif-parse - docker run --rm $TEST_IMAGE python -m nomad.cli dev search-quantities > gui/src/searchQuantities.json + - docker run --rm $TEST_IMAGE python -m nomad.cli dev gui-config > gui/public/env.js - docker run --rm $TEST_IMAGE python -m nomad.cli dev units > gui/src/unitsData.js - docker run --rm $TEST_IMAGE python -m nomad.cli dev toolkit-metadata > gui/src/toolkitMetadata.json - docker run --rm $TEST_IMAGE python -m nomad.cli dev example-upload-metadata > gui/src/exampleUploads.json diff --git a/generate_gui_artifacts.sh b/generate_gui_artifacts.sh index e6367cc7f..1d8a0d593 100755 --- a/generate_gui_artifacts.sh +++ b/generate_gui_artifacts.sh @@ -4,5 +4,6 @@ python -m nomad.cli dev search-quantities > gui/src/searchQuantities.json python -m nomad.cli dev toolkit-metadata > gui/src/toolkitMetadata.json python -m nomad.cli dev units > gui/src/unitsData.js python -m nomad.cli dev parser-metadata > gui/src/parserMetadata.json +python -m nomad.cli dev gui-config > gui/public/env.js cp dependencies/nomad-remote-tools-hub/tools.json gui/src/northTools.json -python -m nomad.cli dev example-upload-metadata > gui/src/exampleUploads.json \ No newline at end of file +python -m nomad.cli dev example-upload-metadata > gui/src/exampleUploads.json diff --git a/gui/public/env.js b/gui/public/env.js index 7ec89862f..58dbb75b0 100644 --- a/gui/public/env.js +++ b/gui/public/env.js @@ -1,24 +1,15 @@ window.nomadEnv = { - 'keycloakBase': 'https://nomad-lab.eu/fairdi/keycloak/auth/', - // Use the production API - // 'keycloakRealm': 'fairdi_nomad_prod', - // 'keycloakClientId': 'nomad_public', - // 'appBase': 'https://nomad-lab.eu/prod/v1', - // Use the local API - 'keycloakRealm': 'fairdi_nomad_test', - 'keycloakClientId': 'nomad_gui_dev', - 'appBase': 'http://localhost:8000/fairdi/nomad/latest', - 'encyclopediaBase': 'https://nomad-lab.eu/prod/rae/encyclopedia/#', - 'northBase': 'http://localhost:9000/fairdi/nomad/latest/north', - 'debug': false, - 'version': { - 'label': '1.1.4', - 'isBeta': false, - 'isTest': true, - 'usesBetaData': true, - 'officialUrl': 'https://nomad-lab.eu/prod/rae/gui' - }, - 'aitoolkitEnabled': false, - 'oasis': true, - 'servicesUploadLimit': 10 -} + 'appBase': 'http://localhost:8000/fairdi/nomad/latest', + 'northBase': 'http://localhost:9000/fairdi/nomad/latest/north', + 'keycloakBase': 'https://nomad-lab.eu/fairdi/keycloak/auth/', + 'keycloakRealm': 'fairdi_nomad_test', + 'keycloakClientId': 'nomad_public', + 'debug': false, + 'encyclopediaBase': 'https://nomad-lab.eu/prod/rae/encyclopedia/#', + 'aitoolkitEnabled': false, + 'oasis': false, + 'version': {}, + 'globalLoginRequired': false, + 'servicesUploadLimit': 10, + 'ui': {"search_contexts": {"include": ["entries"], "exclude": [], "options": {"entries": {"label": "Entries", "path": "entries", "resource": "entries", "breadcrumb": "Entries search", "description": "Search individual database entries", "help": {"title": "Entries search", "content": "\n This page allows you to **search entries** within NOMAD. Entries represent\n individual calculations or experiments that have bee uploaded into NOMAD.\n\n The search page consists of three main elements: the filter panel, the search\n bar, and the result list.\n\n The filter panel on the left allows you to graphically explore and enter\n different search filters. It also gives a visual indication of the currently\n active search filters for each category. This is a good place to start exploring\n the available search filters and their meaning.\n\n The search bar allows you to specify filters by typing them in and pressing\n enter. You can also start by simply typing keywords of interest, which will\n toggle a list of suggestions. For numerical data you can also use range queries,\n e.g. \\`0.0 < band_gap <= 0.1\\`.\n\n Notice that the units used in the filter panel and in the queries can be changed\n using the **units** button on the top right corner. When using the search bar,\n you can also specify a unit by typing the unit abbreviations, e.g. \\`band_gap >=\n 0.1 Ha\\`\n\n The result list on the right is automatically updated according to the filters\n you have specified. You can browse through the results by scrolling through the\n available items and loading more results as you go. Here you can also change the\n sorting of the results, modify the displayed columns, access individual entries\n or even download the raw data or the archive document by selecting individual\n entries and pressing the download button that appears. The ellipsis button shown\n for each entry will navigate you to that entry's page. This entry page will show\n more metadata, raw files, the entry's archive, and processing logs.\n "}, "pagination": {"order_by": "upload_create_time", "order": "desc"}, "columns": {"enable": ["entry_name", "results.material.chemical_formula_hill", "entry_type", "upload_create_time", "authors"], "include": ["entry_name", "results.material.chemical_formula_hill", "entry_type", "results.method.method_name", "results.method.simulation.program_name", "results.method.simulation.dft.basis_set_name", "results.method.simulation.dft.xc_functional_type", "results.material.structural_type", "results.material.symmetry.crystal_system", "results.material.symmetry.space_group_symbol", "results.material.symmetry.space_group_number", "results.eln.lab_ids", "results.eln.sections", "results.eln.methods", "results.eln.tags", "results.eln.instruments", "mainfile", "upload_create_time", "authors", "comment", "references", "datasets", "published"], "exclude": [], "options": {"entry_name": {"label": "Name", "align": "left"}, "results.material.chemical_formula_hill": {"label": "Formula", "align": "left"}, "entry_type": {"label": "Entry type", "align": "left"}, "results.method.method_name": {"label": "Method name"}, "results.method.simulation.program_name": {"label": "Program name"}, "results.method.simulation.dft.basis_set_name": {"label": "Basis set name"}, "results.method.simulation.dft.xc_functional_type": {"label": "XC functional type"}, "results.material.structural_type": {"label": "Structural type"}, "results.material.symmetry.crystal_system": {"label": "Crystal system"}, "results.material.symmetry.space_group_symbol": {"label": "Space group symbol"}, "results.material.symmetry.space_group_number": {"label": "Space group number"}, "results.eln.lab_ids": {"label": "Lab IDs"}, "results.eln.sections": {"label": "Sections"}, "results.eln.methods": {"label": "Methods"}, "results.eln.tags": {"label": "Tags"}, "results.eln.instruments": {"label": "Instruments"}, "mainfile": {"label": "Mainfile", "align": "left"}, "upload_create_time": {"label": "Upload time", "align": "left"}, "authors": {"label": "Authors", "align": "left"}, "comment": {"label": "Comment", "align": "left"}, "references": {"label": "References", "align": "left"}, "datasets": {"label": "Datasets", "align": "left"}, "published": {"label": "Access"}}}, "filter_menus": {"include": ["material", "elements", "symmetry", "method", "simulation", "dft", "gw", "experiment", "eels", "properties", "electronic", "optoelectronic", "vibrational", "mechanical", "spectroscopy", "thermodynamic", "geometry_optimization", "eln", "author", "dataset", "access", "ids", "processed_data_quantities", "optimade"], "exclude": [], "options": {"material": {"label": "Material", "level": 0, "size": "small"}, "elements": {"label": "Elements / Formula", "level": 1, "size": "large"}, "symmetry": {"label": "Symmetry", "level": 1, "size": "small"}, "method": {"label": "Method", "level": 0, "size": "small"}, "simulation": {"label": "Simulation", "level": 1, "size": "small"}, "dft": {"label": "DFT", "level": 2, "size": "small"}, "gw": {"label": "GW", "level": 2, "size": "small"}, "experiment": {"label": "Experiment", "level": 1, "size": "small"}, "eels": {"label": "EELS", "level": 2, "size": "small"}, "properties": {"label": "Properties", "level": 0, "size": "small"}, "electronic": {"label": "Electronic", "level": 1, "size": "small"}, "optoelectronic": {"label": "Optoelectronic", "level": 1, "size": "small"}, "vibrational": {"label": "Vibrational", "level": 1, "size": "small"}, "mechanical": {"label": "Mechanical", "level": 1, "size": "small"}, "spectroscopy": {"label": "Spectroscopy", "level": 1, "size": "small"}, "thermodynamic": {"label": "Thermodynamic", "level": 1, "size": "small"}, "geometry_optimization": {"label": "Geometry Optimization", "level": 1, "size": "small"}, "eln": {"label": "Electronic Lab Notebook", "level": 0, "size": "small"}, "author": {"label": "Author / Origin", "level": 0, "size": "medium"}, "dataset": {"label": "Dataset", "level": 0, "size": "small"}, "access": {"label": "Access", "level": 0, "size": "small"}, "ids": {"label": "IDs", "level": 0, "size": "small"}, "processed_data_quantities": {"label": "Processed Data Quantities", "level": 0, "size": "medium"}, "optimade": {"label": "Optimade", "level": 0, "size": "medium"}}}}}}} +}; diff --git a/gui/src/components/UserdataPage.js b/gui/src/components/UserdataPage.js index 8c0951867..b9fcce7a4 100644 --- a/gui/src/components/UserdataPage.js +++ b/gui/src/components/UserdataPage.js @@ -16,13 +16,14 @@ * limitations under the License. */ import React from 'react' +import { ui } from '../config' import { withLoginRequired } from './api' import { SearchContext } from './search/SearchContext' -import Search from './search/Search' +import SearchPage from './search/SearchPage' export const help = ` -This page allows you to **inspect** and **manage** you own data. It is similar to the -*search page*, but it will only show data that was uploaded by you or is shared with you. +This page allows you to **inspect** and **manage** your own data. It is similar to the +*entry search page*, but it will only show data that was uploaded by you or is shared with you. Besides giving you a filter for your data, this page also allows you to edit the *user metadata* on all entries. User metadata is assigned to individual entries, but of course you can @@ -70,15 +71,19 @@ DOI, they will be redirected to a NOMAD view that shows the dataset and allows i Once you assigned a DOI to a dataset, no entries can be removed or added to the dataset. ` +const context = ui?.search_contexts?.options?.entries const filtersLocked = { 'visibility': 'user' } const UserdataPage = React.memo(() => { return - + }) diff --git a/gui/src/components/UserdataPage.spec.js b/gui/src/components/UserdataPage.spec.js new file mode 100644 index 000000000..7174a9e79 --- /dev/null +++ b/gui/src/components/UserdataPage.spec.js @@ -0,0 +1,33 @@ +/* + * Copyright The NOMAD Authors. + * + * This file is part of NOMAD. See https://nomad-lab.eu for further info. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react' +import { render, startAPI, closeAPI } from './conftest.spec' +import { expectFilterMainMenu, expectSearchResults } from './search/conftest.spec' +import { ui } from '../config' +import UserDatapage from './UserdataPage' + +test('renders user data search page correctly', async () => { + const context = ui.search_contexts.options.entries + await startAPI('tests.states.search.search', 'tests/data/search/userdatapage', 'test', 'password') + render() + + await expectFilterMainMenu(context) + await expectSearchResults(context) + closeAPI() +}) diff --git a/gui/src/components/conftest.spec.js b/gui/src/components/conftest.spec.js index fcd45b924..236e94d8b 100644 --- a/gui/src/components/conftest.spec.js +++ b/gui/src/components/conftest.spec.js @@ -47,7 +47,7 @@ import { GlobalMetainfo } from './archive/metainfo' beforeEach(async () => { // For some strange reason, the useKeycloak mock gets reset if we set it earlier - if (!useKeycloak()) { + if (!useKeycloak()?.keycloak) { useKeycloak.mockReturnValue( { keycloak: { @@ -346,7 +346,7 @@ if (!fs.existsSync(`../${configPath}`)) { * @param {string} password Password for the username. */ export async function startAPI(state, path, username = '', password = '') { - mockKeycloak(username, password) + await mockKeycloak(username, password) // Prepare API state for reading responses directly from it. const jsonPath = `${path}.json` @@ -451,12 +451,12 @@ ${func}()"`) * realm is limited. Inspired by: * https://stackoverflow.com/questions/63627652/testing-pages-secured-by-react-keycloak */ -function mockKeycloak(username, password) { - const login = (username, password) => { +async function mockKeycloak(username, password) { + const login = async (username, password) => { if ((username === undefined || username === '') && (password === undefined || password === '')) return const response = getRefreshToken(username, password) const authenticated = response.access_token !== undefined - if (authenticated) updateToken(response.refresh_token) + if (authenticated) await updateToken(response.refresh_token) } const logout = () => { @@ -564,7 +564,7 @@ function mockKeycloak(username, password) { } if (username && password) { - login(username, password) + await login(username, password) } useKeycloak.mockReturnValue({keycloak: mockedKeycloak, initialized: true}) diff --git a/gui/src/components/dataset/DatasetPage.js b/gui/src/components/dataset/DatasetPage.js index c0dea5374..7efe310f5 100644 --- a/gui/src/components/dataset/DatasetPage.js +++ b/gui/src/components/dataset/DatasetPage.js @@ -18,9 +18,10 @@ import React, { useContext, useState, useEffect, useMemo } from 'react' import PropTypes from 'prop-types' import { Typography, makeStyles } from '@material-ui/core' +import { ui } from '../../config' import { errorContext } from '../errors' import { useApi } from '../api' -import Search from '../search/Search' +import SearchPage from '../search/SearchPage' import { SearchContext } from '../search/SearchContext' import { DOI } from './DOI' @@ -29,13 +30,14 @@ This page allows you to **inspect** and **download** NOMAD datasets. It also all to explore a dataset with similar controls that the search page offers. ` +const context = ui?.search_contexts?.options?.entries const useStyles = makeStyles(theme => ({ header: { display: 'flex', flexDirection: 'column' } })) -const UserdataPage = React.memo(({match}) => { +const DatasetPage = React.memo(({match}) => { const styles = useStyles() const [dataset, setDataset] = useState() const {raiseError} = useContext(errorContext) @@ -55,25 +57,31 @@ const UserdataPage = React.memo(({match}) => { }) }, [datasetId, api, raiseError]) - // Shows basic dataset information above the searchbar - return dataset && - - - {dataset.dataset_name || (dataset.isEmpty && 'Empty or non existing dataset') || 'loading ...'} - - - dataset{dataset.doi ? , with DOI : ''} - - } - /> - + // Show a customized search page for this dataset. Basic dataset information + // shown in the header. + return dataset + ? + + + {dataset.dataset_name || (dataset.isEmpty && 'Empty or non existing dataset') || 'loading ...'} + + + dataset{dataset.doi ? , with DOI : ''} + + } + /> + + : null }) -UserdataPage.propTypes = { +DatasetPage.propTypes = { match: PropTypes.object } -export default UserdataPage +export default DatasetPage diff --git a/gui/src/components/nav/Routes.js b/gui/src/components/nav/Routes.js index 4c833f37e..1aa193c62 100644 --- a/gui/src/components/nav/Routes.js +++ b/gui/src/components/nav/Routes.js @@ -32,10 +32,10 @@ import EntryPage, { help as entryHelp } from '../entry/EntryPage' import UploadsPage, { help as uploadsHelp } from '../uploads/UploadsPage' import UserdataPage, { help as userdataHelp } from '../UserdataPage' import APIs from '../APIs' -import SearchPageEntries, {help as searchEntriesHelp} from '../search/SearchPageEntries' +import SearchPage from '../search/SearchPage' +import { SearchContext } from '../search/SearchContext' import NorthPage, {help as NORTHHelp} from '../north/NorthPage' -// import SearchPageMaterials, {help as searchMaterialsHelp} from '../search/SearchPageMaterials' -import { aitoolkitEnabled, appBase, oasis, encyclopediaBase } from '../../config' +import { aitoolkitEnabled, appBase, oasis, encyclopediaBase, ui } from '../../config' import EntryQuery from '../entry/EntryQuery' import ResolvePID from '../entry/ResolvePID' import DatasetPage, { help as datasetHelp } from '../dataset/DatasetPage' @@ -181,6 +181,48 @@ const toolkitRoute = (!oasis && aitoolkitEnabled) tooltip: 'Visit the NOMAD Artificial Intelligence Analytics Toolkit' } +const searchRoutes = ui?.search_contexts?.include + ? ui.search_contexts.include + .filter(key => !ui?.search_contexts?.exclude?.includes(key)) + .map(key => { + const context = ui.search_contexts.options[key] + const routeMap = { + entries: entryRoutes + } + return { + path: context.path, + exact: true, + cache: 'always', + menu: context.label, + tooltip: context.description, + breadcrumb: context.breadcrumb, + render: (props) => ( + + + + ), + help: { + title: context.help?.title, + content: context.help?.content + }, + routes: routeMap[context.resource] + } + }) + : [] +if (encyclopediaBase) { + searchRoutes.push({ + menu: 'Material Encyclopedia', + href: `${encyclopediaBase}/search`, + tooltip: 'Search materials in the NOMAD Encyclopedia' + }) +} + /** * The list with all routes. This is used to determine the routes for routing, the breadcrumbs, * and the main menu. @@ -238,40 +280,7 @@ export const routes = [ path: 'search', redirect: '/search/entries', menu: 'Explore', - routes: [ - { - path: 'entries', - exact: true, - cache: 'always', - component: SearchPageEntries, - menu: 'Entries', - tooltip: 'Search individual database entries', - breadcrumb: 'Entries search', - help: { - title: 'Searching for entries', - content: searchEntriesHelp - }, - routes: entryRoutes - }, - { - menu: 'Material Encyclopedia', - href: 'https://nomad-lab.eu/prod/rae/encyclopedia', - tooltip: 'Search materials in the NOMAD Encyclopedia' - } - // { - // path: 'materials', - // exact: true, - // cache: 'always', - // component: SearchPageMaterials, - // menu: 'Material Encyclopedia', - // tooltip: 'Search materials', - // breadcrumb: 'Materials search', - // help: { - // title: 'Searching for materials', - // content: searchMaterialsHelp - // } - // } - ] + routes: searchRoutes }, { path: 'analyze', diff --git a/gui/src/components/search/FilterRegistry.js b/gui/src/components/search/FilterRegistry.js index 3b182aeae..8010094e2 100644 --- a/gui/src/components/search/FilterRegistry.js +++ b/gui/src/components/search/FilterRegistry.js @@ -32,31 +32,30 @@ export const filterAbbreviations = [] // Mapping of filter full name -> abbrevia export const filterFullnames = [] // Mapping of filter abbreviation -> full name export const filterData = {} // Stores data for each registered filter -// Labels for the filter menus -export const labelMaterial = 'Material' -export const labelElements = 'Elements / Formula' -export const labelSymmetry = 'Symmetry' -export const labelMethod = 'Method' -export const labelSimulation = 'Simulation' -export const labelDFT = 'DFT' -export const labelGW = 'GW' -export const labelExperiment = 'Experiment' -export const labelEELS = 'EELS' -export const labelProperties = 'Properties' -export const labelElectronic = 'Electronic' -export const labelOptoelectronic = 'Optoelectronic' -export const labelVibrational = 'Vibrational' -export const labelMechanical = 'Mechanical' -export const labelSpectroscopy = 'Spectroscopy' -export const labelThermodynamic = 'Thermodynamic' -export const labelGeometryOptimization = 'Geometry optimization' -export const labelELN = 'Electronic Lab Notebook' -export const labelAuthor = 'Author / Origin' -export const labelAccess = 'Access' -export const labelDataset = 'Dataset' -export const labelIDs = 'IDs' -export const labelArchive = 'Processed data quantities' -export const labelOptimade = 'Optimade' +// Ids for the filter menus: used to tie filter chips to a specific menu. +const idMaterial = 'material' +const idElements = 'elements' +const idSymmetry = 'symmetry' +const idMethod = 'method' +const idSimulation = 'simulation' +const idDFT = 'dft' +const idGW = 'gw' +const idProperties = 'properties' +const idElectronic = 'electronic' +const idOptoelectronic = 'optoelectronic' +const idVibrational = 'vibrational' +const idMechanical = 'mechanical' +const idSpectroscopy = 'spectroscopy' +const idThermodynamic = 'thermodynamic' +const idGeometryOptimization = 'geometry_optimization' +const idELN = 'eln' +const idAuthor = 'author' +const idDataset = 'dataset' +const idAccess = 'access' +const idIDs = 'ids' +const idArchive = 'processed_data_quantities' +const idOptimade = 'optimade' + /** * Used to gather a list of fixed filter options from the metainfo. * @param {string} quantity Metainfo name @@ -291,52 +290,52 @@ const noQueryQuantity = {guiOnly: true, multiple: false} const numberHistogramQuantity = {multiple: false, exclusive: false} // Filters that directly correspond to a metainfo value -registerFilter('results.material.structural_type', labelMaterial, {...termQuantity, scale: '1/4'}) -registerFilter('results.material.functional_type', labelMaterial, termQuantityNonExclusive) -registerFilter('results.material.compound_type', labelMaterial, termQuantityNonExclusive) -registerFilter('results.material.material_name', labelMaterial, termQuantity) -registerFilter('results.material.chemical_formula_hill', labelElements, termQuantity) -registerFilter('results.material.chemical_formula_anonymous', labelElements, termQuantity) -registerFilter('results.material.n_elements', labelElements, {...numberHistogramQuantity, label: 'Number of Elements'}) -registerFilter('results.material.symmetry.bravais_lattice', labelSymmetry, termQuantity) -registerFilter('results.material.symmetry.crystal_system', labelSymmetry, termQuantity) -registerFilter('results.material.symmetry.structure_name', labelSymmetry, termQuantity) -registerFilter('results.material.symmetry.strukturbericht_designation', labelSymmetry, termQuantity) -registerFilter('results.material.symmetry.space_group_symbol', labelSymmetry, termQuantity) -registerFilter('results.material.symmetry.point_group', labelSymmetry, termQuantity) -registerFilter('results.material.symmetry.hall_symbol', labelSymmetry, termQuantity) -registerFilter('results.material.symmetry.prototype_aflow_id', labelSymmetry, termQuantity) -registerFilter('results.method.method_name', labelMethod, {...termQuantity, scale: '1/4'}) -registerFilter('results.method.workflow_name', labelMethod, {...termQuantity, scale: '1/4'}) -registerFilter('results.method.simulation.program_name', labelSimulation, {...termQuantity, scale: '1/4'}) -registerFilter('results.method.simulation.program_version', labelSimulation, termQuantity) -registerFilter('results.method.simulation.dft.basis_set_type', labelDFT, {...termQuantity, scale: '1/4'}) -registerFilter('results.method.simulation.dft.core_electron_treatment', labelDFT, termQuantity) -registerFilter('results.method.simulation.dft.xc_functional_type', labelDFT, {...termQuantity, scale: '1/2', label: 'XC Functional Type'}) -registerFilter('results.method.simulation.dft.xc_functional_names', labelDFT, {...termQuantityNonExclusive, scale: '1/2', label: 'XC Functional Names'}) -registerFilter('results.method.simulation.dft.relativity_method', labelDFT, termQuantity) -registerFilter('results.method.simulation.gw.type', labelGW, {...termQuantity, label: 'GW Type'}) -registerFilter('results.eln.sections', labelELN, termQuantity) -registerFilter('results.eln.tags', labelELN, termQuantity) -registerFilter('results.eln.methods', labelELN, termQuantity) -registerFilter('results.eln.instruments', labelELN, termQuantity) -registerFilter('results.eln.lab_ids', labelELN, termQuantity) -registerFilter('results.eln.names', labelELN, termQuantity) -registerFilter('results.eln.descriptions', labelELN, termQuantity) -registerFilter('external_db', labelAuthor, {...termQuantity, label: 'External Database', scale: '1/4'}) -registerFilter('authors.name', labelAuthor, {...termQuantityNonExclusive, label: 'Author Name'}) -registerFilter('upload_create_time', labelAuthor, {...numberHistogramQuantity, scale: '1/2'}) -registerFilter('datasets.dataset_name', labelDataset, {...termQuantityLarge, label: 'Dataset Name'}) -registerFilter('datasets.doi', labelDataset, {...termQuantity, label: 'Dataset DOI'}) -registerFilter('entry_id', labelIDs, termQuantity) -registerFilter('upload_id', labelIDs, termQuantity) -registerFilter('quantities', labelArchive, {...noAggQuantity, label: 'Metainfo definition', queryMode: 'all'}) -registerFilter('results.material.material_id', labelIDs, termQuantity) -registerFilter('datasets.dataset_id', labelIDs, termQuantity) -registerFilter('optimade_filter', labelOptimade, {multiple: true, queryMode: 'all'}) +registerFilter('results.material.structural_type', idMaterial, {...termQuantity, scale: '1/4'}) +registerFilter('results.material.functional_type', idMaterial, termQuantityNonExclusive) +registerFilter('results.material.compound_type', idMaterial, termQuantityNonExclusive) +registerFilter('results.material.material_name', idMaterial, termQuantity) +registerFilter('results.material.chemical_formula_hill', idElements, termQuantity) +registerFilter('results.material.chemical_formula_anonymous', idElements, termQuantity) +registerFilter('results.material.n_elements', idElements, {...numberHistogramQuantity, label: 'Number of Elements'}) +registerFilter('results.material.symmetry.bravais_lattice', idSymmetry, termQuantity) +registerFilter('results.material.symmetry.crystal_system', idSymmetry, termQuantity) +registerFilter('results.material.symmetry.structure_name', idSymmetry, termQuantity) +registerFilter('results.material.symmetry.strukturbericht_designation', idSymmetry, termQuantity) +registerFilter('results.material.symmetry.space_group_symbol', idSymmetry, termQuantity) +registerFilter('results.material.symmetry.point_group', idSymmetry, termQuantity) +registerFilter('results.material.symmetry.hall_symbol', idSymmetry, termQuantity) +registerFilter('results.material.symmetry.prototype_aflow_id', idSymmetry, termQuantity) +registerFilter('results.method.method_name', idMethod, {...termQuantity, scale: '1/4'}) +registerFilter('results.method.workflow_name', idMethod, {...termQuantity, scale: '1/4'}) +registerFilter('results.method.simulation.program_name', idSimulation, {...termQuantity, scale: '1/4'}) +registerFilter('results.method.simulation.program_version', idSimulation, termQuantity) +registerFilter('results.method.simulation.dft.basis_set_type', idDFT, {...termQuantity, scale: '1/4'}) +registerFilter('results.method.simulation.dft.core_electron_treatment', idDFT, termQuantity) +registerFilter('results.method.simulation.dft.xc_functional_type', idDFT, {...termQuantity, scale: '1/2', label: 'XC Functional Type'}) +registerFilter('results.method.simulation.dft.xc_functional_names', idDFT, {...termQuantityNonExclusive, scale: '1/2', label: 'XC Functional Names'}) +registerFilter('results.method.simulation.dft.relativity_method', idDFT, termQuantity) +registerFilter('results.method.simulation.gw.type', idGW, {...termQuantity, label: 'GW Type'}) +registerFilter('results.eln.sections', idELN, termQuantity) +registerFilter('results.eln.tags', idELN, termQuantity) +registerFilter('results.eln.methods', idELN, termQuantity) +registerFilter('results.eln.instruments', idELN, termQuantity) +registerFilter('results.eln.lab_ids', idELN, termQuantity) +registerFilter('results.eln.names', idELN, termQuantity) +registerFilter('results.eln.descriptions', idELN, termQuantity) +registerFilter('external_db', idAuthor, {...termQuantity, label: 'External Database', scale: '1/4'}) +registerFilter('authors.name', idAuthor, {...termQuantityNonExclusive, label: 'Author Name'}) +registerFilter('upload_create_time', idAuthor, {...numberHistogramQuantity, scale: '1/2'}) +registerFilter('datasets.dataset_name', idDataset, {...termQuantityLarge, label: 'Dataset Name'}) +registerFilter('datasets.doi', idDataset, {...termQuantity, label: 'Dataset DOI'}) +registerFilter('entry_id', idIDs, termQuantity) +registerFilter('upload_id', idIDs, termQuantity) +registerFilter('quantities', idArchive, {...noAggQuantity, label: 'Metainfo definition', queryMode: 'all'}) +registerFilter('results.material.material_id', idIDs, termQuantity) +registerFilter('datasets.dataset_id', idIDs, termQuantity) +registerFilter('optimade_filter', idOptimade, {multiple: true, queryMode: 'all'}) registerFilter( 'results.properties.spectroscopy.eels', - labelSpectroscopy, + idSpectroscopy, {...nestedQuantity, label: 'Electron Energy Loss Spectrum (EELS)'}, [ {name: 'detector_type', ...termQuantity}, @@ -347,7 +346,7 @@ registerFilter( ) registerFilter( 'results.properties.electronic.band_structure_electronic', - labelElectronic, + idElectronic, {...nestedQuantity, label: 'Band Structure'}, [ {name: 'spin_polarized', label: 'Spin-polarized', ...termQuantityBool} @@ -355,7 +354,7 @@ registerFilter( ) registerFilter( 'results.properties.electronic.dos_electronic', - labelElectronic, + idElectronic, {...nestedQuantity, label: 'Density of States (DOS)'}, [ {name: 'spin_polarized', label: 'Spin-polarized', ...termQuantityBool} @@ -363,7 +362,7 @@ registerFilter( ) registerFilter( 'results.properties.electronic.band_structure_electronic.band_gap', - labelElectronic, + idElectronic, nestedQuantity, [ {name: 'type', ...termQuantity}, @@ -372,7 +371,7 @@ registerFilter( ) registerFilter( 'results.properties.optoelectronic.band_gap', - labelOptoelectronic, + idOptoelectronic, nestedQuantity, [ {name: 'type', ...termQuantity}, @@ -381,7 +380,7 @@ registerFilter( ) registerFilter( 'results.properties.optoelectronic.solar_cell', - labelOptoelectronic, + idOptoelectronic, nestedQuantity, [ {name: 'efficiency', ...numberHistogramQuantity, scale: '1/4'}, @@ -402,7 +401,7 @@ registerFilter( ) registerFilter( 'results.properties.mechanical.bulk_modulus', - labelMechanical, + idMechanical, nestedQuantity, [ {name: 'type', ...termQuantity}, @@ -411,7 +410,7 @@ registerFilter( ) registerFilter( 'results.properties.mechanical.shear_modulus', - labelMechanical, + idMechanical, nestedQuantity, [ {name: 'type', ...termQuantity}, @@ -420,12 +419,12 @@ registerFilter( ) registerFilter( 'results.properties.available_properties', - labelProperties, + idProperties, termQuantityAll ) registerFilter( 'results.properties.mechanical.energy_volume_curve', - labelMechanical, + idMechanical, nestedQuantity, [ {name: 'type', ...termQuantity} @@ -433,7 +432,7 @@ registerFilter( ) registerFilter( 'results.properties.geometry_optimization', - labelGeometryOptimization, + idGeometryOptimization, nestedQuantity, [ {name: 'final_energy_difference', ...numberHistogramQuantity, scale: '1/8'}, @@ -443,7 +442,7 @@ registerFilter( ) registerFilter( 'results.properties.thermodynamic.trajectory', - labelThermodynamic, + idThermodynamic, nestedQuantity, [ {name: 'available_properties', ...termQuantityAll}, @@ -454,7 +453,7 @@ registerFilter( // Visibility: controls the 'owner'-parameter in the API query, not part of the // query itself. -registerFilter('visibility', labelAccess, {...noQueryQuantity, default: 'visible'}) +registerFilter('visibility', idAccess, {...noQueryQuantity, default: 'visible'}) // Combine: controls whether materials search combines data from several // entries. @@ -471,7 +470,7 @@ registerFilter('exclusive', undefined, {...noQueryQuantity, default: false}) // into a single string. registerFilter( 'results.material.elements', - labelElements, + idElements, { stats: ptStatConfig, aggs: {terms: {size: elementData.elements.length}}, @@ -495,7 +494,7 @@ registerFilter( // Electronic properties: subset of results.properties.available_properties registerFilterOptions( 'electronic_properties', - labelElectronic, + idElectronic, 'results.properties.available_properties', 'Electronic Properties', 'The electronic properties that are present in an entry.', @@ -509,7 +508,7 @@ registerFilterOptions( // Optoelectronic properties: subset of results.properties.available_properties registerFilterOptions( 'optoelectronic_properties', - labelOptoelectronic, + idOptoelectronic, 'results.properties.available_properties', 'Optoelectronic properties', 'The optoelectronic properties that are present in an entry.', @@ -522,7 +521,7 @@ registerFilterOptions( // Vibrational properties: subset of results.properties.available_properties registerFilterOptions( 'vibrational_properties', - labelVibrational, + idVibrational, 'results.properties.available_properties', 'Vibrational Properties', 'The vibrational properties that are present in an entry.', @@ -537,7 +536,7 @@ registerFilterOptions( // Mechanical properties: subset of results.properties.available_properties registerFilterOptions( 'mechanical_properties', - labelMechanical, + idMechanical, 'results.properties.available_properties', 'Mechanical Properties', 'The mechanical properties that are present in an entry.', @@ -551,7 +550,7 @@ registerFilterOptions( // Spectroscopic properties: subset of results.properties.available_properties registerFilterOptions( 'spectroscopic_properties', - labelSpectroscopy, + idSpectroscopy, 'results.properties.available_properties', 'Spectroscopic Properties', 'The spectroscopic properties that are present in an entry.', @@ -563,7 +562,7 @@ registerFilterOptions( // Thermodynamical properties: subset of results.properties.available_properties registerFilterOptions( 'thermodynamic_properties', - labelThermodynamic, + idThermodynamic, 'results.properties.available_properties', 'Thermodynamic Properties', 'The thermodynamic properties that are present.', diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 4bbde470b..40d3bf1e5 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -37,16 +37,19 @@ import { isFunction, size, isEqual, - merge + merge, + cloneDeep } from 'lodash' +import { Link } from '@material-ui/core' import qs from 'qs' import PropTypes from 'prop-types' import { useHistory } from 'react-router-dom' import { useApi } from '../api' -import { setToArray } from '../../utils' +import { setToArray, authorList, formatTimestamp } from '../../utils' import { Quantity } from '../../units' import { useErrors } from '../errors' -import { combinePagination } from '../datatable/Datatable' +import { combinePagination, addColumnDefaults } from '../datatable/Datatable' +import { Published } from '../entry/EntryDetails' import { inputSectionContext } from './input/InputSection' import { filterData as filterDataGlobal, @@ -75,10 +78,6 @@ import { * depending on the context would re-render for each filter change regardless if * it actually changes the state of that component or not. */ -const orderByMap = { - 'entries': {order_by: 'upload_create_time', order: 'desc'}, - 'materials': {order_by: 'chemical_formula_hill', order: 'asc'} -} let indexContext = 0 let indexFilters = 0 let indexLocked = 0 @@ -103,6 +102,8 @@ export const searchContext = React.createContext() export const SearchContext = React.memo(({ resource, filtersLocked, + initialColumns, + initialFilterMenus, initialPagination, initialStatistics, children @@ -119,6 +120,79 @@ export const SearchContext = React.memo(({ const firstLoad = useRef(true) const disableUpdate = useRef(false) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() @@ -285,10 +359,7 @@ export const SearchContext = React.memo(({ const paginationState = atom({ key: `pagination_${indexContext}`, - default: initialPagination || { - page_size: 20, - ...orderByMap[resource] - } + default: initialPagination }) const resultsUsedState = atom({ @@ -742,7 +813,7 @@ export const SearchContext = React.memo(({ useAgg, useSetFilters ] - }, [initialQuery, filters, filtersLocked, finalStatistics, initialAggs, initialPagination, resource, filterData]) + }, [initialQuery, filters, filtersLocked, finalStatistics, initialAggs, initialPagination, filterData]) const setResults = useSetRecoilState(resultsState) const setApiData = useSetRecoilState(apiDataState) @@ -1001,6 +1072,8 @@ export const SearchContext = React.memo(({ return { resource, + columns, + filterMenus, useIsMenuOpen: () => useRecoilValue(isMenuOpenState), useSetIsMenuOpen: () => useSetRecoilState(isMenuOpenState), useIsCollapsed: () => useRecoilValue(isCollapsedState), @@ -1033,6 +1106,8 @@ export const SearchContext = React.memo(({ } }, [ resource, + columns, + filterMenus, useFilterValue, useSetFilter, useFilterState, @@ -1068,9 +1143,12 @@ export const SearchContext = React.memo(({ {children} }) + SearchContext.propTypes = { resource: PropTypes.string, filtersLocked: PropTypes.object, + initialColumns: PropTypes.object, + initialFilterMenus: PropTypes.object, initialPagination: PropTypes.object, initialStatistics: PropTypes.object, children: PropTypes.node diff --git a/gui/src/components/search/Search.js b/gui/src/components/search/SearchPage.js similarity index 96% rename from gui/src/components/search/Search.js rename to gui/src/components/search/SearchPage.js index 33101614f..dc8902483 100644 --- a/gui/src/components/search/Search.js +++ b/gui/src/components/search/SearchPage.js @@ -23,8 +23,8 @@ import { makeStyles } from '@material-ui/core/styles' import FilterMainMenu from './menus/FilterMainMenu' import { collapsedMenuWidth } from './menus/FilterMenu' import SearchBar from './SearchBar' +import SearchResults from './SearchResults' import StatisticsGrid from './statistics/StatisticsGrid' -import SearchResults from './results/SearchResults' import { useSearchContext } from './SearchContext' /** @@ -80,7 +80,7 @@ const useStyles = makeStyles(theme => { } }) -const Search = React.memo(({ +const SearchPage = React.memo(({ header }) => { const styles = useStyles() @@ -121,8 +121,8 @@ const Search = React.memo(({ }) -Search.propTypes = { +SearchPage.propTypes = { header: PropTypes.node } -export default Search +export default SearchPage diff --git a/gui/src/components/search/SearchPage.spec.js b/gui/src/components/search/SearchPage.spec.js new file mode 100644 index 000000000..eeed7d974 --- /dev/null +++ b/gui/src/components/search/SearchPage.spec.js @@ -0,0 +1,49 @@ +/* + * Copyright The NOMAD Authors. + * + * This file is part of NOMAD. See https://nomad-lab.eu for further info. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react' +import { render, startAPI, closeAPI } from '../conftest.spec' +import { expectFilterMainMenu, expectSearchResults } from './conftest.spec' +import { ui } from '../../config' +import { SearchContext } from './SearchContext' +import SearchPage from './SearchPage' + +describe('', () => { + beforeAll(async () => { + await startAPI('tests.states.search.search', 'tests/data/search/searchpage') + }) + afterAll(() => closeAPI()) + + test.each( + Object.entries(ui.search_contexts.options) + )('renders search page correctly, context: %s', async (key, context) => { + render( + + + + ) + + await expectFilterMainMenu(context) + await expectSearchResults(context) + }) +}) diff --git a/gui/src/components/search/SearchPageEntries.js b/gui/src/components/search/SearchPageEntries.js deleted file mode 100644 index 972161860..000000000 --- a/gui/src/components/search/SearchPageEntries.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright The NOMAD Authors. - * - * This file is part of NOMAD. See https://nomad-lab.eu for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react' -import Search from './Search' -import { SearchContext } from './SearchContext' - -export const help = ` -This page allows you to **search entries** within NOMAD. Entries represent -individual calculations or experiments that have bee uploaded into NOMAD. - -The search page consists of three main elements: the filter panel, the search -bar, and the result list. - -The filter panel on the left allows you to graphically explore and enter -different search filters. It also gives a visual indication of the currently -active search filters for each category. This is a good place to start exploring -the available search filters and their meaning. - -The search bar allows you to specify filters by typing them in and pressing -enter. You can also start by simply typing keywords of interest, which will -toggle a list of suggestions. For numerical data you can also use range queries, -e.g. \`0.0 < band_gap <= 0.1\`. - -Notice that the units used in the filter panel and in the queries can be changed -using the **units** button on the top right corner. When using the search bar, -you can also specify a unit by typing the unit abbreviations, e.g. \`band_gap >= -0.1 Ha\` - -The result list on the right is automatically updated according to the filters -you have specified. You can browse through the results by scrolling through the -available items and loading more results as you go. Here you can also change the -sorting of the results, modify the displayed columns, access individual entries -or even download the raw data or the archive document by selecting individual -entries and pressing the download button that appears. The ellipsis button shown -for each entry will navigate you to that entry's page. This entry page will show -more metadata, raw files, the entry's archive, and processing logs. -` - -const SearchPageEntries = React.memo(() => { - return - - -}) - -export default SearchPageEntries diff --git a/gui/src/components/search/SearchPageMaterials.js b/gui/src/components/search/SearchPageMaterials.js deleted file mode 100644 index cb97d750e..000000000 --- a/gui/src/components/search/SearchPageMaterials.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright The NOMAD Authors. - * - * This file is part of NOMAD. See https://nomad-lab.eu for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react' -import Search from './Search' -import { SearchContext } from './SearchContext' - -export const help = ` -This page allows you to **search materials** within NOMAD. NOMAD can -automatically detect the material from individual entries and can then group the -data by using these detected materials. This allows you to search individual -materials which have properties that are aggregated from several entries. - -The search page consists of three main elements: the filter panel, the search -bar, and the result list. - -The filter panel on the left allows you to graphically explore and enter -different search filters. It also gives a visual indication of the currently -active search filters for each category. This is a good place to start exploring -the available search filters and their meaning. - -The search bar allows you to specify filters by typing them in and pressing -enter. You can also start by simply typing keywords of interest, which will -toggle a list of suggestions. For numerical data you can also use range queries, -e.g. \`0.0 < band_gap <= 0.1\`. - -The units used in the filter panel and in the queries can be changed -using the **units** button on the top right corner. When using the search bar, -you can also specify a unit by typing the unit abbreviations, e.g. \`band_gap >= -0.1 Ha\`. - -Notice that by default the properties that you search can be combined from -several different entries. If instead you wish to search for a material with an -individual entry fullfilling your search criteria, uncheck the **combine results -from several entries**-checkbox. - -The result list on the right is automatically updated according to the filters -you have specified. You can scroll through the available items and load more -results as you go. Here you can also change the sorting of the results, modify -the displayed columns and access individual materials. The ellipsis button shown -for each material will navigate you into the material overview page within the -NOMAD Encyclopedia. This page will show a more detailed overview for that -specific material. -` - -const SearchPageMaterials = React.memo(() => { - return - - -}) - -export default SearchPageMaterials diff --git a/gui/src/components/search/SearchResults.js b/gui/src/components/search/SearchResults.js new file mode 100644 index 000000000..e853d7fac --- /dev/null +++ b/gui/src/components/search/SearchResults.js @@ -0,0 +1,85 @@ +/* + * Copyright The NOMAD Authors. + * + * This file is part of NOMAD. See https://nomad-lab.eu for further info. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState, useMemo } from 'react' +import { Paper, Typography } from '@material-ui/core' +import { Alert } from '@material-ui/lab' +import { + Datatable, + DatatableLoadMorePagination, + DatatableTable, + DatatableToolbar, + DatatableToolbarActions +} from '../datatable/Datatable' +import EntryDownloadButton from '../entry/EntryDownloadButton' +import EntryDetails, { EntryRowActions } from '../entry/EntryDetails' +import { pluralize, formatInteger } from '../../utils' +import { useSearchContext } from './SearchContext' + +/** + * Displays the list of search results. + */ +const SearchResults = React.memo(function SearchResults() { + const {columns, useResults, useQuery} = useSearchContext() + const {data, pagination, setPagination} = useResults() + const searchQuery = useQuery() + const [selected, setSelected] = useState([]) + + const query = useMemo(() => { + if (selected === 'all') { + return searchQuery + } + return {entry_id: selected.map(data => data.entry_id)} + }, [selected, searchQuery]) + + if (!columns) { + return + No search columns defined within this search context. Ensure that all GUI artifacts are created. + + } + + if (pagination.total === 0) { + return no results + } + + if (!pagination.total) { + return searching ... + } + + return + + + + + + + + load more + + + +}) + +export default SearchResults diff --git a/gui/src/components/search/conftest.spec.js b/gui/src/components/search/conftest.spec.js index cba7c14e0..55a1256da 100644 --- a/gui/src/components/search/conftest.spec.js +++ b/gui/src/components/search/conftest.spec.js @@ -21,7 +21,7 @@ import PropTypes from 'prop-types' import assert from 'assert' import { waitFor } from '@testing-library/dom' import elementData from '../../elementData.json' -import { screen, WrapperDefault, WrapperNoAPI } from '../conftest.spec' +import { screen, WrapperDefault } from '../conftest.spec' import { render } from '@testing-library/react' import { SearchContext } from './SearchContext' import { filterData } from './FilterRegistry' @@ -31,7 +31,7 @@ import { DType } from '../../utils' /*****************************************************************************/ // Renders /** - * Entry search render. + * Render within a search context. */ const WrapperSearch = ({children}) => { return @@ -48,24 +48,6 @@ WrapperSearch.propTypes = { export const renderSearchEntry = (ui, options) => render(ui, {wrapper: WrapperSearch, ...options}) -/** - * Entry search render without API. - */ -const WrapperNoAPISearch = ({children}) => { - return - - {children} - - -} - -WrapperNoAPISearch.propTypes = { - children: PropTypes.node -} - -export const renderNoAPISearchEntry = (ui, options) => - render(ui, {wrapper: WrapperNoAPISearch, ...options}) - /*****************************************************************************/ // Expects /** @@ -222,3 +204,39 @@ export async function expectInputPeriodicTableItems(elements, root = screen) { }) }) } + +/** + * Tests that the correct FilterMainMenu items are displayed. + * @param {object} context The used search context + * @param {object} root The root element to perform the search on. + */ +export async function expectFilterMainMenu(context, root = screen) { + // Check that menu title is displayed + expect(screen.getByText(`${context.resource} search`)).toBeInTheDocument() + + // Check that menu items are displayed + const menuConfig = context.filter_menus + const menus = menuConfig.include + .filter(key => !menuConfig.exclude.includes(key)) + .map(key => menuConfig.options[key].label) + for (const menu of menus) { + expect(screen.getByText(menu, {selector: 'span'})).toBeInTheDocument() + } +} + +/** + * Tests that the correct SearchResults are displayed. + * @param {object} context The used search context + * @param {object} root The root element to perform the search on. + */ +export async function expectSearchResults(context, root = screen) { + // Wait until search results are in + expect(await screen.findByText("search results", {exact: false})).toBeInTheDocument() + + // Check that correct columns are displayed + const columnConfig = context.columns + const columns = columnConfig.enable.map(key => columnConfig.options[key].label) + for (const column of columns) { + expect(screen.getByText(column)).toBeInTheDocument() + } +} diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 6a9ec4f34..dd8979ee6 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -15,15 +15,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import PropTypes from 'prop-types' +import { Alert } from '@material-ui/lab' +import { has } from 'lodash' import { FilterMenu, FilterMenuItem, FilterMenuItems, FilterSubMenus } from './FilterMenu' -import { makeStyles } from '@material-ui/core/styles' import FilterSubMenuMaterial from './FilterSubMenuMaterial' import FilterSubMenuElements from './FilterSubMenuElements' import FilterSubMenuSymmetry from './FilterSubMenuSymmetry' @@ -45,46 +46,39 @@ import FilterSubMenuDataset from './FilterSubMenuDataset' import FilterSubMenuIDs from './FilterSubMenuIDs' import FilterSubMenuArchive from './FilterSubMenuArchive' import FilterSubMenuOptimade from './FilterSubMenuOptimade' -import { - labelMaterial, - labelElements, - labelSymmetry, - labelMethod, - labelSimulation, - labelDFT, - labelGW, - labelExperiment, - labelEELS, - labelProperties, - labelElectronic, - labelOptoelectronic, - labelVibrational, - labelMechanical, - labelELN, - labelAuthor, - labelDataset, - labelIDs, - labelAccess, - labelSpectroscopy, - labelThermodynamic, - labelArchive, - labelGeometryOptimization, - labelOptimade -} from '../FilterRegistry' import { useSearchContext } from '../SearchContext' -import InputCheckbox from '../input/InputCheckbox' import { delay } from '../../../utils' import FilterSubMenuGeometryOptimization from './FilterSubMenuGeometryOptimization' +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. */ -const useStyles = makeStyles(theme => ({ - combine: { - paddingLeft: theme.spacing(2) - } -})) const FilterMainMenu = React.memo(({ open, onOpenChange, @@ -92,8 +86,7 @@ const FilterMainMenu = React.memo(({ onCollapsedChange }) => { const [value, setValue] = React.useState() - const {resource} = useSearchContext() - const styles = useStyles() + const {filterMenus} = useSearchContext() const [loaded, setLoaded] = useState(false) // Rendering the submenus is delayed on the event queue: this makes loading @@ -102,6 +95,35 @@ const FilterMainMenu = React.memo(({ delay(() => { setLoaded(true) }) }, []) + // The shown menu items + const menuItems = useMemo(() => { + return filterMenus + ? filterMenus.map(option => { + return + }) + : + No search menus defined within this search context. Ensure that all GUI artifacts are created. + + }, [filterMenus]) + + // The shown submenus + const subMenus = useMemo(() => { + return filterMenus + ? filterMenus + .filter(option => has(menuMap, option.key)) + .map(option => { + const Comp = menuMap[option.key] + return + }) + : null + }, [filterMenus]) + return - - - - - - - - - - - - - - - - - - - - - - - - - {resource === 'materials' && - - } + {menuItems} - {loaded && <> - - - - - - - - - - - - - - - - - - - - - - - } + {loaded ? subMenus : null} }) diff --git a/gui/src/components/search/menus/FilterMenu.js b/gui/src/components/search/menus/FilterMenu.js index 61cff3e21..fe565628b 100644 --- a/gui/src/components/search/menus/FilterMenu.js +++ b/gui/src/components/search/menus/FilterMenu.js @@ -440,33 +440,34 @@ const useFilterMenuItemStyles = makeStyles(theme => { } }) export const FilterMenuItem = React.memo(({ - value, + id, + label, group, onClick, disableButton, - depth + level }) => { const styles = useFilterMenuItemStyles() const theme = useTheme() - const groupFinal = group || filterGroups[value] + const groupFinal = group || filterGroups[id] const { selected, open, onChange } = useContext(filterMenuContext) const handleClick = disableButton ? undefined : (onClick || onChange) - const opened = open && value === selected + const opened = open && id === selected return <> handleClick(value))} + onClick={handleClick && (() => handleClick(id))} > {handleClick && @@ -478,14 +479,15 @@ export const FilterMenuItem = React.memo(({ }) FilterMenuItem.propTypes = { - value: PropTypes.string, + id: PropTypes.string, + label: PropTypes.string, group: PropTypes.string, onClick: PropTypes.func, disableButton: PropTypes.bool, - depth: PropTypes.number + level: PropTypes.number } FilterMenuItem.defaultProps = { - depth: 0 + level: 0 } const useFilterSubMenusStyles = makeStyles(theme => { @@ -614,14 +616,14 @@ const useFilterSubMenuStyles = makeStyles(theme => ({ })) export const FilterSubMenu = React.memo(({ - value, + id, size, actions, children }) => { const styles = useFilterSubMenuStyles() const { selected, onSizeChange } = useContext(filterMenuContext) - const visible = value === selected + const visible = id === selected useEffect(() => { if (visible) { onSizeChange(size) @@ -638,7 +640,7 @@ export const FilterSubMenu = React.memo(({ }) FilterSubMenu.propTypes = { - value: PropTypes.string, + id: PropTypes.string, size: PropTypes.oneOf(['small', 'medium', 'large']), actions: PropTypes.node, children: PropTypes.node diff --git a/gui/src/components/search/menus/FilterSubMenuAccess.js b/gui/src/components/search/menus/FilterSubMenuAccess.js index 6d9d435a7..6b43bcd56 100644 --- a/gui/src/components/search/menus/FilterSubMenuAccess.js +++ b/gui/src/components/search/menus/FilterSubMenuAccess.js @@ -23,13 +23,13 @@ import InputRadio from '../input/InputRadio' import { useApi } from '../../api' const FilterSubMenuAccess = React.memo(({ - value, + id, ...rest }) => { const {api} = useApi() const authenticated = api?.keycloak?.authenticated - return + return }) FilterSubMenuAccess.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuAccess diff --git a/gui/src/components/search/menus/FilterSubMenuArchive.js b/gui/src/components/search/menus/FilterSubMenuArchive.js index 2095613b1..4b61bb881 100644 --- a/gui/src/components/search/menus/FilterSubMenuArchive.js +++ b/gui/src/components/search/menus/FilterSubMenuArchive.js @@ -146,7 +146,7 @@ Definition.propTypes = { } const FilterSubMenuArchive = React.memo(({ - value, + id, ...rest }) => { const dataStore = useDataStore() @@ -193,7 +193,7 @@ const FilterSubMenuArchive = React.memo(({ .catch(raiseError) }, [raiseError, dataStore, globalMetainfo, setOptions]) - return + return @@ -210,7 +210,7 @@ const FilterSubMenuArchive = React.memo(({ }) FilterSubMenuArchive.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuArchive diff --git a/gui/src/components/search/menus/FilterSubMenuAuthor.js b/gui/src/components/search/menus/FilterSubMenuAuthor.js index 59e6d95b8..bdabaf3c7 100644 --- a/gui/src/components/search/menus/FilterSubMenuAuthor.js +++ b/gui/src/components/search/menus/FilterSubMenuAuthor.js @@ -23,13 +23,13 @@ import InputField from '../input/InputField' import InputRange from '../input/InputRange' const FilterSubMenuAuthor = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuAuthor.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuAuthor diff --git a/gui/src/components/search/menus/FilterSubMenuDFT.js b/gui/src/components/search/menus/FilterSubMenuDFT.js index 69f4fcf8d..b230865db 100644 --- a/gui/src/components/search/menus/FilterSubMenuDFT.js +++ b/gui/src/components/search/menus/FilterSubMenuDFT.js @@ -23,14 +23,14 @@ import InputField from '../input/InputField' import { InputCheckboxValue } from '../input/InputCheckbox' const FilterSubMenuDFT = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected return }) FilterSubMenuDFT.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuDFT diff --git a/gui/src/components/search/menus/FilterSubMenuDataset.js b/gui/src/components/search/menus/FilterSubMenuDataset.js index e96a5ad64..8a66ebbcf 100644 --- a/gui/src/components/search/menus/FilterSubMenuDataset.js +++ b/gui/src/components/search/menus/FilterSubMenuDataset.js @@ -22,13 +22,13 @@ import { InputGrid, InputGridItem } from '../input/InputGrid' import InputField from '../input/InputField' const FilterSubMenuDataset = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuDataset.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuDataset diff --git a/gui/src/components/search/menus/FilterSubMenuEELS.js b/gui/src/components/search/menus/FilterSubMenuEELS.js index 07a8e0694..922af8baa 100644 --- a/gui/src/components/search/menus/FilterSubMenuEELS.js +++ b/gui/src/components/search/menus/FilterSubMenuEELS.js @@ -25,14 +25,14 @@ import InputSection from '../input/InputSection' import { InputCheckboxValue } from '../input/InputCheckbox' const FilterSubMenuEELS = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected return }) FilterSubMenuEELS.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuEELS diff --git a/gui/src/components/search/menus/FilterSubMenuELN.js b/gui/src/components/search/menus/FilterSubMenuELN.js index d197b4fe1..c5566bb13 100644 --- a/gui/src/components/search/menus/FilterSubMenuELN.js +++ b/gui/src/components/search/menus/FilterSubMenuELN.js @@ -23,14 +23,14 @@ import InputField from '../input/InputField' import { InputCheckboxValue } from '../input/InputCheckbox' const FilterSubMenuELN = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected return }) FilterSubMenuELN.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuELN diff --git a/gui/src/components/search/menus/FilterSubMenuElectronic.js b/gui/src/components/search/menus/FilterSubMenuElectronic.js index 43fd42983..27beef3ca 100644 --- a/gui/src/components/search/menus/FilterSubMenuElectronic.js +++ b/gui/src/components/search/menus/FilterSubMenuElectronic.js @@ -24,13 +24,13 @@ import InputRange from '../input/InputRange' import InputField from '../input/InputField' const FilterSubMenuElectronic = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuElectronic.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuElectronic diff --git a/gui/src/components/search/menus/FilterSubMenuElements.js b/gui/src/components/search/menus/FilterSubMenuElements.js index f28228aed..f76c23e94 100644 --- a/gui/src/components/search/menus/FilterSubMenuElements.js +++ b/gui/src/components/search/menus/FilterSubMenuElements.js @@ -34,14 +34,14 @@ const useStyles = makeStyles(theme => ({ })) const FilterSubMenuElements = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected const styles = useStyles() - return + return }) FilterSubMenuElements.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuElements diff --git a/gui/src/components/search/menus/FilterSubMenuGW.js b/gui/src/components/search/menus/FilterSubMenuGW.js index f9e394ac6..5461606e9 100644 --- a/gui/src/components/search/menus/FilterSubMenuGW.js +++ b/gui/src/components/search/menus/FilterSubMenuGW.js @@ -23,14 +23,14 @@ import InputField from '../input/InputField' import { InputCheckboxValue } from '../input/InputCheckbox' const FilterSubMenuGW = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected return }) FilterSubMenuGW.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuGW diff --git a/gui/src/components/search/menus/FilterSubMenuGeometryOptimization.js b/gui/src/components/search/menus/FilterSubMenuGeometryOptimization.js index ca0443e0e..28c281c54 100644 --- a/gui/src/components/search/menus/FilterSubMenuGeometryOptimization.js +++ b/gui/src/components/search/menus/FilterSubMenuGeometryOptimization.js @@ -24,14 +24,14 @@ import { InputCheckboxValue } from '../input/InputCheckbox' import { InputGrid, InputGridItem } from '../input/InputGrid' const FilterSubMenuGeometryOptimization = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected return }) FilterSubMenuGeometryOptimization.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuGeometryOptimization diff --git a/gui/src/components/search/menus/FilterSubMenuIDs.js b/gui/src/components/search/menus/FilterSubMenuIDs.js index f2b7e9883..55dafe986 100644 --- a/gui/src/components/search/menus/FilterSubMenuIDs.js +++ b/gui/src/components/search/menus/FilterSubMenuIDs.js @@ -22,13 +22,13 @@ import { InputGrid, InputGridItem } from '../input/InputGrid' import InputField from '../input/InputField' const FilterSubMenuIDs = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuIDs.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuIDs diff --git a/gui/src/components/search/menus/FilterSubMenuMaterial.js b/gui/src/components/search/menus/FilterSubMenuMaterial.js index 34fed7488..36d3b425f 100644 --- a/gui/src/components/search/menus/FilterSubMenuMaterial.js +++ b/gui/src/components/search/menus/FilterSubMenuMaterial.js @@ -22,13 +22,13 @@ import { InputGrid, InputGridItem } from '../input/InputGrid' import InputField from '../input/InputField' const FilterSubMenuMaterial = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuMaterial.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuMaterial diff --git a/gui/src/components/search/menus/FilterSubMenuMechanical.js b/gui/src/components/search/menus/FilterSubMenuMechanical.js index 73ea73fd8..c8dfdb231 100644 --- a/gui/src/components/search/menus/FilterSubMenuMechanical.js +++ b/gui/src/components/search/menus/FilterSubMenuMechanical.js @@ -24,13 +24,13 @@ import InputSection from '../input/InputSection' import { InputGrid, InputGridItem } from '../input/InputGrid' const FilterSubMenuMechanical = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuMechanical.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuMechanical diff --git a/gui/src/components/search/menus/FilterSubMenuMethod.js b/gui/src/components/search/menus/FilterSubMenuMethod.js index 2340e5a47..e40502574 100644 --- a/gui/src/components/search/menus/FilterSubMenuMethod.js +++ b/gui/src/components/search/menus/FilterSubMenuMethod.js @@ -22,13 +22,13 @@ import { InputGrid, InputGridItem } from '../input/InputGrid' import InputField from '../input/InputField' const FilterSubMenuMethod = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuMethod.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuMethod diff --git a/gui/src/components/search/menus/FilterSubMenuOptimade.js b/gui/src/components/search/menus/FilterSubMenuOptimade.js index f44e6427e..a4b7db9e6 100644 --- a/gui/src/components/search/menus/FilterSubMenuOptimade.js +++ b/gui/src/components/search/menus/FilterSubMenuOptimade.js @@ -23,11 +23,11 @@ import {Box, TextField} from "@material-ui/core" import {useSearchContext} from "../SearchContext" import AutoComplete from "@material-ui/lab/Autocomplete" import {useApi} from '../../api' -import {debounce} from 'lodash' +import {debounce, isArray} from 'lodash' import {useErrors} from '../../errors' const FilterSubMenuOptimade = React.memo(({ - value, + id, ...rest }) => { const [suggestions, setSuggestions] = useState([['']]) @@ -37,7 +37,7 @@ const FilterSubMenuOptimade = React.memo(({ const {useFilterState} = useSearchContext() const {api} = useApi() const {raiseError} = useErrors() - const visible = open && value === selected + const visible = open && id === selected const [optimadeFilters, setOptimadeFilters] = useFilterState("optimade_filter") useEffect(() => { @@ -87,7 +87,7 @@ const FilterSubMenuOptimade = React.memo(({ }) return {valid: true, suggestions: []} } catch (error) { - if (error?.apiMessage) { + if (isArray(error?.apiMessage)) { const suggestions = [] error?.apiMessage.forEach((err) => { const errorLocations = err?.loc @@ -117,6 +117,7 @@ const FilterSubMenuOptimade = React.memo(({ }) return {valid: false, suggestions: suggestions} } + return {valid: false, suggestions: []} } }, [api, identifiers, renderSuggestion]) @@ -200,7 +201,7 @@ const FilterSubMenuOptimade = React.memo(({ }, [setFilter, setOptimadeFilter, validate, values]) return }) FilterSubMenuOptimade.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuOptimade diff --git a/gui/src/components/search/menus/FilterSubMenuOptoelectronic.js b/gui/src/components/search/menus/FilterSubMenuOptoelectronic.js index da3bf1481..a77e89fcf 100644 --- a/gui/src/components/search/menus/FilterSubMenuOptoelectronic.js +++ b/gui/src/components/search/menus/FilterSubMenuOptoelectronic.js @@ -23,14 +23,14 @@ import InputSection from '../input/InputSection' import InputRange from '../input/InputRange' import InputField from '../input/InputField' -const FilterSubMenuOptoElectronic = React.memo(({ - value, +const FilterSubMenuOptoelectronic = React.memo(({ + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) -FilterSubMenuOptoElectronic.propTypes = { - value: PropTypes.string +FilterSubMenuOptoelectronic.propTypes = { + id: PropTypes.string } -export default FilterSubMenuOptoElectronic +export default FilterSubMenuOptoelectronic diff --git a/gui/src/components/search/menus/FilterSubMenuQuantities.js b/gui/src/components/search/menus/FilterSubMenuQuantities.js index 0f58c1983..ebbc6f983 100644 --- a/gui/src/components/search/menus/FilterSubMenuQuantities.js +++ b/gui/src/components/search/menus/FilterSubMenuQuantities.js @@ -22,13 +22,13 @@ import { InputGrid, InputGridItem } from '../input/InputGrid' import InputField from '../input/InputField' const FilterSubMenuQuantities = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuQuantities.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuQuantities diff --git a/gui/src/components/search/menus/FilterSubMenuSimulation.js b/gui/src/components/search/menus/FilterSubMenuSimulation.js index f60553d13..44cf1cae8 100644 --- a/gui/src/components/search/menus/FilterSubMenuSimulation.js +++ b/gui/src/components/search/menus/FilterSubMenuSimulation.js @@ -22,13 +22,13 @@ import { InputGrid, InputGridItem } from '../input/InputGrid' import InputField from '../input/InputField' const FilterSubMenuSimulation = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuSimulation.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuSimulation diff --git a/gui/src/components/search/menus/FilterSubMenuSpectroscopy.js b/gui/src/components/search/menus/FilterSubMenuSpectroscopy.js index 30fa14888..1f068688d 100644 --- a/gui/src/components/search/menus/FilterSubMenuSpectroscopy.js +++ b/gui/src/components/search/menus/FilterSubMenuSpectroscopy.js @@ -24,13 +24,13 @@ import InputSection from '../input/InputSection' import InputField from '../input/InputField' const FilterSubMenuSpectroscopy = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuSpectroscopy.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuSpectroscopy diff --git a/gui/src/components/search/menus/FilterSubMenuSymmetry.js b/gui/src/components/search/menus/FilterSubMenuSymmetry.js index 99f4a83ff..a64895a71 100644 --- a/gui/src/components/search/menus/FilterSubMenuSymmetry.js +++ b/gui/src/components/search/menus/FilterSubMenuSymmetry.js @@ -22,13 +22,13 @@ import { InputGrid, InputGridItem } from '../input/InputGrid' import InputField from '../input/InputField' const FilterSubMenuSymmetry = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuSymmetry.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuSymmetry diff --git a/gui/src/components/search/menus/FilterSubMenuThermodynamic.js b/gui/src/components/search/menus/FilterSubMenuThermodynamic.js index fd8a175ab..5d397812b 100644 --- a/gui/src/components/search/menus/FilterSubMenuThermodynamic.js +++ b/gui/src/components/search/menus/FilterSubMenuThermodynamic.js @@ -24,13 +24,13 @@ import InputSection from '../input/InputSection' import { InputGrid, InputGridItem } from '../input/InputGrid' const FilterSubMenuThermodynamic = React.memo(({ - value, + id, ...rest }) => { const {selected} = useContext(filterMenuContext) - const visible = value === selected + const visible = id === selected - return + return }) FilterSubMenuThermodynamic.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuThermodynamic diff --git a/gui/src/components/search/menus/FilterSubMenuVibrational.js b/gui/src/components/search/menus/FilterSubMenuVibrational.js index 25a1d076b..c4b30868f 100644 --- a/gui/src/components/search/menus/FilterSubMenuVibrational.js +++ b/gui/src/components/search/menus/FilterSubMenuVibrational.js @@ -22,13 +22,13 @@ import { InputGrid, InputGridItem } from '../input/InputGrid' import InputField from '../input/InputField' const FilterSubMenuElectronic = React.memo(({ - value, + id, ...rest }) => { const {selected, open} = useContext(filterMenuContext) - const visible = open && value === selected + const visible = open && id === selected - return + return }) FilterSubMenuElectronic.propTypes = { - value: PropTypes.string + id: PropTypes.string } export default FilterSubMenuElectronic diff --git a/gui/src/components/search/results/SearchResults.js b/gui/src/components/search/results/SearchResults.js deleted file mode 100644 index 703cf6c2e..000000000 --- a/gui/src/components/search/results/SearchResults.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright The NOMAD Authors. - * - * This file is part of NOMAD. See https://nomad-lab.eu for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react' -import { - Paper, Typography -} from '@material-ui/core' -import SearchResultsMaterials from './SearchResultsMaterials' -import SearchResultsEntries from './SearchResultsEntries' -import { useSearchContext } from '../SearchContext' - -/** - * Displays the list of search results - */ -const SearchResults = React.memo(function SearchResults() { - const {resource, useResults} = useSearchContext() - const {data, pagination, setPagination} = useResults() - - if (pagination.total === 0) { - return no results - } - - if (!pagination.total) { - return searching ... - } - - const Component = resource === 'materials' ? SearchResultsMaterials : SearchResultsEntries - return - - -}) - -export default SearchResults diff --git a/gui/src/components/search/results/SearchResultsEntries.js b/gui/src/components/search/results/SearchResultsEntries.js deleted file mode 100644 index 894489383..000000000 --- a/gui/src/components/search/results/SearchResultsEntries.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright The NOMAD Authors. - * - * This file is part of NOMAD. See https://nomad-lab.eu for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { useState, useMemo } from 'react' -import PropTypes from 'prop-types' -import { Link } from '@material-ui/core' -import EntryDownloadButton from '../../entry/EntryDownloadButton' -import EntryDetails, { EntryRowActions, Published } from '../../entry/EntryDetails' -import { authorList, pluralize, formatInteger, formatTimestamp } from '../../../utils' -import { - addColumnDefaults, - Datatable, DatatableLoadMorePagination, DatatableTable, - DatatableToolbar, DatatableToolbarActions } from '../../datatable/Datatable' -import { useSearchContext } from '../SearchContext' - -const columns = [ - {key: 'entry_name', label: 'Name', align: 'left'}, - {key: 'results.material.chemical_formula_hill', label: 'Formula', align: 'left'}, - {key: 'entry_type', label: 'Entry type', align: 'left'}, - {key: 'results.method.method_name'}, - {key: 'results.method.simulation.program_name'}, - {key: 'results.method.simulation.dft.basis_set_name'}, - {key: 'results.method.simulation.dft.xc_functional_type', label: 'XC functional type'}, - {key: 'results.material.structural_type'}, - {key: 'results.material.symmetry.crystal_system'}, - {key: 'results.material.symmetry.space_group_symbol'}, - {key: 'results.material.symmetry.space_group_number'}, - {key: 'results.eln.lab_ids'}, - {key: 'results.eln.sections'}, - {key: 'results.eln.methods'}, - {key: 'results.eln.tags'}, - {key: 'results.eln.instruments'}, - {key: 'mainfile', align: 'left'}, - { - key: 'upload_create_time', - label: 'Upload time', - align: 'left', - render: row => row?.upload_create_time - ? formatTimestamp(row.upload_create_time) - : no upload time - }, - {key: 'authors', render: row => authorList(row), align: 'left', sortable: false}, - {key: 'comment', sortable: false, align: 'left'}, - { - key: 'references', - sortable: false, - align: 'left', - render: row => { - const refs = row.references || [] - if (refs.length > 0) { - return ( -
- {refs.map((ref, i) => - {ref}{(i + 1) < refs.length ? ', ' : } - )} -
- ) - } else { - return no references - } - } - }, - { - key: 'datasets', - align: 'left', - sortable: false, - render: entry => { - const datasets = entry.datasets || [] - if (datasets.length > 0) { - return datasets.map(dataset => dataset.dataset_name).join(', ') - } else { - return no datasets - } - } - }, - { - key: 'published', - label: 'Access', - render: (entry) => - } -] - -addColumnDefaults(columns) - -const defaultSelectedColumns = [ - 'entry_name', - 'results.material.chemical_formula_hill', - 'entry_type', - 'upload_create_time', - 'authors' -] - -const SearchResultsEntries = React.memo((props) => { - const { useQuery } = useSearchContext() - const [selected, setSelected] = useState([]) - const searchQuery = useQuery() - const {pagination, data} = props - - const query = useMemo(() => { - if (selected === 'all') { - return searchQuery - } - - return {entry_id: selected.map(data => data.entry_id)} - }, [selected, searchQuery]) - - return - - - - - - - load more - - -}) -SearchResultsEntries.propTypes = { - pagination: PropTypes.object, - data: PropTypes.arrayOf(PropTypes.object) -} - -export default SearchResultsEntries diff --git a/gui/src/components/search/results/SearchResultsMaterials.js b/gui/src/components/search/results/SearchResultsMaterials.js deleted file mode 100644 index 6e5ba427c..000000000 --- a/gui/src/components/search/results/SearchResultsMaterials.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright The NOMAD Authors. - * - * This file is part of NOMAD. See https://nomad-lab.eu for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react' -import PropTypes from 'prop-types' -import { IconButton } from '@material-ui/core' -import DetailsIcon from '@material-ui/icons/MoreHoriz' -import { formatInteger, pluralize } from '../../../utils' -import { encyclopediaBase } from '../../../config' -import { MaterialButton } from '../../nav/Routes' -import { - addColumnDefaults, - Datatable, DatatableLoadMorePagination, DatatableTable, - DatatableToolbar } from '../../datatable/Datatable' - -const columns = [ - {key: 'chemical_formula_hill', label: 'Formula', align: 'left'}, - {key: 'structural_type'}, - {key: 'symmetry.structure_name'}, - {key: 'symmetry.crystal_system'}, - {key: 'symmetry.space_group_symbol'}, - {key: 'symmetry.space_group_number'}, - {key: 'material_id', align: 'left'} -] - -addColumnDefaults(columns) - -const defaultSelectedColumns = [ - 'chemical_formula_hill', - 'structural_type', - 'symmetry.structure_name', - 'symmetry.space_group_number', - 'symmetry.crystal_system', - 'material_id'] - -const VisitMaterialAction = React.memo(function VisitMaterialAction({data}) { - return - - -}) -VisitMaterialAction.propTypes = { - data: PropTypes.object.isRequired -} - -/** - * Displays the list of search results for materials. - */ -const SearchResultsMaterials = React.memo(function SearchResultsMaterials(props) { - const {data, pagination} = props - - return - - - - load more - - -}) -SearchResultsMaterials.propTypes = { - pagination: PropTypes.object.isRequired, - data: PropTypes.arrayOf(PropTypes.object) -} - -export default SearchResultsMaterials diff --git a/gui/src/config.js b/gui/src/config.js index eb43eb6ed..b47e981c7 100644 --- a/gui/src/config.js +++ b/gui/src/config.js @@ -16,17 +16,14 @@ * limitations under the License. */ import { createTheme } from '@material-ui/core' -import { urlAbs } from './utils' window.nomadEnv = window.nomadEnv || {} export const version = window.nomadEnv.version -// Ensure that appBase is a correct absolute url that uses the same protocol as -// the page in which this script is loaded. -export const appBase = urlAbs(window.nomadEnv.appBase.replace(/\/$/, '')) -// export const apiBase = 'http://nomad-lab.eu/prod/rae/api' +export const appBase = window.nomadEnv.appBase.replace(/\/$/, '') export const apiBase = `${appBase}/api` export const northBase = window.nomadEnv.northBase export const guiBase = process.env.PUBLIC_URL +export const ui = window.nomadEnv.ui export const servicesUploadLimit = window.nomadEnv.servicesUploadLimit export const keycloakBase = window.nomadEnv.keycloakBase export const keycloakRealm = window.nomadEnv.keycloakRealm diff --git a/gui/src/setupTests.js b/gui/src/setupTests.js index 75d3694e8..c769d63c0 100644 --- a/gui/src/setupTests.js +++ b/gui/src/setupTests.js @@ -20,6 +20,7 @@ import { setupServer } from 'msw/node' import { configure } from '@testing-library/react' +import "../public/env" import '@testing-library/jest-dom' // Adds convenient expect-methods /** @@ -27,29 +28,6 @@ import '@testing-library/jest-dom' // Adds convenient expect-methods * the suite is executed. Contains e.g. global setup/teardown functionality for * tests. */ -global.nomadEnv = { - 'keycloakBase': 'https://nomad-lab.eu/fairdi/keycloak/auth/', - // Use the production API - // 'keycloakRealm': 'fairdi_nomad_prod', - // 'keycloakClientId': 'nomad_public', - // 'appBase': 'https://nomad-lab.eu/prod/v1', - // Use the local API - 'keycloakRealm': 'fairdi_nomad_test', - 'keycloakClientId': 'nomad_gui_dev', - 'appBase': 'http://localhost:8000/fairdi/nomad/latest', - 'encyclopediaBase': 'https://nomad-lab.eu/prod/rae/encyclopedia/#', - 'debug': false, - 'version': { - 'label': '1.1.0', - 'isBeta': false, - 'isTest': true, - 'usesBetaData': true, - 'officialUrl': 'https://nomad-lab.eu/prod/rae/gui' - }, - 'aitoolkitEnabled': false, - 'oasis': false, - 'servicesUploadLimit': 10 -} export const seconds = 1000 export const minutes = 60 * seconds diff --git a/gui/tests/data/search/searchpage.json b/gui/tests/data/search/searchpage.json new file mode 100644 index 000000000..6bb29b2ee --- /dev/null +++ b/gui/tests/data/search/searchpage.json @@ -0,0 +1,648 @@ +{ + "afa783d98e040c74b68e5cfd3d82f3f0": [ + { + "request": { + "url": "http://localhost:8000/fairdi/nomad/latest/api/v1/entries/query", + "method": "POST", + "body": { + "exclude": [ + "atoms", + "only_atoms", + "files", + "quantities", + "dft.quantities", + "optimade", + "dft.labels", + "dft.geometries" + ], + "owner": "visible", + "query": {}, + "aggregations": {}, + "pagination": { + "order_by": "upload_create_time", + "order": "desc" + } + }, + "headers": { + "accept": "application/json, text/plain, */*", + "content-type": "application/json", + "cookie": null + } + }, + "response": { + "status": 200, + "body": { + "owner": "visible", + "query": {}, + "pagination": { + "page_size": 10, + "order_by": "upload_create_time", + "order": "desc", + "total": 3 + }, + "data": [ + { + "upload_id": "k5_X2bQtRkGPLSv6Oob-uw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "m7fNWksjSMK3rUwglYP0uQ", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:43:34.189237+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:43:34.193237+00:00", + "publish_time": "2022-09-23T08:43:34.192237+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C", + "H" + ], + "elements_exclusive": "C H", + "chemical_formula_anonymous": "AB2", + "material_id": "test_material_id", + "structural_type": "molecule / cluster", + "chemical_formula_reduced": "CH3", + "n_elements": 2, + "chemical_formula_descriptive": "CH3", + "chemical_formula_hill": "CH3" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "m7fNWksjSMK3rUwglYP0uQ", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "k5_X2bQtRkGPLSv6Oob-uw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "E5TXC654S1CeAxiy9F29eQ", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:43:34.189237+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:43:34.194237+00:00", + "publish_time": "2022-09-23T08:43:34.192237+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C" + ], + "elements_exclusive": "C", + "chemical_formula_anonymous": "A", + "material_id": "test_material_id", + "structural_type": "2D", + "chemical_formula_reduced": "C", + "n_elements": 1, + "chemical_formula_descriptive": "C", + "chemical_formula_hill": "C" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "LDA_X_PZ", + "LDA_C_PZ" + ] + }, + "program_name": "exciting" + } + }, + "properties": {} + }, + "entry_id": "E5TXC654S1CeAxiy9F29eQ", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "k5_X2bQtRkGPLSv6Oob-uw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "C0q53EgRRSmUIICDMEopaQ", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:43:34.189237+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:43:34.195237+00:00", + "publish_time": "2022-09-23T08:43:34.192237+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "Hf", + "Nb", + "Ta", + "Ti", + "Zr" + ], + "elements_exclusive": "Hf Nb Ta Ti Zr", + "chemical_formula_anonymous": "ABCDE", + "material_id": "test_material_id", + "structural_type": "bulk", + "chemical_formula_reduced": "HfNbTaTiZr", + "n_elements": 5, + "chemical_formula_descriptive": "HfNbTaTiZr", + "chemical_formula_hill": "HfNbTaTiZr" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "C0q53EgRRSmUIICDMEopaQ", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + } + ] + }, + "headers": { + "connection": "close", + "content-length": "6650", + "content-type": "application/json", + "server": "uvicorn" + } + } + }, + { + "request": { + "url": "http://localhost:8000/fairdi/nomad/latest/api/v1/entries/query", + "method": "POST", + "body": { + "exclude": [ + "atoms", + "only_atoms", + "files", + "quantities", + "dft.quantities", + "dft.labels", + "dft.geometries" + ], + "owner": "visible" + }, + "headers": { + "accept": "application/json", + "content-type": "application/json", + "cookie": null + } + }, + "response": { + "status": 200, + "body": { + "owner": "visible", + "query": { + "and": [] + }, + "pagination": { + "page_size": 10, + "order_by": "entry_id", + "order": "asc", + "total": 3 + }, + "data": [ + { + "upload_id": "k5_X2bQtRkGPLSv6Oob-uw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "C0q53EgRRSmUIICDMEopaQ", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:43:34.189237+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:43:34.195237+00:00", + "publish_time": "2022-09-23T08:43:34.192237+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "Hf", + "Nb", + "Ta", + "Ti", + "Zr" + ], + "elements_exclusive": "Hf Nb Ta Ti Zr", + "chemical_formula_anonymous": "ABCDE", + "material_id": "test_material_id", + "structural_type": "bulk", + "chemical_formula_reduced": "HfNbTaTiZr", + "n_elements": 5, + "chemical_formula_descriptive": "HfNbTaTiZr", + "chemical_formula_hill": "HfNbTaTiZr" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "C0q53EgRRSmUIICDMEopaQ", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "k5_X2bQtRkGPLSv6Oob-uw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "E5TXC654S1CeAxiy9F29eQ", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:43:34.189237+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:43:34.194237+00:00", + "publish_time": "2022-09-23T08:43:34.192237+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C" + ], + "elements_exclusive": "C", + "chemical_formula_anonymous": "A", + "material_id": "test_material_id", + "structural_type": "2D", + "chemical_formula_reduced": "C", + "n_elements": 1, + "chemical_formula_descriptive": "C", + "chemical_formula_hill": "C" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "LDA_X_PZ", + "LDA_C_PZ" + ] + }, + "program_name": "exciting" + } + }, + "properties": {} + }, + "entry_id": "E5TXC654S1CeAxiy9F29eQ", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "k5_X2bQtRkGPLSv6Oob-uw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "m7fNWksjSMK3rUwglYP0uQ", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:43:34.189237+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:43:34.193237+00:00", + "publish_time": "2022-09-23T08:43:34.192237+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C", + "H" + ], + "elements_exclusive": "C H", + "chemical_formula_anonymous": "AB2", + "material_id": "test_material_id", + "structural_type": "molecule / cluster", + "chemical_formula_reduced": "CH3", + "n_elements": 2, + "chemical_formula_descriptive": "CH3", + "chemical_formula_hill": "CH3" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "m7fNWksjSMK3rUwglYP0uQ", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + } + ] + }, + "headers": { + "connection": "close", + "content-length": "6656", + "content-type": "application/json", + "server": "uvicorn" + } + } + }, + { + "request": { + "url": "http://localhost:8000/fairdi/nomad/latest/api/v1/entries/query", + "method": "POST", + "body": { + "exclude": [ + "atoms", + "only_atoms", + "files", + "quantities", + "dft.quantities", + "optimade", + "dft.labels", + "dft.geometries" + ], + "verify_only": true, + "owner": "public", + "query": { + "optimade_filter": "." + } + }, + "headers": { + "accept": "application/json", + "content-type": "application/json", + "cookie": null + } + }, + "response": { + "status": 422, + "body": { + "detail": [ + { + "loc": [ + [ + "optimade_filter" + ] + ], + "msg": "Could not parse optimade filter: Syntax error: Unable to parse filter .. Lark traceback: \nNo terminal matches '.' in the current parser context, at line 1 col 1\n\n.\n^\nExpected one of: \n\t* SIGNED_FLOAT\n\t* SIGNED_INT\n\t* ESCAPED_STRING\n\t* IDENTIFIER\n\t* NOT\n\t* LPAR\n", + "type": "value_error.exception" + } + ] + }, + "headers": { + "connection": "close", + "content-length": "361", + "content-type": "application/json", + "server": "uvicorn" + } + } + } + ], + "cd2b263727fb2d6704d7b6a1e68a2c83": [ + { + "request": { + "url": "http://localhost:8000/fairdi/nomad/latest/api/v1/entries/archive/query", + "method": "POST", + "body": { + "owner": "visible", + "query": { + "quantities": "definitions.section_definitions", + "processed": true + }, + "required": { + "metadata": { + "entry_id": "*" + } + } + }, + "headers": { + "accept": "application/json", + "content-type": "application/json", + "cookie": null + } + }, + "response": { + "status": 200, + "body": { + "owner": "visible", + "query": { + "and": [ + { + "name": "quantities", + "value": "definitions.section_definitions" + }, + { + "name": "processed", + "value": true + } + ] + }, + "pagination": { + "page_size": 10, + "order_by": "entry_id", + "order": "asc", + "total": 0 + }, + "required": { + "metadata": { + "entry_id": "*" + } + }, + "data": [] + }, + "headers": { + "connection": "close", + "content-length": "414", + "content-type": "application/json", + "server": "uvicorn" + } + } + } + ] +} \ No newline at end of file diff --git a/gui/tests/data/search/userdatapage.json b/gui/tests/data/search/userdatapage.json new file mode 100644 index 000000000..d1d93b2d9 --- /dev/null +++ b/gui/tests/data/search/userdatapage.json @@ -0,0 +1,1088 @@ +{ + "afa783d98e040c74b68e5cfd3d82f3f0": [ + { + "request": { + "url": "http://localhost:8000/fairdi/nomad/latest/api/v1/entries/query", + "method": "POST", + "body": { + "exclude": [ + "atoms", + "only_atoms", + "files", + "quantities", + "dft.quantities", + "optimade", + "dft.labels", + "dft.geometries" + ], + "owner": "user", + "query": {}, + "aggregations": {}, + "pagination": { + "order_by": "upload_create_time", + "order": "desc" + } + }, + "headers": { + "accept": "application/json, text/plain, */*", + "content-type": "application/json", + "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSWFFIV1YxSEJ6cmh5U3h3UmRDdkhCcUF1WVNKRzZWSEJSZXg0TW5oX293In0.eyJqdGkiOiJmODdiNDQ3Ni02NzEzLTRmNDItYTZkNi1hMzllMTE2MzcxZjIiLCJleHAiOjE2NjM5NTg0NzIsIm5iZiI6MCwiaWF0IjoxNjYzOTIyNDkwLCJpc3MiOiJodHRwczovL25vbWFkLWxhYi5ldS9mYWlyZGkva2V5Y2xvYWsvYXV0aC9yZWFsbXMvZmFpcmRpX25vbWFkX3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNjg4NzhhZjctNjg0NS00NmMwLWIyYzEtMjUwZDRkOGViNDcwIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibm9tYWRfZ3VpX2RldiIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6Ijc5Y2IwNzA5LTc3MTItNGI3NS1hYzNkLTFmYTQyNDc2MjNhNCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiTWFya3VzIFNjaGVpZGdlbiIsInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QiLCJnaXZlbl9uYW1lIjoiTWFya3VzIiwiZmFtaWx5X25hbWUiOiJTY2hlaWRnZW4iLCJlbWFpbCI6Im1hcmt1cy5zY2hlaWRnZW5AZmhpLWJlcmxpbi5kZSJ9.IgFHsT-PMGymNtkGZKV7uXpDQw6ByV5hYlawZpUjcQvucktNGYH8DmNT2Bmb9-axMq4fAJQ3HRcnmrbl4Do2aR0OSaLXKw9NG_IVXohTs1hO_kLX04orwI8o-sExMP12PQEvGb361OOBs9J_jm70X_u8tsUQFts6niRsjJPzkwjnATYtlpYEQeWqz9FMrYK8JfrGJ-LdD9PvNU6JTQevA7tkGZwNdfjEkSOS9afY1MOKGCqVZqn-cna7L5ACSgEhyKW6XCYXehZmUXJ5-GSCTEnOLaP8CX5RJwI9fmN9b5err7X7pkJA8RvmkRzncG9rjvj1TroTLY34_kSlnFDZsQ", + "cookie": null + } + }, + "response": { + "status": 200, + "body": { + "owner": "user", + "query": {}, + "pagination": { + "page_size": 10, + "order_by": "upload_create_time", + "order": "desc", + "total": 6 + }, + "data": [ + { + "upload_id": "Ki9Q8rlqTg2mbH3F-vTcCw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "kJVNJu-7QIiAA7mFfFCUHA", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:41:25.964423+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:41:25.970423+00:00", + "publish_time": "2022-09-23T08:41:25.967423+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "Hf", + "Nb", + "Ta", + "Ti", + "Zr" + ], + "elements_exclusive": "Hf Nb Ta Ti Zr", + "chemical_formula_anonymous": "ABCDE", + "material_id": "test_material_id", + "structural_type": "bulk", + "chemical_formula_reduced": "HfNbTaTiZr", + "n_elements": 5, + "chemical_formula_descriptive": "HfNbTaTiZr", + "chemical_formula_hill": "HfNbTaTiZr" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "kJVNJu-7QIiAA7mFfFCUHA", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "Ki9Q8rlqTg2mbH3F-vTcCw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "S4TS7jK6SZih_TlG6f6pzw", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:41:25.964423+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:41:25.969423+00:00", + "publish_time": "2022-09-23T08:41:25.967423+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C" + ], + "elements_exclusive": "C", + "chemical_formula_anonymous": "A", + "material_id": "test_material_id", + "structural_type": "2D", + "chemical_formula_reduced": "C", + "n_elements": 1, + "chemical_formula_descriptive": "C", + "chemical_formula_hill": "C" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "LDA_X_PZ", + "LDA_C_PZ" + ] + }, + "program_name": "exciting" + } + }, + "properties": {} + }, + "entry_id": "S4TS7jK6SZih_TlG6f6pzw", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "Ki9Q8rlqTg2mbH3F-vTcCw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "L2XdKIejR26-tY8oh-8Elg", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:41:25.964423+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:41:25.968423+00:00", + "publish_time": "2022-09-23T08:41:25.967423+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C", + "H" + ], + "elements_exclusive": "C H", + "chemical_formula_anonymous": "AB2", + "material_id": "test_material_id", + "structural_type": "molecule / cluster", + "chemical_formula_reduced": "CH3", + "n_elements": 2, + "chemical_formula_descriptive": "CH3", + "chemical_formula_hill": "CH3" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "L2XdKIejR26-tY8oh-8Elg", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "hjmWTEduRbu38CyRfioc6Q", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "wgUs1YepS_qsmjDxxDiPzg", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:40:30.393991+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:40:30.399991+00:00", + "publish_time": "2022-09-23T08:40:30.396991+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "Hf", + "Nb", + "Ta", + "Ti", + "Zr" + ], + "elements_exclusive": "Hf Nb Ta Ti Zr", + "chemical_formula_anonymous": "ABCDE", + "material_id": "test_material_id", + "structural_type": "bulk", + "chemical_formula_reduced": "HfNbTaTiZr", + "n_elements": 5, + "chemical_formula_descriptive": "HfNbTaTiZr", + "chemical_formula_hill": "HfNbTaTiZr" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "wgUs1YepS_qsmjDxxDiPzg", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "hjmWTEduRbu38CyRfioc6Q", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "RmcAx4wdTfSaLjOLfIwuXw", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:40:30.393991+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:40:30.398991+00:00", + "publish_time": "2022-09-23T08:40:30.396991+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C" + ], + "elements_exclusive": "C", + "chemical_formula_anonymous": "A", + "material_id": "test_material_id", + "structural_type": "2D", + "chemical_formula_reduced": "C", + "n_elements": 1, + "chemical_formula_descriptive": "C", + "chemical_formula_hill": "C" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "LDA_X_PZ", + "LDA_C_PZ" + ] + }, + "program_name": "exciting" + } + }, + "properties": {} + }, + "entry_id": "RmcAx4wdTfSaLjOLfIwuXw", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "hjmWTEduRbu38CyRfioc6Q", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "DKtmMb41Q8CjZSZDJUdD-g", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:40:30.393991+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:40:30.397991+00:00", + "publish_time": "2022-09-23T08:40:30.396991+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C", + "H" + ], + "elements_exclusive": "C H", + "chemical_formula_anonymous": "AB2", + "material_id": "test_material_id", + "structural_type": "molecule / cluster", + "chemical_formula_reduced": "CH3", + "n_elements": 2, + "chemical_formula_descriptive": "CH3", + "chemical_formula_hill": "CH3" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "DKtmMb41Q8CjZSZDJUdD-g", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + } + ] + }, + "headers": { + "connection": "close", + "content-length": "13124", + "content-type": "application/json", + "server": "uvicorn" + } + } + }, + { + "request": { + "url": "http://localhost:8000/fairdi/nomad/latest/api/v1/entries/query", + "method": "POST", + "body": { + "exclude": [ + "atoms", + "only_atoms", + "files", + "quantities", + "dft.quantities", + "dft.labels", + "dft.geometries" + ], + "owner": "visible" + }, + "headers": { + "accept": "application/json", + "content-type": "application/json", + "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSWFFIV1YxSEJ6cmh5U3h3UmRDdkhCcUF1WVNKRzZWSEJSZXg0TW5oX293In0.eyJqdGkiOiI0ZGE5ZTRmOC01NjcxLTQzNTktYjU2NS0xMjg1ZTg1ODIwODMiLCJleHAiOjE2NjM5NTg0NzIsIm5iZiI6MCwiaWF0IjoxNjYzOTIyNDk1LCJpc3MiOiJodHRwczovL25vbWFkLWxhYi5ldS9mYWlyZGkva2V5Y2xvYWsvYXV0aC9yZWFsbXMvZmFpcmRpX25vbWFkX3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNjg4NzhhZjctNjg0NS00NmMwLWIyYzEtMjUwZDRkOGViNDcwIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibm9tYWRfZ3VpX2RldiIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6Ijc5Y2IwNzA5LTc3MTItNGI3NS1hYzNkLTFmYTQyNDc2MjNhNCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiTWFya3VzIFNjaGVpZGdlbiIsInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QiLCJnaXZlbl9uYW1lIjoiTWFya3VzIiwiZmFtaWx5X25hbWUiOiJTY2hlaWRnZW4iLCJlbWFpbCI6Im1hcmt1cy5zY2hlaWRnZW5AZmhpLWJlcmxpbi5kZSJ9.GeFNgOEBxtRibwOc-mmDL0JHFP5yI3RWGwo7QyPnZTi4jnL7qEnkIG0_BbdBbC7B1hKppXjGGHLY2zgYMoPddJkeywXwqdZP72WY3Wijb8qpCilGHrMTToGobUNetR_NMRzjs1zTYp02ilzpeL_F0BCluMxxF9DHuKLtCrzPiOC-M7vvojxN1W6NjvJF0oWPRZH8hRoAtITJCEVjMFqA853UOKzYt3pxA8SAHeFCrshfLgHMqLLnqnE2awCzaVOznsnCYnvqftAkdDDcKkBi3AbrW-OkBPRsWgTptSkRMpTbz289Dqm7YnpiaTZcMM5PHnClOFpB3Q3Y6H86pYDS9Q", + "cookie": null + } + }, + "response": { + "status": 200, + "body": { + "owner": "visible", + "query": { + "and": [] + }, + "pagination": { + "page_size": 10, + "order_by": "entry_id", + "order": "asc", + "total": 6 + }, + "data": [ + { + "upload_id": "hjmWTEduRbu38CyRfioc6Q", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "DKtmMb41Q8CjZSZDJUdD-g", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:40:30.393991+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:40:30.397991+00:00", + "publish_time": "2022-09-23T08:40:30.396991+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C", + "H" + ], + "elements_exclusive": "C H", + "chemical_formula_anonymous": "AB2", + "material_id": "test_material_id", + "structural_type": "molecule / cluster", + "chemical_formula_reduced": "CH3", + "n_elements": 2, + "chemical_formula_descriptive": "CH3", + "chemical_formula_hill": "CH3" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "DKtmMb41Q8CjZSZDJUdD-g", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "Ki9Q8rlqTg2mbH3F-vTcCw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "L2XdKIejR26-tY8oh-8Elg", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:41:25.964423+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:41:25.968423+00:00", + "publish_time": "2022-09-23T08:41:25.967423+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C", + "H" + ], + "elements_exclusive": "C H", + "chemical_formula_anonymous": "AB2", + "material_id": "test_material_id", + "structural_type": "molecule / cluster", + "chemical_formula_reduced": "CH3", + "n_elements": 2, + "chemical_formula_descriptive": "CH3", + "chemical_formula_hill": "CH3" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "L2XdKIejR26-tY8oh-8Elg", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "hjmWTEduRbu38CyRfioc6Q", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "RmcAx4wdTfSaLjOLfIwuXw", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:40:30.393991+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:40:30.398991+00:00", + "publish_time": "2022-09-23T08:40:30.396991+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C" + ], + "elements_exclusive": "C", + "chemical_formula_anonymous": "A", + "material_id": "test_material_id", + "structural_type": "2D", + "chemical_formula_reduced": "C", + "n_elements": 1, + "chemical_formula_descriptive": "C", + "chemical_formula_hill": "C" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "LDA_X_PZ", + "LDA_C_PZ" + ] + }, + "program_name": "exciting" + } + }, + "properties": {} + }, + "entry_id": "RmcAx4wdTfSaLjOLfIwuXw", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "Ki9Q8rlqTg2mbH3F-vTcCw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "S4TS7jK6SZih_TlG6f6pzw", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:41:25.964423+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:41:25.969423+00:00", + "publish_time": "2022-09-23T08:41:25.967423+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "C" + ], + "elements_exclusive": "C", + "chemical_formula_anonymous": "A", + "material_id": "test_material_id", + "structural_type": "2D", + "chemical_formula_reduced": "C", + "n_elements": 1, + "chemical_formula_descriptive": "C", + "chemical_formula_hill": "C" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "LDA_X_PZ", + "LDA_C_PZ" + ] + }, + "program_name": "exciting" + } + }, + "properties": {} + }, + "entry_id": "S4TS7jK6SZih_TlG6f6pzw", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "Ki9Q8rlqTg2mbH3F-vTcCw", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "kJVNJu-7QIiAA7mFfFCUHA", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:41:25.964423+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:41:25.970423+00:00", + "publish_time": "2022-09-23T08:41:25.967423+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "Hf", + "Nb", + "Ta", + "Ti", + "Zr" + ], + "elements_exclusive": "Hf Nb Ta Ti Zr", + "chemical_formula_anonymous": "ABCDE", + "material_id": "test_material_id", + "structural_type": "bulk", + "chemical_formula_reduced": "HfNbTaTiZr", + "n_elements": 5, + "chemical_formula_descriptive": "HfNbTaTiZr", + "chemical_formula_hill": "HfNbTaTiZr" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "kJVNJu-7QIiAA7mFfFCUHA", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + }, + { + "upload_id": "hjmWTEduRbu38CyRfioc6Q", + "parser_name": "parsers/vasp", + "origin": "Markus Scheidgen", + "calc_id": "wgUs1YepS_qsmjDxxDiPzg", + "published": true, + "writers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "datasets": [], + "n_quantities": 0, + "upload_create_time": "2022-09-23T08:40:30.393991+00:00", + "processed": true, + "mainfile": "upload/archive.json", + "main_author": { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + }, + "viewers": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "entry_create_time": "2022-09-23T08:40:30.399991+00:00", + "publish_time": "2022-09-23T08:40:30.396991+00:00", + "with_embargo": false, + "domain": "dft", + "results": { + "material": { + "elements": [ + "Hf", + "Nb", + "Ta", + "Ti", + "Zr" + ], + "elements_exclusive": "Hf Nb Ta Ti Zr", + "chemical_formula_anonymous": "ABCDE", + "material_id": "test_material_id", + "structural_type": "bulk", + "chemical_formula_reduced": "HfNbTaTiZr", + "n_elements": 5, + "chemical_formula_descriptive": "HfNbTaTiZr", + "chemical_formula_hill": "HfNbTaTiZr" + }, + "method": { + "simulation": { + "program_version": "not processed", + "dft": { + "basis_set_type": "unavailable", + "core_electron_treatment": "unavailable", + "xc_functional_type": "not processed", + "xc_functional_names": [ + "GGA_X_PBE_SOL", + "GGA_C_PBE_SOL" + ] + }, + "program_name": "VASP" + } + }, + "properties": {} + }, + "entry_id": "wgUs1YepS_qsmjDxxDiPzg", + "authors": [ + { + "user_id": "68878af7-6845-46c0-b2c1-250d4d8eb470", + "name": "Markus Scheidgen" + } + ], + "license": "CC BY 4.0" + } + ] + }, + "headers": { + "connection": "close", + "content-length": "13133", + "content-type": "application/json", + "server": "uvicorn" + } + } + }, + { + "request": { + "url": "http://localhost:8000/fairdi/nomad/latest/api/v1/entries/query", + "method": "POST", + "body": { + "exclude": [ + "atoms", + "only_atoms", + "files", + "quantities", + "dft.quantities", + "optimade", + "dft.labels", + "dft.geometries" + ], + "verify_only": true, + "owner": "public", + "query": { + "optimade_filter": "." + } + }, + "headers": { + "accept": "application/json", + "content-type": "application/json", + "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSWFFIV1YxSEJ6cmh5U3h3UmRDdkhCcUF1WVNKRzZWSEJSZXg0TW5oX293In0.eyJqdGkiOiI0ZGE5ZTRmOC01NjcxLTQzNTktYjU2NS0xMjg1ZTg1ODIwODMiLCJleHAiOjE2NjM5NTg0NzIsIm5iZiI6MCwiaWF0IjoxNjYzOTIyNDk1LCJpc3MiOiJodHRwczovL25vbWFkLWxhYi5ldS9mYWlyZGkva2V5Y2xvYWsvYXV0aC9yZWFsbXMvZmFpcmRpX25vbWFkX3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNjg4NzhhZjctNjg0NS00NmMwLWIyYzEtMjUwZDRkOGViNDcwIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibm9tYWRfZ3VpX2RldiIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6Ijc5Y2IwNzA5LTc3MTItNGI3NS1hYzNkLTFmYTQyNDc2MjNhNCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiTWFya3VzIFNjaGVpZGdlbiIsInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QiLCJnaXZlbl9uYW1lIjoiTWFya3VzIiwiZmFtaWx5X25hbWUiOiJTY2hlaWRnZW4iLCJlbWFpbCI6Im1hcmt1cy5zY2hlaWRnZW5AZmhpLWJlcmxpbi5kZSJ9.GeFNgOEBxtRibwOc-mmDL0JHFP5yI3RWGwo7QyPnZTi4jnL7qEnkIG0_BbdBbC7B1hKppXjGGHLY2zgYMoPddJkeywXwqdZP72WY3Wijb8qpCilGHrMTToGobUNetR_NMRzjs1zTYp02ilzpeL_F0BCluMxxF9DHuKLtCrzPiOC-M7vvojxN1W6NjvJF0oWPRZH8hRoAtITJCEVjMFqA853UOKzYt3pxA8SAHeFCrshfLgHMqLLnqnE2awCzaVOznsnCYnvqftAkdDDcKkBi3AbrW-OkBPRsWgTptSkRMpTbz289Dqm7YnpiaTZcMM5PHnClOFpB3Q3Y6H86pYDS9Q", + "cookie": null + } + }, + "response": { + "status": 422, + "body": { + "detail": [ + { + "loc": [ + [ + "optimade_filter" + ] + ], + "msg": "Could not parse optimade filter: Syntax error: Unable to parse filter .. Lark traceback: \nNo terminal matches '.' in the current parser context, at line 1 col 1\n\n.\n^\nExpected one of: \n\t* SIGNED_FLOAT\n\t* SIGNED_INT\n\t* ESCAPED_STRING\n\t* IDENTIFIER\n\t* NOT\n\t* LPAR\n", + "type": "value_error.exception" + } + ] + }, + "headers": { + "connection": "close", + "content-length": "361", + "content-type": "application/json", + "server": "uvicorn" + } + } + } + ], + "cd2b263727fb2d6704d7b6a1e68a2c83": [ + { + "request": { + "url": "http://localhost:8000/fairdi/nomad/latest/api/v1/entries/archive/query", + "method": "POST", + "body": { + "owner": "visible", + "query": { + "quantities": "definitions.section_definitions", + "processed": true + }, + "required": { + "metadata": { + "entry_id": "*" + } + } + }, + "headers": { + "accept": "application/json", + "content-type": "application/json", + "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSWFFIV1YxSEJ6cmh5U3h3UmRDdkhCcUF1WVNKRzZWSEJSZXg0TW5oX293In0.eyJqdGkiOiI0ZGE5ZTRmOC01NjcxLTQzNTktYjU2NS0xMjg1ZTg1ODIwODMiLCJleHAiOjE2NjM5NTg0NzIsIm5iZiI6MCwiaWF0IjoxNjYzOTIyNDk1LCJpc3MiOiJodHRwczovL25vbWFkLWxhYi5ldS9mYWlyZGkva2V5Y2xvYWsvYXV0aC9yZWFsbXMvZmFpcmRpX25vbWFkX3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNjg4NzhhZjctNjg0NS00NmMwLWIyYzEtMjUwZDRkOGViNDcwIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibm9tYWRfZ3VpX2RldiIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6Ijc5Y2IwNzA5LTc3MTItNGI3NS1hYzNkLTFmYTQyNDc2MjNhNCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiTWFya3VzIFNjaGVpZGdlbiIsInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QiLCJnaXZlbl9uYW1lIjoiTWFya3VzIiwiZmFtaWx5X25hbWUiOiJTY2hlaWRnZW4iLCJlbWFpbCI6Im1hcmt1cy5zY2hlaWRnZW5AZmhpLWJlcmxpbi5kZSJ9.GeFNgOEBxtRibwOc-mmDL0JHFP5yI3RWGwo7QyPnZTi4jnL7qEnkIG0_BbdBbC7B1hKppXjGGHLY2zgYMoPddJkeywXwqdZP72WY3Wijb8qpCilGHrMTToGobUNetR_NMRzjs1zTYp02ilzpeL_F0BCluMxxF9DHuKLtCrzPiOC-M7vvojxN1W6NjvJF0oWPRZH8hRoAtITJCEVjMFqA853UOKzYt3pxA8SAHeFCrshfLgHMqLLnqnE2awCzaVOznsnCYnvqftAkdDDcKkBi3AbrW-OkBPRsWgTptSkRMpTbz289Dqm7YnpiaTZcMM5PHnClOFpB3Q3Y6H86pYDS9Q", + "cookie": null + } + }, + "response": { + "status": 200, + "body": { + "owner": "visible", + "query": { + "and": [ + { + "name": "quantities", + "value": "definitions.section_definitions" + }, + { + "name": "processed", + "value": true + } + ] + }, + "pagination": { + "page_size": 10, + "order_by": "entry_id", + "order": "asc", + "total": 0 + }, + "required": { + "metadata": { + "entry_id": "*" + } + }, + "data": [] + }, + "headers": { + "connection": "close", + "content-length": "414", + "content-type": "application/json", + "server": "uvicorn" + } + } + } + ] +} \ No newline at end of file diff --git a/nomad/cli/admin/admin.py b/nomad/cli/admin/admin.py index 5b750404b..7a9a5b354 100644 --- a/nomad/cli/admin/admin.py +++ b/nomad/cli/admin/admin.py @@ -21,6 +21,7 @@ import click from nomad import config from nomad.cli.cli import cli +from nomad.cli.dev import get_gui_config @cli.group(help='''The nomad admin commands to do nasty stuff directly on the databases. @@ -230,7 +231,6 @@ def gui_config(): from nomad import config import glob import shutil - import json gui_folder = os.path.abspath( os.path.join(os.path.dirname(__file__), '../../app/static/gui')) @@ -244,20 +244,8 @@ def gui_config(): env_js_file = os.path.join(run_gui_folder, 'env.js') if not os.path.exists(env_js_file): with open(env_js_file, 'wt') as f: - f.write(f''' -window.nomadEnv = {{ - 'appBase': '{config.services.api_base_path.rstrip("/")}', - 'northBase': '{config.services.api_base_path.rstrip("/")}/north', - 'keycloakBase': '{config.keycloak.public_server_url}', - 'keycloakRealm': '{config.keycloak.realm_name}', - 'keycloakClientId': '{config.keycloak.client_id}', - 'debug': false, - 'encyclopediaBase': '{config.encyclopedia_base if config.encyclopedia_base else 'undefined'}', - 'aitoolkitEnabled': {'true' if config.aitoolkit_enabled else 'false'}, - 'oasis': {'true' if config.oasis.is_oasis else 'false'}, - 'version': {json.dumps(config.meta.beta) if config.meta.beta else dict()}, - 'globalLoginRequired': {'false' if config.oasis.allowed_users is None else 'true'} -}};''') + conf = get_gui_config() + f.write(conf) # replace base path in all GUI files source_file_globs = [ diff --git a/nomad/cli/dev.py b/nomad/cli/dev.py index 278d272e6..481426327 100644 --- a/nomad/cli/dev.py +++ b/nomad/cli/dev.py @@ -1,4 +1,3 @@ - # Copyright The NOMAD Authors. # # This file is part of NOMAD. See https://nomad-lab.eu for further info. @@ -17,6 +16,7 @@ # import sys +import json from collections import defaultdict from pint.converters import ScaleConverter import os @@ -67,7 +67,6 @@ def gui_qa(skip_tests: bool): @dev.command(help='Generates a JSON with all metainfo.') def metainfo(): - import json export = _all_metainfo_packages() metainfo_data = export.m_to_dict(with_meta=True) print(json.dumps(metainfo_data, indent=2)) @@ -107,8 +106,6 @@ def _all_metainfo_packages(): @dev.command(help='Generates a JSON with all search quantities.') def search_quantities(): _all_metainfo_packages() - import json - # Currently only quantities with "entry_type" are included. from nomad.metainfo.elasticsearch_extension import entry_type, Elasticsearch from nomad.datamodel import EntryArchive @@ -161,15 +158,41 @@ def search_quantities(): @dev.command(help='Generates a JSON file that compiles all the parser metadata from each parser project.') def parser_metadata(): - import json from nomad.parsing.parsers import code_metadata print(json.dumps(code_metadata, indent=2, sort_keys=True)) +def get_gui_config(): + '''Create a simplified and strippped version of the nomad.yaml contents that + is used by the GUI. + ''' + from nomad import config + + return f'''window.nomadEnv = {{ + 'appBase': '{"https" if config.services.https else "http"}://{config.services.api_host}:{config.services.api_port}{config.services.api_base_path.rstrip("/")}', + 'northBase': '{"https" if config.services.https else "http"}://{config.north.hub_host}:{config.north.hub_port}{config.services.api_base_path.rstrip("/")}/north', + 'keycloakBase': '{config.keycloak.public_server_url}', + 'keycloakRealm': '{config.keycloak.realm_name}', + 'keycloakClientId': '{config.keycloak.client_id}', + 'debug': false, + 'encyclopediaBase': '{config.encyclopedia_base if config.encyclopedia_base else 'undefined'}', + 'aitoolkitEnabled': {'true' if config.aitoolkit_enabled else 'false'}, + 'oasis': {'true' if config.oasis.is_oasis else 'false'}, + 'version': {json.dumps(config.meta.beta) if config.meta.beta else dict()}, + 'globalLoginRequired': {'false' if config.oasis.allowed_users is None else 'true'}, + 'servicesUploadLimit': { config.services.upload_limit }, + 'ui': {json.dumps(config.ui) if config.ui else dict()} +}};''' + + +@dev.command(help='Generates a JS file that contains a subset of the nomad.yaml config needed by the GUI.') +def gui_config(): + print(get_gui_config()) + + @dev.command(help='Generates a JSON file from example-uploads metadata in the YAML file.') def example_upload_metadata(): - import json import yaml os_list = {} @@ -184,7 +207,6 @@ def example_upload_metadata(): def toolkit_metadata(): import requests import re - import json modules = requests.get( 'https://gitlab.mpcdf.mpg.de/api/v4/projects/3161/repository/files/.gitmodules/raw?ref=master').text @@ -339,7 +361,6 @@ def example_data(username: str): @click.pass_context def units(ctx): import re - import json from nomad.units import ureg # TODO: Check that all units are unambiguously defined, and that there are @@ -541,7 +562,6 @@ def units(ctx): def vscode_extension(output: str): import shutil import yaml - import json extension_path = os.path.normpath(output) + "/nomad-vscode" if output else "./nomad-vscode" snippets_path = os.path.join(extension_path, "snippets") diff --git a/nomad/config.py b/nomad/config.py index 1ea2cae1d..eba40159b 100644 --- a/nomad/config.py +++ b/nomad/config.py @@ -40,7 +40,7 @@ import os import os.path import yaml import warnings -from typing import Dict, Any +from typing import Dict, List, Any try: from nomad import gitinfo @@ -439,6 +439,175 @@ archive = NomadConfig( min_entries_per_process=20 # minimum number of entries per process ) +ui = NomadConfig( + search_contexts={ + "include": ["entries"], + "exclude": [], + "options": { + "entries": { + 'label': "Entries", + 'path': "entries", + 'resource': 'entries', + 'breadcrumb': "Entries search", + 'description': "Search individual database entries", + 'help': { + 'title': 'Entries search', + 'content': r''' + This page allows you to **search entries** within NOMAD. Entries represent + individual calculations or experiments that have bee uploaded into NOMAD. + + The search page consists of three main elements: the filter panel, the search + bar, and the result list. + + The filter panel on the left allows you to graphically explore and enter + different search filters. It also gives a visual indication of the currently + active search filters for each category. This is a good place to start exploring + the available search filters and their meaning. + + The search bar allows you to specify filters by typing them in and pressing + enter. You can also start by simply typing keywords of interest, which will + toggle a list of suggestions. For numerical data you can also use range queries, + e.g. \`0.0 < band_gap <= 0.1\`. + + Notice that the units used in the filter panel and in the queries can be changed + using the **units** button on the top right corner. When using the search bar, + you can also specify a unit by typing the unit abbreviations, e.g. \`band_gap >= + 0.1 Ha\` + + The result list on the right is automatically updated according to the filters + you have specified. You can browse through the results by scrolling through the + available items and loading more results as you go. Here you can also change the + sorting of the results, modify the displayed columns, access individual entries + or even download the raw data or the archive document by selecting individual + entries and pressing the download button that appears. The ellipsis button shown + for each entry will navigate you to that entry's page. This entry page will show + more metadata, raw files, the entry's archive, and processing logs. + ''', + }, + 'pagination': { + 'order_by': 'upload_create_time', + 'order': 'desc', + }, + 'columns': { + 'enable': [ + 'entry_name', + 'results.material.chemical_formula_hill', + 'entry_type', + 'upload_create_time', + 'authors' + ], + 'include': [ + 'entry_name', + 'results.material.chemical_formula_hill', + 'entry_type', + 'results.method.method_name', + 'results.method.simulation.program_name', + 'results.method.simulation.dft.basis_set_name', + 'results.method.simulation.dft.xc_functional_type', + 'results.material.structural_type', + 'results.material.symmetry.crystal_system', + 'results.material.symmetry.space_group_symbol', + 'results.material.symmetry.space_group_number', + 'results.eln.lab_ids', + 'results.eln.sections', + 'results.eln.methods', + 'results.eln.tags', + 'results.eln.instruments', + 'mainfile', + 'upload_create_time', + 'authors', + 'comment', + 'references', + 'datasets', + 'published', + ], + 'exclude': [], + 'options': { + 'entry_name': {'label': 'Name', 'align': 'left'}, + 'results.material.chemical_formula_hill': {'label': 'Formula', 'align': 'left'}, + 'entry_type': {'label': 'Entry type', 'align': 'left'}, + 'results.method.method_name': {'label': 'Method name'}, + 'results.method.simulation.program_name': {'label': 'Program name'}, + 'results.method.simulation.dft.basis_set_name': {'label': 'Basis set name'}, + 'results.method.simulation.dft.xc_functional_type': {'label': 'XC functional type'}, + 'results.material.structural_type': {'label': 'Structural type'}, + 'results.material.symmetry.crystal_system': {'label': 'Crystal system'}, + 'results.material.symmetry.space_group_symbol': {'label': 'Space group symbol'}, + 'results.material.symmetry.space_group_number': {'label': 'Space group number'}, + 'results.eln.lab_ids': {'label': 'Lab IDs'}, + 'results.eln.sections': {'label': 'Sections'}, + 'results.eln.methods': {'label': 'Methods'}, + 'results.eln.tags': {'label': 'Tags'}, + 'results.eln.instruments': {'label': 'Instruments'}, + 'mainfile': {'label': 'Mainfile', 'align': 'left'}, + 'upload_create_time': {'label': 'Upload time', 'align': 'left'}, + 'authors': {'label': 'Authors', 'align': 'left'}, + 'comment': {'label': 'Comment', 'align': 'left'}, + 'references': {'label': 'References', 'align': 'left'}, + 'datasets': {'label': 'Datasets', 'align': 'left'}, + 'published': {'label': 'Access'} + } + }, + 'filter_menus': { + 'include': [ + 'material', + 'elements', + 'symmetry', + 'method', + 'simulation', + 'dft', + 'gw', + 'experiment', + 'eels', + 'properties', + 'electronic', + 'optoelectronic', + 'vibrational', + 'mechanical', + 'spectroscopy', + 'thermodynamic', + 'geometry_optimization', + 'eln', + 'author', + 'dataset', + 'access', + 'ids', + 'processed_data_quantities', + 'optimade', + ], + 'exclude': [], + 'options': { + 'material': {'label': 'Material', 'level': 0, 'size': 'small'}, + 'elements': {'label': 'Elements / Formula', 'level': 1, 'size': 'large'}, + 'symmetry': {'label': 'Symmetry', 'level': 1, 'size': 'small'}, + 'method': {'label': 'Method', 'level': 0, 'size': 'small'}, + 'simulation': {'label': 'Simulation', 'level': 1, 'size': 'small'}, + 'dft': {'label': 'DFT', 'level': 2, 'size': 'small'}, + 'gw': {'label': 'GW', 'level': 2, 'size': 'small'}, + 'experiment': {'label': 'Experiment', 'level': 1, 'size': 'small'}, + 'eels': {'label': 'EELS', 'level': 2, 'size': 'small'}, + 'properties': {'label': 'Properties', 'level': 0, 'size': 'small'}, + 'electronic': {'label': 'Electronic', 'level': 1, 'size': 'small'}, + 'optoelectronic': {'label': 'Optoelectronic', 'level': 1, 'size': 'small'}, + 'vibrational': {'label': 'Vibrational', 'level': 1, 'size': 'small'}, + 'mechanical': {'label': 'Mechanical', 'level': 1, 'size': 'small'}, + 'spectroscopy': {'label': 'Spectroscopy', 'level': 1, 'size': 'small'}, + 'thermodynamic': {'label': 'Thermodynamic', 'level': 1, 'size': 'small'}, + 'geometry_optimization': {'label': 'Geometry Optimization', 'level': 1, 'size': 'small'}, + 'eln': {'label': 'Electronic Lab Notebook', 'level': 0, 'size': 'small'}, + 'author': {'label': 'Author / Origin', 'level': 0, 'size': 'medium'}, + 'dataset': {'label': 'Dataset', 'level': 0, 'size': 'small'}, + 'access': {'label': 'Access', 'level': 0, 'size': 'small'}, + 'ids': {'label': 'IDs', 'level': 0, 'size': 'small'}, + 'processed_data_quantities': {'label': 'Processed Data Quantities', 'level': 0, 'size': 'medium'}, + 'optimade': {'label': 'Optimade', 'level': 0, 'size': 'medium'}, + } + } + } + } + } +) + def north_url(ssl: bool = True): return api_url(ssl=ssl, api='north', api_host=north.hub_host, api_port=north.hub_port) @@ -476,6 +645,25 @@ _transformations = { logger = logging.getLogger(__name__) +def _merge(a: dict, b: dict, path: List[str] = None) -> dict: + ''' + Recursively merges b into a. Will add new key-value pairs, and will + overwrite existing key-value pairs. Notice that this mutates the original + dictionary a and if you want to return a copy, you will want to first + (deep)copy the original dictionary. + ''' + if path is None: path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + _merge(a[key], b[key], path + [str(key)]) + else: + a[key] = b[key] + else: + a[key] = b[key] + return a + + def _apply(key, value, raise_error: bool = True) -> None: ''' Changes the config according to given key and value. The first part of a key @@ -516,6 +704,9 @@ def _apply(key, value, raise_error: bool = True) -> None: if current_value is not None and not isinstance(value, type(current_value)): value = _transformations.get(full_key, type(current_value))(value) + if isinstance(value, dict): + value = _merge(current[key], value) + current[key] = value logger.info(f'set config setting {full_key}={value}') except Exception as e: -- GitLab From acb34a973b079386b95b062e26a462b42ca6fdfa Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Tue, 27 Sep 2022 12:30:32 +0000 Subject: [PATCH 021/117] Resolve "Recoil memory leak" --- gui/package.json | 2 +- gui/src/components/search/SearchContext.js | 62 ++++++++++++---------- gui/yarn.lock | 8 +-- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/gui/package.json b/gui/package.json index d59ff785b..175e05a5e 100644 --- a/gui/package.json +++ b/gui/package.json @@ -60,7 +60,7 @@ "react-swipeable-views": "^0.14.0", "react-virtualized-auto-sizer": "^1.0.2", "react-window": "^1.8.5", - "recoil": "0.3.1", + "recoil": "0.7.5", "recompose": "^0.30.0", "remark": "^14.0.2", "remark-math": "^5.1.1", diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 40d3bf1e5..c31a0df71 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -15,6 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React, { useCallback, useEffect, useState, useRef, useMemo, useContext } from 'react' import { atom, @@ -42,6 +43,7 @@ import { } from 'lodash' import { Link } from '@material-ui/core' import qs from 'qs' +import { v4 as uuidv4 } from 'uuid' import PropTypes from 'prop-types' import { useHistory } from 'react-router-dom' import { useApi } from '../api' @@ -78,9 +80,6 @@ import { * depending on the context would re-render for each filter change regardless if * it actually changes the state of that component or not. */ -let indexContext = 0 -let indexFilters = 0 -let indexLocked = 0 /** * Used to turn empty containers into undefined which is used to indicate that a @@ -119,6 +118,9 @@ export const SearchContext = React.memo(({ const updatedFilters = useRef(new Set()) const firstLoad = useRef(true) const disableUpdate = useRef(false) + const contextID = useMemo(() => uuidv4(), []) + const indexFilters = useRef(0) + const indexLocked = useRef(0) // The final filtered set of columns const columns = useMemo(() => { @@ -278,12 +280,12 @@ export const SearchContext = React.memo(({ useSetFilters ] = useMemo(() => { const queryFamily = atomFamily({ - key: `queryFamily_${indexContext}`, + key: `queryFamily_${contextID}`, default: (name) => initialQuery[name] }) // Used to get/set the locked state of all filters at once const filtersState = selector({ - key: `filtersState_${indexContext}`, + key: `filtersState_${contextID}`, get: ({get}) => { const query = {} for (const key of filters) { @@ -299,13 +301,13 @@ export const SearchContext = React.memo(({ const guiLocked = toGUIFilter(filtersLocked) const lockedFamily = atomFamily({ - key: `lockedFamily_${indexContext}`, + key: `lockedFamily_${contextID}`, default: (name) => !isNil(guiLocked?.[name]) }) // Used to set the locked state of several filters at once const lockedState = selector({ - key: `lockedState_${indexContext}`, + key: `lockedState_${contextID}`, get: ({get}) => { const locks = {} for (const key of filters) { @@ -321,7 +323,7 @@ export const SearchContext = React.memo(({ * single query object used by the API. */ const queryState = selector({ - key: `query_${indexContext}`, + key: `query_${contextID}`, get: ({get}) => { const query = {} for (const key of filters) { @@ -345,36 +347,36 @@ export const SearchContext = React.memo(({ }) const isStatisticsEnabledState = atom({ - key: `statisticsEnabled_${indexContext}`, + key: `statisticsEnabled_${contextID}`, default: true }) const isMenuOpenState = atom({ - key: `isMenuOpen_${indexContext}`, + key: `isMenuOpen_${contextID}`, default: false }) const isCollapsedState = atom({ - key: `isCollapsed_${indexContext}`, + key: `isCollapsed_${contextID}`, default: false }) const paginationState = atom({ - key: `pagination_${indexContext}`, + key: `pagination_${contextID}`, default: initialPagination }) const resultsUsedState = atom({ - key: `resultsUsed_${indexContext}`, + key: `resultsUsed_${contextID}`, default: false }) const statisticFamily = atomFamily({ - key: `statisticFamily_${indexContext}`, + key: `statisticFamily_${contextID}`, default: (name) => finalStatistics[name] }) // Used to get/set the the statistics configuration of all filters const statisticsState = selector({ - key: `statisticsState_${indexContext}`, + key: `statisticsState_${contextID}`, set: ({set}, stats) => { stats && Object.entries(stats).forEach(([key, value]) => set(statisticFamily(key), value)) }, @@ -444,29 +446,29 @@ export const SearchContext = React.memo(({ } const resultsState = atom({ - key: `results_${indexContext}`, + key: `results_${contextID}`, default: { pagination: {} } }) const apiDataState = atom({ - key: `apiData_${indexContext}`, + key: `apiData_${contextID}`, default: null }) const aggsFamilyRaw = atomFamily({ - key: `aggsFamilyRaw_${indexContext}`, + key: `aggsFamilyRaw_${contextID}`, default: (name) => initialAggs[name] }) const aggKeys = atom({ - key: `aggKeys_${indexContext}`, + key: `aggKeys_${contextID}`, default: [] }) const aggsFamily = selectorFamily({ - key: `aggsFamily_${indexContext}`, + key: `aggsFamily_${contextID}`, get: (id) => ({ get }) => { return get(aggsFamilyRaw(id)) }, @@ -481,7 +483,7 @@ export const SearchContext = React.memo(({ * requests into a single query object used by the API. */ const aggsState = selector({ - key: `aggs_${indexContext}`, + key: `aggs_${contextID}`, get: ({get}) => { const aggs = {} for (const key of get(aggKeys)) { @@ -496,13 +498,16 @@ export const SearchContext = React.memo(({ // Atom for each aggregation response. const aggsResponseFamily = atomFamily({ - key: `aggsResponseFamily_${indexContext}`, + key: `aggsResponseFamily_${contextID}`, default: undefined }) // Recoil.js selector for updating the aggs response in one go. const aggsResponseState = selector({ - key: `aggsResponse_${indexContext}`, + key: `aggsResponse_${contextID}`, + get: ({get}) => { + return undefined + }, set: ({ set }, data) => { if (data) { for (const [key, value] of Object.entries(data)) { @@ -650,8 +655,8 @@ export const SearchContext = React.memo(({ // id. Because this hook can be called dynamically, we simply generate the ID // sequentially. const filterState = useMemo(() => { - const id = `locked_selector${indexLocked}` - indexLocked += 1 + const id = `locked_selector_${contextID}_${indexLocked.current}` + indexLocked.current += 1 return selector({ key: id, get: ({get}) => { @@ -684,8 +689,8 @@ export const SearchContext = React.memo(({ // id. Because this hook can be called dynamically, we simply generate the ID // sequentially. const filterState = useMemo(() => { - const id = `dynamic_selector${indexFilters}` - indexFilters += 1 + const id = `dynamic_selector_${contextID}_${indexFilters.current}` + indexFilters.current += 1 return selector({ key: id, get: ({get}) => { @@ -781,7 +786,6 @@ export const SearchContext = React.memo(({ return useSetRecoilState(filtersState) } - ++indexContext return [ useFilterLocked, useFiltersLocked, @@ -813,7 +817,7 @@ export const SearchContext = React.memo(({ useAgg, useSetFilters ] - }, [initialQuery, filters, filtersLocked, finalStatistics, initialAggs, initialPagination, filterData]) + }, [contextID, initialQuery, filters, filtersLocked, finalStatistics, initialAggs, initialPagination, filterData]) const setResults = useSetRecoilState(resultsState) const setApiData = useSetRecoilState(apiDataState) diff --git a/gui/yarn.lock b/gui/yarn.lock index a6b46b282..71546883f 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -13838,10 +13838,10 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -recoil@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.3.1.tgz#40ef544160d19d76e25de8929d7e512eace13b90" - integrity sha512-KNA3DRqgxX4rRC8E7fc6uIw7BACmMPuraIYy+ejhE8tsw7w32CetMm8w7AMZa34wzanKKkev3vl3H7Z4s0QSiA== +recoil@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.7.5.tgz#9a33a03350cfd99e08bdd5b73bfc8b8b9ee751b9" + integrity sha512-GVShsj5+M/2GULWBs5WBJGcsNis/d3YvDiaKjYh3mLKXftjtmk9kfaQ8jwjoIXySCwn8/RhgJ4Sshwgzj2UpFA== dependencies: hamt_plus "1.0.2" -- GitLab From f4e4e2f087a7da4afe035bf58d82ecd0e45cd110 Mon Sep 17 00:00:00 2001 From: Markus Scheidgen Date: Tue, 27 Sep 2022 15:29:35 +0200 Subject: [PATCH 022/117] Fixed default value in ArchiveBrowser. #1087 --- gui/src/components/archive/ArchiveBrowser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/src/components/archive/ArchiveBrowser.js b/gui/src/components/archive/ArchiveBrowser.js index fe2fdb32b..529409ae3 100644 --- a/gui/src/components/archive/ArchiveBrowser.js +++ b/gui/src/components/archive/ArchiveBrowser.js @@ -487,7 +487,7 @@ class SectionAdaptor extends ArchiveAdaptor { async itemAdaptor(key) { const [name, index] = key.split(':') const property = this.def._properties[name] - const value = property?.default || this.obj[name] + const value = this.obj[name] || property?.default if (!property) { return super.itemAdaptor(key) } else if (property.m_def === SubSectionMDef) { @@ -799,7 +799,7 @@ function Section({section, def, parentRelation, sectionIsEditable, sectionIsInEl const renderQuantity = useCallback(quantityDef => { const key = quantityDef.name - const value = quantityDef.default || section[key] + const value = section[key] || quantityDef.default const isDefault = value && !section[key] const disabled = value === undefined if (!disabled && quantityDef.type.type_kind === 'reference' && quantityDef.shape.length === 1) { -- GitLab From 89907ab0ba49ed320092fc41726f9a2d1bae4bdf Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 023/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index c31a0df71..b3729c398 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -195,6 +195,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From e1e8a54cbd0710e2ec6eb73c76054883da476b34 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 024/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index b3729c398..c13506e31 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -268,6 +268,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From ea95b87382b05cc4f96cc7616a713aedfa54a4a3 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 025/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index f5c427355..11e78bd77 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -240,6 +240,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 669691c87e5a4a4432c58a6d42c28fe4b9b5823e Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 026/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 11e78bd77..dcde6c500 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -313,6 +313,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 08966eb1d00ae37c0dacd9f016a0bd1693cf23f5 Mon Sep 17 00:00:00 2001 From: Thea Denell Date: Tue, 1 Nov 2022 09:15:14 +0100 Subject: [PATCH 027/117] added new unit test data for surfaces and 2D structures and modified unit tests respectively --- nomad/normalizing/material.py | 57 ++- tests/normalizing/conftest.py | 37 ++ tests/normalizing/test_material.py | 785 ++++++++++++++++++++++------- 3 files changed, 671 insertions(+), 208 deletions(-) diff --git a/nomad/normalizing/material.py b/nomad/normalizing/material.py index beba2f130..19dcff4a8 100644 --- a/nomad/normalizing/material.py +++ b/nomad/normalizing/material.py @@ -27,9 +27,9 @@ import numpy as np import matid.geometry from matid.classification.structureclusterer import StructureClusterer from matid import Classifier -from matid.classifications import Class0D, Atom, Class1D, Material2D, Surface, Class3D, Unknown +from matid.classifications import Class0D, Atom, Class1D, Material2D, Surface, Class3D, Class2D, Unknown from matid.symmetry.symmetryanalyzer import SymmetryAnalyzer -from nomad.datamodel.results import Symmetry, Material, System, Relation, Structure, Prototype +from nomad.datamodel.results import Symmetry, Material, System, Relation, Structure, Prototype, Cell from nomad import atomutils from nomad.utils import hash from nomad.units import ureg @@ -568,7 +568,6 @@ class MaterialNormalizer(): clusters = self._perform_matid_clustering(structure_original) cluster_indices_list, cluster_symmetries = self._filter_clusters(clusters) - # Add all meaningful clusters to the topology topologies.append(original) for indices, symm in zip(cluster_indices_list, cluster_symmetries): @@ -579,9 +578,9 @@ class MaterialNormalizer(): continue topologies.append(subsystem) original = self._add_child_system(original, id_subsystem) - if subsystem.structural_type == 'surface': + if subsystem.structural_type == 'surface' or subsystem.structural_type == '2D': id_conv = f'/results/material/topology/{len(topologies)}' - symmsystem = self._create_conv_cell_system(symm, id_conv, id_subsystem) + symmsystem = self._create_conv_cell_system(symm, id_conv, id_subsystem, subsystem.structural_type) topologies.append(symmsystem) subsystem = self._add_child_system(subsystem, id_conv) @@ -589,8 +588,8 @@ class MaterialNormalizer(): # the topology is accepted and returned. TODO: This should be modified # in the future to also accept other kind of topologies besides # heterostructures. - if len([x for x in topologies if (x.structural_type in ('surface', '2D') and x.label == "subsystem")]) < 2: - return None + # if len([x for x in topologies if (x.structural_type in ('surface', '2D') and x.label == "subsystem")]) < 2: + # return None return topologies def _create_orig_topology(self, material: Material, top_id: str) -> Tuple[System, Structure]: @@ -648,7 +647,7 @@ class MaterialNormalizer(): subsystem.child_systems = parent_children_subsystem return subsystem - def _create_conv_cell_system(self, symm, top_id: str, parent_id: str): + def _create_conv_cell_system(self, symm, top_id: str, parent_id: str, structural_type: str): ''' Creates a new topology item for a conventional cell. ''' @@ -661,12 +660,21 @@ class MaterialNormalizer(): parent_system=parent_id ) conv_system = symm.get_conventional_system() - wyckoff_sets = symm.get_wyckoff_sets_conventional() - symmsystem.atoms = structure_from_ase_atoms(conv_system, wyckoff_sets, logger=self.logger) + if structural_type == 'surface': + wyckoff_sets = symm.get_wyckoff_sets_conventional() + symmsystem.atoms = structure_from_ase_atoms(conv_system, wyckoff_sets, logger=self.logger) + elif structural_type == '2D': + wyckoff_sets = None + symmsystem.atoms = structure_from_ase_atoms(conv_system, wyckoff_sets, logger=self.logger) + symmsystem.atoms.lattice_parameters.c = None + symmsystem.atoms.lattice_parameters.alpha = None + symmsystem.atoms.lattice_parameters.beta = None + symmsystem.atoms.cell_volume = None + subspecies = conv_system.get_chemical_symbols() symmsystem.structural_type = 'bulk' symmsystem = self._add_subsystem_properties(subspecies, symmsystem) - symmsystem = self._create_symmsystem(symm, symmsystem) + symmsystem = self._create_symmsystem(symm, symmsystem, structural_type) return symmsystem def _check_original_structure(self) -> Optional[Structure]: @@ -729,12 +737,15 @@ class MaterialNormalizer(): regions = cluster.regions number_of_atoms: List[int] = [] for region in regions: + # TODO: some how regions[0] is None if region: number_of_atoms.append(region.cell.get_number_of_atoms()) - + else: + number_of_atoms.append(-1) # TODO: What happens when there are 2 regions that have the same size? largest_region_index = number_of_atoms.index(max(number_of_atoms)) largest_region_system = regions[largest_region_index].cell + # TODO: only SymmetryAnalyzer for 2D and surface symm = SymmetryAnalyzer(largest_region_system) cluster_symmetries += [symm] @@ -748,6 +759,7 @@ class MaterialNormalizer(): Atom: 'atom', Class0D: 'molecule / cluster', Class1D: '1D', + Class2D: '2D', Surface: 'surface', Material2D: '2D', Unknown: 'unavailable'} @@ -787,13 +799,22 @@ class MaterialNormalizer(): subsystem.elements = elements return subsystem - def _create_symmsystem(self, symm: SymmetryAnalyzer, subsystem: System) -> System: + def _create_symmsystem(self, symm: SymmetryAnalyzer, subsystem: System, structural_type: str) -> System: """ Creates the subsystem with the symmetry information of the conventional cell """ conv_system = symm.get_conventional_system() subsystem.cell = cell_from_ase_atoms(conv_system) - symmetry = self._create_symmetry(subsystem, symm) + if structural_type == 'surface': + symmetry = self._create_symmetry(symm) + elif structural_type == '2D': + subsystem.cell.c = None + subsystem.cell.alpha = None + subsystem.cell.beta = None + subsystem.cell.volume = None + subsystem.cell.atomic_density = None + subsystem.cell.mass_density = None + symmetry = None subsystem.symmetry = symmetry prototype = self._create_prototype(symm, conv_system) spg_number = symm.get_space_group_number() @@ -803,7 +824,7 @@ class MaterialNormalizer(): subsystem.material_id = material_id return subsystem - def _create_symmetry(self, subsystem: System, symm: SymmetryAnalyzer) -> Symmetry: + def _create_symmetry(self, symm: SymmetryAnalyzer) -> Symmetry: international_short = symm.get_space_group_international_short() sec_symmetry = Symmetry() @@ -823,7 +844,11 @@ class MaterialNormalizer(): def _create_prototype(self, symm: SymmetryAnalyzer, conv_system: System) -> Prototype: spg_number = symm.get_space_group_number() atom_species = conv_system.get_atomic_numbers() - wyckoffs = conv_system.wyckoff_letters + #TODO: for some reasons conv_system comes back as Atoms or None + if type(conv_system) == Atoms or conv_system.wyckoff_letters is None: + wyckoffs = symm.get_wyckoff_letters_conventional() + else: + wyckoffs = conv_system.wyckoff_letters norm_wyckoff = atomutils.get_normalized_wyckoff(atom_species, wyckoffs) protoDict = atomutils.search_aflow_prototype(spg_number, norm_wyckoff) diff --git a/tests/normalizing/conftest.py b/tests/normalizing/conftest.py index d82dad4d2..417666e51 100644 --- a/tests/normalizing/conftest.py +++ b/tests/normalizing/conftest.py @@ -27,6 +27,7 @@ from nomad.utils import strip from nomad.units import ureg from nomad.normalizing import normalizers from nomad.datamodel import EntryArchive +from nomad.datamodel.results import System, Relation, Symmetry, Prototype, Cell, Structure from nomad.datamodel.metainfo.simulation.run import Run, Program from nomad.datamodel.metainfo.simulation.method import ( Method, BasisSet, Electronic, DFT, XCFunctional, Functional, @@ -823,3 +824,39 @@ def band_path_cF_nonstandard() -> EntryArchive: filepath = 'tests/data/normalizers/band_structure/cF_nonstandard/INFO.OUT' archive = parse_file((parser_name, filepath)) return run_normalize(archive) + +def create_system(label: str, + structural_type: str, + elements: List[str], + formula_hill: str, + formula_reduced: str, + formula_anonymous: str, + system_relation: Relation, + indices: List[int]=None, + material_id: str=None, + atoms: Structure=None, + cell: Cell=None, + symmetry: Symmetry=None, + prototype: Prototype=None) -> System: + from nomad.datamodel.results import System + system = System() + system.label = label + system.structural_type = structural_type + system.elements = elements + system.formula_hill = formula_hill + system.formula_reduced = formula_reduced + system.formula_anonymous = formula_anonymous + system.system_relation = system_relation + if label == 'subsystem': + system.indices = indices + elif label == 'conventional cell': + system.material_id = material_id + system.atoms = atoms + system.cell = cell + system.symmetry = Symmetry() + system.symmetry = symmetry + system.prototype = prototype + else: + from warnings import warn + warn('Warning: subsystem label is missing') + return system diff --git a/tests/normalizing/test_material.py b/tests/normalizing/test_material.py index 4fc491941..2f2e0423b 100644 --- a/tests/normalizing/test_material.py +++ b/tests/normalizing/test_material.py @@ -27,7 +27,9 @@ from matid.symmetry.wyckoffset import WyckoffSet from nomad.units import ureg from nomad import atomutils from nomad.utils import hash -from tests.normalizing.conftest import get_template_for_structure, get_template_topology +from nomad.datamodel.results import Symmetry, Cell, Prototype, Relation, Structure, LatticeParameters, WyckoffSet +from nomad.datamodel.optimade import Species +from tests.normalizing.conftest import get_template_for_structure, get_template_topology, create_system def assert_material(material): @@ -62,7 +64,8 @@ def assert_structure(structure, has_cell=True, has_wyckoff=False): assert structure.species[0].chemical_symbols if has_cell: assert len(structure.dimension_types) == 3 - assert np.sum(structure.dimension_types) == structure.nperiodic_dimensions + assert np.sum( + structure.dimension_types) == structure.nperiodic_dimensions assert structure.lattice_vectors.shape == (3, 3) a = structure.lattice_parameters.a b = structure.lattice_parameters.b @@ -145,9 +148,11 @@ def test_material_1d(one_d): assert conv.n_sites == 4 assert conv.species_at_sites == ["C", "C", "H", "H"] assert np.array_equal(conv.dimension_types, [1, 0, 0]) - assert conv.lattice_parameters.a.to(ureg.angstrom).magnitude == pytest.approx(2.459, abs=1e-3) + assert conv.lattice_parameters.a.to( + ureg.angstrom).magnitude == pytest.approx(2.459, abs=1e-3) assert conv.lattice_parameters.b.to(ureg.angstrom).magnitude == 0 - assert conv.lattice_parameters.c.to(ureg.angstrom).magnitude == pytest.approx(2.890, abs=1e-3) + assert conv.lattice_parameters.c.to( + ureg.angstrom).magnitude == pytest.approx(2.890, abs=1e-3) assert conv.lattice_parameters.alpha is None assert conv.lattice_parameters.beta.magnitude == pytest.approx(np.pi / 2) assert conv.lattice_parameters.gamma is None @@ -182,12 +187,15 @@ def test_material_2d(two_d): assert conv.n_sites == 2 assert conv.species_at_sites == ["C", "C"] assert np.array_equal(conv.dimension_types, [1, 1, 0]) - assert conv.lattice_parameters.a.to(ureg.angstrom).magnitude == pytest.approx(2.461, abs=1e-3) - assert conv.lattice_parameters.b.to(ureg.angstrom).magnitude == pytest.approx(2.461, abs=1e-3) + assert conv.lattice_parameters.a.to( + ureg.angstrom).magnitude == pytest.approx(2.461, abs=1e-3) + assert conv.lattice_parameters.b.to( + ureg.angstrom).magnitude == pytest.approx(2.461, abs=1e-3) assert conv.lattice_parameters.c.to(ureg.angstrom).magnitude == 0 assert conv.lattice_parameters.alpha is None assert conv.lattice_parameters.beta is None - assert conv.lattice_parameters.gamma.magnitude == pytest.approx(120 / 180 * np.pi) + assert conv.lattice_parameters.gamma.magnitude == pytest.approx( + 120 / 180 * np.pi) # Original structure assert_structure(two_d.results.properties.structures.structure_original) @@ -232,11 +240,15 @@ def test_material_bulk(bulk): conv = bulk.results.properties.structures.structure_conventional assert_structure(conv, has_wyckoff=True) assert conv.n_sites == 8 - assert conv.species_at_sites == ["Si", "Si", "Si", "Si", "Si", "Si", "Si", "Si"] + assert conv.species_at_sites == [ + "Si", "Si", "Si", "Si", "Si", "Si", "Si", "Si"] assert np.array_equal(conv.dimension_types, [1, 1, 1]) - assert conv.lattice_parameters.a.to(ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) - assert conv.lattice_parameters.b.to(ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) - assert conv.lattice_parameters.c.to(ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) + assert conv.lattice_parameters.a.to( + ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) + assert conv.lattice_parameters.b.to( + ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) + assert conv.lattice_parameters.c.to( + ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) assert conv.lattice_parameters.alpha.magnitude == pytest.approx(np.pi / 2) assert conv.lattice_parameters.beta.magnitude == pytest.approx(np.pi / 2) assert conv.lattice_parameters.gamma.magnitude == pytest.approx(np.pi / 2) @@ -281,7 +293,8 @@ def test_1d_material_identification(): pos = nanotube4.get_positions() pos += 0.2 * np.random.rand(pos.shape[0], pos.shape[1]) nanotube4.set_positions(pos) - hash4 = get_template_for_structure(nanotube4).results.material.material_id + hash4 = get_template_for_structure( + nanotube4).results.material.material_id assert hash4 == hash1 # Too distorted copy should not match @@ -303,7 +316,8 @@ def test_2d_material_identification(): indices=[0, 1] )] space_group_number = 191 - norm_hash_string = atomutils.get_symmetry_string(space_group_number, wyckoff_sets, is_2d=True) + norm_hash_string = atomutils.get_symmetry_string( + space_group_number, wyckoff_sets, is_2d=True) graphene_material_id = hash(norm_hash_string) # Graphene orthogonal cell @@ -322,13 +336,15 @@ def test_2d_material_identification(): ], pbc=True ) - material_id = get_template_for_structure(graphene).results.material.material_id + material_id = get_template_for_structure( + graphene).results.material.material_id assert material_id == graphene_material_id # Graphene orthogonal supercell graphene2 = graphene.copy() graphene2 *= [2, 1, 2] - material_id = get_template_for_structure(graphene2).results.material.material_id + material_id = get_template_for_structure( + graphene2).results.material.material_id assert material_id == graphene_material_id # Graphene primitive cell @@ -345,7 +361,8 @@ def test_2d_material_identification(): ], pbc=True ) - material_id = get_template_for_structure(graphene3).results.material.material_id + material_id = get_template_for_structure( + graphene3).results.material.material_id assert material_id == graphene_material_id # Slightly distorted system should match @@ -355,7 +372,8 @@ def test_2d_material_identification(): pos = graphene4.get_positions() pos += 0.05 * np.random.rand(pos.shape[0], pos.shape[1]) graphene4.set_positions(pos) - material_id = get_template_for_structure(graphene4).results.material.material_id + material_id = get_template_for_structure( + graphene4).results.material.material_id assert material_id == graphene_material_id # Too distorted system should not match @@ -364,7 +382,8 @@ def test_2d_material_identification(): np.random.seed(4) pos += 1 * np.random.rand(pos.shape[0], pos.shape[1]) graphene5.set_positions(pos) - material_id = get_template_for_structure(graphene5).results.material.material_id + material_id = get_template_for_structure( + graphene5).results.material.material_id assert material_id != graphene_material_id # Expected information for MoS2. MoS2 has finite thichkness unlike @@ -388,7 +407,8 @@ def test_2d_material_identification(): ) ] space_group_number = 11 - norm_hash_string = atomutils.get_symmetry_string(space_group_number, wyckoff_sets, is_2d=True) + norm_hash_string = atomutils.get_symmetry_string( + space_group_number, wyckoff_sets, is_2d=True) mos2_material_id = hash(norm_hash_string) # MoS2 orthogonal cell @@ -409,30 +429,36 @@ def test_2d_material_identification(): ], pbc=True ) - material_id = get_template_for_structure(atoms).results.material.material_id + material_id = get_template_for_structure( + atoms).results.material.material_id assert material_id == mos2_material_id # MoS2 orthogonal supercell atoms *= [2, 3, 1] - material_id = get_template_for_structure(atoms).results.material.material_id + material_id = get_template_for_structure( + atoms).results.material.material_id assert material_id == mos2_material_id def test_bulk_material_identification(): # Original system - wurtzite = ase.build.bulk("SiC", crystalstructure="wurtzite", a=3.086, c=10.053) - material_id_wurtzite = get_template_for_structure(wurtzite).results.material.material_id + wurtzite = ase.build.bulk( + "SiC", crystalstructure="wurtzite", a=3.086, c=10.053) + material_id_wurtzite = get_template_for_structure( + wurtzite).results.material.material_id # Rotated wurtzite2 = wurtzite.copy() wurtzite2.rotate(90, "z", rotate_cell=True) - material_id = get_template_for_structure(wurtzite2).results.material.material_id + material_id = get_template_for_structure( + wurtzite2).results.material.material_id assert material_id == material_id_wurtzite # Supercell wurtzite3 = wurtzite.copy() wurtzite3 *= [2, 3, 1] - materia_id = get_template_for_structure(wurtzite3).results.material.material_id + materia_id = get_template_for_structure( + wurtzite3).results.material.material_id assert materia_id == material_id_wurtzite # Slightly distorted system should match @@ -442,7 +468,8 @@ def test_bulk_material_identification(): pos = wurtzite4.get_positions() pos += 0.05 * np.random.rand(pos.shape[0], pos.shape[1]) wurtzite4.set_positions(pos) - material_id = get_template_for_structure(wurtzite4).results.material.material_id + material_id = get_template_for_structure( + wurtzite4).results.material.material_id assert material_id == material_id_wurtzite # Too distorted system should not match @@ -451,7 +478,8 @@ def test_bulk_material_identification(): np.random.seed(4) pos += 1 * np.random.rand(pos.shape[0], pos.shape[1]) wurtzite5.set_positions(pos) - material_id = get_template_for_structure(wurtzite5).results.material.material_id + material_id = get_template_for_structure( + wurtzite5).results.material.material_id assert material_id != material_id_wurtzite @@ -552,13 +580,15 @@ def test_conventional_structure(atoms, expected): """ entry = get_template_for_structure(atoms) structure_conventional = entry.results.properties.structures.structure_conventional - pos = structure_conventional.cartesian_site_positions.to(ureg.angstrom).magnitude + pos = structure_conventional.cartesian_site_positions.to( + ureg.angstrom).magnitude cell = structure_conventional.lattice_vectors.to(ureg.angstrom).magnitude pbc = np.array(structure_conventional.dimension_types, dtype=bool) assert np.array_equal(pbc, expected.get_pbc()) assert np.allclose(pos, expected.get_positions()) - assert np.array_equal(structure_conventional.species_at_sites, expected.get_chemical_symbols()) + assert np.array_equal( + structure_conventional.species_at_sites, expected.get_chemical_symbols()) assert np.allclose(cell, expected.get_cell()) @@ -663,168 +693,539 @@ def test_no_topology(fixture, request): assert not entry.results.material.topology -@pytest.mark.parametrize('entry_id', [ - pytest.param('heterostructure_2d_1', id='heterostructure-three-2D'), - pytest.param('heterostructure_surface_1', id='heterostructure-surface-1'), - pytest.param('heterostructure_surface_2', id='heterostructure-surface-2'), -]) -def test_topology_matid(entry_id): - # Load test data - with open(f'tests/data/normalizers/topology/{entry_id}.json', 'r') as f: - test_data = load(f) - - # Get system values - ref_topology = test_data['topology'] - ref_labels = np.array(ref_topology[0]['atoms']['labels']) - ref_positions = np.array(ref_topology[0]['atoms']['positions']) - ref_lattice_vectors = ref_topology[0]['atoms']['lattice_vectors'] - ref_pbc = ref_topology[0]['atoms']['periodic'] - - # Create ase.atoms - atoms = Atoms( - symbols=ref_labels, - positions=ref_positions, - cell=ref_lattice_vectors, - pbc=ref_pbc - ) - # Parse ase.atoms and get calculated topology - entry_archive = get_template_for_structure(atoms) +Symmetry_fcc = Symmetry(bravais_lattice="cF", crystal_system="cubic", hall_number=523, + hall_symbol="-F 4 2 3", point_group="m-3m", space_group_number=225, space_group_symbol="Fm-3m") + +Symmetry_bcc = Symmetry(bravais_lattice="cI", crystal_system="cubic", hall_number=529, + hall_symbol="-I 4 2 3", point_group="m-3m", space_group_number=229, space_group_symbol="Im-3m") + +# single Cu surface +Cu_fcc_100 = ase.build.fcc100('Cu', (3, 5, 5), vacuum=10, periodic=True) +Cu_fcc_100.rattle(stdev=0.001, seed=None, rng=None) +Cu_fcc_110 = ase.build.fcc110('Cu', (3, 5, 5), vacuum=10, periodic=True) +Cu_fcc_110.rattle(stdev=0.001, seed=None, rng=None) +Cell_Cu_fcc = [Cell(a=3.610000000000001 * ureg.angstrom, + b=3.610000000000001 * ureg.angstrom, + c=3.610000000000001 * ureg.angstrom, + alpha=1.5707963267948966 * ureg.rad, + beta=1.5707963267948966 * ureg.rad, + gamma=1.5707963267948966 * ureg.rad)] +formula_hill_Cu_fcc = ["Cu4"] +prototype_Cu_fcc = Prototype() +prototype_Cu_fcc.aflow_id = "A_cF4_225_a" +prototype_Cu_fcc.assignment_method = "normalized-wyckoff" +prototype_Cu_fcc.label = "225-Cu-cF4" +prototype_Cu_fcc.formula = "Cu4" + +# create Cu topology system +label_Cu = 'subsystem' +structural_type_Cu = 'surface' +elements_Cu = ['Cu'] +formula_hill_Cu = 'Cu75' +formula_reduced_Cu = 'Cu75' +formula_anonymous_Cu = 'A75' +system_relation = Relation() +system_relation.type = "subsystem" +indices_Cu = [i for i in range(75)] + +subsystem_Cu = create_system(label_Cu, structural_type_Cu, elements_Cu, formula_hill_Cu, formula_reduced_Cu, formula_anonymous_Cu, system_relation, indices=indices_Cu) +topologies_Cu = [subsystem_Cu] + +label_Cu_conv = 'conventional cell' +structural_type_Cu_conv = 'bulk' +formula_hill_Cu_conv = 'Cu4' +formula_reduced_Cu_conv = 'Cu4' +formula_anonymous_Cu_conv = 'A4' +material_id_Cu_conv = "3M6onRRrQbutydx916-Y15I79Z_X" +atoms_Cu_conv = Structure() +atoms_Cu_conv.dimension_types = [0, 0, 0] +atoms_Cu_conv.lattice_vectors = [[3.609999999999999, 0.0, 0.0], [0.0, 3.609999999999999e-10, 0.0], [0.0, 0.0, 3.609999999999999]] * ureg.angstrom +atoms_Cu_conv.cartesian_site_positions = [[0.0, 0.0, 0.0], [0.0, 1.8049999999999995, 1.8049999999999995], [1.8049999999999995, 0.0, 1.8049999999999995], [1.8049999999999995, 1.8049999999999995, 0.0]] * ureg.angstrom +atoms_Cu_conv.species_at_sites = ["Cu", "Cu", "Cu", "Cu"] +atoms_Cu_conv.cell_volume = 4.704588099999991e-29 +species = Species() +species.name = "Cu" +species.chemical_symbols = ["Cu"] +species.concentration = [1.0] +atoms_Cu_conv.species = [species] +atoms_Cu_conv.lattice_parameters = LatticeParameters() +atoms_Cu_conv.lattice_parameters.a = 3.609999999999999 * ureg.angstrom +atoms_Cu_conv.lattice_parameters.b = 3.609999999999999 * ureg.angstrom +atoms_Cu_conv.lattice_parameters.c = 3.609999999999999 * ureg.angstrom +atoms_Cu_conv.lattice_parameters.alpha = 1.5707963267948966 * ureg.rad +atoms_Cu_conv.lattice_parameters.beta = 1.5707963267948966 * ureg.rad +atoms_Cu_conv.lattice_parameters.gamma = 1.5707963267948966 * ureg.rad +wyckoff_sets = WyckoffSet() +wyckoff_sets.wyckoff_letter = "a" +wyckoff_sets.indices = [0, 1, 2, 3] +wyckoff_sets.element = "Cu" +atoms_Cu_conv.wyckoff_sets = [wyckoff_sets] + +convsystem_Cu = create_system(label_Cu_conv, structural_type_Cu_conv, elements_Cu, formula_hill_Cu_conv, formula_reduced_Cu_conv, formula_anonymous_Cu_conv, system_relation, material_id=material_id_Cu_conv, atoms=atoms_Cu_conv, cell=Cell_Cu_fcc[0], symmetry=Symmetry_fcc, prototype=prototype_Cu_fcc) +topologies_Cu.append(convsystem_Cu) + +# single Cr surface +Cr_bcc_100 = ase.build.bcc100('Cr', (5, 5, 5), vacuum=10, periodic=True) +Cr_bcc_100.rattle(stdev=0.001, seed=None, rng=None) +Cr_bcc_110 = ase.build.bcc110('Cr', (5, 5, 5), vacuum=10, periodic=True) +Cr_bcc_110.rattle(stdev=0.001, seed=None, rng=None) +Cell_Cr_bcc = [Cell(a=2.8800000000000014 * ureg.angstrom, + b=2.8800000000000014 * ureg.angstrom, + c=2.8800000000000014 * ureg.angstrom, + alpha=1.5707963267948966 * ureg.rad, + beta=1.5707963267948966 * ureg.rad, + gamma=1.5707963267948966 * ureg.rad)] +formula_hill_Cr_bcc = ["Cr2"] +prototype_Cr_bcc = Prototype() +prototype_Cr_bcc.aflow_id = "A_cI2_229_a" +prototype_Cr_bcc.assignment_method = "normalized-wyckoff" +prototype_Cr_bcc.label = "229-W-cI2" +prototype_Cr_bcc.formula = "W2" + +# create Cr topology system +label = 'subsystem' +structural_type = 'surface' +elements = ['Cr'] +formula_hill = 'Cr125' +formula_reduced = 'Cr125' +formula_anonymous = 'A125' +system_relation = Relation() +system_relation.type = "subsystem" +indices = [i for i in range(75)] + +subsystem_Cr = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, indices=indices) +topologies_Cr = [subsystem_Cr] + +label = 'conventional cell' +structural_type = 'bulk' +elements = ['Cr'] +formula_hill = 'Cr2' +formula_reduced = 'Cr2' +formula_anonymous = 'A2' +material_id = "MDlo8h4C2Ppy-kLY9fHRovgnTN9T" +atoms = Structure() +atoms.dimension_types = [0, 0, 0] +atoms.lattice_vectors = [[2.8800000000000014, 0.0, 0.0], [0.0, 2.8800000000000014e-10, 0.0], [0.0, 0.0, 2.8800000000000014]] * ureg.angstrom +atoms.cartesian_site_positions = [[0.0, 0.0, 0.0], [1.4400000000000007, 1.4400000000000007, 1.4400000000000007]] * ureg.angstrom +atoms.species_at_sites = ["Cr", "Cr"] +atoms.cell_volume = 2.388787200000006e-29 +species = Species() +species.name = "Cr" +species.chemical_symbols = ["Cr"] +species.concentration = [1.0] +atoms.species = [species] +atoms.lattice_parameters = LatticeParameters() +atoms.lattice_parameters.a = 2.8800000000000014 * ureg.angstrom +atoms.lattice_parameters.b = 2.8800000000000014 * ureg.angstrom +atoms.lattice_parameters.c = 2.8800000000000014 * ureg.angstrom +atoms.lattice_parameters.alpha = 1.5707963267948966 * ureg.rad +atoms.lattice_parameters.beta = 1.5707963267948966 * ureg.rad +atoms.lattice_parameters.gamma = 1.5707963267948966 * ureg.rad +wyckoff_sets = WyckoffSet() +wyckoff_sets.wyckoff_letter = "a" +wyckoff_sets.indices = [0, 1] +wyckoff_sets.element = "Cr" +atoms.wyckoff_sets = [wyckoff_sets] + +convsystem_Cr = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, material_id=material_id, atoms=atoms, cell=Cell_Cr_bcc[0], symmetry=Symmetry_bcc, prototype=prototype_Cr_bcc) +topologies_Cr.append(convsystem_Cr) + +# single Ni surface +Ni_fcc_111 = ase.build.fcc111('Ni', (3, 5, 5), vacuum=None, periodic=False) +Ni_fcc_111.rattle(stdev=0.001, seed=None, rng=None) +Cell_Ni_fcc = [Cell(a=3.52 * ureg.angstrom, + b=3.52 * ureg.angstrom, + c=3.52 * ureg.angstrom, + alpha=1.5707963267948966 * ureg.rad, + beta=1.5707963267948966 * ureg.rad, + gamma=1.5707963267948966 * ureg.rad)] +formula_hill_Ni_fcc = ["Ni4"] + +# create Ni topology +label = 'subsystem' +structural_type = 'surface' +elements = ['Ni'] +formula_hill = 'Ni75' +formula_reduced = 'Ni75' +formula_anonymous = 'A75' +system_relation = Relation() +system_relation.type = "subsystem" +indices = [i for i in range(75, 150)] + +subsystem_Ni = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, indices=indices) +topologies_Ni = [subsystem_Ni] + +label = 'conventional cell' +structural_type = 'bulk' +elements = ['Ni'] +formula_hill = 'Ni4' +formula_reduced = 'Ni4' +formula_anonymous = 'A4' +material_id = "NdIWxnQzlp-aeP1IM2d8YJ04h6T0" +atoms = Structure() +atoms.dimension_types = [0, 0, 0] +atoms.lattice_vectors = [[3.52, 0.0, 0.0], [0.0, 3.52, 0.0], [0.0, 0.0, 3.52]] * ureg.angstrom +atoms.cartesian_site_positions = [[0.0, 0.0, 0.0], [0.0, 1.76, 1.76], [1.76, 0.0, 1.76], [1.76, 1.76, 0.0]] * ureg.angstrom +atoms.species_at_sites = ["Ni", "Ni", "Ni", "Ni"] +atoms.cell_volume = 4.3614208000000044e-29 +species = Species() +species.name = "Ni" +species.chemical_symbols = ["Ni"] +species.concentration = [1.0] +atoms.species = [species] +atoms.lattice_parameters = LatticeParameters() +atoms.lattice_parameters.a = 3.52 * ureg.angstrom +atoms.lattice_parameters.b = 3.52 * ureg.angstrom +atoms.lattice_parameters.c = 3.52 * ureg.angstrom +atoms.lattice_parameters.alpha = 1.5707963267948966 * ureg.rad +atoms.lattice_parameters.beta = 1.5707963267948966 * ureg.rad +atoms.lattice_parameters.gamma = 1.5707963267948966 * ureg.rad +wyckoff_sets = WyckoffSet() +wyckoff_sets.wyckoff_letter = "a" +wyckoff_sets.indices = [0, 1, 2, 3] +wyckoff_sets.element = "Ni" +atoms.wyckoff_sets = [wyckoff_sets] + +convsystem_Ni = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, material_id=material_id, atoms=atoms, cell=Cell_Ni_fcc[0], symmetry=Symmetry_fcc, prototype=prototype_Cu_fcc) +topologies_Ni.append(convsystem_Ni) + +# stacked Cu and Ni surface +Cu_fcc_111 = ase.build.fcc111('Cu', (3, 5, 5), vacuum=None, periodic=False) +Cu_fcc_111.rattle(stdev=0.001, seed=None, rng=None) +Ni_fcc_111 = ase.build.fcc111('Ni', (3, 5, 5), vacuum=None, periodic=False) +Ni_fcc_111.rattle(stdev=0.001, seed=None, rng=None) +CuNi_fcc_111 = ase.build.stack(Cu_fcc_111, Ni_fcc_111, axis=2, distance=2, maxstrain=2.4) +Cell_CuNi = Cell_Cu_fcc + Cell_Ni_fcc +formula_hill_CuNi = formula_hill_Cu_fcc + formula_hill_Ni_fcc + +# create stacked Cu and Ni surface topology +topologies_Cu_Ni = topologies_Cu + topologies_Ni + +#------------------------------------------------ +# 2D +# Graphene +symbols_C = ['C', 'C'] +positions_C = [[0.0, 0.0, 2.1712595], + [1.2338620706831436, -0.712370598651782, 2.1712595]] * ureg.angstrom +cell_C = [[1.2338620706831436, -2.137111795955346, 0.0], + [1.2338620706831436, 2.137111795955346, 0.0], + [0.0, 0.0, 8.685038]] * ureg.angstrom +system_C = Atoms( + symbols=symbols_C, + positions=positions_C, + cell=cell_C, + pbc=True +) +system_C.rattle(stdev=0.001, seed=None, rng=None) +C_2 = ase.build.surface(system_C, (0, 0, 1), layers=1, periodic=True) +C_4 = ase.build.stack(C_2, C_2, axis=0) +C_8 = ase.build.stack(C_4, C_4, axis=1) +C_16 = ase.build.stack(C_8, C_8, axis=0) +C_32 = ase.build.stack(C_16, C_16, axis=1) + +# create graphene topology +label_C = 'subsystem' +structural_type_C = '2D' +elements_C = ['C'] +formula_hill_C = 'C32' +formula_reduced_C = 'C32' +formula_anonymous_C = 'A32' +system_relation = Relation() +system_relation.type = "subsystem" +indices_C = [i for i in range(32)] + +subsystem_C = create_system(label_C, structural_type_C, elements_C, formula_hill_C, formula_reduced_C, formula_anonymous_C, system_relation, indices=indices_C) +topologies_C = [subsystem_C] + +label_C_conv = 'conventional cell' +structural_type_C_conv = 'bulk' +elements_C_conv = ['C'] +formula_hill_C_conv = 'C2' +formula_reduced_C_conv = 'C2' +formula_anonymous_C_conv = 'A2' +material_id_C_conv = "jdP9AhZIFuYhubLWkm2FPtEV5IZA" +atoms_C_conv = Structure() +atoms_C_conv.dimension_types = [1, 1, 0] + +atoms_C_conv.lattice_vectors = [[2.4677241413662866, 0.0, 0.0], + [-1.2338620706831433, 2.1371117959553457, 0.0], + [0.0, 0.0, 1]] * ureg.angstrom +atoms_C_conv.cartesian_site_positions = [ + [1.2338620706831433, 0.712370598651782, 0.5], + [-2.7636130944313266e-16, 1.4247411973035641, 0.5]] * ureg.angstrom +atoms_C_conv.species_at_sites = ["C", "C"] +species = Species() +species.name = "C" +species.chemical_symbols = ["C"] +species.concentration = [1.0] +atoms_C_conv.species = [species] +atoms_C_conv.lattice_parameters = LatticeParameters() +atoms_C_conv.lattice_parameters.a = 2.4677241413662866 * ureg.angstrom +atoms_C_conv.lattice_parameters.b = 2.4677241413662866 * ureg.angstrom +atoms_C_conv.lattice_parameters.gamma = 2.0943951023931957 * ureg.rad +atoms_C_conv.wyckoff_sets = None + +Cell_C_conv = [Cell(a=atoms_C_conv.lattice_parameters.a, + b=atoms_C_conv.lattice_parameters.b, + gamma=atoms_C_conv.lattice_parameters.gamma)] + +convsystem_C = create_system(label_C_conv, structural_type_C_conv, elements_C_conv, formula_hill_C_conv, formula_reduced_C_conv, formula_anonymous_C_conv, system_relation, material_id=material_id_C_conv, atoms=atoms_C_conv, cell=Cell_C_conv[0], symmetry=None, prototype=None) +topologies_C.append(convsystem_C) + + +# boron nitride +symbols_BN = ['B', 'N'] +positions_BN = [[1.2557999125000436, -0.7250364175302085, 6.200847], [0.0, 0.0, 6.200847]] * ureg.angstrom +cell_BN = [[1.2557999125000436, -2.1751092525906257, 0.0], + [1.2557999125000436, 2.1751092525906257, 0.0], + [0.0, 0.0, 8.267796]] * ureg.angstrom +system_BN = Atoms( + symbols=symbols_BN, + positions=positions_BN, + cell=cell_BN, + pbc=True +) +system_BN.rattle(stdev=0.001, seed=None, rng=None) +BN_2D = ase.build.surface(system_BN, (0, 0, 1), layers=1, periodic=True) +BN_4 = ase.build.stack(BN_2D, BN_2D, axis=0) +BN_8 = ase.build.stack(BN_4, BN_4, axis=1) +BN_16 = ase.build.stack(BN_8, BN_8, axis=0) +BN_32 = ase.build.stack(BN_16, BN_16, axis=1) + +# create boron nitrid topology +label = 'subsystem' +structural_type = '2D' +elements = ['B', 'N'] +formula_hill = 'B16N16' +formula_reduced = 'B16N16' +formula_anonymous = 'A16B16' +system_relation = Relation() +system_relation.type = "subsystem" +indices_BN = [i for i in range(32)] + +subsystem_BN = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, indices=indices_BN) +topologies_BN = [subsystem_BN] + +label = 'conventional cell' +structural_type = 'bulk' +elements = ['B', 'N'] +formula_hill = 'BN' +formula_reduced = 'BN' +formula_anonymous = 'AB' +material_id = "RxRsol0dp1vDkU7-pE3v2exglkpM" +atoms = Structure() +atoms.dimension_types = [1, 1, 0] +atoms.lattice_vectors = [[2.510266994011973, 0.0, 0.0], + [-1.2551334970059864, 2.1739549870959678, 0.0], + [0.0, 0.0, 1]] * ureg.angstrom +atoms.cartesian_site_positions = [[0.0, 0.0, 0.5], + [9.441836090972668e-18, 1.449303324730645, 0.5]] * ureg.angstrom +atoms.species_at_sites = ["B", "N"] +species_B = Species() +species_B.name = "B" +species_B.chemical_symbols = ["B"] +species_B.concentration = [1.0] +species_N = Species() +species_N.name = "N" +species_N.chemical_symbols = ["N"] +species_N.concentration = [1.0] +atoms.species = [species_B, species_N] +atoms.lattice_parameters = LatticeParameters() +atoms.lattice_parameters.a = 2.510266994011973 * ureg.angstrom +atoms.lattice_parameters.b = 2.510266994011973 * ureg.angstrom +atoms.lattice_parameters.gamma = 2.0943951023931957 * ureg.rad +atoms.wyckoff_sets = None + +Cell_BN = [Cell(a=atoms.lattice_parameters.a, + b=atoms.lattice_parameters.b, + c=atoms.lattice_parameters.c, + alpha=atoms.lattice_parameters.alpha, + beta=atoms.lattice_parameters.beta, + gamma=atoms.lattice_parameters.gamma)] +convsystem_BN = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, material_id=material_id, atoms=atoms, cell=Cell_BN[0], symmetry=None, prototype=None) +topologies_BN.append(convsystem_BN) + +# MoS2 +symbols_MoS2 = ['Mo', 'S', 'S'] +positions_MoS2 = [[0.0, 0.0, 9.063556323175761], + [1.5920332323422965, 0.9191608152516547, 10.62711264635152], + [1.5920332323422965, 0.9191608152516547, 7.5]] * ureg.angstrom +cell_MoS2 = [[3.184066464684593, 0.0, 0.0], + [-1.5920332323422965, 2.7574824457549643, 0.0], + [0.0, 0.0, 18.127112646351521]]* ureg.angstrom +system_MoS2 = Atoms( + symbols=symbols_MoS2, + positions=positions_MoS2, + cell=cell_MoS2, + pbc=True +) +system_MoS2.rattle(stdev=0.001, seed=None, rng=None) +MoS2_2D = ase.build.surface(system_MoS2, (1,1,0), layers=4, vacuum=None, periodic=True) +stacked_2D_MoS2 = ase.build.stack(MoS2_2D, MoS2_2D, axis=2, distance=2.5) +stacked_2D_MoS2_2 = ase.build.stack(stacked_2D_MoS2, stacked_2D_MoS2, axis=2) +stacked_2D_MoS2_3 = ase.build.stack(stacked_2D_MoS2, stacked_2D_MoS2, axis=0) + +# create MoS2 topology +label = 'subsystem' +structural_type = '2D' +elements = ['Mo', 'S'] +formula_hill = 'Mo16S32' +formula_reduced = 'Mo16S32' +formula_anonymous = 'A32B16' +system_relation = Relation() +system_relation.type = "subsystem" +indices_MoS2 = [i for i in range(48)] +subsystem_MoS2 = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, indices=indices_MoS2) +topologies_MoS2 = [subsystem_MoS2] + +label = 'conventional cell' +structural_type = 'bulk' +elements = ['Mo', 'S'] +formula_hill = 'MoS2' +formula_reduced = 'MoS2' +formula_anonymous = 'A2B' +material_id = "KV4aYm-S1VJOH-SKeXXuG8JkTiGF" +atoms = Structure() +atoms.dimension_types = [1, 1, 0] +atoms.lattice_vectors = [[3.253646631826119, 0.0, 0.0], + [-1.6268233159130596, 2.8177406380990937, 0.0], + [0.0, 0.0, 3.124912396241947]] * ureg.angstrom +atoms.cartesian_site_positions = [[0.0, 0.0, 1.562456198120974], + [1.626823332181293, 0.9392468699738958, 3.124912396241947], + [1.626823332181293, 0.9392468699738958, 0.0]] * ureg.angstrom +atoms.species_at_sites = ["Mo", "S", "S"] +species_Mo = Species() +species_Mo.name = "Mo" +species_Mo.chemical_symbols = ["Mo"] +species_Mo.concentration = [1.0] +species_S = Species() +species_S.name = "S" +species_S.chemical_symbols = ["S"] +species_S.concentration = [1.0] +atoms.species = [species_Mo, species_S] +atoms.lattice_parameters = LatticeParameters() +atoms.lattice_parameters.a = 3.253646631826119 * ureg.angstrom +atoms.lattice_parameters.b = 3.253646631826119 * ureg.angstrom +atoms.lattice_parameters.gamma = 2.0943951023931957 * ureg.rad +atoms.wyckoff_sets = None +Cell_MoS2 = [Cell(a=atoms.lattice_parameters.a, + b=atoms.lattice_parameters.b, + gamma=atoms.lattice_parameters.gamma)] +convsystem_MoS2 = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, material_id=material_id, atoms=atoms, cell=Cell_MoS2[0], symmetry=None, prototype=None) +topologies_MoS2.append(convsystem_MoS2) + +# stacked 2D of C and BN +stacked_C_BN = ase.build.stack(BN_32, C_32, axis=2, maxstrain=6.7) +stacked_C_BN = ase.build.surface(stacked_C_BN, (0, 0, 1), layers=1, vacuum=10, periodic=True) + +# create stacked C and BN topologies +indices_C_stack = [i for i in range(32, 64)] +subsystem_C_stack = create_system(label_C, structural_type_C, elements_C, formula_hill_C, formula_reduced_C, formula_anonymous_C, system_relation, indices=indices_C_stack) +topologies_C_stack = [subsystem_C_stack] + +convsystem_C_stack = create_system(label_C_conv, structural_type_C_conv, elements_C_conv, formula_hill_C_conv, formula_reduced_C_conv, formula_anonymous_C_conv, system_relation, material_id=material_id_C_conv, atoms=atoms_C_conv, cell=Cell_C_conv[0], symmetry=None, prototype=None) +topologies_C_stack.append(convsystem_C_stack) + +topologies_C_BN = topologies_C_stack + topologies_BN + + +@pytest.mark.parametrize('surface, ref_topologies', [ + pytest.param(Cu_fcc_100, topologies_Cu, + id='single surface Cu FCC 100'), + pytest.param(Cu_fcc_110, topologies_Cu, + id='single surface Cu FCC 110'), + pytest.param(Cr_bcc_100, topologies_Cr, + id='single surface Cr BCC 100'), + pytest.param(Cr_bcc_110, topologies_Cr, id='single surface Cr BCC 110'), + pytest.param(CuNi_fcc_111, topologies_Cu_Ni, id='stacked surfaces of Cu and Ni'), + pytest.param(C_32, topologies_C, id='single 2D layer of graphene'), + pytest.param(BN_32, topologies_BN, id='single 2D layer of BN'), + pytest.param(stacked_2D_MoS2_3, topologies_MoS2, id='single 2D layer of MoS2'), + pytest.param(stacked_C_BN, topologies_C_BN, id='stacked layer of BN and C') + ]) +def test_surface_2D_topology(surface, ref_topologies): + entry_archive = get_template_for_structure(surface) topology = entry_archive.results.material.topology - - number_of_systems = 1 - outlier_threshold = 1 - + subsystem_topologies = topology[1:] # Compare topology with reference system topology. topology[0] is the original system - for cluster in topology[1:]: - if cluster['label'] == 'subsystem': - ref_number_of_systems = assert_subsystem(cluster, ref_topology, outlier_threshold) - number_of_systems += 1 - if ref_number_of_systems is None: - continue - elif cluster['label'] == 'conventional cell': - assert_conventional_cell(cluster, ref_topology) - assert number_of_systems == ref_number_of_systems - - -def assert_subsystem(cluster, ref_topology, outlier_threshold): - elements = cluster['elements'] - formula_hill = cluster['formula_hill'] - indices = cluster['indices'] - system_type = cluster['structural_type'] - if len(indices[0]) <= outlier_threshold: - return None - similarity_value = [] - ref_number_of_systems = 1 - for ref_cluster in ref_topology[1:]: - if ref_cluster['label'] != 'subsystem': - similarity_value += [0] - continue - ref_number_of_systems += 1 - # Load reference cluster. Pass if system type is not a surface or 2D. - ref_system_type = ref_cluster['structural_type'] - assert ref_system_type in {'2D', 'surface'} - - ref_elements = ref_cluster['elements'] - ref_formula_hill = ref_cluster['formula_hill'] - ref_indices = ref_cluster['indices'] - # Similarity calculation - indices_overlap = set( - ref_indices).intersection(set(indices[0])) - indices_similarity = len( - indices_overlap) / len(ref_indices) > 0.90 - element_similarity = set(ref_elements) == set(elements) - formula_hill_similarity = ref_formula_hill == formula_hill - system_type_similarity = ref_system_type == system_type - - similarity_value += [indices_similarity + element_similarity - + formula_hill_similarity + system_type_similarity] - - # Get most similar reference cluster. +1 because 0 is the original system - max_similarity = similarity_value.index(max(similarity_value)) + 1 - topology_max_similarity = ref_topology[max_similarity] - - # Indices: passes if the index overlapp is great enough - ref_indices_most_similar = topology_max_similarity['indices'] - indices_overlap_most_similar = set( - ref_indices_most_similar).intersection(set(indices[0])) - assert len(indices_overlap_most_similar) / \ - len(ref_indices_most_similar) > 0.85 - - # Elements - assert set(topology_max_similarity['elements']) == set(elements) - - # Formula hill: passes if the deviation is smaller than 15% - if topology_max_similarity['formula_hill'] != formula_hill: - ref_element_quantity = Formula(topology_max_similarity['formula_hill']).count() - element_quantity = Formula(formula_hill).count() - diff = 0 - for element in ref_element_quantity.keys(): - diff += abs(ref_element_quantity[element] - element_quantity[element]) - deviation = diff / sum(ref_element_quantity.values()) - assert deviation < 0.15 - - # System type - assert topology_max_similarity['structural_type'] == system_type - - return ref_number_of_systems - - -def assert_conventional_cell(cluster, ref_topology): - elements = cluster['elements'] - formula_hill = cluster['formula_hill'] - material_id = cluster['material_id'] - cell = cluster['cell'] - symmetry = cluster['symmetry'].m_to_dict() - - similarity_value = [] - - for ref_cluster in ref_topology[1:]: - if ref_cluster['label'] != 'conventional cell': - similarity_value += [0] - continue - ref_elements = ref_cluster['elements'] - ref_formula_hill = ref_cluster['formula_hill'] - ref_material_id = ref_cluster['material_id'] - ref_cell = ref_cluster['cell'] - ref_symmetry = ref_cluster['symmetry'] - - element_similarity = set(ref_elements) == set(elements) - formula_hill_similarity = ref_formula_hill == formula_hill - material_id_similarity = ref_material_id == material_id - symmetrie_similarity = 0 - - # Cell - cell_similarity = np.allclose(list(cell.values()), list(ref_cell.values()), rtol=1e-05, atol=1e-12) - - # Symmetry - for ref_symmetry_property_key, ref_symmetry_property in ref_symmetry.items(): - symmetry_property = symmetry[ref_symmetry_property_key] - symmetrie_similarity += symmetry_property == ref_symmetry_property - - symmetrie_similarity = symmetrie_similarity / len(symmetry) - - similarity_value += [element_similarity + formula_hill_similarity + material_id_similarity + cell_similarity + symmetrie_similarity] - - if similarity_value == []: - return - - # TODO: For now, this is necessary to prevent some tests from failing. The algorithm calculates conventional cells that are most likely not correct. Therefore, these conventional cells are not included in the reference data, but are calculated nevertheless. To prevent the comparison of these conventional cells, I set a threshold for the similarity value for comparison. This should be removed as soon as the test data is more suitable! - if max(similarity_value) <= 3: - return - - # Get most similar reference cluster. +1 because 0 is the original system - max_similarity = similarity_value.index(max(similarity_value)) + 1 - topology_max_similarity = ref_topology[max_similarity] - - # Elements, formula hill, material id: - assert topology_max_similarity['elements'] == elements - assert topology_max_similarity['formula_hill'] == formula_hill - assert topology_max_similarity['material_id'] == material_id - - # Cell: - assert np.allclose(list(cell.values()), list(topology_max_similarity['cell'].values()), rtol=3e-03, atol=1e-12) - - # Symmetry: - for ref_symmetry_property_key, ref_symmetry_property in ref_symmetry.items(): - symmetry_property = symmetry[ref_symmetry_property_key] - assert symmetry_property == ref_symmetry_property + assert len(subsystem_topologies) == len(ref_topologies) + for subsystem_topology in subsystem_topologies: + formula_hill = subsystem_topology['formula_hill'] + for ref_top_counter, ref_topology in enumerate(ref_topologies): + if ref_topology['formula_hill'] == formula_hill: + ref_formula_hill = ref_topology['formula_hill'] + ref_index = ref_top_counter + break + ref_elements = ref_topologies[ref_index]['elements'] + elements = subsystem_topology['elements'] + assert elements == ref_elements + assert formula_hill == ref_formula_hill + + ref_structural_type = ref_topologies[ref_index]['structural_type'] + structural_type = subsystem_topology['structural_type'] + assert ref_structural_type == structural_type + + if subsystem_topology['label'] == 'conventional cell': + + # Cell + ref_cell = ref_topologies[ref_index]['cell'] + cell = subsystem_topology['cell'] + if ref_structural_type == '2D': + assert np.allclose(list(cell.values()), list(ref_cell.values()), rtol=1e-05, atol=1e-9) + else: + assert np.allclose(list(cell.values())[:6], list(ref_cell.values()), rtol=1e-05, atol=1e-9) + + # Symmetry + if ref_topologies[ref_index].symmetry: + symmetry = subsystem_topology['symmetry'].m_to_dict() + ref_symmetry = ref_topologies[ref_index]['symmetry'].m_to_dict() + for ref_symmetry_property_key, ref_symmetry_property in ref_symmetry.items(): + symmetry_property = symmetry[ref_symmetry_property_key] + assert ref_symmetry_property == symmetry_property + else: + assert subsystem_topology.symmetry == ref_topologies[ref_index].symmetry + + # Prototype + if ref_topologies[ref_index].prototype: + prototype = subsystem_topology['prototype'].m_to_dict() + ref_prototype = ref_topologies[ref_index]['prototype'].m_to_dict() + for ref_prototype_property_key, ref_prototype_property in ref_prototype.items(): + prototype_property = prototype[ref_prototype_property_key] + assert ref_prototype_property == prototype_property + else: + assert ref_topologies[ref_index].prototype == subsystem_topology.prototype + + # Atoms + atoms = subsystem_topology['atoms'].m_to_dict() + ref_atoms = ref_topologies[ref_index]['atoms'].m_to_dict() + for ref_atoms_property_key, ref_atoms_property in ref_atoms.items(): + atoms_property = atoms[ref_atoms_property_key] + if type(atoms_property) == list: + property = atoms_property[0] + if type(property) == list: + assert np.allclose(atoms_property, ref_atoms_property, rtol=1e-05, atol=1e-9) + elif type(property) == dict: + for property_keys, property_values in property.items(): + ref_property = ref_atoms_property[0][property_keys] + assert property_values == ref_property + elif type(atoms_property) == dict: + for property_keys, property_values in atoms_property.items(): + ref_property_value = ref_atoms_property[property_keys] + if type(property_values) == float: + assert np.allclose(property_values, ref_property_value, rtol=1e-05, atol=1e-9) + else: + assert ref_atoms_property == property_values + else: + if type(atoms_property) == float: + assert np.allclose(ref_atoms_property, atoms_property, rtol=1e-05, atol=1e-9) + else: + assert ref_atoms_property == atoms_property + + + elif subsystem_topology['label'] == 'subsystem': + # Indices: passes if the index overlapp is large enough + ref_indices = ref_topologies[ref_index].indices + indices = subsystem_topology['indices'][0] + indices_overlap = set(ref_indices).intersection(set(indices)) + assert len(indices_overlap) / \ + len(ref_indices) > 0.85 -- GitLab From 216f0dac4e63021136b1fcd1e24dc7d4848f64e5 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 028/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index e29f2c28d..50449a9de 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -237,6 +237,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From b8d8db309562e9702efbf632725eb05a71760e79 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 029/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 50449a9de..f79bbb4da 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -310,6 +310,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 2fdfdb9be348ee0426770f73716b1842d12b5328 Mon Sep 17 00:00:00 2001 From: Thea Denell Date: Tue, 1 Nov 2022 09:15:14 +0100 Subject: [PATCH 030/117] added new unit test data for surfaces and 2D structures and modified unit tests respectively --- nomad/normalizing/material.py | 57 ++- tests/normalizing/conftest.py | 37 ++ tests/normalizing/test_material.py | 785 ++++++++++++++++++++++------- 3 files changed, 671 insertions(+), 208 deletions(-) diff --git a/nomad/normalizing/material.py b/nomad/normalizing/material.py index beba2f130..19dcff4a8 100644 --- a/nomad/normalizing/material.py +++ b/nomad/normalizing/material.py @@ -27,9 +27,9 @@ import numpy as np import matid.geometry from matid.classification.structureclusterer import StructureClusterer from matid import Classifier -from matid.classifications import Class0D, Atom, Class1D, Material2D, Surface, Class3D, Unknown +from matid.classifications import Class0D, Atom, Class1D, Material2D, Surface, Class3D, Class2D, Unknown from matid.symmetry.symmetryanalyzer import SymmetryAnalyzer -from nomad.datamodel.results import Symmetry, Material, System, Relation, Structure, Prototype +from nomad.datamodel.results import Symmetry, Material, System, Relation, Structure, Prototype, Cell from nomad import atomutils from nomad.utils import hash from nomad.units import ureg @@ -568,7 +568,6 @@ class MaterialNormalizer(): clusters = self._perform_matid_clustering(structure_original) cluster_indices_list, cluster_symmetries = self._filter_clusters(clusters) - # Add all meaningful clusters to the topology topologies.append(original) for indices, symm in zip(cluster_indices_list, cluster_symmetries): @@ -579,9 +578,9 @@ class MaterialNormalizer(): continue topologies.append(subsystem) original = self._add_child_system(original, id_subsystem) - if subsystem.structural_type == 'surface': + if subsystem.structural_type == 'surface' or subsystem.structural_type == '2D': id_conv = f'/results/material/topology/{len(topologies)}' - symmsystem = self._create_conv_cell_system(symm, id_conv, id_subsystem) + symmsystem = self._create_conv_cell_system(symm, id_conv, id_subsystem, subsystem.structural_type) topologies.append(symmsystem) subsystem = self._add_child_system(subsystem, id_conv) @@ -589,8 +588,8 @@ class MaterialNormalizer(): # the topology is accepted and returned. TODO: This should be modified # in the future to also accept other kind of topologies besides # heterostructures. - if len([x for x in topologies if (x.structural_type in ('surface', '2D') and x.label == "subsystem")]) < 2: - return None + # if len([x for x in topologies if (x.structural_type in ('surface', '2D') and x.label == "subsystem")]) < 2: + # return None return topologies def _create_orig_topology(self, material: Material, top_id: str) -> Tuple[System, Structure]: @@ -648,7 +647,7 @@ class MaterialNormalizer(): subsystem.child_systems = parent_children_subsystem return subsystem - def _create_conv_cell_system(self, symm, top_id: str, parent_id: str): + def _create_conv_cell_system(self, symm, top_id: str, parent_id: str, structural_type: str): ''' Creates a new topology item for a conventional cell. ''' @@ -661,12 +660,21 @@ class MaterialNormalizer(): parent_system=parent_id ) conv_system = symm.get_conventional_system() - wyckoff_sets = symm.get_wyckoff_sets_conventional() - symmsystem.atoms = structure_from_ase_atoms(conv_system, wyckoff_sets, logger=self.logger) + if structural_type == 'surface': + wyckoff_sets = symm.get_wyckoff_sets_conventional() + symmsystem.atoms = structure_from_ase_atoms(conv_system, wyckoff_sets, logger=self.logger) + elif structural_type == '2D': + wyckoff_sets = None + symmsystem.atoms = structure_from_ase_atoms(conv_system, wyckoff_sets, logger=self.logger) + symmsystem.atoms.lattice_parameters.c = None + symmsystem.atoms.lattice_parameters.alpha = None + symmsystem.atoms.lattice_parameters.beta = None + symmsystem.atoms.cell_volume = None + subspecies = conv_system.get_chemical_symbols() symmsystem.structural_type = 'bulk' symmsystem = self._add_subsystem_properties(subspecies, symmsystem) - symmsystem = self._create_symmsystem(symm, symmsystem) + symmsystem = self._create_symmsystem(symm, symmsystem, structural_type) return symmsystem def _check_original_structure(self) -> Optional[Structure]: @@ -729,12 +737,15 @@ class MaterialNormalizer(): regions = cluster.regions number_of_atoms: List[int] = [] for region in regions: + # TODO: some how regions[0] is None if region: number_of_atoms.append(region.cell.get_number_of_atoms()) - + else: + number_of_atoms.append(-1) # TODO: What happens when there are 2 regions that have the same size? largest_region_index = number_of_atoms.index(max(number_of_atoms)) largest_region_system = regions[largest_region_index].cell + # TODO: only SymmetryAnalyzer for 2D and surface symm = SymmetryAnalyzer(largest_region_system) cluster_symmetries += [symm] @@ -748,6 +759,7 @@ class MaterialNormalizer(): Atom: 'atom', Class0D: 'molecule / cluster', Class1D: '1D', + Class2D: '2D', Surface: 'surface', Material2D: '2D', Unknown: 'unavailable'} @@ -787,13 +799,22 @@ class MaterialNormalizer(): subsystem.elements = elements return subsystem - def _create_symmsystem(self, symm: SymmetryAnalyzer, subsystem: System) -> System: + def _create_symmsystem(self, symm: SymmetryAnalyzer, subsystem: System, structural_type: str) -> System: """ Creates the subsystem with the symmetry information of the conventional cell """ conv_system = symm.get_conventional_system() subsystem.cell = cell_from_ase_atoms(conv_system) - symmetry = self._create_symmetry(subsystem, symm) + if structural_type == 'surface': + symmetry = self._create_symmetry(symm) + elif structural_type == '2D': + subsystem.cell.c = None + subsystem.cell.alpha = None + subsystem.cell.beta = None + subsystem.cell.volume = None + subsystem.cell.atomic_density = None + subsystem.cell.mass_density = None + symmetry = None subsystem.symmetry = symmetry prototype = self._create_prototype(symm, conv_system) spg_number = symm.get_space_group_number() @@ -803,7 +824,7 @@ class MaterialNormalizer(): subsystem.material_id = material_id return subsystem - def _create_symmetry(self, subsystem: System, symm: SymmetryAnalyzer) -> Symmetry: + def _create_symmetry(self, symm: SymmetryAnalyzer) -> Symmetry: international_short = symm.get_space_group_international_short() sec_symmetry = Symmetry() @@ -823,7 +844,11 @@ class MaterialNormalizer(): def _create_prototype(self, symm: SymmetryAnalyzer, conv_system: System) -> Prototype: spg_number = symm.get_space_group_number() atom_species = conv_system.get_atomic_numbers() - wyckoffs = conv_system.wyckoff_letters + #TODO: for some reasons conv_system comes back as Atoms or None + if type(conv_system) == Atoms or conv_system.wyckoff_letters is None: + wyckoffs = symm.get_wyckoff_letters_conventional() + else: + wyckoffs = conv_system.wyckoff_letters norm_wyckoff = atomutils.get_normalized_wyckoff(atom_species, wyckoffs) protoDict = atomutils.search_aflow_prototype(spg_number, norm_wyckoff) diff --git a/tests/normalizing/conftest.py b/tests/normalizing/conftest.py index 9f92bc5c8..4b57800c7 100644 --- a/tests/normalizing/conftest.py +++ b/tests/normalizing/conftest.py @@ -29,6 +29,7 @@ from nomad.utils import strip from nomad.units import ureg from nomad.normalizing import normalizers from nomad.datamodel import EntryArchive +from nomad.datamodel.results import System, Relation, Symmetry, Prototype, Cell, Structure from nomad.datamodel.metainfo.simulation.run import Run, Program from nomad.datamodel.metainfo.simulation.method import ( Method, BasisSet, Electronic, DFT, XCFunctional, Functional, @@ -848,3 +849,39 @@ def band_path_cF_nonstandard() -> EntryArchive: filepath = 'tests/data/normalizers/band_structure/cF_nonstandard/INFO.OUT' archive = parse_file((parser_name, filepath)) return run_normalize(archive) + +def create_system(label: str, + structural_type: str, + elements: List[str], + formula_hill: str, + formula_reduced: str, + formula_anonymous: str, + system_relation: Relation, + indices: List[int]=None, + material_id: str=None, + atoms: Structure=None, + cell: Cell=None, + symmetry: Symmetry=None, + prototype: Prototype=None) -> System: + from nomad.datamodel.results import System + system = System() + system.label = label + system.structural_type = structural_type + system.elements = elements + system.formula_hill = formula_hill + system.formula_reduced = formula_reduced + system.formula_anonymous = formula_anonymous + system.system_relation = system_relation + if label == 'subsystem': + system.indices = indices + elif label == 'conventional cell': + system.material_id = material_id + system.atoms = atoms + system.cell = cell + system.symmetry = Symmetry() + system.symmetry = symmetry + system.prototype = prototype + else: + from warnings import warn + warn('Warning: subsystem label is missing') + return system diff --git a/tests/normalizing/test_material.py b/tests/normalizing/test_material.py index 4fc491941..2f2e0423b 100644 --- a/tests/normalizing/test_material.py +++ b/tests/normalizing/test_material.py @@ -27,7 +27,9 @@ from matid.symmetry.wyckoffset import WyckoffSet from nomad.units import ureg from nomad import atomutils from nomad.utils import hash -from tests.normalizing.conftest import get_template_for_structure, get_template_topology +from nomad.datamodel.results import Symmetry, Cell, Prototype, Relation, Structure, LatticeParameters, WyckoffSet +from nomad.datamodel.optimade import Species +from tests.normalizing.conftest import get_template_for_structure, get_template_topology, create_system def assert_material(material): @@ -62,7 +64,8 @@ def assert_structure(structure, has_cell=True, has_wyckoff=False): assert structure.species[0].chemical_symbols if has_cell: assert len(structure.dimension_types) == 3 - assert np.sum(structure.dimension_types) == structure.nperiodic_dimensions + assert np.sum( + structure.dimension_types) == structure.nperiodic_dimensions assert structure.lattice_vectors.shape == (3, 3) a = structure.lattice_parameters.a b = structure.lattice_parameters.b @@ -145,9 +148,11 @@ def test_material_1d(one_d): assert conv.n_sites == 4 assert conv.species_at_sites == ["C", "C", "H", "H"] assert np.array_equal(conv.dimension_types, [1, 0, 0]) - assert conv.lattice_parameters.a.to(ureg.angstrom).magnitude == pytest.approx(2.459, abs=1e-3) + assert conv.lattice_parameters.a.to( + ureg.angstrom).magnitude == pytest.approx(2.459, abs=1e-3) assert conv.lattice_parameters.b.to(ureg.angstrom).magnitude == 0 - assert conv.lattice_parameters.c.to(ureg.angstrom).magnitude == pytest.approx(2.890, abs=1e-3) + assert conv.lattice_parameters.c.to( + ureg.angstrom).magnitude == pytest.approx(2.890, abs=1e-3) assert conv.lattice_parameters.alpha is None assert conv.lattice_parameters.beta.magnitude == pytest.approx(np.pi / 2) assert conv.lattice_parameters.gamma is None @@ -182,12 +187,15 @@ def test_material_2d(two_d): assert conv.n_sites == 2 assert conv.species_at_sites == ["C", "C"] assert np.array_equal(conv.dimension_types, [1, 1, 0]) - assert conv.lattice_parameters.a.to(ureg.angstrom).magnitude == pytest.approx(2.461, abs=1e-3) - assert conv.lattice_parameters.b.to(ureg.angstrom).magnitude == pytest.approx(2.461, abs=1e-3) + assert conv.lattice_parameters.a.to( + ureg.angstrom).magnitude == pytest.approx(2.461, abs=1e-3) + assert conv.lattice_parameters.b.to( + ureg.angstrom).magnitude == pytest.approx(2.461, abs=1e-3) assert conv.lattice_parameters.c.to(ureg.angstrom).magnitude == 0 assert conv.lattice_parameters.alpha is None assert conv.lattice_parameters.beta is None - assert conv.lattice_parameters.gamma.magnitude == pytest.approx(120 / 180 * np.pi) + assert conv.lattice_parameters.gamma.magnitude == pytest.approx( + 120 / 180 * np.pi) # Original structure assert_structure(two_d.results.properties.structures.structure_original) @@ -232,11 +240,15 @@ def test_material_bulk(bulk): conv = bulk.results.properties.structures.structure_conventional assert_structure(conv, has_wyckoff=True) assert conv.n_sites == 8 - assert conv.species_at_sites == ["Si", "Si", "Si", "Si", "Si", "Si", "Si", "Si"] + assert conv.species_at_sites == [ + "Si", "Si", "Si", "Si", "Si", "Si", "Si", "Si"] assert np.array_equal(conv.dimension_types, [1, 1, 1]) - assert conv.lattice_parameters.a.to(ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) - assert conv.lattice_parameters.b.to(ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) - assert conv.lattice_parameters.c.to(ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) + assert conv.lattice_parameters.a.to( + ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) + assert conv.lattice_parameters.b.to( + ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) + assert conv.lattice_parameters.c.to( + ureg.angstrom).magnitude == pytest.approx(5.431, abs=1e-3) assert conv.lattice_parameters.alpha.magnitude == pytest.approx(np.pi / 2) assert conv.lattice_parameters.beta.magnitude == pytest.approx(np.pi / 2) assert conv.lattice_parameters.gamma.magnitude == pytest.approx(np.pi / 2) @@ -281,7 +293,8 @@ def test_1d_material_identification(): pos = nanotube4.get_positions() pos += 0.2 * np.random.rand(pos.shape[0], pos.shape[1]) nanotube4.set_positions(pos) - hash4 = get_template_for_structure(nanotube4).results.material.material_id + hash4 = get_template_for_structure( + nanotube4).results.material.material_id assert hash4 == hash1 # Too distorted copy should not match @@ -303,7 +316,8 @@ def test_2d_material_identification(): indices=[0, 1] )] space_group_number = 191 - norm_hash_string = atomutils.get_symmetry_string(space_group_number, wyckoff_sets, is_2d=True) + norm_hash_string = atomutils.get_symmetry_string( + space_group_number, wyckoff_sets, is_2d=True) graphene_material_id = hash(norm_hash_string) # Graphene orthogonal cell @@ -322,13 +336,15 @@ def test_2d_material_identification(): ], pbc=True ) - material_id = get_template_for_structure(graphene).results.material.material_id + material_id = get_template_for_structure( + graphene).results.material.material_id assert material_id == graphene_material_id # Graphene orthogonal supercell graphene2 = graphene.copy() graphene2 *= [2, 1, 2] - material_id = get_template_for_structure(graphene2).results.material.material_id + material_id = get_template_for_structure( + graphene2).results.material.material_id assert material_id == graphene_material_id # Graphene primitive cell @@ -345,7 +361,8 @@ def test_2d_material_identification(): ], pbc=True ) - material_id = get_template_for_structure(graphene3).results.material.material_id + material_id = get_template_for_structure( + graphene3).results.material.material_id assert material_id == graphene_material_id # Slightly distorted system should match @@ -355,7 +372,8 @@ def test_2d_material_identification(): pos = graphene4.get_positions() pos += 0.05 * np.random.rand(pos.shape[0], pos.shape[1]) graphene4.set_positions(pos) - material_id = get_template_for_structure(graphene4).results.material.material_id + material_id = get_template_for_structure( + graphene4).results.material.material_id assert material_id == graphene_material_id # Too distorted system should not match @@ -364,7 +382,8 @@ def test_2d_material_identification(): np.random.seed(4) pos += 1 * np.random.rand(pos.shape[0], pos.shape[1]) graphene5.set_positions(pos) - material_id = get_template_for_structure(graphene5).results.material.material_id + material_id = get_template_for_structure( + graphene5).results.material.material_id assert material_id != graphene_material_id # Expected information for MoS2. MoS2 has finite thichkness unlike @@ -388,7 +407,8 @@ def test_2d_material_identification(): ) ] space_group_number = 11 - norm_hash_string = atomutils.get_symmetry_string(space_group_number, wyckoff_sets, is_2d=True) + norm_hash_string = atomutils.get_symmetry_string( + space_group_number, wyckoff_sets, is_2d=True) mos2_material_id = hash(norm_hash_string) # MoS2 orthogonal cell @@ -409,30 +429,36 @@ def test_2d_material_identification(): ], pbc=True ) - material_id = get_template_for_structure(atoms).results.material.material_id + material_id = get_template_for_structure( + atoms).results.material.material_id assert material_id == mos2_material_id # MoS2 orthogonal supercell atoms *= [2, 3, 1] - material_id = get_template_for_structure(atoms).results.material.material_id + material_id = get_template_for_structure( + atoms).results.material.material_id assert material_id == mos2_material_id def test_bulk_material_identification(): # Original system - wurtzite = ase.build.bulk("SiC", crystalstructure="wurtzite", a=3.086, c=10.053) - material_id_wurtzite = get_template_for_structure(wurtzite).results.material.material_id + wurtzite = ase.build.bulk( + "SiC", crystalstructure="wurtzite", a=3.086, c=10.053) + material_id_wurtzite = get_template_for_structure( + wurtzite).results.material.material_id # Rotated wurtzite2 = wurtzite.copy() wurtzite2.rotate(90, "z", rotate_cell=True) - material_id = get_template_for_structure(wurtzite2).results.material.material_id + material_id = get_template_for_structure( + wurtzite2).results.material.material_id assert material_id == material_id_wurtzite # Supercell wurtzite3 = wurtzite.copy() wurtzite3 *= [2, 3, 1] - materia_id = get_template_for_structure(wurtzite3).results.material.material_id + materia_id = get_template_for_structure( + wurtzite3).results.material.material_id assert materia_id == material_id_wurtzite # Slightly distorted system should match @@ -442,7 +468,8 @@ def test_bulk_material_identification(): pos = wurtzite4.get_positions() pos += 0.05 * np.random.rand(pos.shape[0], pos.shape[1]) wurtzite4.set_positions(pos) - material_id = get_template_for_structure(wurtzite4).results.material.material_id + material_id = get_template_for_structure( + wurtzite4).results.material.material_id assert material_id == material_id_wurtzite # Too distorted system should not match @@ -451,7 +478,8 @@ def test_bulk_material_identification(): np.random.seed(4) pos += 1 * np.random.rand(pos.shape[0], pos.shape[1]) wurtzite5.set_positions(pos) - material_id = get_template_for_structure(wurtzite5).results.material.material_id + material_id = get_template_for_structure( + wurtzite5).results.material.material_id assert material_id != material_id_wurtzite @@ -552,13 +580,15 @@ def test_conventional_structure(atoms, expected): """ entry = get_template_for_structure(atoms) structure_conventional = entry.results.properties.structures.structure_conventional - pos = structure_conventional.cartesian_site_positions.to(ureg.angstrom).magnitude + pos = structure_conventional.cartesian_site_positions.to( + ureg.angstrom).magnitude cell = structure_conventional.lattice_vectors.to(ureg.angstrom).magnitude pbc = np.array(structure_conventional.dimension_types, dtype=bool) assert np.array_equal(pbc, expected.get_pbc()) assert np.allclose(pos, expected.get_positions()) - assert np.array_equal(structure_conventional.species_at_sites, expected.get_chemical_symbols()) + assert np.array_equal( + structure_conventional.species_at_sites, expected.get_chemical_symbols()) assert np.allclose(cell, expected.get_cell()) @@ -663,168 +693,539 @@ def test_no_topology(fixture, request): assert not entry.results.material.topology -@pytest.mark.parametrize('entry_id', [ - pytest.param('heterostructure_2d_1', id='heterostructure-three-2D'), - pytest.param('heterostructure_surface_1', id='heterostructure-surface-1'), - pytest.param('heterostructure_surface_2', id='heterostructure-surface-2'), -]) -def test_topology_matid(entry_id): - # Load test data - with open(f'tests/data/normalizers/topology/{entry_id}.json', 'r') as f: - test_data = load(f) - - # Get system values - ref_topology = test_data['topology'] - ref_labels = np.array(ref_topology[0]['atoms']['labels']) - ref_positions = np.array(ref_topology[0]['atoms']['positions']) - ref_lattice_vectors = ref_topology[0]['atoms']['lattice_vectors'] - ref_pbc = ref_topology[0]['atoms']['periodic'] - - # Create ase.atoms - atoms = Atoms( - symbols=ref_labels, - positions=ref_positions, - cell=ref_lattice_vectors, - pbc=ref_pbc - ) - # Parse ase.atoms and get calculated topology - entry_archive = get_template_for_structure(atoms) +Symmetry_fcc = Symmetry(bravais_lattice="cF", crystal_system="cubic", hall_number=523, + hall_symbol="-F 4 2 3", point_group="m-3m", space_group_number=225, space_group_symbol="Fm-3m") + +Symmetry_bcc = Symmetry(bravais_lattice="cI", crystal_system="cubic", hall_number=529, + hall_symbol="-I 4 2 3", point_group="m-3m", space_group_number=229, space_group_symbol="Im-3m") + +# single Cu surface +Cu_fcc_100 = ase.build.fcc100('Cu', (3, 5, 5), vacuum=10, periodic=True) +Cu_fcc_100.rattle(stdev=0.001, seed=None, rng=None) +Cu_fcc_110 = ase.build.fcc110('Cu', (3, 5, 5), vacuum=10, periodic=True) +Cu_fcc_110.rattle(stdev=0.001, seed=None, rng=None) +Cell_Cu_fcc = [Cell(a=3.610000000000001 * ureg.angstrom, + b=3.610000000000001 * ureg.angstrom, + c=3.610000000000001 * ureg.angstrom, + alpha=1.5707963267948966 * ureg.rad, + beta=1.5707963267948966 * ureg.rad, + gamma=1.5707963267948966 * ureg.rad)] +formula_hill_Cu_fcc = ["Cu4"] +prototype_Cu_fcc = Prototype() +prototype_Cu_fcc.aflow_id = "A_cF4_225_a" +prototype_Cu_fcc.assignment_method = "normalized-wyckoff" +prototype_Cu_fcc.label = "225-Cu-cF4" +prototype_Cu_fcc.formula = "Cu4" + +# create Cu topology system +label_Cu = 'subsystem' +structural_type_Cu = 'surface' +elements_Cu = ['Cu'] +formula_hill_Cu = 'Cu75' +formula_reduced_Cu = 'Cu75' +formula_anonymous_Cu = 'A75' +system_relation = Relation() +system_relation.type = "subsystem" +indices_Cu = [i for i in range(75)] + +subsystem_Cu = create_system(label_Cu, structural_type_Cu, elements_Cu, formula_hill_Cu, formula_reduced_Cu, formula_anonymous_Cu, system_relation, indices=indices_Cu) +topologies_Cu = [subsystem_Cu] + +label_Cu_conv = 'conventional cell' +structural_type_Cu_conv = 'bulk' +formula_hill_Cu_conv = 'Cu4' +formula_reduced_Cu_conv = 'Cu4' +formula_anonymous_Cu_conv = 'A4' +material_id_Cu_conv = "3M6onRRrQbutydx916-Y15I79Z_X" +atoms_Cu_conv = Structure() +atoms_Cu_conv.dimension_types = [0, 0, 0] +atoms_Cu_conv.lattice_vectors = [[3.609999999999999, 0.0, 0.0], [0.0, 3.609999999999999e-10, 0.0], [0.0, 0.0, 3.609999999999999]] * ureg.angstrom +atoms_Cu_conv.cartesian_site_positions = [[0.0, 0.0, 0.0], [0.0, 1.8049999999999995, 1.8049999999999995], [1.8049999999999995, 0.0, 1.8049999999999995], [1.8049999999999995, 1.8049999999999995, 0.0]] * ureg.angstrom +atoms_Cu_conv.species_at_sites = ["Cu", "Cu", "Cu", "Cu"] +atoms_Cu_conv.cell_volume = 4.704588099999991e-29 +species = Species() +species.name = "Cu" +species.chemical_symbols = ["Cu"] +species.concentration = [1.0] +atoms_Cu_conv.species = [species] +atoms_Cu_conv.lattice_parameters = LatticeParameters() +atoms_Cu_conv.lattice_parameters.a = 3.609999999999999 * ureg.angstrom +atoms_Cu_conv.lattice_parameters.b = 3.609999999999999 * ureg.angstrom +atoms_Cu_conv.lattice_parameters.c = 3.609999999999999 * ureg.angstrom +atoms_Cu_conv.lattice_parameters.alpha = 1.5707963267948966 * ureg.rad +atoms_Cu_conv.lattice_parameters.beta = 1.5707963267948966 * ureg.rad +atoms_Cu_conv.lattice_parameters.gamma = 1.5707963267948966 * ureg.rad +wyckoff_sets = WyckoffSet() +wyckoff_sets.wyckoff_letter = "a" +wyckoff_sets.indices = [0, 1, 2, 3] +wyckoff_sets.element = "Cu" +atoms_Cu_conv.wyckoff_sets = [wyckoff_sets] + +convsystem_Cu = create_system(label_Cu_conv, structural_type_Cu_conv, elements_Cu, formula_hill_Cu_conv, formula_reduced_Cu_conv, formula_anonymous_Cu_conv, system_relation, material_id=material_id_Cu_conv, atoms=atoms_Cu_conv, cell=Cell_Cu_fcc[0], symmetry=Symmetry_fcc, prototype=prototype_Cu_fcc) +topologies_Cu.append(convsystem_Cu) + +# single Cr surface +Cr_bcc_100 = ase.build.bcc100('Cr', (5, 5, 5), vacuum=10, periodic=True) +Cr_bcc_100.rattle(stdev=0.001, seed=None, rng=None) +Cr_bcc_110 = ase.build.bcc110('Cr', (5, 5, 5), vacuum=10, periodic=True) +Cr_bcc_110.rattle(stdev=0.001, seed=None, rng=None) +Cell_Cr_bcc = [Cell(a=2.8800000000000014 * ureg.angstrom, + b=2.8800000000000014 * ureg.angstrom, + c=2.8800000000000014 * ureg.angstrom, + alpha=1.5707963267948966 * ureg.rad, + beta=1.5707963267948966 * ureg.rad, + gamma=1.5707963267948966 * ureg.rad)] +formula_hill_Cr_bcc = ["Cr2"] +prototype_Cr_bcc = Prototype() +prototype_Cr_bcc.aflow_id = "A_cI2_229_a" +prototype_Cr_bcc.assignment_method = "normalized-wyckoff" +prototype_Cr_bcc.label = "229-W-cI2" +prototype_Cr_bcc.formula = "W2" + +# create Cr topology system +label = 'subsystem' +structural_type = 'surface' +elements = ['Cr'] +formula_hill = 'Cr125' +formula_reduced = 'Cr125' +formula_anonymous = 'A125' +system_relation = Relation() +system_relation.type = "subsystem" +indices = [i for i in range(75)] + +subsystem_Cr = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, indices=indices) +topologies_Cr = [subsystem_Cr] + +label = 'conventional cell' +structural_type = 'bulk' +elements = ['Cr'] +formula_hill = 'Cr2' +formula_reduced = 'Cr2' +formula_anonymous = 'A2' +material_id = "MDlo8h4C2Ppy-kLY9fHRovgnTN9T" +atoms = Structure() +atoms.dimension_types = [0, 0, 0] +atoms.lattice_vectors = [[2.8800000000000014, 0.0, 0.0], [0.0, 2.8800000000000014e-10, 0.0], [0.0, 0.0, 2.8800000000000014]] * ureg.angstrom +atoms.cartesian_site_positions = [[0.0, 0.0, 0.0], [1.4400000000000007, 1.4400000000000007, 1.4400000000000007]] * ureg.angstrom +atoms.species_at_sites = ["Cr", "Cr"] +atoms.cell_volume = 2.388787200000006e-29 +species = Species() +species.name = "Cr" +species.chemical_symbols = ["Cr"] +species.concentration = [1.0] +atoms.species = [species] +atoms.lattice_parameters = LatticeParameters() +atoms.lattice_parameters.a = 2.8800000000000014 * ureg.angstrom +atoms.lattice_parameters.b = 2.8800000000000014 * ureg.angstrom +atoms.lattice_parameters.c = 2.8800000000000014 * ureg.angstrom +atoms.lattice_parameters.alpha = 1.5707963267948966 * ureg.rad +atoms.lattice_parameters.beta = 1.5707963267948966 * ureg.rad +atoms.lattice_parameters.gamma = 1.5707963267948966 * ureg.rad +wyckoff_sets = WyckoffSet() +wyckoff_sets.wyckoff_letter = "a" +wyckoff_sets.indices = [0, 1] +wyckoff_sets.element = "Cr" +atoms.wyckoff_sets = [wyckoff_sets] + +convsystem_Cr = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, material_id=material_id, atoms=atoms, cell=Cell_Cr_bcc[0], symmetry=Symmetry_bcc, prototype=prototype_Cr_bcc) +topologies_Cr.append(convsystem_Cr) + +# single Ni surface +Ni_fcc_111 = ase.build.fcc111('Ni', (3, 5, 5), vacuum=None, periodic=False) +Ni_fcc_111.rattle(stdev=0.001, seed=None, rng=None) +Cell_Ni_fcc = [Cell(a=3.52 * ureg.angstrom, + b=3.52 * ureg.angstrom, + c=3.52 * ureg.angstrom, + alpha=1.5707963267948966 * ureg.rad, + beta=1.5707963267948966 * ureg.rad, + gamma=1.5707963267948966 * ureg.rad)] +formula_hill_Ni_fcc = ["Ni4"] + +# create Ni topology +label = 'subsystem' +structural_type = 'surface' +elements = ['Ni'] +formula_hill = 'Ni75' +formula_reduced = 'Ni75' +formula_anonymous = 'A75' +system_relation = Relation() +system_relation.type = "subsystem" +indices = [i for i in range(75, 150)] + +subsystem_Ni = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, indices=indices) +topologies_Ni = [subsystem_Ni] + +label = 'conventional cell' +structural_type = 'bulk' +elements = ['Ni'] +formula_hill = 'Ni4' +formula_reduced = 'Ni4' +formula_anonymous = 'A4' +material_id = "NdIWxnQzlp-aeP1IM2d8YJ04h6T0" +atoms = Structure() +atoms.dimension_types = [0, 0, 0] +atoms.lattice_vectors = [[3.52, 0.0, 0.0], [0.0, 3.52, 0.0], [0.0, 0.0, 3.52]] * ureg.angstrom +atoms.cartesian_site_positions = [[0.0, 0.0, 0.0], [0.0, 1.76, 1.76], [1.76, 0.0, 1.76], [1.76, 1.76, 0.0]] * ureg.angstrom +atoms.species_at_sites = ["Ni", "Ni", "Ni", "Ni"] +atoms.cell_volume = 4.3614208000000044e-29 +species = Species() +species.name = "Ni" +species.chemical_symbols = ["Ni"] +species.concentration = [1.0] +atoms.species = [species] +atoms.lattice_parameters = LatticeParameters() +atoms.lattice_parameters.a = 3.52 * ureg.angstrom +atoms.lattice_parameters.b = 3.52 * ureg.angstrom +atoms.lattice_parameters.c = 3.52 * ureg.angstrom +atoms.lattice_parameters.alpha = 1.5707963267948966 * ureg.rad +atoms.lattice_parameters.beta = 1.5707963267948966 * ureg.rad +atoms.lattice_parameters.gamma = 1.5707963267948966 * ureg.rad +wyckoff_sets = WyckoffSet() +wyckoff_sets.wyckoff_letter = "a" +wyckoff_sets.indices = [0, 1, 2, 3] +wyckoff_sets.element = "Ni" +atoms.wyckoff_sets = [wyckoff_sets] + +convsystem_Ni = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, material_id=material_id, atoms=atoms, cell=Cell_Ni_fcc[0], symmetry=Symmetry_fcc, prototype=prototype_Cu_fcc) +topologies_Ni.append(convsystem_Ni) + +# stacked Cu and Ni surface +Cu_fcc_111 = ase.build.fcc111('Cu', (3, 5, 5), vacuum=None, periodic=False) +Cu_fcc_111.rattle(stdev=0.001, seed=None, rng=None) +Ni_fcc_111 = ase.build.fcc111('Ni', (3, 5, 5), vacuum=None, periodic=False) +Ni_fcc_111.rattle(stdev=0.001, seed=None, rng=None) +CuNi_fcc_111 = ase.build.stack(Cu_fcc_111, Ni_fcc_111, axis=2, distance=2, maxstrain=2.4) +Cell_CuNi = Cell_Cu_fcc + Cell_Ni_fcc +formula_hill_CuNi = formula_hill_Cu_fcc + formula_hill_Ni_fcc + +# create stacked Cu and Ni surface topology +topologies_Cu_Ni = topologies_Cu + topologies_Ni + +#------------------------------------------------ +# 2D +# Graphene +symbols_C = ['C', 'C'] +positions_C = [[0.0, 0.0, 2.1712595], + [1.2338620706831436, -0.712370598651782, 2.1712595]] * ureg.angstrom +cell_C = [[1.2338620706831436, -2.137111795955346, 0.0], + [1.2338620706831436, 2.137111795955346, 0.0], + [0.0, 0.0, 8.685038]] * ureg.angstrom +system_C = Atoms( + symbols=symbols_C, + positions=positions_C, + cell=cell_C, + pbc=True +) +system_C.rattle(stdev=0.001, seed=None, rng=None) +C_2 = ase.build.surface(system_C, (0, 0, 1), layers=1, periodic=True) +C_4 = ase.build.stack(C_2, C_2, axis=0) +C_8 = ase.build.stack(C_4, C_4, axis=1) +C_16 = ase.build.stack(C_8, C_8, axis=0) +C_32 = ase.build.stack(C_16, C_16, axis=1) + +# create graphene topology +label_C = 'subsystem' +structural_type_C = '2D' +elements_C = ['C'] +formula_hill_C = 'C32' +formula_reduced_C = 'C32' +formula_anonymous_C = 'A32' +system_relation = Relation() +system_relation.type = "subsystem" +indices_C = [i for i in range(32)] + +subsystem_C = create_system(label_C, structural_type_C, elements_C, formula_hill_C, formula_reduced_C, formula_anonymous_C, system_relation, indices=indices_C) +topologies_C = [subsystem_C] + +label_C_conv = 'conventional cell' +structural_type_C_conv = 'bulk' +elements_C_conv = ['C'] +formula_hill_C_conv = 'C2' +formula_reduced_C_conv = 'C2' +formula_anonymous_C_conv = 'A2' +material_id_C_conv = "jdP9AhZIFuYhubLWkm2FPtEV5IZA" +atoms_C_conv = Structure() +atoms_C_conv.dimension_types = [1, 1, 0] + +atoms_C_conv.lattice_vectors = [[2.4677241413662866, 0.0, 0.0], + [-1.2338620706831433, 2.1371117959553457, 0.0], + [0.0, 0.0, 1]] * ureg.angstrom +atoms_C_conv.cartesian_site_positions = [ + [1.2338620706831433, 0.712370598651782, 0.5], + [-2.7636130944313266e-16, 1.4247411973035641, 0.5]] * ureg.angstrom +atoms_C_conv.species_at_sites = ["C", "C"] +species = Species() +species.name = "C" +species.chemical_symbols = ["C"] +species.concentration = [1.0] +atoms_C_conv.species = [species] +atoms_C_conv.lattice_parameters = LatticeParameters() +atoms_C_conv.lattice_parameters.a = 2.4677241413662866 * ureg.angstrom +atoms_C_conv.lattice_parameters.b = 2.4677241413662866 * ureg.angstrom +atoms_C_conv.lattice_parameters.gamma = 2.0943951023931957 * ureg.rad +atoms_C_conv.wyckoff_sets = None + +Cell_C_conv = [Cell(a=atoms_C_conv.lattice_parameters.a, + b=atoms_C_conv.lattice_parameters.b, + gamma=atoms_C_conv.lattice_parameters.gamma)] + +convsystem_C = create_system(label_C_conv, structural_type_C_conv, elements_C_conv, formula_hill_C_conv, formula_reduced_C_conv, formula_anonymous_C_conv, system_relation, material_id=material_id_C_conv, atoms=atoms_C_conv, cell=Cell_C_conv[0], symmetry=None, prototype=None) +topologies_C.append(convsystem_C) + + +# boron nitride +symbols_BN = ['B', 'N'] +positions_BN = [[1.2557999125000436, -0.7250364175302085, 6.200847], [0.0, 0.0, 6.200847]] * ureg.angstrom +cell_BN = [[1.2557999125000436, -2.1751092525906257, 0.0], + [1.2557999125000436, 2.1751092525906257, 0.0], + [0.0, 0.0, 8.267796]] * ureg.angstrom +system_BN = Atoms( + symbols=symbols_BN, + positions=positions_BN, + cell=cell_BN, + pbc=True +) +system_BN.rattle(stdev=0.001, seed=None, rng=None) +BN_2D = ase.build.surface(system_BN, (0, 0, 1), layers=1, periodic=True) +BN_4 = ase.build.stack(BN_2D, BN_2D, axis=0) +BN_8 = ase.build.stack(BN_4, BN_4, axis=1) +BN_16 = ase.build.stack(BN_8, BN_8, axis=0) +BN_32 = ase.build.stack(BN_16, BN_16, axis=1) + +# create boron nitrid topology +label = 'subsystem' +structural_type = '2D' +elements = ['B', 'N'] +formula_hill = 'B16N16' +formula_reduced = 'B16N16' +formula_anonymous = 'A16B16' +system_relation = Relation() +system_relation.type = "subsystem" +indices_BN = [i for i in range(32)] + +subsystem_BN = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, indices=indices_BN) +topologies_BN = [subsystem_BN] + +label = 'conventional cell' +structural_type = 'bulk' +elements = ['B', 'N'] +formula_hill = 'BN' +formula_reduced = 'BN' +formula_anonymous = 'AB' +material_id = "RxRsol0dp1vDkU7-pE3v2exglkpM" +atoms = Structure() +atoms.dimension_types = [1, 1, 0] +atoms.lattice_vectors = [[2.510266994011973, 0.0, 0.0], + [-1.2551334970059864, 2.1739549870959678, 0.0], + [0.0, 0.0, 1]] * ureg.angstrom +atoms.cartesian_site_positions = [[0.0, 0.0, 0.5], + [9.441836090972668e-18, 1.449303324730645, 0.5]] * ureg.angstrom +atoms.species_at_sites = ["B", "N"] +species_B = Species() +species_B.name = "B" +species_B.chemical_symbols = ["B"] +species_B.concentration = [1.0] +species_N = Species() +species_N.name = "N" +species_N.chemical_symbols = ["N"] +species_N.concentration = [1.0] +atoms.species = [species_B, species_N] +atoms.lattice_parameters = LatticeParameters() +atoms.lattice_parameters.a = 2.510266994011973 * ureg.angstrom +atoms.lattice_parameters.b = 2.510266994011973 * ureg.angstrom +atoms.lattice_parameters.gamma = 2.0943951023931957 * ureg.rad +atoms.wyckoff_sets = None + +Cell_BN = [Cell(a=atoms.lattice_parameters.a, + b=atoms.lattice_parameters.b, + c=atoms.lattice_parameters.c, + alpha=atoms.lattice_parameters.alpha, + beta=atoms.lattice_parameters.beta, + gamma=atoms.lattice_parameters.gamma)] +convsystem_BN = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, material_id=material_id, atoms=atoms, cell=Cell_BN[0], symmetry=None, prototype=None) +topologies_BN.append(convsystem_BN) + +# MoS2 +symbols_MoS2 = ['Mo', 'S', 'S'] +positions_MoS2 = [[0.0, 0.0, 9.063556323175761], + [1.5920332323422965, 0.9191608152516547, 10.62711264635152], + [1.5920332323422965, 0.9191608152516547, 7.5]] * ureg.angstrom +cell_MoS2 = [[3.184066464684593, 0.0, 0.0], + [-1.5920332323422965, 2.7574824457549643, 0.0], + [0.0, 0.0, 18.127112646351521]]* ureg.angstrom +system_MoS2 = Atoms( + symbols=symbols_MoS2, + positions=positions_MoS2, + cell=cell_MoS2, + pbc=True +) +system_MoS2.rattle(stdev=0.001, seed=None, rng=None) +MoS2_2D = ase.build.surface(system_MoS2, (1,1,0), layers=4, vacuum=None, periodic=True) +stacked_2D_MoS2 = ase.build.stack(MoS2_2D, MoS2_2D, axis=2, distance=2.5) +stacked_2D_MoS2_2 = ase.build.stack(stacked_2D_MoS2, stacked_2D_MoS2, axis=2) +stacked_2D_MoS2_3 = ase.build.stack(stacked_2D_MoS2, stacked_2D_MoS2, axis=0) + +# create MoS2 topology +label = 'subsystem' +structural_type = '2D' +elements = ['Mo', 'S'] +formula_hill = 'Mo16S32' +formula_reduced = 'Mo16S32' +formula_anonymous = 'A32B16' +system_relation = Relation() +system_relation.type = "subsystem" +indices_MoS2 = [i for i in range(48)] +subsystem_MoS2 = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, indices=indices_MoS2) +topologies_MoS2 = [subsystem_MoS2] + +label = 'conventional cell' +structural_type = 'bulk' +elements = ['Mo', 'S'] +formula_hill = 'MoS2' +formula_reduced = 'MoS2' +formula_anonymous = 'A2B' +material_id = "KV4aYm-S1VJOH-SKeXXuG8JkTiGF" +atoms = Structure() +atoms.dimension_types = [1, 1, 0] +atoms.lattice_vectors = [[3.253646631826119, 0.0, 0.0], + [-1.6268233159130596, 2.8177406380990937, 0.0], + [0.0, 0.0, 3.124912396241947]] * ureg.angstrom +atoms.cartesian_site_positions = [[0.0, 0.0, 1.562456198120974], + [1.626823332181293, 0.9392468699738958, 3.124912396241947], + [1.626823332181293, 0.9392468699738958, 0.0]] * ureg.angstrom +atoms.species_at_sites = ["Mo", "S", "S"] +species_Mo = Species() +species_Mo.name = "Mo" +species_Mo.chemical_symbols = ["Mo"] +species_Mo.concentration = [1.0] +species_S = Species() +species_S.name = "S" +species_S.chemical_symbols = ["S"] +species_S.concentration = [1.0] +atoms.species = [species_Mo, species_S] +atoms.lattice_parameters = LatticeParameters() +atoms.lattice_parameters.a = 3.253646631826119 * ureg.angstrom +atoms.lattice_parameters.b = 3.253646631826119 * ureg.angstrom +atoms.lattice_parameters.gamma = 2.0943951023931957 * ureg.rad +atoms.wyckoff_sets = None +Cell_MoS2 = [Cell(a=atoms.lattice_parameters.a, + b=atoms.lattice_parameters.b, + gamma=atoms.lattice_parameters.gamma)] +convsystem_MoS2 = create_system(label, structural_type, elements, formula_hill, formula_reduced, formula_anonymous, system_relation, material_id=material_id, atoms=atoms, cell=Cell_MoS2[0], symmetry=None, prototype=None) +topologies_MoS2.append(convsystem_MoS2) + +# stacked 2D of C and BN +stacked_C_BN = ase.build.stack(BN_32, C_32, axis=2, maxstrain=6.7) +stacked_C_BN = ase.build.surface(stacked_C_BN, (0, 0, 1), layers=1, vacuum=10, periodic=True) + +# create stacked C and BN topologies +indices_C_stack = [i for i in range(32, 64)] +subsystem_C_stack = create_system(label_C, structural_type_C, elements_C, formula_hill_C, formula_reduced_C, formula_anonymous_C, system_relation, indices=indices_C_stack) +topologies_C_stack = [subsystem_C_stack] + +convsystem_C_stack = create_system(label_C_conv, structural_type_C_conv, elements_C_conv, formula_hill_C_conv, formula_reduced_C_conv, formula_anonymous_C_conv, system_relation, material_id=material_id_C_conv, atoms=atoms_C_conv, cell=Cell_C_conv[0], symmetry=None, prototype=None) +topologies_C_stack.append(convsystem_C_stack) + +topologies_C_BN = topologies_C_stack + topologies_BN + + +@pytest.mark.parametrize('surface, ref_topologies', [ + pytest.param(Cu_fcc_100, topologies_Cu, + id='single surface Cu FCC 100'), + pytest.param(Cu_fcc_110, topologies_Cu, + id='single surface Cu FCC 110'), + pytest.param(Cr_bcc_100, topologies_Cr, + id='single surface Cr BCC 100'), + pytest.param(Cr_bcc_110, topologies_Cr, id='single surface Cr BCC 110'), + pytest.param(CuNi_fcc_111, topologies_Cu_Ni, id='stacked surfaces of Cu and Ni'), + pytest.param(C_32, topologies_C, id='single 2D layer of graphene'), + pytest.param(BN_32, topologies_BN, id='single 2D layer of BN'), + pytest.param(stacked_2D_MoS2_3, topologies_MoS2, id='single 2D layer of MoS2'), + pytest.param(stacked_C_BN, topologies_C_BN, id='stacked layer of BN and C') + ]) +def test_surface_2D_topology(surface, ref_topologies): + entry_archive = get_template_for_structure(surface) topology = entry_archive.results.material.topology - - number_of_systems = 1 - outlier_threshold = 1 - + subsystem_topologies = topology[1:] # Compare topology with reference system topology. topology[0] is the original system - for cluster in topology[1:]: - if cluster['label'] == 'subsystem': - ref_number_of_systems = assert_subsystem(cluster, ref_topology, outlier_threshold) - number_of_systems += 1 - if ref_number_of_systems is None: - continue - elif cluster['label'] == 'conventional cell': - assert_conventional_cell(cluster, ref_topology) - assert number_of_systems == ref_number_of_systems - - -def assert_subsystem(cluster, ref_topology, outlier_threshold): - elements = cluster['elements'] - formula_hill = cluster['formula_hill'] - indices = cluster['indices'] - system_type = cluster['structural_type'] - if len(indices[0]) <= outlier_threshold: - return None - similarity_value = [] - ref_number_of_systems = 1 - for ref_cluster in ref_topology[1:]: - if ref_cluster['label'] != 'subsystem': - similarity_value += [0] - continue - ref_number_of_systems += 1 - # Load reference cluster. Pass if system type is not a surface or 2D. - ref_system_type = ref_cluster['structural_type'] - assert ref_system_type in {'2D', 'surface'} - - ref_elements = ref_cluster['elements'] - ref_formula_hill = ref_cluster['formula_hill'] - ref_indices = ref_cluster['indices'] - # Similarity calculation - indices_overlap = set( - ref_indices).intersection(set(indices[0])) - indices_similarity = len( - indices_overlap) / len(ref_indices) > 0.90 - element_similarity = set(ref_elements) == set(elements) - formula_hill_similarity = ref_formula_hill == formula_hill - system_type_similarity = ref_system_type == system_type - - similarity_value += [indices_similarity + element_similarity - + formula_hill_similarity + system_type_similarity] - - # Get most similar reference cluster. +1 because 0 is the original system - max_similarity = similarity_value.index(max(similarity_value)) + 1 - topology_max_similarity = ref_topology[max_similarity] - - # Indices: passes if the index overlapp is great enough - ref_indices_most_similar = topology_max_similarity['indices'] - indices_overlap_most_similar = set( - ref_indices_most_similar).intersection(set(indices[0])) - assert len(indices_overlap_most_similar) / \ - len(ref_indices_most_similar) > 0.85 - - # Elements - assert set(topology_max_similarity['elements']) == set(elements) - - # Formula hill: passes if the deviation is smaller than 15% - if topology_max_similarity['formula_hill'] != formula_hill: - ref_element_quantity = Formula(topology_max_similarity['formula_hill']).count() - element_quantity = Formula(formula_hill).count() - diff = 0 - for element in ref_element_quantity.keys(): - diff += abs(ref_element_quantity[element] - element_quantity[element]) - deviation = diff / sum(ref_element_quantity.values()) - assert deviation < 0.15 - - # System type - assert topology_max_similarity['structural_type'] == system_type - - return ref_number_of_systems - - -def assert_conventional_cell(cluster, ref_topology): - elements = cluster['elements'] - formula_hill = cluster['formula_hill'] - material_id = cluster['material_id'] - cell = cluster['cell'] - symmetry = cluster['symmetry'].m_to_dict() - - similarity_value = [] - - for ref_cluster in ref_topology[1:]: - if ref_cluster['label'] != 'conventional cell': - similarity_value += [0] - continue - ref_elements = ref_cluster['elements'] - ref_formula_hill = ref_cluster['formula_hill'] - ref_material_id = ref_cluster['material_id'] - ref_cell = ref_cluster['cell'] - ref_symmetry = ref_cluster['symmetry'] - - element_similarity = set(ref_elements) == set(elements) - formula_hill_similarity = ref_formula_hill == formula_hill - material_id_similarity = ref_material_id == material_id - symmetrie_similarity = 0 - - # Cell - cell_similarity = np.allclose(list(cell.values()), list(ref_cell.values()), rtol=1e-05, atol=1e-12) - - # Symmetry - for ref_symmetry_property_key, ref_symmetry_property in ref_symmetry.items(): - symmetry_property = symmetry[ref_symmetry_property_key] - symmetrie_similarity += symmetry_property == ref_symmetry_property - - symmetrie_similarity = symmetrie_similarity / len(symmetry) - - similarity_value += [element_similarity + formula_hill_similarity + material_id_similarity + cell_similarity + symmetrie_similarity] - - if similarity_value == []: - return - - # TODO: For now, this is necessary to prevent some tests from failing. The algorithm calculates conventional cells that are most likely not correct. Therefore, these conventional cells are not included in the reference data, but are calculated nevertheless. To prevent the comparison of these conventional cells, I set a threshold for the similarity value for comparison. This should be removed as soon as the test data is more suitable! - if max(similarity_value) <= 3: - return - - # Get most similar reference cluster. +1 because 0 is the original system - max_similarity = similarity_value.index(max(similarity_value)) + 1 - topology_max_similarity = ref_topology[max_similarity] - - # Elements, formula hill, material id: - assert topology_max_similarity['elements'] == elements - assert topology_max_similarity['formula_hill'] == formula_hill - assert topology_max_similarity['material_id'] == material_id - - # Cell: - assert np.allclose(list(cell.values()), list(topology_max_similarity['cell'].values()), rtol=3e-03, atol=1e-12) - - # Symmetry: - for ref_symmetry_property_key, ref_symmetry_property in ref_symmetry.items(): - symmetry_property = symmetry[ref_symmetry_property_key] - assert symmetry_property == ref_symmetry_property + assert len(subsystem_topologies) == len(ref_topologies) + for subsystem_topology in subsystem_topologies: + formula_hill = subsystem_topology['formula_hill'] + for ref_top_counter, ref_topology in enumerate(ref_topologies): + if ref_topology['formula_hill'] == formula_hill: + ref_formula_hill = ref_topology['formula_hill'] + ref_index = ref_top_counter + break + ref_elements = ref_topologies[ref_index]['elements'] + elements = subsystem_topology['elements'] + assert elements == ref_elements + assert formula_hill == ref_formula_hill + + ref_structural_type = ref_topologies[ref_index]['structural_type'] + structural_type = subsystem_topology['structural_type'] + assert ref_structural_type == structural_type + + if subsystem_topology['label'] == 'conventional cell': + + # Cell + ref_cell = ref_topologies[ref_index]['cell'] + cell = subsystem_topology['cell'] + if ref_structural_type == '2D': + assert np.allclose(list(cell.values()), list(ref_cell.values()), rtol=1e-05, atol=1e-9) + else: + assert np.allclose(list(cell.values())[:6], list(ref_cell.values()), rtol=1e-05, atol=1e-9) + + # Symmetry + if ref_topologies[ref_index].symmetry: + symmetry = subsystem_topology['symmetry'].m_to_dict() + ref_symmetry = ref_topologies[ref_index]['symmetry'].m_to_dict() + for ref_symmetry_property_key, ref_symmetry_property in ref_symmetry.items(): + symmetry_property = symmetry[ref_symmetry_property_key] + assert ref_symmetry_property == symmetry_property + else: + assert subsystem_topology.symmetry == ref_topologies[ref_index].symmetry + + # Prototype + if ref_topologies[ref_index].prototype: + prototype = subsystem_topology['prototype'].m_to_dict() + ref_prototype = ref_topologies[ref_index]['prototype'].m_to_dict() + for ref_prototype_property_key, ref_prototype_property in ref_prototype.items(): + prototype_property = prototype[ref_prototype_property_key] + assert ref_prototype_property == prototype_property + else: + assert ref_topologies[ref_index].prototype == subsystem_topology.prototype + + # Atoms + atoms = subsystem_topology['atoms'].m_to_dict() + ref_atoms = ref_topologies[ref_index]['atoms'].m_to_dict() + for ref_atoms_property_key, ref_atoms_property in ref_atoms.items(): + atoms_property = atoms[ref_atoms_property_key] + if type(atoms_property) == list: + property = atoms_property[0] + if type(property) == list: + assert np.allclose(atoms_property, ref_atoms_property, rtol=1e-05, atol=1e-9) + elif type(property) == dict: + for property_keys, property_values in property.items(): + ref_property = ref_atoms_property[0][property_keys] + assert property_values == ref_property + elif type(atoms_property) == dict: + for property_keys, property_values in atoms_property.items(): + ref_property_value = ref_atoms_property[property_keys] + if type(property_values) == float: + assert np.allclose(property_values, ref_property_value, rtol=1e-05, atol=1e-9) + else: + assert ref_atoms_property == property_values + else: + if type(atoms_property) == float: + assert np.allclose(ref_atoms_property, atoms_property, rtol=1e-05, atol=1e-9) + else: + assert ref_atoms_property == atoms_property + + + elif subsystem_topology['label'] == 'subsystem': + # Indices: passes if the index overlapp is large enough + ref_indices = ref_topologies[ref_index].indices + indices = subsystem_topology['indices'][0] + indices_overlap = set(ref_indices).intersection(set(indices)) + assert len(indices_overlap) / \ + len(ref_indices) > 0.85 -- GitLab From 51c66cfd58c5623410e3c020846e30577932c7b8 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 031/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 26 ++++++- gui/src/config.js | 1 - 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index f79bbb4da..cac5fbc1a 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -383,6 +383,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 03bf3a89b..257ae0388 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -25,7 +25,6 @@ import { FilterMenuItems, FilterSubMenus } from './FilterMenu' -import { makeStyles } from '@material-ui/core/styles' import FilterSubMenuMaterial from './FilterSubMenuMaterial' import FilterSubMenuElements from './FilterSubMenuElements' import FilterSubMenuSymmetry from './FilterSubMenuSymmetry' @@ -82,6 +81,31 @@ const useFilterMainMenuStyles = makeStyles(theme => ({ } })) +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. diff --git a/gui/src/config.js b/gui/src/config.js index 7e463c078..e313994de 100644 --- a/gui/src/config.js +++ b/gui/src/config.js @@ -16,7 +16,6 @@ * limitations under the License. */ import { createTheme } from '@material-ui/core' -import { urlAbs } from './utils' window.nomadEnv = window.nomadEnv || {} export const version = window.nomadEnv.version -- GitLab From 206452a7e037e7fa80378858705439418b42fafa Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 032/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index cac5fbc1a..d2ff39d7a 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -456,6 +456,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 902bc0a9fe4b040daf084a261934914a0c966355 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 033/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index d2ff39d7a..d13bc10de 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -529,6 +529,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 49cb387c50aaa12c99075c385b7887266fb8aad0 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 034/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index d13bc10de..bc0d27ca8 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -602,6 +602,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 257ae0388..0f9c8e9f7 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -106,6 +106,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From 8f0e015af2bcabc7f48313b3a3b82dc17dbd2f31 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 035/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index bc0d27ca8..d9745d2e6 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -675,6 +675,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From dc154803e66c8f5e08ffeb74e7e9374376a2945b Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 036/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index d9745d2e6..2bd890624 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -748,6 +748,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 7a75db0475be4af7ea15af5631f4d507adbbecf1 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 037/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 2bd890624..359002950 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -821,6 +821,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 0f9c8e9f7..50c77bdf7 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -131,6 +131,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From 472aabce99c465f997d50fd792a5b5a67016dcd3 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 038/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 359002950..2965a9d03 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -894,6 +894,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 1c50de087934b39629d002537aaa1ef64da6dded Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 039/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 2965a9d03..5f2dcf710 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -967,6 +967,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 9b974dc10188d373bfad9463680856768949a0f3 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Tue, 1 Nov 2022 08:31:24 +0000 Subject: [PATCH 040/117] Added logic for updating entry data based on overview visibility. --- gui/src/components/entry/EntryPageContext.js | 27 +++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/gui/src/components/entry/EntryPageContext.js b/gui/src/components/entry/EntryPageContext.js index 73f5451cd..923c2a6ab 100644 --- a/gui/src/components/entry/EntryPageContext.js +++ b/gui/src/components/entry/EntryPageContext.js @@ -27,24 +27,39 @@ const entryPageContext = React.createContext() * Hook for fetching data from the current EntryPageContext. * * @param {*} requireArchive Optional query filter - * @param {*} update Whether to keep updating the data if changes are made in - * the store. Sometimes the first version of the data should be kept to avoid - * unnecessary re-renders and layout inconsistencies. + * @param {*} update Whether to keep updating the data for the original entry + * if changes beyond the first successfull archive load are made in the store. + * Sometimes the first version of the data should be kept to avoid unnecessary + * re-renders and layout inconsistencies. Note that if the entry id changes, + * the update is forced. * @returns */ export const useEntryPageContext = (requireArchive, update = true) => { const entryId = useContext(entryPageContext) const entryData = useEntryStoreObj(apiBase, entryId, true, requireArchive) const [data, setData] = useState(entryData) - const firstRender = useRef(true) + const oldEntryId = useRef() + const completed = useRef(false) + // This effect controls how the data returned by this hook is synchronized + // with the data coming from the store. If update = true, the data is always + // synchronized. If update = false, it is only synchronized until the archive + // data is fully set, or the entry changes. useEffect(() => { + const newEntryId = entryData?.entryId + const isNew = newEntryId !== oldEntryId.current if (update) { setData(entryData) - } else if (firstRender.current && entryData.archive) { + } else if (isNew || !completed.current) { setData(entryData) - firstRender.current = false + if (isNew) { + completed.current = false + } + if (entryData?.archive) { + completed.current = true + } } + oldEntryId.current = newEntryId }, [entryData, update]) return data -- GitLab From c0e95991f18146cde831e01b990f3008750812ea Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 041/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 7f46fc02d..8c5a90401 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1141,6 +1141,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From ef51dfeaeb961d3e1c2011db1d37a43a1dff407f Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 042/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 8c5a90401..be4311c5b 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1214,6 +1214,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 200dfc8809220a1aca617706b3f0bf7e467956db Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 043/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index be4311c5b..434d42400 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1287,6 +1287,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 0c4985227b8ba8c180dc517112ed5ccad66115d4 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 044/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 434d42400..3d1907046 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1360,6 +1360,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 5290f5248a71484015fb1e4cbf845e4ae6fae0c3 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 045/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 3d1907046..ad8f8e5c5 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1433,6 +1433,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 061df7650372dfd4d53eacc63bcb6e5eab933112 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 046/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index ad8f8e5c5..9c3c5fd8a 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1506,6 +1506,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 5b4a16103..101f9a018 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -156,6 +156,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From 29a10f7d4d164c6b1a8de7aa8edb1e315795dbff Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 047/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 9c3c5fd8a..cf02f04b2 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1579,6 +1579,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 173fbd022510289239ed6b014653e1295027963d Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 048/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index cf02f04b2..5c0969633 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1652,6 +1652,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 941763cb4b643862c6a2d76b0a7dab6b3b07563f Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 049/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 5c0969633..f0d730842 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1725,6 +1725,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 101f9a018..0b0ba1691 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -181,6 +181,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From a6aa03368f317154d329d809c06aebbac72b7861 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 050/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index f0d730842..f7c24234f 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1798,6 +1798,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From f04698373d0fc9bec92ecd69f1bec402b279efc7 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 051/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index f7c24234f..59730f6da 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1871,6 +1871,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From e78126bc252caa233a2089269b0291a35f27615c Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 052/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 59730f6da..ede9dac4f 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -1944,6 +1944,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 33cfe1f884486f73764f0cd7de2492dddfa16201 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 053/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index ede9dac4f..b8b77ebf0 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2017,6 +2017,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 356ab64e676493a68a5efa455b4ce526aadff9a2 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 054/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index b8b77ebf0..1f3730da2 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2090,6 +2090,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 6338c0e62d6580b435669fc5be7624335c918fef Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 055/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 1f3730da2..b74347c8d 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2163,6 +2163,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 451a01cc2d41c558b26b3325cfc8ee589200388a Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 056/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index b74347c8d..5f090b832 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2236,6 +2236,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From b50c3146f58a372aca5fc1b3f0f2f8a17a1d6d7e Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 057/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 5f090b832..55d3de235 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2309,6 +2309,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 0b0ba1691..a1c4824c4 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -206,6 +206,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From 2c4b70cebf5d4ea15e9c06190ab0fcf988b71cd9 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 058/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 55d3de235..f38e7239b 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2382,6 +2382,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 545b7e865e547a6077f496a427a996052b32fd6e Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 059/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index f38e7239b..8ea164822 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2455,6 +2455,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 79feaeb1d14b236119a8eda46771c6ed28aabb4a Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 060/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 8ea164822..1354e0995 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2528,6 +2528,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index a1c4824c4..4e31b3f92 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -231,6 +231,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From f1460740685d2f10e3372824f5416312000bdb69 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 061/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 1354e0995..3db34229b 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2601,6 +2601,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 6beea15032f321ee3479e71e7977e4b011535a05 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 062/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 3db34229b..9016e9b4d 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2674,6 +2674,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 7e30cb384b1941c9e69fed832269cc39b6f8ebc4 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 063/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 9016e9b4d..903abce76 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2747,6 +2747,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 5bc90d2534aa8a0bd6e51967687c677e1ae01d9f Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 064/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 903abce76..aae736c05 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2820,6 +2820,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From ddbd131637c20de2f488e7b8c5869ba56d4e8c54 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 065/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index aae736c05..d92e1770e 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2893,6 +2893,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From fd8ec32c9070950f074a7fd88d1307a6e56a0c8b Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 066/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index d92e1770e..b91eb29c5 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -2966,6 +2966,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 746f7f7ae6770d42e1c58127cd090d88f088ec6f Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 067/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index b91eb29c5..3becb85fb 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3039,6 +3039,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 55ab1ff6ffcec87a2ac4854ffd0ac4f3e826026e Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 068/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 3becb85fb..e65f49298 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3112,6 +3112,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 00cda35fd487f8ebd77cc13ed13697d13f3221f3 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 069/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index e65f49298..5c29b641a 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3185,6 +3185,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From dac43b7c71b0ed21d7bc9d23dbdf4a9cd9baa6d6 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 070/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 5c29b641a..e006b97bb 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3258,6 +3258,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From d6bc445a92d10de46c8f4cc99153516edbd3375f Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 071/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index e006b97bb..50a848f6e 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3331,6 +3331,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 4e31b3f92..20a17fdfc 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -256,6 +256,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From a330f1dd8ab3c6536ede542692436dcb5823aa86 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 072/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 50a848f6e..bd16243b0 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3404,6 +3404,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 7ec1176ce6885925508bf231a53f3e9d0f525545 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 073/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index bd16243b0..1f84c654c 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3477,6 +3477,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 780bd4781c2fbd985d3bd158964fecb63ebe46e4 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 074/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 1f84c654c..c1dbbd299 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3550,6 +3550,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 28e215cdc4b2b8fe3ab932aed12bfa645a16479f Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 075/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index c1dbbd299..fb663ae3d 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3623,6 +3623,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 6a0f931355c5e178b2bb421fb2aed87c5cdc5e44 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 076/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index fb663ae3d..6f43caa41 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3696,6 +3696,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 2606652b3174c233c33c1b51d083be668e967bbe Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 077/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 6f43caa41..4bd8cd9c4 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3769,6 +3769,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From bdb1426dd5ffae9bc6b46bdbdc0d0a93791da129 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 078/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 4bd8cd9c4..75bf23a89 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3842,6 +3842,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From fdd290b64a1c728ddc0677c6f66aadc2c3291d60 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 079/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 75bf23a89..c67ccc773 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3915,6 +3915,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 20a17fdfc..459308a32 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -281,6 +281,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From c8d28a2700840b450df2925faf2586124b509adc Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 080/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index c67ccc773..f86cca6c7 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -3988,6 +3988,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 739b40b145a332d8ce61032f0fcf520efdbd45e7 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 081/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index f86cca6c7..c1c5d4456 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4061,6 +4061,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From e99185e5fb10e89e8c8b3b32afec4be0d1f62d92 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 082/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index c1c5d4456..5236191f8 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4134,6 +4134,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 459308a32..81da96d84 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -306,6 +306,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From 280151123968709b26726b59c9b17a4329dbe818 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 083/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 5236191f8..fe41502a7 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4207,6 +4207,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From fcfe2c01c367ba281296a7432307e7b41e5a2feb Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 084/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index fe41502a7..edd86a85c 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4280,6 +4280,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 9b3ea9aabb291f80308ea6942cd34577e871a35f Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 085/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index edd86a85c..9d32996de 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4353,6 +4353,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From b10f83f09c2f016fd32adec76197402200b7885b Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 086/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 9d32996de..8040decf2 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4426,6 +4426,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From ddf0b1f308d518bc180186c1b62931a3e17a957c Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 087/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 8040decf2..9b68254a6 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4499,6 +4499,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From ca1160607eab238314537cd31413cfe525af9c6e Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 088/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 9b68254a6..5e312582f 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4572,6 +4572,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From cd92e3246bf3e1499b5f5da34701ccdbfab92860 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 089/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 5e312582f..1d585d372 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4645,6 +4645,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From edf39427d6b7125bf9a5f296ea5869388586593f Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 090/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 1d585d372..d06b73bec 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4718,6 +4718,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 35695e6db9032f4691841e60fbeb6ced77be2ece Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 091/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index d06b73bec..e41145401 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4791,6 +4791,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 4e90e0abdd13c86cb3f36217226bee9bd1533f8e Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 092/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index e41145401..fc0dec4e9 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4864,6 +4864,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From d21df7a8b94b001a8178ee1c9e2fda514c3f4d70 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 093/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index fc0dec4e9..47d394390 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -4937,6 +4937,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 81da96d84..d989df7b9 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -331,6 +331,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From 1506d4b21d2b60c74eabe622701d13a4ee5e8644 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 094/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 47d394390..825741c2d 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5010,6 +5010,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From f663fec2d803a1b56d02a1f5893807a3ff0b86d1 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 095/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 825741c2d..bae52b9df 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5083,6 +5083,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From e4389f618875c3faec84f2dd5b54527c0ed114de Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 096/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index bae52b9df..07d8e4a5b 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5156,6 +5156,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 0bceeb0030532de98a5690926fe32151686eef48 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 097/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 07d8e4a5b..1412a283b 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5229,6 +5229,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 34c4a6c1196ff7be3d34bc4323735c506c6d9749 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 098/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 1412a283b..302a259a2 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5302,6 +5302,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From e9a1adb1d7e6263165c0007e22cd229c5d22ea5c Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 099/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 302a259a2..fa08542ff 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5375,6 +5375,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 9f18b698928802fcbdb180235829e82803872d60 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 100/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index fa08542ff..e9af2771a 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5448,6 +5448,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 84dedc993694a010b1005692c86930773bd5b322 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 101/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index e9af2771a..6c0004c11 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5521,6 +5521,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index d989df7b9..5ffc14244 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -356,6 +356,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From 2245c5661630ea6ff5bf66def0ac6017ad80df3b Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 102/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 6c0004c11..f0526be77 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5594,6 +5594,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 223eebb1d405fcb8cb37b4e6b767bf5480f333d5 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 103/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index f0526be77..9d174d16d 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5667,6 +5667,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 6543de2ffa573af77b96449c7fa499632d69063c Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 104/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 9d174d16d..dd7fbff8d 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5740,6 +5740,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 5ffc14244..98a616a39 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -381,6 +381,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From 7e2111e3a584f43926583789503019bdc35bc428 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 105/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index dd7fbff8d..baaf74eeb 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5813,6 +5813,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 75dcf949149db38e7d7fc793f87a041b3fac7e35 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 106/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index baaf74eeb..0df260d65 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5886,6 +5886,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 6b0f9674fd928a631ec93ed57e70e09f0a2e91f7 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 107/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 0df260d65..00b0f3ae1 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -5959,6 +5959,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 84d792f119c73b01446bb0be21a3d2b39e9d3285 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 108/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 00b0f3ae1..79ba951e0 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6032,6 +6032,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 33ea53ed526b4f75d2c31eac0c49068316bda95d Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 109/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 79ba951e0..06256d852 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6105,6 +6105,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From c093a63b4f4d98411dba4f0f0298d3956db9ad4e Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 110/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 06256d852..771d4e251 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6178,6 +6178,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From c82d39b7e5286b7608312de3ad6118ef9c7c7b7a Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 111/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 771d4e251..014392639 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6251,6 +6251,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From a472f0246eb19fd56f7a03de16d6e5471725503b Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 112/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 014392639..b9ca988db 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6324,6 +6324,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 65c1608e25193f6b3004100e393b3c3540e7f898 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 113/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index b9ca988db..ebcb17c31 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6397,6 +6397,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From e6d7f8e3a1bc92c6d7b35dd29156885543075db7 Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 114/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index ebcb17c31..63175a505 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6470,6 +6470,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From 06a912df68ff008f03ec1e693f978d12dadae37a Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 115/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 +++++++++++++++++++ .../components/search/menus/FilterMainMenu.js | 25 +++++++ 2 files changed, 98 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 63175a505..5321a6d6f 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6543,6 +6543,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 98a616a39..173161d84 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -406,6 +406,31 @@ const menuMap = { optimade: FilterSubMenuOptimade } +const menuMap = { + material: FilterSubMenuMaterial, + elements: FilterSubMenuElements, + symmetry: FilterSubMenuSymmetry, + method: FilterSubMenuMethod, + simulation: FilterSubMenuSimulation, + dft: FilterSubMenuDFT, + gw: FilterSubMenuGW, + eels: FilterSubMenuEELS, + electronic: FilterSubMenuElectronic, + optoelectronic: FilterSubMenuOptoElectronic, + vibrational: FilterSubMenuVibrational, + mechanical: FilterSubMenuMechanical, + spectroscopy: FilterSubMenuSpectroscopy, + thermodynamic: FilterSubMenuThermodynamic, + geometry_optimization: FilterSubMenuGeometryOptimization, + eln: FilterSubMenuELN, + author: FilterSubMenuAuthor, + dataset: FilterSubMenuDataset, + access: FilterSubMenuAccess, + id: FilterSubMenuIDs, + processed_data_quantities: FilterSubMenuArchive, + optimade: FilterSubMenuOptimade +} + /** * Swipable menu that shows the available filters on the left side of the * screen. -- GitLab From cb0cc3e97e54bbc6d4f1dbdc74b1e624cc12f2ad Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 116/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 5321a6d6f..ac8dc50e9 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6616,6 +6616,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab From aad6066c196da2b21f8cbc46c315c1b9326ad2cf Mon Sep 17 00:00:00 2001 From: Lauri Himanen Date: Mon, 26 Sep 2022 12:14:46 +0000 Subject: [PATCH 117/117] Resolve "Search customization" --- gui/src/components/search/SearchContext.js | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index ac8dc50e9..a7a3174d6 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -6689,6 +6689,79 @@ export const SearchContext = React.memo(({ : undefined }, [initialFilterMenus]) + // The final filtered set of columns + const columns = useMemo(() => { + if (!initialColumns) return undefined + const columns = cloneDeep(initialColumns) + let options = columns.include + .filter(key => !columns?.exclude?.includes(key)) + .map(key => ({key, ...columns.options[key]})) + + // Custom render and sortability is enforced for a subset of columns. + const overrides = { + upload_create_time: { + render: row => row?.upload_create_time + ? formatTimestamp(row.upload_create_time) + : no upload time + }, + authors: { + render: row => authorList(row), + align: 'left', + sortable: false + }, + references: { + sortable: false, + render: row => { + const refs = row.references || [] + if (refs.length > 0) { + return ( +
+ {refs.map((ref, i) => + {ref}{(i + 1) < refs.length ? ', ' : } + )} +
+ ) + } else { + return no references + } + } + }, + datasets: { + sortable: false, + render: entry => { + const datasets = entry.datasets || [] + if (datasets.length > 0) { + return datasets.map(dataset => dataset.dataset_name).join(', ') + } else { + return no datasets + } + } + }, + published: { + render: (entry) => + } + } + + addColumnDefaults(options) + options = options.map( + option => ({...option, ...(overrides[option.key] || {})}) + ) + + return { + options, + enable: columns.enable + } + }, [initialColumns]) + + // The final filtered set of menus + const filterMenus = useMemo(() => { + return initialFilterMenus?.include + ? initialFilterMenus.include + .filter(key => !initialFilterMenus.exclude.includes(key)) + .map(key => ({key, ...initialFilterMenus.options[key]})) + : undefined + }, [initialFilterMenus]) + // Initialize the set of available filters. This may depend on the resource. const [filtersLocal, filterDataLocal] = useMemo(() => { const filtersLocal = new Set() -- GitLab