Commit 40f8745e authored by Alvin Noe Ladines's avatar Alvin Noe Ladines
Browse files

Implemented fixes in issue #312

parent e1952263
Pipeline #72446 passed with stages
in 17 minutes and 27 seconds
......@@ -386,7 +386,7 @@ class Api {
.finally(this.onFinishLoading)
}
async search(search) {
async search(search, statisticsToRefresh = []) {
this.onStartLoading()
return this.swagger()
.then(client => client.apis.repo.search({
......@@ -399,11 +399,12 @@ class Api {
// this helps to keep consistent values, e.g. in the metadata search view
if (response.statistics) {
const empty = {}
const refreshList = ['total', 'authors', 'atoms'].concat(statisticsToRefresh)
Object.keys(response.statistics.total.all).forEach(metric => {
empty[metric] = 0
})
Object.keys(response.statistics)
.filter(key => !['total', 'authors', 'atoms'].includes(key))
.filter(key => !refreshList.includes(key))
.forEach(key => {
if (!this.statistics[key]) {
this.statistics[key] = new Set()
......
......@@ -12,6 +12,11 @@ class DFTSearchAggregations extends React.Component {
static contextType = SearchContext.type
componentDidMount() {
const {setStatisticsToRefresh} = this.context
setStatisticsToRefresh('dft.labels_springer_compound_class')
}
render() {
const {info} = this.props
const {state: {response: {statistics}, usedMetric}} = this.context
......
......@@ -12,6 +12,11 @@ class DFTSearchByPropertyAggregations extends React.Component {
static contextType = SearchContext.type
componentDidMount() {
const {setStatisticsToRefresh} = this.context
setStatisticsToRefresh('dft.labels_springer_classification')
}
render() {
const {info} = this.props
const {state: {response: {statistics}, usedMetric}} = this.context
......
......@@ -35,11 +35,17 @@ const _mapping = {
'oscillator_strengths': 'Oscillator strengths',
'transition_dipole_moments': 'Transition dipole moments'}
function mapKey(name) {
if (name in _mapping) {
return _mapping[name]
function mapKey(key) {
let name = key
const maxLength = 17
if (key in _mapping) {
name = _mapping[key]
}
if (name.length > maxLength) {
return name.substring(0, maxLength) + '...'
} else {
return name
}
return name
}
class QuantityHistogramUnstyled extends React.Component {
......@@ -82,10 +88,10 @@ class QuantityHistogramUnstyled extends React.Component {
}
handleItemClicked(item) {
if (this.props.value === item.name) {
if (this.props.value === item.key) {
this.props.onChanged(null)
} else {
this.props.onChanged(item.name)
this.props.onChanged(item.key)
}
}
......@@ -102,6 +108,7 @@ class QuantityHistogramUnstyled extends React.Component {
const data = Object.keys(this.props.data)
.map(key => ({
key: key,
name: mapKey(key),
value: this.props.data[key][this.props.metric]
}))
......@@ -147,8 +154,8 @@ class QuantityHistogramUnstyled extends React.Component {
withData.exit().remove()
const rectColor = d => selected === d.name ? nomadPrimaryColor.main : nomadSecondaryColor.light
const textColor = d => selected === d.name ? '#FFF' : '#000'
const rectColor = d => selected === d.key ? nomadPrimaryColor.main : nomadSecondaryColor.light
const textColor = d => selected === d.key ? '#FFF' : '#000'
let item = withData.enter()
.append('g')
......
......@@ -17,6 +17,7 @@ import GroupList from './GroupList'
import ApiDialogButton from '../ApiDialogButton'
import SearchIcon from '@material-ui/icons/Search'
import UploadsChart from './UploadsChart'
import UploadersList from './UploadersList'
class Search extends React.Component {
static tabs = {
......@@ -255,11 +256,14 @@ class UsersVisualization extends React.Component {
const {open} = this.props
return <KeepState visible={open} render={() =>
<Card>
<CardContent>
<UploadsChart metricsDefinitions={domain.searchMetrics}/>
</CardContent>
</Card>
<div>
<Card>
<CardContent>
<UploadsChart metricsDefinitions={domain.searchMetrics}/>
</CardContent>
</Card>
<UploadersList />
</div>
}/>
}
}
......
......@@ -34,6 +34,7 @@ class SearchContext extends React.Component {
this.handleQueryChange = this.handleQueryChange.bind(this)
this.handleMetricChange = this.handleMetricChange.bind(this)
this.handleDomainChange = this.handleDomainChange.bind(this)
this.handleStatisticsToRefreshChange = this.handleStatisticsToRefreshChange.bind(this)
this.state.query = this.props.initialQuery || {}
if (this.props.initialRequest) {
this.state.request = {...this.state.request, ...this.props.initialRequest}
......@@ -56,7 +57,8 @@ class SearchContext extends React.Component {
metric: this.defaultMetric,
usedMetric: this.defaultMetric,
domain: domains.dft,
query: {}
query: {},
statisticsToRefresh: []
}
handleRequestChange(changes) {
......@@ -99,9 +101,15 @@ class SearchContext extends React.Component {
}
}
handleStatisticsToRefreshChange(statistics) {
let currentValue = this.state.statisticsToRefresh
currentValue.push(statistics)
this.setState({statisticsToRefresh: currentValue})
}
update() {
const {api, raiseError} = this.props
const {request, query, metric, domain} = this.state
const {request, query, metric, domain, statisticsToRefresh} = this.state
const search = {
...request,
...query,
......@@ -109,7 +117,7 @@ class SearchContext extends React.Component {
metrics: metric === this.defaultMetric ? [] : [metric],
...(this.props.query || {})}
api.search(search)
api.search(search, statisticsToRefresh)
.then(response => {
// find the first statistic to determine which metric is used
const {statistics} = response
......@@ -153,7 +161,8 @@ class SearchContext extends React.Component {
setRequest: this.handleRequestChange,
setQuery: this.handleQueryChange,
setMetric: this.handleMetricChange,
setDomain: this.handleDomainChange
setDomain: this.handleDomainChange,
setStatisticsToRefresh: this.handleStatisticsToRefreshChange
}
return <SearchContext.type.Provider value={value} >
{children}
......
import React from 'react'
import PropTypes from 'prop-types'
import Grid from '@material-ui/core/Grid'
import { Quantity } from './QuantityHistogram'
import { withStyles } from '@material-ui/core'
import SearchContext from './SearchContext'
import { compose } from 'recompose'
import { withApi } from '../api'
class UploadersList extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired
}
static styles = theme => ({
root: {
marginTop: theme.spacing.unit * 2
}
})
static contextType = SearchContext.type
componentDidMount() {
const {state: {query}, setQuery, setStatisticsToRefresh} = this.context
setQuery({...query, statistics_order: '_count'})
setStatisticsToRefresh('uploader')
}
render() {
const {state: {usedMetric}} = this.context
return (
<Grid>
<Quantity quantity="uploader" title="Uploaders" scale={1} metric={usedMetric} />
</Grid>
)
}
}
export default compose(withApi(false, false), withStyles(UploadersList.styles))(UploadersList)
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Select, MenuItem } from '@material-ui/core'
import Button from '@material-ui/core/Button'
import RefreshIcon from '@material-ui/icons/Refresh'
import Grid from '@material-ui/core/Grid'
import TextField from '@material-ui/core/TextField'
import * as d3 from 'd3'
......@@ -9,14 +11,12 @@ import { nomadSecondaryColor } from '../../config.js'
import SearchContext from './SearchContext'
import { compose } from 'recompose'
import { withApi } from '../api'
import { Quantity } from './QuantityHistogram'
class UploadsHistogramUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
height: PropTypes.number.isRequired,
data: PropTypes.object,
interval: PropTypes.string,
metric: PropTypes.string.isRequired,
metricsDefinitions: PropTypes.object.isRequired,
onChanged: PropTypes.func.isRequired,
......@@ -34,7 +34,6 @@ class UploadsHistogramUnstyled extends React.Component {
super(props)
this.state = {
scalePower: this.props.defaultScale || 1.0,
interval: this.props.interval || '1M',
time: null,
from_time: 0,
until_time: 0
......@@ -65,41 +64,6 @@ class UploadsHistogramUnstyled extends React.Component {
}
]
intervals = [
{
label: 'Yearly',
value: '1y',
number: 31536000000
},
{
label: 'Monthly',
value: '1M',
number: 2678400000
},
{
label: 'Daily',
value: '1d',
number: 86400000
},
{
label: 'Hourly',
value: '1h',
number: 3600000
},
{
label: 'Minute',
value: '1m',
number: 60000
},
{
label: 'Second',
value: '1s',
number: 1000
}
]
timeInterval = Object.assign({}, ...this.intervals.map(e => ({[e.value]: e.number})))
componentDidMount() {
const from_time = new Date(this.startDate).getTime()
const until_time = new Date().getTime()
......@@ -112,15 +76,9 @@ class UploadsHistogramUnstyled extends React.Component {
}
handleQueryChange() {
const interval = this.state.interval
const from_time = new Date(this.state.from_time)
const until_time = new Date(this.state.until_time)
this.props.onChanged(from_time.toISOString(), until_time.toISOString(), interval)
}
handleIntervalChange(newInterval) {
// TODO: add a refresh button so directly updating interval is not necessary
this.setState({interval: newInterval}, () => this.handleQueryChange())
this.props.onChanged(from_time.toISOString(), until_time.toISOString())
}
handleTimeChange(newTime, key, target) {
......@@ -130,45 +88,54 @@ class UploadsHistogramUnstyled extends React.Component {
} else {
date = new Date(newTime)
}
if (target === 'state' || target === 'all') {
key === 'from_time' ? this.setState({from_time: date.getTime()}) : this.setState({until_time: date.getTime()})
if (key === 'from_time') {
if (this.state.from_time !== date.getTime()) {
this.setState({from_time: date.getTime()})
}
} else if (key === 'until_time') {
if (this.state.until_time !== date.getTime()) {
this.setState({until_time: date.getTime()})
}
}
}
if (target === 'picker' || target === 'all') {
document.getElementById(key).value = date.toISOString().substring(0, 10)
}
}
handleItemClicked(item) {
handleItemClicked(item, deltaT) {
const selected = item.time
if (selected === this.state.time) {
this.props.onChanged(null, null, null)
this.props.onChanged(null, null)
} else {
const deltaT = this.timeInterval[this.state.interval]
this.handleTimeChange(selected, 'from_time', 'all')
this.handleTimeChange(selected + deltaT, 'until_time', 'all')
this.handleQueryChange()
}
}
resolveDate(name) {
resolveDate(name, deltaT) {
const date = new Date(parseInt(name, 10))
const year = date.toLocaleDateString(undefined, {year: 'numeric'})
const quarter = Math.floor((date.getMonth() + 3) / 3)
const month = date.toLocaleDateString(undefined, {month: 'short'})
const week = (date) => {
const first = new Date(date.getFullYear(), 0, 1)
return Math.ceil((((date - first) / 86400000) + first.getDay() + 1) / 7)
}
const day = date.toLocaleDateString(undefined, {day: 'numeric'})
const hour = date.toLocaleTimeString(undefined, {hour: 'numeric'})
const min = date.toLocaleTimeString(undefined, {minute: 'numeric'})
const sec = date.toLocaleTimeString(undefined, {second: 'numeric'})
const intervals = {
'1y': year,
'1M': month,
'1d': day,
'1h': hour,
'1m': min,
'1s': sec
}
const times = [31536000, 7776000, 2419200, 604800, 864000, 3600, 60, 1]
const diffs = times.map(t => Math.abs(t - (deltaT / 1000)))
return intervals[this.state.interval]
const intervals = [year, 'Q' + quarter, month, 'W' + week, day, hour, min, sec]
return intervals[diffs.indexOf(Math.min(...diffs))]
}
hover(svg, bar) {
......@@ -215,7 +182,7 @@ class UploadsHistogramUnstyled extends React.Component {
} else {
data = Object.keys(this.props.data).map(key => ({
time: parseInt(key, 10),
name: this.resolveDate(key),
// name: this.resolveDate(key),
value: this.props.data[key][this.props.metric]
}))
}
......@@ -226,10 +193,17 @@ class UploadsHistogramUnstyled extends React.Component {
this.handleTimeChange(this.state.until_time, 'until_time', 'picker')
}
let deltaT = 31536000000
if (data.length > 1) {
deltaT = data[1].time - data[0].time
}
data.forEach(d => { d.name = this.resolveDate(d.time, deltaT) })
const scalePower = this.state.scalePower
const width = this.container.current.offsetWidth
const height = this.props.height
const margin = Math.round(0.1 * height)
const margin = Math.round(0.15 * height)
const x = scaleBand().rangeRound([margin, width]).padding(0.1)
const y = scalePow().range([height - margin, margin]).exponent(scalePower)
......@@ -258,7 +232,7 @@ class UploadsHistogramUnstyled extends React.Component {
.attr('font-size', '12px')
.style('text-anchor', 'end')
const yAxis = d3.axisLeft(y)
const yAxis = d3.axisLeft(y).ticks(Math.min(max, 5), '.0s')
svg.select('.yaxis').remove()
svg.append('g')
.attr('transform', `translate(${margin}, 0)`)
......@@ -294,16 +268,17 @@ class UploadsHistogramUnstyled extends React.Component {
item
.style('cursor', 'pointer')
.on('click', d => this.handleItemClicked(d))
.on('click', d => this.handleItemClicked(d, deltaT))
svg.select('.tooltip').remove()
svg.call(this.hover, item)
}
render() {
return (
<div>
<Grid container justify='space-around' spacing={24}>
<Grid container justify='space-between' alignItems='flex-end'>
<Grid item xs={2}>
<Select
margin='none'
......@@ -318,7 +293,7 @@ class UploadsHistogramUnstyled extends React.Component {
</MenuItem>))}
</Select>
</Grid>
<Grid item xs={3}>
<Grid item xs={2}>
<TextField
id='from_time'
label="from time"
......@@ -329,19 +304,7 @@ class UploadsHistogramUnstyled extends React.Component {
}}
/>
</Grid>
<Grid item xs={3}>
<Select
id='interval'
value={this.state.interval}
onChange={(event) => this.handleIntervalChange(event.target.value)}
label= 'interval'
> {this.intervals.map(item => (
<MenuItem value={item.value} key={item.value}>
{item.label}
</MenuItem>))}
</Select>
</Grid>
<Grid item xs={3}>
<Grid item xs={2}>
<TextField
id='until_time'
label="until time"
......@@ -352,6 +315,14 @@ class UploadsHistogramUnstyled extends React.Component {
}}
/>
</Grid>
<Grid item xs={2}>
<Button
variant='outlined'
color='default'
onClick={() => this.handleQueryChange()}
> Refresh <RefreshIcon/>
</Button>
</Grid>
</Grid>
<div ref={this.container}>
<svg ref={this.svgEl}></svg>
......@@ -363,32 +334,6 @@ class UploadsHistogramUnstyled extends React.Component {
export const UploadsHistogram = withStyles(UploadsHistogramUnstyled.styles)(UploadsHistogramUnstyled)
class UploadersListUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired
}
static styles = theme => ({
root: {
marginTop: theme.spacing.unit * 2
}
})
static contextType = SearchContext.type
render() {
const {state: {usedMetric}} = this.context
return (
<Grid>
<Quantity quantity="uploader" title="Top Uploaders" scale={1} metric={usedMetric} />
</Grid>
)
}
}
export const UploadersList = withStyles(UploadersListUnstyled.styles)(UploadersListUnstyled)
class UploadsChart extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
......@@ -402,6 +347,11 @@ class UploadsChart extends React.Component {
static contextType = SearchContext.type
componentDidMount() {
const {setStatisticsToRefresh} = this.context
setStatisticsToRefresh('date_histogram')
}
render() {
const {classes, metricsDefinitions, ...props} = this.props
const {state: {response, usedMetric, query}, setQuery} = this.context
......@@ -416,13 +366,9 @@ class UploadsChart extends React.Component {
data={response.statistics.date_histogram}
metric={usedMetric}
metricsDefinitions={metricsDefinitions}
interval={'1M'}
onChanged={(from_time, until_time, interval) => setQuery({...query, from_time: from_time, until_time: until_time, interval: interval})}
onChanged={(from_time, until_time) => setQuery({...query, from_time: from_time, until_time: until_time})}
{...props} />
</Grid>
<Grid item xs={12}>
<UploadersList />
</Grid>
</Grid>
)
}
......
......@@ -29,7 +29,7 @@ from nomad import search, utils, datamodel, processing as proc, infrastructure,
from nomad.metainfo import search_extension
from nomad.datamodel import Dataset, User, EditableUserMetadata
from nomad.app import common
from nomad.app.common import RFC3339DateTime, DotKeyNested
from nomad.app.common import RFC3339DateTime, DotKeyNested, rfc3339DateTime
from .api import api
from .auth import authenticate
......@@ -73,6 +73,29 @@ class RepoCalcResource(Resource):
return result, 200
def resolve_interval(from_time, until_time):
if from_time is None:
from_time = datetime.fromtimestamp(0)
if until_time is None:
until_time = datetime.utcnow()
dt = rfc3339DateTime.parse(until_time) - rfc3339DateTime.parse(from_time)
if dt.days >= 1826:
return '1y'
elif dt.days >= 731:
return '1q'
elif dt.days >= 121:
return '1M'
elif dt.days >= 28:
return '1w'
elif dt.days >= 4:
return '1d'
elif dt.total_seconds() >= 14400:
return '1h'
else:
return '1m'
_search_request_parser = api.parser()
add_pagination_parameters(_search_request_parser)
add_scroll_parameters(_search_request_parser)
......@@ -87,6 +110,8 @@ _search_request_parser.add_argument(
'Possible values are %s.' % ', '.join(search_extension.metrics.keys())))
_search_request_parser.add_argument(
'statistics', type=bool, help=('Return statistics.'))
_search_request_parser.add_argument(
'statistics_order', type=str, help='Statistics order (can be _key or _count)')
_search_request_parser.add_argument(
'exclude', type=str, action='split', help='Excludes the given keys in the returned data.')
for group_name in search_extension.groups:
......@@ -170,17 +195,25 @@ class RepoCalcsResource(Resource):
order_by = args.get('order_by', 'upload_time')
date_histogram = args.get('date_histogram', False)
interval = args.get('interval', '1M')