| @@ -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", | |||
| @@ -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'}), | |||
| @@ -8,6 +8,7 @@ | |||
| <script language="javascript" src="/js/bootstrap.min.js"></script> | |||
| <script language="javascript" src="/js/fontawesome.min.js"></script> | |||
| <script language="javascript" src="/js/js-uuid.js"></script> | |||
| <script language="javascript" src="/js/filesize.js"></script> | |||
| </head> | |||
| <body> | |||
| <script language="javascript" src="/js/app.min.js"></script> | |||
| @@ -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 => [ | |||
| (<div> | |||
| <div> | |||
| <a href={ urlForObject(item) }>{ item['name'] }</a> | |||
| </div> | |||
| <div>{ item['uuid'] }</div> | |||
| </div>), | |||
| item['description'], | |||
| (<div> | |||
| <div> | |||
| { ownerLookup[item.owner_uuid] ? ( | |||
| <a href={ urlForObject(ownerLookup[item.owner_uuid]) }> | |||
| { arvadosObjectName(ownerLookup[item.owner_uuid]) } | |||
| </a> | |||
| ) : 'Not Found' } | |||
| </div> | |||
| <div>{ item.owner_uuid }</div> | |||
| </div>), | |||
| 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 ( | |||
| <div> | |||
| <WBTable columns={ [ 'Name', 'Description', 'Owner', 'File Count', 'Total Size' ] } | |||
| rows={ rows } /> | |||
| <WBPagination numPages={ numPages } | |||
| activePage={ activePage } | |||
| getPageUrl={ getPageUrl } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBCollectionListing.defaultProps = { | |||
| 'itemsPerPage': 100, | |||
| 'ownerUuid': null | |||
| }; | |||
| export default WBCollectionListing; | |||
| @@ -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; | |||
| @@ -58,7 +58,8 @@ class WBApp extends Component { | |||
| <WBBrowse path="/browse/:ownerUuid?/:activePage?/:objTypeTab?/:collectionPage?/:processPage?/:workflowPage?" | |||
| appCallbacks={ this.appCallbacks } | |||
| appState={ this.appState } /> | |||
| appState={ this.appState } | |||
| app={ this } /> | |||
| <WBProcessView path="/process/:uuid" app={ this } /> | |||
| </Router> | |||
| @@ -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 | |||
| <WBCollectionListing app={ app } | |||
| ownerUuid={ ownerUuid } | |||
| itemsPerPage="20" | |||
| activePage={ Number(collectionPage || 0) } | |||
| getPageUrl={ i => this.getUrl({ 'collectionPage': i }) } /> | |||
| ) : (objTypeTab === 'process' ? ( | |||
| <WBProcessListing appState={ appState } | |||
| ownerUuid={ ownerUuid } | |||
| itemsPerPage="20" | |||
| activePage={ Number(processPage || 0) } | |||
| onPageChanged={ i => this.route({ 'processPage': i }) } /> | |||
| ) : (objTypeTab === 'workflow' ? ( | |||
| null | |||
| ) : null)) | |||
| @@ -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(( | |||
| <li class={ activePage === 0 ? "page-item disabled" : "page-item" }> | |||
| <a class="page-link" href="#" | |||
| onclick={ e => { e.preventDefault(); onPageChanged(activePage - 1); } }>Previous</a> | |||
| <a class="page-link" href={ getPageUrl(activePage - 1) } | |||
| onclick={ e => this.changePage(e, activePage - 1) }>Previous</a> | |||
| </li> | |||
| )); | |||
| @@ -33,35 +33,42 @@ class WBPagination extends Component { | |||
| if (i > prev + 1) | |||
| res.push(( | |||
| <li class="page-item"> | |||
| <a class="page-link" href="#" | |||
| onclick={ e => { e.preventDefault(); onPageChanged(i - 1); } }>...</a> | |||
| <a class="page-link" href={ getPageUrl(i - 1) } | |||
| onclick={ e => this.changePage(e, i - 1) }>...</a> | |||
| </li> | |||
| )); | |||
| prev = i; | |||
| res.push(( | |||
| <li class={ i === activePage ? "page-item active" : "page-item" }> | |||
| <a class="page-link" href="#" | |||
| onclick={ e => { e.preventDefault(); onPageChanged(i); } }>{ i + 1 }</a> | |||
| <a class="page-link" href={ getPageUrl(i) } | |||
| onclick={ e => this.changePage(e, i) }>{ i + 1 }</a> | |||
| </li> | |||
| )); | |||
| } | |||
| res.push(( | |||
| <li class={ activePage >= numPages - 1 ? "page-item disabled" : "page-item" }> | |||
| <a class="page-link" href="#" | |||
| onclick={ e => { e.preventDefault(); onPageChanged(activePage + 1); } }>Next</a> | |||
| <a class="page-link" href={ getPageUrl(activePage + 1) } | |||
| onclick={ e => this.changePage(e, activePage + 1) }>Next</a> | |||
| </li> | |||
| )); | |||
| 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 ( | |||
| <nav aria-label="Pagination"> | |||
| <ul class="pagination"> | |||
| { this.renderVisiblePages(numPages, activePage, chunkSize, onPageChanged) } | |||
| { this.renderVisiblePages(numPages, activePage, chunkSize, onPageChanged, getPageUrl) } | |||
| </ul> | |||
| </nav> | |||
| ); | |||
| @@ -69,7 +76,8 @@ class WBPagination extends Component { | |||
| } | |||
| WBPagination.defaultProps = { | |||
| 'chunkSize': 5 | |||
| 'chunkSize': 5, | |||
| 'getPageUrl': () => ('#') | |||
| }; | |||
| export default WBPagination; | |||