Skip to content

Commit

Permalink
Merge pull request #301 from cytoscape/feature/report
Browse files Browse the repository at this point in the history
Feature/report
  • Loading branch information
mikekucera authored Jul 9, 2024
2 parents 1d9fd5c + 818e612 commit 305fbe0
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 7 deletions.
1 change: 1 addition & 0 deletions .env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ FGSEA_PRERANKED_SERVICE_URL=https://fgseadev.baderlab.net/v1/preranked
FGSEA_RNASEQ_SERVICE_URL=https://fgseadev.baderlab.net/v1/rnaseq
EM_SERVICE_URL=https://emjavadev.baderlab.net/v1
BRIDGEDB_URL=https://webservice.bridgedb.org
REPORT_SECRET=baderlab
1 change: 1 addition & 0 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ FGSEA_PRERANKED_SERVICE_URL=http://localhost:8000/preranked
FGSEA_RNASEQ_SERVICE_URL=http://localhost:8000/rnaseq
EM_SERVICE_URL=http://localhost:8080/v1
BRIDGEDB_URL=https://webservice.bridgedb.org
REPORT_SECRET=baderlab
13 changes: 13 additions & 0 deletions src/client/components/network-editor/export-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const Path = {
DATA_ENRICH: 'data/enrichment_results.txt',
DATA_RANKS: 'data/ranks.txt',
DATA_GENESETS: 'data/gene_sets.gmt',
DATA_JSON: 'data/network.json',
README: 'README.md'
};

