diff --git a/frontend/package.json b/frontend/package.json index 4abf4ac..199ef17 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,6 +2,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.12.0", "bootstrap": "^4.4.1", + "filesize": "^6.0.1", "jquery": "^3.4.1", "js-uuid": "0.0.6", "linkstate": "^1.1.1", diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js index b250a14..e98dca8 100755 --- a/frontend/rollup.config.js +++ b/frontend/rollup.config.js @@ -42,6 +42,7 @@ export default { 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff': 'dist/webfonts/fa-brands-400.woff', 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2': 'dist/webfonts/fa-brands-400.woff2', 'node_modules/js-uuid/js-uuid.js': 'dist/js/js-uuid.js', + 'node_modules/filesize/lib/filesize.js': 'dist/js/filesize.js', verbose: true }), buble({jsx: 'h'}), diff --git a/frontend/src/html/index.html b/frontend/src/html/index.html index 79f83d9..114c934 100755 --- a/frontend/src/html/index.html +++ b/frontend/src/html/index.html @@ -8,6 +8,7 @@ + diff --git a/frontend/src/js/component/wb-collection-listing.js b/frontend/src/js/component/wb-collection-listing.js new file mode 100644 index 0000000..993839c --- /dev/null +++ b/frontend/src/js/component/wb-collection-listing.js @@ -0,0 +1,128 @@ +import { h, Component } from 'preact'; +import { route } from 'preact-router'; +import makeArvadosRequest from 'make-arvados-request'; +import WBTable from 'wb-table'; +import WBPagination from 'wb-pagination'; +import urlForObject from 'url-for-object'; +import arvadosTypeName from 'arvados-type-name'; +import arvadosObjectName from 'arvados-object-name'; + +class WBCollectionListing extends Component { + + constructor(...args) { + super(...args); + this.state.rows = []; + this.state.numPages = 0; + } + + componentDidMount() { + this.fetchItems(); + } + + prepareRows(items, ownerLookup) { + return items.map(item => [ + (
+
+ { item['name'] } +
+
{ item['uuid'] }
+
), + item['description'], + (
+
+ { ownerLookup[item.owner_uuid] ? ( + + { arvadosObjectName(ownerLookup[item.owner_uuid]) } + + ) : 'Not Found' } +
+
{ item.owner_uuid }
+
), + item['file_count'], + filesize(item['file_size_total']) + ]); + } + + fetchItems() { + let { arvHost, arvToken } = this.props.app.state; + + let { activePage, itemsPerPage, ownerUuid } = this.props; + + let filters = []; + + if (ownerUuid) + filters.push([ 'owner_uuid', '=', ownerUuid ]); + + let prom = makeArvadosRequest(arvHost, arvToken, + '/arvados/v1/collections?filters=' + encodeURIComponent(JSON.stringify(filters)) + + '&limit=' + encodeURIComponent(itemsPerPage) + + '&offset=' + encodeURIComponent(itemsPerPage * activePage)); + + let collections; + let numPages + + prom = prom.then(xhr => { + collections = xhr.response['items']; + numPages = Math.ceil(xhr.response['items_available'] / xhr.response['limit']); + + let owners = {}; + collections.map(c => { + let typeName = arvadosTypeName(c.owner_uuid); + if (!(typeName in owners)) + owners[typeName] = []; + owners[typeName].push(c.owner_uuid); + }); + + let lookup = {}; + let prom_1 = new Promise(accept => accept()); + for (let typeName in owners) { + let filters_1 = [ + ['uuid', 'in', owners[typeName]] + ]; + prom_1 = prom_1.then(() => makeArvadosRequest(arvHost, arvToken, + '/arvados/v1/' + typeName + 's?filters=' + + encodeURIComponent(JSON.stringify(filters_1)))); + prom_1 = prom_1.then(xhr => xhr.response.items.map(item => ( + lookup[item.uuid] = item))); + } + + prom_1 = prom_1.then(() => lookup); + + return prom_1; + }); + + //let ownerLookup = {}; + //prom = prom.then(lookup => (ownerLookup = lookup)); + + prom = prom.then(ownerLookup => + this.setState({ + 'numPages': numPages, + 'rows': this.prepareRows(collections, ownerLookup) + })); + } + + componentWillReceiveProps(nextProps, nextState) { + this.props = nextProps; + this.fetchItems(); + } + + render({ app, ownerUuid, activePage, getPageUrl }, { rows, numPages }) { + return ( +
+ + + +
+ ); + } +} + +WBCollectionListing.defaultProps = { + 'itemsPerPage': 100, + 'ownerUuid': null +}; + +export default WBCollectionListing; diff --git a/frontend/src/js/misc/wb-arvados-collection.js b/frontend/src/js/misc/wb-arvados-collection.js new file mode 100644 index 0000000..751a19a --- /dev/null +++ b/frontend/src/js/misc/wb-arvados-collection.js @@ -0,0 +1,50 @@ +import makeArvadosRequest from 'make-arvados-request'; + +class WBArvadosCollection { + constructor(arvHost, arvToken, uuid) { + this.arvHost = arvHost; + this.arvToken = arvToken; + this.uuid = uuid; + this.meta = null; + } + + fetchMeta() { + let prom = makeArvadosRequest(this.arvHost, this.arvToken, + '/arvados/v1/collections/' + this.uuid); + prom = prom.then(xhr => { + this.meta = xhr.response; + }); + return prom; + } + + parseManifest() { + if (this.meta === null) + throw Error('You must call fetchMeta() first and wait for the returned Promise.'); + + let manifest = this.meta.manifest_text; + let streams = manifest.split('\n'); + this.content = streams.map(s => { + let tokens = s.split(' '); + let streamName = tokens[0]; + let rx = /^[a-f0-9]{32}\+[0-9]+/; + let n = tokens.map(t => rx.exec(t)); + n = n.indexOf(null); + let locators = tokens.slice(1, n) + let fileTokens = tokens.slice(n); + let fileNames = fileTokens.map(t => t.split(':')[2]); + let fileSizes = {}; + fileTokens.map(t => { + let [ start, end, name ] = t.split(':'); + if (!(name in fileSizes)) + fileSizes[name] = 0; + fileSizes[name] += Number(end) - Number(start); + }); + fileSizes = fileNames.map(n => fileSizes[n]); + return [ streamName, fileNames, fileSizes ]; + }); + + return this.content; + } +} + +export WBArvadosCollection; diff --git a/frontend/src/js/page/wb-app.js b/frontend/src/js/page/wb-app.js index e4a71e7..47d3311 100644 --- a/frontend/src/js/page/wb-app.js +++ b/frontend/src/js/page/wb-app.js @@ -58,7 +58,8 @@ class WBApp extends Component { + appState={ this.appState } + app={ this } /> diff --git a/frontend/src/js/page/wb-browse.js b/frontend/src/js/page/wb-browse.js index 6e78c35..9efb555 100644 --- a/frontend/src/js/page/wb-browse.js +++ b/frontend/src/js/page/wb-browse.js @@ -6,19 +6,26 @@ import WBInlineSearch from 'wb-inline-search'; import WBProjectCrumbs from 'wb-project-crumbs'; import WBTabs from 'wb-tabs'; import WBProcessListing from 'wb-process-listing'; +import WBCollectionListing from 'wb-collection-listing'; class WBBrowse extends Component { - route(params) { - route('/browse/' + + getUrl(params) { + let res = '/browse/' + ('ownerUuid' in params ? params.ownerUuid : (this.props.ownerUuid || '')) + '/' + ('activePage' in params ? params.activePage : (this.props.activePage || '')) + '/' + ('objTypeTab' in params ? params.objTypeTab : (this.props.objTypeTab || '')) + '/' + ('collectionPage' in params ? params.collectionPage : (this.props.collectionPage || '')) + '/' + ('processPage' in params ? params.processPage : (this.props.processPage || '')) + '/' + - ('workflowPage' in params ? params.workflowPage : (this.props.workflowPage || ''))); + ('workflowPage' in params ? params.workflowPage : (this.props.workflowPage || '')); + + return res; + } + + route(params) { + route(this.getUrl(params)); } - render({ ownerUuid, activePage, appCallbacks, appState, + render({ ownerUuid, activePage, appCallbacks, appState, app, objTypeTab, collectionPage, processPage, workflowPage }) { return ( @@ -51,13 +58,19 @@ class WBBrowse extends Component { { (!objTypeTab || objTypeTab === 'collection') ? ( - null + this.getUrl({ 'collectionPage': i }) } /> + ) : (objTypeTab === 'process' ? ( this.route({ 'processPage': i }) } /> + ) : (objTypeTab === 'workflow' ? ( null ) : null)) diff --git a/frontend/src/js/widget/wb-pagination.js b/frontend/src/js/widget/wb-pagination.js index 8e45602..5d0ff89 100644 --- a/frontend/src/js/widget/wb-pagination.js +++ b/frontend/src/js/widget/wb-pagination.js @@ -1,7 +1,7 @@ import { h, Component } from 'preact'; class WBPagination extends Component { - renderVisiblePages(numPages, activePage, chunkSize, onPageChanged) { + renderVisiblePages(numPages, activePage, chunkSize, onPageChanged, getPageUrl) { let visible = {}; let begActChnk = activePage - Math.floor(chunkSize / 2); @@ -23,8 +23,8 @@ class WBPagination extends Component { res.push((
  • - { e.preventDefault(); onPageChanged(activePage - 1); } }>Previous + this.changePage(e, activePage - 1) }>Previous
  • )); @@ -33,35 +33,42 @@ class WBPagination extends Component { if (i > prev + 1) res.push((
  • - { e.preventDefault(); onPageChanged(i - 1); } }>... + this.changePage(e, i - 1) }>...
  • )); prev = i; res.push((
  • - { e.preventDefault(); onPageChanged(i); } }>{ i + 1 } + this.changePage(e, i) }>{ i + 1 }
  • )); } res.push((
  • = numPages - 1 ? "page-item disabled" : "page-item" }> - { e.preventDefault(); onPageChanged(activePage + 1); } }>Next + this.changePage(e, activePage + 1) }>Next
  • )); return res; } - render({ numPages, activePage, chunkSize, onPageChanged }) { + changePage(e, pageIdx) { + if (this.props.onPageChanged) { + e.preventDefault(); + this.props.onPageChanged(pageIdx); + } + } + + render({ numPages, activePage, chunkSize, onPageChanged, getPageUrl }) { return ( ); @@ -69,7 +76,8 @@ class WBPagination extends Component { } WBPagination.defaultProps = { - 'chunkSize': 5 + 'chunkSize': 5, + 'getPageUrl': () => ('#') }; export default WBPagination;