Commit 87e8602a authored by Matija Obreza's avatar Matija Obreza
Browse files

Browse accessions data from Genesys

- GenesysService for filtering accessions
- Browse page and initial filtering
parent 2534033e
......@@ -22,6 +22,10 @@ const GEONAMES_USERNAME = process.env.GEONAMES_USERNAME || 'geonames_test';
const GEONAMES_API_URL = 'https://secure.geonames.org';
const GENESYS_URL = process.env.GENESYS_URL || 'http://localhost:8081';
const GENESYS_CLIENT_ID = process.env.GENESYS_CLIENT_ID || 'oauth-client';
const GENESYS_CLIENT_SECRET = process.env.GENESYS_CLIENT_SECRET || 'changeme';
const sortedChunks = function(list) {
return function(chunk1, chunk2) {
var index1 = list.indexOf(chunk1.names[0]);
......@@ -95,6 +99,7 @@ module.exports = {
'/api/geonames': {
target: GEONAMES_API_URL,
logLevel: 'debug',
pathRewrite(path, req) {
const p = `${path.replace('/api/geonames', '')}&username=${GEONAMES_USERNAME}`;
// remove all headers from request
......@@ -103,6 +108,43 @@ module.exports = {
console.log(`HTTP proxy to ${GEONAMES_API_URL}${p}`);
return p;
}
},
'/api/genesys': {
target: GENESYS_URL,
logLevel: 'debug',
ws: true,
// secure: false,
pathRewrite(path, req) {
let p = path.replace('/api/genesys', '');
if (p.startsWith('/oauth/token')) {
const grantType = req.query['grant_type'];
if(grantType === 'client_credentials' || grantType === 'password') {
p = `${p}&client_id=${GENESYS_CLIENT_ID}&client_secret=${GENESYS_CLIENT_SECRET}`;
}
// remove all headers from request
req.headers = {};
} else {
p = path.replace('/api/genesys', '/api');
req.headers = { ...req.headers };
// scrub headers
delete req.headers['host'];
delete req.headers['origin'];
delete req.headers['referer'];
delete req.headers['cookie'];
}
console.log(`Proxy headers: `, req.headers);
console.log(`HTTP proxy ${path} to ${GENESYS_URL}${p}`);
return p;
},
onError(err, req, res) {
console.log(err);
},
onClose(res, socket, head) {
// view disconnected websocket connections
console.log('Client disconnected', res);
}
}
},
},
......
apps:
- script: server.js
args: --api-url=${CATALOG_API_URL} --client-id=${CLIENT_ID} --client-secret=${CLIENT_SECRET} --geonames-username=${GEONAMES_USERNAME}
args: >-
--api-url=${CATALOG_API_URL}
--api-timeout=${API_TIMEOUT}
--client-id=${CLIENT_ID} --client-secret=${CLIENT_SECRET}
--geonames-username=${GEONAMES_USERNAME}
--genesys-url=${GENESYS_URL}
--genesys-client-id=${GENESYS_CLIENT_ID}
--genesys-client-secret=${GENESYS_CLIENT_SECRET}
name: catalogui
exec_mode: cluster
instances: 3
......
......@@ -2,7 +2,10 @@ import * as minimist from 'minimist';
// console.log(process.argv);
const argv = minimist(process.argv.slice(2), {
string: ['--api-url', '--api-timeout', '--client-id', '--client-secret', '--geonames-username' ],
string: ['--api-url', '--client-id', '--client-secret',
'--geonames-username',
'--genesys-url', '--genesys-client-id', '--genesys-client-secret'
],
});
console.dir(argv);
......@@ -14,11 +17,17 @@ const config = {
apiUrl: argv['api-url'] || 'https://api.catalog.demo.genesys-pgr.org',
// Timeout (ms) for proxied calls to the API
apiTimeout: +(argv['api-timeout'] || 2000),
// OAuth Client
// Catalog OAuth Client
clientId: argv['client-id'] || 'my-trusted-client',
clientSecret: argv['client-secret'] || 'my-secret-client',
// Username for Geonames http://www.geonames.org/login
// Geonames username: http://www.geonames.org/login
geonamesUsername: argv['geonames-username'] || 'geonames_test',
// Genesys PGR
genesysUrl: argv['genesys-url'] || 'https://www.genesys-pgr.org',
genesysClientId: argv['genesys-client-id'],
genesysClientSecret: argv['genesys-client-secret'],
};
console.log('Catalog config', config);
......
import * as proxy from 'express-http-proxy';
import config from '../config';
const genesysProxy = proxy(config.genesysUrl, {
parseReqBody: false,
timeout: config.apiTimeout * 2, // double normal timeout
filter: (req, res) => {
if (req.url.startsWith('/oauth/') || req.url.startsWith('/token') || req.url.startsWith('/google')) {
// console.log('Will proxy /oauth');
return true;
} else {
console.log(`Will HTTP Proxy filter? ${typeof req.headers.authorization !== 'undefined'}`);
return typeof req.headers.authorization !== 'undefined';
}
},
proxyReqPathResolver: (req) => {
let path = req.url;
if (path.startsWith('/oauth/token')) {
const grantType = req.query.grant_type;
if (grantType === 'client_credentials' || grantType === 'password') {
path = `${path}&client_id=${config.genesysClientId}&client_secret=${config.genesysClientSecret}`;
// remove all headers from request
req.headers = {};
}
} else {
// insert /api
path = `/api${path}`;
// remove some heaaders
delete req.headers['host'];
delete req.headers['origin'];
delete req.headers['referer'];
delete req.headers['cookie'];
}
console.log(`HTTP proxy to ${config.genesysUrl}${path}`);
return path;
},
});
export default genesysProxy;
......@@ -10,6 +10,7 @@ import {readFileSync} from 'fs';
import prerenderer from './middleware/prerenderer';
import httpProxy from './middleware/httpProxy';
import geonamesHttpProxy from './middleware/geonamesHttpProxy';
import genesysProxy from './middleware/genesysProxy';
import robots from './robots';
const app = express();
......@@ -29,6 +30,8 @@ app.use(compression());
app.use('/proxy', httpProxy);
// Proxy geonames requests
app.use('/api/geonames', geonamesHttpProxy);
// Proxy genesys requests
app.use('/api/genesys', genesysProxy);
// robots.txt
app.get('/robots.txt', robots);
// Serve static resources (this should be the only thing publicly accessible)
......
// import {push} from 'react-router-redux';
import { GenesysService } from 'service/GenesysService';
// import {CREATE_CROP, GET_CROP, RECEIVE_CROP, RECEIVE_CROPS} from 'constants/crop';
// import {IReducerAction} from 'model/common.model';
import { log } from 'utilities/debug';
export const listAccessions = (filters: object, page: number = 0, results: number = 50) => (dispatch, getState) => {
return GenesysService.listAccessions(filters, page, results)
.then((data) => {
return data;
}).catch((error) => {
log(`Genesys error`, error);
});
};
export const GENESYSBROWSE_FILTERFORM = 'Form/Genesys/BROWSE';
// import { ServerInfo } from 'model/serverinfo.model';
// import { SERVER_INFO_URL } from 'constants/apiURLS';
import axios from 'axios';
import authenticatedRequest from 'utilities/requestUtils';
import * as _ from 'lodash';
import {Page} from 'model/common.model';
const API_PREFIX = '/api/genesys';
export class GenesysService {
private static accessToken: any = null;
public static login(): Promise<any> {
if (GenesysService.accessToken === null || GenesysService.accessToken.expireTimestamp < Date.now()) {
return axios.post(`${API_PREFIX}/oauth/token`, null, {
params: {
grant_type: 'client_credentials',
},
})
.then(({ data }) => {
data.expireTimestamp = Date.now() + (data.expires_in - 10) * 1000;
return GenesysService.accessToken = data;
});
} else {
return Promise.resolve(GenesysService.accessToken);
}
}
public static listAccessions(filters: object, page: number = 0, results: number = 50): Promise<Page<object>> {
const flatFilters = genesysFlatten('', renameFilters(filters), {});
// console.log(`Flatfilters ${JSON.stringify(flatFilters)}`, filters, flatFilters);
return GenesysService.login()
.then((token) => {
// console.log('Genesys token', token);
return authenticatedRequest(token.access_token, {
url: `${API_PREFIX}/v0/acn/filter?p=${page}&l=${results}`,
method: 'POST',
data: {
...flatFilters,
},
});
})
.then(({ data }) => new Page<object>(data));
}
}
const renameFilters = (filters: any) => {
const renamed: any = { ...filters };
renamed.crops = renamed.crop;
delete renamed.crop;
return renamed;
};
/**
* Genesys PGR uses a semi-flat JSON structure, with arrays and literals as values:
* { institute: { code: [ 'NGA039' ] } } --> { 'institute.code': [ 'NGA039' ] }
*/
const genesysFlatten = (prefix: string, obj: object, result: any) => {
for (const k of Object.keys(obj)) {
const val = obj[k];
// console.log(`${k} = ${val} typeof ${typeof val}`);
if (_.isObject(val)) {
if (_.isArray(val) && val.length > 0) {
result[`${prefix}${k}`] = val;
} else {
genesysFlatten(`${prefix}${k}.`, val, result);
}
} else if (val !== null) {
result[`${prefix}${k}`] = val;
}
}
return result;
};
......@@ -100,6 +100,9 @@ class LeftMenu extends React.Component<ILeftMenuProps, any> {
<ListItem button>
<Link to="/descriptors" >Descriptors</Link>
</ListItem>
<ListItem button>
<Link to="/accessions" >Accessions</Link>
</ListItem>
<ListItem button>
<Link to="/crops" >Crops</Link>
</ListItem>
......
......@@ -100,6 +100,7 @@ class UserMenuComponent extends React.Component<IUserMenuComponentProps, any> {
onClose={ this.handleRequestClose }
>
<Link to="/dashboard"><MenuItem >My Dashboard</MenuItem></Link>
<Link to="/accessions"><MenuItem>Accessions</MenuItem></Link>
<Link to="/crops"><MenuItem>Crops</MenuItem></Link>
<Link to="/vocabulary"><MenuItem>Controlled vocabularies</MenuItem></Link>
<Link to="/gui"><MenuItem>UI Tests</MenuItem></Link>
......
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {log} from 'utilities/debug';
import { listCrops } from 'actions/crop';
import { listAccessions } from 'actions/genesys';
import { Page, Pagination } from 'model/common.model';
import { withStyles } from 'material-ui/styles';
import Grid from 'material-ui/Grid';
import Loading from 'ui/common/Loading';
import PaginationComponent from 'ui/common/pagination';
import GenesysBrowseFilters from './c/Filters';
import PrettyFilters from 'ui/common/filter/PrettyFilters';
import DOI from 'ui/common/DOI';
const styles = (theme) => ({
filterSection: theme.leftPanel.root,
});
interface IBrowsePageProps extends React.ClassAttributes<any> {
classes: any;
router: any;
pagination?: Pagination<object>;
listCrops: any;
listAccessions: any;
}
const SORT_OPTIONS = null;
// Page to browse and filter descriptor lists
class BrowsePage extends React.Component<IBrowsePageProps, any> {
protected static needs = [
listCrops,
];
public constructor(props: any) {
super(props);
this.state = { paged: null, filter: {} };
}
public componentWillMount() {
const {pagination, listCrops} = this.props;
log(`BrowsePage.componentWillMount...`, this.props);
listCrops();
// if (! paged) {
log('Loading genesys accessions');
this.fetchAccessions(this.state.filter, pagination.page, pagination.size);
// }
}
public componentWillReceiveProps(nextProps) {
const {pagination: oldPagination} = this.props;
const {pagination} = nextProps;
if (! oldPagination.equals(pagination)) {
log('Paginations differ!', pagination);
this.fetchAccessions(this.state.filter, pagination.page, pagination.size);
}
}
protected onPaginationChange = (page, results, sortBy, dir) => {
log('onPaginationChange', page, results, sortBy);
const {router, router: { location }} = this.props;
location.query.p = page;
location.query.l = results;
location.query.s = sortBy;
location.query.d = dir;
router.push(location);
}
protected applyFilters = (newFilters) => {
const {pagination} = this.props;
log('Applying new filter', newFilters);
this.fetchAccessions(newFilters, pagination.page, pagination.size);
}
private fetchAccessions(filter, page, size) {
const {listAccessions} = this.props;
console.log(`Fetching p=${page} l=${size}`, filter);
listAccessions(filter, page, size)
.then((data: Page<object>) => {
this.setState({...this.state, paged: data, filter });
})
.catch((error) => {
console.log(error);
});
}
private htmlTaxa(taxonomyHtml: string) {
return { __html: taxonomyHtml };
}
public render() {
const { classes } = this.props;
const { paged, filter } = this.state;
const stillLoading: boolean = (! paged || ! paged.content);
return (
<Grid container spacing={ 0 }>
<Grid item xs={ 12 } md={ 3 } lg={ 2 } className={ classes.filterSection }>
<GenesysBrowseFilters initialValues={ filter } onSubmit={ this.applyFilters } />
</Grid>
<Grid item xs={ 12 } md={ 9 } lg={ 10 } className="back-gray">
<Grid container spacing={ 0 }>
<Grid item xs={ 12 }>
<PaginationComponent pageObj={ paged }
onChange={ this.onPaginationChange } displayName="accessions"
sortOptions={ SORT_OPTIONS } />
</Grid>
<Grid item xs={ 12 }>
<PrettyFilters
filterObj={ filter }
onSubmit={ this.applyFilters }
/>
</Grid>
</Grid>
{ stillLoading ? <Loading /> :
<Grid container spacing={ 0 }>
{ paged.content.map((acce, index) => (
<Grid item xs={ 12 } key={ `${index}` } className="stripy-item" style={ { margin: '.3rem 0', padding: '.3rem 1rem' } }>
<Grid container spacing={ 0 }>
<Grid item xs={ 4 } md={ 2 } >{ acce.acceNumb }</Grid>
<Grid item xs={ 4 } md={ 2 } ><DOI value={ acce.doi } noPrefix /></Grid>
<Grid item xs={ 4 } md={ 2 }>{ acce.institute.code }</Grid>
<Grid item xs={ 4 } md={ 2 } >{ acce.sampStat }</Grid>
<Grid item xs={ 8 } md={ 2 } >{ acce.orgCty ? acce.orgCty.name : '' }</Grid>
<Grid item xs={ 12 } md={ 12 } dangerouslySetInnerHTML={ this.htmlTaxa(acce.taxonomy.sciNameHtml) } />
</Grid>
</Grid>
)) }
</Grid>
}
</Grid>
<Grid container spacing={ 0 }>
<Grid item xs={ 12 }>
<PaginationComponent pageObj={ paged }
onChange={ this.onPaginationChange } displayName="accessions"
sortOptions={ SORT_OPTIONS } />
</Grid>
</Grid>
</Grid>
);
}
}
const mapStateToProps = (state, ownProps) => ({
pagination: new Pagination<object>({
page: +ownProps.location.query.p || 0, // current page
size: +ownProps.location.query.l || 50, // page size
sort: ownProps.location.query.s, // page sorts
dir: ownProps.location.query.d, // page sort directions
filter: null,
}),
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
listCrops,
listAccessions,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(BrowsePage));
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { withStyles } from 'material-ui/styles';
import {log} from 'utilities/debug';
import { loadDescriptorList, publishDescriptorList, deleteDescriptorList } from 'actions/descriptorList';
import { DescriptorList, Descriptor } from 'model/descriptor.model';
import confirm from 'utilities/confirmAlert';
import Authorize from 'ui/common/authorized/Authorize';
import Loading from 'ui/common/Loading';
import Markdown from 'ui/common/markdown';
import DescriptorCard from 'ui/catalog/descriptor/DescriptorCard';
import ContentHeaderWithButton from 'ui/common/heading/ContentHeaderWithButton';
import { PartnerLink, DescriptorListLink, CropLink } from 'ui/catalog/Links';
import { Properties, PropertiesItem } from 'ui/catalog/Properties';
import Grid from 'material-ui/Grid';
import Card, {CardHeader, CardContent, CardActions } from 'ui/common/Card';
import Section from 'ui/common/layout/Section';
import Divider from 'material-ui/Divider';
import Button from 'material-ui/Button';
interface IDescriptorListPageProps extends React.ClassAttributes<any> {
classes: any;
uuid?: string;
descriptorList?: DescriptorList;
loading?: any;
loadDescriptorList: any;
publishDescriptorList: any;
deleteDescriptorList: any;
}
const styles = (theme) => ({
filterSection: theme.leftPanel.root,
contentContainer: {
backgroundColor: '#E8E5E0',
padding: '1.5rem',
},
card: {
marginBottom: '1.5rem',
},
propertiesContainer: {
marginTop: '20px',
marginBottom: '20px',
},
propertiesRow: {
/* tslint:disable */
'marginTop': '1px',
'marginBottom': '1px',
'& > *:first-child': {
borderRight: 'solid 1px white',
},
'&:nth-child(even)': {
backgroundColor: '#f8f7f5',
},
'&:nth-child(odd)': {
backgroundColor: '#f3f2ee',
},
/* tslint:enable */
},
});
// Page to edit a descriptor list
class DescriptorListPage extends React.Component<IDescriptorListPageProps, any> {
protected static needs = [
({ params: { uuid } }) => loadDescriptorList(uuid),
];
public componentWillMount() {
const {uuid, loading, loadDescriptorList} = this.props;
if (uuid && (! loading || loading.uuid !== uuid)) {
loadDescriptorList(uuid);
}
}
private onPublish = (e) => {
const {descriptorList, publishDescriptorList} = this.props;
confirm(<span>Publish <b>{ descriptorList.title }</b>?</span>, {
description: `After publishing the descriptor list no changes are permitted, a new version must be created.`,
confirmLabel: 'Publish',
abortLabel: 'Cancel',
}).then(() => {
log('Publishing descriptor list', descriptorList);
publishDescriptorList(descriptorList);
}).catch(() => {
// don't
});
}
private onUnpublish = (e) => {
const {descriptorList, publishDescriptorList} = this.props;
confirm(<span>Unpublish <b>{ descriptorList.title }</b>?</span>, {
// description: `Deleting the descriptor is only possible when there is no associated data.`,
confirmLabel: 'Unpublish',
abortLabel: 'Cancel',
}).then(() => {
log('Publishing descriptor list', descriptorList);
publishDescriptorList(descriptorList, false);
}).catch(() => {
// don't
});
}
private onDelete = (e) => {
const {descriptorList, deleteDescriptorList} = this.props;
confirm(<span>Delete <b>{ descriptorList.title }</b>?</span>, {
description: `Deleting the descriptor is only possible when there is no associated data.`,
confirmLabel: 'Delete',
abortLabel: 'Cancel',
}).then(() => {
deleteDescriptorList(descriptorList);
}).catch(() => {
// don't
});
}