github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-supply-chain-master/asset_client/src/views/asset_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 ['weight', 'Weight'], 40 ['location', 'Location'], 41 ['temperature', 'Temperature'], 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=/assets/${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 AuthorizeReporter = { 354 oninit (vnode) { 355 vnode.state.properties = [] 356 }, 357 358 view (vnode) { 359 return [ 360 _row(m('strong', 'Authorize Reporter')), 361 m('.row', 362 m('.col-6', 363 m('input.form-control', { 364 type: 'text', 365 placeholder: 'Add reporter by name or public key...', 366 value: vnode.state.reporter, 367 oninput: m.withAttr('value', (value) => { 368 // clear any previously matched values 369 vnode.state.reporterKey = null 370 vnode.state.reporter = value 371 let reporter = vnode.attrs.agents.find( 372 (agent) => agent.name === value || agent.key === value) 373 if (reporter) { 374 vnode.state.reporterKey = reporter.key 375 } 376 }) 377 })), 378 379 m('.col-4', 380 m(MultiSelect, { 381 label: 'Select Fields', 382 color: 'primary', 383 options: authorizableProperties, 384 selected: vnode.state.properties, 385 onchange: (selection) => { 386 vnode.state.properties = selection 387 } 388 })), 389 390 m('.col-2', 391 m('button.btn.btn-primary', 392 { 393 disabled: (!vnode.state.reporterKey || vnode.state.properties.length === 0), 394 onclick: (e) => { 395 e.preventDefault() 396 vnode.attrs.onsubmit([vnode.state.reporterKey, vnode.state.properties]) 397 vnode.state.reporterKey = null 398 vnode.state.reporter = null 399 vnode.state.properties = [] 400 } 401 }, 402 'Authorize'))) 403 ] 404 } 405 } 406 407 const AssetDetail = { 408 oninit (vnode) { 409 _loadData(vnode.attrs.recordId, vnode.state) 410 vnode.state.refreshId = setInterval(() => { 411 _loadData(vnode.attrs.recordId, vnode.state) 412 }, 2000) 413 }, 414 415 onbeforeremove (vnode) { 416 clearInterval(vnode.state.refreshId) 417 }, 418 419 view (vnode) { 420 if (!vnode.state.record) { 421 return m('.alert-warning', `Loading ${vnode.attrs.recordId}`) 422 } 423 424 let publicKey = api.getPublicKey() 425 let owner = vnode.state.owner 426 let custodian = vnode.state.custodian 427 let record = vnode.state.record 428 return [ 429 m('.asset-detail', 430 m('h1.text-center', record.recordId), 431 _row( 432 _labelProperty('Created', 433 _formatTimestamp(getOldestPropertyUpdateTime(record))), 434 _labelProperty('Updated', 435 _formatTimestamp(getLatestPropertyUpdateTime(record)))), 436 437 _row( 438 _labelProperty('Owner', _agentLink(owner)), 439 m(TransferControl, { 440 publicKey, 441 record, 442 agents: vnode.state.agents, 443 role: 'owner', 444 label: 'Ownership', 445 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 446 })), 447 448 _row( 449 _labelProperty('Custodian', _agentLink(custodian)), 450 m(TransferControl, { 451 publicKey, 452 record, 453 agents: vnode.state.agents, 454 role: 'custodian', 455 label: 'Custodianship', 456 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 457 })), 458 459 _row( 460 _labelProperty('Type', getPropertyValue(record, 'type')), 461 _labelProperty('Subtype', getPropertyValue(record, 'subtype'))), 462 463 _row( 464 _labelProperty( 465 'Weight', 466 _propLink(record, 'weight', _formatValue(record, 'weight'))), 467 (isReporter(record, 'weight', publicKey) && !record.final 468 ? m(ReportValue, 469 { 470 name: 'weight', 471 label: 'Weight (kg)', 472 record, 473 typeField: 'numberValue', 474 type: payloads.updateProperties.enum.NUMBER, 475 xform: (x) => parsing.toInt(x), 476 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 477 }) 478 : null)), 479 480 _row( 481 _labelProperty( 482 'Location', 483 _propLink(record, 'location', _formatLocation(getPropertyValue(record, 'location'))) 484 ), 485 (isReporter(record, 'location', publicKey) && !record.final 486 ? m(ReportLocation, { record, onsuccess: () => _loadData(record.recordId, vnode.state) }) 487 : null)), 488 489 _row( 490 _labelProperty( 491 'Temperature', 492 _propLink(record, 'temperature', _formatTemp(getPropertyValue(record, 'temperature')))), 493 (isReporter(record, 'temperature', publicKey) && !record.final 494 ? m(ReportValue, 495 { 496 name: 'temperature', 497 label: 'Temperature (°C)', 498 record, 499 typeField: 'numberValue', 500 type: payloads.updateProperties.enum.NUMBER, 501 xform: (x) => parsing.toInt(x), 502 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 503 }) 504 : null)), 505 506 _row( 507 _labelProperty( 508 'Shock', 509 _propLink(record, 'shock', _formatValue(record, 'shock'))), 510 (isReporter(record, 'shock', publicKey) && !record.final 511 ? m(ReportValue, 512 { 513 name: 'shock', 514 label: 'Shock (g)', 515 record, 516 typeField: 'numberValue', 517 type: payloads.updateProperties.enum.NUMBER, 518 xform: (x) => parsing.toInt(x), 519 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 520 }) 521 : null)), 522 523 _row(m(ReporterControl, { 524 record, 525 publicKey, 526 agents: vnode.state.agents, 527 onsuccess: () => _loadData(vnode.attrs.recordId, vnode.state) 528 })), 529 530 ((record.owner === publicKey && !record.final) 531 ? m('.row.m-2', 532 m('.col.text-center', 533 m('button.btn.btn-danger', { 534 onclick: (e) => { 535 e.preventDefault() 536 _finalizeRecord(record).then(() => 537 _loadData(vnode.attrs.recordId, vnode.state)) 538 } 539 }, 540 'Finalize'))) 541 : '') 542 ) 543 ] 544 } 545 } 546 547 const _formatValue = (record, propName) => { 548 let prop = getPropertyValue(record, propName) 549 if (prop) { 550 return parsing.stringifyValue(parsing.floatifyValue(prop), '***', propName) 551 } else { 552 return 'N/A' 553 } 554 } 555 556 const _formatLocation = (location) => { 557 if (location && location.latitude !== undefined && location.longitude !== undefined) { 558 let latitude = parsing.toFloat(location.latitude) 559 let longitude = parsing.toFloat(location.longitude) 560 return `${latitude}, ${longitude}` 561 } else { 562 return 'Unknown' 563 } 564 } 565 566 const _formatTemp = (temp) => { 567 if (temp !== undefined && temp !== null) { 568 return `${parsing.toFloat(temp)} °C` 569 } 570 571 return 'Unknown' 572 } 573 574 const _formatTimestamp = (sec) => { 575 if (!sec) { 576 sec = Date.now() / 1000 577 } 578 return moment.unix(sec).format('YYYY-MM-DD') 579 } 580 581 const _loadData = (recordId, state) => { 582 let publicKey = api.getPublicKey() 583 return api.get(`records/${recordId}`) 584 .then(record => 585 Promise.all([ 586 record, 587 api.get('agents')])) 588 .then(([record, agents, owner, custodian]) => { 589 state.record = record 590 state.agents = agents.filter((agent) => agent.key !== publicKey) 591 state.owner = agents.find((agent) => agent.key === record.owner) 592 state.custodian = agents.find((agent) => agent.key === record.custodian) 593 }) 594 } 595 596 const _submitProposal = (record, role, publicKey) => { 597 let transferPayload = payloads.createProposal({ 598 recordId: record.recordId, 599 receivingAgent: publicKey, 600 role: role 601 }) 602 603 return transactions.submit([transferPayload], true).then(() => { 604 console.log('Successfully submitted proposal') 605 }) 606 } 607 608 const _answerProposal = (record, publicKey, role, response) => { 609 let answerPayload = payloads.answerProposal({ 610 recordId: record.recordId, 611 receivingAgent: publicKey, 612 role, 613 response 614 }) 615 616 return transactions.submit([answerPayload], true).then(() => { 617 console.log('Successfully submitted answer') 618 }) 619 } 620 621 const _updateProperty = (record, value) => { 622 let updatePayload = payloads.updateProperties({ 623 recordId: record.recordId, 624 properties: [value] 625 }) 626 627 return transactions.submit([updatePayload], true).then(() => { 628 console.log('Successfully submitted property update') 629 }) 630 } 631 632 const _finalizeRecord = (record) => { 633 let finalizePayload = payloads.finalizeRecord({ 634 recordId: record.recordId 635 }) 636 637 return transactions.submit([finalizePayload], true).then(() => { 638 console.log('finalized') 639 }) 640 } 641 642 const _authorizeReporter = (record, reporterKey, properties) => { 643 let authroizePayload = payloads.createProposal({ 644 recordId: record.recordId, 645 receivingAgent: reporterKey, 646 role: payloads.createProposal.enum.REPORTER, 647 properties: properties 648 }) 649 650 return transactions.submit([authroizePayload], true).then(() => { 651 console.log('Successfully submitted proposal') 652 }) 653 } 654 655 const _revokeAuthorization = (record, reporterKey, properties) => { 656 let revokePayload = payloads.revokeReporter({ 657 recordId: record.recordId, 658 reporterId: reporterKey, 659 properties 660 }) 661 662 return transactions.submit([revokePayload], true).then(() => { 663 console.log('Successfully revoked reporter') 664 }) 665 } 666 667 module.exports = AssetDetail