diff --git a/.env.defaults b/.env.defaults
index fabcb48..ab6f80f 100644
--- a/.env.defaults
+++ b/.env.defaults
@@ -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
diff --git a/.env.local b/.env.local
index 13f4285..60872ec 100644
--- a/.env.local
+++ b/.env.local
@@ -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
diff --git a/src/client/components/network-editor/export-controller.js b/src/client/components/network-editor/export-controller.js
index 38e8f59..cf88b74 100644
--- a/src/client/components/network-editor/export-controller.js
+++ b/src/client/components/network-editor/export-controller.js
@@ -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'
};
@@ -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);
@@ -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]);
@@ -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({
diff --git a/src/client/components/network-editor/pathway-table.js b/src/client/components/network-editor/pathway-table.js
index c1204d9..d76db60 100644
--- a/src/client/components/network-editor/pathway-table.js
+++ b/src/client/components/network-editor/pathway-table.js
@@ -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);
diff --git a/src/client/components/report/index.js b/src/client/components/report/index.js
new file mode 100644
index 0000000..4b9bca6
--- /dev/null
+++ b/src/client/components/report/index.js
@@ -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 (
+
+
+
+
+ );
+}
+
+ReportHome.propTypes = {
+ secret: PropTypes.string,
+};
+
+export default ReportHome;
\ No newline at end of file
diff --git a/src/client/components/report/report.js b/src/client/components/report/report.js
new file mode 100644
index 0000000..ca225f7
--- /dev/null
+++ b/src/client/components/report/report.js
@@ -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
Loading...
;
+ } else if(report === 'error') {
+ return Error fetching report.
;
+ }
+
+ const comparator = getComparator(order, orderBy);
+ const sortedNetworks = report.networks.sort(comparator);
+
+ return
+
EnrichmentMap:RNA-Seq - Usage Report
+
Demo Networks: {report.counts.demo}
+
User Created Networks ({report.counts.user}):
+
+ Sort:
+
+
+
+
+
+
+
+
+
+
+ Network Name
+ Nodes
+ Edges
+ Type
+ Creation Time
+ Last Access Time
+
+
+
+
+ {sortedNetworks.map(network => {
+ const createTime = new Date(network.creationTime).toLocaleString('en-CA');
+ const accessTime = new Date(network.lastAccessTime).toLocaleString('en-CA');
+ return (
+
+ {network.networkName}
+ {network.nodeCount}
+ {network.edgeCount}
+ {network.inputType}
+ {createTime}
+ {accessTime}
+ open
+
+ );
+ })}
+
+
+
+
;
+}
+
+Report.propTypes = {
+ secret: PropTypes.string,
+};
+
+export default Report;
\ No newline at end of file
diff --git a/src/client/router.js b/src/client/router.js
index bfb89fb..5ff8e95 100644
--- a/src/client/router.js
+++ b/src/client/router.js
@@ -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();
@@ -38,6 +39,13 @@ export function Router() {
)}
/>
+ {
+ const secret = _.get(props, ['match', 'params', 'secret'], _.get(props, 'secret'));
+ return ;
+ }}
+ />
(
diff --git a/src/server/datastore.js b/src/server/datastore.js
index ec4af50..5754d59 100644
--- a/src/server/datastore.js
+++ b/src/server/datastore.js
@@ -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
diff --git a/src/server/env.js b/src/server/env.js
index 3417945..6fedc3f 100644
--- a/src/server/env.js
+++ b/src/server/env.js
@@ -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;
diff --git a/src/server/index.js b/src/server/index.js
index fe1b117..9206fe9 100755
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -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';
@@ -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) {
diff --git a/src/server/routes/api/index.js b/src/server/routes/api/index.js
index 8fff02e..16292ca 100644
--- a/src/server/routes/api/index.js
+++ b/src/server/routes/api/index.js
@@ -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();
diff --git a/src/server/routes/api/report.js b/src/server/routes/api/report.js
new file mode 100644
index 0000000..073aed4
--- /dev/null
+++ b/src/server/routes/api/report.js
@@ -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;
\ No newline at end of file