@@ -21,3 +21,7 @@ div.wb-json-viewer { | |||||
font-family: "Courier New", fixed-width; | font-family: "Courier New", fixed-width; | ||||
white-space: pre-wrap; | white-space: pre-wrap; | ||||
} | } | ||||
textarea.wb-json-editor { | |||||
font-family: "Courier New", fixed-width; | |||||
} |
@@ -0,0 +1,16 @@ | |||||
import makeArvadosRequest from 'make-arvados-request'; | |||||
import arvadosTypeName from 'arvados-type-name'; | |||||
function wbUpdateField(arvHost, arvToken, uuid, fieldName, fieldValue) { | |||||
const typeName = arvadosTypeName(uuid); | |||||
const data = {}; | |||||
data[fieldName] = fieldValue; | |||||
const prom = makeArvadosRequest(arvHost, arvToken, | |||||
'/arvados/v1/' + typeName + 's/' + uuid, { | |||||
method: 'PUT', | |||||
data: JSON.stringify(data) | |||||
}); | |||||
return prom; | |||||
} | |||||
export default wbUpdateField; |
@@ -9,18 +9,54 @@ import WBNameAndUuid from 'wb-name-and-uuid'; | |||||
import WBAccordion from 'wb-accordion'; | import WBAccordion from 'wb-accordion'; | ||||
import wbFormatSpecialValue from 'wb-format-special-value'; | import wbFormatSpecialValue from 'wb-format-special-value'; | ||||
import WBJsonViewer from 'wb-json-viewer'; | import WBJsonViewer from 'wb-json-viewer'; | ||||
import WBJsonEditor from 'wb-json-editor'; | |||||
import wbUpdateField from 'wb-update-field'; | |||||
class WBCollectionFields extends Component { | class WBCollectionFields extends Component { | ||||
componentDidMount() { | componentDidMount() { | ||||
this.prepareRows(); | |||||
this.fetchData(); | |||||
} | } | ||||
componentWillReceiveProps(nextProps) { | componentWillReceiveProps(nextProps) { | ||||
this.props = nextProps; | this.props = nextProps; | ||||
this.prepareRows(); | |||||
this.fetchData(); | |||||
} | } | ||||
prepareRows() { | |||||
prepareRows(item) { | |||||
const { app } = this.props; | |||||
const { arvHost, arvToken } = app.state; | |||||
let rows = [ | |||||
[ 'Name', item.name ], | |||||
[ 'Description', wbFormatSpecialValue(item.description) ], | |||||
[ 'Properties', ( | |||||
<WBJsonEditor name="Properties" app={ app } value={ item.properties } | |||||
onChange={ value => wbUpdateField(arvHost, arvToken, item.uuid, 'properties', value) | |||||
.then(() => { item.properties = value; this.prepareRows(item); }) } /> | |||||
) ], | |||||
[ 'Portable Data Hash', item.portable_data_hash ], | |||||
[ 'Replication Desired', item.replication_desired ? item.replication_desired : ( | |||||
<i>{ String(item.replication_desired) }</i> | |||||
) ], | |||||
[ 'Replication Confirmed', item.replication_confirmed ? item.replication_confirmed : ( | |||||
<i>{ String(item.replication_confirmed) }</i> | |||||
) ], | |||||
[ 'Replication Confirmed At', wbFormatDate(item.replication_confirmed_at) ], | |||||
[ 'Trash At', wbFormatDate(item.trash_at) ], | |||||
[ 'Delete At', wbFormatDate(item.delete_at) ], | |||||
[ 'Is Trashed', String(item.is_trashed) ], | |||||
[ 'Current Version UUID', ( | |||||
<WBNameAndUuid app={ app } uuid={ item.current_version_uuid } /> | |||||
) ], | |||||
[ 'Version', item.version ], | |||||
[ 'Preserve Version', String(item.preserve_version) ], | |||||
[ 'File Count', item.file_count ], | |||||
[ 'Total Size', filesize(item.file_size_total) ] | |||||
]; | |||||
this.setState({ 'rows': rows }); | |||||
} | |||||
fetchData() { | |||||
let { uuid, app } = this.props; | let { uuid, app } = this.props; | ||||
let { arvHost, arvToken } = app.state; | let { arvHost, arvToken } = app.state; | ||||
@@ -35,34 +71,7 @@ class WBCollectionFields extends Component { | |||||
const item = xhr.response.items[0]; | const item = xhr.response.items[0]; | ||||
if (!item) | if (!item) | ||||
throw Error('Item not found'); | throw Error('Item not found'); | ||||
let rows = [ | |||||
[ 'Name', item.name ], | |||||
[ 'Description', wbFormatSpecialValue(item.description) ], | |||||
[ 'Properties', ( | |||||
<WBAccordion names={ ['Properties'] } cardHeaderClass="card-header-sm"> | |||||
<WBJsonViewer app={ app } value={ item.properties } /> | |||||
</WBAccordion> | |||||
) ], | |||||
[ 'Portable Data Hash', item.portable_data_hash ], | |||||
[ 'Replication Desired', item.replication_desired ? item.replication_desired : ( | |||||
<i>{ String(item.replication_desired) }</i> | |||||
) ], | |||||
[ 'Replication Confirmed', item.replication_confirmed ? item.replication_confirmed : ( | |||||
<i>{ String(item.replication_confirmed) }</i> | |||||
) ], | |||||
[ 'Replication Confirmed At', wbFormatDate(item.replication_confirmed_at) ], | |||||
[ 'Trash At', wbFormatDate(item.trash_at) ], | |||||
[ 'Delete At', wbFormatDate(item.delete_at) ], | |||||
[ 'Is Trashed', String(item.is_trashed) ], | |||||
[ 'Current Version UUID', ( | |||||
<WBNameAndUuid app={ app } uuid={ item.current_version_uuid } /> | |||||
) ], | |||||
[ 'Version', item.version ], | |||||
[ 'Preserve Version', String(item.preserve_version) ], | |||||
[ 'File Count', item.file_count ], | |||||
[ 'Total Size', filesize(item.file_size_total) ] | |||||
]; | |||||
this.setState({ 'rows': rows }); | |||||
this.prepareRows(item); | |||||
}); | }); | ||||
} | } | ||||
@@ -0,0 +1,60 @@ | |||||
import { h, Component, createRef } from 'preact'; | |||||
import WBJsonViewer from 'wb-json-viewer'; | |||||
import WBAccordion from 'wb-accordion'; | |||||
import WBDialog from 'wb-dialog'; | |||||
class WBJsonEditor extends Component { | |||||
constructor(...args) { | |||||
super(...args); | |||||
this.dialogRef = createRef(); | |||||
} | |||||
render({ app, name, value, stringify, pretty, onChange }, { editValue, parseError }) { | |||||
return ( | |||||
<div> | |||||
<WBDialog title={ 'Edit ' + name } ref={ this.dialogRef } | |||||
accept={ () => { | |||||
onChange(JSON.parse(editValue)); | |||||
} } | |||||
canAccept={ () => { | |||||
try { JSON.parse(editValue) } | |||||
catch (exc) { this.setState({ parseError: exc.message }); return false; } | |||||
return true; | |||||
} }> | |||||
<div> | |||||
<textarea class="form-control wb-json-editor" value={ editValue } rows="10" | |||||
onChange={ e => this.setState({ editValue: e.target.value }) } /> | |||||
{ parseError ? ( | |||||
<div class="alert alert-danger" role="alert"> | |||||
{ parseError } | |||||
</div> | |||||
) : null } | |||||
</div> | |||||
</WBDialog> | |||||
<WBAccordion names={ [ name ] } extraHeaderUi={ [ ( | |||||
<button class="btn btn-link px-0" title="Edit" | |||||
onclick={ () => { | |||||
this.setState({ parseError: null, | |||||
editValue: stringify ? | |||||
pretty ? JSON.stringify(value, null, 2) | |||||
: JSON.stringify(value) : value }); | |||||
this.dialogRef.current.show(); | |||||
} }> | |||||
<i class="fas fa-edit text-secondary" /> | |||||
</button> | |||||
) ] } cardHeaderClass="card-header-sm"> | |||||
<WBJsonViewer app={ app } value={ value } stringify={ stringify } | |||||
pretty={ pretty } /> | |||||
</WBAccordion> | |||||
</div> | |||||
); | |||||
} | |||||
} | |||||
WBJsonEditor.defaultProps = { | |||||
stringify: true, | |||||
pretty: true, | |||||
onChange: () => {} | |||||
}; | |||||
export default WBJsonEditor; |
@@ -39,7 +39,7 @@ class WBAccordion extends Component { | |||||
this.setState({ ariaExpanded, collapseClass, buttonClass }); | this.setState({ ariaExpanded, collapseClass, buttonClass }); | ||||
} | } | ||||
render({ children, names, cardHeaderClass }, { domId, headerDomIds, | |||||
render({ children, names, extraHeaderUi, cardHeaderClass }, { domId, headerDomIds, | |||||
collapseDomIds, collapseClass, ariaExpanded, buttonClass }) { | collapseDomIds, collapseClass, ariaExpanded, buttonClass }) { | ||||
return ( | return ( | ||||
@@ -53,6 +53,7 @@ class WBAccordion extends Component { | |||||
onclick={ () => this.toggle(i) }> | onclick={ () => this.toggle(i) }> | ||||
{ names[i] } | { names[i] } | ||||
</button> | </button> | ||||
{ extraHeaderUi[i] } | |||||
</h2> | </h2> | ||||
</div> | </div> | ||||
@@ -70,7 +71,8 @@ class WBAccordion extends Component { | |||||
}; | }; | ||||
WBAccordion.defaultProps = { | WBAccordion.defaultProps = { | ||||
'cardHeaderClass': 'card-header' | |||||
cardHeaderClass: 'card-header', | |||||
extraHeaderUi: [] | |||||
}; | }; | ||||
export default WBAccordion; | export default WBAccordion; |
@@ -18,7 +18,7 @@ class WBDialog extends Component { | |||||
$(this.modalRef.current).modal('hide'); | $(this.modalRef.current).modal('hide'); | ||||
} | } | ||||
render({ title, children, accept, reject }) { | |||||
render({ title, children, canAccept, accept, reject }) { | |||||
return ( | return ( | ||||
<form class="m-0"> | <form class="m-0"> | ||||
<div class="modal" tabindex="-1" role="dialog" ref={ this.modalRef }> | <div class="modal" tabindex="-1" role="dialog" ref={ this.modalRef }> | ||||
@@ -38,12 +38,12 @@ class WBDialog extends Component { | |||||
<div class="modal-footer"> | <div class="modal-footer"> | ||||
{ children[1] ? children[1] : [ | { children[1] ? children[1] : [ | ||||
<input type="submit" class="btn btn-primary" value="Accept" | <input type="submit" class="btn btn-primary" value="Accept" | ||||
onclick={ e => { e.preventDefault(); this.hide(); accept(); } } />, | |||||
onclick={ e => { e.preventDefault(); if (!canAccept()) return; this.hide(); accept(); } } />, | |||||
<button type="button" class="btn btn-secondary" | <button type="button" class="btn btn-secondary" | ||||
onclick={ () => { this.hide(); reject(); } }>Cancel</button> | onclick={ () => { this.hide(); reject(); } }>Cancel</button> | ||||
] } | ] } | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -53,9 +53,10 @@ class WBDialog extends Component { | |||||
} | } | ||||
WBDialog.defaultProps = { | WBDialog.defaultProps = { | ||||
'title': 'Dialog', | |||||
'accept': () => {}, | |||||
'reject': () => {} | |||||
title: 'Dialog', | |||||
accept: () => {}, | |||||
reject: () => {}, | |||||
canAccept: () => true | |||||
}; | }; | ||||
export default WBDialog; | export default WBDialog; |