github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-supply-chain-master/fish_client/src/views/fish_detail.js (about) 1 /** 2 * Copyright 2017 Intel Corporation 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * ---------------------------------------------------------------------------- 16 */ 17 'use strict' 18 19 const m = require('mithril') 20 const moment = require('moment') 21 const truncate = require('lodash/truncate') 22 23 const {MultiSelect} = require('../components/forms') 24 const payloads = require('../services/payloads') 25 const parsing = require('../services/parsing') 26 const transactions = require('../services/transactions') 27 const api = require('../services/api') 28 const { 29 getPropertyValue, 30 getLatestPropertyUpdateTime, 31 getOldestPropertyUpdateTime, 32 isReporter 33 } = require('../utils/records') 34 35 /** 36 * Possible selection options 37 */ 38 const authorizableProperties = [ 39 ['location', 'Location'], 40 ['temperature', 'Temperature'], 41 ['tilt', 'Tilt'], 42 ['shock', 'Shock'] 43 ] 44 45 const _labelProperty = (label, value) => [ 46 m('dl', 47 m('dt', label), 48 m('dd', value)) 49 ] 50 51 const _row = (...cols) => 52 m('.row', 53 cols 54 .filter((col) => col !== null) 55 .map((col) => m('.col', col))) 56 57 const TransferDropdown = { 58 view (vnode) { 59 // Default to no-op 60 let onsuccess = vnode.attrs.onsuccess || (() => null) 61 let record = vnode.attrs.record 62 let role = vnode.attrs.role 63 let publicKey = vnode.attrs.publicKey 64 return [ 65 m('.dropdown', 66 m('button.btn.btn-primary.btn-block.dropdown-toggle.text-left', 67 { 'data-toggle': 'dropdown' }, 68 vnode.children), 69 m('.dropdown-menu', 70 vnode.attrs.agents.map(agent => { 71 let proposal = _getProposal(record, agent.key, role) 72 return [ 73 m("a.dropdown-item[href='#']", { 74 onclick: (e) => { 75 e.preventDefault() 76 if (proposal && proposal.issuingAgent === publicKey) { 77 _answerProposal(record, agent.key, ROLE_TO_ENUM[role], 78 payloads.answerProposal.enum.CANCEL) 79 .then(onsuccess) 80 } else { 81 _submitProposal(record, ROLE_TO_ENUM[role], agent.key) 82 .then(onsuccess) 83 } 84 } 85 }, m('span.text-truncate', 86 truncate(agent.name, { length: 32 }), 87 (proposal ? ' \u2718' : ''))) 88 ] 89 }))) 90 ] 91 } 92 } 93 94 const ROLE_TO_ENUM = { 95 'owner': payloads.createProposal.enum.OWNER, 96 'custodian': payloads.createProposal.enum.CUSTODIAN, 97 'reporter': payloads.createProposal.enum.REPORTER 98 } 99 100 const TransferControl = { 101 view (vnode) { 102 let {record, agents, publicKey, role, label} = vnode.attrs 103 if (record.final) { 104 return null 105 } 106 107 let onsuccess = vnode.attrs.onsuccess || (() => null) 108 if (record[role] === publicKey) { 109 return [ 110 m(TransferDropdown, { 111 publicKey, 112 agents, 113 record, 114 role, 115 onsuccess 116 }, `Transfer ${label}`) 117 ] 118 } else if (_hasProposal(record, publicKey, role)) { 119 return [ 120 m('.d-flex.justify-content-start', 121 m('button.btn.btn-primary', { 122 onclick: (e) => { 123 e.preventDefault() 124 _answerProposal(record, publicKey, ROLE_TO_ENUM[role], 125 payloads.answerProposal.enum.ACCEPT) 126 127 .then(onsuccess) 128 } 129 }, 130 `Accept ${label}`), 131 m('button.btn.btn-danger.ml-auto', { 132 onclick: (e) => { 133 e.preventDefault() 134 _answerProposal(record, publicKey, ROLE_TO_ENUM[role], 135 payloads.answerProposal.enum.REJECT) 136 .then(onsuccess) 137 } 138 }, 139 `Reject`)) 140 ] 141 } else { 142 return null 143 } 144 } 145 } 146 147 const _getProposal = (record, receivingAgent, role) => 148 record.proposals.find( 149 (proposal) => (proposal.role.toLowerCase() === role && proposal.receivingAgent === receivingAgent)) 150 151 const _hasProposal = (record, receivingAgent, role) => 152 !!_getProposal(record, receivingAgent, role) 153 154 const ReporterControl = { 155 view (vnode) { 156 let {record, agents, publicKey} = vnode.attrs 157 if (record.final) { 158 return null 159 } 160 161 let onsuccess = vnode.attrs.onsuccess || (() => null) 162 if (record.owner === publicKey) { 163 return [ 164 m(AuthorizeReporter, { 165 record, 166 agents, 167 onsubmit: ([publicKey, properties]) => 168 _authorizeReporter(record, publicKey, properties).then(onsuccess) 169 }), 170 171 // Outstanding reporters 172 Object.entries(_reporters(record)) 173 .filter(([key, _]) => key !== publicKey) 174 .map(([key, properties]) => { 175 return [ 176 m('.mt-2.d-flex.justify-content-start', 177 `${_agentByKey(agents, key).name} authorized for ${properties}`, 178 m('.button.btn.btn-outline-danger.ml-auto', { 179 onclick: (e) => { 180 e.preventDefault() 181 _revokeAuthorization(record, key, properties) 182 .then(onsuccess) 183 } 184 }, 185 'Revoke Authorization')) 186 ] 187 }), 188 189 // Pending authorizations 190 record.proposals.filter((p) => p.role === 'REPORTER' && p.issuingAgent === publicKey).map( 191 (p) => 192 m('.mt-2.d-flex.justify-content-start', 193 `Pending proposal for ${_agentByKey(agents, p.receivingAgent).name} on ${p.properties}`, 194 m('.button.btn.btn-outline-danger.ml-auto', 195 { 196 onclick: (e) => { 197 e.preventDefault() 198 _answerProposal(record, p.receivingAgent, ROLE_TO_ENUM['reporter'], 199 payloads.answerProposal.enum.CANCEL) 200 .then(onsuccess) 201 } 202 }, 203 'Rescind Proposal'))) 204 205 ] 206 } else if (_hasProposal(record, publicKey, 'reporter')) { 207 let proposal = _getProposal(record, publicKey, 'reporter') 208 return [ 209 m('.d-flex.justify-content-start', 210 m('button.btn.btn-primary', { 211 onclick: (e) => { 212 e.preventDefault() 213 _answerProposal(record, publicKey, ROLE_TO_ENUM['reporter'], 214 payloads.answerProposal.enum.ACCEPT) 215 .then(onsuccess) 216 } 217 }, 218 `Accept Reporting Authorization for ${proposal.properties}`), 219 m('button.btn.btn-danger.ml-auto', { 220 onclick: (e) => { 221 e.preventDefault() 222 _answerProposal(record, publicKey, ROLE_TO_ENUM['reporter'], 223 payloads.answerProposal.enum.REJECT) 224 .then(onsuccess) 225 } 226 }, 227 `Reject`)) 228 ] 229 } else { 230 return null 231 } 232 } 233 } 234 235 /** 236 * Returns a map of reporter key, to authorized fields 237 */ 238 const _reporters = (record) => 239 record.properties.reduce((acc, property) => { 240 return property.reporters.reduce((acc, key) => { 241 let props = (acc[key] || []) 242 props.push(property.name) 243 acc[key] = props 244 return acc 245 }, acc) 246 }, {}) 247 248 const _agentByKey = (agents, key) => 249 agents.find((agent) => agent.key === key) || { name: 'Unknown Agent' } 250 251 const _agentLink = (agent) => 252 m(`a[href=/agents/${agent.key}]`, 253 { oncreate: m.route.link }, 254 agent.name) 255 256 const _propLink = (record, propName, content) => 257 m(`a[href=/properties/${record.recordId}/${propName}]`, 258 { oncreate: m.route.link }, 259 content) 260 261 const ReportLocation = { 262 view: (vnode) => { 263 let onsuccess = vnode.attrs.onsuccess || (() => null) 264 return [ 265 m('form', { 266 onsubmit: (e) => { 267 e.preventDefault() 268 _updateProperty(vnode.attrs.record, { 269 name: 'location', 270 locationValue: { 271 latitude: parsing.toInt(vnode.state.latitude), 272 longitude: parsing.toInt(vnode.state.longitude) 273 }, 274 dataType: payloads.updateProperties.enum.LOCATION 275 }).then(() => { 276 vnode.state.latitude = '' 277 vnode.state.longitude = '' 278 }) 279 .then(onsuccess) 280 } 281 }, 282 m('.form-row', 283 m('.form-group.col-5', 284 m('label.sr-only', { 'for': 'latitude' }, 'Latitude'), 285 m("input.form-control[type='text']", { 286 name: 'latitude', 287 type: 'number', 288 step: 'any', 289 min: -90, 290 max: 90, 291 onchange: m.withAttr('value', (value) => { 292 vnode.state.latitude = value 293 }), 294 value: vnode.state.latitude, 295 placeholder: 'Latitude' 296 })), 297 m('.form-group.col-5', 298 m('label.sr-only', { 'for': 'longitude' }, 'Longitude'), 299 m("input.form-control[type='text']", { 300 name: 'longitude', 301 type: 'number', 302 step: 'any', 303 min: -180, 304 max: 180, 305 onchange: m.withAttr('value', (value) => { 306 vnode.state.longitude = value 307 }), 308 value: vnode.state.longitude, 309 placeholder: 'Longitude' 310 })), 311 312 m('.col-2', 313 m('button.btn.btn-primary', 'Update')))) 314 ] 315 } 316 } 317 318 const ReportValue = { 319 view: (vnode) => { 320 let onsuccess = vnode.attrs.onsuccess || (() => null) 321 let xform = vnode.attrs.xform || ((x) => x) 322 return [ 323 m('form', { 324 onsubmit: (e) => { 325 e.preventDefault() 326 _updateProperty(vnode.attrs.record, { 327 name: vnode.attrs.name, 328 [vnode.attrs.typeField]: xform(vnode.state.value), 329 dataType: vnode.attrs.type 330 }).then(() => { 331 vnode.state.value = '' 332 }) 333 .then(onsuccess) 334 } 335 }, 336 m('.form-row', 337 m('.form-group.col-10', 338 m('label.sr-only', { 'for': vnode.attrs.name }, vnode.attrs.label), 339 m("input.form-control[type='text']", { 340 name: vnode.attrs.name, 341 onchange: m.withAttr('value', (value) => { 342 vnode.state.value = value 343 }), 344 value: vnode.state.value, 345 placeholder: vnode.attrs.label 346 })), 347 m('.col-2', 348 m('button.btn.btn-primary', 'Update')))) 349 ] 350 } 351 } 352 353 const ReportTilt = { 354 view: (vnode) => { 355 let onsuccess = vnode.attrs.onsuccess || (() => null) 356 return [ 357 m('form', { 358 onsubmit: (e) => { 359 e.preventDefault() 360 _updateProperty(vnode.attrs.record, { 361 name: 'tilt', 362 stringValue: JSON.stringify({ 363 x: parsing.toInt(vnode.state.x), 364 y: parsing.toInt(vnode.state.y) 365 }), 366 dataType: payloads.updateProperties.enum.STRING 367 }) 368 .then(() => { 369 vnode.state.x = null 370 vnode.state.y = null 371 }) 372 .then(onsuccess) 373 } 374 }, 375 m('.form-row', 376 m('.col.md-4.mr-1', 377 m('input.form-control', { 378 placeholder: 'Enter X...', 379 type: 'number', 380 step: 'any', 381 oninput: m.withAttr('value', (value) => { 382 vnode.state.x = value 383 }) 384 })), 385 m('.col.md-4', 386 m('input.form-control', { 387 placeholder: 'Enter Y...', 388 type: 'number', 389 step: 'any', 390 oninput: m.withAttr('value', (value) => { 391 vnode.state.y = value 392 }) 393 })), 394 m('.col-2', 395 m('button.btn.btn-primary', 'Update')))) 396 ] 397 } 398 } 399 400 const ReportShock = { 401 view: (vnode) => { 402 let onsuccess = vnode.attrs.onsuccess || (() => null) 403 return [ 404 m('form', { 405 onsubmit: (e) => { 406 e.preventDefault() 407 _updateProperty(vnode.attrs.record, { 408 name: 'shock', 409 stringValue: JSON.stringify({ 410 accel: parsing.toInt(vnode.state.accel), 411 duration: parsing.toInt(vnode.state.duration) 412 }), 413 dataType: payloads.updateProperties.enum.STRING 414 }) 415 .then(() => { 416 vnode.state.accel = null 417 vnode.state.duration = null 418 }) 419 .then(onsuccess) 420 } 421 }, 422 m('.form-row', 423 m('.col.md-4.mr-1', 424 m('input.form-control', { 425 placeholder: 'Enter Acceleration...', 426 type: 'number', 427 step: 'any', 428 min: 0, 429 oninput: m.withAttr('value', (value) => { 430 vnode.state.accel = value 431 }) 432 })), 433 m('.col.md-4', 434 m('input.form-control', { 435 placeholder: 'Enter Duration...', 436 type: 'number', 437 step: 'any', 438 min: 0, 439 oninput: m.withAttr('value', (value) => { 440 vnode.state.duration = value 441 }) 442 })), 443 m('.col-2', 444 m('button.btn.btn-primary', 'Update')))) 445 ] 446 } 447 } 448 449 const AuthorizeReporter = { 450 oninit (vnode) { 451 vnode.state.properties = [] 452 }, 453 454 view (vnode) { 455 return [ 456 _row(m('strong', 'Authorize Reporter')), 457 m('.row', 458 m('.col-6', 459 m('input.form-control', { 460 type: 'text', 461 placeholder: 'Add reporter by name or public key...', 462 value: vnode.state.reporter, 463 oninput: m.withAttr('value', (value) => { 464 // clear any previously matched values 465 vnode.state.reporterKey = null 466 vnode.state.reporter = value 467 let reporter = vnode.attrs.agents.find( 468 (agent) => agent.name === value || agent.key === value) 469 if (reporter) { 470 vnode.state.reporterKey = reporter.key 471 } 472 }) 473 })), 474 475 m('.col-4', 476 m(MultiSelect, { 477 label: 'Select Fields', 478 color: 'primary', 479 options: authorizableProperties, 480 selected: vnode.state.properties, 481 onchange: (selection) => { 482 vnode.state.properties = selection 483 } 484 })), 485 486 m('.col-2', 487 m('button.btn.btn-primary', 488 { 489 disabled: (!vnode.state.reporterKey || vnode.state.properties.length === 0), 490 onclick: (e) => { 491 e.preventDefault() 492 vnode.attrs.onsubmit([vnode.state.reporterKey, vnode.state.properties]) 493 vnode.state.reporterKey = null 494 vnode.state.reporter = null 495 vnode.state.properties = [] 496 } 497 }, 498 'Authorize'))) 499 ] 500 } 501 } 502 503 const FishDetail = { 504 oninit (vnode) { 505 _loadData(vnode.attrs.recordId, vnode.state) 506 vnode.state.refreshId = setInterval(() => { 507 _loadData(vnode.attrs.recordId, vnode.state) 508 }, 2000) 509 }, 510 511 onbeforeremove (vnode) { 512 clearInterval(vnode.state.refreshId) 513 }, 514 515 view (vnode) { 516 if (!vnode.state.record) { 517 return m('.alert-warning', `Loading ${vnode.attrs.recordId}`) 518 } 519 520 let publicKey = api.getPublicKey() 521 let owner = vnode.state.owner 522 let custodian = vnode.state.custodian 523 let record = vnode.state.record 524 return [ 525 m('.fish-detail', 526 m('h1.text-center', record.recordId), 527 _row( 528 _labelProperty('Created', 529 _formatTimestamp(getOldestPropertyUpdateTime(record))), 530 _labelProperty('Updated', 531 _formatTimestamp(getLatestPropertyUpdateTime(record)))), 532 533 _row( 534 _labelProperty('Owner', _agentLink(owner)), 535 m(TransferControl, { 536 publicKey, 537 record, 538 agents: vnode.state.agents, 539 role: 'owner', 540 label: 'Ownership', 541 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 542 })), 543 544 _row( 545 _labelProperty('Custodian', _agentLink(custodian)), 546 m(TransferControl, { 547 publicKey, 548 record, 549 agents: vnode.state.agents, 550 role: 'custodian', 551 label: 'Custodianship', 552 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 553 })), 554 555 _row(_labelProperty('Species', getPropertyValue(record, 'species'))), 556 557 _row( 558 _labelProperty('Length (m)', parsing.toFloat(getPropertyValue(record, 'length', 0))), 559 _labelProperty('Weight (kg)', parsing.toFloat(getPropertyValue(record, 'weight', 0)))), 560 561 _row( 562 _labelProperty( 563 'Location', 564 _propLink(record, 'location', _formatLocation(getPropertyValue(record, 'location'))) 565 ), 566 (isReporter(record, 'location', publicKey) && !record.final 567 ? m(ReportLocation, { record, onsuccess: () => _loadData(record.recordId, vnode.state) }) 568 : null)), 569 570 _row( 571 _labelProperty( 572 'Temperature', 573 _propLink(record, 'temperature', _formatTemp(getPropertyValue(record, 'temperature')))), 574 (isReporter(record, 'temperature', publicKey) && !record.final 575 ? m(ReportValue, 576 { 577 name: 'temperature', 578 label: 'Temperature (C°)', 579 record, 580 typeField: 'numberValue', 581 type: payloads.updateProperties.enum.NUMBER, 582 xform: (x) => parsing.toInt(x), 583 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 584 }) 585 : null)), 586 587 _row( 588 _labelProperty( 589 'Tilt', 590 _propLink(record, 'tilt', _formatValue(record, 'tilt'))), 591 (isReporter(record, 'tilt', publicKey) && !record.final 592 ? m(ReportTilt, { 593 record, 594 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 595 }) 596 : null)), 597 598 _row( 599 _labelProperty( 600 'Shock', 601 _propLink(record, 'shock', _formatValue(record, 'shock'))), 602 (isReporter(record, 'shock', publicKey) && !record.final 603 ? m(ReportShock, { 604 record, 605 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 606 }) 607 : null)), 608 609 _row(m(ReporterControl, { 610 record, 611 publicKey, 612 agents: vnode.state.agents, 613 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 614 })), 615 616 ((record.owner === publicKey && !record.final) 617 ? m('.row.m-2', 618 m('.col.text-center', 619 m('button.btn.btn-danger', { 620 onclick: (e) => { 621 e.preventDefault() 622 _finalizeRecord(record).then(() => 623 _loadData(vnode.attrs.recordId, vnode.state)) 624 } 625 }, 626 'Finalize'))) 627 : '') 628 ) 629 ] 630 } 631 } 632 633 const _formatValue = (record, propName) => { 634 let prop = getPropertyValue(record, propName) 635 if (prop) { 636 return parsing.stringifyValue(parsing.floatifyValue(prop), '***', propName) 637 } else { 638 return 'N/A' 639 } 640 } 641 642 const _formatLocation = (location) => { 643 if (location && location.latitude !== undefined && location.longitude !== undefined) { 644 let latitude = parsing.toFloat(location.latitude) 645 let longitude = parsing.toFloat(location.longitude) 646 return `${latitude}, ${longitude}` 647 } else { 648 return 'Unknown' 649 } 650 } 651 652 const _formatTemp = (temp) => { 653 if (temp !== undefined && temp !== null) { 654 return `${parsing.toFloat(temp)} °C` 655 } 656 657 return 'Unknown' 658 } 659 660 const _formatTimestamp = (sec) => { 661 if (!sec) { 662 sec = Date.now() / 1000 663 } 664 return moment.unix(sec).format('YYYY-MM-DD') 665 } 666 667 const _loadData = (recordId, state) => { 668 let publicKey = api.getPublicKey() 669 return api.get(`records/${recordId}`) 670 .then(record => 671 Promise.all([ 672 record, 673 api.get('agents')])) 674 .then(([record, agents, owner, custodian]) => { 675 state.record = record 676 state.agents = agents.filter((agent) => agent.key !== publicKey) 677 state.owner = agents.find((agent) => agent.key === record.owner) 678 state.custodian = agents.find((agent) => agent.key === record.custodian) 679 }) 680 } 681 682 const _submitProposal = (record, role, publicKey) => { 683 let transferPayload = payloads.createProposal({ 684 recordId: record.recordId, 685 receivingAgent: publicKey, 686 role: role 687 }) 688 689 return transactions.submit([transferPayload], true).then(() => { 690 console.log('Successfully submitted proposal') 691 }) 692 } 693 694 const _answerProposal = (record, publicKey, role, response) => { 695 let answerPayload = payloads.answerProposal({ 696 recordId: record.recordId, 697 receivingAgent: publicKey, 698 role, 699 response 700 }) 701 702 return transactions.submit([answerPayload], true).then(() => { 703 console.log('Successfully submitted answer') 704 }) 705 } 706 707 const _updateProperty = (record, value) => { 708 let updatePayload = payloads.updateProperties({ 709 recordId: record.recordId, 710 properties: [value] 711 }) 712 713 return transactions.submit([updatePayload], true).then(() => { 714 console.log('Successfully submitted property update') 715 }) 716 } 717 718 const _finalizeRecord = (record) => { 719 let finalizePayload = payloads.finalizeRecord({ 720 recordId: record.recordId 721 }) 722 723 return transactions.submit([finalizePayload], true).then(() => { 724 console.log('finalized') 725 }) 726 } 727 728 const _authorizeReporter = (record, reporterKey, properties) => { 729 let authroizePayload = payloads.createProposal({ 730 recordId: record.recordId, 731 receivingAgent: reporterKey, 732 role: payloads.createProposal.enum.REPORTER, 733 properties: properties 734 }) 735 736 return transactions.submit([authroizePayload], true).then(() => { 737 console.log('Successfully submitted proposal') 738 }) 739 } 740 741 const _revokeAuthorization = (record, reporterKey, properties) => { 742 let revokePayload = payloads.revokeReporter({ 743 recordId: record.recordId, 744 reporterId: reporterKey, 745 properties 746 }) 747 748 return transactions.submit([revokePayload], true).then(() => { 749 console.log('Successfully revoked reporter') 750 }) 751 } 752 753 module.exports = FishDetail