UNPKG

labo-components

Version:
461 lines (408 loc) 15.5 kB
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)}/> &nbsp; {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&nbsp;<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;