diff --git a/package-lock.json b/package-lock.json index 7ba99706f..ae69b1d78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,14 +5,15 @@ "requires": true, "packages": { "": { + "name": "bento_web", "version": "0.2.0", "license": "LGPL-3.0-only", "dependencies": { "@babel/plugin-proposal-class-properties": "^7.12.1", "antd": "^3.26.19", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "file-saver": "^2.0.5", - "igv": "https://github.com/bento-platform/igv.js/archive/bento.2020-10-30.tar.gz", + "igv": "^2.10.1", "iso8601-duration": "^1.2.0", "prop-types": "^15.7.2", "query-string": "^6.13.7", @@ -20,7 +21,6 @@ "react-dom": "^16.14.0", "react-json-view": "^1.21.1", "react-redux": "^7.2.2", - "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-router-prop-types": "^1.0.5", "react-syntax-highlighter": "^13.5.3", @@ -43,6 +43,7 @@ "css-loader": "^5.0.0", "eslint": "^7.12.0", "eslint-plugin-react": "^7.21.5", + "file-loader": "^6.2.0", "html-webpack-plugin": "^4.5.0", "style-loader": "^2.0.0", "webpack": "^4.44.2", @@ -1784,9 +1785,9 @@ "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, "node_modules/@types/node": { @@ -3619,11 +3620,11 @@ } }, "node_modules/cross-fetch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", - "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", "dependencies": { - "node-fetch": "2.6.1" + "node-fetch": "2.6.7" } }, "node_modules/cross-spawn": { @@ -5130,6 +5131,58 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/file-saver": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", @@ -5835,10 +5888,9 @@ } }, "node_modules/igv": { - "version": "2.6.8", - "resolved": "https://github.com/bento-platform/igv.js/archive/bento.2020-10-30.tar.gz", - "integrity": "sha1-ZIYfMCook25+sMMgtmE2cCGuwoU= sha512-B0XbuB0keZVzTZO0+m/8vtI9IU3gApgCEAiy/zcsrvO8q2ftD5rt3+IM51QmQsrOLCSaDFzwoLEzT9kdi1Vm/Q==", - "license": "MIT" + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/igv/-/igv-2.13.1.tgz", + "integrity": "sha512-XqFqHLfDxgl6WSk6Cy2hSMfjg5mhzWFRxk+6joTKkZHXcprSvfYNNLB+ggjrTLkNPBf1p9JikHqwirEIJIdSqg==" }, "node_modules/immutable": { "version": "3.7.6", @@ -6901,11 +6953,22 @@ } }, "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/node-libs-browser": { @@ -10287,6 +10350,11 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-essentials": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz", @@ -11175,6 +11243,11 @@ "node": ">=0.10" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, "node_modules/webpack": { "version": "4.46.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", @@ -11353,6 +11426,15 @@ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12735,9 +12817,9 @@ "dev": true }, "@types/json-schema": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, "@types/node": { @@ -14320,11 +14402,11 @@ } }, "cross-fetch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", - "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", "requires": { - "node-fetch": "2.6.1" + "node-fetch": "2.6.7" } }, "cross-spawn": { @@ -15555,6 +15637,40 @@ "flat-cache": "^3.0.4" } }, + "file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, "file-saver": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", @@ -16092,8 +16208,9 @@ "dev": true }, "igv": { - "version": "https://github.com/bento-platform/igv.js/archive/bento.2020-10-30.tar.gz", - "integrity": "sha1-ZIYfMCook25+sMMgtmE2cCGuwoU= sha512-B0XbuB0keZVzTZO0+m/8vtI9IU3gApgCEAiy/zcsrvO8q2ftD5rt3+IM51QmQsrOLCSaDFzwoLEzT9kdi1Vm/Q==" + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/igv/-/igv-2.13.1.tgz", + "integrity": "sha512-XqFqHLfDxgl6WSk6Cy2hSMfjg5mhzWFRxk+6joTKkZHXcprSvfYNNLB+ggjrTLkNPBf1p9JikHqwirEIJIdSqg==" }, "immutable": { "version": "3.7.6", @@ -16931,9 +17048,12 @@ } }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } }, "node-libs-browser": { "version": "2.2.1", @@ -19735,6 +19855,11 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "ts-essentials": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz", @@ -20536,6 +20661,11 @@ } } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, "webpack": { "version": "4.46.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", @@ -20658,6 +20788,15 @@ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 31bdc8eb6..b922765af 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dependencies": { "@babel/plugin-proposal-class-properties": "^7.12.1", "antd": "^3.26.19", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "file-saver": "^2.0.5", "igv": "^2.10.1", "iso8601-duration": "^1.2.0", @@ -40,6 +40,7 @@ "eslint-plugin-react": "^7.21.5", "file-loader": "^6.2.0", "html-webpack-plugin": "^4.5.0", + "prettier": "^2.7.1", "style-loader": "^2.0.0", "webpack": "^4.44.2", "webpack-cli": "^4.1.0", @@ -67,6 +68,7 @@ "trailingComma": "es5", "tabWidth": 4, "semi": true, - "singleQuote": false + "singleQuote": false, + "printWidth": 120 } } diff --git a/src/components/ServiceList.js b/src/components/ServiceList.js index 42bdd1f73..324fae5f7 100644 --- a/src/components/ServiceList.js +++ b/src/components/ServiceList.js @@ -42,7 +42,9 @@ const serviceColumns = (isOwner) => [ { title: "URL", dataIndex: "serviceInfo.url", - render: (url) => {`${url}/service-info`}, + // url is undefined when service-registry does not receive replies from + // the container. + render: (url) => url ? {`${url}/service-info`} : "N/A", }, { title: "Status", diff --git a/src/components/datasets/DatasetForm.js b/src/components/datasets/DatasetForm.js index ccfebd9b0..dd968cfed 100644 --- a/src/components/datasets/DatasetForm.js +++ b/src/components/datasets/DatasetForm.js @@ -1,51 +1,57 @@ -import React, {Component} from "react"; +import React from "react"; import PropTypes from "prop-types"; -import {Form, Input} from "antd"; +import { Form, Input } from "antd"; +const { Item } = Form; import DataUseInput from "../DataUseInput"; -import {DATA_USE_PROP_TYPE_SHAPE, INITIAL_DATA_USE_VALUE} from "../../duo"; -import {simpleDeepCopy} from "../../utils/misc"; +import { DATA_USE_PROP_TYPE_SHAPE, INITIAL_DATA_USE_VALUE } from "../../duo"; +import { simpleDeepCopy } from "../../utils/misc"; - -class DatasetForm extends Component { - render() { - return
- - {this.props.form.getFieldDecorator("title", { - initialValue: (this.props.initialValue || {title: ""}).title || "", - rules: [{required: true}, {min: 3}] +const DatasetForm = ({ style, initialValue, form }) => { + return ( + + + {form.getFieldDecorator("title", { + initialValue: (initialValue || { title: "" }).title || "", + rules: [{ required: true }, { min: 3 }], })()} - - - {this.props.form.getFieldDecorator("description", { - initialValue: (this.props.initialValue || {description: ""}).description || "", - rules: [{required: true}] + + + {form.getFieldDecorator("description", { + initialValue: (initialValue || { description: "" }).description || "", + rules: [{ required: true }], })()} - - - {this.props.form.getFieldDecorator("contact_info", { - initialValue: (this.props.initialValue || {contact_info: ""}).contact_info || "", - })()} - - - {this.props.form.getFieldDecorator("data_use", { - initialValue: ((this.props.initialValue || - {data_use: simpleDeepCopy(INITIAL_DATA_USE_VALUE)}).data_use || - simpleDeepCopy(INITIAL_DATA_USE_VALUE)), - rules: [{required: true}, (rule, value, callback) => { - if (!(value.consent_code || {}).primary_category) { - callback(["Please specify one primary consent code"]); - return; - } - callback([]); - }] + + + {form.getFieldDecorator("contact_info", { + initialValue: (initialValue || { contact_info: "" }).contact_info || "", + })()} + + + {form.getFieldDecorator("data_use", { + initialValue: + ( + initialValue || { + data_use: simpleDeepCopy(INITIAL_DATA_USE_VALUE), + } + ).data_use || simpleDeepCopy(INITIAL_DATA_USE_VALUE), + rules: [ + { required: true }, + (rule, value, callback) => { + if (!(value.consent_code || {}).primary_category) { + callback(["Please specify one primary consent code"]); + return; + } + callback([]); + }, + ], })()} - -
; - } -} + + + ); +}; DatasetForm.propTypes = { style: PropTypes.object, @@ -53,8 +59,8 @@ DatasetForm.propTypes = { title: PropTypes.string, description: PropTypes.string, contact_info: PropTypes.string, - data_use: DATA_USE_PROP_TYPE_SHAPE, // TODO: Shared shape for data use - }) + data_use: DATA_USE_PROP_TYPE_SHAPE, // TODO: Shared shape for data use + }), }; -export default Form.create({name: "dataset_form"})(DatasetForm); +export default Form.create({ name: "dataset_form" })(DatasetForm); diff --git a/src/components/datasets/DatasetTables.js b/src/components/datasets/DatasetTables.js index a1cca80e1..1260d6032 100644 --- a/src/components/datasets/DatasetTables.js +++ b/src/components/datasets/DatasetTables.js @@ -1,10 +1,9 @@ -import React, {Component} from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; -import {bindActionCreators} from "redux"; -import {connect} from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; -import {Button, Col, Row, Table, Typography} from "antd"; +import { Button, Col, Row, Table, Typography } from "antd"; import TableAdditionModal from "./table/TableAdditionModal"; import TableDeletionModal from "./table/TableDeletionModal"; @@ -12,173 +11,181 @@ import TableDeletionModal from "./table/TableDeletionModal"; import { addProjectTable, deleteProjectTableIfPossible, - fetchProjectsWithDatasetsAndTables + fetchProjectsWithDatasetsAndTables, } from "../../modules/metadata/actions"; -import {nop} from "../../utils/misc"; -import {fetchTableSummaryIfPossible} from "../../modules/tables/actions"; +import { nop } from "../../utils/misc"; +import { fetchTableSummaryIfPossible } from "../../modules/tables/actions"; import TableSummaryModal from "./table/TableSummaryModal"; -import {datasetPropTypesShape, projectPropTypesShape, serviceInfoPropTypesShape} from "../../propTypes"; +import { datasetPropTypesShape, projectPropTypesShape } from "../../propTypes"; +const NA_TEXT = N/A; -const NA_TEXT = (N/A); +const DatasetTables = ({ isPrivate, project, dataset, onTableIngest, isFetchingTables }) => { + const dispatch = useDispatch(); -class DatasetTables extends Component { - constructor(props) { - super(props); + const chordServicesByArtifact = useSelector((state) => state.chordServices.itemsByArtifact); + const serviceInfoByArtifact = useSelector((state) => state.services.itemsByArtifact); - this.state = { - additionModalVisible: false, - deletionModalVisible: false, - tableSummaryModalVisible: false, - selectedTable: null, - }; + const [additionModalVisible, setAdditionModalVisible] = useState(false); + const [deletionModalVisible, setDeletionModalVisible] = useState(false); + const [tableSummaryModalVisible, setTableSummaryModalVisible] = useState(false); + const [selectedTable, setSelectedTable] = useState(null); - this.handleAdditionClick = this.handleAdditionClick.bind(this); - this.handleAdditionCancel = this.handleAdditionCancel.bind(this); - this.handleAdditionSubmit = this.handleAdditionSubmit.bind(this); - - this.handleTableDeletionClick = this.handleTableDeletionClick.bind(this); - this.handleTableDeletionCancel = this.handleTableDeletionCancel.bind(this); - this.handleTableDeletionSubmit = this.handleTableDeletionSubmit.bind(this); - - this.showTableSummaryModal = this.showTableSummaryModal.bind(this); - } - - handleAdditionClick() { - this.setState({additionModalVisible: true}); - } - - handleAdditionCancel() { - this.setState({additionModalVisible: false}); - } - - async handleAdditionSubmit(values) { + const handleAdditionSubmit = async (values) => { const [serviceArtifact, dataTypeID] = values.dataType.split(":"); - const serviceInfo = this.props.serviceInfoByArtifact[serviceArtifact]; - await this.props.addProjectTable(this.props.dataset.identifier, serviceInfo, dataTypeID, values.name); - - await this.props.fetchProjectsWithDatasetsAndTables(); // TODO: If needed / only this project... - - this.setState({additionModalVisible: false}); - } - - handleTableDeletionClick(t) { - this.setState({deletionModalVisible: true, selectedTable: t}); - } - - handleTableDeletionCancel() { - this.setState({deletionModalVisible: false}); - } - - async handleTableDeletionSubmit() { - if (this.state.selectedTable === null) return; - await this.props.deleteProjectTable(this.state.selectedTable); - - await this.props.fetchProjectsWithDatasetsAndTables(); // TODO: If needed / only this project... - - this.setState({deletionModalVisible: false}); - } - - showTableSummaryModal(table) { - this.props.fetchTableSummaryIfPossible(this.props.chordServicesByArtifact[table.service_artifact], - this.props.serviceInfoByArtifact[table.service_artifact], table.table_id); // TODO - this.setState({tableSummaryModalVisible: true, selectedTable: table}); - } - - render() { - const tableListColumns = [ - { - title: "ID", - dataIndex: "table_id", - render: (tableID, t) => this.props.isPrivate - ? this.showTableSummaryModal(t)}>{tableID} - : tableID, - }, - { - title: "Name", - dataIndex: "name", - render: n => (n ? n : NA_TEXT), - defaultSortOrder: "ascend", - sorter: (a, b) => (a.name && b.name) - ? a.name.localeCompare(b.name) - : a.table_id.localeCompare(b.table_id) - }, - {title: "Data Type", dataIndex: "data_type"}, - ...(this.props.isPrivate ? [ + const serviceInfo = serviceInfoByArtifact[serviceArtifact]; + await dispatch(addProjectTable(project, dataset.identifier, serviceInfo, dataTypeID, values.name)); + + await dispatch(fetchProjectsWithDatasetsAndTables()); // TODO: If needed / only this project... + + setAdditionModalVisible(false); + }; + + const handleTableDeletionClick = (t) => { + setDeletionModalVisible(true); + setSelectedTable(t); + }; + + const handleTableDeletionSubmit = async () => { + if (selectedTable === null) return; + await dispatch(deleteProjectTableIfPossible(project, selectedTable)); + await dispatch(fetchProjectsWithDatasetsAndTables()); // TODO: If needed / only this project... + + setDeletionModalVisible(false); + }; + + const showTableSummaryModal = (table) => { + dispatch( + fetchTableSummaryIfPossible( + chordServicesByArtifact[table.service_artifact], + serviceInfoByArtifact[table.service_artifact], + table.table_id + ) + ); + setTableSummaryModalVisible(true); + setSelectedTable(table); + }; + + const tableListColumns = [ + { + title: "ID", + dataIndex: "table_id", + render: (tableID, t) => + isPrivate ? ( + showTableSummaryModal(t)}> + {tableID} + + ) : ( + {tableID} + ), + }, + { + title: "Name", + dataIndex: "name", + render: (n) => (n ? n : NA_TEXT), + defaultSortOrder: "ascend", + sorter: (a, b) => (a.name && b.name ? a.name.localeCompare(b.name) : a.table_id.localeCompare(b.table_id)), + }, + { title: "Data Type", dataIndex: "data_type" }, + ...(isPrivate + ? [ { title: "Actions", key: "actions", - width: 230, /*330,*/ - render: t => ( - - - - - {/* TODO: Edit Table Name: v0.2 */} - {/**/} - {t.manageable !== false ? ( - - ) : null} - - ) - } - ] : []) - ]; - - const dataset = this.props.dataset || {}; - const tables = (dataset.tables || []).map(t => ({...t, name: t.name || null})); - return <> + width: 230 /*330,*/, + render: (t) => ( + + + + + {/* TODO: Edit Table Name: v0.2 */} + {/**/} + {t.manageable !== false ? ( + + + + ) : null} + + ), + }, + ] + : []), + ]; + + dataset = dataset || {}; + const tables = (dataset.tables || []).map((t) => ({ + ...t, + name: t.name || null, + })); + return ( + <> Tables - {this.props.isPrivate ? ( -
+ {isPrivate ? ( +
{/* TODO: Implement v0.2 - {(this.props.strayTables || []).length > 0 ? ( + {(strayTables || []).length > 0 ? ( ) : null} */} -
) : null} - (TODO: List of files)} TODO: Implement v0.2 - columns={tableListColumns} - loading={this.props.isFetchingTables} /> - - this.setState({tableSummaryModalVisible: false})} /> - - this.handleAdditionSubmit(vs)} - onCancel={() => this.handleAdditionCancel()} /> - - this.handleTableDeletionSubmit()} - onCancel={() => this.handleTableDeletionCancel()} /> - ; - } -} +
(TODO: List of files)} TODO: Implement v0.2 + columns={tableListColumns} + loading={isFetchingTables} + /> + + setTableSummaryModalVisible(false)} + /> + + setAdditionModalVisible(false)} + /> + + setDeletionModalVisible(false)} + /> + + ); +}; DatasetTables.propTypes = { isPrivate: PropTypes.bool, @@ -186,48 +193,6 @@ DatasetTables.propTypes = { dataset: datasetPropTypesShape, onTableIngest: PropTypes.func, isFetchingTables: PropTypes.bool, - - chordServicesByArtifact: PropTypes.objectOf(PropTypes.shape({ - apt_dependencies: PropTypes.arrayOf(PropTypes.string), - data_service: PropTypes.bool, - manageable_tables: PropTypes.string, - python_callable: PropTypes.string, - python_module: PropTypes.string, - repository: PropTypes.string, - run_environment: PropTypes.objectOf(PropTypes.string), - service_runnable: PropTypes.string, - type: PropTypes.shape({ - artifact: PropTypes.string.isRequired, - language: PropTypes.string, - organization: PropTypes.string, - }), - wsgi: PropTypes.bool, - - post_stop_commands: PropTypes.arrayOf(PropTypes.string), - post_start_commands: PropTypes.arrayOf(PropTypes.string), - pre_install_commands: PropTypes.arrayOf(PropTypes.string), - pre_start_commands: PropTypes.arrayOf(PropTypes.string), - })), - serviceInfoByArtifact: PropTypes.objectOf(serviceInfoPropTypesShape), - - addProjectTable: PropTypes.func, - deleteProjectTable: PropTypes.func, - fetchProjectsWithDatasetsAndTables: PropTypes.func, - fetchTableSummaryIfPossible: PropTypes.func, }; -const mapStateToProps = state => ({ - chordServicesByArtifact: state.chordServices.itemsByArtifact, - serviceInfoByArtifact: state.services.itemsByArtifact, -}); - -const mapDispatchToProps = (dispatch, ownProps) => ({ - addProjectTable: (ds, s, dt, name) => dispatch(addProjectTable(ownProps.project, ds, s, dt, name)), - deleteProjectTable: table => dispatch(deleteProjectTableIfPossible(ownProps.project, table)), - ...bindActionCreators({ - fetchProjectsWithDatasetsAndTables, - fetchTableSummaryIfPossible, - }, dispatch), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(DatasetTables); +export default DatasetTables; diff --git a/src/components/explorer/ExplorerDatasetSearch.js b/src/components/explorer/ExplorerDatasetSearch.js index 67e570eac..ddfbc0a46 100644 --- a/src/components/explorer/ExplorerDatasetSearch.js +++ b/src/components/explorer/ExplorerDatasetSearch.js @@ -46,7 +46,7 @@ const SEARCH_RESULT_COLUMNS = [ dataIndex: "biosamples", render: samples => <> {samples.length} Sample{samples.length === 1 ? "" : "s"}{samples.length ? ": " : ""} - {samples.map(b => b.id).join(", ")} + {samples.join(", ")} , sorter: (a, b) => a.biosamples.length - b.biosamples.length, sortDirections: ["descend", "ascend", "descend"], @@ -54,8 +54,8 @@ const SEARCH_RESULT_COLUMNS = [ { title: "Experiments", dataIndex: "experiments", - render: experiments => <>{experiments.length} Experiment{experiments.length === 1 ? "" : "s"}, - sorter: (a, b) => a.experiments.length - b.experiments.length, + render: experiments => <>{experiments} Experiment{experiments === 1 ? "" : "s"}, + sorter: (a, b) => a.experiments - b.experiments, sortDirections: ["descend", "ascend", "descend"], }, ]; @@ -117,7 +117,7 @@ class ExplorerDatasetSearch extends Component { ? (this.state.currentPage * this.state.pageSize) - this.state.pageSize + 1 : 0; - console.log("search results: " + this.props.searchResults); + console.log("search results: ", this.props.searchResults); return <> Explore Dataset {selectedDataset.title} @@ -142,9 +142,9 @@ class ExplorerDatasetSearch extends Component { onClick={() => this.setState({tracksModalVisible: true})} disabled={true}> Visualize Tracks */} - + onClick={() => this.setState({summaryModalVisible: true})}>View Summary */} - } - value={numVCFs} /> + } value={numVCFs} /> - ; - } - - updateDimensions = () => { - if (window.innerWidth < 576) { //xs - this.setState({ - chartPadding: "0rem", - chartWidthHeight: window.innerWidth, - chartLabelPaddingTop: 3, - chartLabelPaddingLeft: 3 - }); - } else if (window.innerWidth < 768) { // sm - this.setState({ - chartPadding: "1rem", - chartWidthHeight: window.innerWidth, - chartLabelPaddingTop: 3, - chartLabelPaddingLeft: 6 }); - } else if (window.innerWidth < 992) { // md - this.setState({ - chartPadding: "2rem", - chartWidthHeight: window.innerWidth, - chartLabelPaddingTop: 3, - chartLabelPaddingLeft: 5 }); - } else if (window.innerWidth < 1200) { // lg - this.setState({ - chartPadding: "4rem", - chartWidthHeight: window.innerWidth / 2, - chartLabelPaddingTop: 3, - chartLabelPaddingLeft: 6 }); - } else if (window.innerWidth < 1600) { // xl - this.setState({ - chartPadding: "6rem", - chartWidthHeight: window.innerWidth / 2, - chartLabelPaddingTop: 3, - chartLabelPaddingLeft: 7 }); - } else { - this.setState({ - chartPadding: "10rem", - chartWidthHeight: window.innerWidth / 2, - chartLabelPaddingTop: 5, - chartLabelPaddingLeft: 7 }); // > xl - } - } - - componentDidMount() { - this.updateDimensions(); - window.addEventListener("resize", this.updateDimensions); - } - - componentWillUnmount() { - window.removeEventListener("resize", this.updateDimensions); - } -} + + ); +}; -export default connect(mapStateToProps, actionCreators)(VariantsSummary); +export default VariantsSummary; diff --git a/src/modules/explorer/actions.js b/src/modules/explorer/actions.js index 2958e378e..21141db4b 100644 --- a/src/modules/explorer/actions.js +++ b/src/modules/explorer/actions.js @@ -2,6 +2,8 @@ import {createNetworkActionTypes, networkAction} from "../../utils/actions"; import {jsonRequest} from "../../utils/requests"; import {extractQueriesFromDataTypeForms} from "../../utils/search"; +import FileSaver from "file-saver"; + export const PERFORM_SEARCH = createNetworkActionTypes("EXPLORER.PERFORM_SEARCH"); export const PERFORM_INDIVIDUAL_CSV_DOWNLOAD = createNetworkActionTypes("EXPLORER.PERFORM_INDIVIDUAL_CSV_DOWNLOAD"); export const ADD_DATA_TYPE_QUERY_FORM = "EXPLORER.ADD_DATA_TYPE_QUERY_FORM"; @@ -26,13 +28,6 @@ const performSearch = networkAction((datasetID, dataTypeQueries, excludeFromAuto err: "Error performing search", })); -const performIndividualCSVDownload = networkAction((individualsUrl) => () => ({ - types: PERFORM_INDIVIDUAL_CSV_DOWNLOAD, - url: individualsUrl, - parse: r => r.blob(), // Parse the CSV as a binary blob rather than e.g. a JSON file - err: "Error performing individual csv download", -})); - export const performSearchIfPossible = (datasetID) => (dispatch, getState) => { if (getState().explorer.fetchingSearchByDatasetID[datasetID]) return; @@ -57,21 +52,30 @@ export const performSearchIfPossible = (datasetID) => (dispatch, getState) => { export const performIndividualsDownloadCSVIfPossible = (datasetId, individualIds, allSearchResults) => (dispatch, getState) => { - console.log("Initiating PerformIndividualsDownloadCSVIfPossible"); - - let dataUrl = `${getState().services.itemsByArtifact.metadata.url}/api/individuals?format=csv`; - - // build query string - // TODO: This should use the actual JS API for URL construction - if (individualIds.length > 0) { // Get only selected results - dataUrl += ("&page_size=" + individualIds.length); - individualIds.forEach(id => dataUrl += `&id=${id}`); - } else { // Get all search results - dataUrl += ("&page_size=" + allSearchResults.length); - allSearchResults.forEach(sr => dataUrl += `&id=${sr.key}`); - } - return dispatch(performIndividualCSVDownload(dataUrl)); + const ids = individualIds.length ? individualIds : allSearchResults.map(sr => sr.key); + + const myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/json"); + + const raw = JSON.stringify({ + "id": ids, + "format": "csv" + }); + + const requestOptions = { + method: "POST", + headers: myHeaders, + body: raw, + redirect: "follow" + }; + + fetch(`${getState().services.itemsByArtifact.metadata.url}/api/batch/individuals`, requestOptions) + .then(response => response.blob()) + .then(result => { + FileSaver.saveAs(result, "data.csv"); + }) + .catch(error => console.log("error", error)); }; @@ -124,7 +128,8 @@ export const neutralizeAutoQueryPageTransition = () => ({ const performFreeTextSearch = networkAction((datasetID, term) => (dispatch, getState) => ({ types: FREE_TEXT_SEARCH, params: {datasetID}, - url: `${getState().services.metadataService.url}/api/individuals?search=${term}&page_size=10000`, + url: `${getState().services.metadataService.url}/api/individuals?search=${term}&page_size=10000` + + "&format=bento_search_result", err: `Error searching in all records with term ${term}`, })); diff --git a/src/modules/explorer/reducers.js b/src/modules/explorer/reducers.js index 9baf197d1..4c6b97558 100644 --- a/src/modules/explorer/reducers.js +++ b/src/modules/explorer/reducers.js @@ -178,7 +178,7 @@ export const explorer = ( ...state.searchResultsByDatasetID, [action.datasetID]: { results: freeTextResults(action.data), - searchFormattedResults: freeTextSearchFormattedResults(action.data.results), + searchFormattedResults: tableSearchResults(action.data), }, }, }; @@ -204,64 +204,19 @@ export const explorer = ( // helpers const tableSearchResults = (searchResults) => { - const results = (searchResults || {}).results || {}; - const tableResultSet = {}; - - // Collect all experiments (which have a biosample, not necessarily unique - // - one can perform multiple experiments on the same biosample) in arrays - // by their biosample ID. - const experimentsByBiosample = {}; - (results.experiment ?? []).forEach(experiment => { - const biosampleID = experiment.biosample; - if (!experimentsByBiosample.hasOwnProperty(biosampleID)) { - experimentsByBiosample[biosampleID] = []; - } - experimentsByBiosample[biosampleID].push(experiment); - }); - - // First, collect different data from phenopackets and sort them into an object for an individual - (results.phenopacket || []).forEach(p => { - const individualID = p.subject.id; - if (!tableResultSet.hasOwnProperty(individualID)) { - // We DO NOT initialize diseases, PFs, experiments etc. here because we may have - // multiple phenopackets on the same individual, and we want to combine this - // into one record for the individual. It gets populated separately below. - tableResultSet[individualID] = { - key: individualID, - individual: p.subject, - biosamples: {}, // Only includes biosamples from the phenopackets that matched the search query. - diseases: {}, // Only includes diseases from " - phenotypic_features: {}, - experiments: {}, // Filled from all the experiments conducted on the biosamples - }; - } - - // Accumulate biosamples based on individual ID and experiments by that biosample - // ID, de-duplicating in the process to event overlap if multiple phenopackets - // have the same biosample(s) or something weird. - (p.biosamples ?? []).forEach(b => { - tableResultSet[individualID].biosamples[b.id] = b; - (experimentsByBiosample[b.id] ?? []).forEach(e => tableResultSet[individualID].experiments[e.id] = e); - }); + const results = (searchResults || {}).results || []; - // Accumulate diseases and phenotypic features in the same manner. - (p.diseases ?? []).forEach(d => tableResultSet[individualID].diseases[d.id] = d); - (p.phenotypic_features ?? []).forEach(pf => tableResultSet[individualID].phenotypic_features[pf.type.id] = pf); + return results.map((p) => { + return { + key: p.subject_id, + individual: { + id: p.subject_id, + alternate_ids: p.alternate_ids ?? [] + }, + biosamples: p.biosamples, + experiments: p.num_experiments + }; }); - - // Now that the biosamples/diseases/PFs/experiments are de-duplicated, turn - // them into arrays and sort them in a consistent manner. Also flatten the - // stuff-by-individual object into a sorted array. - return Object.values(tableResultSet).map(i => ({ - ...i, - biosamples: Object.values(i.biosamples).sort((b1, b2) => b1.id.localeCompare(b2.id)), - diseases: Object.values(i.diseases).sort( - (d1, d2) => d1.id.toString().localeCompare(d2.id.toString())), - phenotypic_features: Object.values(i.phenotypic_features).sort( - (pf1, pf2) => pf1.type.id.localeCompare(pf2.type.id)), - experiments: Object.values(i.experiments).sort( - (e1, e2) => e1.id.toString().localeCompare(e2.id.toString())), - })).sort((i1, i2) => i1.key.localeCompare(i2.key)); }; @@ -284,28 +239,3 @@ function freeTextResults(_searchResults) { } }; } - -// produces searchFormattedResults (input for results table and other components) -// free-text equivalent to tableSearchResults() above -function freeTextSearchFormattedResults(searchResults) { - return searchResults.map(r => { - return { - key: r.id, - biosamples: (r.phenopackets || []).flatMap((p) => p.biosamples), - diseases: r.phenopackets[0].diseases, //TO FIX, multiple phenopackets possible - experiments: (r.phenopackets || []).flatMap((p) => - (p.biosamples || []).flatMap((b) => b?.experiments ? [b.experiments] : []) - ), - phenotypic_features: r.phenopackets[0].phenotypic_features || [], //TO FIX, as above - individual: { - age: r.age, - created: r.created, - date_of_birth: r.date_of_birth, - id: r.id, - karyotypic_sex: r.karyotypic_sex, - taxonomy: r.taxonomy, - updated: r.updated, - }, - }; - }); -} diff --git a/src/propTypes.js b/src/propTypes.js index abd02f88f..cb96d075b 100644 --- a/src/propTypes.js +++ b/src/propTypes.js @@ -361,10 +361,12 @@ export const explorerSearchResultsPropTypesShape = PropTypes.shape({ }), searchFormattedResults: PropTypes.arrayOf(PropTypes.shape({ key: PropTypes.string.isRequired, - individual: individualPropTypesShape, - biosamples: PropTypes.arrayOf(biosamplePropTypesShape), - diseases: PropTypes.arrayOf(diseasePropTypesShape), - experiments: PropTypes.arrayOf(experimentPropTypesShape), // TODO + individual: PropTypes.shape({ + id: PropTypes.string.isRequired, + alternate_ids: PropTypes.arrayOf(PropTypes.string) + }), + biosamples: PropTypes.arrayOf(PropTypes.string), + experiments: PropTypes.number })), });