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