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