| @@ -21,3 +21,7 @@ div.wb-json-viewer { | |||
| font-family: "Courier New", fixed-width; | |||
| 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 wbFormatSpecialValue from 'wb-format-special-value'; | |||
| import WBJsonViewer from 'wb-json-viewer'; | |||
| import WBJsonEditor from 'wb-json-editor'; | |||
| import wbUpdateField from 'wb-update-field'; | |||
| class WBCollectionFields extends Component { | |||
| componentDidMount() { | |||
| this.prepareRows(); | |||
| this.fetchData(); | |||
| } | |||
| componentWillReceiveProps(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 { arvHost, arvToken } = app.state; | |||
| @@ -35,34 +71,7 @@ class WBCollectionFields extends Component { | |||
| const item = xhr.response.items[0]; | |||
| if (!item) | |||
| 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 }); | |||
| } | |||
| render({ children, names, cardHeaderClass }, { domId, headerDomIds, | |||
| render({ children, names, extraHeaderUi, cardHeaderClass }, { domId, headerDomIds, | |||
| collapseDomIds, collapseClass, ariaExpanded, buttonClass }) { | |||
| return ( | |||
| @@ -53,6 +53,7 @@ class WBAccordion extends Component { | |||
| onclick={ () => this.toggle(i) }> | |||
| { names[i] } | |||
| </button> | |||
| { extraHeaderUi[i] } | |||
| </h2> | |||
| </div> | |||
| @@ -70,7 +71,8 @@ class WBAccordion extends Component { | |||
| }; | |||
| WBAccordion.defaultProps = { | |||
| 'cardHeaderClass': 'card-header' | |||
| cardHeaderClass: 'card-header', | |||
| extraHeaderUi: [] | |||
| }; | |||
| export default WBAccordion; | |||
| @@ -18,7 +18,7 @@ class WBDialog extends Component { | |||
| $(this.modalRef.current).modal('hide'); | |||
| } | |||
| render({ title, children, accept, reject }) { | |||
| render({ title, children, canAccept, accept, reject }) { | |||
| return ( | |||
| <form class="m-0"> | |||
| <div class="modal" tabindex="-1" role="dialog" ref={ this.modalRef }> | |||
| @@ -38,12 +38,12 @@ class WBDialog extends Component { | |||
| <div class="modal-footer"> | |||
| { children[1] ? children[1] : [ | |||
| <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" | |||
| onclick={ () => { this.hide(); reject(); } }>Cancel</button> | |||
| ] } | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -53,9 +53,10 @@ class WBDialog extends Component { | |||
| } | |||
| WBDialog.defaultProps = { | |||
| 'title': 'Dialog', | |||
| 'accept': () => {}, | |||
| 'reject': () => {} | |||
| title: 'Dialog', | |||
| accept: () => {}, | |||
| reject: () => {}, | |||
| canAccept: () => true | |||
| }; | |||
| export default WBDialog; | |||