diff --git a/docs/howto/plugins/apps.md b/docs/howto/plugins/apps.md index 68bba66282b50a870d30b170c07b8cbebbd8b746..ad85e77a5a85fcaca371942cd048ab7d5f3475ba 100644 --- a/docs/howto/plugins/apps.md +++ b/docs/howto/plugins/apps.md @@ -46,13 +46,13 @@ The entry point instance should then be added to the `[project.entry-points.'nom myapp = "nomad_example.apps:myapp" ``` -## `App` class +## Creating an `App` The definition fo the actual app is given as an instance of the `App` class specified as part of the entry point. A full breakdown of the model is given below in the [app reference](#app-reference), but here is a small example: ```python from nomad.config.models.plugins import AppEntryPoint -from nomad.config.models.ui import App, Column, FilterMenu, FilterMenus, Filters +from nomad.config.models.ui import App, Column, Menu, MenuItemPeriodicTable, MenuItemHistogram, MenuItemTerms, SearchQuantities schema = 'nomad_example.schema_packages.mypackage.MySchema' myapp = AppEntryPoint( @@ -69,12 +69,12 @@ myapp = AppEntryPoint( description='An app customized for me.', # Longer description that can also use markdown readme='Here is a much longer description of this app.', - # Controls the available search filters. If you want to filter by - # quantities in a schema package, you need to load the schema package - # explicitly here. Note that you can use a glob syntax to load the - # entire package, or just a single schema from a package. - filters=Filters( - include=[f'*#{schema}'], + # If you want to use quantities from a custom schema, you need to load + # the search quantities from it first here. Note that you can use a glob + # syntax to load the entire package, or just a single schema from a + # package. + search_quantities=SearchQuantities( + include=['*#nomad_example.schema_packages.mypackage.MySchema'], ), # Controls which columns are shown in the results table columns=[ @@ -97,18 +97,39 @@ myapp = AppEntryPoint( filters_locked={ "section_defs.definition_qualified_name:all": [schema] }, - # Controls the filter menus shown on the left - filter_menus=FilterMenus( - options={ - 'material': FilterMenu(label="Material"), - } - ), + # Controls the menu shown on the left + menu = Menu( + title='Material', + items=[ + Menu( + title='elements', + items=[ + MenuItemPeriodicTable( + quantity='results.material.elements', + ), + MenuItemTerms( + quantity='results.material.chemical_formula_hill', + width=6, + options=0, + ), + MenuItemTerms( + quantity='results.material.chemical_formula_iupac', + width=6, + options=0, + ), + MenuItemHistogram( + x='results.material.n_elements', + ) + ] + ) + ] + ) # Controls the default dashboard shown in the search interface dashboard={ 'widgets': [ { 'type': 'histogram', - 'showinput': False, + 'show_input': False, 'autorange': True, 'nbins': 30, 'scale': 'linear', @@ -151,19 +172,15 @@ myapp = AppEntryPoint( ) ``` -### Loading custom quantity definitions into an app +### Loading quantity definitions into an app -By default, none of the quantities from custom schemas are available in an app, and they need to be explicitly added. Each app may define additional **filters** that should be enabled in it. Filters have a special meaning in the app context: filters are pieces of (meta)info that can be queried in the search interface of the app, but also targeted in the rest of the app configuration as explained below in. +By default, quantities from custom schemas are not available in an app, and they need to be explicitly added. Each app may define the quantities to load by using the **search_quantities** field in the app config. Once loaded, these search quantities can be queried in the search interface, but also targeted in the rest of the app configuration as explained below. -!!! note +!!! important - Note that not all of the quantities from a custom schema can be exposed as - filters. At the moment we only support targeting **scalar** quantities from - custom schemas. + Note that not all of the quantities from a custom schema can be loaded into the search. At the moment we only support loading **scalar** quantities from custom schemas. -Each schema has a unique name within the NOMAD ecosystem, which is needed to -target them in the configuration. The name depends on the resource in which the -schema is defined in: +Each schema has a unique name within the NOMAD ecosystem, which is needed to target them in the configuration. The name depends on the resource in which the schema is defined in: - Python schemas are identified by the python path for the class that inherits from `Schema`. For example, if you have a python package called `nomad_example`, @@ -182,7 +199,7 @@ include all filters from the Python schema defined in the class `nomad_example.schema_packages.mypackage.MySchema`, you could use: ```python -filters=Filters( +search_quantities=SearchQuantities( include=['*#nomad_example.schema_packages.mypackage.MySchema'] ) ``` @@ -190,12 +207,12 @@ filters=Filters( The same thing for a YAML schema could be achieved with: ```python -filters=Filters( +search_quantities=SearchQuantities( include=['*#entry_id:<entry_id>.MySchema'] ) ``` -Once quantities from a schema are included in an app as filters, they can be targeted in the rest of the app. The app configuration often refers to specific filters to configure parts of the user interface. For example, one could configure the results table to show a new column using one of the schema quantities with: +Once search quantities are loaded, they can be targeted in the rest of the app. The app configuration often refers to specific search quantities to configure parts of the user interface. For example, one could configure the results table to show a new column using one of the search quantities with: ```python columns=[ @@ -217,6 +234,56 @@ by a hashtag (#), for example `data.mysection.myquantity#entry_id:<entry_id>.MyS - Quantities that are common for all NOMAD entries can be targeted by using only the path without the need for specifying a schema, e.g. `results.material.symmetry.space_group`. +### Menu + +The `menu` field controls the structure of the menu shown on the left side of the search interface. Menus have a controllable width, and they contains items that are displayed on a 12-based grid. You can also nest menus within each other. For example, this defines a menu with two levels: + +```python +# This is a top level menu that is always visible. It shows two items: a terms +# item and a submenu beneath it. +menu = Menu( + size='sm', + items=[ + MenuItemTerms( + search_quantity='authors.name', + options=5 + ), + # This is a submenu whose items become visible once selected. It + # contains three items: one full-width histogram and two terms items + # which are displayed side-by-side. + Menu( + title='Submenu' + size='md', + items=[ + MenuItemHistogram( + search_quantity='upload_create_time' + ), + # These items target data from a custom schema + MenuItemTerms( + width=6, + search_quantity='data.quantity1#nomad_example.schema_packages.mypackage.MySchema' + ), + MenuItemTerms( + width=6, + search_quantity='data.quantity2#nomad_example.schema_packages.mypackage.MySchema' + ) + ] + ) + ] +) +``` + +The following items are supported in menus, and you can read more about them in the App reference: + + - [`Menu`](#menu_1): Defines a nested submenu. + - [`MenuItemTerms`](#menuitemterms): Used to display a set of possible text options. + - [`MenuItemHistogram`](#menuitemhistogram): Histogram of numerical values. + - [`MenuItemPeriodictable`](#menuitemperiodictable): Displays a periodic table. + - [`MenuItemOptimade`](#menuitemoptimade): OPTIMADE query field. + - [`MenuItemVisibility`](#menuitemvisibility): Controls for the query visibility. + - [`MenuItemDefinitions`](#menuitemdefinitions): Shows a tree of available definitions from which items can be selected for the query. + - [`MenuItemCustomQuantities`](#menuitemcustomquantities): Form for querying custom quantities coming from any schema. + - [`MenuItemNestedObject`](#menuitemnestedobject): Used to group together menu items so that their query is performed using an Elasticsearch nested query. Note that you cannot yet use nested queries for search quantities originating from custom schemas. ## App reference diff --git a/examples/data/cow_tutorial/nomad-countries/src/nomad_countries/apps/__init__.py b/examples/data/cow_tutorial/nomad-countries/src/nomad_countries/apps/__init__.py index 8ed73adc2980826035174041f62c9bf50f6277dc..45376d3fd0ec16962b42a54c2fae1da8b720f982 100644 --- a/examples/data/cow_tutorial/nomad-countries/src/nomad_countries/apps/__init__.py +++ b/examples/data/cow_tutorial/nomad-countries/src/nomad_countries/apps/__init__.py @@ -38,7 +38,6 @@ country = AppEntryPoint( dashboard=Dashboard( widgets=[ WidgetScatterPlot( - type='scatterplot', layout={'lg': Layout(h=6, w=8, x=0, y=0)}, x=Axis(quantity=f'data.literacy#{schema}'), y=Axis(quantity=f'data.industry#{schema}'), @@ -46,7 +45,6 @@ country = AppEntryPoint( autorange=True, ), WidgetScatterPlot( - type='scatterplot', layout={'lg': Layout(h=6, w=8, x=8, y=0)}, x=Axis(quantity=f'data.literacy#{schema}'), y=Axis(quantity=f'data.agriculture#{schema}'), @@ -54,7 +52,6 @@ country = AppEntryPoint( autorange=True, ), WidgetScatterPlot( - type='scatterplot', layout={'lg': Layout(h=6, w=8, x=16, y=0)}, x=Axis(quantity=f'data.literacy#{schema}'), y=Axis(quantity=f'data.service#{schema}'), @@ -62,30 +59,27 @@ country = AppEntryPoint( autorange=True, ), WidgetHistogram( - type='histogram', layout={'lg': Layout(h=3, w=8, x=0, y=6)}, quantity=f'data.phones#{schema}', scale='1/2', nbins=30, - showinput=False, + show_input=False, autorange=True, ), WidgetHistogram( - type='histogram', layout={'lg': Layout(h=3, w=8, x=8, y=6)}, quantity=f'data.birthrate#{schema}', scale='linear', nbins=30, - showinput=False, + show_input=False, autorange=True, ), WidgetHistogram( - type='histogram', layout={'lg': Layout(h=3, w=8, x=16, y=6)}, quantity=f'data.net_migration#{schema}', scale='linear', nbins=30, - showinput=False, + show_input=False, autorange=True, ), ] diff --git a/examples/data/rdm_tutorial/tutorial.ipynb b/examples/data/rdm_tutorial/tutorial.ipynb index 741ae2e4210d1401774a0ba555c8059837d19db9..08f156e09195284615ee245dbc4cba6e1886be84 100644 --- a/examples/data/rdm_tutorial/tutorial.ipynb +++ b/examples/data/rdm_tutorial/tutorial.ipynb @@ -8741,13 +8741,11 @@ " dashboard=Dashboard(\n", " widgets=[\n", " WidgetPeriodicTable(\n", - " type='periodictable',\n", " scale='linear',\n", " quantity='results.material.elements',\n", " layout={'lg': Layout(w=11, h=7, x=0, y=0)},\n", " ),\n", " WidgetScatterPlot(\n", - " type='scatterplot',\n", " layout={'lg': Layout(w=7, h=7, x=11, y=0)},\n", " x=Axis(quantity=f'data.peak_emission_wavelength#{schema}', unit='nm'),\n", " y=Axis(quantity=f'data.photoluminescence_quantum_yield#{schema}'),\n", @@ -8759,7 +8757,6 @@ " scale='linear',\n", " ),\n", " WidgetHistogram(\n", - " type='histogram',\n", " layout={'lg': Layout(w=12, h=4, x=12, y=7)},\n", " autorange=False,\n", " nbins=30,\n", @@ -8767,7 +8764,6 @@ " quantity=f'data.delayed_lifetime#{schema}',\n", " ),\n", " WidgetHistogram(\n", - " type='histogram',\n", " layout={'lg': Layout(w=12, h=4, x=12, y=7)},\n", " autorange=False,\n", " nbins=30,\n", diff --git a/gui/src/components/UserdataPage.js b/gui/src/components/UserdataPage.js index 2481cfd5419b516c35e6cfa570f386ddc772e4da..73bc57d998b856a60d90825bff1bf39f9aa23268 100644 --- a/gui/src/components/UserdataPage.js +++ b/gui/src/components/UserdataPage.js @@ -86,8 +86,8 @@ const UserdataPage = React.memo(() => { initialPagination={context?.pagination} initialColumns={context?.columns} initialRows={context?.rows} - initialFilterMenus={context?.filter_menus} - initialFilters={context?.filters} + initialMenu={context?.menu} + initialSearchQuantities={context?.search_quantities} initialFiltersLocked={initialFiltersLocked} initialDashboard={context?.dashboard} initialSearchSyntaxes={context?.search_syntaxes} diff --git a/gui/src/components/UserdataPage.spec.js b/gui/src/components/UserdataPage.spec.js index 01e1e6866ee61ec4fefdec57dae2c30a20935f97..c9e7da93a2d7594bedc2f07260d8b525e5f87c40 100644 --- a/gui/src/components/UserdataPage.spec.js +++ b/gui/src/components/UserdataPage.spec.js @@ -18,7 +18,7 @@ import React from 'react' import { render, startAPI, closeAPI } from './conftest.spec' -import { expectFilterMainMenu, expectSearchResults } from './search/conftest.spec' +import { expectMenu, expectSearchResults } from './search/conftest.spec' import { ui } from '../config' import UserDatapage from './UserdataPage' import { minutes } from '../setupTests' @@ -28,7 +28,7 @@ test('renders user data search page correctly', async () => { await startAPI('tests.states.search.search', 'tests/data/search/userdatapage', 'test', 'password') render(<UserDatapage />) - await expectFilterMainMenu(context) - await expectSearchResults(context) + await expectMenu(context.menu) + await expectSearchResults(context.columns) closeAPI() }, 5 * minutes) diff --git a/gui/src/components/dataset/DatasetPage.js b/gui/src/components/dataset/DatasetPage.js index dbd81e300e86b0d5968e0bdb4696ab6fb953433e..01f287289e3801031f05cb73a755eb285657a3e8 100644 --- a/gui/src/components/dataset/DatasetPage.js +++ b/gui/src/components/dataset/DatasetPage.js @@ -73,8 +73,8 @@ const DatasetPage = React.memo(({match}) => { initialPagination={context?.pagination} initialColumns={context?.columns} initialRows={context?.rows} - initialFilterMenus={context?.filter_menus} - initialFilters={context?.filters} + initialMenu={context?.menu} + initialSearchQuantities={context?.search_quantities} initialFiltersLocked={datasetFilter} initialDashboard={context?.dashboard} initialSearchSyntaxes={context?.search_syntaxes} diff --git a/gui/src/components/editQuantity/QueryEditQuantity.js b/gui/src/components/editQuantity/QueryEditQuantity.js index 87af053a8e0af16b63fd7ae941208d46d237156e..8417258974fc331e82441110aace61a5ceeab990 100644 --- a/gui/src/components/editQuantity/QueryEditQuantity.js +++ b/gui/src/components/editQuantity/QueryEditQuantity.js @@ -269,7 +269,7 @@ function QueryEditQuantity({quantityDef, onChange, value, storeInArchive, index, initialPagination={context?.pagination} initialColumns={columns} initialRows={rows} - initialFilterMenus={context?.filter_menus} + initialMenu={context?.menu} initialFiltersLocked={undefined} initialFilterValues={filters} initialSearchSyntaxes={context?.search_syntaxes} diff --git a/gui/src/components/entry/properties/SampleHistoryCard.js b/gui/src/components/entry/properties/SampleHistoryCard.js index b5e16ba637cfada9cef4397d7d635b51198da57a..0dc26bbb664d0b5fd64e30aa261ab08ce79dd808 100644 --- a/gui/src/components/entry/properties/SampleHistoryCard.js +++ b/gui/src/components/entry/properties/SampleHistoryCard.js @@ -76,7 +76,7 @@ const SampleHistoryUsingCard = memo(({ index }) => { initialPagination={context?.pagination} initialColumns={context?.columns} initialRows={context?.rows} - initialFilterMenus={context?.filter_menus} + initialMenu={context?.menu} initialFiltersLocked={filtersLocked} initialSearchSyntaxes={context?.search_syntaxes} > diff --git a/gui/src/components/nav/Routes.js b/gui/src/components/nav/Routes.js index aef86e2a287ddacd9d04c13a78cc53eb4b8c1185..85387fd2becc5b6d4d035c85a89eab1bcd48834c 100644 --- a/gui/src/components/nav/Routes.js +++ b/gui/src/components/nav/Routes.js @@ -182,8 +182,8 @@ const searchRoutes = apps initialPagination={context.pagination} initialColumns={context.columns} initialRows={context.rows} - initialFilterMenus={context.filter_menus} - initialFilters={context?.filters} + initialMenu={context.menu} + initialSearchQuantities={context?.search_quantities} initialFiltersLocked={context.filters_locked} initialDashboard={context?.dashboard} initialSearchSyntaxes={context?.search_syntaxes} diff --git a/gui/src/components/plotting/PlotHistogram.js b/gui/src/components/plotting/PlotHistogram.js index 1414a77e6d699e8ce976143b2058f5c28e7f6ed9..d83861097ff6ccdf9e8119e80f53be470175fac8 100644 --- a/gui/src/components/plotting/PlotHistogram.js +++ b/gui/src/components/plotting/PlotHistogram.js @@ -150,6 +150,9 @@ const useStyles = makeStyles(theme => ({ flexDirection: 'row', justifyContent: 'center', alignItems: 'flex-start' + }, + axisTitle: { + fontSize: '0.75rem' } })) const PlotHistogram = React.memo(({ @@ -178,7 +181,7 @@ const PlotHistogram = React.memo(({ maxXInclusive, className, classes, - showinput, + showInput, minError, maxError, minInput, @@ -191,6 +194,7 @@ const PlotHistogram = React.memo(({ 'data-testid': testID }) => { const styles = useStyles(classes) + const titleClasses = {text: styles.axisTitle} const useDynamicStyles = makeStyles((theme) => { const color = highlight ? theme.palette.secondary.main : theme.palette.primary.main return { @@ -531,11 +535,11 @@ const PlotHistogram = React.memo(({ const titleComp = <div className={styles.title}> <FilterTitle + variant="subtitle2" + classes={titleClasses} quantity={xAxis.quantity} label={xAxis.title} unit={xAxis.unit} - variant="caption" - className={styles.titletext} noWrap={false} /> </div> @@ -545,7 +549,7 @@ const PlotHistogram = React.memo(({ {!disableHistogram && <div className={clsx(styles.histogram, classes?.histogram)}>{histComp}</div>} {!disableXTitle && titleComp} <div className={styles.row}> - {showinput + {showInput ? <> {inputMinField} {(disableHistogram && !isTime) @@ -609,7 +613,6 @@ PlotHistogram.propTypes = { onMaxChange: PropTypes.func, onMinSubmit: PropTypes.func, onMaxSubmit: PropTypes.func, - showinput: PropTypes.bool, maxError: PropTypes.any, minError: PropTypes.any, maxInput: PropTypes.any, diff --git a/gui/src/components/plotting/PlotScatter.js b/gui/src/components/plotting/PlotScatter.js index a74666ef20090ad0f3304c6f695a1ca8cd2ec25b..d856bf9e2f2569068f8d54857fd1a2dc28dc5821 100644 --- a/gui/src/components/plotting/PlotScatter.js +++ b/gui/src/components/plotting/PlotScatter.js @@ -82,6 +82,9 @@ const useStyles = makeStyles(theme => ({ }, colorlabel: { transform: 'rotate(90deg)' + }, + axisTitle: { + fontSize: '0.75rem' } })) const PlotScatter = React.memo(forwardRef(( @@ -101,6 +104,7 @@ const PlotScatter = React.memo(forwardRef(( }, canvas) => { const styles = useStyles() const theme = useTheme() + const titleClasses = {text: styles.axisTitle} const [finalData, setFinalData] = useState(!data ? data : undefined) const history = useHistory() @@ -131,7 +135,7 @@ const PlotScatter = React.memo(forwardRef(( // If dealing with a quantized color, each group is separated into it's own // trace which has a legend as well. const traces = [] - if (colorAxis?.quantity && discrete) { + if (colorAxis?.search_quantity && discrete) { const options = [...new Set(data.color)] const nOptions = options.length const scale = d3.scaleSequential([0, 1], d3.interpolateTurbo) @@ -178,7 +182,7 @@ const PlotScatter = React.memo(forwardRef(( }) } // When dealing with a continuous color, display a colormap - } else if (colorAxis?.quantity && !discrete) { + } else if (colorAxis?.search_quantity && !discrete) { traces.push({ x: data.x, y: data.y, @@ -247,7 +251,7 @@ const PlotScatter = React.memo(forwardRef(( }) } setFinalData(traces) - }, [colorAxis?.quantity, colorAxis?.title, colorAxis?.unit, data, discrete, theme, xAxis.title, xAxis.unit, yAxis.title, yAxis.unit]) + }, [colorAxis?.search_quantity, colorAxis?.title, colorAxis?.unit, data, discrete, theme, xAxis.title, xAxis.unit, yAxis.title, yAxis.unit]) const layout = useMemo(() => { return { @@ -314,10 +318,11 @@ const PlotScatter = React.memo(forwardRef(( return <div className={styles.root}> <div className={styles.yaxis}> <FilterTitle + variant="subtitle2" + classes={titleClasses} quantity={yAxis.quantity} label={yAxis.title} unit={yAxis.unit} - variant="caption" rotation="up" /> </div> @@ -340,21 +345,23 @@ const PlotScatter = React.memo(forwardRef(( <div className={styles.square} /> <div className={styles.xaxis}> <FilterTitle + variant="subtitle2" + classes={titleClasses} quantity={xAxis.quantity} label={xAxis.title} unit={xAxis.unit} - variant="caption" /> </div> {!discrete && colorAxis && <div className={styles.color}> <FilterTitle + variant="subtitle2" + classes={titleClasses} rotation="down" quantity={colorAxis.quantity} unit={colorAxis.unit} label={colorAxis.title} description="" - variant="caption" /> </div> } diff --git a/gui/src/components/plotting/common.js b/gui/src/components/plotting/common.js index e9c3086e5faea720faf86c6019a23660fa75ee9d..559fc3f8599d23565126b8e78cae0f11fb0c7941 100644 --- a/gui/src/components/plotting/common.js +++ b/gui/src/components/plotting/common.js @@ -39,7 +39,8 @@ import { eachQuarterOfInterval } from 'date-fns' import { scale as chromaScale } from 'chroma-js' -import { scale as scaleUtils, add, DType, formatNumber } from '../../utils.js' +import { scale as scaleUtils, add, DType, formatNumber, getDisplayLabel, parseJMESPath } from '../../utils.js' +import { Unit } from '../units/Unit' export const scales = { 'linear': 'linear', @@ -485,3 +486,28 @@ export function getPlotTracesVertical(plots, theme) { } return size(traces) ? traces : undefined } + +/** + * Returns a fully configured axis object for plotting. + * + * @param {object} axis The original axis configuration. + * @param {object} filterData Filters registered in the current search context. + * @param {object} units Units in current unit system. + */ +export function getAxisConfig(axis, filterData, units) { + const {quantity} = parseJMESPath(axis?.search_quantity) + const filter = filterData[quantity] + const title = axis.title || filter?.label || getDisplayLabel(filter) + const dtype = filter?.dtype + const unit = axis.unit + ? new Unit(axis.unit) + : new Unit(filter?.unit || 'dimensionless').toSystem(units) + + return { + ...axis, + title, + unit, + dtype, + quantity: quantity + } +} diff --git a/gui/src/components/search/Filter.js b/gui/src/components/search/Filter.js index 3434600073f00526a42a3e9c0b52288958140855..fb518d9a4ae8a6d90ae1b0f299d467c4f0087d10 100644 --- a/gui/src/components/search/Filter.js +++ b/gui/src/components/search/Filter.js @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { isNil, isArray, isEmpty } from 'lodash' +import { isNil, isArray, isEmpty, capitalize, split } from 'lodash' import { searchQuantities } from '../../config' import { getDatatype, @@ -23,7 +23,8 @@ import { getDeserializer, getDisplayLabel, DType, - multiTypes + multiTypes, + parseQuantityName } from '../../utils' import { Unit } from '../units/Unit' @@ -139,7 +140,12 @@ export class Filter { this.description = params?.description || def?.description this.unit = params?.unit || def?.unit this.dimension = def?.unit ? new Unit(def?.unit).dimension() : 'dimensionless' - this.label = params?.label || getDisplayLabel(def) + function getLabel(quantity) { + if (isNil(quantity)) return '' + const {path} = parseQuantityName(quantity) + return capitalize(split(path, '.').slice(-1)[0].replace(/_/g, ' ')) + } + this.label = params?.label || getDisplayLabel(def) || getLabel(this.quantity) this.parent = parent this.group = params.group this.placeholder = params?.placeholder @@ -148,7 +154,7 @@ export class Filter { : params?.multiple this.exclusive = params?.exclusive === undefined ? true : params?.exclusive this.queryMode = params?.queryMode || (this.multiple ? 'any' : undefined) - this.options = params?.options || getEnumOptions(def) + this.options = params?.options || getEnumOptions(this.quantity) this.default = params?.default this.suggestion = !isNil(params?.suggestion) ? params.suggestion : (!isNil(def?.suggestion) ? def.suggestion : false) this.scale = params?.scale || 'linear' @@ -184,7 +190,7 @@ export class Filter { */ export function getEnumOptions(quantity, exclude = ['not processed']) { const metainfoOptions = searchQuantities?.[quantity]?.type?.type_data - if (isArray(metainfoOptions) && metainfoOptions.length > 0) { + if (isArray(metainfoOptions) && metainfoOptions.length > 0 && metainfoOptions.length <= 20) { const opt = {} for (const name of metainfoOptions) { opt[name] = {label: name} @@ -206,11 +212,11 @@ export function getEnumOptions(quantity, exclude = ['not processed']) { export const getWidgetConfig = (quantity, dtype, aggregatable, scale) => { if (dtype === DType.Float || dtype === DType.Int || dtype === DType.Timestamp) { return { - x: {quantity}, + x: {search_quantity: quantity}, y: {scale: 'linear'}, type: 'histogram', scale, - showinput: false, + show_input: false, autorange: false, nbins: 30, layout: { @@ -223,10 +229,10 @@ export const getWidgetConfig = (quantity, dtype, aggregatable, scale) => { } } else if (aggregatable) { return { - quantity, + search_quantity: quantity, type: 'terms', scale: scale, - showinput: false, + show_input: false, layout: { sm: {w: 6, h: 9, minW: 3, minH: 3}, md: {w: 6, h: 9, minW: 3, minH: 3}, diff --git a/gui/src/components/search/FilterRegistry.js b/gui/src/components/search/FilterRegistry.js index 911385f30836b9d7e83e134149ab4ce397f52fd1..9d48dffd957dedc87c77cb17ca6488bd34c9bfe6 100644 --- a/gui/src/components/search/FilterRegistry.js +++ b/gui/src/components/search/FilterRegistry.js @@ -27,7 +27,6 @@ import { Typography, Box } from '@material-ui/core' import { useErrors } from '../errors' // Containers for filter information -export const defaultFilterGroups = {} // Mapping from a group name -> set of filter names export const defaultFilterData = {} // Stores data for each registered filter const dtypeMap = { [DType.Int]: 'int', @@ -38,45 +37,6 @@ const dtypeMap = { [DType.Boolean]: 'bool' } -// Ids for the filter menus: used to tie filter chips to a specific menu. -const idElements = 'elements' -const idStructure = 'structure' -const idMethod = 'method' -const idDFT = 'dft' -const idTB = 'tb' -const idGW = 'gw' -const idBSE = 'bse' -const idDMFT = 'dmft' -const idPrecision = 'precision' -const idProperties = 'properties' -const idElectronic = 'electronic' -const idSolarCell = 'solarcell' -const idCatalyst = 'heterogeneouscatalyst' -const idVibrational = 'vibrational' -const idMechanical = 'mechanical' -const idSpectroscopic = 'spectroscopic' -const idMolecularDynamics = 'molecular_dynamics' -const idGeometryOptimization = 'geometry_optimization' -const idELN = 'eln' -const idCustomQuantities = 'custom_quantities' -const idAuthor = 'author' -const idMetadata = 'metadata' -const idOptimade = 'optimade' - -/** - * Associates the given quantity name with the given group name in the filter - * group data. - * - * @param {object} groups The groups to work on. - * @param {str} groupName - * @param {str} quantityName - */ -function addToGroup(groups, groupName, quantityName) { - groups[groupName] - ? groups[groupName].add(quantityName) - : groups[groupName] = new Set([quantityName]) -} - /** * This function is used to register a new filter within the SearchContext. * Filters are entities that can be searched through the filter panel and the @@ -94,17 +54,21 @@ function addToGroup(groups, groupName, quantityName) { * components. * * @param {string} name Name of the filter. - * @param {string} group The group into which the filter belongs to. Groups - * are used to e.g. in showing FilterSummaries about a group of filters. * @param {obj} config Data object containing options for the filter. */ -function saveFilter(name, group, config, parent) { +function saveFilter(name, config, parent) { if (defaultFilterData[name]) { throw Error(`Trying to register filter "${name}"" multiple times.`) } const def = searchQuantities[name] const {path: quantity, schema} = parseQuantityName(name) - const newConf = {...(config || {}), quantity, schema, name: config?.name || def?.name || name, aggregatable: def?.aggregatable, group: group} + const newConf = { + ...(config || {}), + quantity, + schema, + name: config?.name || def?.name || name, + aggregatable: def?.aggregatable + } const data = defaultFilterData[name] || new Filter(def, newConf, parent) defaultFilterData[name] = data return data @@ -114,12 +78,12 @@ function saveFilter(name, group, config, parent) { * Used to register a filter value for an individual metainfo quantity or * section. */ -function registerFilter(name, group, config, subQuantities) { - const parent = saveFilter(name, group, config) +function registerFilter(name, config, subQuantities) { + const parent = saveFilter(name, config) if (subQuantities) { for (const subConfig of subQuantities) { const subname = `${name}.${subConfig.name}` - saveFilter(subname, group, subConfig, parent) + saveFilter(subname, subConfig, parent) } } } @@ -127,11 +91,10 @@ function registerFilter(name, group, config, subQuantities) { /** * Used to register a filter that is based on a subset of quantity values. */ -function registerFilterOptions(name, group, target, label, description, options) { +function registerFilterOptions(name, target, label, description, options) { const keys = Object.keys(options) registerFilter( name, - group, { aggs: { terms: { @@ -179,7 +142,6 @@ const numberHistogramQuantity = {multiple: false, exclusive: false} // Filters that directly correspond to a metainfo value registerFilter( 'results.material.structural_type', - idStructure, { ...termQuantity, scale: 'log', @@ -188,32 +150,30 @@ registerFilter( } ) -registerFilter('results.material.functional_type', idStructure, termQuantityNonExclusive) -registerFilter('results.material.compound_type', idStructure, termQuantityNonExclusive) -registerFilter('results.material.material_name', idStructure, termQuantity) -registerFilter('results.material.chemical_formula_hill', idElements, {...termQuantity, placeholder: "E.g. H2O2, C2H5Br"}) -registerFilter('results.material.chemical_formula_iupac', idElements, {...termQuantity, placeholder: "E.g. GaAs, SiC", label: 'Chemical formula IUPAC'}) -registerFilter('results.material.chemical_formula_reduced', idElements, {...termQuantity, placeholder: "E.g. H2NaO, ClNa"}) -registerFilter('results.material.chemical_formula_anonymous', idElements, {...termQuantity, placeholder: "E.g. A2B, A3B2C2"}) -registerFilter('results.material.n_elements', idElements, {...numberHistogramQuantity}) -registerFilter('results.material.symmetry.bravais_lattice', idStructure, termQuantity) -registerFilter('results.material.symmetry.crystal_system', idStructure, termQuantity) +registerFilter('results.material.functional_type', termQuantityNonExclusive) +registerFilter('results.material.compound_type', termQuantityNonExclusive) +registerFilter('results.material.material_name', termQuantity) +registerFilter('results.material.chemical_formula_hill', {...termQuantity, placeholder: "E.g. H2O2, C2H5Br"}) +registerFilter('results.material.chemical_formula_iupac', {...termQuantity, placeholder: "E.g. GaAs, SiC", label: 'Chemical formula IUPAC'}) +registerFilter('results.material.chemical_formula_reduced', {...termQuantity, placeholder: "E.g. H2NaO, ClNa"}) +registerFilter('results.material.chemical_formula_anonymous', {...termQuantity, placeholder: "E.g. A2B, A3B2C2"}) +registerFilter('results.material.n_elements', {...numberHistogramQuantity}) +registerFilter('results.material.symmetry.bravais_lattice', termQuantity) +registerFilter('results.material.symmetry.crystal_system', termQuantity) registerFilter( 'results.material.symmetry.structure_name', - idStructure, { ...termQuantity, options: getEnumOptions('results.material.symmetry.structure_name', ['not processed', 'cubic perovskite']) } ) -registerFilter('results.material.symmetry.strukturbericht_designation', idStructure, termQuantity) -registerFilter('results.material.symmetry.space_group_symbol', idStructure, {...termQuantity, placeholder: "E.g. Pnma, Fd-3m, P6_3mc"}) -registerFilter('results.material.symmetry.point_group', idStructure, {...termQuantity, placeholder: "E.g. 6mm, m-3m, 6/mmm"}) -registerFilter('results.material.symmetry.hall_symbol', idStructure, {...termQuantity, placeholder: "E.g. F 4d 2 3 -1d"}) -registerFilter('results.material.symmetry.prototype_aflow_id', idStructure, {...termQuantity, placeholder: "E.g. A_cF8_227_a"}) +registerFilter('results.material.symmetry.strukturbericht_designation', termQuantity) +registerFilter('results.material.symmetry.space_group_symbol', {...termQuantity, placeholder: "E.g. Pnma, Fd-3m, P6_3mc"}) +registerFilter('results.material.symmetry.point_group', {...termQuantity, placeholder: "E.g. 6mm, m-3m, 6/mmm"}) +registerFilter('results.material.symmetry.hall_symbol', {...termQuantity, placeholder: "E.g. F 4d 2 3 -1d"}) +registerFilter('results.material.symmetry.prototype_aflow_id', {...termQuantity, placeholder: "E.g. A_cF8_227_a"}) registerFilter( 'results.material.topology', - idStructure, nestedQuantity, [ {name: 'label', ...termQuantity}, @@ -264,7 +224,6 @@ registerFilter( ) registerFilter( 'results.material.elemental_composition', - idStructure, nestedQuantity, [ {name: 'element', ...termQuantity}, @@ -274,7 +233,6 @@ registerFilter( ) registerFilter( 'results.material.topology.elemental_composition', - idStructure, nestedQuantity, [ {name: 'element', ...termQuantity}, @@ -285,7 +243,6 @@ registerFilter( ) registerFilter( 'results.material.topology.active_orbitals', - idStructure, nestedQuantity, [ {name: 'n_quantum_number', ...termQuantity}, @@ -300,19 +257,19 @@ registerFilter( {name: 'degeneracy', ...termQuantity} ] ) -registerFilter('results.method.method_name', idMethod, {...termQuantity, scale: 'log'}) -registerFilter('results.method.workflow_name', idMethod, {...termQuantity, scale: 'log'}) -registerFilter('results.method.simulation.program_name', idMethod, {...termQuantity, scale: 'log'}) -registerFilter('results.method.simulation.program_version', idMethod, termQuantity) -registerFilter('results.method.simulation.program_version_internal', idMethod, termQuantity) -registerFilter('results.method.simulation.precision.native_tier', idPrecision, {...termQuantity, placeholder: "E.g. VASP - accurate", label: 'Code-specific tier'}) -registerFilter('results.method.simulation.precision.k_line_density', idPrecision, {...numberHistogramQuantity, scale: 'log', label: 'k-line density'}) -registerFilter('results.method.simulation.precision.basis_set', idPrecision, {...termQuantity, scale: 'log'}) -registerFilter('results.method.simulation.precision.planewave_cutoff', idPrecision, {...numberHistogramQuantity, label: 'Plane-wave cutoff', scale: 'log'}) -registerFilter('results.method.simulation.precision.apw_cutoff', idPrecision, {...numberHistogramQuantity, label: 'APW cutoff', scale: 'log'}) -registerFilter('results.method.simulation.dft.core_electron_treatment', idDFT, termQuantity) -registerFilter('results.method.simulation.dft.jacobs_ladder', idDFT, {...termQuantity, scale: 'log', label: 'Jacob\'s ladder'}) -registerFilter('results.method.simulation.dft.xc_functional_type', idDFT, { +registerFilter('results.method.method_name', {...termQuantity, scale: 'log'}) +registerFilter('results.method.workflow_name', {...termQuantity, scale: 'log'}) +registerFilter('results.method.simulation.program_name', {...termQuantity, scale: 'log'}) +registerFilter('results.method.simulation.program_version', termQuantity) +registerFilter('results.method.simulation.program_version_internal', termQuantity) +registerFilter('results.method.simulation.precision.native_tier', {...termQuantity, placeholder: "E.g. VASP - accurate", label: 'Code-specific tier'}) +registerFilter('results.method.simulation.precision.k_line_density', {...numberHistogramQuantity, scale: 'log', label: 'k-line density'}) +registerFilter('results.method.simulation.precision.basis_set', {...termQuantity, scale: 'log'}) +registerFilter('results.method.simulation.precision.planewave_cutoff', {...numberHistogramQuantity, label: 'Plane-wave cutoff', scale: 'log'}) +registerFilter('results.method.simulation.precision.apw_cutoff', {...numberHistogramQuantity, label: 'APW cutoff', scale: 'log'}) +registerFilter('results.method.simulation.dft.core_electron_treatment', termQuantity) +registerFilter('results.method.simulation.dft.jacobs_ladder', {...termQuantity, scale: 'log', label: 'Jacob\'s ladder'}) +registerFilter('results.method.simulation.dft.xc_functional_type', { ...termQuantity, scale: 'log', label: 'Jacob\'s ladder', @@ -324,14 +281,14 @@ registerFilter('results.method.simulation.dft.xc_functional_type', idDFT, { 'hybrid': {label: 'Hybrid'} } }) -registerFilter('results.method.simulation.dft.xc_functional_names', idDFT, {...termQuantityNonExclusive, scale: 'log', label: 'XC functional names'}) -registerFilter('results.method.simulation.dft.exact_exchange_mixing_factor', idDFT, {...numberHistogramQuantity, scale: 'log'}) -registerFilter('results.method.simulation.dft.hubbard_kanamori_model.u_effective', idDFT, {...numberHistogramQuantity, scale: 'log'}) -registerFilter('results.method.simulation.dft.relativity_method', idDFT, termQuantity) -registerFilter('results.method.simulation.tb.type', idTB, {...termQuantity, scale: 'log'}) -registerFilter('results.method.simulation.tb.localization_type', idTB, {...termQuantity, scale: 'log'}) -registerFilter('results.method.simulation.gw.type', idGW, {...termQuantity, label: 'GW type'}) -registerFilter('results.method.simulation.gw.starting_point_type', idGW, { +registerFilter('results.method.simulation.dft.xc_functional_names', {...termQuantityNonExclusive, scale: 'log', label: 'XC functional names'}) +registerFilter('results.method.simulation.dft.exact_exchange_mixing_factor', {...numberHistogramQuantity, scale: 'log'}) +registerFilter('results.method.simulation.dft.hubbard_kanamori_model.u_effective', {...numberHistogramQuantity, scale: 'log'}) +registerFilter('results.method.simulation.dft.relativity_method', termQuantity) +registerFilter('results.method.simulation.tb.type', {...termQuantity, scale: 'log'}) +registerFilter('results.method.simulation.tb.localization_type', {...termQuantity, scale: 'log'}) +registerFilter('results.method.simulation.gw.type', {...termQuantity, label: 'GW type'}) +registerFilter('results.method.simulation.gw.starting_point_type', { ...termQuantity, scale: 'log', options: { @@ -343,10 +300,10 @@ registerFilter('results.method.simulation.gw.starting_point_type', idGW, { 'HF': {label: 'HF'} } }) -registerFilter('results.method.simulation.gw.basis_set_type', idGW, {...termQuantity, scale: 'log'}) -registerFilter('results.method.simulation.bse.type', idBSE, termQuantity) -registerFilter('results.method.simulation.bse.solver', idBSE, termQuantity) -registerFilter('results.method.simulation.bse.starting_point_type', idBSE, { +registerFilter('results.method.simulation.gw.basis_set_type', {...termQuantity, scale: 'log'}) +registerFilter('results.method.simulation.bse.type', termQuantity) +registerFilter('results.method.simulation.bse.solver', termQuantity) +registerFilter('results.method.simulation.bse.starting_point_type', { ...termQuantity, scale: 'log', options: { @@ -358,47 +315,48 @@ registerFilter('results.method.simulation.bse.starting_point_type', idBSE, { 'HF': {label: 'HF'} } }) -registerFilter('results.method.simulation.bse.basis_set_type', idBSE, {...termQuantity, scale: 'log'}) -registerFilter('results.method.simulation.bse.gw_type', idBSE, {...termQuantity, scale: 'log', label: `GW type`}) -registerFilter('results.method.simulation.dmft.impurity_solver_type', idDMFT, {...termQuantity}) -registerFilter('results.method.simulation.dmft.magnetic_state', idDMFT, {...termQuantity}) -registerFilter('results.method.simulation.dmft.inverse_temperature', idDMFT, {...numberHistogramQuantity, scale: 'log'}) -registerFilter('results.method.simulation.dmft.u', idDMFT, {...numberHistogramQuantity, scale: 'log'}) -registerFilter('results.method.simulation.dmft.jh', idDMFT, {...numberHistogramQuantity, label: `JH`, scale: 'log'}) -registerFilter('results.method.simulation.dmft.analytical_continuation', idDMFT, {...termQuantity}) -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, label: 'Lab IDs'}) -registerFilter('results.eln.names', idELN, noAggQuantity) -registerFilter('results.eln.descriptions', idELN, noAggQuantity) -registerFilter('external_db', idAuthor, {...termQuantity, label: 'External database', scale: 'log'}) -registerFilter('authors.name', idAuthor, {...termQuantityNonExclusive, label: 'Author name'}) -registerFilter('upload_create_time', idAuthor, {...numberHistogramQuantity, scale: 'log'}) -registerFilter('entry_create_time', idAuthor, {...numberHistogramQuantity, scale: 'log'}) -registerFilter('datasets.dataset_name', idAuthor, {...termQuantityLarge, label: 'Dataset name'}) -registerFilter('datasets.doi', idAuthor, {...termQuantity, label: 'Dataset DOI'}) -registerFilter('datasets.dataset_id', idAuthor, termQuantity) -registerFilter('domain', idMetadata, termQuantity) -registerFilter('entry_id', idMetadata, termQuantity) -registerFilter('entry_name', idMetadata, termQuantity) -registerFilter('mainfile', idMetadata, termQuantity) -registerFilter('upload_id', idMetadata, termQuantity) -registerFilter('upload_name', idMetadata, termQuantity) -registerFilter('published', idMetadata, termQuantity) -registerFilter('main_author.user_id', idMetadata, termQuantity) -registerFilter('quantities', idMetadata, {...noAggQuantity, label: 'Metainfo definition', queryMode: 'all'}) -registerFilter('sections', idMetadata, {...noAggQuantity, label: 'Metainfo sections', queryMode: 'all'}) -registerFilter('section_defs.definition_qualified_name', idMetadata, {...noAggQuantity, label: 'Section defs qualified name', queryMode: 'all'}) -registerFilter('entry_references.target_entry_id', idMetadata, {...noAggQuantity, label: 'Entry references target entry id', queryMode: 'all'}) -registerFilter('entry_type', idMetadata, {...noAggQuantity, label: 'Entry type', queryMode: 'all'}) -registerFilter('entry_name.prefix', idMetadata, {...noAggQuantity, label: 'Entry name', queryMode: 'all'}) -registerFilter('results.material.material_id', idMetadata, termQuantity) -registerFilter('optimade_filter', idOptimade, {multiple: true, queryMode: 'all'}) -registerFilter('processed', idMetadata, {label: 'Processed', queryMode: 'all'}) -registerFilter('text_search_contents', idMetadata, {multiple: true, queryMode: 'all'}) -registerFilter('custom_quantities', idCustomQuantities, { +registerFilter('results.method.simulation.bse.basis_set_type', {...termQuantity, scale: 'log'}) +registerFilter('results.method.simulation.bse.gw_type', {...termQuantity, scale: 'log', label: `GW type`}) +registerFilter('results.method.simulation.dmft.impurity_solver_type', {...termQuantity}) +registerFilter('results.method.simulation.dmft.magnetic_state', {...termQuantity}) +registerFilter('results.method.simulation.dmft.inverse_temperature', {...numberHistogramQuantity, scale: 'log'}) +registerFilter('results.method.simulation.dmft.u', {...numberHistogramQuantity, scale: 'log'}) +registerFilter('results.method.simulation.dmft.jh', {...numberHistogramQuantity, label: `JH`, scale: 'log'}) +registerFilter('results.method.simulation.dmft.analytical_continuation', {...termQuantity}) +registerFilter('results.eln.sections', termQuantity) +registerFilter('results.eln.tags', termQuantity) +registerFilter('results.eln.methods', termQuantity) +registerFilter('results.eln.instruments', termQuantity) +registerFilter('results.eln.lab_ids', {...termQuantity, label: 'Lab IDs'}) +registerFilter('results.eln.names', noAggQuantity) +registerFilter('results.eln.descriptions', noAggQuantity) +registerFilter('external_db', {...termQuantity, label: 'External database', scale: 'log'}) +registerFilter('authors.name', {...termQuantityNonExclusive, label: 'Author name'}) +registerFilter('upload_create_time', {...numberHistogramQuantity, scale: 'log'}) +registerFilter('entry_create_time', {...numberHistogramQuantity, scale: 'log'}) +registerFilter('datasets.dataset_name', {...termQuantityLarge, label: 'Dataset name'}) +registerFilter('datasets.doi', {...termQuantity, label: 'Dataset DOI'}) +registerFilter('datasets.dataset_id', termQuantity) +registerFilter('domain', termQuantity) +registerFilter('entry_id', termQuantity) +registerFilter('entry_name', termQuantity) +registerFilter('mainfile', termQuantity) +registerFilter('upload_id', termQuantity) +registerFilter('upload_name', termQuantity) +registerFilter('published', termQuantity) +registerFilter('main_author.user_id', termQuantity) +registerFilter('quantities', {...noAggQuantity, label: 'Metainfo definition', queryMode: 'all'}) +registerFilter('sections', {...noAggQuantity, label: 'Metainfo sections', queryMode: 'all'}) +registerFilter('files', {...noAggQuantity, queryMode: 'all'}) +registerFilter('section_defs.definition_qualified_name', {...noAggQuantity, label: 'Section defs qualified name', queryMode: 'all'}) +registerFilter('entry_references.target_entry_id', {...noAggQuantity, label: 'Entry references target entry id', queryMode: 'all'}) +registerFilter('entry_type', {...noAggQuantity, label: 'Entry type', queryMode: 'all'}) +registerFilter('entry_name.prefix', {...noAggQuantity, label: 'Entry name', queryMode: 'all'}) +registerFilter('results.material.material_id', termQuantity) +registerFilter('optimade_filter', {multiple: true, queryMode: 'all'}) +registerFilter('processed', {label: 'Processed', queryMode: 'all'}) +registerFilter('text_search_contents', {multiple: true, queryMode: 'all'}) +registerFilter('custom_quantities', { serializerExact: value => { const jsonStr = JSON.stringify(value) const result = encodeURIComponent(jsonStr) @@ -426,7 +384,6 @@ registerFilter('custom_quantities', idCustomQuantities, { }) registerFilter( 'results.properties.spectroscopic.spectra.provenance.eels', - idSpectroscopic, {...nestedQuantity, label: 'Electron energy loss spectrum (EELS)'}, [ {name: 'detector_type', ...termQuantity}, @@ -437,7 +394,6 @@ registerFilter( ) registerFilter( 'results.properties.electronic.band_structure_electronic', - idElectronic, {...nestedQuantity, label: 'Band structure'}, [ {name: 'spin_polarized', label: 'Spin-polarized', ...termQuantityBool} @@ -445,7 +401,6 @@ registerFilter( ) registerFilter( 'results.properties.electronic.dos_electronic', - idElectronic, {...nestedQuantity, label: 'Density of states'}, [ {name: 'spin_polarized', label: 'Spin-polarized', ...termQuantityBool} @@ -453,7 +408,6 @@ registerFilter( ) registerFilter( 'results.properties.electronic.band_structure_electronic.band_gap', - idElectronic, nestedQuantity, [ {name: 'type', ...termQuantity}, @@ -462,17 +416,15 @@ registerFilter( ) registerFilter( 'results.properties.electronic.band_gap', - idElectronic, nestedQuantity, [ {name: 'type', ...termQuantity}, {name: 'value', ...numberHistogramQuantity, scale: 'log'} ] ) -registerFilter('results.properties.electronic.band_gap.provenance.label', idElectronic, termQuantity) +registerFilter('results.properties.electronic.band_gap.provenance.label', termQuantity) registerFilter( 'results.properties.optoelectronic.solar_cell', - idSolarCell, nestedQuantity, [ {name: 'efficiency', ...numberHistogramQuantity, scale: 'log'}, @@ -493,7 +445,6 @@ registerFilter( ) registerFilter( 'results.properties.catalytic.catalyst', - idCatalyst, nestedQuantity, [ {name: 'characterization_methods', ...termQuantity}, @@ -505,7 +456,6 @@ registerFilter( ) registerFilter( 'results.properties.catalytic.reaction', - idCatalyst, nestedQuantity, [ {name: 'name', ...termQuantity}, @@ -514,7 +464,6 @@ registerFilter( ) registerFilter( 'results.properties.catalytic.reaction.reaction_conditions', - idCatalyst, nestedQuantity, [ {name: 'temperature', ...numberHistogramQuantity, scale: 'linear'}, @@ -527,7 +476,6 @@ registerFilter( ) registerFilter( 'results.properties.catalytic.reaction.products', - idCatalyst, nestedQuantity, [ {name: 'name', ...termQuantityAllNonExclusive}, @@ -538,7 +486,6 @@ registerFilter( ) registerFilter( 'results.properties.catalytic.reaction.reactants', - idCatalyst, nestedQuantity, [ {name: 'name', ...termQuantityAllNonExclusive}, @@ -549,7 +496,6 @@ registerFilter( ) registerFilter( 'results.properties.catalytic.reaction.rates', - idCatalyst, nestedQuantity, [ {name: 'name', ...termQuantityAllNonExclusive}, @@ -573,7 +519,6 @@ registerFilter( ) registerFilter( 'results.properties.mechanical.bulk_modulus', - idMechanical, nestedQuantity, [ {name: 'type', ...termQuantity}, @@ -582,7 +527,6 @@ registerFilter( ) registerFilter( 'results.properties.mechanical.shear_modulus', - idMechanical, nestedQuantity, [ {name: 'type', ...termQuantity}, @@ -591,12 +535,10 @@ registerFilter( ) registerFilter( 'results.properties.available_properties', - idProperties, termQuantityAll ) registerFilter( 'results.properties.mechanical.energy_volume_curve', - idMechanical, nestedQuantity, [ {name: 'type', ...termQuantity} @@ -604,7 +546,6 @@ registerFilter( ) registerFilter( 'results.properties.geometry_optimization', - idGeometryOptimization, nestedQuantity, [ {name: 'final_energy_difference', ...numberHistogramQuantity, scale: 'log'}, @@ -614,7 +555,6 @@ registerFilter( ) registerFilter( 'results.properties.thermodynamic.trajectory', - idMolecularDynamics, nestedQuantity, [ {name: 'available_properties', ...termQuantityAll}, @@ -627,7 +567,6 @@ registerFilter( // query itself. registerFilter( 'visibility', - idMetadata, { ...noQueryQuantity, default: 'visible', @@ -639,7 +578,6 @@ registerFilter( // entries. registerFilter( 'combine', - undefined, { ...noQueryQuantity, default: true, @@ -650,9 +588,9 @@ registerFilter( // Exclusive: controls the way elements search is done. registerFilter( 'exclusive', - undefined, { ...noQueryQuantity, + dtype: DType.Boolean, default: false, description: "Search for entries with compositions that only (exclusively) contain the selected atoms. The default is to return all entries that have at least (inclusively) the selected atoms." } @@ -662,11 +600,10 @@ registerFilter( // into a single string. registerFilter( 'results.material.elements', - idElements, { widget: { - quantity: 'results.material.elements', - type: 'periodictable', + search_quantity: 'results.material.elements', + type: 'periodic_table', scale: 'log', layout: { sm: {w: 12, h: 8, minW: 12, minH: 8}, @@ -697,7 +634,6 @@ registerFilter( // Electronic properties: subset of results.properties.available_properties registerFilterOptions( 'electronic_properties', - idElectronic, 'results.properties.available_properties', 'Electronic properties', 'The electronic properties that are present in an entry.', @@ -713,7 +649,6 @@ registerFilterOptions( // Vibrational properties: subset of results.properties.available_properties registerFilterOptions( 'vibrational_properties', - idVibrational, 'results.properties.available_properties', 'Vibrational properties', 'The vibrational properties that are present in an entry.', @@ -728,7 +663,6 @@ registerFilterOptions( // Mechanical properties: subset of results.properties.available_properties registerFilterOptions( 'mechanical_properties', - idMechanical, 'results.properties.available_properties', 'Mechanical properties', 'The mechanical properties that are present in an entry.', @@ -742,7 +676,6 @@ registerFilterOptions( // Spectroscopic properties: subset of results.properties.available_properties registerFilterOptions( 'spectroscopic_properties', - idSpectroscopic, 'results.properties.available_properties', 'Spectroscopic properties', 'The spectroscopic properties that are present in an entry.', @@ -754,7 +687,6 @@ registerFilterOptions( // Thermodynamical properties: subset of results.properties.available_properties registerFilterOptions( 'thermodynamic_properties', - idMolecularDynamics, 'results.properties.available_properties', 'Thermodynamic properties', 'The thermodynamic properties that are present.', @@ -800,16 +732,16 @@ export function getStaticSuggestions(quantities, filterData) { } /** - * HOC that is used to preload search filters from all required schemas. This + * HOC that is used to preload search quantities from all required schemas. This * simplifies the rendering logic by first loading all schemas before rendering * any components that rely on them. */ -export const withFilters = (WrappedComponent) => { - const WithFilters = ({initialFilters, ...rest}) => { +export const withSearchQuantities = (WrappedComponent) => { + const WithFilters = ({initialSearchQuantities, ...rest}) => { // Here we load the python schemas, and determine which YAML/Nexus schemas // to load later. - const [yamlOptions, nexusOptions, initialFilterData, initialFilterGroups] = useMemo(() => { - const options = getOptions(initialFilters) + const [yamlOptions, nexusOptions, initialFilterData] = useMemo(() => { + const options = getOptions(initialSearchQuantities) const yamlOptions = options.filter((name) => name.includes(`#${yamlSchemaPrefix}`)) const nexusOptions = options.filter((name) => name.startsWith('nexus.')) @@ -817,44 +749,37 @@ export const withFilters = (WrappedComponent) => { // default filters. const defaultFilters = {} for (const [key, value] of Object.entries(defaultFilterData)) { - if (glob(key, [key], initialFilters?.exclude)) { + if (glob(key, [key], initialSearchQuantities?.exclude)) { defaultFilters[key] = value - if (value.group) { - addToGroup(defaultFilterGroups, value.group, key) - } } } // Load the python filters from plugins const pythonFilterData = {} - const mergedFilterGroups = {...defaultFilterGroups} for (const [name, def] of Object.entries(searchQuantities)) { - if (def.dynamic && glob(name, initialFilters?.include, initialFilters?.exclude)) { + if (def.dynamic && glob(name, initialSearchQuantities?.include, initialSearchQuantities?.exclude)) { const {path, schema} = parseQuantityName(name) const params = { name: path, - quantity: path, + quantity: name, schema, aggregatable: def.aggregatable } pythonFilterData[name] = new Filter(def, params) - addToGroup(mergedFilterGroups, idCustomQuantities, name) } } return [ yamlOptions, nexusOptions, - {...defaultFilters, ...pythonFilterData}, - mergedFilterGroups + {...defaultFilters, ...pythonFilterData} ] - }, [initialFilters]) + }, [initialSearchQuantities]) const metainfo = useGlobalMetainfo() const [loadingYaml, setLoadingYaml] = useState(yamlOptions.length) const [loadingNexus, setLoadingNexus] = useState(nexusOptions.length) const [filters, setFilters] = useState(initialFilterData) - const [filterGroups, setFilterGroups] = useState({...initialFilterGroups}) const { raiseError } = useErrors() // Nexus metainfo is loaded here once metainfo is ready @@ -863,7 +788,6 @@ export const withFilters = (WrappedComponent) => { const pkg = metainfo._packageDefs['nexus'] const sections = pkg.section_definitions const nexusFilters = {} - const nexusFilterGroups = {} for (const section of sections) { const sectionPath = `nexus.${section.name}` @@ -874,14 +798,14 @@ export const withFilters = (WrappedComponent) => { if (section?.more?.nx_category !== 'application') continue // Sections from which no quantities are included are skipped - if (!glob(sectionPath, initialFilters?.include, initialFilters?.exclude) && !initialFilters?.include.some(x => x.includes(sectionPath))) { + if (!glob(sectionPath, initialSearchQuantities?.include, initialSearchQuantities?.exclude) && !initialSearchQuantities?.include.some(x => x.includes(sectionPath))) { continue } // Add all included quantities recursively for (const [def, path, repeats] of getQuantities(section)) { const filterPath = `${sectionPath}.${path}` - const included = glob(filterPath, initialFilters?.include, initialFilters?.exclude) + const included = glob(filterPath, initialSearchQuantities?.include, initialSearchQuantities?.exclude) if (!included) continue const dtype = dtypeMap[getDatatype(def)] // TODO: For some Nexus quantities, the data types cannot be fetched. @@ -895,28 +819,17 @@ export const withFilters = (WrappedComponent) => { repeats: repeats } nexusFilters[filterPath] = new Filter(def, params) - addToGroup(nexusFilterGroups, idCustomQuantities, filterPath) } } setFilters((old) => ({...old, ...nexusFilters})) - setFilterGroups((old) => { - const newGroups = {...old} - for (const [groupName, names] of Object.entries(nexusFilterGroups)) { - for (const quantityName of [...names]) { - addToGroup(newGroups, groupName, quantityName) - } - } - return newGroups - }) setLoadingNexus(false) - }, [metainfo, nexusOptions, initialFilters]) + }, [metainfo, nexusOptions, initialSearchQuantities]) // YAML schemas are loaded here asynchronously useEffect(() => { if (!yamlOptions.length || !metainfo) return async function fetchSchemas(options) { const yamlFilters = {} - const yamlFilterGroups = {} for (const schemaPath of options) { let schemaDefinition try { @@ -935,24 +848,14 @@ export const withFilters = (WrappedComponent) => { throw Error(`Unable to load the data type for ${path}.`) } const filterPath = `data.${path}${schemaSeparator}${schemaPath}` - const included = glob(filterPath, initialFilters?.include, initialFilters?.exclude) + const included = glob(filterPath, initialSearchQuantities?.include, initialSearchQuantities?.exclude) if (!included) continue const apiPath = `${filterPath}${dtypeSeparator}${dtype}` const {path: quantity, schema} = parseQuantityName(filterPath) yamlFilters[filterPath] = new Filter(def, {name: path, schema, quantity, requestQuantity: apiPath}) - addToGroup(yamlFilterGroups, idCustomQuantities, filterPath) } } setFilters((old) => ({...old, ...yamlFilters})) - setFilterGroups((old) => { - const newGroups = {...old} - for (const [groupName, names] of Object.entries(yamlFilterGroups)) { - for (const quantityName of [...names]) { - addToGroup(newGroups, groupName, quantityName) - } - } - return newGroups - }) setLoadingYaml(false) } @@ -960,18 +863,18 @@ export const withFilters = (WrappedComponent) => { // and the required filters are registered from each. const yamlSchemas = new Set(yamlOptions.map(x => x.split('#').pop())) fetchSchemas(yamlSchemas) - }, [yamlOptions, metainfo, raiseError, initialFilters]) + }, [yamlOptions, metainfo, raiseError, initialSearchQuantities]) return (loadingYaml || loadingNexus) ? <Box margin={1}> <Typography>Loading the required schemas...</Typography> </Box> - : <WrappedComponent {...rest} initialFilterData={filters} initialFilterGroups={filterGroups}/> + : <WrappedComponent {...rest} initialSearchQuantities={filters}/> } WithFilters.displayName = `withFilter(${WrappedComponent.displayName || WrappedComponent.name})` WithFilters.propTypes = { - initialFilters: PropTypes.object // Determines which filters are available + initialSearchQuantities: PropTypes.object // Determines which filters are available } return WithFilters diff --git a/gui/src/components/search/FilterRegistry.spec.js b/gui/src/components/search/FilterRegistry.spec.js index 08c3fb53b2409806d3a05a96921bcd3dc724da09..6c75e670fb8641258ef840d23140029e4357a439 100644 --- a/gui/src/components/search/FilterRegistry.spec.js +++ b/gui/src/components/search/FilterRegistry.spec.js @@ -17,10 +17,10 @@ */ import React from 'react' import { render, screen } from '../conftest.spec' -import { withFilters } from './FilterRegistry' +import { withSearchQuantities } from './FilterRegistry' -const TestComponent = withFilters(({initialFilterData}) => { - return Object.keys(initialFilterData).map((filter) => <div key={filter}>{filter}</div>) +const TestComponent = withSearchQuantities(({initialSearchQuantities}) => { + return Object.keys(initialSearchQuantities).map((filter) => <div key={filter}>{filter}</div>) }) test.each([ @@ -28,7 +28,7 @@ test.each([ ['nomad include', {include: ['mainfile']}, ['mainfile'], []], ['nomad exclude', {exclude: ['mainfile']}, [], ['mainfile']] ])('%s', async (name, config, include, exclude) => { - render(<TestComponent initialFilters={config}/>) + render(<TestComponent initialSearchQuantities={config}/>) for (const filter of include) { expect(await screen.findByText(filter, {exact: true})).toBeInTheDocument() } diff --git a/gui/src/components/search/FilterTitle.js b/gui/src/components/search/FilterTitle.js index 625a58af43503d569999c997b8b6078cc5e9c971..941a3537eba1176cff1f280492255afca5140b3e 100644 --- a/gui/src/components/search/FilterTitle.js +++ b/gui/src/components/search/FilterTitle.js @@ -21,7 +21,7 @@ import { Typography, Tooltip } from '@material-ui/core' import PropTypes from 'prop-types' import clsx from 'clsx' import { useSearchContext } from './SearchContext' -import { inputSectionContext } from './input/InputSection' +import { inputSectionContext } from './input/InputNestedObject' import { Unit } from '../units/Unit' import { useUnitContext } from '../units/UnitContext' import Ellipsis from '../visualization/Ellipsis' @@ -36,8 +36,7 @@ const useStaticStyles = makeStyles(theme => ({ }, text: { }, - title: { - fontWeight: 600, + subtitle2: { color: theme.palette.grey[800] }, right: { @@ -83,7 +82,7 @@ const FilterTitle = React.memo(({ let finalUnit if (unit) { finalUnit = new Unit(unit).label() - } else if (filterData[quantity]?.unit) { + } else if (quantity && filterData[quantity]?.unit) { finalUnit = new Unit(filterData[quantity].unit).toSystem(units).label() } if (finalUnit) { @@ -94,17 +93,14 @@ const FilterTitle = React.memo(({ }, [filterData, quantity, units, label, unit, disableUnit]) // Determine the final description - const finalDescription = description || filterData[quantity]?.description || '' - let tooltip = '' - if (finalDescription && quantity) { - tooltip = ( - <> - <Typography>{finalLabel}</Typography> - <b>Description: </b>{finalDescription}<br/> - <b>Path: </b>{quantity} - </> - ) - } + const finalDescription = description || (quantity && filterData[quantity]?.description) || '' + const tooltip = (quantity) + ? <> + <Typography>{finalLabel}</Typography> + <b>Description: </b>{finalDescription || '-'}<br/> + <b>Path: </b>{quantity} + </> + : finalDescription || '' return <Tooltip title={tooltip} interactive enterDelay={400} enterNextDelay={400} {...(TooltipProps || {})}> <div className={clsx(className, styles.root, @@ -114,7 +110,7 @@ const FilterTitle = React.memo(({ )}> <Typography noWrap={noWrap} - className={clsx(styles.text, (!section) && styles.title)} + className={clsx(styles.text, (!section) && (variant === "subtitle2") && styles.subtitle2)} variant={variant} onMouseDown={onMouseDown} onMouseUp={onMouseUp} diff --git a/gui/src/components/search/Query.spec.js b/gui/src/components/search/Query.spec.js index f8c5f8d0b3d57d71b036057fe7d977ae7c20fb8a..ebff06aa38ae3697bb650b3aff2b35f93de32dbb 100644 --- a/gui/src/components/search/Query.spec.js +++ b/gui/src/components/search/Query.spec.js @@ -35,7 +35,7 @@ test.each([ initialPagination={context.pagination} initialColumns={context.columns} initialRows={context.rows} - initialFilterMenus={context.filter_menus} + initialMenu={context?.menu} initialFiltersLocked={context.filters_locked} initialDashboard={context?.dashboard} initialFilterValues={{[quantity]: input}} diff --git a/gui/src/components/search/SearchBar.spec.js b/gui/src/components/search/SearchBar.spec.js index 4411240dadbd8e80f5e538cec098f9caba2d1c96..2b49c9c78f38cd324eb251d34e9ba02b3a60296d 100644 --- a/gui/src/components/search/SearchBar.spec.js +++ b/gui/src/components/search/SearchBar.spec.js @@ -51,7 +51,7 @@ describe('searchbar queries', function() { initialPagination={context.pagination} initialColumns={context.columns} initialRows={context.rows} - initialFilterMenus={context.filter_menus} + initialMenu={context?.menu} initialFiltersLocked={context.filters_locked} initialDashboard={context?.dashboard} initialSearchSyntaxes={context?.search_syntaxes} @@ -91,7 +91,7 @@ describe('suggestions: history', function() { initialPagination={context.pagination} initialColumns={context.columns} initialRows={context.rows} - initialFilterMenus={context.filter_menus} + initialMenu={context?.menu} initialFiltersLocked={context.filters_locked} initialDashboard={context?.dashboard} initialSearchSyntaxes={context?.search_syntaxes} @@ -116,7 +116,7 @@ describe('suggestions: history', function() { initialPagination={context.pagination} initialColumns={context.columns} initialRows={context.rows} - initialFilterMenus={context.filter_menus} + initialMenu={context?.menu} initialFiltersLocked={context.filters_locked} initialDashboard={context?.dashboard} initialSearchSyntaxes={context?.search_syntaxes} @@ -140,7 +140,7 @@ describe('suggestions: history', function() { initialPagination={context.pagination} initialColumns={context.columns} initialRows={context.rows} - initialFilterMenus={context.filter_menus} + initialMenu={context?.menu} initialFiltersLocked={context.filters_locked} initialDashboard={context?.dashboard} initialSearchSyntaxes={context?.search_syntaxes} diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 67c6b072f7fe59912521189db4b28ad77669d67a..bbedb85215c87cafd3d01920d48261c81ba5c8ed 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -70,9 +70,9 @@ import { useErrors } from '../errors' import { combinePagination, addColumnDefaults } from '../datatable/Datatable' import UploadStatusIcon from '../uploads/UploadStatusIcon' import { getWidgetsObject } from './widgets/Widget' -import { inputSectionContext } from './input/InputSection' +import { inputSectionContext } from './input/InputNestedObject' import { SearchSuggestion } from './SearchSuggestion' -import { withFilters } from './FilterRegistry' +import { withSearchQuantities } from './FilterRegistry' import { useUnitContext } from '../units/UnitContext' const useWidthConstrainedStyles = makeStyles(theme => ({ @@ -209,11 +209,10 @@ export const SearchContextRaw = React.memo(({ initialFiltersLocked, initialColumns, initialRows, - initialFilterMenus, + initialMenu, initialPagination, initialDashboard, - initialFilterData, - initialFilterGroups, + initialSearchQuantities, initialFilterValues, initialSearchSyntaxes, id, @@ -239,10 +238,10 @@ export const SearchContextRaw = React.memo(({ // Initialize the set of filters that are available in this context const {initialFilterPaths, initialFilterAbbreviations} = useMemo(() => { return { - initialFilterPaths: new Set(Object.keys(initialFilterData)), - initialFilterAbbreviations: getAbbreviations(initialFilterData) + initialFilterPaths: new Set(Object.keys(initialSearchQuantities)), + initialFilterAbbreviations: getAbbreviations(initialSearchQuantities) } - }, [initialFilterData]) + }, [initialSearchQuantities]) // The final set of columns const columns = useMemo(() => { @@ -255,7 +254,7 @@ export const SearchContextRaw = React.memo(({ throw Error(`Invalid JMESPath query in the app columns: ${option.quantity}`) } option.sortable = parseResults.quantity === option.quantity - const filter = initialFilterData[parseResults.quantity] + const filter = initialSearchQuantities[parseResults.quantity] const storageUnit = filter?.unit const displayUnit = option.unit const finalUnit = (storageUnit || displayUnit) @@ -364,11 +363,11 @@ export const SearchContextRaw = React.memo(({ // Add key, defaults, and custom overrides options = options.map((column) => ({...column, key: column.quantity})) - addColumnDefaults(options, undefined, initialFilterData) + addColumnDefaults(options, undefined, initialSearchQuantities) options = options.map((column) => ({...column, ...(overrides[column.quantity] || {})})) return options - }, [initialFilterData, initialColumns, user, units]) + }, [initialSearchQuantities, initialColumns, user, units]) // The final row configuration const rows = useMemo(() => { @@ -376,9 +375,9 @@ export const SearchContextRaw = React.memo(({ }, [initialRows]) // The final menu configuration - const filterMenus = useMemo(() => { - return initialFilterMenus || undefined - }, [initialFilterMenus]) + const menu = useMemo(() => { + return initialMenu + }, [initialMenu]) // The final dashboard configuration const dashboard = useMemo(() => { @@ -391,7 +390,7 @@ export const SearchContextRaw = React.memo(({ // default values as specified in filter registry are loaded const [initialQuery, initialAggs, filterDefaults] = useMemo(() => { const filterDefaults = {} - for (const [key, value] of Object.entries(initialFilterData)) { + for (const [key, value] of Object.entries(initialSearchQuantities)) { if (!isNil(value.default)) { filterDefaults[key] = value.default } @@ -403,11 +402,11 @@ export const SearchContextRaw = React.memo(({ } } return [ - parseQueries(initialFilterValues, initialFilterData, initialFilterAbbreviations.filterFullnames), + parseQueries(initialFilterValues, initialSearchQuantities, initialFilterAbbreviations.filterFullnames), initialAggs, filterDefaults ] - }, [initialFilterData, initialFilterAbbreviations, initialFilterValues, initialFilterPaths]) + }, [initialSearchQuantities, initialFilterAbbreviations, initialFilterValues, initialFilterPaths]) // Atoms + setters and getters are used instead of regular React states to // avoid re-rendering components that are not depending on these values. The @@ -537,7 +536,7 @@ export const SearchContextRaw = React.memo(({ // modified later. const filtersDataState = atom({ key: `filtersData_${contextID}`, - default: initialFilterData + default: initialSearchQuantities }) const filterNamesState = selector({ @@ -587,7 +586,7 @@ export const SearchContextRaw = React.memo(({ default: initialPagination }) - const guiLocked = parseQueries(initialFiltersLocked, initialFilterData, initialFilterAbbreviations.filterFullnames) + const guiLocked = parseQueries(initialFiltersLocked, initialSearchQuantities, initialFilterAbbreviations.filterFullnames) const lockedFamily = atomFamily({ key: `lockedFamily_${contextID}`, default: (name) => guiLocked?.[name] @@ -1200,7 +1199,7 @@ export const SearchContextRaw = React.memo(({ resource, contextID, initialQuery, - initialFilterData, + initialSearchQuantities, initialFilterAbbreviations, initialFiltersLocked, initialPagination, @@ -1674,10 +1673,9 @@ export const SearchContextRaw = React.memo(({ resource, columns, rows, - filterMenus, + menu, filters, filterData: filtersData, - filterGroups: initialFilterGroups, filterFullnames, filterAbbreviations, searchSyntaxes: initialSearchSyntaxes, @@ -1725,10 +1723,9 @@ export const SearchContextRaw = React.memo(({ resource, rows, columns, - filterMenus, + menu, filters, filtersData, - initialFilterGroups, filterFullnames, filterAbbreviations, initialSearchSyntaxes, @@ -1790,11 +1787,10 @@ SearchContextRaw.propTypes = { initialFiltersLocked: PropTypes.object, initialColumns: PropTypes.arrayOf(PropTypes.object), initialRows: PropTypes.object, - initialFilterMenus: PropTypes.object, + initialMenu: PropTypes.object, initialPagination: PropTypes.object, initialDashboard: PropTypes.object, - initialFilterData: PropTypes.object, // Determines which filters are available - initialFilterGroups: PropTypes.object, // Maps filter groups to a set of filter names + initialSearchQuantities: PropTypes.object, // Determines which quantities are available for search initialFilterValues: PropTypes.object, // Here one can provide default filter values initialSearchSyntaxes: PropTypes.object, // Determines which syntaxes are supported children: PropTypes.node, @@ -1806,8 +1802,8 @@ SearchContextRaw.defaultProps = { suggestionHistorySize: 20 } -export const FreeformSearchContext = withFilters(SearchContextRaw) -export const SearchContext = compose(withQueryString, withFilters)(SearchContextRaw) +export const FreeformSearchContext = withSearchQuantities(SearchContextRaw) +export const SearchContext = compose(withQueryString, withSearchQuantities)(SearchContextRaw) /** * Hook for accessing the current SearchContext. diff --git a/gui/src/components/search/SearchContext.spec.js b/gui/src/components/search/SearchContext.spec.js index be0e37b67ce88c9544c8a51b46d290f9209ff550..282c4fe9df1ea2d7093fce5a2511b0630a67e6a9 100644 --- a/gui/src/components/search/SearchContext.spec.js +++ b/gui/src/components/search/SearchContext.spec.js @@ -134,7 +134,7 @@ describe.only('test that final column information is generated correctly', funct ])('%s', async (name, column, filter, label) => { const quantity = 'test_filter' const { result: resultUseSearchContext } = renderHook(() => useSearchContext(), { wrapper: (props) => <Wrapper - initialFilterData={{[quantity]: filter}} + initialSearchQuantities={{[quantity]: filter}} initialColumns={[{quantity, ...column}]} {...props} />}) diff --git a/gui/src/components/search/SearchMenu.js b/gui/src/components/search/SearchMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..521cbc0f1ad886cf84069f4878721757cb666620 --- /dev/null +++ b/gui/src/components/search/SearchMenu.js @@ -0,0 +1,287 @@ +/* + * 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, { useEffect, useMemo, useState, useCallback } from 'react' +import PropTypes from 'prop-types' +import { isEmpty } from 'lodash' +import { + Menu as MenuMUI +} from '@material-ui/core' +import { + Menu, + MenuHeader, + MenuItem, + MenuContent, + MenuSubMenus, + MenuSettings +} from './menus/Menu' +import ArrowBackIcon from '@material-ui/icons/ArrowBack' +import MoreVert from '@material-ui/icons/MoreVert' +import { Action } from '../Actions' +import { useSearchContext } from './SearchContext' +import { delay, camelCase } from '../../utils' +import InputTerms from './input/InputTerms' +import InputHistogram from './input/InputHistogram' +import InputPeriodicTable from './input/InputPeriodicTable' +import InputNestedObject from './input/InputNestedObject' +import InputVisibility from './input/InputVisibility' +import InputDefinitions from './input/InputDefinitions' +import InputOptimade from './input/InputOptimade' +import InputCustomQuantities from './input/InputCustomQuantities' +import { InputGrid, InputGridItem } from './input/InputGrid' + +function createItems(items, visible) { + return (items || []).map((item, index) => { + let props = Object.fromEntries(Object.entries(item).map(([key, value]) => [camelCase(key), value])) + props = {visible, ...props} + let Comp + switch (item.type) { + case 'terms': + Comp = InputTerms + break + case 'histogram': + Comp = InputHistogram + break + case 'periodic_table': + Comp = InputPeriodicTable + break + case 'nested_object': + Comp = InputNestedObject + props.children = createItems(item.items, visible) + break + case 'visibility': + Comp = InputVisibility + break + case 'definitions': + Comp = InputDefinitions + break + case 'optimade': + Comp = InputOptimade + break + case 'custom_quantities': + Comp = InputCustomQuantities + break + case 'menu': + Comp = MenuItem + props.disableButton = isEmpty(item.items) + props.id = index + props.level = item.indentation + break + default: + throw Error(`Unknown menu item type: ${item.type}.`) + } + + return <InputGridItem + key={index} + disablePadding={item.type === 'menu'} + xs={item.type === 'menu' ? 12 : item.width}> + {Comp && <Comp {...props}/>} + </InputGridItem> + }) +} +/** + * Creates an instance of FilterMenu based on the menu in the SearchContext. + */ +const SearchMenu = React.memo(() => { + const [selected, setSelected] = React.useState() + const [isSubMenuOpen, setIsSubMenuOpen] = React.useState(false) + const [loaded, setLoaded] = useState(false) + const {menu, useSetIsMenuOpen, useIsMenuOpen, useIsCollapsed, useSetIsCollapsed} = useSearchContext() + const [isMenuOpen, setIsMenuOpen] = [useIsMenuOpen(), useSetIsMenuOpen()] + const [isCollapsed, setIsCollapsed] = [useIsCollapsed(), useSetIsCollapsed()] + const handleMenuCollapse = useCallback(() => setIsCollapsed(old => !old), [setIsCollapsed]) + + // Rendering the submenus is delayed on the event queue: this makes loading + // the search page more responsive by first loading everything else. + useEffect(() => { + delay(() => { setLoaded(true) }) + }, []) + + useEffect(() => { + setIsSubMenuOpen(isMenuOpen) + }, [isMenuOpen]) + + const [anchorEl, setAnchorEl] = React.useState(null) + const isSettingsOpen = Boolean(anchorEl) + + // Callbacks + const openMenu = useCallback((event) => { + setAnchorEl(event.currentTarget) + }, []) + const closeMenu = useCallback(() => { + setAnchorEl(null) + }, []) + const handleOpenChange = useCallback((value) => { + setIsMenuOpen(value) + setIsSubMenuOpen(value) + }, [setIsMenuOpen]) + + // Create the list of menu items + const menuItems = useMemo( + () => createItems(menu?.items || [], true), + [menu] + ) + + // Create the list of submenus + const subMenus = useMemo(() => { + return (menu?.items || []) + .map((item, index) => + item.type === 'menu' + ? <SearchSubMenu + key={index} + menu={item} + selected={selected} + open={isSubMenuOpen} + visible={index === selected} + onOpenChange={handleOpenChange} + /> + : null + ) + }, [menu, selected, handleOpenChange, isSubMenuOpen]) + + return <Menu + size={menu?.size} + open={true} + collapsed={isCollapsed} + onCollapsedChanged={setIsCollapsed} + subMenuOpen={isSubMenuOpen} + visible={true} + selected={selected} + onSelectedChange={(value) => { + setIsMenuOpen(old => value !== selected ? true : !old) + setSelected(value) + setIsSubMenuOpen(old => value !== selected ? true : !old) + }} + > + <MenuHeader + title="Filters" + actions={<> + <Action + tooltip={'Collapse menu'} + onClick={handleMenuCollapse} + > + <ArrowBackIcon fontSize="small"/> + </Action> + <Action + tooltip="Options" + onClick={openMenu} + > + <MoreVert fontSize="small"/> + </Action> + <MenuMUI + anchorEl={anchorEl} + open={isSettingsOpen} + onClose={closeMenu} + getContentAnchorEl={null} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + keepMounted + > + <div> + <MenuSettings/> + </div> + </MenuMUI> + </>} + /> + <MenuContent> + <InputGrid>{menuItems}</InputGrid> + </MenuContent> + <MenuSubMenus> + {loaded && subMenus} + </MenuSubMenus> + </Menu> +}) + +/** + * Submenu that pops on the right side of the parent menu. + */ +const SearchSubMenu = React.memo(({ + menu, + open, + visible, + onOpenChange +}) => { + const [selected, setSelected] = React.useState() + const [isSubMenuOpen, setIsSubMenuOpen] = React.useState(false) + + // Callbacks + const handleClose = useCallback(() => onOpenChange(false), [onOpenChange]) + const handleOpenChange = useCallback((value) => { + setIsSubMenuOpen(value) + }, []) + + // Create the list of menu items + const menuItems = useMemo( + () => createItems(menu.items, visible), + [menu.items, visible] + ) + + // Create the list of submenus + const subMenus = useMemo(() => { + return (menu.items || []) + .map((item, index) => + item.type === 'menu' + ? <SearchSubMenu + key={index} + menu={item} + open={open && isSubMenuOpen} + visible={visible && index === selected} + onOpenChange={handleOpenChange} + /> + : null + ) + }, [menu, selected, handleOpenChange, isSubMenuOpen, open, visible]) + + return <Menu + size={menu.size} + open={open} + subMenuOpen={open && isSubMenuOpen} + visible={visible} + selected={selected} + onSelectedChange={(value) => { + setSelected(value) + setIsSubMenuOpen(old => value !== selected ? true : !old) + }} + > + <MenuHeader + title={menu.title} + actions={<> + <Action + tooltip={'Close menu'} + onClick={handleClose} + > + <ArrowBackIcon fontSize="small"/> + </Action> + </>} + /> + <MenuContent> + <InputGrid>{menuItems}</InputGrid> + </MenuContent> + <MenuSubMenus> + {subMenus} + </MenuSubMenus> + </Menu> +}) +SearchSubMenu.propTypes = { + menu: PropTypes.object, + open: PropTypes.bool, + visible: PropTypes.bool, + selected: PropTypes.number, + onOpenChange: PropTypes.func +} + +export default SearchMenu diff --git a/gui/src/components/search/SearchMenu.spec.js b/gui/src/components/search/SearchMenu.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..bfd7e8bd537770de35da48558ad3dc168a7b5cc9 --- /dev/null +++ b/gui/src/components/search/SearchMenu.spec.js @@ -0,0 +1,100 @@ +/* + * 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, { useMemo } from 'react' +import { render, screen } from '../conftest.spec' +import { SearchContextRaw } from './SearchContext' +import SearchMenu from './SearchMenu' +import { Filter } from './Filter' +import { DType } from '../../utils' + +// We set an initial mock for the SearchContext module +const mockSetFilter = jest.fn() +const mockUseMemo = useMemo +jest.mock('./SearchContext', () => ({ + ...jest.requireActual('./SearchContext'), + useSearchContext: () => ({ + ...jest.requireActual('./SearchContext').useSearchContext(), + useAgg: (quantity, visible, id, config) => { + const response = mockUseMemo(() => { + return visible + ? {data: quantity === 'my_histogram' + ? [{value: 1, count: 5}] + : [ + {value: 'A', count: 6}, + {value: 'B', count: 5}, + {value: 'C', count: 4}, + {value: 'D', count: 3}, + {value: 'E', count: 2}, + {value: 'F', count: 1} + ].slice(0, config.size)} + : undefined + }, []) + return response + }, + useFilterState: jest.fn((quantity) => { + const response = mockUseMemo(() => { + return [undefined, mockSetFilter] + }, []) + return response + }), + useResults: jest.fn((quantity) => { + const response = mockUseMemo(() => { + return undefined + }, []) + return response + }) + }) +})) + +describe('test menu items', () => { + test.each([ + ['terms', {type: 'terms', search_quantity: 'my_terms'}, 'My terms'], + ['histogram', {type: 'histogram', x: {search_quantity: 'my_histogram'}}, 'My histogram'], + ['periodic table', {type: 'periodic_table', search_quantity: 'my_periodic_table'}, 'My periodic table'], + ['nested object', {type: 'nested_object', path: 'my_nested_object'}, 'My nested object'], + ['visibility', {type: 'visibility'}, 'Visibility'], + ['definitions', {type: 'definitions'}, 'Definitions'], + ['optimade', {type: 'optimade'}, 'Optimade query'], + ['custom quantities', {type: 'custom_quantities'}, 'Custom quantities'], + ['menu', {type: 'menu', 'title': 'Submenu'}, 'Submenu'] + ])('%s', async (name, menuItem, text) => { + const mainmenu = { + items: [menuItem] + } + render( + <SearchContextRaw + resource="entries" + id='entries' + initialSearchQuantities={{ + my_terms: new Filter(undefined, {quantity: 'my_terms', dtype: DType.String}), + my_histogram: new Filter(undefined, {quantity: 'my_histogram', dtype: DType.Int}), + my_periodic_table: new Filter(undefined, {quantity: 'my_periodic_table', dtype: DType.String}), + my_nested_object: new Filter(undefined, {quantity: 'my_nested_object', dtype: DType.String}), + visibility: new Filter(undefined, {quantity: 'visibility', dtype: DType.String, global: true}), + quantities: new Filter(undefined, {quantity: 'quantities', dtype: DType.String}) + }} + initialMenu={mainmenu} + > + <SearchMenu/> + </SearchContextRaw> + ) + // Test that the menu item is displayed + screen.getByText(text) + }) +}) diff --git a/gui/src/components/search/SearchPage.js b/gui/src/components/search/SearchPage.js index 38f5ae72a9b29e51e6c2f0089657dcd99ddadf4a..5b578374683ec917ea7d3f952170deaea5c9bbe8 100644 --- a/gui/src/components/search/SearchPage.js +++ b/gui/src/components/search/SearchPage.js @@ -20,8 +20,7 @@ import clsx from 'clsx' import PropTypes from 'prop-types' import { Box } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' -import FilterMainMenu from './menus/FilterMainMenu' -import { collapsedMenuWidth } from './menus/FilterMenu' +import SearchMenu from './SearchMenu' import SearchBar from './SearchBar' import Query from './Query.js' import { SearchResults } from './SearchResults' @@ -47,9 +46,6 @@ const useStyles = makeStyles(theme => { height: '100%', zIndex: 2 }, - leftColumnCollapsed: { - maxWidth: `${collapsedMenuWidth}rem` - }, center: { flexGrow: 1, height: '100%', @@ -86,23 +82,12 @@ const SearchPage = React.memo(({ header }) => { const styles = useStyles() - const { - useIsMenuOpen, - useSetIsMenuOpen, - useIsCollapsed, - useSetIsCollapsed - } = useSearchContext() + const {useIsMenuOpen, useSetIsMenuOpen} = useSearchContext() const [isMenuOpen, setIsMenuOpen] = [useIsMenuOpen(), useSetIsMenuOpen()] - const [isCollapsed, setIsCollapsed] = [useIsCollapsed(), useSetIsCollapsed()] return <div className={styles.root}> - <div className={clsx(styles.leftColumn, isCollapsed && styles.leftColumnCollapsed)}> - <FilterMainMenu - open={isMenuOpen} - onOpenChange={setIsMenuOpen} - collapsed={isCollapsed} - onCollapsedChange={setIsCollapsed} - /> + <div className={clsx(styles.leftColumn)}> + <SearchMenu/> </div> <div className={styles.center} onClick={() => setIsMenuOpen(false)}> <Box margin={2.5} paddingBottom={3}> diff --git a/gui/src/components/search/SearchPage.spec.js b/gui/src/components/search/SearchPage.spec.js index bf0936a71182b7ed176b297d3653c8eb062e9eea..94619b2e5e9b3fe946eb1d0a318ca90006691f9a 100644 --- a/gui/src/components/search/SearchPage.spec.js +++ b/gui/src/components/search/SearchPage.spec.js @@ -16,21 +16,43 @@ * limitations under the License. */ -import React from 'react' -import { render, startAPI, closeAPI } from '../conftest.spec' -import { expectFilterMainMenu, expectSearchResults } from './conftest.spec' +import React, { useMemo } from 'react' +import { render, screen } from '../conftest.spec' +import { expectMenu, expectSearchResults } from './conftest.spec' import { ui } from '../../config' import { SearchContext } from './SearchContext' import SearchPage from './SearchPage' -import { minutes } from '../../setupTests' -describe('', () => { - beforeAll(async () => { - await startAPI('tests.states.search.search', 'tests/data/search/searchpage') - }) - afterAll(() => closeAPI()) +// We set an initial mock for the SearchContext module +const mockSetFilter = jest.fn() +const mockUseMemo = useMemo +jest.mock('./SearchContext', () => ({ + ...jest.requireActual('./SearchContext'), + useSearchContext: () => ({ + ...jest.requireActual('./SearchContext').useSearchContext(), + useAgg: (quantity, visible, id, config) => { + const response = mockUseMemo(() => { + return undefined + }, []) + return response + }, + useFilterState: jest.fn((quantity) => { + const response = mockUseMemo(() => { + return [undefined, mockSetFilter] + }, []) + return response + }), + useResults: jest.fn((quantity) => { + const response = mockUseMemo(() => { + return {data: [{}], pagination: {total: 1}} + }, []) + return response + }) + }) +})) - test('renders search page correctly', async () => { +describe('', () => { + test('render search page components', async () => { const context = ui.apps.options.entries render( <SearchContext @@ -38,7 +60,7 @@ describe('', () => { initialPagination={context.pagination} initialColumns={context.columns} initialRows={context.rows} - initialFilterMenus={context.filter_menus} + initialMenu={context.menu} initialFiltersLocked={context.filters_locked} initialDashboard={context?.dashboard} initialSearchSyntaxes={context?.search_syntaxes} @@ -47,8 +69,16 @@ describe('', () => { <SearchPage /> </SearchContext> ) + // Test that menu is shown + await expectMenu(context.menu) - await expectFilterMainMenu(context) - await expectSearchResults(context) - }, 5 * minutes) + // Test that search bar is shown + screen.getByPlaceholderText('Type your query or keyword here') + + // Test that query is shown + screen.getByText('Your query will be shown here') + + // Test that results table is shown + await expectSearchResults(context.columns) + }) }) diff --git a/gui/src/components/search/SearchResults.spec.js b/gui/src/components/search/SearchResults.spec.js index ca521c758399193701c2c4c1a7d81493992beff9..cdfc0de37dcec57bc1d58a0dbf21a711d77328b5 100644 --- a/gui/src/components/search/SearchResults.spec.js +++ b/gui/src/components/search/SearchResults.spec.js @@ -80,7 +80,7 @@ describe('test search columns', () => { <SearchContextRaw resource="entries" id='entries' - initialFilterData={{ + initialSearchQuantities={{ 'my_float': new Filter(undefined, {quantity: 'my_float', dtype: DType.Float}), 'my_boolean': new Filter(undefined, {quantity: 'my_boolean', dtype: DType.Boolean}), 'my_float_array': new Filter(undefined, {quantity: 'my_float_array', dtype: DType.Float, shape: '*'}), diff --git a/gui/src/components/search/conftest.spec.js b/gui/src/components/search/conftest.spec.js index b2bf298476f69e8c3e5dbc75c3de8ac51e5541b7..0e49362b7a300af6c8d0ff42f35c5cd03f4e287b 100644 --- a/gui/src/components/search/conftest.spec.js +++ b/gui/src/components/search/conftest.spec.js @@ -30,7 +30,6 @@ import { format } from 'date-fns' import { DType, getDisplayLabel, parseJMESPath } from '../../utils' import { Unit } from '../units/Unit' import { ui } from '../../config' -import { menuMap } from './menus/FilterMainMenu' /*****************************************************************************/ // Renders @@ -129,7 +128,7 @@ export async function expectWidgetTerms(widget, loaded, items, prompt, root = sc ) // Test immediately displayed elements - await expectFilterTitle(widget.quantity) + await expectFilterTitle(widget.search_quantity) // Check that placeholder disappears if (!loaded) { @@ -153,6 +152,28 @@ export async function expectWidgetTerms(widget, loaded, items, prompt, root = sc } } +/** + * Tests that an InputItem is displayed. + * + * @param {string} item The item specification + * @param {object} root The container to work on. + */ +export async function expectInputItem(item, root = screen) { + root.getByText(item.label) +} + +/** + * Tests that a WidgetHistogram is rendered with the given contents. + * @param {object} widget The widget setup + * @param {bool} loaded Whether the data is already loaded + */ +export async function expectWidgetHistogram(widget, root = screen) { + // Test immediately displayed elements + const xAxis = widget.x + const {quantity: x} = parseJMESPath(xAxis.search_quantity) + await expectFilterTitle(x, xAxis.title, xAxis.unit, undefined, undefined, root) +} + /** * Tests that a WidgetScatterPlot is rendered with the given contents. * @param {object} widget The widget setup @@ -160,13 +181,13 @@ export async function expectWidgetTerms(widget, loaded, items, prompt, root = sc */ export async function expectWidgetScatterPlot(widget, loaded, colorTitle, legend, root = screen) { // Test immediately displayed elements - const {quantity: x} = parseJMESPath(widget.x.quantity) - const {quantity: y} = parseJMESPath(widget.y.quantity) + const {quantity: x} = parseJMESPath(widget.x.search_quantity) + const {quantity: y} = parseJMESPath(widget.y.search_quantity) await expectFilterTitle(x) await expectFilterTitle(y) if (colorTitle) { - if (colorTitle.quantity) { - await expectFilterTitle(colorTitle.quantity) + if (colorTitle.search_quantity) { + await expectFilterTitle(colorTitle.search_quantity) } else { await expectFilterTitle(undefined, colorTitle.title, colorTitle.unit) } @@ -190,7 +211,7 @@ export async function expectWidgetScatterPlot(widget, loaded, colorTitle, legend */ export async function expectInputRange(widget, loaded, histogram, anchored, min, max, root = screen) { // Test header - await expectInputHeader(widget.x.quantity, true) + await expectInputHeader(widget.x.search_quantity, true) // Check histogram if (histogram) { @@ -202,7 +223,7 @@ export async function expectInputRange(widget, loaded, histogram, anchored, min, // Test text elements if the component is not anchored if (!anchored) { - const data = defaultFilterData[widget.x.quantity] + const data = defaultFilterData[widget.x.search_quantity] const dtype = data.dtype if (dtype === DType.Timestamp) { expect(root.getByText('Start time')).toBeInTheDocument() @@ -285,51 +306,23 @@ export async function expectPeriodicTableItems(elements, root = screen) { } /** - * Tests that the correct FilterMainMenu items are displayed. + * Tests that the menu is 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 item labels are displayed - const menuConfig = context.filter_menus - const menuItems = (menuConfig.include || Object.keys(menuConfig.options)) - .filter(key => !menuConfig?.exclude?.includes(key)) - .map(key => ({key, ...menuConfig.options[key]})) - for (const menuItem of menuItems) { - const label = menuItem.label - if (label) { - const labelElement = screen.getByTestId(`menu-item-label-${menuItem.key}`) - expect(labelElement).toBeInTheDocument() - expect(within(labelElement).getByText(label)).toBeInTheDocument() - } - } - - // Check that action labels are displayed - const actionItems = [] - for (const menuItem of menuItems) { - for (const key of menuItem?.actions?.include || []) { - actionItems.push(menuItem.actions.options[key]) - } - } - for (const actionItem of actionItems) { - const actionLabel = actionItem.label - if (actionLabel) { - const labelElement = screen.getByText(actionLabel) - expect(labelElement).toBeInTheDocument() - expect(within(labelElement).getByText(actionLabel)).toBeInTheDocument() - } - } - - // Check that clicking the menu items with a submenu opens up the menu - for (const menuItem of menuItems) { - if (menuMap[menuItem.key]) { - const labelMenu = screen.getByTestId(`menu-item-label-${menuItem.key}`) - const labelSubMenu = await screen.findByTestId(`filter-menu-header-${menuItem.key}`) - expect(labelSubMenu).not.toBeVisible() - await userEvent.click(labelMenu) - expect(labelSubMenu).toBeVisible() +export async function expectMenu(menu, root = screen) { + // Main title should be shown + screen.getByText('Filters') + + // Check that submenu buttons are displayed + for (const menuItem of menu.items) { + if (menuItem.type === 'menu') { + const title = menuItem.title + if (title) { + screen.getAllByText(title) } } + } } /** @@ -337,7 +330,7 @@ export async function expectFilterMainMenu(context, root = screen) { * @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) { +export async function expectSearchResults(columns, root = screen) { // Wait until search results are in expect(await screen.findByText("search result", {exact: false})).toBeInTheDocument() @@ -347,7 +340,7 @@ export async function expectSearchResults(context, root = screen) { const container = within(screen.getByTestId('search-results')) // Check that correct columns are displayed - const columnLabels = context.columns + const columnLabels = columns .filter((column) => column.selected) .map((column) => { const quantity = column.quantity diff --git a/gui/src/components/search/input/InputCheckbox.js b/gui/src/components/search/input/InputCheckbox.js deleted file mode 100644 index 54bf7ed87b0b6847ff205ee426cf561b69c1dc8e..0000000000000000000000000000000000000000 --- a/gui/src/components/search/input/InputCheckbox.js +++ /dev/null @@ -1,156 +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, { useCallback } from 'react' -import { makeStyles, useTheme } from '@material-ui/core/styles' -import { - FormControlLabel, - Checkbox, - Tooltip, - Typography -} from '@material-ui/core' -import PropTypes from 'prop-types' -import { isNil } from 'lodash' -import clsx from 'clsx' -import { useSearchContext, getValue } from '../SearchContext' - -const useStyles = makeStyles(theme => ({ - root: { - width: '100%', - display: 'flex', - alignItems: 'flex-start', - justifyContent: 'center', - flexDirection: 'column', - boxSizing: 'border-box' - } -})) - -/** - * A checkbox where the selection controls a true/false value within a quantity. - */ -const InputCheckbox = React.memo(({ - quantity, - label, - description, - initialValue, - className, - classes, - 'data-testid': testID -}) => { - const theme = useTheme() - const styles = useStyles({classes: classes, theme: theme}) - const {filterData, useFilterState, useFilterLocked} = useSearchContext() - const [filter, setFilter] = useFilterState(quantity) - const filterLocked = useFilterLocked(quantity) - - // Determine the description and units - const def = filterData[quantity] - const descFinal = description || def?.description || '' - const labelFinal = label || def?.label - const locked = !isNil(filterLocked) && def.global - const val = getValue(def, filter, filterLocked, initialValue) - - const handleChange = useCallback((event, value) => { - setFilter(value) - }, [setFilter]) - - return <div className={clsx(className, styles.root)} data-testid={testID}> - <Tooltip title={descFinal}> - <FormControlLabel - control={<Checkbox - color="secondary" - disabled={locked} - checked={val} - onChange={handleChange} - />} - label={<Typography>{labelFinal}</Typography>} - /> - </Tooltip> - </div> -}) - -InputCheckbox.propTypes = { - quantity: PropTypes.string.isRequired, - label: PropTypes.string, - description: PropTypes.string, - initialValue: PropTypes.bool, - className: PropTypes.string, - classes: PropTypes.object, - 'data-testid': PropTypes.string -} - -export default InputCheckbox - -/** - * A checkbox where the selection activates a specific value within a quantity. - */ -const useInputCheckboxValueStyles = makeStyles(theme => ({ - root: { - '& > :last-child': { - marginRight: -3 - } - }, - control: { - padding: 6 - } -})) -export const InputCheckboxValue = React.memo(({ - quantity, - description, - value, - className, - classes, - 'data-testid': testID -}) => { - const theme = useTheme() - const styles = useInputCheckboxValueStyles({classes: classes, theme: theme}) - const {filterData, useFilterState} = useSearchContext() - const [filter, setFilter] = useFilterState(quantity) - - // Determine the description and units - const def = filterData[quantity] - const descFinal = description || def?.description || '' - - const handleChange = useCallback(() => { - setFilter(old => { - const newValue = old ? new Set(old) : new Set() - newValue.has(value) ? newValue.delete(value) : newValue.add(value) - return newValue - }) - }, [setFilter, value]) - - return <div className={clsx(className, styles.root)} data-testid={testID}> - <Tooltip title={descFinal}> - <Checkbox - color="secondary" - checked={filter ? filter.has(value) : false} - onChange={handleChange} - className={styles.control} - size="small" - /> - </Tooltip> - </div> -}) - -InputCheckboxValue.propTypes = { - quantity: PropTypes.string.isRequired, - description: PropTypes.string, - value: PropTypes.any, - className: PropTypes.string, - classes: PropTypes.object, - 'data-testid': PropTypes.string -} diff --git a/gui/src/components/search/menus/FilterSubMenuCustomQuantities.js b/gui/src/components/search/input/InputCustomQuantities.js similarity index 82% rename from gui/src/components/search/menus/FilterSubMenuCustomQuantities.js rename to gui/src/components/search/input/InputCustomQuantities.js index bff2e151f9b0344d92a7098e57026358ee4cba21..7545a3e86ddb2575c1b9d40baabbe00210691e25 100644 --- a/gui/src/components/search/menus/FilterSubMenuCustomQuantities.js +++ b/gui/src/components/search/input/InputCustomQuantities.js @@ -15,22 +15,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import PropTypes from 'prop-types' import { isNil } from 'lodash' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' import { Box, Button, FormControl, InputLabel, - LinearProgress, MenuItem, Select, Typography } from '@material-ui/core' -import { InputGrid, InputGridItem } from '../input/InputGrid' +import InputHeader from '../input/InputHeader' import { useApi } from '../../api' import { useErrors } from '../../errors' import { useSearchContext } from '../SearchContext' @@ -43,6 +40,7 @@ import { editQuantityComponents } from '../../editQuantity/EditQuantity' import { InputMetainfoControlled } from '../../search/input/InputMetainfo' import { DType, getDatatype, rsplit, parseQuantityName } from '../../../utils' import { InputTextField } from '../input/InputText' +import Placeholder from '../../visualization/Placeholder' const types = { [DType.Int]: 'Integer', @@ -190,7 +188,7 @@ const QuantityFilter = React.memo(({quantities, filter, onChange}) => { const availableOperators = quantityDef ? getOperators(quantityDef) : ['search'] return (<React.Fragment> - <Box display="flex" flexWrap="wrap" flexDirection="row" alignItems="flex-start" marginTop={1}> + <Box display="flex" flexWrap="wrap" flexDirection="row" alignItems="flex-start"> <Box marginBottom={1} width="100%"> <InputMetainfoControlled options={options} @@ -245,15 +243,10 @@ QuantityFilter.propTypes = { onChange: PropTypes.func.isRequired } -const FilterSubMenuCustomQuantities = React.memo(({ - id, - ...rest -}) => { +const InputCustomQuantities = React.memo(({visible, title, showHeader}) => { const metainfo = useGlobalMetainfo() - const {selected, open} = useContext(filterMenuContext) const {useFilterState} = useSearchContext() const [loaded, setLoaded] = useState(false) - const visible = open && id === selected const {api} = useApi() const {raiseError} = useErrors() const [query, setQuery] = useFilterState('custom_quantities') @@ -381,71 +374,69 @@ const FilterSubMenuCustomQuantities = React.memo(({ setAndFilters(andFilters || [{}]) }, [query, setAndFilters]) + let content if (!quantities) { - return ( - <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <LinearProgress/> - </InputGridItem> - </InputGrid> - </FilterSubMenu> - ) - } - - if (quantities.length === 0) { - return ( - <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <Typography> - Could not find valid quantities to search for. Make sure that you have access to the data and that the metainfo associated with the quantities can be loaded correctly. - </Typography> - </InputGridItem> - </InputGrid> - </FilterSubMenu> - ) - } - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <Box marginTop={2}> - {andFilters.map((filter, index) => ( - <Box key={index} marginY={1}> - <QuantityFilter - quantities={quantities} - filter={filter} - onChange={filter => handleFilterChange(filter, index)} - /> - </Box> - ))} - <Box flexDirection="row" display="flex"> - <Button - variant="contained" color="primary" onClick={handleAndClicked} - >And</Button> - <Box marginLeft={1}> - <Button - variant="contained" color="primary" onClick={handleClearClicked} - disabled={andFilters.length === 1 && Object.keys(andFilters[0]).length === 0} - >Clear</Button> - </Box> - </Box> + content = <Placeholder + variant="rect" + margin={0} + /> + } else if (quantities.length === 0) { + content = <Typography> + Could not find valid quantities to search for. Make sure that you have access to the data and that the metainfo associated with the quantities can be loaded correctly. + </Typography> + } else { + content = <> + {andFilters.map((filter, index) => ( + <Box key={index} marginBottom={1}> + <QuantityFilter + quantities={quantities} + filter={filter} + onChange={filter => handleFilterChange(filter, index)} + /> </Box> - </InputGridItem> - <InputGridItem xs={12}> - <Box marginTop={2} display="flex" flexDirection="row" justifyContent="right"> + ))} + <Box flexDirection="row" display="flex" marginBottom={2}> + <Button + variant="contained" color="primary" onClick={handleAndClicked} + >And</Button> + <Box marginLeft={1}> <Button - variant="contained" color="primary" onClick={handleSearchClicked} - disabled={!searchEnabled} - >Update search</Button> + variant="contained" color="primary" onClick={handleClearClicked} + disabled={andFilters.length === 1 && Object.keys(andFilters[0]).length === 0} + >Clear</Button> </Box> - </InputGridItem> - </InputGrid> - </FilterSubMenu> + </Box> + <Box marginTop={2} display="flex" flexDirection="row" justifyContent="right"> + <Button + variant="contained" color="primary" onClick={handleSearchClicked} + disabled={!searchEnabled} + >Update search</Button> + </Box> + </> + } + + return <div> + {showHeader && <InputHeader + label={title || 'Custom quantities'} + description="Use this field to search for quantities defined in custom schemas." + disableStatistics + disableWidget + />} + {/* We need to set a dummy height for the content with relative height/width to fill the parent correctly. */} + <Box minHeight="201px" height="1px" width="100%"> + {content} + </Box> + </div> }) -FilterSubMenuCustomQuantities.propTypes = { - id: PropTypes.string + +InputCustomQuantities.propTypes = { + visible: PropTypes.bool, + title: PropTypes.string, + showHeader: PropTypes.bool +} + +InputCustomQuantities.defaultProps = { + showHeader: true } -export default FilterSubMenuCustomQuantities +export default InputCustomQuantities diff --git a/gui/src/components/search/menus/FilterSubMenuMetadata.js b/gui/src/components/search/input/InputDefinitions.js similarity index 62% rename from gui/src/components/search/menus/FilterSubMenuMetadata.js rename to gui/src/components/search/input/InputDefinitions.js index eafd01ee100092ac63987f528ff342261859b21d..03612dc4333b8a6ed53aff276ea780c62f0a7531 100644 --- a/gui/src/components/search/menus/FilterSubMenuMetadata.js +++ b/gui/src/components/search/input/InputDefinitions.js @@ -15,11 +15,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useCallback, useEffect, useMemo, useState, useContext } from 'react' + +import React, { useCallback, useEffect, useMemo, useState } from 'react' import PropTypes from 'prop-types' import clsx from 'clsx' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' import { Box, Collapse, makeStyles } from '@material-ui/core' import { SectionMDef, SubSectionMDef, useGlobalMetainfo } from '../../archive/metainfo' import ExpandMoreIcon from '@material-ui/icons/ExpandMore' @@ -29,9 +28,6 @@ import InputItem from '../input/InputItem' import { InputTextQuantity } from '../input/InputText' import { useErrors } from '../../errors' import { useDataStore } from '../../DataStore' -import InputField from '../input/InputField' -import InputRadio from '../input/InputRadio' -import { useApi } from '../../api' import InputHeader from '../input/InputHeader' const filterProperties = def => !(def.name.startsWith('x_') || def.virtual) @@ -147,18 +143,12 @@ Definition.propTypes = { className: PropTypes.string } -const FilterSubMenuMetadata = React.memo(({ - id, - ...rest -}) => { - const {api} = useApi() - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - const authenticated = api?.keycloak?.authenticated +const InputDefinitions = React.memo(({visible}) => { const dataStore = useDataStore() const globalMetainfo = useGlobalMetainfo() const [[options, tree], setOptions] = useState([[], {}]) const {raiseError} = useErrors() + useEffect(() => { if (!globalMetainfo || !visible) { return @@ -194,80 +184,22 @@ const FilterSubMenuMetadata = React.memo(({ setOptions([options, tree]) }, [raiseError, dataStore, globalMetainfo, setOptions, visible]) - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputRadio - quantity="visibility" - label="Visibility" - initialValue={authenticated ? 'visible' : 'public'} - options={{ - all: {label: 'All', disabled: !authenticated, tooltip: 'Consider all entries.'}, - public: {label: 'Public', disabled: false, tooltip: 'Consider all entries that can be publically downloaded, i.e. only published entries without embargo.'}, - visible: {label: 'Visible', disabled: !authenticated, tooltip: 'Consider all entries that are visible to you. This includes entries with embargo or unpublished entries that belong to you or are shared with you.'}, - shared: {label: 'Shared', disabled: !authenticated, tooltip: 'Only consider entries that belong to you or are shared with you.'}, - user: {label: 'User', disabled: !authenticated, tooltip: 'Only consider entries that belong to you.'}, - staging: {label: 'Unpublished', disabled: !authenticated, tooltip: 'Only search through unpublished entries.'} - }} - ></InputRadio> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="entry_id" - visible={visible} - disableStatistics - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="upload_id" - visible={visible} - disableStatistics - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="upload_name" - visible={visible} - disableStatistics - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.material.material_id" - visible={visible} - disableStatistics - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="datasets.dataset_id" - visible={visible} - disableStatistics - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputHeader quantity="quantities" disableStatistics/> - <Box marginBottom={1}> - <InputTextQuantity - quantity="quantities" - suggestions={options} - /> - </Box> - {Object.keys(tree).map((name, index) => ( - <Definition node={tree[name]} name={name} path={name} key={index} /> - ))} - </InputGridItem> - </InputGrid> - </FilterSubMenu> + return <> + <InputHeader quantity="quantities" label='Definitions' disableStatistics/> + <Box marginBottom={1}> + <InputTextQuantity + quantity="quantities" + suggestions={options} + /> + </Box> + {Object.keys(tree).map((name, index) => ( + <Definition node={tree[name]} name={name} path={name} key={index} /> + ))} + </> }) -FilterSubMenuMetadata.propTypes = { - id: PropTypes.string + +InputDefinitions.propTypes = { + visible: PropTypes.bool } -export default FilterSubMenuMetadata +export default InputDefinitions diff --git a/gui/src/components/search/input/InputField.spec.js b/gui/src/components/search/input/InputField.spec.js deleted file mode 100644 index 56a5e3790b6b2aa32107465dff6953e3dd3b2bf2..0000000000000000000000000000000000000000 --- a/gui/src/components/search/input/InputField.spec.js +++ /dev/null @@ -1,219 +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 { waitFor, within, waitForElementToBeRemoved } from '@testing-library/dom' -import { startAPI, closeAPI, screen } from '../../conftest.spec' -import { renderSearchEntry, expectInputHeader } from '../conftest.spec' -import { defaultFilterData } from '../FilterRegistry' -import InputField from './InputField' -import userEvent from '@testing-library/user-event' - -const stateName = 'tests.states.search.search' -const optionsStructural = ['bulk', '2D', 'molecule / cluster'] -const optionsProgramName = ['VASP', 'exciting', 'ABACUS', 'ABINIT', 'AFLOW'] -const optionsXC = [ - 'GGA_C_PBE_SOL', - 'GGA_X_PBE_SOL', - 'LDA_C_PZ', - 'LDA_X_PZ' -] - -describe('', () => { - const quantity = 'results.material.structural_type' - beforeEach(async () => { - await startAPI(stateName, 'tests/data/search/inputfield-structural-type') - renderSearchEntry(<InputField - quantity={quantity} - disableSearch - visible - />) - }) - afterEach(() => closeAPI()) - - test('initial state is loaded correctly for quantity with fixed options', async () => { - // Test immediately displayed elements - const allOptions = getAllOptions(quantity) - await expectInputHeader(quantity) - for (const option of allOptions) { - expect(await screen.findByText(option)).toBeInTheDocument() - } - - // Test that options become selectable after API call finishes - await expectOptions(optionsStructural, allOptions) - }) -}) - -describe('', () => { - const quantity = 'results.method.simulation.program_name' - beforeEach(async () => { - await startAPI(stateName, 'tests/data/search/inputfield-program-name') - renderSearchEntry(<InputField - quantity={quantity} - visible - data-testid="inputfield" - />) - }) - afterEach(() => closeAPI()) - - test('initial state is loaded correctly for quantity with dynamically loaded options', async () => { - // Test that placeholder is shown while loading - const placeholder = screen.queryByTestId('inputfield-placeholder') - expect(placeholder).toBeInTheDocument() - - // Check that placeholder disappears - await waitForElementToBeRemoved(() => screen.queryByTestId('inputfield-placeholder')) - - // Test header - await expectInputHeader(quantity) - - // Test that options become selectable after API call finishes - await expectOptions(['VASP', 'exciting'], optionsProgramName) - - // Test that the "show more" button is shown, but "show less" is not shown - expect(screen.getByText('Show more')).toBeInTheDocument() - expect(screen.queryByText('Show less')).not.toBeInTheDocument() - }) -}) - -describe('', () => { - const quantity = 'results.material.structural_type' - beforeEach(async () => { - await startAPI(stateName, 'tests/data/search/inputfield-structural-type-edit') - renderSearchEntry(<InputField - quantity={quantity} - visible - data-testid="inputfield" - />) - }) - afterEach(() => closeAPI()) - - test('selecting an option for an exlusive filter does not update the displayed options', async () => { - // Test that options are selectable after API call finishes - await expectOptions(optionsStructural, optionsStructural) - - // Select 'bulk' and test that all options are still available. - const checkbox = queryByInputItemName('bulk') - await userEvent.click(checkbox) - await expectOptions(optionsStructural, optionsStructural) - }) -}) - -describe('', () => { - const quantity = 'results.method.simulation.dft.xc_functional_names' - beforeEach(async () => { - await startAPI(stateName, 'tests/data/search/inputfield-xc-functional-names') - renderSearchEntry(<InputField - quantity={quantity} - visible - data-testid="inputfield" - />) - }) - afterEach(() => closeAPI()) - - test('selecting an option for a non-exlusive filter updates the displayed options', async () => { - // Test that options are selectable after API call finishes - await expectOptions(optionsXC, optionsXC) - - // Select PBE exchange and test that only PBE correlation is shown - // afterwards - const checkbox = queryByInputItemName('GGA_C_PBE_SOL') - await userEvent.click(checkbox) - await expectOptions(['GGA_C_PBE_SOL', 'GGA_X_PBE_SOL'], optionsXC, false) - }) -}) - -describe('', () => { - const quantity = 'results.method.simulation.dft.xc_functional_names' - beforeEach(async () => { - await startAPI(stateName, 'tests/data/search/inputfield-xc-functional-names-suggestion') - renderSearchEntry(<InputField - quantity={quantity} - visible - data-testid="inputfield" - />) - }) - afterEach(() => closeAPI()) - - test('search field is shown, suggestions are shown when typing, selecting suggested values works correctly', async () => { - // Wait for initial render - await expectOptions(optionsXC, optionsXC, false) - - // See that the input field is shown and is wait until it is not disabled - const input = screen.getByPlaceholderText('Type here') - expect(input).toBeInTheDocument() - await waitFor(() => expect(input).not.toBeDisabled()) - - // Start typing a value, it is expected that a list of suggestions will be - // shown shortly afterwards - await userEvent.type(input, 'SOL') - const suggestions = ['GGA_C_PBE_SOL', 'GGA_X_PBE_SOL'] - for (const suggestion of suggestions) { - await screen.findByMenuItem(suggestion) - } - - // Select a suggested option by clicking it. This should update the list. - const suggestion = screen.getByMenuItem('GGA_C_PBE_SOL') - await userEvent.click(suggestion) - await expectOptions(suggestions, suggestions, false) - }) -}) - -/** - * Tests that only the given options are selectable within an InputField. - * @param {array} selectable Selectable options - * @param {array} all All options - * @param {boolean} visible Whether the non-available options should still be - * displayed in a disabled state. - * @param {object} root The root element to perform the search on. - */ -async function expectOptions(selectable, all, visible = true, root = screen) { - const availableSet = new Set(selectable) - await waitFor(() => { - for (const option of all) { - const inputCheckbox = queryByInputItemName(option) - if (availableSet.has(option)) { - expect(inputCheckbox).not.toHaveAttribute('disabled') - } else if (visible) { - expect(inputCheckbox).toHaveAttribute('disabled') - } else { - expect(inputCheckbox).toBe(null) - } - } - }) -} - -/** - * Finds the checkbox corresponding to an InputItem with the given value. - * @param {string} name The option value that is displayed - * @returns {element} The checkbox input HTML element. - */ -function queryByInputItemName(option, root = screen) { - const inputLabel = root.queryByText(option) - const inputCheckbox = inputLabel && within(inputLabel.closest('label')).getByRole('checkbox') - return inputCheckbox -} - -/** - * Returns the list of all the enumerated options that are available for a - * quantity. - * @param {string} quantity Quantity name - * @returns {array} List of options for the given quantity. - */ -function getAllOptions(quantity) { - return [...Object.values(defaultFilterData[quantity].options)].map(option => option.label) -} diff --git a/gui/src/components/search/input/InputGrid.js b/gui/src/components/search/input/InputGrid.js index 56a21a40f94fccea2a83178de08942f452dd6bdf..35f0828fbaac7a6da53a61a0fdff178081b59d7b 100644 --- a/gui/src/components/search/input/InputGrid.js +++ b/gui/src/components/search/input/InputGrid.js @@ -15,13 +15,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react' +import React, { useContext } from 'react' import PropTypes from 'prop-types' +import clsx from 'clsx' import { Grid, Divider, makeStyles } from '@material-ui/core' +import { inputSectionContext } from './InputNestedObject' /** * For displaying a grid of input properties. @@ -30,24 +32,33 @@ import { * prevent horizontal overflow. A padding is applied to the parent element as * described in https://v4.mui.com/components/grid/#negative-margin */ -const inputGridSpacing = 2 -const useInputGridStyles = makeStyles(theme => ({ - root: { - paddingLeft: theme.spacing(inputGridSpacing / 2), - paddingRight: theme.spacing(inputGridSpacing / 2) + +/** + * For displaying an individual input filter, typically within a InputGrid. + */ +export const inputItemPaddingHorizontal = 0.75 +export const inputItemPaddingVertical = 1 +const useStyles = makeStyles(theme => ({ + root: {}, + padding: { + paddingLeft: theme.spacing(inputItemPaddingHorizontal), + paddingRight: theme.spacing(inputItemPaddingHorizontal) } })) -export function InputGrid({children}) { - const styles = useInputGridStyles() - return <div className={styles.root}> - <Grid container spacing={inputGridSpacing} style={{marginTop: 0}}> + +export function InputGrid({className, disablePadding, children}) { + const styles = useStyles() + return <div className={clsx(className, styles.root, !disablePadding && styles.padding)}> + <Grid container spacing={0}> {children} </Grid> </div> } InputGrid.propTypes = { - children: PropTypes.any + children: PropTypes.any, + disablePadding: PropTypes.bool, + className: PropTypes.string } /** @@ -55,25 +66,36 @@ InputGrid.propTypes = { */ const useInputGridItemStyles = makeStyles(theme => ({ root: { - marginBottom: theme.spacing(inputGridSpacing / 2) }, - content: { - paddingLeft: theme.spacing(inputGridSpacing / 4), - paddingRight: theme.spacing(inputGridSpacing / 4) + padding: { + padding: theme.spacing(inputItemPaddingVertical, inputItemPaddingHorizontal, inputItemPaddingVertical, inputItemPaddingHorizontal) }, divider: { - marginLeft: theme.spacing(-inputGridSpacing / 2), - marginRight: theme.spacing(-inputGridSpacing / 2), - marginTop: theme.spacing(-inputGridSpacing / 2), - marginBottom: theme.spacing(inputGridSpacing / 4), backgroundColor: theme.palette.grey[300] + }, + paddingEnabledDivider: { + marginTop: theme.spacing(-inputItemPaddingVertical), + marginRight: theme.spacing(-2 * inputItemPaddingHorizontal), + marginLeft: theme.spacing(-2 * inputItemPaddingHorizontal) + }, + paddingDisabled: { + marginRight: theme.spacing(-inputItemPaddingHorizontal), + marginLeft: theme.spacing(-inputItemPaddingHorizontal) + }, + section: { + marginTop: theme.spacing(-2) } })) -export function InputGridItem({classes, children, ...other}) { +export function InputGridItem({classes, children, disablePadding, ...other}) { + const sectionContext = useContext(inputSectionContext) + const section = sectionContext?.section const styles = useInputGridItemStyles({classes: classes}) - return <Grid item {...other} className={styles.root}> - <Divider className={styles.divider}/> - <div className={styles.content}> + return <Grid item {...other} className={clsx(styles.root, !disablePadding && styles.padding)}> + {section + ? <div className={styles.section}></div> + : <Divider className={clsx(styles.divider, disablePadding ? styles.paddingDisabled : styles.paddingEnabledDivider)}/> + } + <div className={clsx(disablePadding && styles.paddingDisabled)}> {children} </div> </Grid> @@ -81,8 +103,10 @@ export function InputGridItem({classes, children, ...other}) { InputGridItem.propTypes = { classes: PropTypes.object, - children: PropTypes.any + children: PropTypes.any, + disablePadding: PropTypes.any } InputGridItem.defaultProps = { + xs: 12 } diff --git a/gui/src/components/search/input/InputHeader.js b/gui/src/components/search/input/InputHeader.js index a6c0cd1e10fd229f6aa16890f98c9f76e5794eff..609bfdaed45443ebc3e28f19cfad805c418dc771 100644 --- a/gui/src/components/search/input/InputHeader.js +++ b/gui/src/components/search/input/InputHeader.js @@ -66,6 +66,7 @@ const useStyles = makeStyles(theme => ({ const InputHeader = React.memo(({ quantity, label, + unit, description, disableWidget, disableStatistics, @@ -154,6 +155,7 @@ const InputHeader = React.memo(({ <FilterTitle quantity={quantity} label={label} + unit={unit} description={description} TooltipProps={tooltipProps} /> @@ -168,8 +170,9 @@ const InputHeader = React.memo(({ }) InputHeader.propTypes = { - quantity: PropTypes.string.isRequired, + quantity: PropTypes.string, label: PropTypes.string, + unit: PropTypes.string, description: PropTypes.string, disableWidget: PropTypes.bool, disableStatistics: PropTypes.bool, diff --git a/gui/src/components/search/input/InputRange.js b/gui/src/components/search/input/InputHistogram.js similarity index 83% rename from gui/src/components/search/input/InputRange.js rename to gui/src/components/search/input/InputHistogram.js index 16bce845d5147caea222c7055d2350ed29476f42..9a95c284a5db459dcf06ff28f3d01f8efdfe5124 100644 --- a/gui/src/components/search/input/InputRange.js +++ b/gui/src/components/search/input/InputHistogram.js @@ -17,21 +17,20 @@ */ import React, { useState, useMemo, useCallback, useEffect, useRef, useContext } from 'react' import { makeStyles } from '@material-ui/core/styles' +import { Box } from '@material-ui/core' import PropTypes from 'prop-types' import clsx from 'clsx' import { isNil } from 'lodash' import InputHeader from './InputHeader' -import { inputSectionContext } from './InputSection' +import { inputSectionContext } from './InputNestedObject' import { Quantity } from '../../units/Quantity' import { Unit } from '../../units/Unit' import { useUnitContext } from '../../units/UnitContext' import { DType, formatNumber } from '../../../utils' -import { getInterval } from '../../plotting/common' +import { getAxisConfig, getInterval } from '../../plotting/common' import { useSearchContext } from '../SearchContext' import PlotHistogram from '../../plotting/PlotHistogram' import { isValid, getTime } from 'date-fns' -import { ActionCheckbox } from '../../Actions' -import { autorangeDescription } from '../widgets/WidgetHistogram' /* * Component for displaying a numerical range as a slider/histogram together @@ -45,30 +44,29 @@ const useStyles = makeStyles(theme => ({ histogram: { } })) -export const Range = React.memo(({ - xAxis, - yAxis, +export const Histogram = React.memo(({ + x, + y, nSteps, visible, nBins, - disableHistogram, + showStatistics, disableXTitle, autorange, - showinput, + showInput, aggId, className, classes, 'data-testid': testID }) => { - const {filterData, useAgg, useFilterState, useIsStatisticsEnabled} = useSearchContext() + const {filterData, useAgg, useFilterState} = useSearchContext() const sectionContext = useContext(inputSectionContext) const repeats = sectionContext?.repeats - const isStatisticsEnabled = useIsStatisticsEnabled() const styles = useStyles({classes}) - const [filter, setFilter] = useFilterState(xAxis.quantity) + const [filter, setFilter] = useFilterState(x.search_quantity) const [minLocal, setMinLocal] = useState() const [maxLocal, setMaxLocal] = useState() - const [plotData, setPlotData] = useState({xAxis, yAxis}) + const [plotData, setPlotData] = useState({xAxis: x, yAxis: y}) const loading = useRef(false) const firstRender = useRef(true) const validRange = useRef() @@ -81,13 +79,12 @@ export const Range = React.memo(({ const [minInclusive, setMinInclusive] = useState(true) const [maxInclusive, setMaxInclusive] = useState(true) const highlight = Boolean(filter) - disableHistogram = isNil(disableHistogram) ? !isStatisticsEnabled : disableHistogram // Determine the description and units - const def = filterData[xAxis.quantity] + const def = filterData[x.search_quantity] const unitStorage = useMemo(() => { return new Unit(def?.unit || 'dimensionless') }, [def]) - const discretization = xAxis.dtype === DType.Int ? 1 : undefined - const isTime = xAxis.dtype === DType.Timestamp + const discretization = x.dtype === DType.Int ? 1 : undefined + const isTime = x.dtype === DType.Timestamp const firstLoad = useRef(true) // We need to set a valid initial input state: otherwise the component thinks @@ -117,8 +114,8 @@ export const Range = React.memo(({ ? filter : filter instanceof Quantity ? filter.to(unitStorage) - : new Quantity(filter, xAxis.unit).to(unitStorage) - }, [isTime, xAxis.unit, unitStorage]) + : new Quantity(filter, x.unit).to(unitStorage) + }, [isTime, x.unit, unitStorage]) // Aggregation when the statistics are enabled: a histogram aggregation with // extended bounds based on the currently set filter range. Note: the config @@ -164,7 +161,7 @@ export const Range = React.memo(({ ? {type: 'histogram', buckets: nBins, exclude_from_search, extended_bounds} : {type: 'histogram', interval: discretization, exclude_from_search, extended_bounds} }, [filter, fromDisplayUnit, isTime, discretization, nBins, autorange]) - const agg = useAgg(xAxis.quantity, visible && !disableHistogram, `${aggId}_histogram`, aggHistogramConfig) + const agg = useAgg(x.search_quantity, visible && showStatistics, `${aggId}_histogram`, aggHistogramConfig) useEffect(() => { if (!isNil(agg)) { firstLoad.current = false @@ -174,16 +171,13 @@ export const Range = React.memo(({ // Aggregation when the statistics are disabled: a simple min_max aggregation // is enough in order to get the slider range. const aggSliderConfig = useMemo(() => ({type: 'min_max', exclude_from_search: true}), []) - const aggSlider = useAgg(xAxis.quantity, visible && disableHistogram, `${aggId}_slider`, aggSliderConfig) + const aggSlider = useAgg(x.search_quantity, visible && !showStatistics, `${aggId}_slider`, aggSliderConfig) // Determine the global minimum and maximum values const [minGlobal, maxGlobal] = useMemo(() => { let minGlobal let maxGlobal - if (disableHistogram) { - minGlobal = aggSlider?.data?.[0] - maxGlobal = aggSlider?.data?.[1] - } else { + if (showStatistics) { const nBuckets = agg?.data?.length || 0 if (nBuckets === 1) { minGlobal = agg.data[0].value @@ -202,10 +196,13 @@ export const Range = React.memo(({ maxGlobal = agg.data[agg.data.length - 1].value + (discretization ? 0 : agg.interval) } } + } else { + minGlobal = aggSlider?.data?.[0] + maxGlobal = aggSlider?.data?.[1] } firstRender.current = false return [minGlobal, maxGlobal] - }, [agg, aggSlider, disableHistogram, discretization]) + }, [agg, aggSlider, showStatistics, discretization]) const stepHistogram = agg?.interval const unavailable = isNil(minGlobal) || isNil(maxGlobal) || isNil(range) @@ -222,10 +219,10 @@ export const Range = React.memo(({ return undefined } const rangeSI = maxLocal - minLocal - const range = new Quantity(rangeSI, unitStorage.toDelta()).to(xAxis.unit).value() - const intervalCustom = getInterval(range, nSteps, xAxis.dtype) - return new Quantity(intervalCustom, xAxis.unit).to(unitStorage).value() - }, [maxLocal, minLocal, discretization, nSteps, xAxis.dtype, xAxis.unit, unitStorage]) + const range = new Quantity(rangeSI, unitStorage.toDelta()).to(x.unit).value() + const intervalCustom = getInterval(range, nSteps, x.dtype) + return new Quantity(intervalCustom, x.unit).to(unitStorage).value() + }, [maxLocal, minLocal, discretization, nSteps, x.dtype, x.unit, unitStorage]) // When filter changes, the plot data should not be updated. useEffect(() => { @@ -249,27 +246,28 @@ export const Range = React.memo(({ setPlotData({ xAxis: { - quantity: xAxis.quantity, - unit: xAxis.unit, + search_quantity: x.search_quantity, + quantity: x.quantity, + unit: x.unit, unitStorage: unitStorage, - dtype: xAxis.dtype, - title: xAxis.title, + dtype: x.dtype, + title: x.title, min: minLocal, max: maxLocal }, - yAxis, + yAxis: y, step: stepHistogram, data: agg.data }) - }, [loading, nBins, agg, minLocal, maxLocal, stepHistogram, unitStorage, xAxis.quantity, xAxis.unit, xAxis.dtype, xAxis.title, xAxis.scale, yAxis]) + }, [loading, nBins, agg, minLocal, maxLocal, stepHistogram, unitStorage, x.search_quantity, x.quantity, x.unit, x.dtype, x.title, x.scale, y]) // Function for converting search values into the currently selected unit // system. const toInternal = useCallback(filter => { return (!isTime && unitStorage) - ? formatNumber(new Quantity(filter, unitStorage).to(xAxis.unit).value()) + ? formatNumber(new Quantity(filter, unitStorage).to(x.unit).value()) : filter - }, [unitStorage, isTime, xAxis.unit]) + }, [unitStorage, isTime, x.unit]) // If no filter has been specified by the user, the range is automatically // adjusted according to global min/max of the field. If filter is set, the @@ -532,18 +530,18 @@ export const Range = React.memo(({ maxInput={maxInput} minLocal={minLocal} maxLocal={maxLocal} - showinput={showinput} + showInput={showInput} stepSlider={stepSlider} - disableHistogram={disableHistogram} + disableHistogram={!showStatistics} disableXTitle={disableXTitle} data-testid={`${testID}-histogram`} /> </div> }) -Range.propTypes = { - xAxis: PropTypes.object, - yAxis: PropTypes.object, +Histogram.propTypes = { + x: PropTypes.object, + y: PropTypes.object, /* Target number of steps for the slider that is shown when statistics are * disabled. The actual number may vary, as the step is chosen to be a * human-readable value that depends on the range and the unit. */ @@ -552,21 +550,21 @@ Range.propTypes = { * enabled. */ nBins: PropTypes.number, visible: PropTypes.bool, - /* Whether the histogram is disabled */ - disableHistogram: PropTypes.bool, + /* Whether to show advanced statistics */ + showStatistics: PropTypes.bool, /* Whether the x title is disabled */ disableXTitle: PropTypes.bool, /* Set the range automatically according to data. */ autorange: PropTypes.bool, /* Show the input fields for min and max value */ - showinput: PropTypes.bool, + showInput: PropTypes.bool, aggId: PropTypes.string, className: PropTypes.string, classes: PropTypes.object, 'data-testid': PropTypes.string } -Range.defaultProps = { +Histogram.defaultProps = { nSteps: 20, nBins: 30, aggId: 'default', @@ -576,7 +574,7 @@ Range.defaultProps = { /** * A small wrapper around Range for use in the filter menus. */ -const useInputRangeStyles = makeStyles(theme => ({ +const useInputHistogramStyles = makeStyles(theme => ({ root: { display: 'flex', flexDirection: 'column' @@ -585,69 +583,64 @@ const useInputRangeStyles = makeStyles(theme => ({ height: '8rem' } })) -const InputRange = React.memo(({ - label, - quantity, +const InputHistogram = React.memo(({ + title, + x, + y, description, nSteps, visible, - initialScale, nBins, - disableHistogram, - initialAutorange, + showHeader, + showInput, + showStatistics, + autorange, aggId, className, 'data-testid': testID }) => { - const {filterData} = useSearchContext() + const {filterData, useIsStatisticsEnabled} = useSearchContext() + const isStatisticsEnabled = useIsStatisticsEnabled() const {units} = useUnitContext() - const styles = useInputRangeStyles() - const [scale, setScale] = useState(initialScale || filterData[quantity].scale) - const dtype = filterData[quantity].dtype + const styles = useInputHistogramStyles() + const [scaleState, setScaleState] = useState(y?.scale || filterData[x?.search_quantity].scale) + const dtype = filterData[x.search_quantity].dtype const isTime = dtype === DType.Timestamp - const [autorange, setAutorange] = useState(isNil(initialAutorange) ? isTime : initialAutorange) - const x = useMemo(() => ( - { - quantity, - dtype, - unit: new Unit(filterData[quantity]?.unit || 'dimensionless').toSystem(units) - } - ), [quantity, filterData, dtype, units]) - const y = useMemo(() => ({scale: scale}), [scale]) + const autorangeFinal = isNil(autorange) ? isTime : autorange + showStatistics = isStatisticsEnabled && showStatistics + + // Create final axis configs for the histogram + const xAxis = useMemo(() => getAxisConfig(x, filterData, units), [x, filterData, units]) + const yAxis = useMemo(() => ({...y, scale: scaleState}), [scaleState, y]) // Determine the description and title - const def = filterData[quantity] + const def = filterData[x.search_quantity] const descFinal = description || def?.description || '' - const labelFinal = label || def?.label - - // Component for enabling/disabling autorange - const actions = <ActionCheckbox - value={autorange} - label="zoom" - tooltip={autorangeDescription} - onChange={(value) => setAutorange(value)} - /> + const labelFinal = title || def?.label return <div className={clsx(styles.root, className)}> - <InputHeader - label={labelFinal} - quantity={quantity} - description={descFinal} - scale={scale} - onChangeScale={setScale} - disableStatistics={disableHistogram} - actions={actions} - /> - <Range - xAxis={x} - yAxis={y} + {showHeader + ? <InputHeader + label={labelFinal} + quantity={x.search_quantity} + unit={x.unit} + description={descFinal} + scale={scaleState} + onChangeScale={setScaleState} + disableStatistics={!showStatistics} + /> + : <Box marginTop={1.5}/> + } + <Histogram + x={xAxis} + y={yAxis} nSteps={nSteps} visible={visible} nBins={nBins} - disableHistogram={disableHistogram} + showStatistics={showStatistics} disableXTitle - autorange={autorange} - showinput + autorange={autorangeFinal} + showInput={showInput} aggId={aggId} classes={{histogram: styles.histogram}} data-testid={testID} @@ -655,26 +648,31 @@ const InputRange = React.memo(({ </div> }) -InputRange.propTypes = { - label: PropTypes.string, - quantity: PropTypes.string.isRequired, +InputHistogram.propTypes = { + title: PropTypes.string, description: PropTypes.string, + x: PropTypes.object, + y: PropTypes.object, nSteps: PropTypes.number, nBins: PropTypes.number, visible: PropTypes.bool, - initialScale: PropTypes.string, - disableHistogram: PropTypes.bool, - initialAutorange: PropTypes.bool, + showHeader: PropTypes.bool, + showInput: PropTypes.bool, + showStatistics: PropTypes.bool, + autorange: PropTypes.bool, aggId: PropTypes.string, className: PropTypes.string, 'data-testid': PropTypes.string } -InputRange.defaultProps = { +InputHistogram.defaultProps = { nSteps: 20, nBins: 30, + showHeader: true, + showInput: true, + showStatistics: true, aggId: 'default', 'data-testid': 'inputrange' } -export default InputRange +export default InputHistogram diff --git a/gui/src/components/search/input/InputHistogram.spec.js b/gui/src/components/search/input/InputHistogram.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..8779135514a320541b8d0528a0f794f38a3c2b87 --- /dev/null +++ b/gui/src/components/search/input/InputHistogram.spec.js @@ -0,0 +1,311 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react' +import { fireEvent } from '@testing-library/dom' +import { format } from 'date-fns' +import { render, screen } from '../../conftest.spec' +import { Filter } from '../Filter' +import InputHistogram from './InputHistogram' +import userEvent from '@testing-library/user-event' +import { DType, formatNumber } from '../../../utils' +import { useSearchContext, SearchContextRaw } from '../SearchContext' + +// We set an initial mock for the SearchContext module +const mockSetFilter = jest.fn() +jest.mock('../SearchContext', () => ({ + ...jest.requireActual('../SearchContext'), + useSearchContext: jest.fn() +})) + +describe('', () => { + // Provide a default implementation for the search context mock + beforeEach(() => { + useSearchContext.mockImplementation(() => ({ + ...jest.requireActual('../SearchContext').useSearchContext(), + useAgg: (quantity, visible, id, config) => { + return useMemo(() => { + return visible + ? { + data: [{value: 0, count: 10}, {value: 1, count: 20}, {value: 2, count: 20}], + interval: 1 + } + : undefined + }, [visible]) + }, + useFilterState: (quantity) => { + const response = useMemo(() => { + return [undefined, mockSetFilter] + }, []) + return response + } + })) + }) + + describe('test showHeader', () => { + test.each([ + ['show header', {showHeader: true}], + ['do not show header', {showHeader: false}] + ])('%s', async (name, config) => { + renderInputHistogram(config, new Filter(undefined, {quantity: 'test'})) + if (config.showHeader) { + expect(screen.getByText('Test')).toBeInTheDocument() + } else { + expect(screen.queryByText('Test')).not.toBeInTheDocument() + } + }) + }) + + describe('test title', () => { + test.each([ + ['default title', {}, 'Test'], + ['custom title', {title: 'Custom title'}, 'Custom title'] + ])('%s', async (name, config, expected) => { + renderInputHistogram(config, new Filter(undefined, {quantity: 'test'})) + expect(screen.getByText(expected)).toBeInTheDocument() + }) + }) + + describe('test showStatistics', () => { + test.each([ + ['show statistics', {showStatistics: true}], + ['do not show statistics', {showStatistics: false}] + ])('%s', async (name, config) => { + renderInputHistogram(config, new Filter(undefined, {quantity: 'test'})) + const option = screen.queryAllByText('1') + const scaling = screen.queryByText('linear') + if (config.showStatistics) { + expect(option).toHaveLength(2) + expect(scaling).toBeInTheDocument() + } else { + expect(option).toHaveLength(0) + expect(scaling).not.toBeInTheDocument() + } + }) + }) + + describe('test showInput', () => { + test.each([ + ['show input', {showInput: true}], + ['do not show input', {showInput: false}] + ])('%s', async (name, config) => { + renderInputHistogram(config, new Filter(undefined, {quantity: 'test'})) + if (config.showInput) { + expect(screen.getByText('min:')).toBeInTheDocument() + expect(screen.getByText('max:')).toBeInTheDocument() + } else { + expect(screen.queryByPlaceholderText('min:')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('max:')).not.toBeInTheDocument() + } + }) + }) + + describe('test invalid/valid numeric input', () => { + for (const isMin of [false, true]) { + const value = isMin ? 0 : 3 + const field = isMin ? 'minimum' : 'maximum' + const message = `Invalid ${field} value.` + test.each([ + ['1', true], + ['1.0', true], + ['-1.0', true], + ['-1.0e5', true], + ['-1.0e-5', true], + ['hello', false], + [' ', false] + ])( + `input: %s, valid: %s`, + async (input, valid) => { + const user = userEvent.setup() + renderInputHistogram({}, new Filter(undefined, {quantity: 'test'})) + const field = await screen.findByDisplayValue(value) + await user.clear(field) + await user.type(field, input) + await user.keyboard('[Enter]') + if (valid) { + expect(screen.queryByText(message)).toBeNull() + } else { + expect(screen.queryByText(message)).toBeInTheDocument() + } + } + ) + } + }) + + describe('test input field change', () => { + // Custom aggregation data for this test + beforeEach(() => { + useSearchContext.mockImplementation(() => ({ + ...jest.requireActual('../SearchContext').useSearchContext(), + useAgg: (quantity, visible, id, config) => { + return useMemo(() => { + return visible + ? { + data: [{value: 0, count: 10}, {value: 1, count: 20}, {value: 2, count: 20}], + interval: 1 + } + : undefined + }, [visible]) + }, + useFilterState: (quantity) => { + const [filter, setFilter] = useState() + const setFilterWrapper = useCallback((value) => { + setFilter(value) + mockSetFilter(value) + }, []) + const response = useMemo(() => { + return [filter, setFilterWrapper] + }, [filter, setFilterWrapper]) + return response + } + })) + }) + test.each([ + ['min field', 0, 1, 3, 0, `left: 0%`], + ['max field', 3, 0, 1, 1, `left: 100%`] + ])('%s', async (name, value, gte, lte, slider, position) => { + const filter = new Filter(undefined, {quantity: 'test'}) + const user = userEvent.setup() + renderInputHistogram({}, filter) + const sliders = screen.getAllByRole('slider') + slider = sliders[slider] + + // Initially slider at the end + expect(slider).toHaveStyle(position) + + // Type in a new value + const input = await screen.findByDisplayValue(value) + await user.clear(input) + await user.type(input, '1') + await user.keyboard('[Enter]') + + // setFilter is triggered + expect(mockSetFilter.mock.calls).toHaveLength(1) + const argument = mockSetFilter.mock.calls[0][0]() + expect(argument.gte.value()).toBe(gte) + expect(argument.lte.value()).toBe(lte) + + // Changing input moves slider + await testSliderMove(filter.dtype, 0, 3, input, slider, 50, false) + }) + }) + + describe('test slider change', () => { + test.each([ + ['min slider', 0, 1, 3, 0, 1, {key: 'Up', code: 'Up'}], + ['max slider', 3, 0, 2, 1, -1, {key: 'Down', code: 'Down'}] + ])('%s', async (name, value, gte, lte, slider, step, input) => { + renderInputHistogram({}, new Filter(undefined, {quantity: 'test'})) + const sliders = screen.getAllByRole('slider') + slider = sliders[slider] + + // Change slider value with arrow keys + const inputMin = await screen.findByDisplayValue(value) + fireEvent.keyDown(slider, input) + + // setFilter is triggered + expect(mockSetFilter.mock.calls).toHaveLength(1) + const argument = mockSetFilter.mock.calls[0][0] + expect(argument.gte.value()).toBe(gte) + expect(argument.lte.value()).toBe(lte) + + // Moving slider changes input field + expect(mockSetFilter.mock.calls).toHaveLength(1) + expect(inputMin.value).toBe(formatNumber(value + step)) + }) + }) + + describe('test histograms with only one value', () => { + // Custom aggregation data for this test + beforeEach(() => { + useSearchContext.mockImplementation(() => ({ + ...jest.requireActual('../SearchContext').useSearchContext(), + useAgg: (quantity, visible, id, config) => { + return useMemo(() => { + return visible + ? { data: [{ value: 1, count: 10 }], interval: 0 } + : undefined + }, [visible]) + } + })) + }) + test.each([ + ['integer', new Filter(undefined, {quantity: 'test', dtype: DType.Int}), 1], + ['float', new Filter(undefined, {quantity: 'test', dtype: DType.Float}), 1], + ['timestamp', new Filter(undefined, {quantity: 'test', dtype: DType.Timestamp}), 1] + ])('quantity: %s', async (name, filter, value) => { + renderInputHistogram({}, filter) + + // Check that both text fields show the only available value + const inputValue = filter.dtype === DType.Timestamp + ? format(value, 'dd/MM/yyyy kk:mm') + : value + const inputs = await screen.findAllByDisplayValue(inputValue) + expect(inputs.length).toBe(2) + + // Check that slider is disabled: trying to modify the sliders does not + // update the input fields. + const sliders = screen.getAllByRole('slider') + const sliderMin = sliders[0] + const sliderMax = sliders[1] + fireEvent.keyDown(sliderMin, {key: 'Up', code: 'Up'}) + fireEvent.keyDown(sliderMax, {key: 'Down', code: 'Down'}) + const inputsNew = await screen.findAllByDisplayValue(inputValue) + expect(inputsNew.length).toBe(2) + }) + }) +}) + +/** + * Tests that a slider moves to the given location when text input changes. + * @param {string} quantity The quantity name + * @param {number} min Minimum value of the slider + * @param {number} max Maximum value of the slider + * @param {*} input Text input element + * @param {*} slider MUI slider knob element + * @param {number} percentage The percentage to move to. + * @param {bool} isMax Is the max knob being moved. + * @param {bool} isMax Is the slider shown for a histogram. + */ +async function testSliderMove(dtype, min, max, input, slider, percentage, isMax) { + const discretization = (dtype === DType.Int) ? 1 : 0 + const range = max - min + discretization + const value = min + range * (percentage / 100) - (isMax ? discretization : 0) + + const user = userEvent.setup() + await user.clear(input) + await user.type(input, value.toString()) + await user.keyboard('[Enter]') + + const style = window.getComputedStyle(slider) + const left = parseFloat(style.getPropertyValue('left').slice(0, -1)) + expect(left).toBeCloseTo(percentage, 8) +} + +// Helper function for rendering +function renderInputHistogram(config, filter) { + const searchQuantities = {test: filter} + render( + <SearchContextRaw + resource="entries" + id='entries' + initialSearchQuantities={searchQuantities} + > + <InputHistogram visible x={{search_quantity: filter.quantity}} {...config}/> + </SearchContextRaw> + ) +} diff --git a/gui/src/components/search/input/InputMetainfo.spec.js b/gui/src/components/search/input/InputMetainfo.spec.js index 0bc65da2fe6cbb29ca35a3e8602210aa50db9b98..ab1bab88e441cd0ad1b37f5cfcc006ea1ec198a3 100644 --- a/gui/src/components/search/input/InputMetainfo.spec.js +++ b/gui/src/components/search/input/InputMetainfo.spec.js @@ -40,8 +40,8 @@ test.each([ initialPagination={context.pagination} initialColumns={context.columns} initialRows={context.rows} - initialFilters={context?.filters} - initialFilterMenus={context.filter_menus} + initialSearchQuantities={context?.search_quantities} + initialMenu={context?.menu} initialFiltersLocked={context.filters_locked} initialDashboard={context?.dashboard} initialSearchSyntaxes={context?.search_syntaxes} diff --git a/gui/src/components/search/input/InputNestedObject.js b/gui/src/components/search/input/InputNestedObject.js new file mode 100644 index 0000000000000000000000000000000000000000..2f92457c39eac3ca05d9a687b68582756c613d28 --- /dev/null +++ b/gui/src/components/search/input/InputNestedObject.js @@ -0,0 +1,102 @@ +/* + * 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, { createContext } from 'react' +import { makeStyles, useTheme } from '@material-ui/core/styles' +// import { Divider } from '@material-ui/core' +import PropTypes from 'prop-types' +import clsx from 'clsx' +import InputHeader from './InputHeader' +import InputTooltip from './InputTooltip' +import { InputGrid } from './InputGrid' +import { useSearchContext } from '../SearchContext' + +/** + * InputSection can be used to group together quantities that should be searched + * together as an ES nested query: + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html + * + * By wrapping search components in InputSection, the API calls will be + * automatically performed as a nested query, and the visuals will change to + * indicate the grouping. + */ +export const inputSectionContext = createContext() +const useStyles = makeStyles(theme => ({ + root: { + width: '100%' + }, + grid: { + marginTop: theme.spacing(1) + } +})) +const InputNestedObject = React.memo(({ + title, + path, + description, + className, + showHeader, + classes, + children, + 'data-testid': testID +}) => { + const theme = useTheme() + const styles = useStyles({classes: classes, theme: theme}) + const { filterData } = useSearchContext() + + // Determine the description and units + const def = filterData[path] + const nested = def?.nested + const repeats = def?.repeats + const descFinal = description || def?.description || '' + const labelFinal = title || def?.label + + return <InputTooltip> + <div className={clsx(className, styles.root)} data-testid={testID}> + {showHeader && <InputHeader + quantity={path} + label={labelFinal} + description={descFinal} + disableWidget + disableStatistics + /> + } + <inputSectionContext.Provider value={{ + section: path, + nested: nested, + repeats: repeats + }}> + <InputGrid disablePadding className={styles.grid}>{children}</InputGrid> + </inputSectionContext.Provider> + </div> + </InputTooltip> +}) + +InputNestedObject.propTypes = { + title: PropTypes.string, + path: PropTypes.string.isRequired, + description: PropTypes.string, + showHeader: PropTypes.bool, + className: PropTypes.string, + classes: PropTypes.object, + children: PropTypes.node, + 'data-testid': PropTypes.string +} +InputNestedObject.defaultProps = { + showHeader: true +} + +export default InputNestedObject diff --git a/gui/src/components/search/menus/FilterSubMenuOptimade.js b/gui/src/components/search/input/InputOptimade.js similarity index 80% rename from gui/src/components/search/menus/FilterSubMenuOptimade.js rename to gui/src/components/search/input/InputOptimade.js index f42fa2cd3839a28eb008e2f74521453be2eb14c0..d2dc7c7aa89a81a9dbe8615e1963f13d9dc5040b 100644 --- a/gui/src/components/search/menus/FilterSubMenuOptimade.js +++ b/gui/src/components/search/input/InputOptimade.js @@ -15,11 +15,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { Box, TextField } from "@material-ui/core" +import { TextField } from "@material-ui/core" import { useSearchContext } from "../SearchContext" +import InputHeader from '../input/InputHeader' import AutoComplete from "@material-ui/lab/Autocomplete" import { useApi } from '../../api' import { debounce, isArray, isEmpty } from 'lodash' @@ -32,16 +32,11 @@ const identifiers = Object.keys(searchQuantities) .map(key => key.slice(prefix.length)) .sort() -const FilterSubMenuOptimade = React.memo(({ - id, - ...rest -}) => { +const InputOptimade = React.memo(({visible, title, showHeader}) => { const [suggestions, setSuggestions] = useState([['']]) const [values, setValues] = useState([{value: '', valid: true, msg: ''}]) - const {selected, open} = useContext(filterMenuContext) const {useFilterState} = useSearchContext() const {api} = useApi() - const visible = open && id === selected const [optimadeFilters, setOptimadeFilters] = useFilterState("optimade_filter") const renderSuggestion = useCallback((command, options, suggestions) => { @@ -190,34 +185,56 @@ const FilterSubMenuOptimade = React.memo(({ } }, [setFilter, setOptimadeFilter, validate, values]) - return <FilterSubMenu id={id} {...rest}> - {visible && values && values.length === suggestions.length && values.map((value, index) => { - return <Box key={index} paddingLeft={2} paddingRight={2}> - <AutoComplete - options={suggestions[index]} - style={{width: '100%'}} - onChange={(event, value) => handleChange(event, value, index)} - onKeyDown={(event) => (event.key === 'Enter' && handleChange(event, value.value, index))} - value={value.value} - getOptionSelected={(option, value) => option === value} - getOptionLabel={option => (option && String(option)) || ''} - renderInput={params => ( - <TextField - {...params} - variant='filled' label='Filter' - placeholder='Type optimade filter' margin='normal' fullWidth size='small' - onChange={event => handleInputChange(event.target.value, index)} - error={!!value.msg} - helperText={!!value.msg && value.msg} - /> - )} - /> - </Box> - })} - </FilterSubMenu> + return <> + {showHeader + ? <InputHeader + label={title || 'Optimade query'} + description="Use this field for creating an OPTIMADE query." + disableStatistics + disableWidget/> + : null + } + {(visible && values && values.length === suggestions.length) + ? <> + {values.map((value, index) => + <AutoComplete + key={index} + options={suggestions[index]} + style={{width: '100%'}} + onChange={(event, value) => handleChange(event, value, index)} + onKeyDown={(event) => (event.key === 'Enter' && handleChange(event, value.value, index))} + value={value.value} + getOptionSelected={(option, value) => option === value} + getOptionLabel={option => (option && String(option)) || ''} + renderInput={params => ( + <TextField + {...params} + variant='filled' + label='Filter' + placeholder='Type optimade filter' + margin='none' + fullWidth + size='small' + onChange={event => handleInputChange(event.target.value, index)} + error={!!value.msg} + helperText={!!value.msg && value.msg} + /> + )} + />)} + </> + : null + } + </> }) -FilterSubMenuOptimade.propTypes = { - id: PropTypes.string + +InputOptimade.propTypes = { + visible: PropTypes.bool, + title: PropTypes.string, + showHeader: PropTypes.bool +} + +InputOptimade.defaultProps = { + showHeader: true } -export default FilterSubMenuOptimade +export default InputOptimade diff --git a/gui/src/components/search/input/InputPeriodicTable.js b/gui/src/components/search/input/InputPeriodicTable.js index 7d74f53b67c96f7cd9099ff07c9a589baac02d4a..a9a85241b9816aa3c6e176ada395489658facc7f 100644 --- a/gui/src/components/search/input/InputPeriodicTable.js +++ b/gui/src/components/search/input/InputPeriodicTable.js @@ -21,9 +21,10 @@ import clsx from 'clsx' import { isNil } from 'lodash' import elementData from '../../../elementData' import { useResizeDetector } from 'react-resize-detector' -import { Tooltip } from '@material-ui/core' +import { Box, Tooltip } from '@material-ui/core' import InputHeader from './InputHeader' -import InputCheckbox from './InputCheckbox' +// eslint-disable-next-line no-unused-vars +import InputTerms from './InputTerms' import { makeStyles, useTheme, lighten } from '@material-ui/core/styles' import { useSearchContext } from '../SearchContext' import { approxInteger } from '../../../utils' @@ -165,6 +166,7 @@ Element.propTypes = { /** * Displays an interactive periodic table. */ +const options = {true: {label: 'only compositions that exclusively contain these atoms'}} const usePeriodicTableStyles = makeStyles(theme => ({ root: { minHeight: 0, // added min-height: 0 to prevent relayouting when within flexbox @@ -177,6 +179,7 @@ const usePeriodicTableStyles = makeStyles(theme => ({ position: 'absolute', top: theme.spacing(-0.2), left: '10%', + right: '20%', textAlign: 'center' } })) @@ -262,10 +265,14 @@ export const PeriodicTable = React.memo(({ } </svg> <div className={styles.container}> - <InputCheckbox - quantity="exclusive" - label="only compositions that exclusively contain these atoms" - ></InputCheckbox> + <InputTerms + visible={visible} + searchQuantity="exclusive" + options={options} + showHeader={false} + showInput={false} + showStatistics={false} + /> </div> </div> }) @@ -295,65 +302,75 @@ PeriodicTable.defaultProps = { const useInputPeriodicTableStyles = makeStyles(theme => ({ root: { display: 'flex', - flexDirection: 'column' + flexDirection: 'column', + height: '30rem' } })) const InputPeriodicTable = React.memo(({ - quantity, - label, - description, - visible, - initialScale, - anchored, - disableStatistics, - aggId, - className - }) => { + searchQuantity, + title, + description, + visible, + scale, + anchored, + showHeader, + showStatistics, + aggId, + className +}) => { const styles = useInputPeriodicTableStyles() - const [scale, setScale] = useState(initialScale) - const {filterData} = useSearchContext() + const [scaleState, setScaleState] = useState(scale) + const {filterData, useIsStatisticsEnabled} = useSearchContext() + const isStatisticsEnabled = useIsStatisticsEnabled() + showStatistics = isStatisticsEnabled && showStatistics // Determine the description and title - const def = filterData[quantity] + const def = filterData[searchQuantity] const descFinal = description || def?.description || '' - const labelFinal = label || def?.label + const labelFinal = title || def?.label return <div className={clsx(styles.root, className)}> - <InputHeader - quantity={quantity} - label={labelFinal} - description={descFinal} - scale={scale} - onChangeScale={setScale} - disableStatistics={disableStatistics} - disableAggSize - anchored={anchored} - /> + {showHeader + ? <InputHeader + quantity={searchQuantity} + label={labelFinal} + description={descFinal} + scale={scaleState} + onChangeScale={setScaleState} + disableStatistics={!showStatistics} + disableAggSize + anchored={anchored} + /> + : <Box marginTop={1.5}/> + } <PeriodicTable - quantity={quantity} + quantity={searchQuantity} visible={visible} - scale={scale} + scale={scaleState} anchored={anchored} - disableStatistics={disableStatistics} + disableStatistics={!showStatistics} aggId={aggId} /> </div> }) InputPeriodicTable.propTypes = { - quantity: PropTypes.string, - label: PropTypes.string, + searchQuantity: PropTypes.string, + title: PropTypes.string, description: PropTypes.string, + scale: PropTypes.string, + showHeader: PropTypes.bool, + showStatistics: PropTypes.bool, visible: PropTypes.bool, - initialScale: PropTypes.string, anchored: PropTypes.bool, - disableStatistics: PropTypes.bool, aggId: PropTypes.string, className: PropTypes.string } InputPeriodicTable.defaultProps = { - initialScale: 'linear' + scale: 'linear', + showHeader: true, + showStatistics: true } export default InputPeriodicTable diff --git a/gui/src/components/search/input/InputPeriodicTable.spec.js b/gui/src/components/search/input/InputPeriodicTable.spec.js index 1aead838a52df2c2cdf91ae0a18b46256ba575fb..d14ba3724d6990d6179ae2af5e5b667f39b358ae 100644 --- a/gui/src/components/search/input/InputPeriodicTable.spec.js +++ b/gui/src/components/search/input/InputPeriodicTable.spec.js @@ -15,60 +15,115 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react' -import userEvent from '@testing-library/user-event' -import { waitFor } from '@testing-library/dom' -import { startAPI, closeAPI, screen } from '../../conftest.spec' -import { - renderSearchEntry, - expectPeriodicTable, - expectPeriodicTableItems, - expectElement -} from '../conftest.spec' +import React, { useMemo } from 'react' +import { render, screen } from '../../conftest.spec' +import { Filter } from '../Filter' import InputPeriodicTable from './InputPeriodicTable' +import userEvent from '@testing-library/user-event' +import { useSearchContext, SearchContextRaw } from '../SearchContext' -const quantity = 'results.material.elements' -const stateName = 'tests.states.search.search' +// We set an initial mock for the SearchContext module +const mockSetFilter = jest.fn() +jest.mock('../SearchContext', () => ({ + ...jest.requireActual('../SearchContext'), + useSearchContext: jest.fn() +})) describe('', () => { - beforeEach(async () => { - await startAPI(stateName, 'tests/data/search/inputperiodictable') - renderSearchEntry(<InputPeriodicTable - quantity={quantity} - visible - /> - ) + // Provide a default implementation for the search context mock + beforeEach(() => { + useSearchContext.mockImplementation(() => ({ + ...jest.requireActual('../SearchContext').useSearchContext(), + useAgg: (quantity, visible, id, config) => { + return useMemo(() => { + return visible + ? {data: [{value: 'H', count: 123}]} + : undefined + }, [visible]) + }, + useFilterState: (quantity) => { + const response = useMemo(() => { + return [undefined, mockSetFilter] + }, []) + return response + } + })) }) - afterEach(() => closeAPI()) - test('initial state is loaded correctly', async () => { - await expectPeriodicTable(quantity, false, ['H', 'C', 'N', 'I', 'Pb', 'Ti', 'Zr', 'Nb', 'Hf', 'Ta']) + describe('test showHeader', () => { + test.each([ + ['show header', {showHeader: true}], + ['do not show header', {showHeader: false}] + ])('%s', async (name, config) => { + renderPeriodicTable(config, new Filter(undefined, {quantity: 'test'})) + if (config.showHeader) { + expect(screen.getByText('Test')).toBeInTheDocument() + } else { + expect(screen.queryByText('Test')).not.toBeInTheDocument() + } + }) }) -}) -describe('', () => { - beforeEach(async () => { - await startAPI(stateName, 'tests/data/search/inputperiodictable-edit') - renderSearchEntry(<InputPeriodicTable - quantity={quantity} - visible - /> - ) + describe('test title', () => { + test.each([ + ['default title', {}, 'Test'], + ['custom title', {title: 'Custom title'}, 'Custom title'] + ])('%s', async (name, config, expected) => { + renderPeriodicTable(config, new Filter(undefined, {quantity: 'test'})) + expect(screen.getByText(expected)).toBeInTheDocument() + }) }) - afterEach(() => closeAPI()) - test('selecting an element in both non-exclusive and exclusive mode correctly updates the table', async () => { - // Wait for hydrogen to become selectable - await waitFor(() => expectElement('Hydrogen', false)) + describe('test showStatistics', () => { + test.each([ + ['show statistics', {showStatistics: true}], + ['do not show statistics', {showStatistics: false}] + ])('%s', async (name, config) => { + renderPeriodicTable(config, new Filter(undefined, {quantity: 'test'})) - // Test that after selecting C, only the correct elements are selectable. - const cButton = screen.getByTestId('Carbon') - await userEvent.click(cButton) - await expectPeriodicTableItems(['H', 'C', 'N', 'I', 'Pb']) + const option = screen.queryAllByText('123') + const scaling = screen.queryByText('linear') + if (config.showStatistics) { + expect(option).toHaveLength(1) + expect(scaling).toBeInTheDocument() + } else { + expect(option).toHaveLength(0) + expect(scaling).not.toBeInTheDocument() + } + }) + }) + + describe('test selection', () => { + test.each([ + ['show statistics', {showStatistics: true}], + ['do not show statistics', {showStatistics: false}] + ])('%s', async (name, config) => { + renderPeriodicTable(config, new Filter(undefined, {quantity: 'test'})) - // Test that after enabling exclusive search, only C is selectable - const exclusiveCheckbox = screen.getByRole('checkbox') - await userEvent.click(exclusiveCheckbox) - await expectPeriodicTableItems(['C']) + // Click on hydrogen + const cButton = screen.getByTestId('Hydrogen') + await userEvent.click(cButton) + + // setFilter is called + expect(mockSetFilter.mock.calls).toHaveLength(1) + const argument = mockSetFilter.mock.calls[0][0]() + const expectedArgument = new Set(['H']) + expect(argument.size === expectedArgument.size).toBe(true) + expect([...argument].every((x) => expectedArgument.has(x))).toBe(true) + }) }) }) + +// Helper function for rendering +function renderPeriodicTable(config, filter) { + const searchQuantities = {test: filter} + render( + <SearchContextRaw + resource="entries" + id='entries' + initialSearchQuantities={searchQuantities} + > + <InputPeriodicTable visible searchQuantity={filter.quantity} {...config}/> + </SearchContextRaw> + ) +} diff --git a/gui/src/components/search/input/InputRange.spec.js b/gui/src/components/search/input/InputRange.spec.js deleted file mode 100644 index b5da2751d8b0db79c308ad39740a5fae9ddb8826..0000000000000000000000000000000000000000 --- a/gui/src/components/search/input/InputRange.spec.js +++ /dev/null @@ -1,203 +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 { fireEvent } from '@testing-library/dom' -import userEvent from '@testing-library/user-event' -import { format } from 'date-fns' -import { startAPI, closeAPI, screen } from '../../conftest.spec' -import { renderSearchEntry, expectInputRange } from '../conftest.spec' -import InputRange from './InputRange' -import { defaultFilterData } from '../FilterRegistry' -import { DType, formatNumber } from '../../../utils' - -const nBins = 30 -const stateName = 'tests.states.search.histograms' -const discrete = ['results.material.n_elements', false, 1, 5, 1] -const discrete_histogram = ['results.material.n_elements', true, 1, 5, 1] -const continuous = ['results.properties.electronic.band_structure_electronic.band_gap.value', false, 0, 2, 0.1] -const continuous_histogram = ['results.properties.electronic.band_structure_electronic.band_gap.value', true, 0, 2, 2 / nBins] -const time = ['upload_create_time', false, 1585872000000, 1585872240000, (1585872240000 - 1585872000000) / nBins] -const time_histogram = ['upload_create_time', true, 1585872000000, 1585872240000, undefined] - -describe('test initial state', () => { - beforeAll(async () => { await startAPI(stateName, 'tests/data/search/inputrange-init') }) - afterAll(() => closeAPI()) - - test.each([ - continuous, - continuous_histogram, - discrete, - discrete_histogram, - time, - time_histogram - ])('quantity: %s, histogram: %s', async (quantity, histogram, min, max) => { - renderSearchEntry(<InputRange visible quantity={quantity} disableHistogram={!histogram}/>) - await expectInputRange({x: {quantity}}, false, histogram, false, min, max) - }) -}) - -describe('test invalid/valid numeric input', () => { - beforeAll(async () => { await startAPI(stateName, 'tests/data/search/inputrange-validation') }) - afterAll(() => closeAPI()) - const [quantity, histogram, min, max] = discrete - - for (const isMin of [false, true]) { - const value = isMin ? min : max - const field = isMin ? 'minimum' : 'maximum' - const message = `Invalid ${field} value.` - test.each([ - ['1', true], - ['1.0', true], - ['-1.0', true], - ['-1.0e5', true], - ['-1.0e-5', true], - ['hello', false], - [' ', false] - ])( - `field: ${field}, input: %s, valid: %s`, - async (input, valid) => { - renderSearchEntry(<InputRange visible quantity={quantity} disableHistogram={!histogram}/>) - const user = userEvent.setup() - const field = await screen.findByDisplayValue(value) - await user.clear(field) - await user.type(field, input) - await user.keyboard('[Enter]') - if (valid) { - expect(screen.queryByText(message)).toBeNull() - } else { - expect(screen.queryByText(message)).toBeInTheDocument() - } - } - ) - } -}) - -describe('test histograms with only one value', () => { - beforeAll(async () => { - await startAPI( - 'tests.states.search.histograms_one_value', - 'tests/data/search/inputrange-one-value' - ) - }) - afterAll(() => closeAPI()) - - test.each([ - ['results.material.n_elements', 1], - ['results.properties.electronic.band_structure_electronic.band_gap.value', 0.5], - ['upload_create_time', 1585872000000] - ])('quantity: %s', async (quantity, value) => { - renderSearchEntry(<InputRange visible quantity={quantity} disableHistogram={false}/>) - const data = defaultFilterData[quantity] - const dtype = data.dtype - - // Check that both text fields show the only available value - const inputValue = dtype === DType.Timestamp - ? format(value, 'dd/MM/yyyy kk:mm') - : value - const inputs = await screen.findAllByDisplayValue(inputValue) - expect(inputs.length).toBe(2) - - // Check that slider is disabled: trying to modify the sliders does not - // update the input fields. - const sliders = screen.getAllByRole('slider') - const sliderMin = sliders[0] - const sliderMax = sliders[1] - fireEvent.keyDown(sliderMin, {key: 'Up', code: 'Up'}) - fireEvent.keyDown(sliderMax, {key: 'Down', code: 'Down'}) - const inputsNew = await screen.findAllByDisplayValue(inputValue) - expect(inputsNew.length).toBe(2) - }) -}) - -test.each([ - discrete, - discrete_histogram, - continuous, - continuous_histogram -])('inputs react to slider change: quantity: %s, histogram: %s', async (quantity, histogram, min, max, step) => { - await startAPI(stateName, `tests/data/search/inputrange-${quantity}-${histogram}-slider-change`) - renderSearchEntry(<InputRange visible quantity={quantity} disableHistogram={!histogram}/>) - const inputMin = await screen.findByDisplayValue(min) - const inputMax = await screen.findByDisplayValue(max) - const sliders = screen.getAllByRole('slider') - const sliderMin = sliders[0] - const sliderMax = sliders[1] - - // Moving min slider changes min field - fireEvent.keyDown(sliderMin, {key: 'Up', code: 'Up'}) - expect(inputMin.value).toBe(formatNumber(min + step)) - - // Moving max slider changes max field - fireEvent.keyDown(sliderMax, {key: 'Down', code: 'Down'}) - expect(inputMax.value).toBe(formatNumber(max - step)) - closeAPI() -}) - -test.each([ - continuous, - continuous_histogram, - discrete, - discrete_histogram -])('sliders react to input field change: quantity: %s, histogram: %s', async (quantity, histogram, min, max) => { - await startAPI(stateName, `tests/data/search/inputrange-${quantity}-${histogram}-input-change`) - renderSearchEntry(<InputRange visible quantity={quantity} disableHistogram={!histogram}/>) - const inputMin = await screen.findByDisplayValue(min) - const inputMax = await screen.findByDisplayValue(max) - const sliders = screen.getAllByRole('slider') - const sliderMin = sliders[0] - const sliderMax = sliders[1] - - // Initially both sliders at the ends - expect(sliderMin).toHaveStyle(`left: 0%`) - expect(sliderMax).toHaveStyle(`left: 100%`) - - // After changing the min field to 25%, min slider moves to 25% position. - await testSliderMove(quantity, min, max, inputMin, sliderMin, 25, false, histogram) - - // After changing the max field to 75%, min slider moves to 75% position. - await testSliderMove(quantity, min, max, inputMax, sliderMax, 75, true, histogram) - closeAPI() -}) - -/** - * Tests that a slider moves to the given location when text input changes. - * @param {string} quantity The quantity name - * @param {number} min Minimum value of the slider - * @param {number} max Maximum value of the slider - * @param {*} input Text input element - * @param {*} slider MUI slider knob element - * @param {number} percentage The percentage to move to. - * @param {bool} isMax Is the max knob being moved. - * @param {bool} isMax Is the slider shown for a histogram. - */ -async function testSliderMove(quantity, min, max, input, slider, percentage, isMax, histogram) { - const data = defaultFilterData[quantity] - const dtype = data.dtype - const discretization = (histogram && dtype === DType.Int) ? 1 : 0 - const range = max - min + discretization - const value = min + range * (percentage / 100) - (isMax ? discretization : 0) - - const user = userEvent.setup() - await user.clear(input) - await user.type(input, value.toString()) - await user.keyboard('[Enter]') - - const style = window.getComputedStyle(slider) - const left = parseFloat(style.getPropertyValue('left').slice(0, -1)) - expect(left).toBeCloseTo(percentage, 8) -} diff --git a/gui/src/components/search/input/InputField.js b/gui/src/components/search/input/InputTerms.js similarity index 61% rename from gui/src/components/search/input/InputField.js rename to gui/src/components/search/input/InputTerms.js index f3bbb19c19163f279913348a90a625546faa3e70..199e5dbf89d1837324648e4df3d38bf4769e6059 100644 --- a/gui/src/components/search/input/InputField.js +++ b/gui/src/components/search/input/InputTerms.js @@ -17,6 +17,7 @@ */ import React, { useCallback, useEffect, useState, useMemo } from 'react' import { makeStyles, useTheme } from '@material-ui/core/styles' +import { Box } from '@material-ui/core' import PropTypes from 'prop-types' import clsx from 'clsx' import { useRecoilValue } from 'recoil' @@ -25,9 +26,8 @@ import InputTooltip from './InputTooltip' import InputItem, { inputItemHeight } from './InputItem' import InputUnavailable from './InputUnavailable' import Placeholder from '../../visualization/Placeholder' -import { formatLabel } from '../../../utils' import { useSearchContext } from '../SearchContext' -import { isNil } from 'lodash' +import { isNil, isNumber } from 'lodash' import Pagination from '../../visualization/Pagination' import { guiState } from '../../GUIMenu' import { InputTextQuantity } from './InputText' @@ -48,7 +48,8 @@ const useStyles = makeStyles(theme => ({ alignItems: 'flex-start', justifyContent: 'center', flexDirection: 'column', - boxSizing: 'border-box' + boxSizing: 'border-box', + marginBottom: theme.spacing(-0.5) }, container: { width: '100%' @@ -65,20 +66,20 @@ const useStyles = makeStyles(theme => ({ marginBottom: theme.spacing(1) } })) -const InputField = React.memo(({ - quantity, - label, +const InputTerms = React.memo(({ + searchQuantity, + title, description, visible, - xs, - initialScale, - initialSize, + nColumns, + scale, increment, - disableStatistics, - disableSearch, - disableOptions, - disableSuggestions, - formatLabels, + showInput, + showHeader, + showStatistics, + showSuggestions, + options, + sortStatic, className, classes, 'data-testid': testID @@ -96,84 +97,94 @@ const InputField = React.memo(({ const [visibleOptions, setVisibleOptions] = useState() const aggIndicator = useRecoilValue(guiState('aggIndicator')) const aggCollapse = useRecoilValue(guiState('aggCollapse')) - const [scale, setScale] = useState(initialScale || filterData[quantity]?.scale || 'linear') - disableStatistics = isNil(disableStatistics) ? !isStatisticsEnabled : disableStatistics + const [scaleState, setScaleState] = useState(scale || filterData[searchQuantity]?.scale || 'linear') + showStatistics = isStatisticsEnabled && showStatistics + const nOptions = isNumber(options) ? options : undefined // See if the filter has a fixed amount of options. These may have been - // explicitly provided or defined in the metainfo. If you explicitly specify - // an initialSize, any fixed options are ignored and the data is retrieved - // through an aggregation (this is done because the top aggregations may not - // match the list of explicit options). - const fixedOptions = useMemo(() => { - return isNil(initialSize) - ? filterData[quantity]?.options + // explicitly provided or defined in the metainfo. If options is given as a + // number, any fixed options are ignored and the data is retrieved through an + // aggregation. + const fixedOptions = useMemo(() => isNil(nOptions) + ? options || filterData[searchQuantity]?.options : undefined - }, [initialSize, filterData, quantity]) + , [nOptions, filterData, searchQuantity, options]) + const nFixedOptions = fixedOptions && Object.keys(fixedOptions).length - const minSize = disableOptions ? 0 : initialSize || nFixedOptions || filterData[quantity]?.aggs?.terms?.size - const placeholder = filterData[quantity]?.placeholder || "Type here" + const minSize = nOptions === 0 + ? 0 + : nOptions || nFixedOptions || filterData[searchQuantity]?.aggs?.terms?.size || 5 + const placeholder = filterData[searchQuantity]?.placeholder || "Type here" const [requestedAggSize, setRequestedAggSize] = useState(minSize) const incr = useState(increment || minSize)[0] const [loading, setLoading] = useState(false) + + // If a fixed list of options is used, we must restrict the aggregation return + // values with 'include'. Otherwise the returned results may contain other + // values. const aggConfig = useMemo(() => { const config = {type: 'terms', size: minSize} - // If a fixed list of options is used, we must restrict the aggregation - // return values with 'include'. Otherwise the returned results may not - // contain the correct values. - const options = filterData[quantity]?.options - if (options) config.include = Object.keys(options) + if (fixedOptions) config.include = Object.keys(fixedOptions) return config - }, [minSize, filterData, quantity]) - const agg = useAgg(quantity, visible && !disableOptions && !(disableStatistics && fixedOptions), 'scroll', aggConfig) - const aggCall = useAggCall(quantity, 'scroll') + }, [minSize, fixedOptions]) + + const agg = useAgg(searchQuantity, visible && !(nOptions === 0) && !(!showStatistics && fixedOptions), 'scroll', aggConfig) + const aggCall = useAggCall(searchQuantity, 'scroll') const receivedAggSize = agg?.data?.length - const [filter, setFilter] = useFilterState(quantity) - const unavailable = disableOptions ? false : !(agg?.data && agg.data.length > 0) + const [filter, setFilter] = useFilterState(searchQuantity) + const unavailable = (nOptions === 0) ? false : !(agg?.data && agg.data.length > 0) const disabled = unavailable // Form the final list of options. If no fixed options are available, the // options are gathered from the aggregation. const finalOptions = useMemo(() => { - if (fixedOptions) { - return fixedOptions - } - if (agg?.data) { - const opt = {} - const maxSize = Math.min(requestedAggSize, agg.data.length) - for (let i = 0; i < maxSize; ++i) { - const value = agg.data[i] - opt[value.value] = {label: value.value} - } + if (fixedOptions) return fixedOptions + if (!agg?.data) return {} + + const maxSize = Math.min(requestedAggSize, agg.data.length) + return agg.data.slice(0, maxSize).reduce((opt, { value }) => { + opt[value] = { label: value } return opt - } - return {} + }, {}) }, [fixedOptions, agg, requestedAggSize]) // Modify the checkboxes according to changing filters, changing aggregation // results or change in the available options. useEffect(() => { - const opt = {} - for (const [key, value] of Object.entries(finalOptions)) { + let options = Object.entries(finalOptions).reduce((opt, [key, value]) => { + const selected = filter?.has(key) || false opt[key] = { - checked: filter ? filter.has(key) : false, - label: formatLabels ? formatLabel(value.label) : value.label, - disabled: !disableStatistics + checked: selected, + label: value.label, + disabled: isStatisticsEnabled && showStatistics && !selected } - } + return opt + }, {}) + if (agg?.data) { - for (const value of agg.data) { - const key = value.value - const selected = filter ? filter.has(key) : false - const oldState = opt[key] - const disabled = selected ? false : value.count === 0 - if (oldState) { - oldState.count = value.count - oldState.disabled = disabled + // Update counts and disable if not selected and count is 0 + agg.data?.forEach(({ value, nested_count }) => { + const selected = filter?.has(value) || false + if (options[value]) { + options[value].nested_count = nested_count + options[value].disabled = selected ? false : nested_count === 0 } + }) + + // Sort by count if using fixed options and sorting is enabled + if (fixedOptions && sortStatic) { + options = Object.fromEntries( + Object.entries(options).sort(([, a], [, b]) => { + const bCount = b.nested_count || 0 + const aCount = a.nested_count || 0 + return bCount - aCount + }) + ) } } - setVisibleOptions(opt) - }, [agg, filter, finalOptions, disableStatistics, formatLabels]) + + setVisibleOptions(options) + }, [agg?.data, filter, finalOptions, fixedOptions, isStatisticsEnabled, showStatistics, sortStatic]) // Show more values const handleShowMore = useCallback(() => { @@ -209,25 +220,25 @@ const InputField = React.memo(({ // Create the search component const searchComponent = useMemo(() => { - return disableSearch - ? null - : <InputTooltip unavailable={unavailable}> + return showInput + ? <InputTooltip unavailable={unavailable}> <div className={styles.container}> <InputTextQuantity className={styles.textField} - quantity={quantity} + quantity={searchQuantity} disabled={disabled} - disableSuggestions={disableSuggestions} + disableSuggestions={!showSuggestions} placeholder={placeholder} fullWidth /> </div> </InputTooltip> - }, [disableSearch, unavailable, styles, quantity, disabled, disableSuggestions, placeholder]) + : null + }, [showInput, unavailable, styles, searchQuantity, disabled, showSuggestions, placeholder]) // Create the options component const optionsComponent = useMemo(() => { - if (disableOptions) { + if ((nOptions === 0)) { return } @@ -242,6 +253,7 @@ const InputField = React.memo(({ ? false : (requestedAggSize - incr >= minSize) + const xs = 12 / nColumns const nRows = Math.ceil(nItems * xs / 12) const actionsHeight = 34 let reservedHeight @@ -251,12 +263,12 @@ const InputField = React.memo(({ reservedHeight = nItems > 0 ? (itemHeight + actionHeight) : undefined } else if (aggCollapse === 'off') { const itemHeight = Math.max(minSize * xs / 12, nRows) * inputItemHeight - const allLoaded = !isNil(nFixedOptions) && initialSize >= nFixedOptions + const allLoaded = !isNil(nFixedOptions) && nOptions >= nFixedOptions const actionHeight = (fixedOptions || allLoaded) ? 0 : actionsHeight reservedHeight = itemHeight + actionHeight } - const max = agg ? Math.max(...agg.data.map(option => option.count)) : 0 + const max = agg ? Math.max(...agg.data.map(option => option.nested_count)) : 0 const items = visibleOptions && <div className={styles.grid} style={{gridTemplateRows: `repeat(${nRows}, 1fr)`}} @@ -265,25 +277,31 @@ const InputField = React.memo(({ <InputItem key={key} value={key} + tooltip={value.description} label={value.label} selected={value.checked} disabled={value.disabled} - disableStatistics={disableStatistics} + disableStatistics={!showStatistics} onChange={handleChange} variant="checkbox" max={max} - count={value.count} - scale={scale} + count={value.nested_count} + scale={scaleState} /> ))} </div> const noMore = agg?.exhausted && receivedAggSize === requestedAggSize let aggComp - if (fixedOptions) { + + // If statistics are disabled and there is a fixed number of options, we + // show the options immediately. + if ((!showStatistics || !sortStatic) && fixedOptions) { aggComp = items - } else if (receivedAggSize === 0) { + // No fixed options or aggregation data is available + } else if (receivedAggSize === 0 && !fixedOptions) { aggComp = <InputUnavailable/> + // Show placeholder if aggregation is underway } else if (!agg && aggIndicator === 'on') { aggComp = <Placeholder variant="rect" @@ -308,8 +326,7 @@ const InputField = React.memo(({ {aggComp} </div> }, [ - disableOptions, - disableStatistics, + showStatistics, agg, finalOptions, minSize, @@ -317,57 +334,65 @@ const InputField = React.memo(({ receivedAggSize, requestedAggSize, incr, - xs, + nColumns, aggCollapse, visibleOptions, styles, aggIndicator, - initialSize, handleChange, - scale, + scaleState, handleShowMore, handleShowLess, loading, nFixedOptions, - testID - ] - ) + testID, + nOptions, + sortStatic + ]) return <div className={clsx(className, styles.root)} data-testid={testID}> - <InputHeader - quantity={quantity} - label={label} - description={description} - scale={scale} - onChangeScale={setScale} - disableStatistics={disableStatistics} - /> + {showHeader + ? <InputHeader + quantity={searchQuantity} + label={title} + description={description} + scale={scaleState} + onChangeScale={setScaleState} + disableStatistics={!showStatistics} + /> + : <Box marginBottom={0.5}/> + } {searchComponent} {optionsComponent} </div> }) -InputField.propTypes = { - quantity: PropTypes.string.isRequired, - label: PropTypes.string, +InputTerms.propTypes = { + searchQuantity: PropTypes.string.isRequired, + title: PropTypes.string, description: PropTypes.string, visible: PropTypes.bool, - xs: PropTypes.number, - initialScale: PropTypes.number, // The initial statistics scaling - initialSize: PropTypes.number, // The initial maximum number of items to load + nColumns: PropTypes.number, // Number of columns to use + scale: PropTypes.string, // The statistics scaling + options: PropTypes.oneOfType([PropTypes.number, PropTypes.object]), // Controls what options to show + sortStatic: PropTypes.bool, // Whether to sort statically defined options by occurrence increment: PropTypes.number, // The amount of new items to load on 'show more' - disableStatistics: PropTypes.bool, // Whether to disable statistics - disableSearch: PropTypes.bool, // Whether to show the search field - disableOptions: PropTypes.bool, // Whether to show the options gathered through aggregations - disableSuggestions: PropTypes.bool, // Whether to disable the text field suggestions - formatLabels: PropTypes.bool, // Whether to reformat the options labels + showInput: PropTypes.bool, // Whether to show the search input field + showHeader: PropTypes.bool, // Whether to show the header + showStatistics: PropTypes.bool, // Whether to disable statistics + showSuggestions: PropTypes.bool, // Whether to disable the text field suggestions className: PropTypes.string, classes: PropTypes.object, 'data-testid': PropTypes.string } -InputField.defaultProps = { - xs: 12 +InputTerms.defaultProps = { + nColumns: 1, + showHeader: true, + showInput: true, + showStatistics: true, + sortStatic: true, + 'data-testid': 'input-terms' } -export default InputField +export default InputTerms diff --git a/gui/src/components/search/input/InputTerms.spec.js b/gui/src/components/search/input/InputTerms.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..cffd755969645b503b5a0f9b11c3bfca10da0826 --- /dev/null +++ b/gui/src/components/search/input/InputTerms.spec.js @@ -0,0 +1,209 @@ +/* + * 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, { useMemo } from 'react' +import { waitFor, within } from '@testing-library/dom' +import { render, screen } from '../../conftest.spec' +import { expectInputItem } from '../conftest.spec' +import { SearchContextRaw } from '../SearchContext' +import { Filter } from '../Filter' +import { isNumber } from 'lodash' +import InputTerms from './InputTerms' +import userEvent from '@testing-library/user-event' + +// Use a mocked SearchContext +const mockSetFilter = jest.fn() +const mockUseMemo = useMemo +jest.mock('../SearchContext', () => ({ + ...jest.requireActual('../SearchContext'), + useSearchContext: () => ({ + ...jest.requireActual('../SearchContext').useSearchContext(), + useAgg: (quantity, visible, id, config) => { + const response = mockUseMemo(() => { + return visible + ? {data: [ + {value: 'A', count: 6}, + {value: 'B', count: 5}, + {value: 'C', count: 4}, + {value: 'D', count: 3}, + {value: 'E', count: 2}, + {value: 'F', count: 1} + ].slice(0, config.size)} + : undefined + }, []) + return response + }, + useFilterState: jest.fn((quantity) => { + const response = mockUseMemo(() => { + return [undefined, mockSetFilter] + }, []) + return response + }) + }) +})) + +describe('test options', () => { + test.each([ + [ + 'show all options for enum by default', + {options: undefined}, + new Filter(undefined, {quantity: 'test', options: {A: {label: 'A'}, B: {label: 'B'}}}), + [{label: 'A'}, {label: 'B'}] + ], + [ + 'show 5 options for str by default', + {options: undefined}, + new Filter(undefined, {quantity: 'test'}), + [{label: 'A'}, {label: 'B'}, {label: 'C'}, {label: 'D'}, {label: 'E'}] + ], + [ + 'no options', + {options: 0}, + new Filter(undefined, {quantity: 'test'}), + [] + ], + [ + 'limited options', + {options: 2}, + new Filter(undefined, {quantity: 'test'}), + [{label: 'A'}, {label: 'B'}] + ], + [ + 'custom options', + {options: {B: {label: 'B'}}}, + new Filter(undefined, {quantity: 'test', options: {A: {label: 'A'}, B: {label: 'B'}}}), + [{label: 'B'}] + ] + ])('%s', async (name, config, filter, expected) => { + renderInputTerms(config, filter) + + // Wait for possible placeholder to disappear + await waitFor(() => expect(screen.queryByTestId(`input-terms-placeholder`)).toBe(null)) + + // Check that each expected item appears + expect(screen.queryAllByRole('checkbox').length).toBe(expected.length) + for (const item of expected) { + expectInputItem(item) + } + + // When getting options dynamically, test that the "show more" button is + // shown, but "show less" is not shown + if (isNumber(config.options) && config.options > 0) { + expect(screen.getByText('Show more')).toBeInTheDocument() + expect(screen.queryByText('Show less')).not.toBeInTheDocument() + } + }) +}) + +describe('test showHeader', () => { + test.each([ + ['show header', {showHeader: true}], + ['do not show header', {showHeader: false}] + ])('%s', async (name, config) => { + renderInputTerms(config, new Filter(undefined, {quantity: 'test'})) + if (config.showHeader) { + expect(screen.getByText('Test')).toBeInTheDocument() + } else { + expect(screen.queryByText('Test')).not.toBeInTheDocument() + } + }) +}) + +describe('test title', () => { + test.each([ + ['default title', {}, 'Test'], + ['custom title', {title: 'Custom title'}, 'Custom title'] + ])('%s', async (name, config, expected) => { + renderInputTerms(config, new Filter(undefined, {quantity: 'test'})) + expect(screen.getByText(expected)).toBeInTheDocument() + }) +}) + +describe('test showStatistics', () => { + test.each([ + ['show statistics', {showStatistics: true}], + ['do not show statistics', {showStatistics: false}] + ])('%s', async (name, config) => { + renderInputTerms(config, new Filter(undefined, {quantity: 'test'})) + const option = screen.queryByText('6') + const scaling = screen.queryByText('linear') + if (config.showStatistics) { + expect(option).toBeInTheDocument() + expect(scaling).toBeInTheDocument() + } else { + expect(option).not.toBeInTheDocument() + expect(scaling).not.toBeInTheDocument() + } + }) +}) + +describe('test showInput', () => { + test.each([ + ['show input', {showInput: true}], + ['do not show input', {showInput: false}] + ])('%s', async (name, config) => { + renderInputTerms(config, new Filter(undefined, {quantity: 'test'})) + if (config.showInput) { + expect(screen.getByPlaceholderText('Type here')).toBeInTheDocument() + } else { + expect(screen.queryByPlaceholderText('Type here')).not.toBeInTheDocument() + } + }) +}) + +test.only('test item selection', async () => { + renderInputTerms({}, new Filter(undefined, {quantity: 'test'})) + + // Wait for possible placeholder to disappear + await waitFor(() => expect(screen.queryByTestId(`input-terms-placeholder`)).toBe(null)) + + // Select one item + const checkbox = queryByInputItemName('A') + await userEvent.click(checkbox) + + // Check that the setFilter function is called once with the correct argument + expect(mockSetFilter.mock.calls).toHaveLength(1) + const argument = mockSetFilter.mock.calls[0][0] + const expectedArgument = new Set(['A']) + expect(argument.size === expectedArgument.size).toBe(true) + expect([...argument].every((x) => expectedArgument.has(x))).toBe(true) +}) + +// Helper function for rendering +function renderInputTerms(config, filter) { + const searchQuantities = {test: filter} + render( + <SearchContextRaw + resource="entries" + id='entries' + initialSearchQuantities={searchQuantities} + > + <InputTerms visible searchQuantity={filter.quantity} {...config}/> + </SearchContextRaw> + ) +} + +/** + * Finds the checkbox corresponding to an InputItem with the given value. + * @param {string} name The option value that is displayed + * @returns {element} The checkbox input HTML element. + */ +function queryByInputItemName(option, root = screen) { + const inputLabel = root.queryByText(option) + const inputCheckbox = inputLabel && within(inputLabel.closest('label')).getByRole('checkbox') + return inputCheckbox +} diff --git a/gui/src/components/search/input/InputVisibility.js b/gui/src/components/search/input/InputVisibility.js new file mode 100644 index 0000000000000000000000000000000000000000..a7efda6881899ff7c33ac19d7ef90cd431a9a411 --- /dev/null +++ b/gui/src/components/search/input/InputVisibility.js @@ -0,0 +1,43 @@ +/* + * 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 InputRadio from './InputRadio' +import { useApi } from '../../api' + +const InputVisibility = React.memo(() => { + const {api} = useApi() + const authenticated = api?.keycloak?.authenticated + + return <InputRadio + quantity="visibility" + label="Visibility" + initialValue={authenticated ? 'visible' : 'public'} + options={{ + all: {label: 'All', disabled: !authenticated, tooltip: 'Consider all entries.'}, + public: {label: 'Public', disabled: false, tooltip: 'Consider all entries that can be publically downloaded, i.e. only published entries without embargo.'}, + visible: {label: 'Visible', disabled: !authenticated, tooltip: 'Consider all entries that are visible to you. This includes entries with embargo or unpublished entries that belong to you or are shared with you.'}, + shared: {label: 'Shared', disabled: !authenticated, tooltip: 'Only consider entries that belong to you or are shared with you.'}, + user: {label: 'User', disabled: !authenticated, tooltip: 'Only consider entries that belong to you.'}, + staging: {label: 'Unpublished', disabled: !authenticated, tooltip: 'Only search through unpublished entries.'} + }} + /> +}) + +InputVisibility.propTypes = {} + +export default InputVisibility diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js deleted file mode 100644 index 37ce4f8544cf4d4be1efc2c9e61f8bb7217e324f..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ /dev/null @@ -1,176 +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, { 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 FilterSubMenuElements from './FilterSubMenuElements' -import FilterSubMenuStructure from './FilterSubMenuStructure' -import FilterSubMenuMethod from './FilterSubMenuMethod' -import FilterSubMenuPrecision from './FilterSubMenuPrecision' -import FilterSubMenuDFT from './FilterSubMenuDFT' -import FilterSubMenuTB from './FilterSubMenuTB' -import FilterSubMenuGW from './FilterSubMenuGW' -import FilterSubMenuBSE from './FilterSubMenuBSE' -import FilterSubMenuDMFT from './FilterSubMenuDMFT' -import FilterSubMenuEELS from './FilterSubMenuEELS' -import FilterSubMenuElectronic from './FilterSubMenuElectronic' -import FilterSubMenuSolarCell from './FilterSubMenuSolarCell' -import FilterSubMenuCatalyst from './FilterSubMenuCatalystProperties' -import FilterSubMenuVibrational from './FilterSubMenuVibrational' -import FilterSubMenuMechanical from './FilterSubMenuMechanical' -import FilterSubMenuMolecularDynamics from './FilterSubMenuMolecularDynamics' -import FilterSubMenuELN from './FilterSubMenuELN' -import FilterSubMenuAuthor from './FilterSubMenuAuthor' -import FilterSubMenuMetadata from './FilterSubMenuMetadata' -import FilterSubMenuOptimade from './FilterSubMenuOptimade' -import { useSearchContext } from '../SearchContext' -import { delay } from '../../../utils' -import FilterSubMenuGeometryOptimization from './FilterSubMenuGeometryOptimization' -import InputCheckbox from '../input/InputCheckbox' -import FilterSubMenuCustomQuantities from './FilterSubMenuCustomQuantities' - -export const menuMap = { - elements: FilterSubMenuElements, - structure: FilterSubMenuStructure, - method: FilterSubMenuMethod, - precision: FilterSubMenuPrecision, - dft: FilterSubMenuDFT, - tb: FilterSubMenuTB, - gw: FilterSubMenuGW, - bse: FilterSubMenuBSE, - dmft: FilterSubMenuDMFT, - eels: FilterSubMenuEELS, - electronic: FilterSubMenuElectronic, - solarcell: FilterSubMenuSolarCell, - heterogeneouscatalyst: FilterSubMenuCatalyst, - vibrational: FilterSubMenuVibrational, - mechanical: FilterSubMenuMechanical, - molecular_dynamics: FilterSubMenuMolecularDynamics, - geometry_optimization: FilterSubMenuGeometryOptimization, - eln: FilterSubMenuELN, - custom_quantities: FilterSubMenuCustomQuantities, - author: FilterSubMenuAuthor, - metadata: FilterSubMenuMetadata, - optimade: FilterSubMenuOptimade -} - -const useFilterMainMenuStyles = makeStyles(theme => ({ - combine: { - } -})) - -/** - * Swipable menu that shows the available filters on the left side of the - * screen. - */ -const FilterMainMenu = React.memo(({ - open, - onOpenChange, - collapsed, - onCollapsedChange -}) => { - const [value, setValue] = React.useState() - const {filterMenus} = useSearchContext() - const [loaded, setLoaded] = useState(false) - const styles = useFilterMainMenuStyles() - - // Rendering the submenus is delayed on the event queue: this makes loading - // the search page more responsive by first loading everything else. - useEffect(() => { - delay(() => { setLoaded(true) }) - }, []) - - // The shown menu items - const menuItems = useMemo(() => { - return filterMenus?.options - ? Object.values(filterMenus.options).map(option => { - return <FilterMenuItem - key={option.key} - id={option.key} - label={option.label} - level={option.level} - disableButton={!has(menuMap, option.key)} - actions={option?.actions?.options && Object.values(option.actions.options) - .map((action) => { - const content = action.type === 'checkbox' - ? <InputCheckbox - key={action.key} - quantity={action.quantity} - description={action.tooltip} - label={action.label} - className={styles.combine} - ></InputCheckbox> - : null - return content - })} - /> - }) - : <Alert severity="warning"> - No search menus defined within this search context. Ensure that all GUI artifacts are created. - </Alert> - }, [filterMenus, styles]) - - // The shown submenus - const subMenus = useMemo(() => { - return filterMenus?.options - ? Object.values(filterMenus.options) - .filter(option => menuMap[option.key]) - .map(option => { - const Comp = menuMap[option.key] - return <Comp - key={option.key} - id={option.key} - label={option.label} - size={option.size} - /> - }) - : null - }, [filterMenus]) - - return <FilterMenu - selected={value} - onSelectedChange={setValue} - open={open} - onOpenChange={onOpenChange} - collapsed={collapsed} - onCollapsedChange={onCollapsedChange} - > - <FilterMenuItems> - {menuItems} - </FilterMenuItems> - <FilterSubMenus> - {loaded && subMenus} - </FilterSubMenus> - </FilterMenu> -}) -FilterMainMenu.propTypes = { - open: PropTypes.bool, - onOpenChange: PropTypes.func, - collapsed: PropTypes.bool, - onCollapsedChange: PropTypes.func -} - -export default FilterMainMenu diff --git a/gui/src/components/search/menus/FilterMenu.js b/gui/src/components/search/menus/FilterMenu.js deleted file mode 100644 index a53dd697f1f4021606eccc7d4d72d4925214b5ea..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterMenu.js +++ /dev/null @@ -1,646 +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, useCallback, useContext, useEffect } from 'react' -import PropTypes from 'prop-types' -import clsx from 'clsx' -import { makeStyles, useTheme } from '@material-ui/core/styles' -import { - List, - ListItem, - ListItemText, - ListItemIcon, - Divider, - Paper, - Menu, - Typography -} from '@material-ui/core' -import ArrowForwardIcon from '@material-ui/icons/ArrowForward' -import ArrowBackIcon from '@material-ui/icons/ArrowBack' -import NavigateNextIcon from '@material-ui/icons/NavigateNext' -import MoreVert from '@material-ui/icons/MoreVert' -import Scrollable from '../../visualization/Scrollable' -import FilterSettings from './FilterSettings' -import { Actions, ActionHeader, Action } from '../../Actions' -import { useSearchContext } from '../SearchContext' -import { pluralize } from '../../../utils' -import { isNil } from 'lodash' - -// The menu animations use a transition on the 'transform' property. Notice that -// animating 'transform' instead of e.g. the 'left' property is much more -// performant. We also hint the browser that the transform property will be -// animated using the 'will-change' property: this will pre-optimize the element -// for animation when possible (the recommendation is to remove/add it when -// needed, but in this case we keep it on constantly). -// -// The menu widths are hardcoded. We need the widths to perform the close -// animation. Another option would be to use useLayoutEffect to determine the -// sizes of the components dynamically, but this seems to be quite a bit less -// responsive compared to hardcoding the values. - -export const filterMenuContext = React.createContext() -const paddingHorizontal = 1.5 -export const collapsedMenuWidth = 3.3 - -// Topmost header for filter menus. Contains menu actions and an overline text -const useFilterMenuTopHeaderStyles = makeStyles(theme => { - return { - root: { - paddingTop: theme.spacing(1.2), - paddingRight: theme.spacing(paddingHorizontal), - paddingLeft: theme.spacing(paddingHorizontal) - }, - title: { - display: 'flex', - alignItems: 'center', - fontSize: '0.75rem', - marginTop: -3, - marginBottom: -3 - } - } -}) -export const FilterMenuTopHeader = React.memo(({ - title, - actions, - className -}) => { - const styles = useFilterMenuTopHeaderStyles() - return <div className={clsx(className, styles.root)}> - <Actions> - <ActionHeader> - <Typography className={styles.title} variant="overline">{title}</Typography> - </ActionHeader> - {actions} - </Actions> - </div> -}) -FilterMenuTopHeader.propTypes = { - overlineTitle: PropTypes.string, - title: PropTypes.string, - topAction: PropTypes.node, - actions: PropTypes.node, - className: PropTypes.string -} - -// Header for filter menus. Contains panel actions and an overline text. -const useFilterMenuHeaderStyles = makeStyles(theme => { - return { - root: { - height: theme.spacing(3), - paddingBottom: theme.spacing(1.1), - paddingTop: theme.spacing(0.3), - paddingLeft: theme.spacing(paddingHorizontal), - paddingRight: theme.spacing(paddingHorizontal), - display: 'flex', - flexDirection: 'column', - justifyContent: 'center' - }, - title: { - display: 'flex', - alignItems: 'center', - fontSize: '0.90rem' - } - } -}) -export const FilterMenuHeader = React.memo(({ - title, - actions, - className, - 'data-testid': testID -}) => { - const styles = useFilterMenuHeaderStyles() - return <div className={clsx(className, styles.root)} data-testid={testID}> - <Actions> - <ActionHeader> - <Typography className={styles.title} variant="button">{title}</Typography> - </ActionHeader> - {actions} - </Actions> - </div> -}) - -FilterMenuHeader.propTypes = { - overlineTitle: PropTypes.string, - title: PropTypes.string, - topAction: PropTypes.node, - actions: PropTypes.node, - className: PropTypes.string, - 'data-testid': PropTypes.string -} - -const useFilterMenuStyles = makeStyles(theme => { - const width = 22 - return { - root: { - boxSizing: 'border-box', - display: 'flex', - position: 'relative', - flexDirection: 'column', - width: `${width}rem`, - height: '100%', - '-webkit-transform': 'none', - transform: 'none', - transition: 'transform 250ms', - willChange: 'transform' - }, - collapsed: { - '-webkit-transform': `translateX(-${width - collapsedMenuWidth}rem)`, - transform: `translateX(-${width - collapsedMenuWidth}rem)` - } - } -}) - -export const FilterMenu = React.memo(({ - selected, - onSelectedChange, - open, - onOpenChange, - collapsed, - onCollapsedChange, - className, - children -}) => { - const styles = useFilterMenuStyles() - const [size, setSize] = useState('m') - - const handleChange = useCallback((newValue) => { - if (newValue !== selected) { - onOpenChange(true) - } else { - onOpenChange(old => !old) - } - onSelectedChange && onSelectedChange(newValue) - }, [selected, onSelectedChange, onOpenChange]) - - return <div className={clsx(className, styles.root, collapsed && styles.collapsed)}> - <filterMenuContext.Provider value={{ - selected: selected, - onChange: handleChange, - open: open, - onOpenChange: onOpenChange, - size: size, - onSizeChange: setSize, - collapsed: collapsed, - onCollapsedChange: onCollapsedChange - }}> - {children} - </filterMenuContext.Provider> - </div> -}) -FilterMenu.propTypes = { - selected: PropTypes.string, - onSelectedChange: PropTypes.func, - open: PropTypes.bool, - onOpenChange: PropTypes.func, - collapsed: PropTypes.bool, - onCollapsedChange: PropTypes.func, - className: PropTypes.string, - children: PropTypes.node -} - -const useFilterMenuItemsStyles = makeStyles(theme => { - return { - root: { - boxSizing: 'border-box', - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - backgroundColor: theme.palette.background.paper, - zIndex: 3 - }, - headerTextVertical: { - display: 'flex', - alignItems: 'center', - position: 'absolute', - right: '0.07rem', - top: '2.5rem', - height: `${collapsedMenuWidth}rem`, - transform: 'rotate(90deg)' - }, - menu: { - position: 'absolute', - right: 0, - top: 0, - bottom: 0, - left: 0, - boxSizing: 'border-box' - }, - menuBorder: { - boxShadow: `1px 0px 0px 0px ${theme.palette.action.selected}` - }, - padding: { - paddingBottom: `${theme.spacing(1.5)}px` - }, - button: { - marginRight: 0, - '-webkit-transform': 'none', - transform: 'none', - transition: 'transform 250ms', - willChange: 'transform' - }, - hidden: { - display: 'none' - }, - overflow: { - overflow: 'visible' - }, - list: { - paddingTop: 0 - }, - container: { - display: 'flex', - flexDirection: 'column', - height: '100%' - }, - content: { - flex: 1, - minHeight: 0 - }, - collapsedActions: { - paddingTop: theme.spacing(0.7), - paddingLeft: theme.spacing(paddingHorizontal), - paddingRight: theme.spacing(paddingHorizontal) - } - } -}) -export const FilterMenuItems = React.memo(({ - className, - children -}) => { - // const { useResetFilters, useRefresh, useApiData } = useSearchContext() - const styles = useFilterMenuItemsStyles() - const { open, onOpenChange, collapsed, onCollapsedChange } = useContext(filterMenuContext) - const [anchorEl, setAnchorEl] = React.useState(null) - const isSettingsOpen = Boolean(anchorEl) - - // Callbacks - const openMenu = useCallback((event) => { - setAnchorEl(event.currentTarget) - }, []) - const closeMenu = useCallback(() => { - setAnchorEl(null) - }, []) - - // Unfortunately the ClickAwayListener does not play nicely together with - // Menus/Select/Popper. When using Portals, the clicks are registered wrong. - // When Portals are disabled (disablePortal), their positioning goes haywire. - // The clicks outside are thus detected by individual event listeners that - // toggle the menu state. - return <div className={clsx(className, styles.root)}> - <div className={clsx(styles.menu, open && styles.menuBorder, collapsed && styles.hidden)}> - <div className={styles.container}> - <FilterMenuTopHeader/> - <FilterMenuHeader - title="Filters" - actions={<> - <Action - tooltip={'Hide filter menu'} - onClick={() => { - onCollapsedChange(old => !old) - onOpenChange(false) - }} - // className={styles.button} - > - <ArrowBackIcon fontSize="small"/> - </Action> - <Action - tooltip="Options" - onClick={openMenu} - > - <MoreVert fontSize="small"/> - </Action> - <Menu - anchorEl={anchorEl} - open={isSettingsOpen} - onClose={closeMenu} - getContentAnchorEl={null} - anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} - transformOrigin={{ vertical: 'top', horizontal: 'right' }} - keepMounted - > - <div> - <FilterSettings/> - </div> - </Menu> - </>} - /> - <div className={styles.content}> - <Scrollable> - <div className={styles.padding}> - <Divider/> - <List dense className={styles.list}> - {children} - </List> - </div> - </Scrollable> - </div> - </div> - </div> - <div className={clsx(!collapsed && styles.hidden)}> - <Actions className={styles.collapsedActions}> - {collapsed && <Action - tooltip={'Show filter menu'} - onClick={() => { - onCollapsedChange(false) - }} - className={styles.button} - > - <ArrowForwardIcon fontSize="small"/> - </Action>} - </Actions> - <Typography - className={clsx(styles.headerText, styles.headerTextVertical)} - variant="button" - >Filters - </Typography> - </div> - </div> -}) -FilterMenuItems.propTypes = { - className: PropTypes.string, - children: PropTypes.node -} - -const levelIndent = 1.8 -const leftGutter = 3.0 -const rightGutter = 2.35 -const itemHeight = 2.6 -const useFilterMenuItemStyles = makeStyles(theme => { - return { - root: { - minHeight: `${itemHeight}rem` - }, - label: { - textTransform: 'capitalize' - }, - listIcon: { - fontsize: '1rem', - minWidth: '1.5rem' - }, - arrow: { - marginLeft: theme.spacing(1), - fontSize: '1.5rem' - }, - gutters: { - paddingLeft: theme.spacing(leftGutter), - paddingRight: theme.spacing(rightGutter) - }, - listItem: { - position: 'relative', - height: `${itemHeight}rem` - }, - actions: { - display: 'flex', - flexDirection: 'column', - paddingLeft: theme.spacing(leftGutter), - paddingRight: theme.spacing(rightGutter) - }, - divider: { - width: '100%', - backgroundColor: theme.palette.grey[300] - } - } -}) -export const FilterMenuItem = React.memo(({ - id, - label, - onClick, - actions, - disableButton, - level -}) => { - const styles = useFilterMenuItemStyles() - const theme = useTheme() - const { selected, open, onChange } = useContext(filterMenuContext) - const handleClick = disableButton ? undefined : (onClick || onChange) - const opened = open && id === selected - - return <div className={styles.root}> - {(label || handleClick) && - <ListItem - button={!!handleClick} - className={styles.listItem} - classes={{gutters: styles.gutters}} - onClick={handleClick && (() => handleClick(id))} - > - {label && <ListItemText - style={{marginLeft: theme.spacing(level * levelIndent)}} - primaryTypographyProps={{ - color: opened ? 'primary' : 'initial', - className: styles.label - }} - primary={label} - data-testid={`menu-item-label-${id}`} - />} - {handleClick && <ListItemIcon className={styles.listIcon}> - <NavigateNextIcon color={opened ? 'primary' : 'action'} className={styles.arrow}/> - </ListItemIcon>} - </ListItem>} - {actions && <div - className={styles.actions} - style={{marginLeft: theme.spacing(level * levelIndent)}} - > - {actions} - </div>} - <Divider className={styles.divider}/> - </div> -}) - -FilterMenuItem.propTypes = { - id: PropTypes.string, - label: PropTypes.string, - onClick: PropTypes.func, - actions: PropTypes.node, - disableButton: PropTypes.bool, - level: PropTypes.number -} -FilterMenuItem.defaultProps = { - level: 0 -} - -const useFilterSubMenusStyles = makeStyles(theme => { - const widthS = 25 - const widthM = 32 - const widthL = 40 - const widthXL = 48 - return { - root: { - zIndex: 2, - display: 'flex', - flexDirection: 'column', - position: 'absolute', - right: 0, - top: 0, - bottom: 0, - width: `${widthXL}rem`, - backgroundColor: theme.palette.background.paper, - '-webkit-transform': 'none', - transform: 'none', - transition: 'transform 250ms', - flexGrow: 1, - boxSizing: 'border-box', - willChange: 'transform' - }, - collapsed: { - display: 'none' - }, - containerS: { - '-webkit-transform': `translateX(${widthS}rem)`, - transform: `translateX(${widthS}rem)` - }, - containerM: { - '-webkit-transform': `translateX(${widthM}rem)`, - transform: `translateX(${widthM}rem)` - }, - containerL: { - '-webkit-transform': `translateX(${widthL}rem)`, - transform: `translateX(${widthL}rem)` - }, - containerXL: { - '-webkit-transform': `translateX(${widthXL}rem)`, - transform: `translateX(${widthXL}rem)` - }, - menu: { - position: 'absolute', - right: 0, - top: 0, - bottom: 0, - boxSizing: 'border-box', - display: 'flex', - flexDirection: 'column' - }, - content: { - flex: 1, - minHeight: 0 - }, - menuS: { - width: `${widthS}rem` - }, - menuM: { - width: `${widthM}rem` - }, - menuL: { - width: `${widthL}rem` - }, - menuXL: { - width: `${widthXL}rem` - }, - button: { - marginRight: 0 - } - } -}) - -export const FilterSubMenus = React.memo(({ - children -}) => { - const { useResults } = useSearchContext() - const nResults = useResults()?.pagination?.total - const styles = useFilterSubMenusStyles() - const { open, onOpenChange, size, collapsed } = useContext(filterMenuContext) - const [menuStyle, containerStyle] = { - s: [styles.menuS, styles.containerS], - m: [styles.menuM, styles.containerM], - l: [styles.menuL, styles.containerL], - xl: [styles.menuXL, styles.containerXL] - }[size] - - return <Paper - elevation={4} - className={clsx(styles.root, open && containerStyle)} - > - <div className={clsx(styles.menu, menuStyle, collapsed && styles.collapsed)}> - <FilterMenuTopHeader - title={isNil(nResults) ? 'loading...' : pluralize('result', nResults, true)} - actions={<Action - tooltip="Close submenu" - onClick={() => { onOpenChange(false) }} - className={styles.button} - > - <ArrowBackIcon fontSize="small"/> - </Action>} - /> - <div className={styles.content}> - {children} - </div> - </div> - </Paper> -}) -FilterSubMenus.propTypes = { - sizes: PropTypes.object, - children: PropTypes.node -} - -const useFilterSubMenuStyles = makeStyles(theme => ({ - root: { - width: '100%', - height: '100%', - display: 'flex', - flexDirection: 'column' - }, - hidden: { - display: 'none' - }, - padding: { - paddingBottom: theme.spacing(1.5), - paddingLeft: theme.spacing(paddingHorizontal), - paddingRight: theme.spacing(paddingHorizontal) - }, - content: { - flex: 1, - minHeight: 0 - } -})) - -export const FilterSubMenu = React.memo(({ - id, - label, - size, - actions, - children -}) => { - const styles = useFilterSubMenuStyles() - const { selected, onSizeChange } = useContext(filterMenuContext) - const visible = id === selected - useEffect(() => { - if (visible) { - onSizeChange(size) - } - }, [size, visible, onSizeChange]) - - return <div className={clsx(styles.root, !visible && styles.hidden)}> - <FilterMenuHeader title={label} actions={actions} data-testid={`filter-menu-header-${id}`}/> - <div className={styles.content}> - <Scrollable> - {children} - </Scrollable> - </div> - </div> -}) -FilterSubMenu.propTypes = { - id: PropTypes.string, - label: PropTypes.string, - size: PropTypes.oneOf(['s', 'm', 'l', 'xl']), - actions: PropTypes.node, - children: PropTypes.node -} -FilterSubMenu.defaultProps = { - size: 's' -} - -export default FilterSubMenu diff --git a/gui/src/components/search/menus/FilterSettings.js b/gui/src/components/search/menus/FilterSettings.js deleted file mode 100644 index e3a14660a7c5648893da8e5ef361071d8ca41c59..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSettings.js +++ /dev/null @@ -1,80 +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, { useCallback } from 'react' -import { makeStyles } from '@material-ui/core/styles' -import { - Checkbox, - MenuItem, - FormControlLabel -} from '@material-ui/core' -import PropTypes from 'prop-types' -import clsx from 'clsx' -import { useSearchContext } from '../SearchContext' - -/** - * Menu showing the Filter settings. - */ -const useStyles = makeStyles((theme) => { - return { - root: { - }, - menuItem: { - width: '10rem' - }, - systems: { - margin: theme.spacing(2), - marginTop: theme.spacing(1) - } - } -}) -const FilterSettings = React.memo(({ - className, - classes, - 'data-testid': testID -}) => { - const styles = useStyles(classes) - const { - useIsStatisticsEnabled, - useSetIsStatisticsEnabled - } = useSearchContext() - const [isStatisticsEnabled, setIsStatisticsEnabled] = [useIsStatisticsEnabled(), useSetIsStatisticsEnabled()] - - const handleStatsChange = useCallback((event, value) => { - setIsStatisticsEnabled(value) - }, [setIsStatisticsEnabled]) - - return <div className={clsx(styles.root, className)} data-testid={testID}> - <MenuItem> - <FormControlLabel - control={<Checkbox - checked={isStatisticsEnabled} - onChange={handleStatsChange} - />} - label="Show advanced statistics" - /> - </MenuItem> - </div> -}) - -FilterSettings.propTypes = { - className: PropTypes.string, - classes: PropTypes.object, - 'data-testid': PropTypes.string -} - -export default FilterSettings diff --git a/gui/src/components/search/menus/FilterSubMenuAuthor.js b/gui/src/components/search/menus/FilterSubMenuAuthor.js deleted file mode 100644 index b8e174469147c31d790b3c763a4bc06329c8191b..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuAuthor.js +++ /dev/null @@ -1,75 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' -import InputRange from '../input/InputRange' - -const FilterSubMenuAuthor = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="authors.name" - visible={visible} - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="upload_create_time" - visible={visible} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="external_db" - visible={visible} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="datasets.dataset_name" - visible={visible} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="datasets.doi" - visible={visible} - disableStatistics - disableOptions - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuAuthor.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuAuthor diff --git a/gui/src/components/search/menus/FilterSubMenuBSE.js b/gui/src/components/search/menus/FilterSubMenuBSE.js deleted file mode 100644 index 64062c41fe446538c8a53e4f81f1b903f5b8b67e..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuBSE.js +++ /dev/null @@ -1,88 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' -import { InputCheckboxValue } from '../input/InputCheckbox' - -const FilterSubMenuBSE = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu - id={id} - actions={<InputCheckboxValue - quantity="results.method.method_name" - value="BSE" - description="Search BSE entries" - />} - {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.bse.type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.bse.solver" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.bse.starting_point_type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.bse.basis_set_type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.bse.gw_type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuBSE.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuBSE diff --git a/gui/src/components/search/menus/FilterSubMenuCatalystProperties.js b/gui/src/components/search/menus/FilterSubMenuCatalystProperties.js deleted file mode 100755 index 01ba2911f26985871d4f9f7e0d9faf2eec2470d7..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuCatalystProperties.js +++ /dev/null @@ -1,124 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputSection from '../input/InputSection' -import InputRange from '../input/InputRange' -import InputField from '../input/InputField' - -const FilterSubMenuCatalyst = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="results.properties.catalytic.reaction.name" - visible={visible} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputSection - section="results.properties.catalytic.reaction.reactants" - visible={visible} - > - <InputField - quantity="results.properties.catalytic.reaction.reactants.name" - visible={visible} - /> - <InputRange - quantity="results.properties.catalytic.reaction.reactants.conversion" - visible={visible} - /> - <InputRange - quantity="results.properties.catalytic.reaction.reactants.gas_concentration_in" - visible={visible} - /> - <InputRange - quantity="results.properties.catalytic.reaction.reactants.gas_concentration_out" - visible={visible} - /> - </InputSection> - </InputGridItem> - <InputGridItem xs={12}> - <InputSection - section="results.properties.catalytic.reaction.products" - visible={visible} - > - <InputField - quantity="results.properties.catalytic.reaction.products.name" - visible={visible} - /> - <InputRange - quantity="results.properties.catalytic.reaction.products.selectivity" - visible={visible} - /> - <InputRange - quantity="results.properties.catalytic.reaction.products.gas_concentration_out" - visible={visible} - /> - </InputSection> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="results.properties.catalytic.reaction.reaction_conditions.temperature" - visible={visible} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputSection - section="results.properties.catalytic.catalyst" - disableHeader - visible={visible} - > - <InputField - quantity="results.properties.catalytic.catalyst.catalyst_type" - visible={visible} - /> - <InputField - quantity="results.properties.catalytic.catalyst.preparation_method" - visible={visible} - /> - <InputField - quantity="results.properties.catalytic.catalyst.catalyst_name" - visible={visible} - /> - <InputField - quantity="results.properties.catalytic.catalyst.characterization_methods" - visible={visible} - /> - <InputRange - quantity="results.properties.catalytic.catalyst.surface_area" - visible={visible} - /> - </InputSection> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuCatalyst.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuCatalyst diff --git a/gui/src/components/search/menus/FilterSubMenuDFT.js b/gui/src/components/search/menus/FilterSubMenuDFT.js deleted file mode 100644 index 3b7626082bdc82440feb363dd5a481fbaddd4f47..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuDFT.js +++ /dev/null @@ -1,93 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' -import InputRange from '../input/InputRange' -import { InputCheckboxValue } from '../input/InputCheckbox' - -const FilterSubMenuDFT = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu - id={id} - actions={<InputCheckboxValue - quantity="results.method.method_name" - value="DFT" - description="Search DFT entries" - />} - {...rest} - > - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.dft.xc_functional_type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.dft.xc_functional_names" - visible={visible} - xs={12} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="results.method.simulation.dft.exact_exchange_mixing_factor" - visible={visible} - xs={12} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="results.method.simulation.dft.hubbard_kanamori_model.u_effective" - visible={visible} - xs={12} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.dft.core_electron_treatment" - visible={visible} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.dft.relativity_method" - visible={visible} - disableSearch - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuDFT.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuDFT diff --git a/gui/src/components/search/menus/FilterSubMenuDMFT.js b/gui/src/components/search/menus/FilterSubMenuDMFT.js deleted file mode 100644 index ca5e0bd46ed98ac569f2b43a3b52c6918adb5bb4..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuDMFT.js +++ /dev/null @@ -1,97 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' -import InputRange from '../input/InputRange' -import { InputCheckboxValue } from '../input/InputCheckbox' - -const FilterSubMenuDMFT = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu - id={id} - actions={<InputCheckboxValue - quantity="results.method.method_name" - value="DMFT" - description="Search DMFT entries" - />} - {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.dmft.impurity_solver_type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="results.method.simulation.dmft.inverse_temperature" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.dmft.magnetic_state" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="results.method.simulation.dmft.u" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="results.method.simulation.dmft.jh" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.dmft.analytical_continuation" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuDMFT.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuDMFT diff --git a/gui/src/components/search/menus/FilterSubMenuEELS.js b/gui/src/components/search/menus/FilterSubMenuEELS.js deleted file mode 100644 index 6339df01faf7aea58e71c1ec5e08f00b4989a704..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuEELS.js +++ /dev/null @@ -1,75 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' -import InputRange from '../input/InputRange' -import InputSection from '../input/InputSection' -import { InputCheckboxValue } from '../input/InputCheckbox' - -const FilterSubMenuEELS = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu - id={id} - actions={<InputCheckboxValue - quantity="results.method.method_name" - value="EELS" - description="Search EELS entries" - />} - {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputSection - section="results.properties.spectroscopic.spectra.provenance.eels" - disableHeader - visible={visible} - > - <InputField - quantity="results.properties.spectroscopic.spectra.provenance.eels.detector_type" - visible={visible} - xs={12} - /> - <InputRange - quantity="results.properties.spectroscopic.spectra.provenance.eels.resolution" - visible={visible} - /> - <InputRange - quantity="results.properties.spectroscopic.spectra.provenance.eels.min_energy" - visible={visible} - /> - <InputRange - quantity="results.properties.spectroscopic.spectra.provenance.eels.max_energy" - visible={visible} - /> - </InputSection> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuEELS.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuEELS diff --git a/gui/src/components/search/menus/FilterSubMenuELN.js b/gui/src/components/search/menus/FilterSubMenuELN.js deleted file mode 100644 index c5566bb136a9e206bc75348b8605dce4dbb00c35..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuELN.js +++ /dev/null @@ -1,105 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' -import { InputCheckboxValue } from '../input/InputCheckbox' - -const FilterSubMenuELN = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu - id={id} - actions={<InputCheckboxValue - quantity="quantities" - value="data" - description="Search ELN entries" - />} - {...rest} - > - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="results.eln.sections" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.eln.tags" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.eln.methods" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.eln.instruments" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.eln.names" - visible={visible} - disableStatistics - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.eln.descriptions" - visible={visible} - disableStatistics - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.eln.lab_ids" - visible={visible} - disableStatistics - disableOptions - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuELN.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuELN diff --git a/gui/src/components/search/menus/FilterSubMenuElectronic.js b/gui/src/components/search/menus/FilterSubMenuElectronic.js deleted file mode 100644 index 27beef3cab66ad81f21f166e64e8d24f0e3a07e0..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuElectronic.js +++ /dev/null @@ -1,89 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputSection from '../input/InputSection' -import InputRange from '../input/InputRange' -import InputField from '../input/InputField' - -const FilterSubMenuElectronic = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="electronic_properties" - visible={visible} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputSection - section="results.properties.electronic.band_structure_electronic.band_gap" - visible={visible} - > - <InputField - quantity="results.properties.electronic.band_structure_electronic.band_gap.type" - visible={visible} - disableSearch - /> - <InputRange - quantity="results.properties.electronic.band_structure_electronic.band_gap.value" - visible={visible} - /> - </InputSection> - </InputGridItem> - <InputGridItem xs={12}> - <InputSection - section="results.properties.electronic.band_structure_electronic" - visible={visible} - > - <InputField - quantity="results.properties.electronic.band_structure_electronic.spin_polarized" - visible={visible} - disableSearch - /> - </InputSection> - </InputGridItem> - <InputGridItem xs={12}> - <InputSection - section="results.properties.electronic.dos_electronic" - visible={visible} - > - <InputField - quantity="results.properties.electronic.dos_electronic.spin_polarized" - visible={visible} - disableSearch - /> - </InputSection> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuElectronic.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuElectronic diff --git a/gui/src/components/search/menus/FilterSubMenuElements.js b/gui/src/components/search/menus/FilterSubMenuElements.js deleted file mode 100644 index 2d2ead49014b1450427be828f49f1405566df427..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuElements.js +++ /dev/null @@ -1,96 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { makeStyles } from '@material-ui/core/styles' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputPeriodicTable from '../input/InputPeriodicTable' -import InputField from '../input/InputField' -import InputRange from '../input/InputRange' - -const useStyles = makeStyles(theme => ({ - grid: { - marginTop: theme.spacing(2) - }, - periodicTable: { - height: '30rem' - } -})) - -const FilterSubMenuElements = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - const styles = useStyles() - - return <FilterSubMenu id={id} {...rest}> - <InputGrid className={styles.grid}> - <InputGridItem xs={12}> - <InputPeriodicTable - quantity="results.material.elements" - visible={visible} - className={styles.periodicTable} - /> - </InputGridItem> - <InputGridItem xs={6}> - <InputField - quantity="results.material.chemical_formula_hill" - visible={visible} - disableOptions - /> - </InputGridItem> - <InputGridItem xs={6}> - <InputField - quantity="results.material.chemical_formula_iupac" - visible={visible} - disableOptions - /> - </InputGridItem> - <InputGridItem xs={6}> - <InputField - quantity="results.material.chemical_formula_reduced" - visible={visible} - disableOptions - /> - </InputGridItem> - <InputGridItem xs={6}> - <InputField - quantity="results.material.chemical_formula_anonymous" - visible={visible} - placeholder="E.g. A2B, A3B2" - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="results.material.n_elements" - visible={visible} - step={1} - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuElements.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuElements diff --git a/gui/src/components/search/menus/FilterSubMenuGW.js b/gui/src/components/search/menus/FilterSubMenuGW.js deleted file mode 100644 index 55aa2c526fccf0f1f68e7e947e42fafb7cdc0190..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuGW.js +++ /dev/null @@ -1,72 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' -import { InputCheckboxValue } from '../input/InputCheckbox' - -const FilterSubMenuGW = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu - id={id} - actions={<InputCheckboxValue - quantity="results.method.method_name" - value="GW" - description="Search GW entries" - />} - {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.gw.type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.gw.starting_point_type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.gw.basis_set_type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuGW.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuGW diff --git a/gui/src/components/search/menus/FilterSubMenuGeometryOptimization.js b/gui/src/components/search/menus/FilterSubMenuGeometryOptimization.js deleted file mode 100644 index daf0e75bcb0bcd8f6db8a97c8254ef0b2c5a5895..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuGeometryOptimization.js +++ /dev/null @@ -1,69 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import InputRange from '../input/InputRange' -import InputSection from '../input/InputSection' -import { InputCheckboxValue } from '../input/InputCheckbox' -import { InputGrid, InputGridItem } from '../input/InputGrid' - -const FilterSubMenuGeometryOptimization = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu - id={id} - actions={<InputCheckboxValue - quantity="results.properties.available_properties" - value="geometry_optimization" - description="Search entries with geometry optimization results" - />} - {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputSection - section="results.properties.geometry_optimization" - visible={visible} - disableHeader - > - <InputRange - quantity="results.properties.geometry_optimization.final_energy_difference" - visible={visible} - /> - <InputRange - quantity="results.properties.geometry_optimization.final_force_maximum" - visible={visible} - /> - <InputRange - quantity="results.properties.geometry_optimization.final_displacement_maximum" - visible={visible} - /> - </InputSection> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuGeometryOptimization.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuGeometryOptimization diff --git a/gui/src/components/search/menus/FilterSubMenuMaterial.js b/gui/src/components/search/menus/FilterSubMenuMaterial.js deleted file mode 100644 index 1ccc6fbef7b2c9362a4e0e97cf421526b6c25542..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuMaterial.js +++ /dev/null @@ -1,47 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' - -const FilterSubMenuMaterial = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="results.material.structural_type" - visible={visible} - disableSearch - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuMaterial.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuMaterial diff --git a/gui/src/components/search/menus/FilterSubMenuMechanical.js b/gui/src/components/search/menus/FilterSubMenuMechanical.js deleted file mode 100644 index c8dfdb231d8bcd9001197d4743a22d5e82a11010..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuMechanical.js +++ /dev/null @@ -1,93 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import InputRange from '../input/InputRange' -import InputField from '../input/InputField' -import InputSection from '../input/InputSection' -import { InputGrid, InputGridItem } from '../input/InputGrid' - -const FilterSubMenuMechanical = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="mechanical_properties" - visible={visible} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputSection - section="results.properties.mechanical.bulk_modulus" - visible={visible} - > - <InputField - quantity="results.properties.mechanical.bulk_modulus.type" - visible={visible} - initialSize={5} - /> - <InputRange - quantity="results.properties.mechanical.bulk_modulus.value" - visible={visible} - /> - </InputSection> - </InputGridItem> - <InputGridItem xs={12}> - <InputSection - section="results.properties.mechanical.shear_modulus" - visible={visible} - > - <InputField - quantity="results.properties.mechanical.shear_modulus.type" - visible={visible} - disableSearch - /> - <InputRange - quantity="results.properties.mechanical.shear_modulus.value" - visible={visible} - /> - </InputSection> - </InputGridItem> - <InputGridItem xs={12}> - <InputSection - section="results.properties.mechanical.energy_volume_curve" - visible={visible} - > - <InputField - quantity="results.properties.mechanical.energy_volume_curve.type" - visible={visible} - initialSize={5} - /> - </InputSection> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuMechanical.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuMechanical diff --git a/gui/src/components/search/menus/FilterSubMenuMethod.js b/gui/src/components/search/menus/FilterSubMenuMethod.js deleted file mode 100644 index 1b12b8c63619861f401ce1a96f48ce77c0049d37..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuMethod.js +++ /dev/null @@ -1,53 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' - -const FilterSubMenuMethod = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid spacing={2}> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.program_name" - visible={visible} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.program_version" - visible={visible} - disableOptions - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuMethod.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuMethod diff --git a/gui/src/components/search/menus/FilterSubMenuMolecularDynamics.js b/gui/src/components/search/menus/FilterSubMenuMolecularDynamics.js deleted file mode 100644 index fe73d0cf473db4bc756698b8d494562208b4c0ec..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuMolecularDynamics.js +++ /dev/null @@ -1,65 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import InputRange from '../input/InputRange' -import InputField from '../input/InputField' -import InputSection from '../input/InputSection' -import { InputGrid, InputGridItem } from '../input/InputGrid' - -const FilterSubMenuMolecularDynamics = React.memo(({ - id, - ...rest -}) => { - const {selected} = useContext(filterMenuContext) - const visible = id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputSection - section="results.properties.thermodynamic.trajectory" - disableHeader - visible={visible} - > - <InputField - quantity="results.properties.thermodynamic.trajectory.available_properties" - visible={visible} - disableSearch - formatLabels - /> - <InputField - quantity="results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type" - disableSearch - visible={visible} - /> - <InputRange - quantity="results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step" - visible={visible} - /> - </InputSection> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuMolecularDynamics.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuMolecularDynamics diff --git a/gui/src/components/search/menus/FilterSubMenuPrecision.js b/gui/src/components/search/menus/FilterSubMenuPrecision.js deleted file mode 100644 index bcafd9f3edc072fad11431de52b593855c917f8d..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuPrecision.js +++ /dev/null @@ -1,81 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' -import InputRange from '../input/InputRange' - -const FilterSubMenuPrecision = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputRange - quantity="results.method.simulation.precision.k_line_density" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.precision.native_tier" - visible={visible} - xs={12} - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.precision.basis_set" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="results.method.simulation.precision.planewave_cutoff" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputRange - quantity="results.method.simulation.precision.apw_cutoff" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuPrecision.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuPrecision diff --git a/gui/src/components/search/menus/FilterSubMenuSolarCell.js b/gui/src/components/search/menus/FilterSubMenuSolarCell.js deleted file mode 100644 index 7bf32fe34fd8627e54bc4ea24e163b62fa04be69..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuSolarCell.js +++ /dev/null @@ -1,99 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputRange from '../input/InputRange' -import InputField from '../input/InputField' - -const FilterSubMenuSolarCell = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputRange - quantity="results.properties.optoelectronic.solar_cell.efficiency" - visible={visible} - /> - <InputRange - quantity="results.properties.optoelectronic.solar_cell.fill_factor" - visible={visible} - /> - <InputRange - quantity="results.properties.optoelectronic.solar_cell.open_circuit_voltage" - visible={visible} - /> - <InputRange - quantity="results.properties.optoelectronic.solar_cell.short_circuit_current_density" - visible={visible} - /> - <InputRange - quantity="results.properties.optoelectronic.solar_cell.illumination_intensity" - visible={visible} - /> - <InputRange - quantity="results.properties.optoelectronic.solar_cell.device_area" - visible={visible} - /> - <InputField - quantity="results.properties.optoelectronic.solar_cell.device_architecture" - visible={visible} - /> - <InputField - quantity="results.properties.optoelectronic.solar_cell.device_stack" - visible={visible} - /> - <InputField - quantity="results.properties.optoelectronic.solar_cell.absorber" - visible={visible} - /> - <InputField - quantity="results.properties.optoelectronic.solar_cell.absorber_fabrication" - visible={visible} - /> - <InputField - quantity="results.properties.optoelectronic.solar_cell.electron_transport_layer" - visible={visible} - /> - <InputField - quantity="results.properties.optoelectronic.solar_cell.hole_transport_layer" - visible={visible} - /> - <InputField - quantity="results.properties.optoelectronic.solar_cell.substrate" - visible={visible} - /> - <InputField - quantity="results.properties.optoelectronic.solar_cell.back_contact" - visible={visible} - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuSolarCell.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuSolarCell diff --git a/gui/src/components/search/menus/FilterSubMenuStructure.js b/gui/src/components/search/menus/FilterSubMenuStructure.js deleted file mode 100644 index 37b3c0aa31fe4b396d415e60bc87f5313f7f599f..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuStructure.js +++ /dev/null @@ -1,104 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' - -const FilterSubMenuStructure = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="results.material.structural_type" - visible={visible} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.material.symmetry.bravais_lattice" - visible={visible} - xs={6} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.material.symmetry.crystal_system" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.material.symmetry.space_group_symbol" - visible={visible} - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.material.symmetry.structure_name" - visible={visible} - initialSize={5} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.material.symmetry.strukturbericht_designation" - visible={visible} - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.material.symmetry.point_group" - visible={visible} - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.material.symmetry.hall_symbol" - visible={visible} - disableOptions - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.material.symmetry.prototype_aflow_id" - visible={visible} - disableOptions - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuStructure.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuStructure diff --git a/gui/src/components/search/menus/FilterSubMenuTB.js b/gui/src/components/search/menus/FilterSubMenuTB.js deleted file mode 100644 index cd6b4aa936bcccf886bbee5e6cb591cadb0d9f98..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuTB.js +++ /dev/null @@ -1,64 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' -import { InputCheckboxValue } from '../input/InputCheckbox' - -const FilterSubMenuTB = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu - id={id} - actions={<InputCheckboxValue - quantity="results.method.method_name" - value="TB" - description="Search TB entries" - />} - {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.tb.type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - <InputGridItem xs={12}> - <InputField - quantity="results.method.simulation.tb.localization_type" - visible={visible} - xs={12} - disableSearch - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuTB.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuTB diff --git a/gui/src/components/search/menus/FilterSubMenuVibrational.js b/gui/src/components/search/menus/FilterSubMenuVibrational.js deleted file mode 100644 index c4b30868fbba4a6b1ba4dd942519521517ceec68..0000000000000000000000000000000000000000 --- a/gui/src/components/search/menus/FilterSubMenuVibrational.js +++ /dev/null @@ -1,47 +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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FilterSubMenu, filterMenuContext } from './FilterMenu' -import { InputGrid, InputGridItem } from '../input/InputGrid' -import InputField from '../input/InputField' - -const FilterSubMenuElectronic = React.memo(({ - id, - ...rest -}) => { - const {selected, open} = useContext(filterMenuContext) - const visible = open && id === selected - - return <FilterSubMenu id={id} {...rest}> - <InputGrid> - <InputGridItem xs={12}> - <InputField - quantity="vibrational_properties" - visible={visible} - disableSearch - /> - </InputGridItem> - </InputGrid> - </FilterSubMenu> -}) -FilterSubMenuElectronic.propTypes = { - id: PropTypes.string -} - -export default FilterSubMenuElectronic diff --git a/gui/src/components/search/menus/Menu.js b/gui/src/components/search/menus/Menu.js new file mode 100644 index 0000000000000000000000000000000000000000..9b133914d583c083d24c9b94f32a793daeb933d2 --- /dev/null +++ b/gui/src/components/search/menus/Menu.js @@ -0,0 +1,489 @@ +/* + * 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, useEffect, isValidElement, Children, useCallback, useContext } from 'react' +import PropTypes from 'prop-types' +import clsx from 'clsx' +import { makeStyles, useTheme } from '@material-ui/core/styles' +import { + List, + ListItem, + ListItemText, + ListItemIcon, + Paper, + Typography, + Checkbox, + MenuItem as MenuItemMUI, + FormControlLabel +} from '@material-ui/core' +import { delay } from '../../../utils' +import NavigateNextIcon from '@material-ui/icons/NavigateNext' +import ArrowForwardIcon from '@material-ui/icons/ArrowForward' +import Scrollable from '../../visualization/Scrollable' +import { Action, Actions, ActionHeader } from '../../Actions' +import FilterTitle from '../FilterTitle' +import { useSearchContext } from '../SearchContext' + +// The menu animations use a transition on the 'transform' property. Notice that +// animating 'transform' instead of e.g. the 'left' property is much more +// performant. We also hint the browser that the transform property will be +// animated using the 'will-change' property: this will pre-optimize the element +// for animation when possible (the recommendation is to remove/add it when +// needed, but in this case we keep it on constantly). +// +// The menu widths are hardcoded. We need the widths to perform the close +// animation. Another option would be to use useLayoutEffect to determine the +// sizes of the components dynamically, but this seems to be quite a bit less +// responsive compared to hardcoding the values. + +export const menuContext = React.createContext() +export const paddingHorizontal = 1.5 +const collapseWidth = '3rem' + +/** + * Provides a context for a menu. + */ +const useMenuStyles = (props) => makeStyles(theme => { + return { + root: { + boxSizing: 'border-box', + display: 'flex', + height: '100%', + flexDirection: 'column', + '-webkit-transform': `translateX(-${props.width})`, + 'transform': `translateX(-${props.width})`, + width: `${props.width}`, + transition: `transform 225ms`, + willChange: 'transform', + visibility: 'hidden' + }, + menu: { + position: 'absolute', + right: 0, + top: 0, + bottom: 0, + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'column', + marginTop: '1px', + width: `${props.width}` + }, + open: { + '-webkit-transform': 'none', + transform: 'none' + }, + container: { + height: '100%' + }, + containerCollapsed: { + width: collapseWidth + }, + collapsed: { + '-webkit-transform': `translateX(calc(-${props.width} + ${collapseWidth}))`, + 'transform': `translateX(calc(-${props.width} + 50px))` + }, + visible: { + visibility: 'visible' + } + } +}) +export const Menu = React.memo(({ + size, + open, + collapsed, + onCollapsedChanged, + subMenuOpen, + visible, + onOpenChange, + selected, + onSelectedChange, + className, + children +}) => { + const width = { + 'xs': '17rem', + 'sm': '21rem', + 'md': '25rem', + 'lg': '29rem', + 'xl': '33rem', + 'xxl': '45rem' + }[size] || size || '21rem' + const styles = useMenuStyles({width})() + + return <div className={clsx(styles.container, collapsed && styles.containerCollapsed)}> + <Paper + elevation={open ? 4 : 0} + className={clsx(className, styles.root, open && styles.open, collapsed && styles.collapsed, visible && styles.visible)} + > + <menuContext.Provider value={{ + collapsed, + selected, + setSelected: onSelectedChange, + setCollapsed: onCollapsedChanged, + subMenuOpen, + open, + setOpen: onOpenChange, + size + }}> + {children} + </menuContext.Provider> + </Paper> + </div> +}) +Menu.propTypes = { + size: PropTypes.string, + open: PropTypes.bool, + collapsed: PropTypes.bool, + onCollapsedChanged: PropTypes.func, + subMenuOpen: PropTypes.bool, + visible: PropTypes.bool, + onOpenChange: PropTypes.func, + selected: PropTypes.number, + onSelectedChange: PropTypes.func, + className: PropTypes.string, + children: PropTypes.node +} + +/** + * Header for filter menus. Contains panel actions and an overline text. + */ +const useMenuHeaderStyles = makeStyles(theme => { + return { + root: { + height: theme.spacing(3), + paddingBottom: theme.spacing(1.2), + paddingTop: theme.spacing(1.4), + paddingLeft: theme.spacing(paddingHorizontal), + paddingRight: theme.spacing(paddingHorizontal), + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + zIndex: 4, + backgroundColor: theme.palette.background.paper, + boxShadow: `1px 0px 0px 0px #e0e0e0` + }, + title: { + display: 'flex', + alignItems: 'center', + fontSize: '0.90rem' + } + } +}) +export const MenuHeader = React.memo(({ + title, + actions, + className, + 'data-testid': testID +}) => { + const styles = useMenuHeaderStyles() + const { collapsed, setCollapsed } = useContext(menuContext) + + return <div className={clsx(className, styles.root)} data-testid={testID}> + <Actions> + <ActionHeader> + <Typography className={styles.title} variant="button">{title}</Typography> + </ActionHeader> + {collapsed + ? <Action + tooltip={'Show menu'} + onClick={() => setCollapsed(false)} + > + <ArrowForwardIcon fontSize="small"/> + </Action> + : actions + } + </Actions> + </div> +}) + +MenuHeader.propTypes = { + overlineTitle: PropTypes.string, + title: PropTypes.string, + topAction: PropTypes.node, + actions: PropTypes.node, + className: PropTypes.string, + 'data-testid': PropTypes.string +} + +/** + * Menu content that is wrapped in a customized scrollable area. + */ +const useMenuContentStyles = makeStyles(theme => { + return { + root: { + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + position: 'relative', + backgroundColor: theme.palette.background.paper, + zIndex: 3 + }, + headerTextVertical: { + display: 'flex', + alignItems: 'center', + position: 'absolute', + right: '0.07rem', + top: '2.5rem', + transform: 'rotate(90deg)' + }, + menu: { + position: 'absolute', + right: 0, + top: 0, + bottom: 0, + left: 0, + boxSizing: 'border-box' + }, + menuBorder: { + boxShadow: `1px 0px 0px 0px #e0e0e0` + }, + button: { + marginRight: 0, + '-webkit-transform': 'none', + transform: 'none', + transition: 'transform 250ms', + willChange: 'transform' + }, + overflow: { + overflow: 'visible' + }, + list: { + paddingTop: 0 + }, + container: { + display: 'flex', + flexDirection: 'column', + height: '100%' + }, + content: { + flex: 1, + minHeight: 0 + }, + hidden: { + visibility: 'hidden' + }, + collapsedText: { + display: 'flex', + alignItems: 'center', + position: 'absolute', + right: '0.07rem', + top: 0, + height: collapseWidth, + transform: 'rotate(90deg)' + } + + } +}) +export const MenuContent = React.memo(({ + className, + children +}) => { + const styles = useMenuContentStyles() + const { open, collapsed } = useContext(menuContext) + // Unfortunately the ClickAwayListener does not play nicely together with + // Menus/Select/Popper. When using Portals, the clicks are registered wrong. + // When Portals are disabled (disablePortal), their positioning goes haywire. + // The clicks outside are thus detected by individual event listeners that + // toggle the menu state. + return <div className={clsx(className, styles.root)}> + <div className={clsx(styles.menu, open && styles.menuBorder)}> + <div className={styles.container}> + <div className={clsx(styles.content, collapsed && styles.hidden)}> + <Scrollable> + <List dense className={styles.list}> + {children} + </List> + </Scrollable> + </div> + {collapsed && <Typography + className={clsx(styles.collapsedText)} + variant="button" + >Filters + </Typography> + } + </div> + </div> + </div> +}) +MenuContent.propTypes = { + className: PropTypes.string, + children: PropTypes.node +} + +/** + * Menu submenus. Ensures that submenus are correctly placed underneath the + * menu and their loading is delayed. + */ +const useMenuSubMenusStyles = makeStyles(theme => { + return { + root: { + position: 'absolute', + top: '0', + bottom: '0', + left: '100%' + }, + relative: { + position: 'relative', + width: '100%', + height: '100%' + }, + absolute: { + position: 'absolute', + top: 0, + bottom: 0, + right: 0, + left: 0 + } + } +}) +export const MenuSubMenus = React.memo(({ + className, + children +}) => { + const styles = useMenuSubMenusStyles() + const [loaded, setLoaded] = useState(false) + + // Rendering the submenus is delayed on the event queue: this makes loading + // the search page more responsive by first loading everything else. + useEffect(() => { + delay(() => { setLoaded(true) }) + }, []) + + return loaded + ? <div className={clsx(className, styles.root)}> + <div className={styles.relatives}> + {Children.map(children, (child) => { + if (isValidElement(child)) { + return ( + <div className={styles.absolute}> + <child.type {...child.props}/> + </div> + ) + } + })} + </div> + </div> + : null +}) +MenuSubMenus.propTypes = { + className: PropTypes.string, + children: PropTypes.node +} + +/** + * Menu item. + */ +const levelIndent = 1.8 +const useMenuItemStyles = makeStyles(theme => { + return { + root: { + }, + listIcon: { + fontsize: '1rem', + minWidth: '1.5rem' + }, + arrow: { + marginLeft: theme.spacing(1), + fontSize: '1.5rem' + }, + listItem: { + paddingTop: theme.spacing(0.75), + paddingBottom: theme.spacing(0.75), + position: 'relative' + }, + actions: { + display: 'flex', + flexDirection: 'column' + } + } +}) +export const MenuItem = React.memo(({ + id, + title, + disableButton, + level +}) => { + const styles = useMenuItemStyles() + const theme = useTheme() + const { selected, setSelected, subMenuOpen } = useContext(menuContext) + const opened = subMenuOpen && id === selected + const handleClick = useCallback(() => { + if (disableButton) return + setSelected?.(id) + }, [disableButton, id, setSelected]) + + return <div className={styles.root}> + {(title || !disableButton) && + <ListItem + button={!disableButton} + className={styles.listItem} + onClick={handleClick} + > + {title && <ListItemText + style={{marginLeft: theme.spacing(level * levelIndent)}} + primaryTypographyProps={{ + color: opened ? 'primary' : 'initial' + }} + primary={<FilterTitle label={title}/>} + data-testid={`menu-item-label-${id}`} + />} + {!disableButton && <ListItemIcon className={styles.listIcon}> + <NavigateNextIcon color={opened ? 'primary' : 'action'} className={styles.arrow}/> + </ListItemIcon>} + </ListItem>} + </div> +}) + +MenuItem.propTypes = { + id: PropTypes.number, + title: PropTypes.string, + disableButton: PropTypes.bool, + level: PropTypes.number +} +MenuItem.defaultProps = { + level: 0 +} + +/** + * Settings for a menu. + */ +export const MenuSettings = React.memo(() => { + const { + useIsStatisticsEnabled, + useSetIsStatisticsEnabled + } = useSearchContext() + const [isStatisticsEnabled, setIsStatisticsEnabled] = [ + useIsStatisticsEnabled(), + useSetIsStatisticsEnabled() + ] + + const handleStatsChange = useCallback((event, value) => { + setIsStatisticsEnabled(value) + }, [setIsStatisticsEnabled]) + + return <MenuItemMUI> + <FormControlLabel + control={<Checkbox + checked={isStatisticsEnabled} + onChange={handleStatsChange} + />} + label="Show advanced statistics" + /> + </MenuItemMUI> +}) diff --git a/gui/src/components/search/widgets/Dashboard.js b/gui/src/components/search/widgets/Dashboard.js index 7a6f526e67652a1875d519e71a5a955cf6b468ca..7350853bc63f5265376096f0989c7da329aad523 100644 --- a/gui/src/components/search/widgets/Dashboard.js +++ b/gui/src/components/search/widgets/Dashboard.js @@ -107,11 +107,9 @@ const Dashboard = React.memo(() => { xl: {...layout}, xxl: {...layout} }, - // x: {scale: 'linear'}, - // y: {scale: 'linear'}, size: 1000, autorange: true, - type: 'scatterplot' + type: 'scatter_plot' } addWidget(id, value) }, [addWidget]) @@ -133,8 +131,8 @@ const Dashboard = React.memo(() => { xxl: {...layout} }, scale: 'linear', - quantity: 'results.material.elements', - type: 'periodictable' + search_quantity: 'results.material.elements', + type: 'periodic_table' } addWidget(id, value) }, [addWidget]) @@ -255,8 +253,10 @@ const Dashboard = React.memo(() => { {widgets && Object.entries(widgets).map(([id, value]) => { if (!value.editing) return null const comp = { - scatterplot: <WidgetScatterPlotEdit key={id} widget={value}/>, - periodictable: <WidgetPeriodicTableEdit key={id} {...value}/>, + scatter_plot: <WidgetScatterPlotEdit key={id} widget={value}/>, + scatterplot: <WidgetScatterPlotEdit key={id} widget={value}/>, // Deprecated misspelling + periodic_table: <WidgetPeriodicTableEdit key={id} {...value}/>, + periodictable: <WidgetPeriodicTableEdit key={id} {...value}/>, // Deprecated misspelling histogram: <WidgetHistogramEdit key={id} widget={value}/>, terms: <WidgetTermsEdit key={id} {...value}/> }[value.type] @@ -283,8 +283,10 @@ DashboardAction.propTypes = { } const schemas = { - scatterplot: schemaWidgetScatterPlot, - periodictable: schemaWidgetPeriodicTable, + scatterplot: schemaWidgetScatterPlot, // Deprecated misspelling + scatter_plot: schemaWidgetScatterPlot, + periodictable: schemaWidgetPeriodicTable, // Deprecated misspelling + periodic_table: schemaWidgetPeriodicTable, histogram: schemaWidgetHistogram, terms: schemaWidgetTerms } diff --git a/gui/src/components/search/widgets/Dashboard.spec.js b/gui/src/components/search/widgets/Dashboard.spec.js index 51e32cdec9a73a3b4d983396c1c11df2213f9d67..bf71438e7aaa51369c94efd074d0c5a0c7506936 100644 --- a/gui/src/components/search/widgets/Dashboard.spec.js +++ b/gui/src/components/search/widgets/Dashboard.spec.js @@ -46,7 +46,7 @@ describe('displaying an initial widget and removing it', () => { 'terms', { type: 'terms', - quantity: 'results.material.structural_type', + search_quantity: 'results.material.structural_type', scale: 'linear', editing: false, visible: true, @@ -70,7 +70,7 @@ describe('displaying an initial widget and removing it', () => { { type: 'histogram', title: 'Test title', - x: {quantity: 'results.material.n_elements'}, + x: {search_quantity: 'results.material.n_elements'}, y: {scale: 'linear'}, editing: false, visible: true, @@ -85,13 +85,13 @@ describe('displaying an initial widget and removing it', () => { async (widget, loaded) => await expectInputRange(widget, loaded, true, true) ], [ - 'scatterplot', + 'scatter_plot', { - type: 'scatterplot', + type: 'scatter_plot', title: 'Test title', - x: {quantity: 'results.properties.optoelectronic.solar_cell.open_circuit_voltage'}, - y: {quantity: 'results.properties.optoelectronic.solar_cell.efficiency'}, - markers: {color: {quantity: 'results.properties.optoelectronic.solar_cell.short_circuit_current_density'}}, + x: {search_quantity: 'results.properties.optoelectronic.solar_cell.open_circuit_voltage'}, + y: {search_quantity: 'results.properties.optoelectronic.solar_cell.efficiency'}, + markers: {color: {search_quantity: 'results.properties.optoelectronic.solar_cell.short_circuit_current_density'}}, size: 1000, autorange: true, editing: false, @@ -107,10 +107,10 @@ describe('displaying an initial widget and removing it', () => { async (widget, loaded) => await expectWidgetScatterPlot(widget, loaded) ], [ - 'periodictable', + 'periodic_table', { - type: 'periodictable', - quantity: 'results.material.elements', + type: 'periodic_table', + search_quantity: 'results.material.elements', scale: 'linear', editing: false, visible: true, @@ -123,7 +123,7 @@ describe('displaying an initial widget and removing it', () => { } }, async (widget, loaded) => { - await expectInputHeader(widget.quantity) + await expectInputHeader(widget.search_quantity) // TODO: For some reason this test works out fine locally, but always // fails in the CI. Could not be resolved even by making the tests wait // much longer. @@ -148,7 +148,7 @@ describe('displaying an initial widget and removing it', () => { // Remove widget, check that it is gone. A test id is used to fetch the // remove button since it is an icon that may appear in several locations. const removeButton = screen.getByTestId(`0-remove-widget`) - const label = widget.title || defaultFilterData[widget.quantity].label + const label = widget.title || defaultFilterData[widget.search_quantity].label expect(screen.queryByText(label, {exact: false})).toBeInTheDocument() await userEvent.click(removeButton) expect(screen.queryByText(label, {exact: false})).not.toBeInTheDocument() diff --git a/gui/src/components/search/widgets/Widget.js b/gui/src/components/search/widgets/Widget.js index a6be7bb7050e61a385dea382367c4c96d9831c5d..a12454779a027435611c9b91717c28007a251a27 100644 --- a/gui/src/components/search/widgets/Widget.js +++ b/gui/src/components/search/widgets/Widget.js @@ -114,13 +114,13 @@ export const schemaAxisBase = object({ scale: string().nullable() }) export const schemaAxis = object({ - quantity: string().required(), + search_quantity: string().required(), unit: string().nullable(), title: string().nullable(), scale: string().nullable() }) export const schemaAxisOptional = object({ - quantity: string().nullable(), + search_quantity: string().nullable(), unit: string().nullable(), title: string().nullable(), scale: string().nullable() diff --git a/gui/src/components/search/widgets/WidgetGrid.js b/gui/src/components/search/widgets/WidgetGrid.js index 0966315a39b7e7c9d5c690d396097d194a0c3a25..9a375334f1cf3ebe3b8cbab75d58fc915860ff8c 100644 --- a/gui/src/components/search/widgets/WidgetGrid.js +++ b/gui/src/components/search/widgets/WidgetGrid.js @@ -294,8 +294,10 @@ const WidgetGrid = React.memo(({ .sort((a, b) => b[1].index - a[1].index) .map(([id, value]) => { const Comp = { - scatterplot: WidgetScatterPlot, - periodictable: WidgetPeriodicTable, + scatter_plot: WidgetScatterPlot, + scatterplot: WidgetScatterPlot, // Deprecated misspelling + periodic_table: WidgetPeriodicTable, + periodictable: WidgetPeriodicTable, // Deprecated misspelling histogram: WidgetHistogram, terms: WidgetTerms }[value.type] diff --git a/gui/src/components/search/widgets/WidgetHeader.js b/gui/src/components/search/widgets/WidgetHeader.js index 66958a682a17101720d152046bef901a690e1482..aa298739595f9ad96b0dc4ab2bee6b9adf35dc5e 100644 --- a/gui/src/components/search/widgets/WidgetHeader.js +++ b/gui/src/components/search/widgets/WidgetHeader.js @@ -90,6 +90,7 @@ const WidgetHeader = React.memo(({ onMouseUp={handleMouseUp} > <FilterTitle + variant="subtitle2" quantity={quantity} label={label} description={description} diff --git a/gui/src/components/search/widgets/WidgetHistogram.js b/gui/src/components/search/widgets/WidgetHistogram.js index 1f54766891b8247b4e74873815ad203135394059..bf610084697553724807d553408875342d984a30 100644 --- a/gui/src/components/search/widgets/WidgetHistogram.js +++ b/gui/src/components/search/widgets/WidgetHistogram.js @@ -20,10 +20,8 @@ import PropTypes from 'prop-types' import { useSearchContext } from '../SearchContext' import { Widget } from './Widget' import { ActionCheckbox, ActionSelect } from '../../Actions' -import { Range } from '../input/InputRange' -import { scales } from '../../plotting/common' -import {getDisplayLabel} from '../../../utils' -import { Unit } from '../../units/Unit' +import { Histogram } from '../input/InputHistogram' +import { getAxisConfig, scales } from '../../plotting/common' import { useUnitContext } from '../../units/UnitContext' /** @@ -39,7 +37,7 @@ export const WidgetHistogram = React.memo(( y, nbins, autorange, - showinput, + show_input, className }) => { const { filterData, useSetWidget } = useSearchContext() @@ -47,21 +45,7 @@ export const WidgetHistogram = React.memo(( const setWidget = useSetWidget(id) // Create final axis config for the plot - const xAxis = useMemo(() => { - const xFilter = filterData[x.quantity] - const xTitle = x.title || getDisplayLabel(xFilter) - const xType = xFilter?.dtype - const xUnit = x.unit - ? new Unit(x.unit) - : new Unit(xFilter.unit || 'dimensionless').toSystem(units) - - return { - ...x, - title: xTitle, - unit: xUnit, - dtype: xType - } - }, [filterData, x, units]) + const xAxis = useMemo(() => getAxisConfig(x, filterData, units), [x, filterData, units]) const handleEdit = useCallback(() => { setWidget(old => { return {...old, editing: true } }) @@ -92,14 +76,15 @@ export const WidgetHistogram = React.memo(( /> </>} > - <Range - xAxis={xAxis} - yAxis={y} + <Histogram + x={xAxis} + y={y} visible={true} nBins={nbins} anchored={true} autorange={autorange} - showinput={showinput} + showInput={show_input} + showStatistics={true} aggId={id} /> </Widget> @@ -113,6 +98,6 @@ WidgetHistogram.propTypes = { y: PropTypes.object, nbins: PropTypes.number, autorange: PropTypes.bool, - showinput: PropTypes.bool, + show_input: PropTypes.bool, className: PropTypes.string } diff --git a/gui/src/components/search/widgets/WidgetHistogram.spec.js b/gui/src/components/search/widgets/WidgetHistogram.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..a5a405dd4c9726709dd3a324e1b3e56b7b04bb2b --- /dev/null +++ b/gui/src/components/search/widgets/WidgetHistogram.spec.js @@ -0,0 +1,61 @@ +/* + * 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 { renderSearchEntry, expectWidgetHistogram } from '../conftest.spec' +import { WidgetHistogram } from './WidgetHistogram' + +// Mock the resize observer that defines the widget size +jest.mock('react-resize-detector', () => { + return {useResizeDetector: () => { + return {height: 400, width: 400, ref: undefined} + }} +}) + +describe.only('test custom axis titles', () => { + test.each([ + ['no unit', {x: {title: 'My Title', search_quantity: 'results.material.n_elements'}}, 'My Title'], + ['with unit', {x: {title: 'My Title', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'My Title (eV)'] + ])('%s', async (name, config, title) => { + const configFinal = { + id: '0', + y: {scale: 'linear'}, + ...config + } + renderSearchEntry(<WidgetHistogram {...configFinal} />) + await expectWidgetHistogram(configFinal) + }) +}) + +describe('test custom axis units', () => { + test.each([ + ['x', {x: {unit: 'Ha', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'Final energy difference (Ha)'], + ['y', {y: {unit: 'Ha', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'Final energy difference (Ha)'], + ['color', {markers: {color: {unit: 'Ha', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}}, 'Final energy difference (Ha)'] + ])('%s', async (name, config, title) => { + const configFinal = { + id: '0', + scale: 'linear', + x: {search_quantity: 'results.material.n_elements'}, + y: {search_quantity: 'results.material.n_elements'}, + markers: {color: {search_quantity: 'results.material.n_elements'}}, + ...config + } + renderSearchEntry(<WidgetHistogram {...configFinal} />) + await expectWidgetHistogram(configFinal) + }) +}) diff --git a/gui/src/components/search/widgets/WidgetHistogramEdit.js b/gui/src/components/search/widgets/WidgetHistogramEdit.js index 5b1bbe1f5d47f5eb46d2fcd1c4e6f5e581e804da..d70f3e4e5899a463976e531fd60e389794e344ad 100644 --- a/gui/src/components/search/widgets/WidgetHistogramEdit.js +++ b/gui/src/components/search/widgets/WidgetHistogramEdit.js @@ -95,9 +95,9 @@ export const WidgetHistogramEdit = React.memo(({widget}) => { // Check for missing values. This check is required because there is no // value set when a new widget is created, and pressing the done button // without filling a value should raise an error. - const xEmpty = isEmptyString(settings?.x?.quantity) + const xEmpty = isEmptyString(settings?.x?.search_quantity) if (xEmpty) { - handleErrorQuantity('x.quantity', 'Please specify a value.') + handleErrorQuantity('x.search_quantity', 'Please specify a value.') } if (!independentErrors && !xEmpty) { @@ -116,20 +116,20 @@ export const WidgetHistogramEdit = React.memo(({widget}) => { <WidgetEditGroup title="x axis"> <WidgetEditOption> <InputMetainfo - label="quantity" - value={settings.x?.quantity} - error={errors['x.quantity']} - onChange={(value) => handleChange('x.quantity', value)} - onAccept={(value) => handleAcceptQuantity('x.quantity', value)} - onSelect={(value) => handleAcceptQuantity('x.quantity', value)} - onError={(value) => handleErrorQuantity('x.quantity', value)} + label="Search quantity" + value={settings.x?.search_quantity} + error={errors['x.search_quantity']} + onChange={(value) => handleChange('x.search_quantity', value)} + onAccept={(value) => handleAcceptQuantity('x.search_quantity', value)} + onSelect={(value) => handleAcceptQuantity('x.search_quantity', value)} + onError={(value) => handleErrorQuantity('x.search_quantity', value)} dtypes={dtypes} dtypesRepeatable={dtypes} /> </WidgetEditOption> <WidgetEditOption> <InputTextField - label="title" + label="Title" fullWidth value={settings.x?.title} onChange={(event) => handleChange('x.title', event.target.value)} @@ -137,14 +137,14 @@ export const WidgetHistogramEdit = React.memo(({widget}) => { </WidgetEditOption> <WidgetEditOption> <UnitInput - label='unit' + label='Unit' value={settings.x?.unit} onChange={(value) => handleChange('x.unit', value)} onSelect={(value) => handleAccept('x.unit', value)} onAccept={(value) => handleAccept('x.unit', value)} error={errors['x.unit']} onError={(value) => handleError('x.unit', value)} - dimension={dimensions['x.quantity'] || null} + dimension={dimensions['x.search_quantity'] || null} optional disableGroup /> @@ -155,7 +155,7 @@ export const WidgetHistogramEdit = React.memo(({widget}) => { <TextField select fullWidth - label="scale" + label="Scale" variant="filled" value={settings.y?.scale} onChange={(event) => { handleChange('y.scale', event.target.value) }} @@ -169,7 +169,7 @@ export const WidgetHistogramEdit = React.memo(({widget}) => { <WidgetEditGroup title="general"> <WidgetEditOption> <InputTextField - label="title" + label="Title" fullWidth value={settings?.title} onChange={(event) => handleChange('title', event.target.value)} @@ -200,7 +200,7 @@ export const WidgetHistogramEdit = React.memo(({widget}) => { </WidgetEditOption> <WidgetEditOption> <FormControlLabel - control={<Checkbox checked={settings.showinput} onChange={(event, value) => handleChange('showinput', value)}/>} + control={<Checkbox checked={settings.show_input} onChange={(event, value) => handleChange('show_input', value)}/>} label='Show input fields' /> </WidgetEditOption> @@ -218,5 +218,5 @@ export const schemaWidgetHistogram = schemaWidget.shape({ y: schemaAxisBase.required('Y-axis configuration is required.'), nbins: number().integer().required(), autorange: bool(), - showinput: bool() + show_input: bool() }) diff --git a/gui/src/components/search/widgets/WidgetHistogramEdit.spec.js b/gui/src/components/search/widgets/WidgetHistogramEdit.spec.js index c449b450099ae759c9062ac1adc5b3a41900a667..f32cd626e5bb8723cb4929f566d6437f2c64cfb1 100644 --- a/gui/src/components/search/widgets/WidgetHistogramEdit.spec.js +++ b/gui/src/components/search/widgets/WidgetHistogramEdit.spec.js @@ -24,9 +24,9 @@ import { WidgetHistogramEdit } from './WidgetHistogramEdit' describe('test edit dialog error messages', () => { test.each([ ['missing x', {x: {}}, 'Please specify a value.'], - ['unavailable x', {x: {quantity: 'results.material.not_a_quantity'}}, 'The quantity "results.material.not_a_quantity" is not available.'], - ['invalid x unit', {x: {quantity: 'results.material.topology.cell.a', unit: 'nounit'}}, 'Unit "nounit" not found.'], - ['incompatible x unit', {x: {quantity: 'results.material.topology.cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length".'] + ['unavailable x', {x: {search_quantity: 'results.material.not_a_quantity'}}, 'The quantity "results.material.not_a_quantity" is not available.'], + ['invalid x unit', {x: {search_quantity: 'results.material.topology.cell.a', unit: 'nounit'}}, 'Unit "nounit" not found.'], + ['incompatible x unit', {x: {search_quantity: 'results.material.topology.cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length".'] ])('%s', async (name, config, error) => { const finalConfig = { id: '0', diff --git a/gui/src/components/search/widgets/WidgetPeriodicTable.js b/gui/src/components/search/widgets/WidgetPeriodicTable.js index 5055fc0fe45a6d546e72f4a9fef25a69fa88ee31..9a5f711a450163718fca008127f0c064ce8f88b5 100644 --- a/gui/src/components/search/widgets/WidgetPeriodicTable.js +++ b/gui/src/components/search/widgets/WidgetPeriodicTable.js @@ -24,8 +24,12 @@ import { Widget, schemaWidget } from './Widget' import { ActionSelect } from '../../Actions' import { WidgetEditDialog, WidgetEditGroup, WidgetEditOption } from './WidgetEdit' import { InputTextField } from '../input/InputText' +import { InputMetainfo } from '../input/InputMetainfo' import { PeriodicTable } from '../input/InputPeriodicTable' import { scales } from '../../plotting/common' +import { DType } from '../../../utils' + +const dtypes = new Set([DType.String, DType.Enum]) /** * Displays a periodic table as a widget. @@ -35,7 +39,7 @@ export const WidgetPeriodicTable = React.memo(( id, title, description, - quantity, + search_quantity, scale, className }) => { @@ -52,7 +56,7 @@ export const WidgetPeriodicTable = React.memo(( return <Widget id={id} - quantity={quantity} + quantity={search_quantity} title={title} description={description} onEdit={handleEdit} @@ -67,10 +71,9 @@ export const WidgetPeriodicTable = React.memo(( } > <PeriodicTable - quantity={quantity} + quantity={search_quantity} scale={scale} anchored={true} - disableStatistics={true} visible={true} // Can't use the same aggregation identifier as the periodic table in the // filter menu: due to rendering order the aggregation may otherwise get @@ -84,7 +87,7 @@ WidgetPeriodicTable.propTypes = { id: PropTypes.string.isRequired, title: PropTypes.string, description: PropTypes.string, - quantity: PropTypes.string, + search_quantity: PropTypes.string, scale: PropTypes.string, className: PropTypes.string } @@ -143,6 +146,19 @@ export const WidgetPeriodicTableEdit = React.memo((props) => { error={hasError} > <WidgetEditGroup title="Elements"> + <WidgetEditOption> + <InputMetainfo + label="Search quantity" + value={settings.search_quantity} + error={errors.search_quantity} + onChange={(value) => handleChange('search_quantity', value)} + onSelect={(value) => handleAccept('search_quantity', value)} + onError={(value) => handleError('search_quantity', value)} + dtypes={dtypes} + dtypesRepeatable={dtypes} + disableNonAggregatable + /> + </WidgetEditOption> <WidgetEditOption> <TextField select @@ -164,7 +180,7 @@ export const WidgetPeriodicTableEdit = React.memo((props) => { <WidgetEditGroup title="General"> <WidgetEditOption> <InputTextField - label="title" + label="Title" fullWidth value={settings?.title} onChange={(event) => handleChange('title', event.target.value)} @@ -184,6 +200,6 @@ WidgetPeriodicTableEdit.propTypes = { } export const schemaWidgetPeriodicTable = schemaWidget.shape({ - quantity: string().required('Quantity is required.'), + search_quantity: string().required('Search quantity is required.'), scale: string().required('Scale is required.') }) diff --git a/gui/src/components/search/widgets/WidgetScatterPlot.js b/gui/src/components/search/widgets/WidgetScatterPlot.js index 84efcd71be70b6d08dede7e870aed3f0507e8a5f..7f56fefdebb96f3130d99a37f1e8585f26c49e45 100644 --- a/gui/src/components/search/widgets/WidgetScatterPlot.js +++ b/gui/src/components/search/widgets/WidgetScatterPlot.js @@ -34,10 +34,11 @@ import { Action, ActionCheckbox } from '../../Actions' import { CropFree, PanTool, Fullscreen, Replay } from '@material-ui/icons' import { autorangeDescription } from './WidgetHistogram' import { styled } from '@material-ui/core/styles' -import {DType, parseJMESPath, getDisplayLabel} from '../../../utils' +import {DType, parseJMESPath} from '../../../utils' import { Quantity } from '../../units/Quantity' import { Unit } from '../../units/Unit' import { useUnitContext } from '../../units/UnitContext' +import { getAxisConfig } from '../../plotting/common' const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({ '& .MuiToggleButtonGroup-grouped': { @@ -93,52 +94,38 @@ export const WidgetScatterPlot = React.memo(( const { useSetWidget, useHits, filterData, useSetFilter } = useSearchContext() // Parse additional JMESPath config - const [xParsed, yParsed, colorParsed, error] = useMemo(() => { - const xParsed = parseJMESPath(x?.quantity) - const yParsed = parseJMESPath(y?.quantity) - const colorParsed = markers?.color?.quantity ? parseJMESPath(markers.color.quantity) : {} + const [xParsed, yParsed, colorParsed, discrete, error] = useMemo(() => { + const xParsed = parseJMESPath(x?.search_quantity) + const yParsed = parseJMESPath(y?.search_quantity) + const colorParsed = markers?.color?.search_quantity ? parseJMESPath(markers.color.search_quantity) : {} + const discrete = colorParsed?.quantity && new Set([DType.String, DType.Enum]).has(filterData[colorParsed.quantity]?.dtype) if (xParsed.error || yParsed.error || colorParsed.error) { - return [{}, {}, {}, 'Invalid JMESPath query, please check your syntax.'] + return [{}, {}, {}, undefined, 'Invalid JMESPath query, please check your syntax.'] } - return [xParsed, yParsed, colorParsed, undefined] - }, [markers?.color?.quantity, x?.quantity, y?.quantity]) + return [xParsed, yParsed, colorParsed, discrete, undefined] + }, [markers?.color?.search_quantity, x?.search_quantity, y?.search_quantity, filterData]) - // Parse units - const {unitXObj, unitYObj, unitColorObj, displayUnitX, displayUnitY, displayUnitColor, discrete} = useMemo(() => { + // Get storage unit for API communication + const {storageUnitX, storageUnitY, storageUnitColor} = useMemo(() => { if (error) return {} - const unitXObj = new Unit(filterData[xParsed.quantity].unit || 'dimensionless') - const unitYObj = new Unit(filterData[yParsed.quantity].unit || 'dimensionless') - const unitColorObj = new Unit(filterData[colorParsed?.quantity]?.unit || 'dimensionless') - const displayUnitX = x.unit ? new Unit(x.unit) : unitXObj.toSystem(units) - const displayUnitY = y.unit ? new Unit(y.unit) : unitYObj.toSystem(units) - const displayUnitColor = markers?.color?.unit ? new Unit(markers?.color?.unit) : unitColorObj.toSystem(units) - const discrete = colorParsed?.quantity && new Set([DType.String, DType.Enum]).has(filterData[colorParsed.quantity]?.dtype) - return {unitXObj, unitYObj, unitColorObj, displayUnitX, displayUnitY, displayUnitColor, discrete} - }, [filterData, x.unit, xParsed.quantity, y.unit, yParsed.quantity, markers?.color?.unit, colorParsed?.quantity, units, error]) + const storageUnitX = new Unit(filterData[xParsed.quantity].unit || 'dimensionless') + const storageUnitY = new Unit(filterData[yParsed.quantity].unit || 'dimensionless') + const storageUnitColor = new Unit(filterData[colorParsed?.quantity]?.unit || 'dimensionless') + return {storageUnitX, storageUnitY, storageUnitColor} + }, [filterData, xParsed.quantity, yParsed.quantity, colorParsed?.quantity, error]) // Create final axis config for the plot const {xAxis, yAxis, colorAxis} = useMemo(() => { - if (error) return {} - const xFilter = filterData[xParsed.quantity] - const yFilter = filterData[yParsed.quantity] - const colorFilter = filterData[colorParsed.quantity] - const xTitle = x.title || xFilter?.label || getDisplayLabel(xFilter) - const yTitle = y.title || yFilter?.label || getDisplayLabel(yFilter) - const xType = xFilter?.dtype - const yType = yFilter?.dtype - const colorTitle = markers?.color?.title || colorFilter?.label || getDisplayLabel(colorFilter) - const unitLabelX = displayUnitX.label() - const unitLabelY = displayUnitY.label() - const unitLabelColor = displayUnitColor.label() + if (error) return {xAxis: {}, yAxis: {}, colorAxis: {}} return { - xAxis: {...x, ...xParsed, title: xTitle, unit: unitLabelX, type: xType}, - yAxis: {...y, ...yParsed, title: yTitle, unit: unitLabelY, type: yType}, - colorAxis: markers?.color ? {...markers.color, ...colorParsed, title: colorTitle, unit: unitLabelColor} : {} + xAxis: getAxisConfig(x, filterData, units), + yAxis: getAxisConfig(y, filterData, units), + colorAxis: markers?.color ? getAxisConfig(markers.color, filterData, units) : {} } - }, [colorParsed, displayUnitColor, displayUnitX, displayUnitY, filterData, markers?.color, x, xParsed, y, yParsed, error]) + }, [error, filterData, markers.color, x, units, y]) - const setXFilter = useSetFilter(xParsed.quantity) - const setYFilter = useSetFilter(yParsed.quantity) + const setXFilter = useSetFilter(xAxis.search_quantity) + const setYFilter = useSetFilter(yAxis.search_quantity) const setWidget = useSetWidget(id) const pagination = useMemo(() => ({ @@ -279,16 +266,16 @@ export const WidgetScatterPlot = React.memo(( if (!dataRaw) return const x = xAxis.type === DType.Timestamp ? dataRaw.x - : new Quantity(dataRaw.x, unitXObj).to(displayUnitX).value() + : new Quantity(dataRaw.x, storageUnitX).to(xAxis.unit).value() const y = yAxis.type === DType.Timestamp ? dataRaw.y - : new Quantity(dataRaw.y, unitYObj).to(displayUnitY).value() + : new Quantity(dataRaw.y, storageUnitY).to(yAxis.unit).value() const color = dataRaw.color && (discrete ? dataRaw.color - : new Quantity(dataRaw.color, unitColorObj).to(displayUnitColor).value() + : new Quantity(dataRaw.color, storageUnitColor).to(colorAxis.unit).value() ) return {x, y, color, id: dataRaw.id} - }, [dataRaw, displayUnitColor, displayUnitX, displayUnitY, unitColorObj, unitXObj, unitYObj, discrete, xAxis, yAxis]) + }, [dataRaw, xAxis.type, xAxis.unit, storageUnitX, yAxis.type, yAxis.unit, storageUnitY, discrete, storageUnitColor, colorAxis.unit]) const handleEdit = useCallback(() => { setWidget(old => { return {...old, editing: true } }) @@ -315,15 +302,15 @@ export const WidgetScatterPlot = React.memo(( const range = data?.range if (!range) return setXFilter({ - gte: new Quantity(range.x[0], displayUnitX), - lte: new Quantity(range.x[1], displayUnitX) + gte: new Quantity(range.x[0], xAxis.unit), + lte: new Quantity(range.x[1], xAxis.unit) }) setYFilter({ - gte: new Quantity(range.y[0], displayUnitY), - lte: new Quantity(range.y[1], displayUnitY) + gte: new Quantity(range.y[0], yAxis.unit), + lte: new Quantity(range.y[1], yAxis.unit) }) onSelected?.(data) - }, [onSelected, setXFilter, setYFilter, displayUnitX, displayUnitY]) + }, [setXFilter, xAxis.unit, setYFilter, yAxis.unit, onSelected]) const handleDeselect = useCallback(() => { onSelected?.(undefined) diff --git a/gui/src/components/search/widgets/WidgetScatterPlot.spec.js b/gui/src/components/search/widgets/WidgetScatterPlot.spec.js index ee7dce6f5a21deccc41eaaead57ce359f3510029..161994bbd966f53642b8f8ed3b8281e08f01cb53 100644 --- a/gui/src/components/search/widgets/WidgetScatterPlot.spec.js +++ b/gui/src/components/search/widgets/WidgetScatterPlot.spec.js @@ -96,9 +96,9 @@ describe('test different combinations of x/y/color produced with JMESPath', () = const config = { id: '0', scale: 'linear', - x: {quantity: x}, - y: {quantity: y}, - markers: {color: {quantity: color}} + x: {search_quantity: x}, + y: {search_quantity: y}, + markers: {color: {search_quantity: color}} } renderSearchEntry(<WidgetScatterPlot {...config} />) await expectWidgetScatterPlot(config, false) @@ -107,19 +107,19 @@ describe('test different combinations of x/y/color produced with JMESPath', () = describe('test custom axis titles', () => { test.each([ - ['x, no unit', {x: {title: 'My Title', quantity: 'results.material.n_elements'}}, 'My Title'], - ['y, no unit', {y: {title: 'My Title', quantity: 'results.material.n_elements'}}, 'My Title'], - ['color, no unit', {markers: {color: {title: 'My Title', quantity: 'results.material.n_elements'}}}, 'My Title'], - ['x, with unit', {x: {title: 'My Title', quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'My Title (eV)'], - ['y, with unit', {y: {title: 'My Title', quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'My Title (eV)'], - ['color, with unit', {markers: {color: {title: 'My Title', quantity: 'results.properties.geometry_optimization.final_energy_difference'}}}, 'My Title (eV)'] + ['x, no unit', {x: {title: 'My Title', search_quantity: 'results.material.n_elements'}}, 'My Title'], + ['y, no unit', {y: {title: 'My Title', search_quantity: 'results.material.n_elements'}}, 'My Title'], + ['color, no unit', {markers: {color: {title: 'My Title', search_quantity: 'results.material.n_elements'}}}, 'My Title'], + ['x, with unit', {x: {title: 'My Title', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'My Title (eV)'], + ['y, with unit', {y: {title: 'My Title', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'My Title (eV)'], + ['color, with unit', {markers: {color: {title: 'My Title', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}}, 'My Title (eV)'] ])('%s', async (name, config, title) => { const configFinal = { id: '0', scale: 'linear', - x: {quantity: 'results.material.n_elements'}, - y: {quantity: 'results.material.n_elements'}, - markers: {color: {quantity: 'results.material.n_elements'}}, + x: {search_quantity: 'results.material.n_elements'}, + y: {search_quantity: 'results.material.n_elements'}, + markers: {color: {search_quantity: 'results.material.n_elements'}}, ...config } renderSearchEntry(<WidgetScatterPlot {...configFinal} />) @@ -129,16 +129,16 @@ describe('test custom axis titles', () => { describe('test custom axis units', () => { test.each([ - ['x', {x: {unit: 'Ha', quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'Final energy difference (Ha)'], - ['y', {y: {unit: 'Ha', quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'Final energy difference (Ha)'], - ['color', {markers: {color: {unit: 'Ha', quantity: 'results.properties.geometry_optimization.final_energy_difference'}}}, 'Final energy difference (Ha)'] + ['x', {x: {unit: 'Ha', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'Final energy difference (Ha)'], + ['y', {y: {unit: 'Ha', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}, 'Final energy difference (Ha)'], + ['color', {markers: {color: {unit: 'Ha', search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}}}, 'Final energy difference (Ha)'] ])('%s', async (name, config, title) => { const configFinal = { id: '0', scale: 'linear', - x: {quantity: 'results.material.n_elements'}, - y: {quantity: 'results.material.n_elements'}, - markers: {color: {quantity: 'results.material.n_elements'}}, + x: {search_quantity: 'results.material.n_elements'}, + y: {search_quantity: 'results.material.n_elements'}, + markers: {color: {search_quantity: 'results.material.n_elements'}}, ...config } renderSearchEntry(<WidgetScatterPlot {...configFinal} />) @@ -149,17 +149,17 @@ describe('test custom axis units', () => { describe('test different colors', () => { test.each([ ['empty', undefined, undefined, []], - ['scalar integer', 'results.material.n_elements', {quantity: 'results.material.n_elements'}, []], - ['scalar float', 'results.properties.geometry_optimization.final_energy_difference', {quantity: 'results.properties.geometry_optimization.final_energy_difference'}, []], + ['scalar integer', 'results.material.n_elements', {search_quantity: 'results.material.n_elements'}, []], + ['scalar float', 'results.properties.geometry_optimization.final_energy_difference', {search_quantity: 'results.properties.geometry_optimization.final_energy_difference'}, []], ['scalar string', 'results.material.chemical_formula_hill', undefined, ['Si2', 'CO2']], ['array string', 'results.material.elements', undefined, ['Si', 'C, O']] ])('%s', async (name, axis, colorTitle, legend) => { const config = { id: '0', scale: 'linear', - x: {quantity: 'results.material.n_elements'}, - y: {quantity: 'results.material.n_elements'}, - markers: {color: {quantity: axis}} + x: {search_quantity: 'results.material.n_elements'}, + y: {search_quantity: 'results.material.n_elements'}, + markers: {color: {search_quantity: axis}} } renderSearchEntry(<WidgetScatterPlot {...config} />) await expectWidgetScatterPlot(config, false, colorTitle, legend) @@ -173,9 +173,9 @@ describe('test error messages', () => { const config = { id: '0', scale: 'linear', - x: {quantity: axis}, - y: {quantity: axis}, - markers: {color: {quantity: axis}} + x: {search_quantity: axis}, + y: {search_quantity: axis}, + markers: {color: {search_quantity: axis}} } renderSearchEntry(<WidgetScatterPlot {...config} />) screen.getByText(message, {exact: false}) diff --git a/gui/src/components/search/widgets/WidgetScatterPlotEdit.js b/gui/src/components/search/widgets/WidgetScatterPlotEdit.js index 04ecdf417545640011d5bfd0d093b6df70c54bf5..61a6991e5f131325bdc4752fac4280f0f7fe5962 100644 --- a/gui/src/components/search/widgets/WidgetScatterPlotEdit.js +++ b/gui/src/components/search/widgets/WidgetScatterPlotEdit.js @@ -102,13 +102,13 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { // Check for missing values. This check is required because there is no // value set when a new widget is created, and pressing the done button // without filling a value should raise an error. - const xEmpty = isEmptyString(settings?.x?.quantity) + const xEmpty = isEmptyString(settings?.x?.search_quantity) if (xEmpty) { - handleErrorQuantity('x.quantity', 'Please specify a value.') + handleErrorQuantity('x.search_quantity', 'Please specify a value.') } - const yEmpty = isEmptyString(settings?.y?.quantity) + const yEmpty = isEmptyString(settings?.y?.search_quantity) if (yEmpty) { - handleErrorQuantity('y.quantity', 'Please specify a value.') + handleErrorQuantity('y.search_quantity', 'Please specify a value.') } if (!independentErrors && !xEmpty && !yEmpty) { @@ -127,20 +127,20 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { <WidgetEditGroup title="x axis"> <WidgetEditOption> <InputJMESPath - label="quantity" - value={settings.x?.quantity} - onChange={(value) => handleChange('x.quantity', value)} - onSelect={(value) => handleAcceptQuantity('x.quantity', value)} - onAccept={(value) => handleAcceptQuantity('x.quantity', value)} - error={errors['x.quantity']} - onError={(value) => handleErrorQuantity('x.quantity', value)} + label="Search quantity" + value={settings.x?.search_quantity} + onChange={(value) => handleChange('x.search_quantity', value)} + onSelect={(value) => handleAcceptQuantity('x.search_quantity', value)} + onAccept={(value) => handleAcceptQuantity('x.search_quantity', value)} + error={errors['x.search_quantity']} + onError={(value) => handleErrorQuantity('x.search_quantity', value)} dtypes={dtypesNumeric} dtypesRepeatable={dtypesNumeric} /> </WidgetEditOption> <WidgetEditOption> <InputTextField - label="title" + label="Title" fullWidth value={settings.x?.title} onChange={(event) => handleChange('x.title', event.target.value)} @@ -148,14 +148,14 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { </WidgetEditOption> <WidgetEditOption> <UnitInput - label='unit' + label='Unit' value={settings.x?.unit} onChange={(value) => handleChange('x.unit', value)} onSelect={(value) => handleAccept('x.unit', value)} onAccept={(value) => handleAccept('x.unit', value)} error={errors['x.unit']} onError={(value) => handleError('x.unit', value)} - dimension={dimensions['x.quantity'] || null} + dimension={dimensions['x.search_quantity'] || null} optional disableGroup /> @@ -164,7 +164,7 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { <TextField select fullWidth - label="scale" + label="Scale" variant="filled" value={settings.x?.scale} onChange={(event) => { handleChange('x.scale', event.target.value) }} @@ -178,20 +178,20 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { <WidgetEditGroup title="y axis"> <WidgetEditOption> <InputJMESPath - label="quantity" - value={settings.y?.quantity} - onChange={(value) => handleChange('y.quantity', value)} - onSelect={(value) => handleAcceptQuantity('y.quantity', value)} - onAccept={(value) => handleAcceptQuantity('y.quantity', value)} - error={errors['y.quantity']} - onError={(value) => handleErrorQuantity('y.quantity', value)} + label="Search quantity" + value={settings.y?.search_quantity} + onChange={(value) => handleChange('y.search_quantity', value)} + onSelect={(value) => handleAcceptQuantity('y.search_quantity', value)} + onAccept={(value) => handleAcceptQuantity('y.search_quantity', value)} + error={errors['y.search_quantity']} + onError={(value) => handleErrorQuantity('y.search_quantity', value)} dtypes={dtypesNumeric} dtypesRepeatable={dtypesNumeric} /> </WidgetEditOption> <WidgetEditOption> <InputTextField - label="title" + label="Title" fullWidth value={settings.y?.title} onChange={(event) => handleChange('y.title', event.target.value)} @@ -199,14 +199,14 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { </WidgetEditOption> <WidgetEditOption> <UnitInput - label='unit' + label='Unit' value={settings.y?.unit} onChange={(value) => handleChange('y.unit', value)} onSelect={(value) => handleAccept('y.unit', value)} onAccept={(value) => handleAccept('y.unit', value)} error={errors['y.unit']} onError={(value) => handleError('y.unit', value)} - dimension={dimensions['y.quantity'] || null} + dimension={dimensions['y.search_quantity'] || null} optional disableGroup /> @@ -215,7 +215,7 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { <TextField select fullWidth - label="scale" + label="Scale" variant="filled" value={settings.y?.scale} onChange={(event) => { handleChange('y.scale', event.target.value) }} @@ -229,13 +229,13 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { <WidgetEditGroup title="marker color"> <WidgetEditOption> <InputJMESPath - label="quantity" - value={settings?.markers?.color?.quantity} - onChange={(value) => handleChange('markers.color.quantity', value)} - onSelect={(value) => handleAcceptQuantity('markers.color.quantity', value)} - onAccept={(value) => handleAcceptQuantity('markers.color.quantity', value)} - error={errors['markers.color.quantity']} - onError={(value) => handleErrorQuantity('markers.color.quantity', value)} + label="Search quantity" + value={settings?.markers?.color?.search_quantity} + onChange={(value) => handleChange('markers.color.search_quantity', value)} + onSelect={(value) => handleAcceptQuantity('markers.color.search_quantity', value)} + onAccept={(value) => handleAcceptQuantity('markers.color.search_quantity', value)} + error={errors['markers.color.search_quantity']} + onError={(value) => handleErrorQuantity('markers.color.search_quantity', value)} dtypes={dtypesColor} dtypesRepeatable={dtypesColor} optional @@ -243,7 +243,7 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { </WidgetEditOption> <WidgetEditOption> <InputTextField - label="title" + label="Title" fullWidth value={settings.markers?.color?.title} onChange={(event) => handleChange('markers.color.title', event.target.value)} @@ -251,23 +251,23 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => { </WidgetEditOption> <WidgetEditOption> <UnitInput - label='unit' + label='Unit' value={settings.markers?.color?.unit} onChange={(value) => handleChange('markers.color.unit', value)} onSelect={(value) => handleAccept('markers.color.unit', value)} onAccept={(value) => handleAccept('markers.color.unit', value)} error={errors['markers.color.unit']} onError={(value) => handleError('markers.color.unit', value)} - dimension={dimensions['markers.color.quantity'] || null} + dimension={dimensions['markers.color.search_quantity'] || null} optional disableGroup /> </WidgetEditOption> </WidgetEditGroup> - <WidgetEditGroup title="general"> + <WidgetEditGroup title="General"> <WidgetEditOption> <InputTextField - label="title" + label="Title" fullWidth value={settings?.title} onChange={(event) => handleChange('title', event.target.value)} @@ -296,8 +296,8 @@ WidgetScatterPlotEdit.propTypes = { } export const schemaWidgetScatterPlot = schemaWidget.shape({ - x: schemaAxis.required('Quantity for the x axis is required.'), - y: schemaAxis.required('Quantity for the y axis is required.'), + x: schemaAxis.required('Search quantity for the x axis is required.'), + y: schemaAxis.required('Search quantity for the y axis is required.'), markers: schemaMarkers, size: number().integer().required('Size is required.'), autorange: bool() diff --git a/gui/src/components/search/widgets/WidgetScatterPlotEdit.spec.js b/gui/src/components/search/widgets/WidgetScatterPlotEdit.spec.js index 7f8d458f4a66f0c8583d276634dc8e4b1c8a6f82..6f7bd36ade5cd38cb4285ca2d26194ca0c9841c5 100644 --- a/gui/src/components/search/widgets/WidgetScatterPlotEdit.spec.js +++ b/gui/src/components/search/widgets/WidgetScatterPlotEdit.spec.js @@ -23,23 +23,23 @@ import { WidgetScatterPlotEdit } from './WidgetScatterPlotEdit' describe('test edit dialog error messages', () => { test.each([ - ['missing x', {x: {quantity: 'results.material.n_elements'}}, 'Please specify a value.'], - ['missing y', {y: {quantity: 'results.material.n_elements'}}, 'Please specify a value.'], - ['unavailable x', {x: {quantity: 'results.material.not_a_quantity'}}, 'The quantity "results.material.not_a_quantity" is not available.'], - ['unavailable y', {y: {quantity: 'results.material.not_a_quantity'}}, 'The quantity "results.material.not_a_quantity" is not available.'], - ['unavailable color', {markers: {color: {quantity: 'results.material.not_a_quantity'}}}, 'The quantity "results.material.not_a_quantity" is not available.'], - ['invalid jmespath x', {x: {quantity: 'results.material.n_elements[*'}}, 'Invalid JMESPath query, please check your syntax.'], - ['invalid jmespath y', {y: {quantity: 'results.material.n_elements[*'}}, 'Invalid JMESPath query, please check your syntax.'], - ['invalid jmespath color', {markers: {color: {quantity: 'results.material.n_elements[*'}}}, 'Invalid JMESPath query, please check your syntax.'], - ['no jmespath for repeating x', {x: {quantity: 'results.material.topology.cell.a'}}, 'The quantity "results.material.topology.cell.a" is contained in at least one repeatable section. Please use JMESPath syntax to select one or more target sections.'], - ['no jmespath for repeating y', {y: {quantity: 'results.material.topology.cell.a'}}, 'The quantity "results.material.topology.cell.a" is contained in at least one repeatable section. Please use JMESPath syntax to select one or more target sections.'], - ['no jmespath for repeating color', {markers: {color: {quantity: 'results.material.topology.cell.a'}}}, 'The quantity "results.material.topology.cell.a" is contained in at least one repeatable section. Please use JMESPath syntax to select one or more target sections.'], - ['invalid x unit', {x: {quantity: 'results.material.topology[0].cell.a', unit: 'nounit'}}, 'Unit "nounit" not found.'], - ['invalid y unit', {y: {quantity: 'results.material.topology[0].cell.a', unit: 'nounit'}}, 'Unit "nounit" not found.'], - ['invalid color unit', {markers: {color: {quantity: 'results.material.topology[0].cell.a', unit: 'nounit'}}}, 'Unit "nounit" not found.'], - ['incompatible x unit', {x: {quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length".'], - ['incompatible y unit', {y: {quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length".'], - ['incompatible color unit', {markers: {color: {quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}}, 'Unit "joule" is incompatible with dimension "length".'] + ['missing x', {x: {search_quantity: 'results.material.n_elements'}}, 'Please specify a value.'], + ['missing y', {y: {search_quantity: 'results.material.n_elements'}}, 'Please specify a value.'], + ['unavailable x', {x: {search_quantity: 'results.material.not_a_quantity'}}, 'The quantity "results.material.not_a_quantity" is not available.'], + ['unavailable y', {y: {search_quantity: 'results.material.not_a_quantity'}}, 'The quantity "results.material.not_a_quantity" is not available.'], + ['unavailable color', {markers: {color: {search_quantity: 'results.material.not_a_quantity'}}}, 'The quantity "results.material.not_a_quantity" is not available.'], + ['invalid jmespath x', {x: {search_quantity: 'results.material.n_elements[*'}}, 'Invalid JMESPath query, please check your syntax.'], + ['invalid jmespath y', {y: {search_quantity: 'results.material.n_elements[*'}}, 'Invalid JMESPath query, please check your syntax.'], + ['invalid jmespath color', {markers: {color: {search_quantity: 'results.material.n_elements[*'}}}, 'Invalid JMESPath query, please check your syntax.'], + ['no jmespath for repeating x', {x: {search_quantity: 'results.material.topology.cell.a'}}, 'The quantity "results.material.topology.cell.a" is contained in at least one repeatable section. Please use JMESPath syntax to select one or more target sections.'], + ['no jmespath for repeating y', {y: {search_quantity: 'results.material.topology.cell.a'}}, 'The quantity "results.material.topology.cell.a" is contained in at least one repeatable section. Please use JMESPath syntax to select one or more target sections.'], + ['no jmespath for repeating color', {markers: {color: {search_quantity: 'results.material.topology.cell.a'}}}, 'The quantity "results.material.topology.cell.a" is contained in at least one repeatable section. Please use JMESPath syntax to select one or more target sections.'], + ['invalid x unit', {x: {search_quantity: 'results.material.topology[0].cell.a', unit: 'nounit'}}, 'Unit "nounit" not found.'], + ['invalid y unit', {y: {search_quantity: 'results.material.topology[0].cell.a', unit: 'nounit'}}, 'Unit "nounit" not found.'], + ['invalid color unit', {markers: {color: {search_quantity: 'results.material.topology[0].cell.a', unit: 'nounit'}}}, 'Unit "nounit" not found.'], + ['incompatible x unit', {x: {search_quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length".'], + ['incompatible y unit', {y: {search_quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length".'], + ['incompatible color unit', {markers: {color: {search_quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}}, 'Unit "joule" is incompatible with dimension "length".'] ])('%s', async (name, config, error) => { const finalConfig = { id: '0', diff --git a/gui/src/components/search/widgets/WidgetTerms.js b/gui/src/components/search/widgets/WidgetTerms.js index f368cc0bfd3144a2f90f604a99f4899b190fb538..130ec89542203eff11d87c587cbc7818bb749c07 100644 --- a/gui/src/components/search/widgets/WidgetTerms.js +++ b/gui/src/components/search/widgets/WidgetTerms.js @@ -102,15 +102,15 @@ export const WidgetTerms = React.memo(( id, title, description, - quantity, + search_quantity, scale, - showinput, + show_input, className, 'data-testid': testID }) => { const {useAgg, useFilterState, filterData} = useSearchContext() const styles = useStyles() - const [filter, setFilter] = useFilterState(quantity) + const [filter, setFilter] = useFilterState(search_quantity) const { height, ref } = useResizeDetector() const { useSetWidget } = useSearchContext() const setWidget = useSetWidget(id) @@ -122,11 +122,11 @@ export const WidgetTerms = React.memo(( // If a fixed list of options is used, we must restrict the aggregation // return values with 'include'. Otherwise the returned results may not // contain the correct values. - const options = filterData[quantity]?.options + const options = filterData[search_quantity]?.options if (options) config.include = Object.keys(options) return config - }, [aggSize, filterData, quantity]) - const agg = useAgg(quantity, !isNil(height), id, aggConfig) + }, [aggSize, filterData, search_quantity]) + const agg = useAgg(search_quantity, !isNil(height), id, aggConfig) const max = agg ? Math.max(...agg.data.map(option => option.nested_count)) : 0 const handleChange = useCallback((event, key, selected) => { @@ -188,7 +188,7 @@ export const WidgetTerms = React.memo(( return <Widget id={id} - quantity={quantity} + quantity={search_quantity} title={title} description={description} onEdit={handleEdit} @@ -205,10 +205,10 @@ export const WidgetTerms = React.memo(( <InputTooltip> <div className={clsx(styles.outerContainer)}> <div className={clsx(styles.innerContainer)}> - {showinput + {show_input ? <InputTextQuantity className={styles.textField} - quantity={quantity} + quantity={search_quantity} disabled={false} disableSuggestions={false} fullWidth @@ -238,11 +238,11 @@ WidgetTerms.propTypes = { id: PropTypes.string.isRequired, title: PropTypes.string, description: PropTypes.string, - quantity: PropTypes.string, + search_quantity: PropTypes.string, nbins: PropTypes.number, scale: PropTypes.string, autorange: PropTypes.bool, - showinput: PropTypes.bool, + show_input: PropTypes.bool, className: PropTypes.string, 'data-testid': PropTypes.string } @@ -307,12 +307,12 @@ export const WidgetTermsEdit = React.memo((props) => { <WidgetEditGroup title="x axis"> <WidgetEditOption> <InputMetainfo - label="quantity" - value={settings.quantity} - error={errors.quantity} - onChange={(value) => handleChange('quantity', value)} - onSelect={(value) => handleAccept('quantity', value)} - onError={(value) => handleError('quantity', value)} + label="Search quantity" + value={settings.search_quantity} + error={errors.search_quantity} + onChange={(value) => handleChange('search_quantity', value)} + onSelect={(value) => handleAccept('search_quantity', value)} + onError={(value) => handleError('search_quantity', value)} dtypes={dtypes} dtypesRepeatable={dtypes} disableNonAggregatable @@ -336,7 +336,7 @@ export const WidgetTermsEdit = React.memo((props) => { <WidgetEditGroup title="general"> <WidgetEditOption> <InputTextField - label="title" + label="Title" fullWidth value={settings?.title} onChange={(event) => handleChange('title', event.target.value)} @@ -344,7 +344,7 @@ export const WidgetTermsEdit = React.memo((props) => { </WidgetEditOption> <WidgetEditOption> <FormControlLabel - control={<Checkbox checked={settings.showinput} onChange={(event, value) => handleChange('showinput', value)}/>} + control={<Checkbox checked={settings.show_input} onChange={(event, value) => handleChange('show_input', value)}/>} label='Show input field' /> </WidgetEditOption> @@ -356,16 +356,16 @@ WidgetTermsEdit.propTypes = { id: PropTypes.string.isRequired, editing: PropTypes.bool, visible: PropTypes.bool, - quantity: PropTypes.string, + search_quantity: PropTypes.string, scale: PropTypes.string, nbins: PropTypes.number, autorange: PropTypes.bool, - showinput: PropTypes.bool, + show_input: PropTypes.bool, onClose: PropTypes.func } export const schemaWidgetTerms = schemaWidget.shape({ - quantity: string().required('Quantity is required.'), + search_quantity: string().required('Search quantity is required.'), scale: string().required('Scale is required.'), - showinput: bool() + show_input: bool() }) diff --git a/gui/src/components/search/widgets/WidgetTerms.spec.js b/gui/src/components/search/widgets/WidgetTerms.spec.js index e6548c80b2aaeaac0dafb81c81d6aea502e11706..33880708fdf6a9dba01427604ade714787d0e675 100644 --- a/gui/src/components/search/widgets/WidgetTerms.spec.js +++ b/gui/src/components/search/widgets/WidgetTerms.spec.js @@ -58,11 +58,11 @@ describe('initial state is loaded correctly', () => { [], undefined ] - ])('%s', async (name, quantity, items, prompt) => { + ])('%s', async (name, search_quantity, items, prompt) => { const widget = { id: '0', scale: 'linear', - quantity: quantity + search_quantity: search_quantity } renderSearchEntry(<WidgetTerms {...widget} />) await expectWidgetTerms(widget, false, items, prompt) diff --git a/gui/src/components/uploads/SectionSelectDialog.js b/gui/src/components/uploads/SectionSelectDialog.js index dad05e574a79f7032525f74958751188f96c4054..9187165cd67b15c4a3f68cf187e00d2ff9911db7 100644 --- a/gui/src/components/uploads/SectionSelectDialog.js +++ b/gui/src/components/uploads/SectionSelectDialog.js @@ -30,7 +30,7 @@ import {useApi} from '../api' import {useUploadPageContext} from './UploadPageContext' import {useEntryStore} from '../entry/EntryContext' import {traverse, useGlobalMetainfo} from '../archive/metainfo' -import { defaultFilterGroups, quantityNameSearch } from '../search/FilterRegistry' +import { quantityNameSearch } from '../search/FilterRegistry' import { SearchResults } from '../search/SearchResults' import {useDataStore} from '../DataStore' import {pluralize, resolveNomadUrlNoThrow} from "../../utils" @@ -44,12 +44,6 @@ const searchDialogContext = React.createContext() const context = cloneDeep(ui?.apps?.options?.entries) context.search_syntaxes.exclude = undefined -const allFilters = new Set(defaultFilterGroups && (Object.keys(context?.filter_menus?.options)) - .map(filter => { - const group = defaultFilterGroups?.[filter] - return group ? Array.from(group) : [] - }).flat()) - const useStyles = makeStyles(theme => ({ dialog: { width: '100%', @@ -218,20 +212,19 @@ function SearchBox({open, onCancel, onSelectedChanged, selected}) { const { useSetFilter, useFilters, - useFiltersLocked, + filters: filterNames, useUpdateFilter, filters: filterList } = useSearchContext() - const filtersLocked = useFiltersLocked() - const filters = useFilters([...allFilters] + const filters = useFilters([...filterNames] .filter( filter => filter !== 'visibility' && filter !== 'processed' && filter !== 'upload_id' && filter !== 'published' && - filter !== 'main_author.user_id' && - !filtersLocked[filter] - )) + filter !== 'main_author.user_id' + ) + ) const updateFilter = useUpdateFilter() const uploadContext = useUploadPageContext() const entryContext = useEntryStore() @@ -416,7 +409,7 @@ function SectionSelectDialog(props) { initialPagination={context?.pagination} initialColumns={columns} initialRows={rows} - initialFilterMenus={context?.filter_menus} + initialMenu={context?.menu} initialFiltersLocked={filtersLocked} initialSearchSyntaxes={context?.search_syntaxes} id='sectionselect' diff --git a/gui/src/components/uploads/UploadSearchMenu.js b/gui/src/components/uploads/UploadSearchMenu.js index dc03f86aa2180949846c04fd9f0125bf1f799899..bebc3591e25888f1d7b2a84c88af8f73d4cae357 100644 --- a/gui/src/components/uploads/UploadSearchMenu.js +++ b/gui/src/components/uploads/UploadSearchMenu.js @@ -31,7 +31,10 @@ const UploadSearchMenu = React.memo(({ } const menu = filteredMenus[0] return ( - <MenuBarRoute menu={menu} label={'Search in this upload'} initialFilters={{upload_id: uploadId}}> + <MenuBarRoute + menu={menu} + label={'Search in this upload'} + initialFilters={{upload_id: uploadId}}> <SearchIcon/> </MenuBarRoute> ) diff --git a/gui/src/utils.js b/gui/src/utils.js index 1346991388f347660c1281bc8ef243e3a5f47446..ca5004af38b1b5bae9f01979ef0208bae2450736 100644 --- a/gui/src/utils.js +++ b/gui/src/utils.js @@ -245,6 +245,15 @@ export function titleCase(str) { return splitStr.join(' ') } +/** + * Converts snake case variable names to camel case. + * @param {*} str Variable name in snake_case + * @returns Variable name in camelCase + */ +export function camelCase(str) { + return str.toLowerCase().replace(/[-_][a-z]/g, (group) => group.slice(-1).toUpperCase()) +} + export function nameList(users, expanded) { const names = users.map(user => titleCase(user.name)).filter(name => name !== '') if (names.length > 3 && !expanded) { @@ -839,19 +848,6 @@ export function pluralize(word, count, inclusive, format = true, prefix) { : `${prefix} `}${form}` } -/** - * Used to create a formatted label for a metainfo name or value. Replaces - * underscores with whitespace and capitalizes the first letters. - * - * @param {str} name Metainfo name - * @returns A formatted label constructed from the metainfo name. - */ -export function formatLabel(label) { - label = label.replace(/_/g, ' ') - label = startCase(label) - return label -} - /** * Used for testing purposes: setting a data-testid to this value signals that the * component waits for further updates of some kind. This is used by some automated tests diff --git a/gui/tests/artifacts.js b/gui/tests/artifacts.js index 43db8aed4f8c2ee093cf61b120697153da4b1547..d2e1e09d96c442522a73389171abf4abf75761d5 100644 --- a/gui/tests/artifacts.js +++ b/gui/tests/artifacts.js @@ -3601,10 +3601,10 @@ window.nomadArtifacts = { "type": { "type_kind": "enum", "type_data": [ - "", "MaxEnt", "Pade", - "SVD" + "SVD", + "Stochastic" ] }, "shape": [], @@ -36087,10 +36087,10 @@ window.nomadArtifacts = { "type": { "type_kind": "enum", "type_data": [ - "", "MaxEnt", "Pade", - "SVD" + "SVD", + "Stochastic" ] }, "shape": [] diff --git a/gui/tests/env.js b/gui/tests/env.js index 26e05f1a58cc606826c3f365749d0c0feaa3a2ef..33faca46628b55698deefb20cbe25f81fb85cdaf 100644 --- a/gui/tests/env.js +++ b/gui/tests/env.js @@ -610,129 +610,1770 @@ window.nomadEnv = { "enabled": true } }, - "filter_menus": { - "options": { - "material": { - "label": "Material", - "level": 0, - "size": "s" + "menu": { + "width": 12, + "show_header": true, + "title": "Filters", + "type": "menu", + "size": "sm", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "title": "Material", + "type": "menu", + "size": "md" }, - "elements": { - "label": "Elements / Formula", - "level": 1, - "size": "xl" + { + "width": 12, + "show_header": true, + "title": "Elements / Formula", + "type": "menu", + "size": "xxl", + "indentation": 1, + "items": [ + { + "type": "periodic_table", + "search_quantity": "results.material.elements", + "scale": "linear", + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_hill", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_iupac", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_reduced", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_anonymous", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.material.n_elements", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] }, - "structure": { - "label": "Structure / Symmetry", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Structure / Symmetry", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.material.structural_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.bravais_lattice", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.crystal_system", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.space_group_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.structure_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.strukturbericht_designation", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.point_group", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.hall_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.prototype_aflow_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "method": { - "label": "Method", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Method", + "type": "menu", + "size": "md", + "items": [ + { + "search_quantity": "results.method.simulation.program_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.program_version", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "precision": { - "label": "Precision", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Precision", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.precision.k_line_density", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.precision.native_tier", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.precision.basis_set", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.precision.planewave_cutoff", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.precision.apw_cutoff", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] }, - "dft": { - "label": "DFT", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "DFT", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "DFT": { + "label": "Search DFT entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.dft.xc_functional_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dft.xc_functional_names", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dft.exact_exchange_mixing_factor", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dft.hubbard_kanamori_model.u_effective", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dft.core_electron_treatment", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dft.relativity_method", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "tb": { - "label": "TB", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "TB", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "TB": { + "label": "Search TB entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.tb.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.tb.localization_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "gw": { - "label": "GW", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "GW", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "GW": { + "label": "Search GW entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.gw.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.gw.starting_point_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.gw.basis_set_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "bse": { - "label": "BSE", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "BSE", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "BSE": { + "label": "Search BSE entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.bse.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.solver", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.starting_point_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.starting_point_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.basis_set_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.gw_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "dmft": { - "label": "DMFT", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "DMFT", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "DMFT": { + "label": "Search DMFT entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.dmft.impurity_solver_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dmft.inverse_temperature", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dmft.magnetic_state", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dmft.u", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dmft.jh", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dmft.analytical_continuation", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "eels": { - "label": "EELS", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "EELS", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "EELS": { + "label": "Search EELS entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.spectroscopic.spectra.provenance.eels", + "items": [ + { + "search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.detector_type", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.resolution", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.min_energy", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.max_energy", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + } + ] }, - "workflow": { - "label": "Workflow", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Workflow", + "type": "menu", + "size": "md" }, - "molecular_dynamics": { - "label": "Molecular dynamics", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Molecular dynamics", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.thermodynamic.trajectory", + "items": [ + { + "search_quantity": "results.properties.thermodynamic.trajectory.available_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 4, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 2, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + } + ] }, - "geometry_optimization": { - "label": "Geometry Optimization", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Geometry Optimization", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.properties.available_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "geometry_optimization": { + "label": "Search geometry optimization entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.geometry_optimization", + "items": [ + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.geometry_optimization.final_energy_difference", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.geometry_optimization.final_force_maximum", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.geometry_optimization.final_displacement_maximum", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + } + ] }, - "properties": { - "label": "Properties", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Properties", + "type": "menu", + "size": "md" }, - "electronic": { - "label": "Electronic", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Electronic", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "electronic_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic.band_gap", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 2, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.dos_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + } + ] }, - "vibrational": { - "label": "Vibrational", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Vibrational", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "vibrational_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "mechanical": { - "label": "Mechanical", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Mechanical", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "mechanical_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.mechanical.bulk_modulus", + "items": [ + { + "search_quantity": "results.properties.mechanical.bulk_modulus.type", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.mechanical.bulk_modulus.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.mechanical.shear_modulus", + "items": [ + { + "search_quantity": "results.properties.mechanical.shear_modulus.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.mechanical.shear_modulus.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.mechanical.energy_volume_curve", + "items": [ + { + "search_quantity": "results.properties.mechanical.energy_volume_curve.type", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + } + ] }, - "usecases": { - "label": "Use Cases", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Use Cases", + "type": "menu", + "size": "md" }, - "solarcell": { - "label": "Solar Cells", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Solar Cells", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.efficiency", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.fill_factor", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.open_circuit_voltage", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.short_circuit_current_density", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.illumination_intensity", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.device_area", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.device_architecture", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.device_stack", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.absorber", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.absorber_fabrication", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.electron_transport_layer", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.hole_transport_layer", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.substrate", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.back_contact", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "heterogeneouscatalyst": { - "label": "Heterogeneous Catalysis", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Heterogeneous Catalysis", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.properties.catalytic.reaction.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.catalytic.reaction.reactants", + "items": [ + { + "search_quantity": "results.properties.catalytic.reaction.reactants.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.catalytic.reaction.reactants.conversion", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.catalytic.reaction.reactants.gas_concentration_in", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.catalytic.reaction.reactants.gas_concentration_out", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.catalytic.reaction.products", + "items": [ + { + "search_quantity": "results.properties.catalytic.reaction.products.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.catalytic.reaction.products.selectivity", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.catalytic.reaction.products.gas_concentration_out", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.catalytic.reaction.reaction_conditions.temperature", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.catalytic.catalyst", + "items": [ + { + "search_quantity": "results.properties.catalytic.catalyst.catalyst_type", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.catalytic.catalyst.preparation_method", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.catalytic.catalyst.catalyst_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.catalytic.catalyst.characterization_methods", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.catalytic.catalyst.surface_area", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + } + ] }, - "author": { - "label": "Author / Origin / Dataset", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Author / Origin / Dataset", + "type": "menu", + "size": "lg", + "items": [ + { + "search_quantity": "authors.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "upload_create_time", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "external_db", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.doi", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "metadata": { - "label": "Visibility / IDs / Schema", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Visibility / IDs / Schema", + "type": "menu", + "size": "md", + "items": [ + { + "width": 12, + "show_header": true, + "type": "visibility" + }, + { + "search_quantity": "entry_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.material_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "definitions" + } + ] }, - "optimade": { - "label": "Optimade", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Optimade", + "type": "menu", + "size": "lg", + "items": [ + { + "width": 12, + "show_header": true, + "type": "optimade" + } + ] } - } + ] }, "filters": { "exclude": [ @@ -741,6 +2382,13 @@ window.nomadEnv = { "combine" ] }, + "search_quantities": { + "exclude": [ + "mainfile", + "entry_name", + "combine" + ] + }, "search_syntaxes": { "exclude": [ "free_text" @@ -885,109 +2533,1269 @@ window.nomadEnv = { "enabled": true } }, - "filter_menus": { - "options": { - "material": { - "label": "Material", - "level": 0, - "size": "s" + "menu": { + "width": 12, + "show_header": true, + "title": "Filters", + "type": "menu", + "size": "sm", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "title": "Material", + "type": "menu", + "size": "md" }, - "elements": { - "label": "Elements / Formula", - "level": 1, - "size": "xl" + { + "width": 12, + "show_header": true, + "title": "Elements / Formula", + "type": "menu", + "size": "xxl", + "indentation": 1, + "items": [ + { + "type": "periodic_table", + "search_quantity": "results.material.elements", + "scale": "linear", + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_hill", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_iupac", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_reduced", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_anonymous", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.material.n_elements", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] }, - "structure": { - "label": "Structure / Symmetry", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Structure / Symmetry", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.material.structural_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.bravais_lattice", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.crystal_system", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.space_group_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.structure_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.strukturbericht_designation", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.point_group", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.hall_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.prototype_aflow_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "method": { - "label": "Method", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Method", + "type": "menu", + "size": "md", + "items": [ + { + "search_quantity": "results.method.simulation.program_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.program_version", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "precision": { - "label": "Precision", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Precision", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.precision.k_line_density", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.precision.native_tier", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.precision.basis_set", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.precision.planewave_cutoff", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.precision.apw_cutoff", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] }, - "dft": { - "label": "DFT", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "DFT", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "DFT": { + "label": "Search DFT entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.dft.xc_functional_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dft.xc_functional_names", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dft.exact_exchange_mixing_factor", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dft.hubbard_kanamori_model.u_effective", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dft.core_electron_treatment", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dft.relativity_method", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "tb": { - "label": "TB", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "TB", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "TB": { + "label": "Search TB entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.tb.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.tb.localization_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "gw": { - "label": "GW", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "GW", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "GW": { + "label": "Search GW entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.gw.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.gw.starting_point_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.gw.basis_set_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "bse": { - "label": "BSE", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "BSE", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "BSE": { + "label": "Search BSE entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.bse.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.solver", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.starting_point_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.starting_point_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.basis_set_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.gw_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "dmft": { - "label": "DMFT", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "DMFT", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "DMFT": { + "label": "Search DMFT entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.dmft.impurity_solver_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dmft.inverse_temperature", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dmft.magnetic_state", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dmft.u", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dmft.jh", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dmft.analytical_continuation", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "workflow": { - "label": "Workflow", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Workflow", + "type": "menu", + "size": "md" }, - "molecular_dynamics": { - "label": "Molecular dynamics", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Molecular dynamics", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.thermodynamic.trajectory", + "items": [ + { + "search_quantity": "results.properties.thermodynamic.trajectory.available_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 4, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 2, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + } + ] }, - "geometry_optimization": { - "label": "Geometry Optimization", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Geometry Optimization", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.properties.available_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "geometry_optimization": { + "label": "Search geometry optimization entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.geometry_optimization", + "items": [ + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.geometry_optimization.final_energy_difference", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.geometry_optimization.final_force_maximum", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.geometry_optimization.final_displacement_maximum", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + } + ] }, - "properties": { - "label": "Properties", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Properties", + "type": "menu", + "size": "md" }, - "electronic": { - "label": "Electronic", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Electronic", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "electronic_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic.band_gap", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 2, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.dos_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + } + ] }, - "vibrational": { - "label": "Vibrational", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Vibrational", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "vibrational_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "mechanical": { - "label": "Mechanical", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Mechanical", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "mechanical_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.mechanical.bulk_modulus", + "items": [ + { + "search_quantity": "results.properties.mechanical.bulk_modulus.type", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.mechanical.bulk_modulus.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.mechanical.shear_modulus", + "items": [ + { + "search_quantity": "results.properties.mechanical.shear_modulus.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.mechanical.shear_modulus.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.mechanical.energy_volume_curve", + "items": [ + { + "search_quantity": "results.properties.mechanical.energy_volume_curve.type", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + } + ] }, - "author": { - "label": "Author / Origin / Dataset", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Author / Origin / Dataset", + "type": "menu", + "size": "lg", + "items": [ + { + "search_quantity": "authors.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "upload_create_time", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "external_db", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.doi", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "metadata": { - "label": "Visibility / IDs / Schema", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Visibility / IDs / Schema", + "type": "menu", + "size": "md", + "items": [ + { + "width": 12, + "show_header": true, + "type": "visibility" + }, + { + "search_quantity": "entry_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.material_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "definitions" + } + ] }, - "optimade": { - "label": "Optimade", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Optimade", + "type": "menu", + "size": "lg", + "items": [ + { + "width": 12, + "show_header": true, + "type": "optimade" + } + ] } - } + ] }, "filters": { "exclude": [ @@ -996,10 +3804,19 @@ window.nomadEnv = { "combine" ] }, + "search_quantities": { + "exclude": [ + "mainfile", + "entry_name", + "combine" + ] + }, "dashboard": { "widgets": [ { "type": "periodictable", + "search_quantity": "results.material.elements", + "scale": "linear", "layout": { "lg": { "h": 11, @@ -1041,12 +3858,13 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.material.elements", - "scale": "linear" + } }, { + "search_quantity": "results.material.symmetry.space_group_symbol", "type": "terms", + "scale": "linear", + "show_input": true, "layout": { "lg": { "h": 5, @@ -1088,13 +3906,13 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.material.symmetry.space_group_symbol", - "scale": "linear", - "showinput": true + } }, { + "search_quantity": "results.material.structural_type", "type": "terms", + "scale": "log", + "show_input": false, "layout": { "lg": { "h": 6, @@ -1136,13 +3954,13 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.material.structural_type", - "scale": "log", - "showinput": false + } }, { + "search_quantity": "results.method.simulation.program_name", "type": "terms", + "scale": "log", + "show_input": true, "layout": { "lg": { "h": 6, @@ -1184,13 +4002,13 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.method.simulation.program_name", - "scale": "log", - "showinput": true + } }, { + "search_quantity": "results.material.symmetry.crystal_system", "type": "terms", + "scale": "linear", + "show_input": false, "layout": { "lg": { "h": 5, @@ -1232,10 +4050,7 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.material.symmetry.crystal_system", - "scale": "linear", - "showinput": false + } } ] }, @@ -1310,117 +4125,1207 @@ window.nomadEnv = { "enabled": false } }, - "filter_menus": { - "options": { - "material": { - "label": "Material", - "level": 0, - "size": "s" + "menu": { + "width": 12, + "show_header": true, + "title": "Filters", + "type": "menu", + "size": "sm", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "title": "Material", + "type": "menu", + "size": "md" }, - "elements": { - "label": "Elements / Formula", - "level": 1, - "size": "xl" + { + "width": 12, + "show_header": true, + "title": "Elements / Formula", + "type": "menu", + "size": "xxl", + "indentation": 1, + "items": [ + { + "type": "periodic_table", + "search_quantity": "results.material.elements", + "scale": "linear", + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_hill", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_iupac", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_reduced", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_anonymous", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.material.n_elements", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] }, - "structure": { - "label": "Structure / Symmetry", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Structure / Symmetry", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.material.structural_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.bravais_lattice", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.crystal_system", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.space_group_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.structure_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.strukturbericht_designation", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.point_group", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.hall_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.prototype_aflow_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "method": { - "label": "Method", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Method", + "type": "menu", + "size": "md", + "items": [ + { + "search_quantity": "results.method.simulation.program_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.program_version", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "dft": { - "label": "DFT", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "DFT", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "DFT": { + "label": "Search DFT entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.dft.xc_functional_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dft.xc_functional_names", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dft.exact_exchange_mixing_factor", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dft.hubbard_kanamori_model.u_effective", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dft.core_electron_treatment", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dft.relativity_method", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "tb": { - "label": "TB", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "TB", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "TB": { + "label": "Search TB entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.tb.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.tb.localization_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "gw": { - "label": "GW", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "GW", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "GW": { + "label": "Search GW entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.gw.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.gw.starting_point_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.gw.basis_set_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "bse": { - "label": "BSE", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "BSE", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "BSE": { + "label": "Search BSE entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.bse.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.solver", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.starting_point_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.starting_point_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.basis_set_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.bse.gw_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "dmft": { - "label": "DMFT", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "DMFT", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "DMFT": { + "label": "Search DMFT entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "search_quantity": "results.method.simulation.dmft.impurity_solver_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dmft.inverse_temperature", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dmft.magnetic_state", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dmft.u", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.method.simulation.dmft.jh", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.dmft.analytical_continuation", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "workflow": { - "label": "Workflow", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Workflow", + "type": "menu", + "size": "md" }, - "molecular_dynamics": { - "label": "Molecular dynamics", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Molecular dynamics", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.thermodynamic.trajectory", + "items": [ + { + "search_quantity": "results.properties.thermodynamic.trajectory.available_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 4, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 2, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + } + ] }, - "geometry_optimization": { - "label": "Geometry Optimization", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Geometry Optimization", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.properties.available_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "geometry_optimization": { + "label": "Search geometry optimization entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.geometry_optimization", + "items": [ + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.geometry_optimization.final_energy_difference", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.geometry_optimization.final_force_maximum", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.geometry_optimization.final_displacement_maximum", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + } + ] }, - "properties": { - "label": "Properties", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Properties", + "type": "menu", + "size": "md" }, - "electronic": { - "label": "Electronic", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Electronic", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "electronic_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic.band_gap", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 2, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.dos_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + } + ] }, - "vibrational": { - "label": "Vibrational", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Vibrational", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "vibrational_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "mechanical": { - "label": "Mechanical", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Mechanical", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "mechanical_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.mechanical.bulk_modulus", + "items": [ + { + "search_quantity": "results.properties.mechanical.bulk_modulus.type", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.mechanical.bulk_modulus.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.mechanical.shear_modulus", + "items": [ + { + "search_quantity": "results.properties.mechanical.shear_modulus.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.mechanical.shear_modulus.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.mechanical.energy_volume_curve", + "items": [ + { + "search_quantity": "results.properties.mechanical.energy_volume_curve.type", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + } + ] }, - "author": { - "label": "Author / Origin / Dataset", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Author / Origin / Dataset", + "type": "menu", + "size": "lg", + "items": [ + { + "search_quantity": "authors.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "upload_create_time", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "external_db", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.doi", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "metadata": { - "label": "Visibility / IDs / Schema", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Visibility / IDs / Schema", + "type": "menu", + "size": "md", + "items": [ + { + "width": 12, + "show_header": true, + "type": "visibility" + }, + { + "search_quantity": "entry_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.material_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "definitions" + } + ] }, - "optimade": { - "label": "Optimade", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Optimade", + "type": "menu", + "size": "lg", + "items": [ + { + "width": 12, + "show_header": true, + "type": "optimade" + } + ] }, - "combine": { - "level": 0, - "size": "s", - "actions": { - "options": { - "combine": { - "type": "checkbox", - "label": "Combine results from several entries", - "quantity": "combine" - } + { + "search_quantity": "combine", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "True": { + "label": "Combine results from several entries", + "description": "If selected, your filters may be matched from several entries that contain the same material. When unchecked, the material has to have a single entry that matches all your filters." } - } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false } - } + ] }, "filters": { "exclude": [ @@ -1428,6 +5333,12 @@ window.nomadEnv = { "entry_name" ] }, + "search_quantities": { + "exclude": [ + "mainfile", + "entry_name" + ] + }, "search_syntaxes": { "exclude": [ "free_text" @@ -1547,44 +5458,370 @@ window.nomadEnv = { "enabled": true } }, - "filter_menus": { - "options": { - "material": { - "label": "Material", - "level": 0, - "size": "s" + "menu": { + "width": 12, + "show_header": true, + "title": "Filters", + "type": "menu", + "size": "sm", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "title": "Material", + "type": "menu", + "size": "md" }, - "elements": { - "label": "Elements / Formula", - "level": 1, - "size": "xl" + { + "width": 12, + "show_header": true, + "title": "Elements / Formula", + "type": "menu", + "size": "xxl", + "indentation": 1, + "items": [ + { + "type": "periodic_table", + "search_quantity": "results.material.elements", + "scale": "linear", + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_hill", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_iupac", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_reduced", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_anonymous", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.material.n_elements", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] }, - "eln": { - "label": "Electronic Lab Notebook", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Electronic Lab Notebook", + "type": "menu", + "size": "md", + "items": [ + { + "search_quantity": "results.eln.sections", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.tags", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.methods", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.instruments", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.names", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.descriptions", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.lab_ids", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "custom_quantities": { - "label": "User Defined Quantities", - "level": 0, - "size": "l" + { + "width": 12, + "show_header": true, + "title": "User Defined Quantities", + "type": "menu", + "size": "xl", + "items": [ + { + "width": 12, + "show_header": true, + "type": "custom_quantities" + } + ] }, - "author": { - "label": "Author / Origin / Dataset", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Author / Origin / Dataset", + "type": "menu", + "size": "lg", + "items": [ + { + "search_quantity": "authors.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "upload_create_time", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "external_db", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.doi", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "metadata": { - "label": "Visibility / IDs / Schema", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Visibility / IDs / Schema", + "type": "menu", + "size": "md", + "items": [ + { + "width": 12, + "show_header": true, + "type": "visibility" + }, + { + "search_quantity": "entry_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.material_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "definitions" + } + ] }, - "optimade": { - "label": "Optimade", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Optimade", + "type": "menu", + "size": "lg", + "items": [ + { + "width": 12, + "show_header": true, + "type": "optimade" + } + ] } - } + ] }, "filters": { "exclude": [ @@ -1593,6 +5830,13 @@ window.nomadEnv = { "combine" ] }, + "search_quantities": { + "exclude": [ + "mainfile", + "entry_name", + "combine" + ] + }, "filters_locked": { "quantities": "data" } @@ -1700,44 +5944,389 @@ window.nomadEnv = { "enabled": true } }, - "filter_menus": { - "options": { - "material": { - "label": "Material", - "level": 0, - "size": "s" + "menu": { + "width": 12, + "show_header": true, + "title": "Filters", + "type": "menu", + "size": "sm", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "title": "Material", + "type": "menu", + "size": "md" }, - "elements": { - "label": "Elements / Formula", - "level": 1, - "size": "xl" + { + "width": 12, + "show_header": true, + "title": "Elements / Formula", + "type": "menu", + "size": "xxl", + "indentation": 1, + "items": [ + { + "type": "periodic_table", + "search_quantity": "results.material.elements", + "scale": "linear", + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_hill", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_iupac", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_reduced", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_anonymous", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.material.n_elements", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] }, - "method": { - "label": "Method", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Method", + "type": "menu", + "size": "md", + "items": [ + { + "search_quantity": "results.method.simulation.program_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.method.simulation.program_version", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "eels": { - "label": "EELS", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "EELS", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.method.method_name", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": false, + "options": { + "EELS": { + "label": "Search EELS entries" + } + }, + "n_columns": 1, + "sort_static": true, + "show_statistics": false + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.spectroscopic.spectra.provenance.eels", + "items": [ + { + "search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.detector_type", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.resolution", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.min_energy", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.max_energy", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + } + ] }, - "author": { - "label": "Author / Origin / Dataset", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Author / Origin / Dataset", + "type": "menu", + "size": "lg", + "items": [ + { + "search_quantity": "authors.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "upload_create_time", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "external_db", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.doi", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "metadata": { - "label": "Visibility / IDs / Schema", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Visibility / IDs / Schema", + "type": "menu", + "size": "md", + "items": [ + { + "width": 12, + "show_header": true, + "type": "visibility" + }, + { + "search_quantity": "entry_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.material_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "definitions" + } + ] }, - "optimade": { - "label": "Optimade", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Optimade", + "type": "menu", + "size": "lg", + "items": [ + { + "width": 12, + "show_header": true, + "type": "optimade" + } + ] } - } + ] }, "filters": { "exclude": [ @@ -1746,6 +6335,13 @@ window.nomadEnv = { "combine" ] }, + "search_quantities": { + "exclude": [ + "mainfile", + "entry_name", + "combine" + ] + }, "filters_locked": { "results.method.method_name": "EELS" }, @@ -2938,34 +7534,200 @@ window.nomadEnv = { "enabled": true } }, - "filter_menus": { - "options": { - "custom_quantities": { - "label": "Notebooks", - "level": 0, - "size": "l" + "menu": { + "width": 12, + "show_header": true, + "title": "Filters", + "type": "menu", + "size": "sm", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "title": "Notebooks", + "type": "menu", + "size": "xl", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "type": "custom_quantities" + } + ] }, - "author": { - "label": "Author", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Author", + "type": "menu", + "size": "lg", + "indentation": 0, + "items": [ + { + "search_quantity": "authors.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "upload_create_time", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "external_db", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.doi", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "metadata": { - "label": "Visibility / IDs", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Visibility / IDs", + "type": "menu", + "size": "md", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "type": "visibility" + }, + { + "search_quantity": "entry_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.material_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "definitions" + } + ] } - } + ] }, "filters": { "include": [ "*#nomad_aitoolkit.schema.AIToolkitNotebook" ] }, + "search_quantities": { + "include": [ + "*#nomad_aitoolkit.schema.AIToolkitNotebook" + ] + }, "dashboard": { "widgets": [ { + "search_quantity": "data.category#nomad_aitoolkit.schema.AIToolkitNotebook", "type": "terms", + "scale": "linear", + "show_input": true, "layout": { "xxl": { "h": 6, @@ -3007,14 +7769,14 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "data.category#nomad_aitoolkit.schema.AIToolkitNotebook", - "scale": "linear", - "showinput": true + } }, { - "title": "Methods", + "search_quantity": "data.methods.name#nomad_aitoolkit.schema.AIToolkitNotebook", "type": "terms", + "scale": "linear", + "show_input": true, + "title": "Methods", "layout": { "xxl": { "h": 6, @@ -3056,14 +7818,14 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "data.methods.name#nomad_aitoolkit.schema.AIToolkitNotebook", - "scale": "linear", - "showinput": true + } }, { - "title": "Systems", + "search_quantity": "data.systems.name#nomad_aitoolkit.schema.AIToolkitNotebook", "type": "terms", + "scale": "linear", + "show_input": true, + "title": "Systems", "layout": { "xxl": { "h": 6, @@ -3104,11 +7866,8 @@ window.nomadEnv = { "y": 0, "minH": 3, "minW": 3 - } - }, - "quantity": "data.systems.name#nomad_aitoolkit.schema.AIToolkitNotebook", - "scale": "linear", - "showinput": true + } + } } ] }, @@ -3198,44 +7957,479 @@ window.nomadEnv = { "enabled": true } }, - "filter_menus": { - "options": { - "material": { - "label": "Material", - "level": 0, - "size": "s" + "menu": { + "width": 12, + "show_header": true, + "title": "Filters", + "type": "menu", + "size": "sm", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "title": "Material", + "type": "menu", + "size": "md", + "indentation": 0 }, - "elements": { - "label": "Elements / Formula", - "level": 1, - "size": "xl" + { + "width": 12, + "show_header": true, + "title": "Elements / Formula", + "type": "menu", + "size": "xxl", + "indentation": 1, + "items": [ + { + "type": "periodic_table", + "search_quantity": "results.material.elements", + "scale": "linear", + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_hill", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_iupac", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_reduced", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_anonymous", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.material.n_elements", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] }, - "structure": { - "label": "Structure", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Structure", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.material.structural_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.bravais_lattice", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.crystal_system", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.space_group_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.structure_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.strukturbericht_designation", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.point_group", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.hall_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.prototype_aflow_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "electronic": { - "label": "Electronic Properties", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Electronic Properties", + "type": "menu", + "size": "md", + "indentation": 0, + "items": [ + { + "search_quantity": "electronic_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic.band_gap", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 2, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.dos_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + } + ] }, - "author": { - "label": "Author / Origin / Dataset", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Author / Origin / Dataset", + "type": "menu", + "size": "lg", + "indentation": 0, + "items": [ + { + "search_quantity": "authors.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "upload_create_time", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "external_db", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.doi", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "metadata": { - "label": "Visibility / IDs / Schema", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Visibility / IDs / Schema", + "type": "menu", + "size": "md", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "type": "visibility" + }, + { + "search_quantity": "entry_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.material_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "definitions" + } + ] }, - "optimade": { - "label": "Optimade", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Optimade", + "type": "menu", + "size": "lg", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "type": "optimade" + } + ] } - } + ] }, "filters": { "exclude": [ @@ -3244,10 +8438,19 @@ window.nomadEnv = { "combine" ] }, + "search_quantities": { + "exclude": [ + "mainfile", + "entry_name", + "combine" + ] + }, "dashboard": { "widgets": [ { - "type": "periodictable", + "type": "periodic_table", + "search_quantity": "results.material.elements", + "scale": "linear", "layout": { "lg": { "h": 9, @@ -3289,13 +8492,15 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.material.elements", - "scale": "linear" + } }, { - "title": "SBU type", + "search_quantity": "results.material.topology.sbu_type", "type": "terms", + "scale": "linear", + "show_input": true, + "showinput": true, + "title": "SBU type", "layout": { "lg": { "h": 9, @@ -3337,13 +8542,22 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.material.topology.sbu_type", - "scale": "linear", - "showinput": true + } }, { "type": "histogram", + "show_input": true, + "showinput": true, + "x": { + "search_quantity": "results.material.topology.pore_limiting_diameter", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "n_bins": 30, + "nbins": 30, "layout": { "lg": { "h": 5, @@ -3385,21 +8599,22 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, + } + }, + { + "type": "histogram", + "show_input": true, + "showinput": true, "x": { - "quantity": "results.material.topology.pore_limiting_diameter", + "search_quantity": "results.material.topology.largest_cavity_diameter", "scale": "linear" }, "y": { "scale": "linear" }, - "scale": "linear", - "autorange": true, - "showinput": true, - "nbins": 30 - }, - { - "type": "histogram", + "autorange": false, + "n_bins": 30, + "nbins": 30, "layout": { "lg": { "h": 5, @@ -3441,21 +8656,22 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, + } + }, + { + "type": "histogram", + "show_input": true, + "showinput": true, "x": { - "quantity": "results.material.topology.largest_cavity_diameter", + "search_quantity": "results.material.topology.accessible_surface_area", "scale": "linear" }, "y": { "scale": "linear" }, - "scale": "linear", - "autorange": true, - "showinput": true, - "nbins": 30 - }, - { - "type": "histogram", + "autorange": false, + "n_bins": 30, + "nbins": 30, "layout": { "lg": { "h": 5, @@ -3497,21 +8713,22 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, + } + }, + { + "type": "histogram", + "show_input": true, + "showinput": true, "x": { - "quantity": "results.material.topology.accessible_surface_area", + "search_quantity": "results.material.topology.void_fraction", "scale": "linear" }, "y": { "scale": "linear" }, - "scale": "linear", - "autorange": true, - "showinput": true, - "nbins": 30 - }, - { - "type": "histogram", + "autorange": false, + "n_bins": 30, + "nbins": 30, "layout": { "lg": { "h": 5, @@ -3553,18 +8770,7 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "x": { - "quantity": "results.material.topology.void_fraction", - "scale": "linear" - }, - "y": { - "scale": "linear" - }, - "scale": "linear", - "autorange": true, - "showinput": true, - "nbins": 30 + } } ] }, @@ -3781,59 +8987,764 @@ window.nomadEnv = { "enabled": true } }, - "filter_menus": { - "options": { - "material": { - "label": "Absorber Material", - "level": 0, - "size": "s" + "menu": { + "width": 12, + "show_header": true, + "title": "Filters", + "type": "menu", + "size": "sm", + "indentation": 0, + "items": [ + { + "width": 12, + "show_header": true, + "title": "Absorber Material", + "type": "menu", + "size": "md" }, - "elements": { - "label": "Elements / Formula", - "level": 1, - "size": "xl" + { + "width": 12, + "show_header": true, + "title": "Elements / Formula", + "type": "menu", + "size": "xxl", + "indentation": 1, + "items": [ + { + "type": "periodic_table", + "search_quantity": "results.material.elements", + "scale": "linear", + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_hill", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_iupac", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_reduced", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.chemical_formula_anonymous", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 6, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.material.n_elements", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] }, - "structure": { - "label": "Structure / Symmetry", - "level": 1, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Structure / Symmetry", + "type": "menu", + "size": "md", + "indentation": 1, + "items": [ + { + "search_quantity": "results.material.structural_type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.bravais_lattice", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.crystal_system", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 2, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.space_group_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.structure_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.strukturbericht_designation", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.point_group", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.hall_symbol", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.symmetry.prototype_aflow_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "electronic": { - "label": "Electronic Properties", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Electronic Properties", + "type": "menu", + "size": "md", + "items": [ + { + "search_quantity": "electronic_properties", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic.band_gap", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 2, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.band_structure_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + }, + { + "width": 12, + "show_header": true, + "type": "nested_object", + "path": "results.properties.electronic.dos_electronic", + "items": [ + { + "search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] + } + ] }, - "solarcell": { - "label": "Solar Cell Properties", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Solar Cell Properties", + "type": "menu", + "size": "md", + "items": [ + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.efficiency", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.fill_factor", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.open_circuit_voltage", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.short_circuit_current_density", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.illumination_intensity", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.device_area", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.device_architecture", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.device_stack", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.absorber", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.absorber_fabrication", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.electron_transport_layer", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.hole_transport_layer", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.substrate", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.properties.optoelectronic.solar_cell.back_contact", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "eln": { - "label": "Electronic Lab Notebook", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Electronic Lab Notebook", + "type": "menu", + "size": "md", + "items": [ + { + "search_quantity": "results.eln.sections", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.tags", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.methods", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.instruments", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.names", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.descriptions", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.eln.lab_ids", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "custom_quantities": { - "label": "User Defined Quantities", - "level": 0, - "size": "l" + { + "width": 12, + "show_header": true, + "title": "User Defined Quantities", + "type": "menu", + "size": "xl", + "items": [ + { + "width": 12, + "show_header": true, + "type": "custom_quantities" + } + ] }, - "author": { - "label": "Author / Origin / Dataset", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Author / Origin / Dataset", + "type": "menu", + "size": "lg", + "items": [ + { + "search_quantity": "authors.name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "type": "histogram", + "show_input": true, + "x": { + "search_quantity": "upload_create_time", + "scale": "linear" + }, + "y": { + "scale": "linear" + }, + "autorange": false, + "width": 12, + "show_header": true, + "show_statistics": true + }, + { + "search_quantity": "external_db", + "type": "terms", + "scale": "linear", + "show_input": false, + "width": 12, + "show_header": true, + "options": 5, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.doi", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + } + ] }, - "metadata": { - "label": "Visibility / IDs / Schema", - "level": 0, - "size": "s" + { + "width": 12, + "show_header": true, + "title": "Visibility / IDs / Schema", + "type": "menu", + "size": "md", + "items": [ + { + "width": 12, + "show_header": true, + "type": "visibility" + }, + { + "search_quantity": "entry_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "upload_name", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "results.material.material_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "search_quantity": "datasets.dataset_id", + "type": "terms", + "scale": "linear", + "show_input": true, + "width": 12, + "show_header": true, + "options": 0, + "n_columns": 1, + "sort_static": true, + "show_statistics": true + }, + { + "width": 12, + "show_header": true, + "type": "definitions" + } + ] }, - "optimade": { - "label": "Optimade", - "level": 0, - "size": "m" + { + "width": 12, + "show_header": true, + "title": "Optimade", + "type": "menu", + "size": "lg", + "items": [ + { + "width": 12, + "show_header": true, + "type": "optimade" + } + ] } - } + ] }, "filters": { "include": [ @@ -3845,10 +9756,22 @@ window.nomadEnv = { "combine" ] }, + "search_quantities": { + "include": [ + "*#perovskite_solar_cell_database.schema.PerovskiteSolarCell" + ], + "exclude": [ + "mainfile", + "entry_name", + "combine" + ] + }, "dashboard": { "widgets": [ { "type": "periodictable", + "search_quantity": "results.material.elements", + "scale": "linear", "layout": { "lg": { "h": 8, @@ -3890,9 +9813,7 @@ window.nomadEnv = { "minH": 8, "minW": 12 } - }, - "quantity": "results.material.elements", - "scale": "linear" + } }, { "type": "scatterplot", @@ -3939,18 +9860,18 @@ window.nomadEnv = { } }, "x": { - "quantity": "results.properties.optoelectronic.solar_cell.open_circuit_voltage", + "search_quantity": "results.properties.optoelectronic.solar_cell.open_circuit_voltage", "scale": "linear" }, "y": { "title": "Efficiency (%)", - "quantity": "results.properties.optoelectronic.solar_cell.efficiency", + "search_quantity": "results.properties.optoelectronic.solar_cell.efficiency", "scale": "linear" }, "markers": { "color": { "unit": "mA/cm^2", - "quantity": "results.properties.optoelectronic.solar_cell.short_circuit_current_density", + "search_quantity": "results.properties.optoelectronic.solar_cell.short_circuit_current_density", "scale": "linear" } }, @@ -4002,17 +9923,17 @@ window.nomadEnv = { } }, "x": { - "quantity": "results.properties.optoelectronic.solar_cell.open_circuit_voltage", + "search_quantity": "results.properties.optoelectronic.solar_cell.open_circuit_voltage", "scale": "linear" }, "y": { "title": "Efficiency (%)", - "quantity": "results.properties.optoelectronic.solar_cell.efficiency", + "search_quantity": "results.properties.optoelectronic.solar_cell.efficiency", "scale": "linear" }, "markers": { "color": { - "quantity": "results.properties.optoelectronic.solar_cell.device_architecture", + "search_quantity": "results.properties.optoelectronic.solar_cell.device_architecture", "scale": "linear" } }, @@ -4020,7 +9941,11 @@ window.nomadEnv = { "autorange": true }, { + "search_quantity": "results.properties.optoelectronic.solar_cell.device_stack", "type": "terms", + "scale": "linear", + "show_input": true, + "showinput": true, "layout": { "lg": { "h": 6, @@ -4062,13 +9987,22 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.properties.optoelectronic.solar_cell.device_stack", - "scale": "linear", - "showinput": true + } }, { "type": "histogram", + "show_input": true, + "showinput": true, + "x": { + "search_quantity": "results.properties.optoelectronic.solar_cell.illumination_intensity", + "scale": "linear" + }, + "y": { + "scale": "1/4" + }, + "autorange": true, + "n_bins": 30, + "nbins": 30, "layout": { "lg": { "h": 4, @@ -4110,21 +10044,14 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "x": { - "quantity": "results.properties.optoelectronic.solar_cell.illumination_intensity", - "scale": "linear" - }, - "y": { - "scale": "1/4" - }, - "scale": "linear", - "autorange": true, - "showinput": true, - "nbins": 30 + } }, { + "search_quantity": "results.properties.optoelectronic.solar_cell.absorber_fabrication", "type": "terms", + "scale": "linear", + "show_input": true, + "showinput": true, "layout": { "lg": { "h": 6, @@ -4166,14 +10093,23 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.properties.optoelectronic.solar_cell.absorber_fabrication", - "scale": "linear", - "showinput": true + } }, { - "title": "Band gap", "type": "histogram", + "show_input": false, + "showinput": false, + "x": { + "search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", + "scale": "linear" + }, + "y": { + "scale": "1/4" + }, + "autorange": false, + "n_bins": 30, + "nbins": 30, + "title": "Band gap", "layout": { "lg": { "h": 4, @@ -4215,21 +10151,14 @@ window.nomadEnv = { "minH": 3, "minW": 8 } - }, - "x": { - "quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", - "scale": "linear" - }, - "y": { - "scale": "1/4" - }, - "scale": "linear", - "autorange": false, - "showinput": false, - "nbins": 30 + } }, { + "search_quantity": "results.properties.optoelectronic.solar_cell.electron_transport_layer", "type": "terms", + "scale": "linear", + "show_input": true, + "showinput": true, "layout": { "lg": { "h": 6, @@ -4271,13 +10200,14 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.properties.optoelectronic.solar_cell.electron_transport_layer", - "scale": "linear", - "showinput": true + } }, { + "search_quantity": "results.properties.optoelectronic.solar_cell.hole_transport_layer", "type": "terms", + "scale": "linear", + "show_input": true, + "showinput": true, "layout": { "lg": { "h": 6, @@ -4319,10 +10249,7 @@ window.nomadEnv = { "minH": 3, "minW": 3 } - }, - "quantity": "results.properties.optoelectronic.solar_cell.hole_transport_layer", - "scale": "linear", - "showinput": true + } } ] }, diff --git a/nomad/config/defaults.yaml b/nomad/config/defaults.yaml index da60ca06e24183d3d0136f1ffb1aaa2a2ac35e93..53820b6b7fe4e20e0430b8236fdaa893ea74119d 100644 --- a/nomad/config/defaults.yaml +++ b/nomad/config/defaults.yaml @@ -857,6 +857,7 @@ ui: optimade: label: Optimade size: m + calculations: label: Calculations path: calculations @@ -983,7 +984,7 @@ ui: xxl: {h: 9, minH: 3, minW: 3, w: 6, x: 30, y: 0} quantity: results.material.symmetry.space_group_symbol scale: linear - showinput: true + show_input: true type: terms - layout: lg: {h: 6, minH: 3, minW: 3, w: 5, x: 19, y: 0} @@ -993,7 +994,7 @@ ui: xxl: {h: 9, minH: 3, minW: 3, w: 6, x: 19, y: 0} quantity: results.material.structural_type scale: log - showinput: false + show_input: false type: terms - layout: lg: {h: 6, minH: 3, minW: 3, w: 5, x: 14, y: 0} @@ -1003,7 +1004,7 @@ ui: xxl: {h: 9, minH: 3, minW: 3, w: 6, x: 13, y: 0} quantity: results.method.simulation.program_name scale: log - showinput: true + show_input: true type: terms - layout: lg: {h: 5, minH: 3, minW: 3, w: 5, x: 14, y: 6} @@ -1013,7 +1014,7 @@ ui: xxl: {h: 9, minH: 3, minW: 3, w: 5, x: 25, y: 0} quantity: results.material.symmetry.crystal_system scale: linear - showinput: false + show_input: false type: terms materials: label: Materials diff --git a/nomad/config/models/ui.py b/nomad/config/models/ui.py index bc58cbb0a3111d8326ea8f8f62819c26a77f3d66..d92e549ee9c8a87a8241c292ffb830fbccd6a181 100644 --- a/nomad/config/models/ui.py +++ b/nomad/config/models/ui.py @@ -31,6 +31,21 @@ from .common import ( ) +class ScaleEnum(str, Enum): + LINEAR = 'linear' + LOG = 'log' + # TODO: The following should possibly be deprecated. + POW1 = 'linear' + POW2 = '1/2' + POW4 = '1/4' + POW8 = '1/8' + + +class ScaleEnumPlot(str, Enum): + LINEAR = 'linear' + LOG = 'log' + + class UnitSystemUnit(ConfigBaseModel): definition: str = Field( description=""" @@ -351,10 +366,12 @@ class Rows(ConfigBaseModel): selection: RowSelection +# Deprecated class FilterMenuActionEnum(str, Enum): CHECKBOX = 'checkbox' +# Deprecated class FilterMenuAction(ConfigBaseModel): """Contains definition for an action in the filter menu.""" @@ -362,12 +379,14 @@ class FilterMenuAction(ConfigBaseModel): label: str = Field(description='Label to show.') +# Deprecated class FilterMenuActionCheckbox(FilterMenuAction): """Contains definition for checkbox action in the filter menu.""" quantity: str = Field(description='Targeted quantity') +# Deprecated class FilterMenuActions(Options): """Contains filter menu action definitions and controls their availability.""" @@ -376,6 +395,7 @@ class FilterMenuActions(Options): ) +# Deprecated class FilterMenuSizeEnum(str, Enum): S = 's' M = 'm' @@ -383,6 +403,7 @@ class FilterMenuSizeEnum(str, Enum): XL = 'xl' +# Deprecated class FilterMenu(ConfigBaseModel): """Defines the layout and functionality for a filter menu.""" @@ -394,6 +415,7 @@ class FilterMenu(ConfigBaseModel): actions: Optional[FilterMenuActions] +# Deprecated class FilterMenus(Options): """Contains filter menu definitions and controls their availability.""" @@ -402,11 +424,421 @@ class FilterMenus(Options): ) -class Filters(OptionsGlob): - """Controls the availability of filters in the app. Filters are pieces of - (meta)info than can be queried in the search interface of the app, but also - targeted in the rest of the app configuration. The `include` and `exlude` - attributes can use glob syntax to target metainfo, e.g. `results.*` or +# NOTE: Once the old power scaling options (1/2, 1/4, 1/8) are deprecated, the +# axis models here can be simplified. +class AxisScale(ConfigBaseModel): + """Basic configuration for a plot axis.""" + + scale: Optional[ScaleEnum] = Field( + ScaleEnum.LINEAR, + description="""Defines the axis scaling. Defaults to linear scaling.""", + ) + + +class AxisQuantity(ConfigBaseModel): + """Configuration for a plot axis.""" + + title: Optional[str] = Field(description="""Custom title to show for the axis.""") + unit: Optional[str] = Field( + description="""Custom unit used for displaying the values.""" + ) + quantity: Optional[str] = Field( + deprecated='The "quantity" field is deprecated, use "search_quantity" instead.' + ) + search_quantity: str = Field( + description=""" + Path of the targeted search quantity. Note that you can most of the features + JMESPath syntax here to further specify a selection of values. This + becomes especially useful when dealing with repeated sections or + statistical values. + """ + ) + + @root_validator(pre=True) + def _validate(cls, values): + # Backwards compatibility for quantity. + quantity = values.get('quantity') + search_quantity = values.get('search_quantity') + if quantity is not None and search_quantity is None: + values['search_quantity'] = quantity + del values['quantity'] + + return values + + +class Axis(AxisScale, AxisQuantity): + """Configuration for a plot axis with limited scaling options.""" + + +class TermsBase(ConfigBaseModel): + """Base model for configuring terms components.""" + + quantity: Optional[str] = Field( + deprecated='The "quantity" field is deprecated, use "search_quantity" instead.' + ) + search_quantity: str = Field(description='The targeted search quantity.') + type: Literal['terms'] = Field( + description='Set as `terms` to get this type.', + ) + scale: ScaleEnum = Field(ScaleEnum.LINEAR, description='Statistics scaling.') + show_input: bool = Field(True, description='Whether to show text input field.') + showinput: Optional[bool] = Field( + deprecated='The "showinput" field is deprecated, use "show_input" instead.' + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'terms' + + # Backwards compatibility for showinput. + showinput = values.get('showinput') + if showinput is not None: + values['show_input'] = showinput + + # Backwards compatibility for quantity. + quantity = values.get('quantity') + search_quantity = values.get('search_quantity') + if quantity is not None and search_quantity is None: + values['search_quantity'] = quantity + del values['quantity'] + + return values + + +class HistogramBase(ConfigBaseModel): + """Base model for configuring histogram components.""" + + type: Literal['histogram'] = Field( + description='Set as `histogram` to get this widget type.' + ) + quantity: Optional[str] = Field( + deprecated='The "quantity" field is deprecated, use "x.search_quantity" instead.' + ) + scale: Optional[ScaleEnum] = Field( + deprecated='The "scale" field is deprecated, use "y.scale" instead.' + ) + show_input: bool = Field(True, description='Whether to show text input field.') + showinput: Optional[bool] = Field( + deprecated='The "showinput" field is deprecated, use "show_input" instead.' + ) + + x: Union[Axis, str] = Field( + description='Configures the information source and display options for the x-axis.' + ) + y: Union[AxisScale, str] = Field( + description='Configures the information source and display options for the y-axis.' + ) + autorange: bool = Field( + False, + description='Whether to automatically set the range according to the data limits.', + ) + n_bins: Optional[int] = Field( + description=""" + Maximum number of histogram bins. Notice that the actual number of bins + may be smaller if there are fewer data items available. + """ + ) + nbins: Optional[int] = Field( + deprecated='The "nbins" field is deprecated, use "n_bins" instead.' + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'histogram' + + # Backwards compatibility for nbins.""" + nbins = values.get('nbins') + if nbins is not None: + values['n_bins'] = nbins + + # Backwards compatibility for showinput.""" + showinput = values.get('showinput') + if showinput is not None: + values['show_input'] = showinput + + # x backwards compatibility + x = values.get('x', {}) + if isinstance(x, str): + x = {'search_quantity': x} + if isinstance(x, dict): + quantity = values.get('quantity') + if quantity and not x.get('search_quantity'): + x['search_quantity'] = quantity + del values['quantity'] + values['x'] = x + + # y backwards compatibility + y = values.get('y', {}) + if isinstance(y, dict): + scale = values.get('scale') + if scale: + y['scale'] = scale + del values['scale'] + values['y'] = y + + return values + + +class PeriodicTableBase(ConfigBaseModel): + """Base model for configuring periodic table components.""" + + type: Literal['periodic_table'] = Field( + description='Set as `periodic_table` to get this widget type.' + ) + quantity: Optional[str] = Field( + deprecated='The "quantity" field is deprecated, use "search_quantity" instead.' + ) + search_quantity: str = Field(description='The targeted search quantity.') + scale: Optional[ScaleEnum] = Field( + ScaleEnum.LINEAR, description='Statistics scaling.' + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'periodic_table' + + # Backwards compatibility for quantity. + quantity = values.get('quantity') + search_quantity = values.get('search_quantity') + if quantity is not None and search_quantity is None: + values['search_quantity'] = quantity + del values['quantity'] + + return values + + +class MenuSizeEnum(str, Enum): + XS = 'xs' + SM = 'sm' + MD = 'md' + LG = 'lg' + XL = 'xl' + XXL = 'xxl' + + +class MenuItem(ConfigBaseModel): + width: int = Field( + 12, + description='Width of the item, 12 means maximum width. Note that the menu size can be changed.', + ) + show_header: bool = Field(True, description='Whether to show the header.') + title: Optional[str] = Field(description='Custom item title.') + + +class MenuItemOption(ConfigBaseModel): + """Represents an option shown for a filter.""" + + label: Optional[str] = Field(description='The label to show for this option.') + description: Optional[str] = Field( + description='Detailed description for this option.' + ) + + +class MenuItemTerms(MenuItem, TermsBase): + """Menu item that shows a list of text values from e.g. `str` or `MEnum` + quantities. + """ + + options: Optional[Union[int, Dict[str, MenuItemOption]]] = Field( + description=""" + Used to control the displayed options: + + - If not specified, sensible default options are shown based on the + definition. For enum fields all of the defined options are shown, + whereas for generic string fields the top 5 options are shown. + + - If a number is specified, that many options are dynamically fetched + in order of occurrence. Set to 0 to completely disable options. + + - If a dictionary of str + MenuItemOption pairs is given, only these + options will be shown. + """ + ) + n_columns: int = Field( + 1, + description='The number of columns to use when displaying the options.', + ) + sort_static: bool = Field( + True, + description=""" + Whether to sort static options by their occurrence in the data. Options + are static if they are read from the enum options of the field or if + they are explicitly given as a dictionary in 'options'. + """, + ) + show_statistics: bool = Field( + True, description='Whether to show statistics for the options.' + ) + + +class MenuItemHistogram(MenuItem, HistogramBase): + """Menu item that shows a histogram for numerical or timestamp quantities.""" + + show_statistics: bool = Field( + True, description='Whether to show the full histogram, or just a range slider.' + ) + + +class MenuItemPeriodicTable(MenuItem, PeriodicTableBase): + """Menu item that shows a periodic table built from values stored into a + text quantity. + """ + + show_statistics: bool = Field( + True, description='Whether to show statistics for the options.' + ) + + +class MenuItemVisibility(MenuItem): + """Menu item that shows a radio button that can be used to change the visiblity.""" + + type: Literal['visibility'] = Field( + description='Set as `visibility` to get this menu item type.', + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'visibility' + return values + + +class MenuItemDefinitions(MenuItem): + """Menu item that shows a tree for filtering data by the presence of definitions.""" + + type: Literal['definitions'] = Field( + description='Set as `definitions` to get this menu item type.', + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'definitions' + return values + + +class MenuItemOptimade(MenuItem): + """Menu item that shows a dialog for entering OPTIMADE queries.""" + + type: Literal['optimade'] = Field( + description='Set as `optimade` to get this menu item type.', + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'optimade' + return values + + +class MenuItemCustomQuantities(MenuItem): + """Menu item that shows a search dialog for filtering by custom quantities + coming from all different custom schemas, including YAML and Python schemas. + Will only show quantities that have been populated in the data. + """ + + type: Literal['custom_quantities'] = Field( + description='Set as `custom_quantities` to get this menu item type.', + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'custom_quantities' + return values + + +# The 'discriminated union' feature of Pydantic is used here: +# https://docs.pydantic.dev/usage/types/#discriminated-unions-aka-tagged-unions +MenuItemTypeNested = Annotated[ + Union[ + MenuItemTerms, + MenuItemHistogram, + MenuItemPeriodicTable, + MenuItemVisibility, + MenuItemDefinitions, + MenuItemOptimade, + MenuItemCustomQuantities, + ], + Field(discriminator='type'), +] + + +class MenuItemNestedObject(MenuItem): + """Menu item that can be used to wrap several subitems into a nested object. + By wrapping items with this class the query for them is performed as an + Elasticsearch nested query: + https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html. + Note that you cannot yet use nested queries for search quantities + originating from custom schemas. + """ + + type: Literal['nested_object'] = Field( + description='Set as `nested_object` to get this menu item type.', + ) + path: str = Field( + description='Path of the nested object. Typically a section name.' + ) + items: Optional[List[MenuItemTypeNested]] = Field( + description='Items that are grouped by this nested object.' + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'nested_object' + return values + + +# The 'discriminated union' feature of Pydantic is used here: +# https://docs.pydantic.dev/usage/types/#discriminated-unions-aka-tagged-unions +MenuItemType = Annotated[ + Union[ + MenuItemTerms, + MenuItemHistogram, + MenuItemPeriodicTable, + MenuItemNestedObject, + MenuItemVisibility, + MenuItemDefinitions, + MenuItemOptimade, + MenuItemCustomQuantities, + 'Menu', + ], + Field(discriminator='type'), +] + + +class Menu(MenuItem): + """Defines a menu that is shown on the left side of the search interface. + Menus have a controllable width, and contains items. Items in the menu are + displayed on a 12-based grid and you can control the width of each item by + using the `width` field. You can also nest menus within each other. + """ + + type: Literal['menu'] = Field( + description='Set as `nested_object` to get this menu item type.', + ) + size: Optional[Union[MenuSizeEnum, str]] = Field( + MenuSizeEnum.SM, + description=""" + Size of the menu. Either use presets as defined by MenuSizeEnum, + or then provide valid CSS widths. + """, + ) + indentation: Optional[int] = Field(0, description='Indentation level for the menu.') + items: Optional[List[MenuItemType]] = Field( + description='List of items in the menu.' + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'menu' + return values + + +class SearchQuantities(OptionsGlob): + """Controls the quantities that are available in the search interface. + Search quantities correspond to pieces of information that can be queried in + the search interface of the app, but also targeted in the rest of the app + configuration. You can load quantities from custom schemas as search + quantities, but note that not all quantities will be loaded: only scalar + values are supported at the moment. The `include` and `exlude` attributes + can use glob syntax to target metainfo, e.g. `results.*` or `*.#myschema.schema.MySchema`. """ @@ -422,6 +854,10 @@ class Filters(OptionsGlob): ) +class Filters(SearchQuantities): + """Alias for SearchQuantities.""" + + class SearchSyntaxes(ConfigBaseModel): """Controls the availability of different search syntaxes. These syntaxes determine how raw user input in e.g. the search bar is parsed into queries @@ -455,21 +891,6 @@ class Layout(ConfigBaseModel): minW: Optional[int] = Field(3, description='Minimum width in grid units.') -class ScaleEnum(str, Enum): - LINEAR = 'linear' - LOG = 'log' - # TODO: The following should possibly be deprecated. - POW1 = 'linear' - POW2 = '1/2' - POW4 = '1/4' - POW8 = '1/8' - - -class ScaleEnumPlot(str, Enum): - LINEAR = 'linear' - LOG = 'log' - - class BreakpointEnum(str, Enum): SM = 'sm' MD = 'md' @@ -478,38 +899,6 @@ class BreakpointEnum(str, Enum): XXL = 'xxl' -# NOTE: Once the old power scaling options (1/2, 1/4, 1/8) are deprecated, the -# axis models here can be simplified. -class AxisScale(ConfigBaseModel): - """Basic configuration for a plot axis.""" - - scale: Optional[ScaleEnum] = Field( - ScaleEnum.LINEAR, - description="""Defines the axis scaling. Defaults to linear scaling.""", - ) - - -class AxisQuantity(ConfigBaseModel): - """Configuration for a plot axis.""" - - title: Optional[str] = Field(description="""Custom title to show for the axis.""") - unit: Optional[str] = Field( - description="""Custom unit used for displaying the values.""" - ) - quantity: str = Field( - description=""" - Path of the targeted quantity. Note that you can most of the features - JMESPath syntax here to further specify a selection of values. This - becomes especially useful when dealing with repeated sections or - statistical values. - """ - ) - - -class Axis(AxisScale, AxisQuantity): - """Configuration for a plot axis with limited scaling options.""" - - class AxisLimitedScale(AxisQuantity): """Configuration for a plot axis with limited scaling options.""" @@ -542,91 +931,56 @@ class Widget(ConfigBaseModel): ) -class WidgetTerms(Widget): +class WidgetTerms(Widget, TermsBase): """Terms widget configuration.""" type: Literal['terms'] = Field( - 'terms', description='Set as `terms` to get this widget type.' + description='Set as `terms` to get this type.', ) - quantity: str = Field(description='Targeted quantity.') - scale: ScaleEnum = Field(description='Statistics scaling.') - showinput: bool = Field(True, description='Whether to show text input field.') -class WidgetHistogram(Widget): +class WidgetHistogram(Widget, HistogramBase): """Histogram widget configuration.""" type: Literal['histogram'] = Field( - 'histogram', description='Set as `histogram` to get this widget type.' - ) - quantity: Optional[str] = Field( - description='Targeted quantity. Note that this field is deprecated and `x` should be used instead.' - ) - x: Union[Axis, str] = Field( - description='Configures the information source and display options for the x-axis.' - ) - y: Union[AxisScale, str] = Field( - description='Configures the information source and display options for the y-axis.' - ) - scale: Optional[ScaleEnum] = Field( - ScaleEnum.LINEAR, description='Statistics scaling.' + description='Set as `histogram` to get this type.', ) - autorange: bool = Field( - True, - description='Whether to automatically set the range according to the data limits.', - ) - showinput: bool = Field( - True, - description='Whether to show input text fields for minimum and maximum value.', + + +class WidgetPeriodicTable(Widget, PeriodicTableBase): + """Periodic table widget configuration.""" + + type: Literal['periodic_table'] = Field( + description='Set as `periodic_table` to get this type.', ) - nbins: int = Field( - description=""" - Maximum number of histogram bins. Notice that the actual number of bins - may be smaller if there are fewer data items available. - """ + + +class WidgetPeriodicTableDeprecated(WidgetPeriodicTable): + """Deprecated copy of WidgetPeriodicTable with a misspelled type.""" + + type: Literal['periodictable'] = Field( # type: ignore[assignment] + description='Set as `periodictable` to get this widget type.' ) @root_validator(pre=True) def _validate(cls, values): - """Ensures backwards compatibility for quantity and scale.""" - # X-axis - x = values.get('x', {}) - if isinstance(x, str): - x = {'quantity': x} - if isinstance(x, dict): - quantity = values.get('quantity') - if quantity and not x.get('quantity'): - x['quantity'] = quantity - del values['quantity'] - values['x'] = x + values['type'] = 'periodictable' - # Y-axis - y = values.get('y', {}) - if isinstance(y, dict): - scale = values.get('scale') - if scale: - y['scale'] = scale - del values['scale'] - values['y'] = y + # Backwards compatibility for quantity. + quantity = values.get('quantity') + search_quantity = values.get('search_quantity') + if quantity is not None and search_quantity is None: + values['search_quantity'] = quantity + del values['quantity'] return values -class WidgetPeriodicTable(Widget): - """Periodic table widget configuration.""" - - type: Literal['periodictable'] = Field( - 'periodictable', description='Set as `periodictable` to get this widget type.' - ) - quantity: str = Field(description='Targeted quantity.') - scale: ScaleEnum = Field(description='Statistics scaling.') - - class WidgetScatterPlot(Widget): """Scatter plot widget configuration.""" - type: Literal['scatterplot'] = Field( - 'scatterplot', description='Set as `scatterplot` to get this widget type.' + type: Literal['scatter_plot'] = Field( + description='Set as `scatter_plot` to get this widget type.' ) x: Union[AxisLimitedScale, str] = Field( description='Configures the information source and display options for the x-axis.' @@ -647,7 +1001,7 @@ class WidgetScatterPlot(Widget): 1000, description=""" Maximum number of entries to fetch. Notice that the actual number may be - more of less, depending on how many entries exist and how many of the + more or less, depending on how many entries exist and how many of the requested values each entry contains. """, ) @@ -658,24 +1012,50 @@ class WidgetScatterPlot(Widget): @root_validator(pre=True) def _validate(cls, values): - """Ensures backwards compatibility of x, y, and color.""" + values['type'] = 'scatter_plot' + + # color backwards compatibility color = values.get('color') if color is not None: - values['markers'] = {'color': {'quantity': color}} + values['markers'] = {'color': {'search_quantity': color}} del values['color'] + + # x backwards compatibility x = values.get('x') if isinstance(x, str): - values['x'] = {'quantity': x} + values['x'] = {'search_quantity': x} + + # y backwards compatibility y = values.get('y') if isinstance(y, str): - values['y'] = {'quantity': y} + values['y'] = {'search_quantity': y} + return values + + +class WidgetScatterPlotDeprecated(WidgetScatterPlot): + """Deprecated copy of WidgetScatterPlot with a misspelled type.""" + + type: Literal['scatterplot'] = Field( # type: ignore[assignment] + description='Set as `scatterplot` to get this widget type.' + ) + + @root_validator(pre=True) + def _validate(cls, values): + values['type'] = 'scatterplot' return values # The 'discriminated union' feature of Pydantic is used here: # https://docs.pydantic.dev/usage/types/#discriminated-unions-aka-tagged-unions WidgetAnnotated = Annotated[ - Union[WidgetTerms, WidgetHistogram, WidgetScatterPlot, WidgetPeriodicTable], + Union[ + WidgetTerms, + WidgetHistogram, + WidgetScatterPlot, + WidgetScatterPlotDeprecated, + WidgetPeriodicTable, + WidgetPeriodicTableDeprecated, + ], Field(discriminator='type'), ] @@ -685,7 +1065,7 @@ class Dashboard(ConfigBaseModel): widgets: List[WidgetAnnotated] = Field( description='List of widgets contained in the dashboard.' - ) # type: ignore + ) class ResourceEnum(str, Enum): @@ -723,12 +1103,18 @@ class App(ConfigBaseModel): ), description='Controls the display of entry rows in the results table.', ) - filter_menus: FilterMenus = Field( - description='Filter menus displayed on the left side of the screen.' + menu: Optional[Menu] = Field( + description='Filter menu displayed on the left side of the screen.' + ) + filter_menus: Optional[FilterMenus] = Field( + deprecated='The "filter_menus" field is deprecated, use "menu" instead.' ) filters: Optional[Filters] = Field( - Filters(exclude=['mainfile', 'entry_name', 'combine']), - description='Controls the filters that are available in this app.', + deprecated='The "filters" field is deprecated, use "search_quantities" instead.' + ) + search_quantities: Optional[SearchQuantities] = Field( + SearchQuantities(exclude=['mainfile', 'entry_name', 'combine']), + description='Controls the quantities that are available for search in this app.', ) dashboard: Optional[Dashboard] = Field(description='Default dashboard layout.') filters_locked: Optional[dict] = Field( @@ -764,6 +1150,672 @@ class App(ConfigBaseModel): new_columns.append(column) values['columns'] = new_columns + # Backwards compatibility for Filters + filters = values.get('filters') + if filters and not values.get('search_quantities'): + values['search_quantities'] = filters + + # Backwards compatibility for FilterMenus + filter_menus = values.get('filter_menus') + if isinstance(filter_menus, FilterMenus): + filter_menus = filter_menus.dict() + options = filter_menus.get('options') if filter_menus else None + menus = values.get('menus') + if options and not menus: + items = [] + for key, value in options.items(): + menu = { + 'material': Menu(), + 'elements': Menu( + items=[ + MenuItemPeriodicTable( + search_quantity='results.material.elements', + ), + MenuItemTerms( + search_quantity='results.material.chemical_formula_hill', + width=6, + options=0, + ), + MenuItemTerms( + search_quantity='results.material.chemical_formula_iupac', + width=6, + options=0, + ), + MenuItemTerms( + search_quantity='results.material.chemical_formula_reduced', + width=6, + options=0, + ), + MenuItemTerms( + search_quantity='results.material.chemical_formula_anonymous', + width=6, + options=0, + ), + MenuItemHistogram( + x='results.material.n_elements', + ), + ] + ), + 'structure': Menu( + items=[ + MenuItemTerms( + search_quantity='results.material.structural_type', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.material.symmetry.bravais_lattice', + n_columns=2, + show_input=False, + ), + MenuItemTerms( + search_quantity='results.material.symmetry.crystal_system', + n_columns=2, + show_input=False, + ), + MenuItemTerms( + search_quantity='results.material.symmetry.space_group_symbol', + options=0, + ), + MenuItemTerms( + search_quantity='results.material.symmetry.structure_name', + options=5, + ), + MenuItemTerms( + search_quantity='results.material.symmetry.strukturbericht_designation', + ), + MenuItemTerms( + search_quantity='results.material.symmetry.point_group', + options=0, + ), + MenuItemTerms( + search_quantity='results.material.symmetry.hall_symbol', + options=0, + ), + MenuItemTerms( + search_quantity='results.material.symmetry.prototype_aflow_id', + options=0, + ), + ] + ), + 'method': Menu( + items=[ + MenuItemTerms( + search_quantity='results.method.simulation.program_name', + ), + MenuItemTerms( + search_quantity='results.method.simulation.program_version', + options=0, + ), + ] + ), + 'precision': Menu( + items=[ + MenuItemHistogram( + x='results.method.simulation.precision.k_line_density', + ), + MenuItemTerms( + search_quantity='results.method.simulation.precision.native_tier', + options=0, + ), + MenuItemTerms( + search_quantity='results.method.simulation.precision.basis_set', + options=5, + ), + MenuItemHistogram( + x=Axis( + search_quantity='results.method.simulation.precision.planewave_cutoff', + ) + ), + MenuItemHistogram( + x='results.method.simulation.precision.apw_cutoff', + ), + ] + ), + 'dft': Menu( + items=[ + MenuItemTerms( + search_quantity='results.method.method_name', + show_header=False, + show_input=False, + show_statistics=False, + options={ + 'DFT': MenuItemOption(label='Search DFT entries') + }, + ), + MenuItemTerms( + search_quantity='results.method.simulation.dft.xc_functional_type', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.dft.xc_functional_names', + ), + MenuItemHistogram( + x='results.method.simulation.dft.exact_exchange_mixing_factor', + ), + MenuItemHistogram( + x='results.method.simulation.dft.hubbard_kanamori_model.u_effective', + ), + MenuItemTerms( + search_quantity='results.method.simulation.dft.core_electron_treatment', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.dft.relativity_method', + show_input=False, + ), + ] + ), + 'tb': Menu( + items=[ + MenuItemTerms( + search_quantity='results.method.method_name', + show_header=False, + show_input=False, + show_statistics=False, + options={ + 'TB': MenuItemOption(label='Search TB entries') + }, + ), + MenuItemTerms( + search_quantity='results.method.simulation.tb.type', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.tb.localization_type', + show_input=False, + ), + ] + ), + 'gw': Menu( + items=[ + MenuItemTerms( + search_quantity='results.method.method_name', + show_header=False, + show_input=False, + show_statistics=False, + options={ + 'GW': MenuItemOption(label='Search GW entries') + }, + ), + MenuItemTerms( + search_quantity='results.method.simulation.gw.type', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.gw.starting_point_type', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.gw.basis_set_type', + show_input=False, + ), + ] + ), + 'bse': Menu( + items=[ + MenuItemTerms( + search_quantity='results.method.method_name', + show_header=False, + show_input=False, + show_statistics=False, + options={ + 'BSE': MenuItemOption(label='Search BSE entries') + }, + ), + MenuItemTerms( + search_quantity='results.method.simulation.bse.type', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.bse.solver', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.bse.starting_point_type', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.bse.starting_point_type', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.bse.basis_set_type', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.method.simulation.bse.gw_type', + show_input=False, + ), + ] + ), + 'dmft': Menu( + items=[ + MenuItemTerms( + search_quantity='results.method.method_name', + show_header=False, + show_input=False, + show_statistics=False, + options={ + 'DMFT': MenuItemOption(label='Search DMFT entries') + }, + ), + MenuItemTerms( + search_quantity='results.method.simulation.dmft.impurity_solver_type', + n_columns=2, + show_input=False, + ), + MenuItemHistogram( + x='results.method.simulation.dmft.inverse_temperature', + ), + MenuItemTerms( + search_quantity='results.method.simulation.dmft.magnetic_state', + show_input=False, + ), + MenuItemHistogram( + x='results.method.simulation.dmft.u', + ), + MenuItemHistogram( + x='results.method.simulation.dmft.jh', + ), + MenuItemTerms( + search_quantity='results.method.simulation.dmft.analytical_continuation', + show_input=False, + ), + ] + ), + 'eels': Menu( + items=[ + MenuItemTerms( + search_quantity='results.method.method_name', + show_header=False, + show_input=False, + show_statistics=False, + options={ + 'EELS': MenuItemOption(label='Search EELS entries') + }, + ), + MenuItemNestedObject( + path='results.properties.spectroscopic.spectra.provenance.eels', + items=[ + MenuItemTerms( + search_quantity='results.properties.spectroscopic.spectra.provenance.eels.detector_type' + ), + MenuItemHistogram( + x='results.properties.spectroscopic.spectra.provenance.eels.resolution', + ), + MenuItemHistogram( + x='results.properties.spectroscopic.spectra.provenance.eels.min_energy', + ), + MenuItemHistogram( + x='results.properties.spectroscopic.spectra.provenance.eels.max_energy', + ), + ], + ), + ] + ), + 'workflow': Menu(), + 'molecular_dynamics': Menu( + items=[ + MenuItemNestedObject( + path='results.properties.thermodynamic.trajectory', + items=[ + MenuItemTerms( + search_quantity='results.properties.thermodynamic.trajectory.available_properties', + show_input=False, + options=4, + ), + MenuItemTerms( + search_quantity='results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type', + show_input=False, + options=2, + ), + MenuItemHistogram( + x='results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step', + ), + ], + ), + ] + ), + 'geometry_optimization': Menu( + items=[ + MenuItemTerms( + search_quantity='results.properties.available_properties', + show_header=False, + show_input=False, + show_statistics=False, + options={ + 'geometry_optimization': MenuItemOption( + label='Search geometry optimization entries' + ) + }, + ), + MenuItemNestedObject( + path='results.properties.geometry_optimization', + items=[ + MenuItemHistogram( + x='results.properties.geometry_optimization.final_energy_difference', + ), + MenuItemHistogram( + x='results.properties.geometry_optimization.final_force_maximum', + ), + MenuItemHistogram( + x='results.properties.geometry_optimization.final_displacement_maximum', + ), + ], + ), + ] + ), + 'properties': Menu(), + 'electronic': Menu( + items=[ + MenuItemTerms( + search_quantity='electronic_properties', + show_input=False, + ), + MenuItemNestedObject( + path='results.properties.electronic.band_structure_electronic.band_gap', + items=[ + MenuItemTerms( + search_quantity='results.properties.electronic.band_structure_electronic.band_gap.type', + options=2, + show_input=False, + ), + MenuItemHistogram( + x='results.properties.electronic.band_structure_electronic.band_gap.value', + ), + ], + ), + MenuItemNestedObject( + path='results.properties.electronic.band_structure_electronic', + items=[ + MenuItemTerms( + search_quantity='results.properties.electronic.band_structure_electronic.spin_polarized', + show_input=False, + ), + ], + ), + MenuItemNestedObject( + path='results.properties.electronic.dos_electronic', + items=[ + MenuItemTerms( + search_quantity='results.properties.electronic.dos_electronic.spin_polarized', + show_input=False, + ), + ], + ), + ] + ), + 'vibrational': Menu( + items=[ + MenuItemTerms( + search_quantity='vibrational_properties', + show_input=False, + ), + ] + ), + 'mechanical': Menu( + items=[ + MenuItemTerms( + search_quantity='mechanical_properties', + show_input=False, + ), + MenuItemNestedObject( + path='results.properties.mechanical.bulk_modulus', + items=[ + MenuItemTerms( + search_quantity='results.properties.mechanical.bulk_modulus.type', + ), + MenuItemHistogram( + x='results.properties.mechanical.bulk_modulus.value', + ), + ], + ), + MenuItemNestedObject( + path='results.properties.mechanical.shear_modulus', + items=[ + MenuItemTerms( + search_quantity='results.properties.mechanical.shear_modulus.type', + show_input=False, + ), + MenuItemHistogram( + x='results.properties.mechanical.shear_modulus.value', + ), + ], + ), + MenuItemNestedObject( + path='results.properties.mechanical.energy_volume_curve', + items=[ + MenuItemTerms( + search_quantity='results.properties.mechanical.energy_volume_curve.type', + options=5, + ), + ], + ), + ] + ), + 'usecases': Menu(), + 'solarcell': Menu( + items=[ + MenuItemHistogram( + x='results.properties.optoelectronic.solar_cell.efficiency', + ), + MenuItemHistogram( + x='results.properties.optoelectronic.solar_cell.fill_factor', + ), + MenuItemHistogram( + x='results.properties.optoelectronic.solar_cell.open_circuit_voltage', + ), + MenuItemHistogram( + x='results.properties.optoelectronic.solar_cell.short_circuit_current_density', + ), + MenuItemHistogram( + x='results.properties.optoelectronic.solar_cell.illumination_intensity', + ), + MenuItemHistogram( + x='results.properties.optoelectronic.solar_cell.device_area', + ), + MenuItemTerms( + search_quantity='results.properties.optoelectronic.solar_cell.device_architecture', + ), + MenuItemTerms( + search_quantity='results.properties.optoelectronic.solar_cell.device_stack', + ), + MenuItemTerms( + search_quantity='results.properties.optoelectronic.solar_cell.absorber', + ), + MenuItemTerms( + search_quantity='results.properties.optoelectronic.solar_cell.absorber_fabrication', + ), + MenuItemTerms( + search_quantity='results.properties.optoelectronic.solar_cell.electron_transport_layer', + ), + MenuItemTerms( + search_quantity='results.properties.optoelectronic.solar_cell.hole_transport_layer', + ), + MenuItemTerms( + search_quantity='results.properties.optoelectronic.solar_cell.substrate', + ), + MenuItemTerms( + search_quantity='results.properties.optoelectronic.solar_cell.back_contact', + ), + ] + ), + 'heterogeneouscatalyst': Menu( + items=[ + MenuItemTerms( + search_quantity='results.properties.catalytic.reaction.name', + ), + MenuItemNestedObject( + path='results.properties.catalytic.reaction.reactants', + items=[ + MenuItemTerms( + search_quantity='results.properties.catalytic.reaction.reactants.name', + ), + MenuItemHistogram( + x='results.properties.catalytic.reaction.reactants.conversion', + ), + MenuItemHistogram( + x='results.properties.catalytic.reaction.reactants.gas_concentration_in', + ), + MenuItemHistogram( + x='results.properties.catalytic.reaction.reactants.gas_concentration_out', + ), + ], + ), + MenuItemNestedObject( + path='results.properties.catalytic.reaction.products', + items=[ + MenuItemTerms( + search_quantity='results.properties.catalytic.reaction.products.name', + ), + MenuItemHistogram( + x='results.properties.catalytic.reaction.products.selectivity', + ), + MenuItemHistogram( + x='results.properties.catalytic.reaction.products.gas_concentration_out', + ), + ], + ), + MenuItemHistogram( + x='results.properties.catalytic.reaction.reaction_conditions.temperature', + ), + MenuItemNestedObject( + path='results.properties.catalytic.catalyst', + items=[ + MenuItemTerms( + search_quantity='results.properties.catalytic.catalyst.catalyst_type', + ), + MenuItemTerms( + search_quantity='results.properties.catalytic.catalyst.preparation_method', + ), + MenuItemTerms( + search_quantity='results.properties.catalytic.catalyst.catalyst_name', + ), + MenuItemTerms( + search_quantity='results.properties.catalytic.catalyst.characterization_methods', + ), + MenuItemHistogram( + x='results.properties.catalytic.catalyst.surface_area', + ), + ], + ), + ] + ), + 'author': Menu( + items=[ + MenuItemTerms( + search_quantity='authors.name', + options=0, + ), + MenuItemHistogram( + x='upload_create_time', + ), + MenuItemTerms( + search_quantity='external_db', + options=5, + show_input=False, + ), + MenuItemTerms( + search_quantity='datasets.dataset_name', + ), + MenuItemTerms( + search_quantity='datasets.doi', + options=0, + ), + ] + ), + 'metadata': Menu( + items=[ + MenuItemVisibility(), + MenuItemTerms( + search_quantity='entry_id', + options=0, + ), + MenuItemTerms( + search_quantity='upload_id', + options=0, + ), + MenuItemTerms( + search_quantity='upload_name', + options=0, + ), + MenuItemTerms( + search_quantity='results.material.material_id', + options=0, + ), + MenuItemTerms( + search_quantity='datasets.dataset_id', + options=0, + ), + MenuItemDefinitions(), + ] + ), + 'optimade': Menu(items=[MenuItemOptimade()]), + 'eln': Menu( + items=[ + MenuItemTerms( + search_quantity='results.eln.sections', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.eln.tags', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.eln.methods', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.eln.instruments', + show_input=False, + ), + MenuItemTerms( + search_quantity='results.eln.names', + options=0, + ), + MenuItemTerms( + search_quantity='results.eln.descriptions', + options=0, + ), + MenuItemTerms( + search_quantity='results.eln.lab_ids', + options=0, + ), + ] + ), + 'custom_quantities': Menu(items=[MenuItemCustomQuantities()]), + 'combine': MenuItemTerms( + search_quantity='combine', + options={ + True: MenuItemOption( + label='Combine results from several entries', + description='If selected, your filters may be matched from several entries that contain the same material. When unchecked, the material has to have a single entry that matches all your filters.', + ) + }, + show_header=False, + show_input=False, + show_statistics=False, + ), + }.get(key) + if not menu: + continue + size = value.get('size') + new_size = { + FilterMenuSizeEnum.S: MenuSizeEnum.MD, + FilterMenuSizeEnum.M: MenuSizeEnum.LG, + FilterMenuSizeEnum.L: MenuSizeEnum.XL, + FilterMenuSizeEnum.XL: MenuSizeEnum.XXL, + None: MenuSizeEnum.MD, + }.get(size) + if isinstance(menu, Menu): + menu.title = value.get('label') + if new_size: + menu.size = new_size + menu.indentation = value.get('level') + items.append(menu) + del values['filter_menus'] + values['menu'] = Menu(title='Filters', size=MenuSizeEnum.SM, items=items) + return values diff --git a/nomad/datamodel/results.py b/nomad/datamodel/results.py index 51e551cef620cbaad202d732ac40278610e1fd4e..71e28f97512fabf046891102c6235a946bcd3713 100644 --- a/nomad/datamodel/results.py +++ b/nomad/datamodel/results.py @@ -1994,7 +1994,7 @@ class DMFT(MSection): a_elasticsearch=[Elasticsearch(material_entry_type)] ) analytical_continuation = Quantity( - type=MEnum('Pade', 'MaxEnt', 'SVD', ''), + type=MEnum('Pade', 'MaxEnt', 'SVD', 'Stochastic'), shape=[], description=""" Analytical continuation used to continuate the imaginary space Green's functions into