labo-components
Version:
461 lines (408 loc) • 15.5 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import CollectionAnalysisAPI from './api/CollectionAnalysisAPI';
import IDUtil from './util/IDUtil';
import FlexRouter from './util/FlexRouter';
import ComponentUtil from './util/ComponentUtil';
import CollectionUtil from './util/CollectionUtil';
import LocalStorageHandler from "./util/LocalStorageHandler";
import ReadMoreLink from './components/helpers/ReadMoreLink';
import ToolHeader from './components/shared/ToolHeader';
import FlexBox from './components/FlexBox';
import FlexModal from './components/FlexModal';
import CollectionAnalyser from './components/collection/CollectionAnalyser';
import CollectionSelector from './components/collection/CollectionSelector';
import DateFieldSelector from './components/collection/DateFieldSelector';
import FieldAnalysisStats from './components/collection/FieldAnalysisStats';
import MetadataCompletenessChart from './components/stats/MetadataCompletenessChart';
import CollectionRegistryAPI from './api/CollectionRegistryAPI';
class CollectionRecipe extends React.Component {
constructor(props) {
super(props);
this.state = {
loadedCollections : {},
activeCollection : null,
fieldAnalysisStats : null, //output from the CollectionAnalyser
fieldAnalysisTimeline : null, //output from the CollectionAnalyser
fieldCompleteness: null,
dateField: null, // datefield used for analysis
field: null, // field to analyse
showModal : false,
collectionList : null
};
this.CLASS_PREFIX = 'rcp__cl';
PropTypes.checkPropTypes(CollectionRecipe.propTypes, this.props, 'prop', this.constructor.name);
}
componentDidMount() {
const cids = LocalStorageHandler.getJSONFromLocalStorage('stored-cids');
if(this.props.params.cids || cids) {
CollectionUtil.generateCollectionConfigs(
this.props.clientId,
this.props.user,
this.getCollectionIds(this.props.params.cids, cids),
this.onConfigsLoaded
);
}
CollectionRegistryAPI.listCollections( collectionList => this.setState({collectionList}));
}
getCollectionIds = (cidsFromParams, cidsFromLocalStorage) => {
return cidsFromParams ? cidsFromParams.split(','): cidsFromLocalStorage.split(',');
};
onConfigsLoaded = configs => {
const loadedCollections = {};
configs.forEach((conf) => {
loadedCollections[conf.collectionId] = conf;
});
this.setState({
loadedCollections: loadedCollections
},
() => this.updateBrowserHistory());
};
//receives data from child components
onComponentOutput(componentClass, data) {
if(componentClass === 'CollectionSelector') {
if(data) {
const sc = this.state.loadedCollections;
CollectionUtil.generateCollectionConfig(
this.props.clientId,
this.props.user,
data.index,
(collectionConfig) => {
sc[collectionConfig.collectionId] = collectionConfig;
this.setState(
{
loadedCollections : sc,
activeCollection : data.collectionId,
fieldAnalysisStats : null,
fieldAnalysisTimeline : null,
field: null
},
this.onCollectionAdded
);
}
);
}
}
}
onCollectionAdded = () => {
ComponentUtil.hideModal(this, 'showModal', 'collection__modal', true)
this.updateBrowserHistory();
};
removeCollection = collectionId => {
const collections = this.state.loadedCollections;
const ac = this.state.activeCollection;
delete collections[collectionId];
const newStateObj = {
loadedCollections : collections
}
//if you remove the selected collection also reset the active stats/visuals
if(ac === collectionId) {
newStateObj['activeCollection'] = null;
newStateObj['fieldAnalysisStats'] = null;
newStateObj['fieldAnalysisTimeline'] = null;
}
this.setState(newStateObj, this.updateBrowserHistory)
};
setActiveCollection = e => {
this.setState({
activeCollection : e.target.id,
fieldAnalysisStats : null, //reset the field stats
fieldAnalysisTimeline : null, //reset the analysis timeline
field: null,
dateField: null,
})
};
updateBrowserHistory = () => {
let params = null;
if(Object.keys(this.state.loadedCollections).length > 0) {
params = {cids : Object.keys(this.state.loadedCollections).join(',')};
LocalStorageHandler.storeJSONInLocalStorage('stored-cids', params['cids'])
} else {
LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-cids');
}
FlexRouter.setBrowserHistory(
params,
this.constructor.name
);
};
getCollectionConfig = collectionId => {
if(this.state.loadedCollections) {
return this.state.loadedCollections[collectionId];
}
return null;
};
//generates the data for the chart TODO optimise this
toTimelineData = data => {
const timelineData = {};
if(data) {
const totalChart = [];
const missingChart = [];
const presentChart = [];
for (const item in data.timeline) {
totalChart.push({
year: data.timeline[item].year, //y-axis
total: data.timeline[item].background_count, //different line on graph
});
presentChart.push({
year : data.timeline[item].year, //y-axis
present: data.timeline[item].field_count, //different line on graph
})
missingChart.push({
year : data.timeline[item].year, //y-axis
missing:data.timeline[item].background_count - data.timeline[item].field_count //different line on graph
})
}
timelineData['total'] = {
label : 'Total',
dateField : null, //what to do here?
prettyQuery : null, //what to do here?
data : totalChart,
queryId : 'total_chart'
};
timelineData['missing'] = {
label : 'Missing',
dateField : null, //what to do here?
prettyQuery : null, //what to do here?
data : missingChart,
queryId : 'missing_chart'
};
timelineData['present'] = {
label : 'Present',
dateField : null,
prettyQuery : null, //what to do here?
data : presentChart,
queryId : 'present_chart'
}
}
return timelineData;
};
/* --------------------------- WHENEVER A FIELD IS SELECTED OR ANALYSED -------------------- */
onFieldSelected = field => {
this.setState({field}, () => {
this.analyseField(this.state.field);
});
};
onCompletenessLoaded = fieldCompleteness => {
this.setState({fieldCompleteness : fieldCompleteness});
};
onDateFieldSelected = dateField => {
this.setState({dateField}, () => {
this.analyseField(this.state.field);
});
};
analyseField = analysisField => {
this.loadAnalysis(analysisField, (data, timelineData) => {
this.setState({
fieldAnalysisStats : data,
fieldAnalysisTimeline : timelineData
})
});
};
loadAnalysis = (analysisField, callback) => {
const collectionConfig = this.getCollectionConfig(this.state.activeCollection);
let nestedPath = null;
const nestedLayers = collectionConfig.getNestedSearchLayers();
if (nestedLayers) {
const foundLayer = nestedLayers.find(nl => {
return analysisField.indexOf(nl.path) !== -1
});
nestedPath = foundLayer ? foundLayer.path : null;
}
CollectionAnalysisAPI.fieldCompletenessTimeline(
collectionConfig.collectionId,
collectionConfig.getDocumentType(),
this.state.dateField ? this.state.dateField : 'null__option',
analysisField ? analysisField : 'null__option',
[], //facets are not yet supported
collectionConfig.getMinimumYear(),
nestedPath,
(data) => {
const timelineData = this.toTimelineData(data);
callback(data, timelineData);
}
);
};
/* ------------------------------------------ RENDERING FUNCTIONS ---------------------------------- */
renderCollectionModal = () => {
return (
<FlexModal
elementId="collection__modal"
stateVariable="showModal"
owner={this}
size="large"
title="Select a collection">
<CollectionSelector
onOutput={this.onComponentOutput.bind(this)}
showSelect={true}
collectionList={this.state.collectionList}
showBrowser={true}/>
</FlexModal>
)
};
renderCollectionOverview = (loadedCollections, activeCollection) => {
const items = Object.keys(loadedCollections).map(collectionId => {
const collectionMetadata = loadedCollections[collectionId].getCollectionMetadata();
const classNames = ['list-group-item'];
const collectionTitle = collectionMetadata ? collectionMetadata.title : collectionId;
let ckanLink = null;
let linkIcon = ReadMoreLink.svgImg('000');
if(collectionId === activeCollection) {
classNames.push('active');
linkIcon = ReadMoreLink.svgImg('fff');
}
if (collectionMetadata && collectionMetadata.registryUrl) {
ckanLink = <ReadMoreLink linkIcon={linkIcon} linkUrl={collectionMetadata.registryUrl}/>
}
return (
<li key={collectionId} id={collectionId} className={classNames.join(' ')} onClick={this.setActiveCollection}>
<span className="fas fa-times" onClick={this.removeCollection.bind(this, collectionId)}/>
{collectionTitle} {ckanLink}
</li>
)
});
return (
<FlexBox title="Selected collections">
<div className="box">
<div className="text-right">
<button className="btn btn-primary" onClick={ComponentUtil.showModal.bind(this, this, 'showModal')}>
Add collection <i className="fas fa-plus"/>
</button>
</div>
<br/>
<ul className="list-group">
{items}
</ul>
</div>
</FlexBox>
);
};
renderDateFieldSelector = (collectionConfig, fieldCompleteness, onDateFieldSelected) => {
return (
<FlexBox title="Date Field selector">
<div className={IDUtil.cssClassName('input-area', this.CLASS_PREFIX)}>
<DateFieldSelector
key={'__dfs__' + collectionConfig.collectionId}
collectionConfig={collectionConfig}
fieldCompleteness={fieldCompleteness} //pass the analysed date fields to the datefield selector
onChange={onDateFieldSelected}
/>
</div>
</FlexBox>
);
};
renderFieldSelector = (collectionConfig, onFieldSelected, onCompletenessLoaded) => {
const collectionAnalyser = (
<CollectionAnalyser
key={'__ca__' + collectionConfig.collectionId}
collectionConfig={collectionConfig}
onChange={onFieldSelected}
onCompletenessLoaded={onCompletenessLoaded}
/>
);
return (
<FlexBox title="Collection analysis">
<div className="row box">
<div className="col-md-12">
{collectionAnalyser}
</div>
</div>
</FlexBox>
)
};
renderCompletenessAnalysisChart = (collectionConfig, dateField, analysisField, chartData) => {
if(!(analysisField && dateField)) return null;
if(!chartData) {
return (
<div className={IDUtil.cssClassName('input-area', this.CLASS_PREFIX)}>
<i className="fas fa-circle-notch fa-spin"/> Loading chart...
</div>
)
}
return (
<MetadataCompletenessChart
collectionConfig={collectionConfig}
dateField={dateField}
analysisField={analysisField}
data={chartData}
/>
);
};
renderCompletenessAnalysisOverview = (collectionConfig, stats) => {
if(!(collectionConfig && stats)) return null;
return (
<div className="fieldAnalysisStats">
<FieldAnalysisStats collectionConfig={collectionConfig} data={stats}/>
</div>
);
};
render() {
const collectionConfig = this.getCollectionConfig(this.state.activeCollection);
const collectionModal = this.state.showModal && this.state.collectionList ? this.renderCollectionModal() : null;
const collectionOverview = this.renderCollectionOverview(this.state.loadedCollections, this.state.activeCollection);
const fieldSelector = collectionConfig ? this.renderFieldSelector(
collectionConfig,
this.onFieldSelected,
this.onCompletenessLoaded,
) : null;
//only show when an analysis field has been selected
const dateFieldSelector = this.state.field && this.state.fieldCompleteness ? this.renderDateFieldSelector(
collectionConfig,
this.state.fieldCompleteness,
this.onDateFieldSelected
) : null;
//only show the chart & stats when both an analysis field and date field have been selected
const chart = collectionConfig && this.state.dateField && this.state.field && this.state.fieldAnalysisTimeline ?
this.renderCompletenessAnalysisChart(
collectionConfig,
this.state.dateField,
this.state.field,
this.state.fieldAnalysisTimeline
) : null;
const fieldAnalysisStats = this.renderCompletenessAnalysisOverview(collectionConfig, this.state.fieldAnalysisStats);
return (
<div className={IDUtil.cssClassName('collection-recipe')}>
<ToolHeader name={"Collection inspector"} />
{collectionModal}
<div className="row">
<div className="col-md-6">
{collectionOverview}
</div>
<div className="col-md-6">
{fieldSelector}
</div>
</div>
<div className="row">
<div className="col-md-12">
{chart}
</div>
</div>
<div className="row">
<div className="col-md-12">
{dateFieldSelector}
</div>
</div>
<div className="row">
<div className="col-md-12">
{fieldAnalysisStats}
</div>
</div>
</div>
)
}
}
CollectionRecipe.propTypes = {
clientId : PropTypes.string.isRequired,
params: PropTypes.shape({
cids: PropTypes.string // identify collections loaded from the url (if any)
}).isRequired,
recipe: PropTypes.object, // passed to the component but never used.
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string,
attributes: PropTypes.shape({
allowPersonalCollections: PropTypes.bool
})
})
};
export default CollectionRecipe;