diff --git a/README.md b/README.md index ff269b1..585eb3f 100755 --- a/README.md +++ b/README.md @@ -1,73 +1,23 @@ -# Template for Gene Expression Atlas and Single Cell Expression Atlas NPM packages - -## Instructions - -***Be sure to be running npm@4.0.0 or later. At least Node.js 8 LTS is strongly recommended.*** - -### Clone this repository -``` -git clone https://github.com/gxa/atlas-package my-package -cd my-package -rm -rf .git -git init -git remote add origin https://github.com/gxa/my-package.git -``` -Remember to create the new repository. The recommendation is to prefix the package name with “atlas-”. - -### Fill in package metadata -Fill in the fields `name`, `description` and `repository`. As a general rule the packages are prefixed with -“expression-atlas-” or “sc-atlas-”. Finally, replace or remove `README.md`. - -## Scripts - -### `prepack` -Runs the `build` script before `npm publish`. Only the `lib` directory is packaged, so make sure everything (including -assests such as CSS or images are there). - -### `postversion`, `postpublish` -After bumping the version with e.g. `npm version minor`, the package is automatically published and pushed, with all -tags, so new versions can be published in a single step. - -### `test` -`npm test` runs all phases of the test lifecycle (i.e. `pretest`, `test` and `posttest`); in case you’ve added support -for Coveralls you won’t likely want to run the `posttest` phase. If that’s the case just do `npx jest`. - -## Testing -Basic test boilerplate is included with [Jest](https://facebook.github.io/jest/) and -[Enzyme](http://airbnb.io/enzyme/). Jest is a test runner, an assertion library and a snapshot tester, whereas Enzyme -allows DOM testing. See the examples included in `__test__` to get an idea. - -### Continuous integration -If you want CI and nice passing/failing badges, enable the repository in [Travis CI](https://travis-ci.org/). Now, with each push, Travis CI will run your tests and generate a report. You can display a test status badge going to -Travis CI, clicking on the badge and pasting the Markdown embed snippet on your `README.md`. - -Enabling code coverage is very similar. You need to enable your repository in [Coveralls](https://coveralls.io/). -Every time that Travis is run, it will generate coverage information and send it to Coveralls for a coverage report. -If you go to Coveralls, you can also get a snippet to embed the coverage report shield on your readme file. - -## What’s included? -- [React 16 and PropTypes](https://facebook.github.io/react/) -- [URI.js](https://medialize.github.io/URI.js/) for URL manipulation (the rich version of `query-string`) -- [Babel](https://babeljs.io/) with presets `env` and `react` (see `.babelrc`) -- [Webpack 4 with Webpack-CLI and Webpack-Dev-Server](https://webpack.js.org/) -- [Jest](https://facebook.github.io/jest/) and [Enzyme](http://airbnb.io/enzyme/) for testing - -## Polyfills -No polyfills are included by default, but you might want one or both of these: -- [Fetch polyfill](https://github.com/github/fetch) -- [Babel polyfill](https://babeljs.io/docs/usage/polyfill/) - -### NPM -``` -npm install --save-dev whatwg-fetch @babel/polyfill -``` - -Tweak your `webpack.config.js` to include them in your entry points: -``` -entry: { - myComponent: [`@babel/polyfill`, `whatwg-fetch`, `./html/render.js`] - ... -} +# Atlas experiment table +We implement a sortable table header and check box table cell for downloading atlas experiments' files. [Evergreen Table](https://evergreen.segment.com/) component is used in this repository. + +## Table header/content props structure + +Table information is passed by an array of objects, named as `tableHeader`, including mandatory entries `type`, `title`, `width` and `dataParam`. +If the table cell links to another page, please indicate `link`, `resource`, `endpoint`, which will be transformed as a href to `host/resource/data[link]/endpoint` + +***For example:*** +``` +[ + {type: `plain`, title: `index`, width: 60, dataParam: null, link: null} + {type: `sort`, title: `Loaded date`, width: 140, dataParam: `lastUpdate`, link: null}, + {type: `search`, title: `species`, width: 200, dataParam: `species`, link: null}, + {type: `search`, title: `experiment description`, width: 360, dataParam: `experimentDescription`, + link: `experimentAccession`, resource: `experiments`, endpoint: `Results`}, + {type: `search`, title: `experiment factors`, width: 260, dataParam: `experimentalFactors`, link: null}, + {type: `sort`, title: `Number of assays`, width: 160, dataParam: `numberOfAssays`, + link: `experimentAccession`, resource: `experiments`, endpoint: `Experiment Design`} +] ``` ## Run it on your browser diff --git a/__test__/CalloutAlert.test.js b/__test__/CalloutAlert.test.js new file mode 100644 index 0000000..2d97491 --- /dev/null +++ b/__test__/CalloutAlert.test.js @@ -0,0 +1,31 @@ +import React from 'react' +import Enzyme from 'enzyme' +import renderer from 'react-test-renderer' +import { shallow } from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' + +import CalloutAlert from '../src/CalloutAlert' + +Enzyme.configure({ adapter: new Adapter() }) + +describe(`CalloutAlert`, () => { + const props = { + error: { + description: `A human-readable description of the error, hopefully useful to the user`, + name: `Error name`, + message: `Error message` + } + } + + it(`prints all the relevant error information`, () => { + const wrapper = shallow() + expect(wrapper.text()).toMatch(props.error.description) + expect(wrapper.text()).toMatch(props.error.name) + expect(wrapper.text()).toMatch(props.error.message) + }) + + it(`matches snapshot`, () => { + const tree = renderer.create().toJSON() + expect(tree).toMatchSnapshot() + }) +}) diff --git a/__test__/ExperimentTable.test.js b/__test__/ExperimentTable.test.js new file mode 100755 index 0000000..d65c130 --- /dev/null +++ b/__test__/ExperimentTable.test.js @@ -0,0 +1,121 @@ +import React from 'react' +import Enzyme from 'enzyme' +import {shallow, mount} from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' +import {getRandomInt, TableCellDiv, data, tableHeader} from './TestUtils' +import ExperimentTable from '../src/ExperimentTable' +import TableFooter from '../src/TableFooter' +import TableHeaderCells from '../src/TableHeaderCells' +import { Table } from 'evergreen-ui' +import _ from "lodash" + +Enzyme.configure({ adapter: new Adapter() }) + +describe(`ExperimentTable`, () => { + const props = { + data: data, + tableHeader: tableHeader, + host: `fool`, + resource: `bool`, + enableDownload: true, + enableIndex: true, + TableCellDiv: TableCellDiv + } + + test(`should render three search general boxes and a table with head and body and two bottom info boxes`, () => { + const wrapper = shallow() + expect(wrapper.find(`.small-8.columns`)).toHaveLength(3) + + expect(wrapper.find(Table)).toHaveLength(1) + expect(wrapper.find(Table.Head)).toHaveLength(1) + expect(wrapper.find(Table.Body)).toHaveLength(1) + + expect(wrapper.find(TableFooter)).toHaveLength(1) + expect(wrapper.find(TableHeaderCells)).toHaveLength(1) + }) + + + test(`should sort table content and change header text icon`, () => { + const randomColumn = getRandomInt(1, tableHeader.length) + props.tableHeader[randomColumn].type=`sort` + const wrapper = mount() + + expect(wrapper.find(`.icon.icon-common.icon-sort-up`)).toHaveLength(1) + expect(wrapper.find(`.icon.icon-common.icon-sort-down`)).toHaveLength(0) + + const sortedHeader = wrapper.find(`.header${randomColumn}`).at(0) + sortedHeader.simulate('click') + wrapper.update() + expect(wrapper.find(`.icon.icon-common.icon-sort-up`)).toHaveLength(0) + sortedHeader.simulate('click') + wrapper.update() + expect(wrapper.find(`.icon.icon-common.icon-sort-up`)).toHaveLength(1) + }) + + test(`should filter based on kingdom selection`, () => { + const event = {target: {name: `pollName`, value: `animals`}} + const wrapper = mount() + const kingdomSearch = wrapper.find(`.kingdom`).at(0) + kingdomSearch.simulate(`change`, event) + + expect(wrapper.state(`selectedKingdom`)).toEqual(`animals`) + expect(wrapper.find(Table.Row).length).toBeLessThanOrEqual(data.length) + }) + + test(`should filter based on table header search`, () => { + const randomValue = `si` + const randomColumn = getRandomInt(1, tableHeader.length) + props.tableHeader[randomColumn].type=`search` + + const wrapper = mount() + expect(wrapper.find(`.searchheader${randomColumn}`).exists()).toBe(true) + wrapper.setState({searchQuery: randomValue}) + wrapper.update() + expect(wrapper.find(Table.Row).length).toBeLessThanOrEqual(data.length) + }) + + test(`should change page by clicking buttons`, () => { + const wrapper = mount() + const currentPage = wrapper.state().currentPage + wrapper.setState({selectedNumber: 1, currentPage: 1}) + wrapper.update() + + const nextButton = wrapper.find('a.next') + nextButton.simulate('click') + wrapper.update() + expect(wrapper.state().currentPage).toEqual(currentPage+1) + + const prevButton = wrapper.find('a.previous') + prevButton.simulate('click') + expect(wrapper.state().currentPage).toEqual(currentPage) + + const pageNumberButton = wrapper.find(`.paginate_button.number a`) + const currentNumberButton = wrapper.find(`.paginate_button.number.current`) + expect(pageNumberButton).toHaveLength(data.length-1) + expect(currentNumberButton).toHaveLength(1) + }) + + test(`should show/hide download based on props`, () => { + const wrapper = shallow() + expect(wrapper.find(`.downloadHeader`)).toHaveLength(1) + + const wrapperNoDownload= shallow() + expect(wrapperNoDownload.find(`.downloadHeader`)).toHaveLength(0) + }) + + test(`should save experiment accession by check download box`, () => { + const randomRow = getRandomInt(0, data.length) + + const wrapper = mount() + const propKey = tableHeader[wrapper.state(`orderedColumn`)].dataParam + const filteredElements = _.sortBy(data, propKey) + + expect(wrapper.state(`checkedArray`)).toEqual([]) + const checkbox = wrapper.find(`.checkbox`).at(randomRow) + checkbox.simulate(`change`) + expect(wrapper.state(`checkedArray`)).toEqual([filteredElements[randomRow].experimentAccession]) + checkbox.simulate(`change`) + expect(wrapper.state(`checkedArray`)).toEqual([]) + }) + +}) diff --git a/__test__/FetchLoader.test.js b/__test__/FetchLoader.test.js new file mode 100644 index 0000000..c9c2614 --- /dev/null +++ b/__test__/FetchLoader.test.js @@ -0,0 +1,104 @@ +import React from 'react' +import Enzyme from 'enzyme' +import { shallow } from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' +import fetchMock from 'fetch-mock' + +import { getRandomInt } from './TestUtils' + +import FetchLoader from '../src/FetchLoader' +import CalloutAlert from '../src/CalloutAlert' + +Enzyme.configure({ adapter: new Adapter() }) + +const DummyComponentClass = () =>
+ +describe(`FetchLoader`, () => { + beforeEach(() => { + fetchMock.restore() + }) + + const props = { + host: `glip/`, + resource: `glops`, + noResultsMessageFormatter: DummyComponentClass + } + + const getRandomHttpErrorCode = () => getRandomInt(400, 600) + + test(`until the fetch promise is not resolved a loading message is displayed, then goes away`, async () => { + fetchMock.get(`*`, `{"results":[]}`) + const wrapper = shallow() + + expect(wrapper.find(`#loader`)).toHaveLength(1) + expect(wrapper.find(CalloutAlert)).toHaveLength(0) + + await wrapper.instance().componentDidMount() + wrapper.update() + + expect(wrapper.find(`#loader`)).toHaveLength(0) + expect(wrapper.find(CalloutAlert)).toHaveLength(0) + }) + + test(`renders an error message if request to the server returns 4xx or 5xx`, async () => { + fetchMock.get(`*`, getRandomHttpErrorCode) + const wrapper = shallow() + + await wrapper.instance().componentDidMount() + wrapper.update() + expect(wrapper.find(CalloutAlert)).toHaveLength(1) + }) + + test(`renders an error message if the component does not receive JSON`, async () => { + fetchMock.get(`*`, `Break the cycle, Morty. Rise above. Focus on the science`) + const wrapper = shallow() + + await wrapper.instance().componentDidMount() + wrapper.update() + expect(wrapper.find(CalloutAlert)).toHaveLength(1) + }) + + test(`renders an error message if the child receives invalid JSON (and the error boundary kicks in)`, async () => { + fetchMock.get(`*`, `{}`) + const wrapper = shallow() + + const e = new Error(`They’re inside you building a monument to compromise!`) + wrapper.instance().componentDidCatch(e, `Ruben’s seen some rough years, Morty.`) + wrapper.update() + expect(wrapper.find(CalloutAlert)).toHaveLength(1) + }) + + test(`re-fetches on props change and recovers from error if new fetch succeeds`, async () => { + fetchMock.get(`/glip/glops`, getRandomHttpErrorCode) + const wrapper = shallow() + + await wrapper.instance().componentDidMount() + wrapper.update() + expect(wrapper.find(CalloutAlert)).toHaveLength(1) + + fetchMock.get(`/glops/glip`, `{"results":[]}`) + wrapper.setProps({ + host: `glops/`, + resource: `glip` + }) + + await wrapper.instance().componentDidUpdate() + wrapper.update() + expect(wrapper.find(CalloutAlert)).toHaveLength(0) + }) + + test(`passes JSON payload to prop noResultsMessageFormatter`, async () => { + fetchMock.get(`*`, `{"results":[], "reason": "Rubber baby bubby bunkers!"}`) + const wrapper = + shallow( + `No results: ${data.reason}`} />) + + await wrapper.instance().componentDidMount() + wrapper.update() + + expect(wrapper.find(`h5`).text()).toBe(`No results: Rubber baby bubby bunkers!`) + }) + +}) diff --git a/__test__/MyComponent.test.js b/__test__/MyComponent.test.js deleted file mode 100755 index fc20715..0000000 --- a/__test__/MyComponent.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' -import renderer from 'react-test-renderer' -import Enzyme from 'enzyme' -import {shallow, mount, render} from 'enzyme' -import Adapter from 'enzyme-adapter-react-16' - -import MyComponent from '../src/MyComponent.js' - -Enzyme.configure({ adapter: new Adapter() }) - -describe(`MyComponent`, () => { - test(`should render without throwing an error`, () => { - expect(shallow().contains(
Bar
)).toBe(true) - }) - - test(`should be selectable by class "foo"`, () => { - expect(shallow().is(`.foo`)).toBe(true) - }) - - test(`should mount in a full DOM`, () => { - expect(mount().find(`.foo`)).toHaveLength(1) - }) - - test(`should render to static HTML`, () => { - expect(render().text()).toEqual(`Bar`) - }) - - test(`matches snapshot`, () => { - const tree = renderer.create().toJSON() - expect(tree).toMatchSnapshot() - }) -}) diff --git a/__test__/TableFooter.test.js b/__test__/TableFooter.test.js new file mode 100644 index 0000000..231a807 --- /dev/null +++ b/__test__/TableFooter.test.js @@ -0,0 +1,26 @@ +import React from 'react' +import Enzyme from 'enzyme' +import {shallow} from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' +import { data} from './TestUtils' +import TableFooter from '../src/TableFooter' + +Enzyme.configure({ adapter: new Adapter() }) + +describe(`TableFooter`, () => { + const props = { + dataArray: data, + currentPage: 1, + selectedNumber: 2, + data: data, + onChange: ()=>{} + } + + test(`should render a previous button, a next button and information text`, () => { + const wrapper = shallow() + expect(wrapper.find('li.next')).toHaveLength(1) + expect(wrapper.find('li.previous')).toHaveLength(1) + expect(wrapper.find(`.dataTables_info`)).toHaveLength(1) + }) + +}) diff --git a/__test__/TableHeaderCells.test.js b/__test__/TableHeaderCells.test.js new file mode 100644 index 0000000..1ae3fbc --- /dev/null +++ b/__test__/TableHeaderCells.test.js @@ -0,0 +1,35 @@ +import React from 'react' +import Enzyme from 'enzyme' +import {shallow} from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' +import {getRandomInt, tableHeader} from './TestUtils' +import TableHeaderCells from '../src/TableHeaderCells' +import { Table } from 'evergreen-ui' + +Enzyme.configure({ adapter: new Adapter() }) +const randomColumn = getRandomInt(1, tableHeader.length) + +describe(`TableHeaderCells`, () => { + const props = { + tableHeader: tableHeader, + searchedColumn: randomColumn, + searchQuery: `bool`, + orderedColumn: randomColumn, + ordering: true, + onChange: ()=>{}, + onClick: ()=>{} + } + + test(`should render different table header based on types`, () => { + props.tableHeader[randomColumn].type=`sort` + const wrapper = shallow() + + expect(wrapper.find(`.icon.icon-common.icon-sort-up`)).toHaveLength(1) + expect(wrapper.find(`.icon.icon-common.icon-sort-down`)).toHaveLength(0) + + props.tableHeader[1].type=`search` + const wrapper2 = shallow() + expect(wrapper2.find(Table.SearchHeaderCell)).toHaveLength(1) + }) + +}) diff --git a/__test__/TestUtils.js b/__test__/TestUtils.js new file mode 100644 index 0000000..df61c0c --- /dev/null +++ b/__test__/TestUtils.js @@ -0,0 +1,57 @@ +import React from 'react' +import styled from 'styled-components' + +// Stolen from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random +const getRandomInt = (min, max) => { + min = Math.ceil(min) + max = Math.floor(max) + return Math.floor(Math.random() * (max - min)) + min //The maximum is exclusive and the minimum is inclusive +} + +const TableCellDiv = styled.div` + font-size: 13px; + font-family: Helvetica, Arial, FreeSans, "Liberation Sans", sans-serif; +` + +const tableHeader = [ + {type: `sort`, title: `Loaded date`, width: 140, dataParam: `lastUpdate`}, + {type: `search`, title: `species`, width: 200, dataParam: `species`}, + {type: ``, title: `experiment description`, width: 360, dataParam: `experimentDescription`, link: `experimentAccession`, resource: `experiments`, endpoint: `Results`}, + {type: `plain`, title: `experiment factors`, width: 260, dataParam: `experimentalFactors`}, + {type: `sort`, title: `Number of assays`, width: 160, dataParam: `numberOfAssays`, link: `experimentAccession`, resource: `experiments`, endpoint: `Experiment Design`}, +] + +const data = [ + {"experimentType":`SINGLE`, + "experimentAccession":`E-EHCA-2`, + "experimentDescription":`Melanoma infiltration`, + "lastUpdate":`16-11-2018`, + "numberOfAssays":6638, + "numberOfContrasts":0, + "species":`Mus musculus`, + "kingdom":`animals`, + "experimentalFactors":[`single cell identifier`,`sampling site`,`time`], + }, + {"experimentType":`SINGLE`, + "experimentAccession":`E-GEOD-99058`, + "experimentDescription":`Single cell`, + "lastUpdate":`11-10-2018`, + "numberOfAssays":250, + "numberOfContrasts":0, + "species":`Mus musculus`, + "kingdom":`animals`, + "experimentalFactors":[`single cell identifier`] + }, + {"experimentType":`SINGLE`, + "experimentAccession":`E-MTAB-5061`, + "experimentDescription":`healthy individuals and type 2 diabetes patients`, + "lastUpdate":`11-10-2018`, + "numberOfAssays":3514, + "numberOfContrasts":0, + "species":`Homo sapiens`, + "kingdom":`plants`, + "experimentalFactors":[`single cell identifier`,`disease`], + } +] + +export {getRandomInt, TableCellDiv, tableHeader, data} diff --git a/__test__/__snapshots__/CalloutAlert.test.js.snap b/__test__/__snapshots__/CalloutAlert.test.js.snap new file mode 100644 index 0000000..8e0f26b --- /dev/null +++ b/__test__/__snapshots__/CalloutAlert.test.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CalloutAlert matches snapshot 1`] = ` +
+
+
+ Oops! +
+