Expand Down Expand Up @@ -60,6 +61,7 @@ export class ExportController {
const blob2 = await this._createNetworkImageBlob(ImageSize.LARGE);
const blob3 = await this._createNetworkPDFBlob();
const blob4 = await this._createSVGLegendBlob();
// const blob5 = await this._createNetworkJSONBlob();
const files = await filesPromise;
const readme = createREADME(this.controller);

Expand All @@ -69,6 +71,7 @@ export class ExportController {
zip.file(Path.IMAGE_LARGE, blob2);
zip.file(Path.IMAGE_PDF, blob3);
zip.file(Path.IMAGE_LEGEND, blob4);
// zip.file(Path.DATA_JSON, blob5);
zip.file(Path.DATA_ENRICH, files[0]);
zip.file(Path.DATA_RANKS, files[1]);
zip.file(Path.DATA_GENESETS, files[2]);
Expand Down Expand Up @@ -148,6 +151,16 @@ export class ExportController {
return blob;
}

async _createNetworkJSONBlob() {
const { cy } = this.controller;
const json = cy.json();
const blob = new Blob(
[ JSON.stringify(json, null, 2) ], {
type: 'text/plain'
});
return blob;
}

async _createNetworkPDFBlob() {
const { cy } = this.controller;
const blob = await cy.pdf({
Expand Down
17 changes: 11 additions & 6 deletions src/client/components/network-editor/pathway-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,24 +244,29 @@ const linkoutProps = { target: "_blank", rel: "noreferrer", underline: "hover"
const DEF_ORDER = 'desc';
const DEF_ORDER_BY = 'nes';

const descendingComparator = (a, b, orderBy) => {
export const descendingComparator = (a, b, orderBy) => {
const aVal = a[orderBy], bVal = b[orderBy];

// null values come last in ascending!
if (a[orderBy] == null) {
if (aVal == null) {
return -1;
}
if (b[orderBy] == null) {
if (bVal == null) {
return 1;
}
if (b[orderBy] < a[orderBy]) {
if(typeof aVal === 'string' && typeof bVal === 'string') {
return aVal.localeCompare(bVal, undefined, { sensitivity: 'accent' });
}
if (bVal < aVal) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
if (bVal > aVal) {
return 1;
}
return 0;
};

const getComparator = (order, orderBy) => {
export const getComparator = (order, orderBy) => {
return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
Expand Down
34 changes: 34 additions & 0 deletions src/client/components/report/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import { currentTheme } from '../../theme';
import Report from './report';


export function ReportHome({ secret }) {
const [ theme, setTheme ] = useState(currentTheme);

useEffect(() => {
// Listen for changes in the user's theme preference
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleThemeChange = () => setTheme(currentTheme());
mediaQuery.addEventListener('change', handleThemeChange);
return () => {
mediaQuery.removeEventListener('change', handleThemeChange);
};
}, []);

return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Report secret={secret} />
</ThemeProvider>
);
}

ReportHome.propTypes = {
secret: PropTypes.string,
};

export default ReportHome;
124 changes: 124 additions & 0 deletions src/client/components/report/report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { getComparator } from '../network-editor/pathway-table';
import { Select, MenuItem } from '@material-ui/core';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';


const useStyles = makeStyles((theme) => ({
orderBy: {
margin: theme.spacing(1),
minWidth: 200,
},
order: {
margin: theme.spacing(1),
minWidth: 120,
},
}));


async function fetchReport(secret) {
try {
const countRes = await fetch(`/api/report/count/${secret}`);
if(!countRes.ok) {
return 'error';
}
const networkRes = await fetch(`/api/report/networks/${secret}`);
if(!networkRes.ok) {
return 'error';
}

const counts = await countRes.json();
const networks = await networkRes.json();

return { counts, networks };
} catch(err) {
console.log(err);
return 'error';
}
}


export function Report({ secret }) {
const classes = useStyles();

const [ report, setReport ] = useState(null);
const [ order, setOrder] = useState('desc');
const [ orderBy, setOrderBy ] = useState('creationTime');

useEffect(() => {
fetchReport(secret).then(setReport);
}, []);

if(!report) {
return <div> Loading... </div>;
} else if(report === 'error') {
return <div> Error fetching report. </div>;
}

const comparator = getComparator(order, orderBy);
const sortedNetworks = report.networks.sort(comparator);

return <div style={{padding: '10px'}}>
<h1>EnrichmentMap:RNA-Seq - Usage Report</h1>
<h3>Demo Networks: {report.counts.demo}</h3>
<h3>User Created Networks ({report.counts.user}):</h3>
<div style={{"float":"right"}} >
Sort:
&nbsp;&nbsp;
<Select value={orderBy} onChange={(event) => setOrderBy(event.target.value)} className={classes.orderBy}>
<MenuItem value="networkName">Name</MenuItem>
<MenuItem value="creationTime">Creation Time</MenuItem>
<MenuItem value="lastAccessTime">Last Accessed Time</MenuItem>
</Select>
&nbsp;
<Select value={order} onChange={(event) => setOrder(event.target.value)} className={classes.order}>
<MenuItem value="desc">Desc</MenuItem>
<MenuItem value="asc">Asc</MenuItem>
</Select>
</div>
<br></br>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell><b>Network Name</b></TableCell>
<TableCell align="right"><b>Nodes</b></TableCell>
<TableCell align="right"><b>Edges</b></TableCell>
<TableCell align="right"><b>Type</b></TableCell>
<TableCell align="right"><b>Creation Time</b></TableCell>
<TableCell align="right"><b>Last Access Time</b></TableCell>
<TableCell align="right"> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{sortedNetworks.map(network => {
const createTime = new Date(network.creationTime).toLocaleString('en-CA');
const accessTime = new Date(network.lastAccessTime).toLocaleString('en-CA');
return (
<TableRow
key={network._id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell component="th" scope="row">{network.networkName}</TableCell>
<TableCell align="right">{network.nodeCount}</TableCell>
<TableCell align="right">{network.edgeCount}</TableCell>
<TableCell align="right">{network.inputType}</TableCell>
<TableCell align="right">{createTime}</TableCell>
<TableCell align="right">{accessTime}</TableCell>
<TableCell align="right"><a href={`/document/${network._id}`} target="_blank" rel = "noopener noreferrer">open</a></TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</div>;
}

Report.propTypes = {
secret: PropTypes.string,
};

export default Report;
8 changes: 8 additions & 0 deletions src/client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PageNotFound from './components/page-not-found';
import { RecentNetworksController } from './components/recent-networks-controller';
import { Home } from './components/home';
import { NetworkEditor } from './components/network-editor';
import { ReportHome } from './components/report';

const recentNetworksController = new RecentNetworksController();

Expand Down Expand Up @@ -38,6 +39,13 @@ export function Router() {
<Home {...props} recentNetworksController={recentNetworksController} />
)}
/>
<Route
path='/report/:secret'
render={(props) => {
const secret = _.get(props, ['match', 'params', 'secret'], _.get(props, 'secret'));
return <ReportHome secret={secret} />;
}}
/>
<Route
path='/document/:id/:secret'
render={(props) => (
Expand Down
45 changes: 45 additions & 0 deletions src/server/datastore.js
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,51 @@ class Datastore {
return cursor;
}


async getNetworkCounts() {
const result = await this.db
.collection(NETWORKS_COLLECTION)
.aggregate([
{ $facet: {
'user': [
{ $match: { demo: { $ne: true } }},
{ $count: 'total' }
],
'demo': [
{ $match: { demo: true }},
{ $count: 'total' }
],
}},
{ $project: {
"user": { $arrayElemAt: ["$user.total", 0] },
"demo": { $arrayElemAt: ["$demo.total", 0] },
}}
]).toArray();

return result[0];
}


async getNetworkStatsCursor() {
const cursor = await this.db
.collection(NETWORKS_COLLECTION)
.aggregate([
{ $match: { demo: { $ne: true } }},
{ $project: {
networkName: 1,
creationTime: 1,
lastAccessTime: 1,
geneSetCollection: 1,
inputType: 1,
nodeCount: { $size: '$network.elements.nodes' },
edgeCount: { $size: '$network.elements.edges' }
}}
]);

return cursor;
}


}

const ds = new Datastore(); // singleton
Expand Down
1 change: 1 addition & 0 deletions src/server/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const LOG_LEVEL = process.env.LOG_LEVEL;
export const BASE_URL = process.env.BASE_URL;
export const UPLOAD_LIMIT = process.env.UPLOAD_LIMIT;
export const TESTING = ('' + process.env.TESTING).toLowerCase() === 'true';
export const REPORT_SECRET = process.env.REPORT_SECRET;

// Service config
export const FGSEA_PRERANKED_SERVICE_URL = process.env.FGSEA_PRERANKED_SERVICE_URL;
Expand Down
2 changes: 2 additions & 0 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import indexRouter from './routes/index.js';
import apiRouter from './routes/api/index.js';
import createRouter, { createRouterErrorHandler } from './routes/api/create.js';
import exportRouter from './routes/api/export.js';
import reportRouter from './routes/api/report.js';

import Datastore, { GMT_2 } from './datastore.js';

Expand Down Expand Up @@ -108,6 +109,7 @@ app.use('/', indexRouter);
app.use('/api', apiRouter);
app.use('/api/create', createRouter);
app.use('/api/export', exportRouter);
app.use('/api/report', reportRouter);

// The error handler must be before any other error middleware and after all controllers
if (SENTRY) {
Expand Down
2 changes: 1 addition & 1 deletion src/server/routes/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ http.delete('/:netid/positions', async function(req, res, next) {
});


async function writeCursorToResult(cursor, res) {
export async function writeCursorToResult(cursor, res) {
res.write('[');
if(await cursor.hasNext()) {
const obj = await cursor.next();
Expand Down
Loading

0 comments on commit 305fbe0

Please sign in to comment.