Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/report #301

Merged
merged 3 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 2 additions & 2 deletions src/client/components/network-editor/pathway-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ 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) => {
// null values come last in ascending!
if (a[orderBy] == null) {
return -1;
Expand All @@ -261,7 +261,7 @@ const descendingComparator = (a, b, orderBy) => {
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
46 changes: 46 additions & 0 deletions src/server/routes/api/report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Express from 'express';
import Datastore from '../../datastore.js';
import { writeCursorToResult } from './index.js';
import { REPORT_SECRET } from '../../env.js';

const http = Express.Router();

// Return enrichment results in TSV format
http.get(`/count/:secret`, async function(req, res, next) {

try {
const { secret } = req.params;
if(secret !== REPORT_SECRET) {
res.sendStatus(404);
return;
}

const counts = await Datastore.getNetworkCounts();
res.send(JSON.stringify(counts));

} catch(err) {
next(err);
}
});

// Return enrichment results in TSV format
http.get(`/networks/:secret`, async function(req, res, next) {
try {
const { secret } = req.params;
if(secret !== REPORT_SECRET) {
res.sendStatus(404);
return;
}

const cursor = await Datastore.getNetworkStatsCursor();
await writeCursorToResult(cursor, res);
cursor.close();

} catch(err) {
next(err);
} finally {
res.end();
}
});

export default http;
Loading