+ A human-readable description of the error, hopefully useful to the user +
+ If the error persists, in order to help us debug the issue, please copy the URL and this message and send it to us via + + the EBI Support & Feedback system + + : +

+ + Error name: Error message + +
+
+`; diff --git a/html/index.html b/html/index.html index 4de7369..abc9cef 100755 --- a/html/index.html +++ b/html/index.html @@ -5,14 +5,13 @@ - - - + + -
-
+
+
@@ -20,12 +19,29 @@ - - - + diff --git a/html/render.js b/html/render.js index a4f0f2a..e95792e 100755 --- a/html/render.js +++ b/html/render.js @@ -1,10 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom' -import MyComponent from '../src/index.js' +import ExperimentTable from '../src/index.js' const render = (options, target) => { - ReactDOM.render(, document.getElementById(target)) + ReactDOM.render(, document.getElementById(target)) } export {render} diff --git a/package-lock.json b/package-lock.json index a64970a..34ea973 100755 --- a/package-lock.json +++ b/package-lock.json @@ -185,7 +185,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -798,6 +797,21 @@ "@babel/plugin-transform-react-jsx-source": "^7.0.0" } }, + "@babel/runtime": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", + "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", + "requires": { + "regenerator-runtime": "^0.12.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" + } + } + }, "@babel/template": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz", @@ -853,18 +867,32 @@ "to-fast-properties": "^2.0.0" } }, + "@blueprintjs/icons": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.5.0.tgz", + "integrity": "sha512-+euHGqIgRJzcUXmtnWUp4vPY++YGA4YztSx2uzCgpTiL0BcFm+syw8yIviyjdhTqMXk8sttk4W2pEALcTJtX6A==", + "requires": { + "classnames": "^2.2", + "tslib": "^1.9.0" + } + }, + "@emotion/hash": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz", + "integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==" + }, "@emotion/is-prop-valid": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.6.8.tgz", - "integrity": "sha512-IMSL7ekYhmFlILXcouA6ket3vV7u9BqStlXzbKOF9HBtpUPMMlHU+bBxrLOa2NvleVwNIxeq/zL8LafLbeUXcA==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz", + "integrity": "sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==", "requires": { - "@emotion/memoize": "^0.6.6" + "@emotion/memoize": "0.7.1" } }, "@emotion/memoize": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz", - "integrity": "sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz", + "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==" }, "@emotion/unitless": { "version": "0.7.3", @@ -1294,8 +1322,7 @@ "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" }, "asap": { "version": "2.0.6", @@ -1545,11 +1572,12 @@ "dev": true }, "babel-plugin-styled-components": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.9.2.tgz", - "integrity": "sha512-McnheW8RkBkur/mQw7rEwQO/oUUruQ/nIIj5LIRpsVL8pzG1oo1Y53xyvAYeOfamIrl4/ta7g1G/kuTR1ekO3A==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.0.tgz", + "integrity": "sha512-sQVKG8irFXx14ZfaK1bBePirfkacl3j8nZwSZK+ZjsbnadRHKQTbhXbe/RB1vT6Vgkz45E+V95LBq4KqdhZUNw==", "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-module-imports": "^7.0.0", "babel-plugin-syntax-jsx": "^6.18.0", "lodash": "^4.17.10" } @@ -1666,7 +1694,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -1675,8 +1702,7 @@ "core-js": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.0.tgz", - "integrity": "sha512-kLRC6ncVpuEW/1kwrOXYX6KQASCVtrh1gQr/UiaVgFlf9WE5Vp+lNe5+h3LuMr5PAucWnnEXwH0nQHRH/gpGtw==", - "dev": true + "integrity": "sha512-kLRC6ncVpuEW/1kwrOXYX6KQASCVtrh1gQr/UiaVgFlf9WE5Vp+lNe5+h3LuMr5PAucWnnEXwH0nQHRH/gpGtw==" } } }, @@ -1899,6 +1925,11 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "bowser": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz", + "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2306,6 +2337,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-webpack-plugin": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-1.0.0.tgz", @@ -2657,6 +2693,15 @@ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" }, + "css-in-js-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", + "integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==", + "requires": { + "hyphenate-style-name": "^1.0.2", + "isobject": "^3.0.1" + } + }, "css-select": { "version": "1.2.0", "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", @@ -2992,6 +3037,14 @@ "esutils": "^2.0.2" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-serializer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", @@ -3050,6 +3103,11 @@ "domelementtype": "1" } }, + "downshift": { + "version": "1.31.16", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-1.31.16.tgz", + "integrity": "sha512-RskXmiGSoz0EHAyBrmTBGSLHg6+NYDGuLu2W3GpmuOe6hmZEWhCiQrq5g6DWzhnUaJD41xHbbfC6j1Fe86YqgA==" + }, "duplexify": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", @@ -3624,6 +3682,29 @@ "original": "^1.0.0" } }, + "evergreen-ui": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/evergreen-ui/-/evergreen-ui-4.9.0.tgz", + "integrity": "sha512-+m9aexfixiktO9fwgYz9hrlUkko5irgenN0U3i45AyXvIpAXYSULy552TZvcexvvs7Ady9G9daEXmOqD2jgaxA==", + "requires": { + "@babel/runtime": "^7.1.2", + "@blueprintjs/icons": "^3.2.0", + "arrify": "^1.0.1", + "classnames": "^2.2.6", + "dom-helpers": "^3.2.1", + "downshift": "^1.31.16", + "fuzzaldrin-plus": "^0.6.0", + "glamor": "^2.20.40", + "lodash.debounce": "^4.0.8", + "lodash.mapvalues": "^4.6.0", + "prop-types": "^15.6.2", + "react-scrollbar-size": "^2.0.2", + "react-tiny-virtual-list": "^2.1.4", + "react-transition-group": "^2.5.0", + "tinycolor2": "^1.4.1", + "ui-box": "^1.4.0" + } + }, "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -4786,6 +4867,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "fuzzaldrin-plus": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/fuzzaldrin-plus/-/fuzzaldrin-plus-0.6.0.tgz", + "integrity": "sha1-gy9kifvodnaUWVmckUpnDsIpR+4=" + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -4813,6 +4899,18 @@ "assert-plus": "^1.0.0" } }, + "glamor": { + "version": "2.20.40", + "resolved": "https://registry.npmjs.org/glamor/-/glamor-2.20.40.tgz", + "integrity": "sha512-DNXCd+c14N9QF8aAKrfl4xakPk5FdcFwmH7sD0qnC0Pr7xoZ5W9yovhUrY/dJc3psfGGXC58vqQyRtuskyUJxA==", + "requires": { + "fbjs": "^0.8.12", + "inline-style-prefixer": "^3.0.6", + "object-assign": "^4.1.1", + "prop-types": "^15.5.10", + "through": "^2.3.8" + } + }, "glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", @@ -5201,6 +5299,11 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "hyphenate-style-name": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz", + "integrity": "sha1-MRYKNpMK2vH8BMYHT360FGXU7Es=" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5265,6 +5368,15 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, + "inline-style-prefixer": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz", + "integrity": "sha1-hVG45bTVcyROZqNLBPfTIHaitTQ=", + "requires": { + "bowser": "^1.7.3", + "css-in-js-utils": "^2.0.0" + } + }, "inquirer": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", @@ -5682,8 +5794,7 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "isomorphic-fetch": { "version": "2.2.1", @@ -7054,8 +7165,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, "lodash.escape": { "version": "4.0.1", @@ -7075,6 +7185,11 @@ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", "dev": true }, + "lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -8473,11 +8588,54 @@ "scheduler": "^0.11.2" } }, + "react-event-listener": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.5.10.tgz", + "integrity": "sha512-YZklRszh9hq3WP3bdNLjFwJcTCVe7qyTf5+LWNaHfZQaZrptsefDK2B5HHpOsEEaMHvjllUPr0+qIFVTSsurow==", + "requires": { + "@babel/runtime": "7.0.0-beta.42", + "fbjs": "^0.8.16", + "prop-types": "^15.6.0", + "warning": "^3.0.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.42.tgz", + "integrity": "sha512-iOGRzUoONLOtmCvjUsZv3mZzgCT6ljHQY5fr1qG1QIiJQwtM7zbPWGGpa3QWETq+UqwWyJnoi5XZDZRwZDFciQ==", + "requires": { + "core-js": "^2.5.3", + "regenerator-runtime": "^0.11.1" + } + }, + "core-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.2.tgz", + "integrity": "sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g==" + } + } + }, "react-is": { "version": "16.6.3", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz", "integrity": "sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-scrollbar-size": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-scrollbar-size/-/react-scrollbar-size-2.1.0.tgz", + "integrity": "sha512-9dDUJvk7S48r0TRKjlKJ9e/LkLLYgc9LdQR6W21I8ZqtSrEsedPOoMji4nU3DHy7fx2l8YMScJS/N7qiloYzXQ==", + "requires": { + "babel-runtime": "^6.26.0", + "prop-types": "^15.6.0", + "react-event-listener": "^0.5.1", + "stifle": "^1.0.2" + } + }, "react-test-renderer": { "version": "16.6.3", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.6.3.tgz", @@ -8490,6 +8648,25 @@ "scheduler": "^0.11.2" } }, + "react-tiny-virtual-list": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-tiny-virtual-list/-/react-tiny-virtual-list-2.2.0.tgz", + "integrity": "sha512-MDiy2xyqfvkWrRiQNdHFdm36lfxmcLLKuYnUqcf9xIubML85cmYCgzBJrDsLNZ3uJQ5LEHH9BnxGKKSm8+C0Bw==", + "requires": { + "prop-types": "^15.5.7" + } + }, + "react-transition-group": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.2.tgz", + "integrity": "sha512-vwHP++S+f6KL7rg8V1mfs62+MBKtbMeZDR8KiNmD7v98Gs3UPGsDZDahPJH2PVprFW5YHJfh6cbNim3zPndaSQ==", + "requires": { + "dom-helpers": "^3.3.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -8585,8 +8762,7 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regenerator-transform": { "version": "0.13.3", @@ -9461,6 +9637,11 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, + "stifle": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stifle/-/stifle-1.1.0.tgz", + "integrity": "sha1-FoC13p3gQHQWT0rA/n0022tvcik=" + }, "stream-browserify": { "version": "2.0.1", "resolved": "http://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", @@ -9605,11 +9786,12 @@ "dev": true }, "styled-components": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.1.2.tgz", - "integrity": "sha512-NdvWatJ2WLqZxAvto+oH0k7GAC/TlAUJTrHoXJddjbCrU6U23EmVbb9LXJBF+d6q6hH+g9nQYOWYPUeX/Vlc2w==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.1.3.tgz", + "integrity": "sha512-0quV4KnSfvq5iMtT0RzpMGl/Dg3XIxIxOl9eJpiqiq4SrAmR1l1DLzNpMzoy3DyzdXVDMJS2HzROnXscWA3SEw==", "requires": { - "@emotion/is-prop-valid": "^0.6.8", + "@babel/helper-module-imports": "^7.0.0", + "@emotion/is-prop-valid": "^0.7.3", "@emotion/unitless": "^0.7.0", "babel-plugin-styled-components": ">= 1", "css-to-react-native": "^2.2.2", @@ -9915,8 +10097,7 @@ "through": { "version": "2.3.8", "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "2.0.5", @@ -9943,6 +10124,11 @@ "setimmediate": "^1.0.4" } }, + "tinycolor2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -10047,8 +10233,7 @@ "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tty-browserify": { "version": "0.0.0", @@ -10128,6 +10313,28 @@ } } }, + "ui-box": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ui-box/-/ui-box-1.4.0.tgz", + "integrity": "sha512-l1rhH+6pRPG5jdHd9qAbKMo0nW9//WDJtqY6WZYk2uiCbfPEbK1oTq6Ly7UXGi887psr8gAsSYfJyyDArYnUxQ==", + "requires": { + "@emotion/hash": "^0.6.5", + "glamor": "^2.20.0", + "inline-style-prefixer": "^4.0.2", + "prop-types": "^15.6.0" + }, + "dependencies": { + "inline-style-prefixer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz", + "integrity": "sha512-N8nVhwfYga9MiV9jWlwfdj1UDIaZlBFu4cJSJkIr7tZX7sHpHhGR5su1qdpW+7KPL8ISTvCIkcaFi/JdBknvPg==", + "requires": { + "bowser": "^1.7.3", + "css-in-js-utils": "^2.0.0" + } + } + } + }, "underscore": { "version": "1.4.4", "resolved": "http://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", @@ -10418,6 +10625,14 @@ "makeerror": "1.0.x" } }, + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watch": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", diff --git a/package.json b/package.json index 5928b33..30f7aa1 100755 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "expression-atlas-my-package", + "name": "atlas-experiment-table", "version": "0.0.1", - "description": "My package does some neat things", + "description": "This package renders an experiment table with user interaction using Evergreen package", "main": "lib/index.js", "scripts": { "prepack": "rm -rf lib && babel src -d lib --copy-files", - "test": "jest --env=node --coverage", + "test": "jest --env=jsdom --coverage", "posttest": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", "postversion": "npm publish", "postpublish": "git push && git push --tags" @@ -21,18 +21,20 @@ }, "author": "Expression Atlas developers ", "collaborators": [ - "Alfonso Muñoz-Pomer Fuentes " + "Lingyun Zhao" ], "license": "Apache-2.0", "repository": { "type": "git", - "url": "https://github.com/gxa/my-package.git" + "url": "https://github.com/ebi-gene-expression-group/scxa-experiment-table" }, "dependencies": { + "evergreen-ui": "^4.9.0", + "lodash": "^4.17.11", "prop-types": "^15.6.2", "react": "^16.6.3", "react-dom": "^16.6.3", - "styled-components": "^4.1.2", + "styled-components": "^4.1.3", "urijs": "^1.19.1" }, "devDependencies": { diff --git a/src/CalloutAlert.js b/src/CalloutAlert.js new file mode 100644 index 0000000..78dc7f1 --- /dev/null +++ b/src/CalloutAlert.js @@ -0,0 +1,25 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const CalloutAlert = ({error}) => +
+
+
Oops!
+

+ {error.description}
+ If the error persists, in order to help us debug the issue, please copy the URL and this message and + send it to us via the EBI Support & Feedback system: +

+ {`${error.name}: ${error.message}`} +
+
+ +CalloutAlert.propTypes = { + error: PropTypes.shape({ + description: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + message: PropTypes.string.isRequired + }) +} + +export default CalloutAlert diff --git a/src/ExperimentTable.js b/src/ExperimentTable.js new file mode 100755 index 0000000..f99ba99 --- /dev/null +++ b/src/ExperimentTable.js @@ -0,0 +1,190 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Table } from 'evergreen-ui' +import _ from 'lodash' +import styled from 'styled-components' + +import TableFooter from './TableFooter' +import TableHeaderCells from './TableHeaderCells' + +const TableFooterDiv = styled.div` + &:before { + content: ''; + width: 100%; + height: 1em; + } +` + +const TableCellDiv = styled.div` + font-size: 13px; + font-family: Helvetica, Arial, FreeSans, "Liberation Sans", sans-serif; +` + +class ExperimentTable extends React.Component { + constructor(props) { + super(props) + + this.state = { + searchQuery: ``, + orderedColumn: 0, + searchedColumn: 1, + ordering: true, + checkedArray: [], + currentPage: 1, + selectedNumber: 10 + } + + this.sort = this.sort.bind(this) + this.filter = this.filter.bind(this) + this.setValue = this.setValue.bind(this) + this.handleCheckbox = this.handleCheckbox.bind(this) + } + + sort(data) { + const {ordering, orderedColumn} = this.state + const propKey = this.props.tableHeader[orderedColumn].dataParam + const filteredElements = _.sortBy(data, propKey) + return ordering ? filteredElements : filteredElements.reverse() + } + + filter(data, tableHeader){ + const searchQuery = this.state.searchQuery.trim() + return searchQuery.length === 0 ? data : + data.filter(profile => _.isArray(profile[tableHeader[this.state.searchedColumn].dataParam]) ? + _.flattenDeep(profile[tableHeader[this.state.searchedColumn].dataParam]).some(item=>item.toLowerCase().includes(searchQuery.toLowerCase())) : + profile[tableHeader[this.state.searchedColumn].dataParam].toString().toLowerCase().includes(searchQuery.toLowerCase()) + ) + } + + setValue(property){ + return e => { + this.setState({ + [property]: e.target.value, + currentPage: 1 + }) + } + } + + handleCheckbox(accession){ + const checkedArray = this.state.checkedArray.slice() + checkedArray.includes(accession) ? + _.remove(checkedArray, function(n) {return n === accession}) + : checkedArray.push(accession) + this.setState({checkedArray: checkedArray}) + } + + render() { + const {selectedSearch, selectedKingdom, checkedArray, selectedNumber, currentPage, searchedColumn, searchQuery, orderedColumn, ordering} = this.state + const {host, data, tableHeader, enableDownload, enableIndex} = this.props + + const dataArray = selectedSearch ? this.sort(data).filter(data => data && Object.values(data).some(value => value.toString().toLowerCase().includes(selectedSearch.toLowerCase()))) : + this.filter(this.sort(data), tableHeader).filter(data => selectedKingdom ? data.kingdom === selectedKingdom : true) + const currentPageData = selectedNumber ? dataArray.slice(selectedNumber*(currentPage-1), selectedNumber*currentPage) : dataArray + + return ( +
+
+
+ +
+
+ +
+
+ +
+
+ + + + {enableIndex &&Index} + + this.setState({ + orderedColumn: columnNumber, + ordering: !this.state.ordering})} + onChange={(value, columnNumber) => + this.setState({ + searchQuery: value, + searchedColumn: columnNumber + })} + /> + { + enableDownload && + {checkedArray.length > 0 ? + Download {checkedArray.length} {checkedArray.length===1?`entry`:`entries`} + : `Download`} + + } + + + + {currentPageData.map((data, index) => { + return ( + + + {[ + enableIndex && + {`${index + 1 + selectedNumber*(currentPage-1)} `} + , + + tableHeader.map((header, index) => { + const cellItem = _.isArray(data[header.dataParam]) ? +
    {data[header.dataParam].map(factor =>
  • {factor}
  • )}
+ : data[header.dataParam] + return + + {header.link ? {cellItem} : cellItem} + + + }), + + enableDownload && + + this.handleCheckbox(data.experimentAccession)} /> + + + ]} + +
) + }) + } +
+
+ + this.setState({currentPage: i})}/> + +
+ ) + } +} + +ExperimentTable.propTypes = { + data: PropTypes.array, + host: PropTypes.string.isRequired, + resource: PropTypes.string.isRequired, + tableHeader: PropTypes.array.isRequired, + enableDownload: PropTypes.bool.isRequired, + enableIndex: PropTypes.bool.isRequired, +} + +export default ExperimentTable diff --git a/src/FetchLoader.js b/src/FetchLoader.js new file mode 100644 index 0000000..978112c --- /dev/null +++ b/src/FetchLoader.js @@ -0,0 +1,113 @@ +import React from 'react' +import PropTypes from 'prop-types' +import URI from 'urijs' + +import CalloutAlert from './CalloutAlert' +import ExperimentTable from './ExperimentTable' + +class FetchLoader extends React.Component { + constructor(props) { + super(props) + + this.state = { + data: null, + loading: true, + error: null + } + } + + static getDerivedStateFromProps(props, state) { + const url = URI(props.resource, props.host).toString() + // Store url in state so we can compare when props change. + // Clear out previously-loaded data (so we don't render stale stuff). + if (url !== state.url) { + return { + data: null, + loading: true, + error: null, + url: url + } + } + + // No state update necessary + return null + } + + render() { + const { noResultsMessageFormatter } = this.props + const { data, loading, error } = this.state + + return( + error ? + : + loading ? +
+
+
Loading, please wait...
+
+
: + data.aaData && data.aaData.length > 0 ? + : +
+
+
{noResultsMessageFormatter(data)}
+
+
+ ) + } + + async componentDidUpdate(prevProps, prevState) { + if (this.state.data === null && this.state.error === null) { + await this._loadAsyncData(URI(this.props.resource, this.props.host).toString()) + } + } + + async componentDidMount() { + await this._loadAsyncData(URI(this.props.resource, this.props.host).toString()) + } + + async _loadAsyncData(url) { + try { + const response = await fetch(url) + // The promise returned by fetch may be fulfilled with a 4xx or 5xx return code, so we need to explicitly check ok + if (!response.ok) { + throw new Error(`${url} => ${response.status}`) + } + + this.setState({ + data: await response.json(), + loading: false, + error: null + }) + } catch (e) { + this.setState({ + data: null, + loading: false, + error: { + description: `There was a problem communicating with the server. Please try again later.`, + name: e.name, + message: e.message + } + }) + } + } + + componentDidCatch(error, info) { + this.setState({ + error: { + description: `There was a problem rendering this component.`, + name: error.name, + message: `${error.message} – ${info}` + } + }) + } +} + +FetchLoader.propTypes = { + host: PropTypes.string.isRequired, + resource: PropTypes.string.isRequired, + noResultsMessageFormatter: PropTypes.func, +} + + +export default FetchLoader diff --git a/src/MyComponent.js b/src/MyComponent.js deleted file mode 100755 index aaa3025..0000000 --- a/src/MyComponent.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -class MyComponent extends React.Component { - constructor(props) { - super(props) - } - - render() { - - } -} - -MyComponent.propTypes = { - atlasUrl: PropTypes.string -} - -export default MyComponent diff --git a/src/TableFooter.js b/src/TableFooter.js new file mode 100644 index 0000000..8f80075 --- /dev/null +++ b/src/TableFooter.js @@ -0,0 +1,39 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const TableFooter = ({dataArray, currentPage, selectedNumber, onChange, data}) => { + const pageNumbersButton = [] + for (let i = 1; i <= Math.ceil(dataArray.length/selectedNumber); i++) { + pageNumbersButton.push( i === currentPage ?
  • {currentPage}
  • : +
  • onChange(i)}>{i}
  • ) + } + return [ +
    +
    {dataArray.length === 0 ? `There is no search results under this query` + : `Showing ${dataArray.length} out of ${data.length} experiments.`}
    +
    , +
    + +
    + ] +} + +TableFooter.propTypes = { + dataArray: PropTypes.array.isRequired, + currentPage: PropTypes.number.isRequired, + selectedNumber: PropTypes.number.isRequired, + data: PropTypes.array.isRequired, + onChange: PropTypes.func.isRequired +} + +export default TableFooter \ No newline at end of file diff --git a/src/TableHeaderCells.js b/src/TableHeaderCells.js new file mode 100644 index 0000000..fe4e9bc --- /dev/null +++ b/src/TableHeaderCells.js @@ -0,0 +1,67 @@ +import _ from "lodash" +import React from 'react' +import PropTypes from 'prop-types' +import { Table } from 'evergreen-ui' +import styled from 'styled-components' + +const TableSearchHeaderCellDiv = styled.div` + display: ruby; +` + +function renderTableSortHeaderCell (columnNumber, headerText, width, orderedColumn, ordering, onClick) { + return onClick(columnNumber)}> + + {headerText} + { columnNumber===orderedColumn ? + ordering ? + + : + : + } + + + +} + +function renderTableSearchHeaderCell(columnNumber, headerText, width, searchedColumn, searchQuery, onChange){ + return onChange(value, columnNumber)} + value={columnNumber===searchedColumn ? searchQuery : ``} + placeholder = {`Search by ${headerText} ...`} + /> +} + +const TableHeaderCells = ({tableHeader, searchedColumn, searchQuery, onClick, onChange, orderedColumn, ordering}) => { + return tableHeader.map((header, index) => { + switch(header.type) { + case `plain`: + return {header.title} + case `sort`: + return renderTableSortHeaderCell(index, header.title, header.width, orderedColumn, ordering, onClick) + case `search`: + return renderTableSearchHeaderCell(index, header.title, header.width, searchedColumn, searchQuery, onChange) + default: + return {header.title} + }} + ) +} + + +TableHeaderCells.propTypes = { + tableHeader: PropTypes.array.isRequired, + searchedColumn: PropTypes.number.isRequired, + searchQuery: PropTypes.string.isRequired, + orderedColumn: PropTypes.number.isRequired, + ordering: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, +} + +export default TableHeaderCells diff --git a/src/index.js b/src/index.js index 58e086c..3e69f9b 100755 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,3 @@ -import MyComponent from './MyComponent.js' +import FetchLoader from './FetchLoader.js' -export default MyComponent +export default FetchLoader diff --git a/webpack.config.js b/webpack.config.js index d53c114..5e6cc37 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,7 +6,7 @@ const vendorsBundleName = `vendors` module.exports = { entry: { - myPackageDemo: [`@babel/polyfill`, `./html/render.js`], + experimentTable: [`@babel/polyfill`, `./html/render.js`], }, plugins: [