| @@ -2,6 +2,7 @@ | |||||
| "dependencies": { | "dependencies": { | ||||
| "@fortawesome/fontawesome-free": "^5.12.0", | "@fortawesome/fontawesome-free": "^5.12.0", | ||||
| "bootstrap": "^4.4.1", | "bootstrap": "^4.4.1", | ||||
| "filesize": "^6.0.1", | |||||
| "jquery": "^3.4.1", | "jquery": "^3.4.1", | ||||
| "js-uuid": "0.0.6", | "js-uuid": "0.0.6", | ||||
| "linkstate": "^1.1.1", | "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.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/@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/js-uuid/js-uuid.js': 'dist/js/js-uuid.js', | ||||
| 'node_modules/filesize/lib/filesize.js': 'dist/js/filesize.js', | |||||
| verbose: true | verbose: true | ||||
| }), | }), | ||||
| buble({jsx: 'h'}), | buble({jsx: 'h'}), | ||||
| @@ -8,6 +8,7 @@ | |||||
| <script language="javascript" src="/js/bootstrap.min.js"></script> | <script language="javascript" src="/js/bootstrap.min.js"></script> | ||||
| <script language="javascript" src="/js/fontawesome.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/js-uuid.js"></script> | ||||
| <script language="javascript" src="/js/filesize.js"></script> | |||||
| </head> | </head> | ||||
| <body> | <body> | ||||
| <script language="javascript" src="/js/app.min.js"></script> | <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?" | <WBBrowse path="/browse/:ownerUuid?/:activePage?/:objTypeTab?/:collectionPage?/:processPage?/:workflowPage?" | ||||
| appCallbacks={ this.appCallbacks } | appCallbacks={ this.appCallbacks } | ||||
| appState={ this.appState } /> | |||||
| appState={ this.appState } | |||||
| app={ this } /> | |||||
| <WBProcessView path="/process/:uuid" app={ this } /> | <WBProcessView path="/process/:uuid" app={ this } /> | ||||
| </Router> | </Router> | ||||
| @@ -6,19 +6,26 @@ import WBInlineSearch from 'wb-inline-search'; | |||||
| import WBProjectCrumbs from 'wb-project-crumbs'; | import WBProjectCrumbs from 'wb-project-crumbs'; | ||||
| import WBTabs from 'wb-tabs'; | import WBTabs from 'wb-tabs'; | ||||
| import WBProcessListing from 'wb-process-listing'; | import WBProcessListing from 'wb-process-listing'; | ||||
| import WBCollectionListing from 'wb-collection-listing'; | |||||
| class WBBrowse extends Component { | class WBBrowse extends Component { | ||||
| route(params) { | |||||
| route('/browse/' + | |||||
| getUrl(params) { | |||||
| let res = '/browse/' + | |||||
| ('ownerUuid' in params ? params.ownerUuid : (this.props.ownerUuid || '')) + '/' + | ('ownerUuid' in params ? params.ownerUuid : (this.props.ownerUuid || '')) + '/' + | ||||
| ('activePage' in params ? params.activePage : (this.props.activePage || '')) + '/' + | ('activePage' in params ? params.activePage : (this.props.activePage || '')) + '/' + | ||||
| ('objTypeTab' in params ? params.objTypeTab : (this.props.objTypeTab || '')) + '/' + | ('objTypeTab' in params ? params.objTypeTab : (this.props.objTypeTab || '')) + '/' + | ||||
| ('collectionPage' in params ? params.collectionPage : (this.props.collectionPage || '')) + '/' + | ('collectionPage' in params ? params.collectionPage : (this.props.collectionPage || '')) + '/' + | ||||
| ('processPage' in params ? params.processPage : (this.props.processPage || '')) + '/' + | ('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 }) { | objTypeTab, collectionPage, processPage, workflowPage }) { | ||||
| return ( | return ( | ||||
| @@ -51,13 +58,19 @@ class WBBrowse extends Component { | |||||
| { | { | ||||
| (!objTypeTab || objTypeTab === 'collection') ? ( | (!objTypeTab || objTypeTab === 'collection') ? ( | ||||
| null | |||||
| <WBCollectionListing app={ app } | |||||
| ownerUuid={ ownerUuid } | |||||
| itemsPerPage="20" | |||||
| activePage={ Number(collectionPage || 0) } | |||||
| getPageUrl={ i => this.getUrl({ 'collectionPage': i }) } /> | |||||
| ) : (objTypeTab === 'process' ? ( | ) : (objTypeTab === 'process' ? ( | ||||
| <WBProcessListing appState={ appState } | <WBProcessListing appState={ appState } | ||||
| ownerUuid={ ownerUuid } | ownerUuid={ ownerUuid } | ||||
| itemsPerPage="20" | itemsPerPage="20" | ||||
| activePage={ Number(processPage || 0) } | activePage={ Number(processPage || 0) } | ||||
| onPageChanged={ i => this.route({ 'processPage': i }) } /> | onPageChanged={ i => this.route({ 'processPage': i }) } /> | ||||
| ) : (objTypeTab === 'workflow' ? ( | ) : (objTypeTab === 'workflow' ? ( | ||||
| null | null | ||||
| ) : null)) | ) : null)) | ||||
| @@ -1,7 +1,7 @@ | |||||
| import { h, Component } from 'preact'; | import { h, Component } from 'preact'; | ||||
| class WBPagination extends Component { | class WBPagination extends Component { | ||||
| renderVisiblePages(numPages, activePage, chunkSize, onPageChanged) { | |||||
| renderVisiblePages(numPages, activePage, chunkSize, onPageChanged, getPageUrl) { | |||||
| let visible = {}; | let visible = {}; | ||||
| let begActChnk = activePage - Math.floor(chunkSize / 2); | let begActChnk = activePage - Math.floor(chunkSize / 2); | ||||
| @@ -23,8 +23,8 @@ class WBPagination extends Component { | |||||
| res.push(( | res.push(( | ||||
| <li class={ activePage === 0 ? "page-item disabled" : "page-item" }> | <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> | </li> | ||||
| )); | )); | ||||
| @@ -33,35 +33,42 @@ class WBPagination extends Component { | |||||
| if (i > prev + 1) | if (i > prev + 1) | ||||
| res.push(( | res.push(( | ||||
| <li class="page-item"> | <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> | </li> | ||||
| )); | )); | ||||
| prev = i; | prev = i; | ||||
| res.push(( | res.push(( | ||||
| <li class={ i === activePage ? "page-item active" : "page-item" }> | <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> | </li> | ||||
| )); | )); | ||||
| } | } | ||||
| res.push(( | res.push(( | ||||
| <li class={ activePage >= numPages - 1 ? "page-item disabled" : "page-item" }> | <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> | </li> | ||||
| )); | )); | ||||
| return res; | 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 ( | return ( | ||||
| <nav aria-label="Pagination"> | <nav aria-label="Pagination"> | ||||
| <ul class="pagination"> | <ul class="pagination"> | ||||
| { this.renderVisiblePages(numPages, activePage, chunkSize, onPageChanged) } | |||||
| { this.renderVisiblePages(numPages, activePage, chunkSize, onPageChanged, getPageUrl) } | |||||
| </ul> | </ul> | ||||
| </nav> | </nav> | ||||
| ); | ); | ||||
| @@ -69,7 +76,8 @@ class WBPagination extends Component { | |||||
| } | } | ||||
| WBPagination.defaultProps = { | WBPagination.defaultProps = { | ||||
| 'chunkSize': 5 | |||||
| 'chunkSize': 5, | |||||
| 'getPageUrl': () => ('#') | |||||
| }; | }; | ||||
| export default WBPagination; | export default WBPagination; | ||||