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