@@ -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; |