github.com/simpleiot/simpleiot@v0.18.3/frontend/src/Pages/Home_.elm (about)

     1  module Pages.Home_ exposing (Model, Msg, NodeEdit, NodeMsg, NodeOperation, page)
     2  
     3  import Api.Data as Data exposing (Data)
     4  import Api.Node as Node exposing (Node, NodeView)
     5  import Api.Point as Point exposing (Point)
     6  import Api.Port as Port
     7  import Api.Response exposing (Response)
     8  import Auth
     9  import Base64.Encode
    10  import Components.NodeAction as NodeAction
    11  import Components.NodeCanBus as NodeCanBus
    12  import Components.NodeCondition as NodeCondition
    13  import Components.NodeDb as NodeDb
    14  import Components.NodeDevice as NodeDevice
    15  import Components.NodeFile as File
    16  import Components.NodeGroup as NodeGroup
    17  import Components.NodeMessageService as NodeMessageService
    18  import Components.NodeMetrics as NodeMetrics
    19  import Components.NodeModbus as NodeModbus
    20  import Components.NodeModbusIO as NodeModbusIO
    21  import Components.NodeNTP as NodeNTP
    22  import Components.NodeNetworkManager as NodeNetworkManager
    23  import Components.NodeNetworkManagerConn as NodeNetworkManagerConn
    24  import Components.NodeNetworkManagerDevice as NodeNetworkManagerDevice
    25  import Components.NodeOneWire as NodeOneWire
    26  import Components.NodeOneWireIO as NodeOneWireIO
    27  import Components.NodeOptions exposing (CopyMove(..))
    28  import Components.NodeParticle as NodeParticle
    29  import Components.NodeRaw as NodeRaw
    30  import Components.NodeRule as NodeRule
    31  import Components.NodeSerialDev as NodeSerialDev
    32  import Components.NodeShelly as NodeShelly
    33  import Components.NodeShellyIO as NodeShellyIO
    34  import Components.NodeSignalGenerator as SignalGenerator
    35  import Components.NodeSync as NodeSync
    36  import Components.NodeUpdate as NodeUpdate
    37  import Components.NodeUser as NodeUser
    38  import Components.NodeVariable as NodeVariable
    39  import Dict
    40  import Effect exposing (Effect)
    41  import Element exposing (..)
    42  import Element.Background as Background
    43  import Element.Font as Font
    44  import Element.Input as Input
    45  import File
    46  import File.Select
    47  import Gen.Params.Home_ exposing (Params)
    48  import Http
    49  import List.Extra
    50  import Page
    51  import Request
    52  import Shared
    53  import Storage
    54  import Task
    55  import Time
    56  import Tree exposing (Tree)
    57  import Tree.Zipper as Zipper
    58  import UI.Button as Button
    59  import UI.Form as Form
    60  import UI.Icon as Icon
    61  import UI.Layout
    62  import UI.Style as Style exposing (colors)
    63  import UI.ViewIf exposing (viewIf)
    64  import View exposing (View)
    65  
    66  
    67  page : Shared.Model -> Request.With Params -> Page.With Model Msg
    68  page shared _ =
    69      Page.protected.advanced <|
    70          \user ->
    71              { init = init shared
    72              , update = update shared
    73              , view = view user shared
    74              , subscriptions = subscriptions
    75              }
    76  
    77  
    78  
    79  -- INIT
    80  
    81  
    82  type alias Model =
    83      { nodeEdit : Maybe NodeEdit
    84      , addPoint : { typ : String, key : String }
    85      , customNodeType : String
    86      , zone : Time.Zone
    87      , now : Time.Posix
    88      , nodes : List (Tree NodeView)
    89      , error : Maybe String
    90      , lastError : Time.Posix
    91      , nodeOp : NodeOperation
    92      , copyMove : CopyMove
    93      , scratch : String
    94      , nodeMsg : Maybe NodeMsg
    95      , token : String
    96      }
    97  
    98  
    99  type alias NodeMsg =
   100      { feID : Int
   101      , text : String
   102      , time : Time.Posix
   103      }
   104  
   105  
   106  type NodeOperation
   107      = OpNone
   108      | OpNodeToAdd NodeToAdd
   109      | OpNodeMessage NodeMessage
   110      | OpNodeDelete Int String String
   111      | OpNodePaste Int String
   112  
   113  
   114  type alias NodeEdit =
   115      { feID : Int
   116      , points : List Point
   117      , viewRaw : Bool
   118      }
   119  
   120  
   121  type alias NodeToAdd =
   122      { typ : Maybe String
   123      , feID : Int
   124      , parent : String
   125      }
   126  
   127  
   128  type alias NodeMessage =
   129      { feID : Int
   130      , id : String
   131      , parent : String
   132      , message : String
   133      }
   134  
   135  
   136  defaultModel : Model
   137  defaultModel =
   138      Model
   139          Nothing
   140          { typ = "", key = "" }
   141          ""
   142          Time.utc
   143          (Time.millisToPosix 0)
   144          []
   145          Nothing
   146          (Time.millisToPosix 0)
   147          OpNone
   148          CopyMoveNone
   149          ""
   150          Nothing
   151          ""
   152  
   153  
   154  init : Shared.Model -> ( Model, Effect Msg )
   155  init shared =
   156      let
   157          token =
   158              case shared.storage.user of
   159                  Just user ->
   160                      user.token
   161  
   162                  Nothing ->
   163                      ""
   164  
   165          model =
   166              { defaultModel | token = token }
   167      in
   168      ( model
   169      , Effect.fromCmd <|
   170          Cmd.batch
   171              [ Task.perform Zone Time.here
   172              , Task.perform Tick Time.now
   173              , Node.list { onResponse = ApiRespList, token = token }
   174              ]
   175      )
   176  
   177  
   178  
   179  -- UPDATE
   180  
   181  
   182  type Msg
   183      = SignOut
   184      | Tick Time.Posix
   185      | Zone Time.Zone
   186      | EditNodePoint Int (List Point)
   187      | EditScratch String
   188      | UploadFile String Bool
   189      | UploadSelected String Bool File.File
   190      | UploadContents String File.File String
   191      | ToggleExpChildren Int
   192      | ToggleExpDetail Int
   193      | DiscardNodeOp
   194      | DiscardEdits
   195      | AddNode Int String
   196      | MsgNode Int String String
   197      | PasteNode Int String
   198      | DeleteNode Int String String
   199      | UpdateMsg String
   200      | SelectAddNodeType String
   201      | ApiDelete String String
   202      | ApiPostPoints String
   203      | ApiPostAddNode Int
   204      | ApiPostMoveNode Int String String String
   205      | ApiPutMirrorNode Int String String
   206      | ApiPutDuplicateNode Int String String
   207      | ApiPostNotificationNode
   208      | ApiRespList (Data (List Node))
   209      | ApiRespDelete (Data Response)
   210      | ApiRespPostPoint (Data Response)
   211      | ApiRespPostAddNode Int (Data Response)
   212      | ApiRespPostMoveNode Int (Data Response)
   213      | ApiRespPutMirrorNode Int (Data Response)
   214      | ApiRespPutDuplicateNode Int (Data Response)
   215      | ApiRespPostNotificationNode (Data Response)
   216      | CopyNode Int String String String
   217      | ClearClipboard
   218      | ToggleRaw Int
   219      | UpdateNewPointType String
   220      | UpdateNewPointKey String
   221      | UpdateCustomNodeType String
   222  
   223  
   224  update : Shared.Model -> Msg -> Model -> ( Model, Effect Msg )
   225  update shared msg model =
   226      case msg of
   227          SignOut ->
   228              ( model, Effect.fromCmd <| Storage.signOut shared.storage )
   229  
   230          EditNodePoint feID points ->
   231              let
   232                  editPoints =
   233                      case model.nodeEdit of
   234                          Just ne ->
   235                              ne.points
   236  
   237                          Nothing ->
   238                              []
   239  
   240                  viewRaw =
   241                      case model.nodeEdit of
   242                          Just ne ->
   243                              ne.viewRaw
   244  
   245                          Nothing ->
   246                              False
   247              in
   248              ( { model
   249                  | nodeEdit =
   250                      Just
   251                          { feID = feID
   252                          , points = Point.updatePoints editPoints points
   253                          , viewRaw = viewRaw
   254                          }
   255                  , scratch = ""
   256                }
   257              , Effect.none
   258              )
   259  
   260          EditScratch s ->
   261              ( { model | scratch = s }, Effect.none )
   262  
   263          UploadFile id binary ->
   264              ( model, Effect.fromCmd <| File.Select.file [ "" ] (UploadSelected id binary) )
   265  
   266          UploadSelected id binary file ->
   267              let
   268                  uploadContents =
   269                      UploadContents id file
   270  
   271                  encode d =
   272                      Base64.Encode.encode (Base64.Encode.bytes d)
   273  
   274                  task =
   275                      if binary then
   276                          Task.map encode (File.toBytes file)
   277  
   278                      else
   279                          File.toString file
   280              in
   281              -- File.toString results in Task x String, thus the complexity of one more step
   282              ( model, Effect.fromCmd <| Task.perform uploadContents task )
   283  
   284          UploadContents id file contents ->
   285              let
   286                  pointName =
   287                      Point Point.typeName "0" model.now 0 (File.name file) 0
   288  
   289                  pointData =
   290                      Point Point.typeData "0" model.now 0 contents 0
   291              in
   292              ( model
   293              , Effect.fromCmd <|
   294                  Node.postPoints
   295                      { token = model.token
   296                      , id = id
   297                      , points = [ pointName, pointData ]
   298                      , onResponse = ApiRespPostPoint
   299                      }
   300              )
   301  
   302          ApiPostPoints id ->
   303              case model.nodeEdit of
   304                  Just edit ->
   305                      let
   306                          points =
   307                              Point.clearText edit.points
   308  
   309                          -- optimistically update nodes
   310                          updatedNodes =
   311                              List.map
   312                                  (Tree.map
   313                                      (\n ->
   314                                          if n.node.id == id then
   315                                              let
   316                                                  node =
   317                                                      n.node
   318                                              in
   319                                              { n
   320                                                  | node =
   321                                                      { node
   322                                                          | points = Point.updatePoints node.points points
   323                                                      }
   324                                              }
   325  
   326                                          else
   327                                              n
   328                                      )
   329                                  )
   330                                  model.nodes
   331                      in
   332                      ( { model | nodeEdit = Nothing, nodes = updatedNodes }
   333                      , Effect.fromCmd <|
   334                          Node.postPoints
   335                              { token = model.token
   336                              , id = id
   337                              , points = points
   338                              , onResponse = ApiRespPostPoint
   339                              }
   340                      )
   341  
   342                  Nothing ->
   343                      ( model, Effect.none )
   344  
   345          DiscardNodeOp ->
   346              ( { model | nodeOp = OpNone }, Effect.none )
   347  
   348          DiscardEdits ->
   349              ( { model | nodeEdit = Nothing }
   350              , Effect.none
   351              )
   352  
   353          ToggleExpChildren feID ->
   354              let
   355                  nodes =
   356                      toggleExpChildren model.nodes feID
   357              in
   358              ( { model | nodes = nodes }, Effect.none )
   359  
   360          ToggleExpDetail feID ->
   361              let
   362                  nodes =
   363                      toggleExpDetail model.nodes feID
   364              in
   365              ( { model | nodes = nodes }, Effect.none )
   366  
   367          AddNode feID id ->
   368              ( { model
   369                  | nodeOp = OpNodeToAdd { typ = Nothing, feID = feID, parent = id }
   370                }
   371              , Effect.none
   372              )
   373  
   374          MsgNode feID id parent ->
   375              ( { model
   376                  | nodeOp =
   377                      OpNodeMessage
   378                          { id = id
   379                          , feID = feID
   380                          , parent = parent
   381                          , message = ""
   382                          }
   383                }
   384              , Effect.none
   385              )
   386  
   387          PasteNode feID id ->
   388              ( { model | nodeOp = OpNodePaste feID id }, Effect.none )
   389  
   390          DeleteNode feID id parent ->
   391              ( { model | nodeOp = OpNodeDelete feID id parent }, Effect.none )
   392  
   393          UpdateMsg message ->
   394              case model.nodeOp of
   395                  OpNodeMessage op ->
   396                      ( { model | nodeOp = OpNodeMessage { op | message = message } }, Effect.none )
   397  
   398                  _ ->
   399                      ( model, Effect.none )
   400  
   401          SelectAddNodeType typ ->
   402              case model.nodeOp of
   403                  OpNodeToAdd add ->
   404                      ( { model | nodeOp = OpNodeToAdd { add | typ = Just typ } }, Effect.none )
   405  
   406                  _ ->
   407                      ( model, Effect.none )
   408  
   409          ApiPostAddNode parent ->
   410              -- FIXME optimistically update nodes
   411              case model.nodeOp of
   412                  OpNodeToAdd addNode ->
   413                      case addNode.typ of
   414                          Just typ ->
   415                              ( { model | nodeOp = OpNone }
   416                              , Effect.fromCmd <|
   417                                  Node.insert
   418                                      { token = model.token
   419                                      , onResponse = ApiRespPostAddNode parent
   420                                      , node =
   421                                          { id = ""
   422                                          , typ =
   423                                              if typ == "custom" then
   424                                                  model.customNodeType
   425  
   426                                              else
   427                                                  typ
   428                                          , hash = 0
   429                                          , parent = addNode.parent
   430                                          , points =
   431                                              [ Point.newText
   432                                                  Point.typeDescription
   433                                                  ""
   434                                                  "New, please edit"
   435                                              ]
   436                                          , edgePoints = []
   437                                          }
   438                                      }
   439                              )
   440  
   441                          Nothing ->
   442                              ( { model | nodeOp = OpNone }, Effect.none )
   443  
   444                  _ ->
   445                      ( { model | nodeOp = OpNone }, Effect.none )
   446  
   447          ApiPostMoveNode parent id src dest ->
   448              ( model
   449              , Effect.fromCmd <|
   450                  Node.move
   451                      { token = model.token
   452                      , id = id
   453                      , oldParent = src
   454                      , newParent = dest
   455                      , onResponse = ApiRespPostMoveNode parent
   456                      }
   457              )
   458  
   459          ApiPutMirrorNode parent id dest ->
   460              ( model
   461              , Effect.fromCmd <|
   462                  Node.copy
   463                      { token = model.token
   464                      , id = id
   465                      , newParent = dest
   466                      , duplicate = False
   467                      , onResponse = ApiRespPutMirrorNode parent
   468                      }
   469              )
   470  
   471          ApiPutDuplicateNode parent id dest ->
   472              ( model
   473              , Effect.fromCmd <|
   474                  Node.copy
   475                      { token = model.token
   476                      , id = id
   477                      , newParent = dest
   478                      , duplicate = True
   479                      , onResponse = ApiRespPutDuplicateNode parent
   480                      }
   481              )
   482  
   483          ApiPostNotificationNode ->
   484              ( model
   485              , case model.nodeOp of
   486                  OpNodeMessage msgNode ->
   487                      Effect.fromCmd <|
   488                          Node.notify
   489                              { token = model.token
   490                              , not =
   491                                  { id = ""
   492                                  , parent = msgNode.parent
   493                                  , sourceNode = msgNode.id
   494                                  , subject = ""
   495                                  , message = msgNode.message
   496                                  }
   497                              , onResponse = ApiRespPostNotificationNode
   498                              }
   499  
   500                  _ ->
   501                      Effect.none
   502              )
   503  
   504          ApiDelete id parent ->
   505              -- optimistically update nodes
   506              let
   507                  nodes =
   508                      -- FIXME Tree.filter (\d -> d.id /= id) model.nodes
   509                      model.nodes
   510              in
   511              ( { model | nodes = nodes, nodeOp = OpNone }
   512              , Effect.fromCmd <|
   513                  Node.delete
   514                      { token = model.token
   515                      , id = id
   516                      , parent = parent
   517                      , onResponse = ApiRespDelete
   518                      }
   519              )
   520  
   521          Zone zone ->
   522              ( { model | zone = zone }, Effect.none )
   523  
   524          Tick now ->
   525              let
   526                  nodeMsg =
   527                      Maybe.andThen
   528                          (\m ->
   529                              let
   530                                  timeMs =
   531                                      Time.posixToMillis m.time
   532  
   533                                  nowMs =
   534                                      Time.posixToMillis model.now
   535                              in
   536                              if nowMs - timeMs > 3000 then
   537                                  Just m
   538  
   539                              else
   540                                  Nothing
   541                          )
   542                          model.nodeMsg
   543  
   544                  error =
   545                      if Time.posixToMillis now - Time.posixToMillis model.lastError > 5 * 1000 then
   546                          Nothing
   547  
   548                      else
   549                          model.error
   550              in
   551              ( { model | now = now, nodeMsg = nodeMsg, error = error }
   552              , updateNodes model
   553              )
   554  
   555          ApiRespList resp ->
   556              case resp of
   557                  Data.Success nodes ->
   558                      let
   559                          new =
   560                              nodes
   561                                  |> nodeListToTrees
   562                                  |> List.map (populateHasChildren "")
   563                                  |> sortNodeTrees
   564                                  |> populateFeID
   565                                  |> mergeNodeTrees model.nodes
   566                      in
   567                      ( { model | nodes = new }, Effect.none )
   568  
   569                  Data.Failure err ->
   570                      let
   571                          signOut =
   572                              case err of
   573                                  Http.BadStatus code ->
   574                                      code == 401
   575  
   576                                  _ ->
   577                                      False
   578                      in
   579                      if signOut then
   580                          ( { model | error = Just "Signed Out" }
   581                          , Effect.fromCmd <| Storage.signOut shared.storage
   582                          )
   583  
   584                      else
   585                          ( popError "Error getting nodes" err model
   586                          , Effect.none
   587                          )
   588  
   589                  _ ->
   590                      ( model, Effect.none )
   591  
   592          ApiRespDelete resp ->
   593              case resp of
   594                  Data.Success _ ->
   595                      ( model
   596                      , updateNodes model
   597                      )
   598  
   599                  Data.Failure err ->
   600                      ( popError "Error deleting device" err model
   601                      , updateNodes model
   602                      )
   603  
   604                  _ ->
   605                      ( model
   606                      , updateNodes model
   607                      )
   608  
   609          ApiRespPostPoint resp ->
   610              case resp of
   611                  Data.Success _ ->
   612                      ( model
   613                      , updateNodes model
   614                      )
   615  
   616                  Data.Failure err ->
   617                      ( popError "Error posting point" err model
   618                      , updateNodes model
   619                      )
   620  
   621                  _ ->
   622                      ( model
   623                      , Effect.none
   624                      )
   625  
   626          ApiRespPostAddNode parentFeID resp ->
   627              case resp of
   628                  Data.Success _ ->
   629                      ( { model | nodes = List.map (expChildren parentFeID) model.nodes }
   630                      , updateNodes model
   631                      )
   632  
   633                  Data.Failure err ->
   634                      ( popError "Error adding node" err model
   635                      , updateNodes model
   636                      )
   637  
   638                  _ ->
   639                      ( model
   640                      , updateNodes model
   641                      )
   642  
   643          ApiRespPostMoveNode parent resp ->
   644              case resp of
   645                  Data.Success _ ->
   646                      let
   647                          nodes =
   648                              List.map (expChildren parent) model.nodes
   649                      in
   650                      ( { model | nodeOp = OpNone, copyMove = CopyMoveNone, nodes = nodes }
   651                      , updateNodes model
   652                      )
   653  
   654                  Data.Failure err ->
   655                      ( popError "Error moving node" err model
   656                      , updateNodes model
   657                      )
   658  
   659                  _ ->
   660                      ( model
   661                      , updateNodes model
   662                      )
   663  
   664          ApiRespPutMirrorNode parent resp ->
   665              case resp of
   666                  Data.Success _ ->
   667                      let
   668                          nodes =
   669                              List.map (expChildren parent) model.nodes
   670                      in
   671                      ( { model | nodeOp = OpNone, copyMove = CopyMoveNone, nodes = nodes }
   672                      , updateNodes model
   673                      )
   674  
   675                  Data.Failure err ->
   676                      ( popError "Error mirroring node" err model
   677                      , updateNodes model
   678                      )
   679  
   680                  _ ->
   681                      ( model
   682                      , updateNodes model
   683                      )
   684  
   685          ApiRespPutDuplicateNode parent resp ->
   686              case resp of
   687                  Data.Success _ ->
   688                      let
   689                          nodes =
   690                              List.map (expChildren parent) model.nodes
   691                      in
   692                      ( { model | nodeOp = OpNone, copyMove = CopyMoveNone, nodes = nodes }
   693                      , updateNodes model
   694                      )
   695  
   696                  Data.Failure err ->
   697                      ( popError "Error duplicating node" err model
   698                      , updateNodes model
   699                      )
   700  
   701                  _ ->
   702                      ( model
   703                      , updateNodes model
   704                      )
   705  
   706          ApiRespPostNotificationNode resp ->
   707              case resp of
   708                  Data.Success _ ->
   709                      ( { model | nodeOp = OpNone }
   710                      , updateNodes model
   711                      )
   712  
   713                  Data.Failure err ->
   714                      ( popError "Error messaging node" err model
   715                      , updateNodes model
   716                      )
   717  
   718                  _ ->
   719                      ( model
   720                      , updateNodes model
   721                      )
   722  
   723          CopyNode feID id src desc ->
   724              ( { model
   725                  | copyMove = Copy id src desc
   726                  , nodeMsg =
   727                      Just
   728                          { feID = feID
   729                          , text = "Node copied\nclick paste in destination node"
   730                          , time = model.now
   731                          }
   732                }
   733              , Effect.fromCmd <| Port.out <| Port.encodeClipboard id
   734              )
   735  
   736          ClearClipboard ->
   737              ( { model | copyMove = CopyMoveNone }, Effect.none )
   738  
   739          UpdateNewPointType typ ->
   740              let
   741                  addPoint =
   742                      model.addPoint
   743  
   744                  addPointNew =
   745                      { addPoint | typ = typ }
   746              in
   747              ( { model | addPoint = addPointNew }, Effect.none )
   748  
   749          UpdateNewPointKey key ->
   750              let
   751                  addPoint =
   752                      model.addPoint
   753  
   754                  addPointNew =
   755                      { addPoint | key = key }
   756              in
   757              ( { model | addPoint = addPointNew }, Effect.none )
   758  
   759          UpdateCustomNodeType typ ->
   760              ( { model | customNodeType = typ }, Effect.none )
   761  
   762          ToggleRaw id ->
   763              let
   764                  viewRaw =
   765                      case model.nodeEdit of
   766                          Just ne ->
   767                              if id == ne.feID then
   768                                  not ne.viewRaw
   769  
   770                              else
   771                                  True
   772  
   773                          Nothing ->
   774                              True
   775              in
   776              ( { model
   777                  | nodeEdit =
   778                      if viewRaw then
   779                          Just
   780                              { feID = id
   781                              , points = []
   782                              , viewRaw = True
   783                              }
   784  
   785                      else
   786                          Nothing
   787                }
   788              , Effect.none
   789              )
   790  
   791  
   792  mergeNodeTrees : List (Tree NodeView) -> List (Tree NodeView) -> List (Tree NodeView)
   793  mergeNodeTrees current new =
   794      List.map
   795          (\n ->
   796              let
   797                  newRootNode =
   798                      Tree.label n
   799              in
   800              case
   801                  List.Extra.find
   802                      (\c ->
   803                          let
   804                              curRootNode =
   805                                  Tree.label c
   806                          in
   807                          newRootNode.node.id == curRootNode.node.id && newRootNode.node.parent == curRootNode.node.parent
   808                      )
   809                      current
   810              of
   811                  Just cur ->
   812                      mergeNodeTree cur n
   813  
   814                  Nothing ->
   815                      n
   816          )
   817          new
   818  
   819  
   820  mergeNodeTree : Tree NodeView -> Tree NodeView -> Tree NodeView
   821  mergeNodeTree current new =
   822      let
   823          z =
   824              Zipper.fromTree current
   825      in
   826      Tree.map
   827          (\n ->
   828              case
   829                  Zipper.findFromRoot
   830                      (\o ->
   831                          o.node.id
   832                              == n.node.id
   833                              && o.parentID
   834                              == n.parentID
   835                      )
   836                      z
   837              of
   838                  Just found ->
   839                      let
   840                          l =
   841                              Zipper.label found
   842                      in
   843                      { n
   844                          | expChildren = l.expChildren
   845                          , expDetail = l.expDetail
   846                      }
   847  
   848                  Nothing ->
   849                      n
   850          )
   851          new
   852  
   853  
   854  
   855  -- FeID stands for front-end ID. This is required because we may
   856  -- have some duplicate nodes in the data set, so we simply give each
   857  -- one a unique ID while we are working with them in the frontend
   858  
   859  
   860  populateFeID : List (Tree NodeView) -> List (Tree NodeView)
   861  populateFeID trees =
   862      List.indexedMap
   863          (\i nodes ->
   864              Tree.indexedMap
   865                  (\j n ->
   866                      { n | feID = i * 10000 + j }
   867                  )
   868                  nodes
   869          )
   870          trees
   871  
   872  
   873  toggleExpChildren : List (Tree NodeView) -> Int -> List (Tree NodeView)
   874  toggleExpChildren nodes feID =
   875      List.map
   876          (Tree.map
   877              (\n ->
   878                  if n.feID == feID then
   879                      { n | expChildren = not n.expChildren }
   880  
   881                  else
   882                      n
   883              )
   884          )
   885          nodes
   886  
   887  
   888  expChildren : Int -> Tree NodeView -> Tree NodeView
   889  expChildren feID tree =
   890      Tree.map
   891          (\n ->
   892              if n.feID == feID then
   893                  { n | expChildren = True }
   894  
   895              else
   896                  n
   897          )
   898          tree
   899  
   900  
   901  toggleExpDetail : List (Tree NodeView) -> Int -> List (Tree NodeView)
   902  toggleExpDetail nodes feID =
   903      List.map
   904          (Tree.map
   905              (\n ->
   906                  if n.feID == feID then
   907                      { n | expDetail = not n.expDetail }
   908  
   909                  else
   910                      n
   911              )
   912          )
   913          nodes
   914  
   915  
   916  nodeListToTrees : List Node -> List (Tree NodeView)
   917  nodeListToTrees nodes =
   918      List.foldr
   919          (\n ret ->
   920              if n.parent == "root" then
   921                  populateChildren nodes n :: ret
   922  
   923              else
   924                  ret
   925          )
   926          []
   927          nodes
   928  
   929  
   930  populateChildren : List Node -> Node -> Tree NodeView
   931  populateChildren nodes root =
   932      Tree.replaceChildren (List.map (populateChildren nodes) (getChildren nodes root))
   933          (Tree.singleton <| nodeToNodeView root)
   934  
   935  
   936  getChildren : List Node -> Node -> List Node
   937  getChildren nodes parent =
   938      List.foldr
   939          (\n acc ->
   940              if n.parent == parent.id then
   941                  n :: acc
   942  
   943              else
   944                  acc
   945          )
   946          []
   947          nodes
   948  
   949  
   950  nodeToNodeView : Node -> NodeView
   951  nodeToNodeView node =
   952      { node = node
   953      , feID = 0
   954      , parentID = ""
   955      , hasChildren = False
   956      , expDetail = False
   957      , expChildren = False
   958      , mod = False
   959      }
   960  
   961  
   962  populateHasChildren : String -> Tree NodeView -> Tree NodeView
   963  populateHasChildren parentID tree =
   964      let
   965          children =
   966              Tree.children tree
   967  
   968          hasChildren =
   969              List.foldr
   970                  (\child count ->
   971                      let
   972                          tombstone =
   973                              isTombstone (Tree.label child).node
   974                      in
   975                      if tombstone then
   976                          count
   977  
   978                      else
   979                          count + 1
   980                  )
   981                  0
   982                  children
   983                  > 0
   984  
   985          label =
   986              Tree.label tree
   987  
   988          node =
   989              { label
   990                  | hasChildren = hasChildren
   991                  , parentID = parentID
   992              }
   993      in
   994      tree
   995          |> Tree.replaceLabel node
   996          |> Tree.replaceChildren
   997              (List.map
   998                  (\c -> populateHasChildren node.node.id c)
   999                  children
  1000              )
  1001  
  1002  
  1003  sortNodeTrees : List (Tree NodeView) -> List (Tree NodeView)
  1004  sortNodeTrees trees =
  1005      List.sortWith nodeSort trees |> List.map sortNodeTree
  1006  
  1007  
  1008  
  1009  -- sortNodeTree recursively sorts the children of the nodes
  1010  -- sort by type and then description
  1011  
  1012  
  1013  sortNodeTree : Tree NodeView -> Tree NodeView
  1014  sortNodeTree nodes =
  1015      let
  1016          children =
  1017              Tree.children nodes
  1018  
  1019          childrenSorted =
  1020              List.sortWith nodeSort children
  1021      in
  1022      Tree.tree (Tree.label nodes) (List.map sortNodeTree childrenSorted)
  1023  
  1024  
  1025  
  1026  -- nodeCustomSortRules struct determines how we sort nodes in the UI
  1027  
  1028  
  1029  nodeCustomSortRules : Dict.Dict String String
  1030  nodeCustomSortRules =
  1031      Dict.fromList
  1032          [ ( Node.typeDevice, "A" )
  1033          , ( Node.typeUser, "B" )
  1034          , ( Node.typeGroup, "C" )
  1035          , ( Node.typeModbus, "D" )
  1036          , ( Node.typeRule, "E" )
  1037          , ( Node.typeSignalGenerator, "F" )
  1038          , ( Node.typeOneWire, "G" )
  1039          , ( Node.typeCanBus, "H" )
  1040          , ( Node.typeSerialDev, "I" )
  1041          , ( Node.typeMsgService, "J" )
  1042          , ( Node.typeFile, "K" )
  1043          , ( Node.typeVariable, "L" )
  1044          , ( Node.typeDb, "M" )
  1045          , ( Node.typeMetrics, "N" )
  1046          , ( Node.typeParticle, "O" )
  1047          , ( Node.typeShelly, "P" )
  1048          , ( Node.typeShellyIO, "Q" )
  1049          , ( Node.typeNetworkManager, "R" )
  1050          , ( Node.typeNTP, "S" )
  1051          , ( Node.typeUpdate, "T" )
  1052  
  1053          -- rule subnodes
  1054          , ( Node.typeCondition, "A" )
  1055          , ( Node.typeAction, "B" )
  1056          , ( Node.typeActionInactive, "C" )
  1057          , ( Node.typeNetworkManagerDevice, "D" )
  1058          , ( Node.typeNetworkManagerConn, "E" )
  1059          ]
  1060  
  1061  
  1062  nodeCustomSort : String -> String
  1063  nodeCustomSort t =
  1064      case Dict.get t nodeCustomSortRules of
  1065          Just s ->
  1066              s
  1067  
  1068          Nothing ->
  1069              t
  1070  
  1071  
  1072  nodeSort : Tree NodeView -> Tree NodeView -> Order
  1073  nodeSort a b =
  1074      let
  1075          aNode =
  1076              Tree.label a
  1077  
  1078          bNode =
  1079              Tree.label b
  1080  
  1081          aType =
  1082              nodeCustomSort aNode.node.typ
  1083  
  1084          bType =
  1085              nodeCustomSort bNode.node.typ
  1086      in
  1087      if aType /= bType then
  1088          compare aType bType
  1089  
  1090      else
  1091          let
  1092              aDesc =
  1093                  String.toLower <| Point.getBestDesc aNode.node.points
  1094  
  1095              bDesc =
  1096                  String.toLower <| Point.getBestDesc bNode.node.points
  1097          in
  1098          if aDesc /= bDesc then
  1099              compare aDesc bDesc
  1100  
  1101          else
  1102              let
  1103                  aIndex =
  1104                      Point.getValue aNode.node.points Point.typeIndex ""
  1105  
  1106                  bIndex =
  1107                      Point.getValue bNode.node.points Point.typeIndex ""
  1108              in
  1109              if aIndex /= bIndex then
  1110                  compare aIndex bIndex
  1111  
  1112              else
  1113                  let
  1114                      aID =
  1115                          Point.getText aNode.node.points Point.typeID ""
  1116  
  1117                      bID =
  1118                          Point.getText bNode.node.points Point.typeID ""
  1119                  in
  1120                  compare aID bID
  1121  
  1122  
  1123  popError : String -> Http.Error -> Model -> Model
  1124  popError desc err model =
  1125      { model | error = Just (desc ++ ": " ++ Data.errorToString err), lastError = model.now }
  1126  
  1127  
  1128  updateNodes : Model -> Effect Msg
  1129  updateNodes model =
  1130      Effect.fromCmd <| Node.list { onResponse = ApiRespList, token = model.token }
  1131  
  1132  
  1133  subscriptions : Model -> Sub Msg
  1134  subscriptions _ =
  1135      Time.every 4000 Tick
  1136  
  1137  
  1138  
  1139  -- VIEW
  1140  
  1141  
  1142  view : Auth.User -> Shared.Model -> Model -> View Msg
  1143  view _ shared model =
  1144      { title = "SIOT"
  1145      , attributes = []
  1146      , element =
  1147          UI.Layout.layout
  1148              { onSignOut = SignOut
  1149              , email = Maybe.map .email shared.storage.user
  1150              , error = model.error
  1151              }
  1152              (viewBody model)
  1153      }
  1154  
  1155  
  1156  viewBody : Model -> Element Msg
  1157  viewBody model =
  1158      column
  1159          [ width fill, spacing 32 ]
  1160          [ wrappedRow [ spacing 10 ] <|
  1161              (el Style.h2 <| text "Nodes")
  1162                  :: (case model.copyMove of
  1163                          CopyMoveNone ->
  1164                              []
  1165  
  1166                          Copy id _ desc ->
  1167                              [ Icon.clipboard
  1168                              , el [ Font.italic ] <| text desc
  1169                              , el [ Font.size 12 ] <| text <| "(" ++ id ++ ")"
  1170                              , Button.x ClearClipboard
  1171                              ]
  1172                     )
  1173          , viewNodes model
  1174          ]
  1175  
  1176  
  1177  viewNodes : Model -> Element Msg
  1178  viewNodes model =
  1179      column
  1180          [ width fill
  1181          , spacing 24
  1182          ]
  1183      <|
  1184          let
  1185              treeWithEdits =
  1186                  mergeNodesEdit model.nodes model.nodeEdit
  1187          in
  1188          List.concatMap
  1189              (\t ->
  1190                  viewNode model Nothing (Tree.label t) [] 0
  1191                      :: viewNodesHelp 1 model t
  1192              )
  1193              treeWithEdits
  1194  
  1195  
  1196  viewNodesHelp :
  1197      Int
  1198      -> Model
  1199      -> Tree NodeView
  1200      -> List (Element Msg)
  1201  viewNodesHelp depth model tree =
  1202      let
  1203          node =
  1204              Tree.label tree
  1205  
  1206          children =
  1207              if node.expChildren then
  1208                  Tree.children tree
  1209  
  1210              else
  1211                  []
  1212      in
  1213      List.foldl
  1214          (\child ret ->
  1215              let
  1216                  childNode =
  1217                      Tree.label child
  1218  
  1219                  tombstone =
  1220                      isTombstone childNode.node
  1221              in
  1222              if not tombstone then
  1223                  let
  1224                      viewChildren =
  1225                          List.map Tree.label
  1226                              (Tree.children child)
  1227                  in
  1228                  ret
  1229                      ++ viewNode model (Just node) childNode viewChildren depth
  1230                      :: viewNodesHelp (depth + 1) model child
  1231  
  1232              else
  1233                  ret
  1234          )
  1235          []
  1236          children
  1237  
  1238  
  1239  isTombstone : Node -> Bool
  1240  isTombstone node =
  1241      Point.getBool node.edgePoints Point.typeTombstone ""
  1242  
  1243  
  1244  viewNode : Model -> Maybe NodeView -> NodeView -> List NodeView -> Int -> Element Msg
  1245  viewNode model parent node children depth =
  1246      let
  1247          viewRaw =
  1248              case model.nodeEdit of
  1249                  Just ne ->
  1250                      ne.feID == node.feID && ne.viewRaw
  1251  
  1252                  Nothing ->
  1253                      False
  1254  
  1255          nodeView =
  1256              if viewRaw then
  1257                  NodeRaw.view
  1258  
  1259              else
  1260                  case node.node.typ of
  1261                      "user" ->
  1262                          NodeUser.view
  1263  
  1264                      "group" ->
  1265                          NodeGroup.view
  1266  
  1267                      "modbus" ->
  1268                          NodeModbus.view
  1269  
  1270                      "modbusIo" ->
  1271                          NodeModbusIO.view
  1272  
  1273                      "oneWire" ->
  1274                          NodeOneWire.view
  1275  
  1276                      "oneWireIO" ->
  1277                          NodeOneWireIO.view
  1278  
  1279                      "serialDev" ->
  1280                          NodeSerialDev.view
  1281  
  1282                      "canBus" ->
  1283                          NodeCanBus.view
  1284  
  1285                      "rule" ->
  1286                          NodeRule.view
  1287  
  1288                      "condition" ->
  1289                          NodeCondition.view
  1290  
  1291                      "action" ->
  1292                          NodeAction.view
  1293  
  1294                      "actionInactive" ->
  1295                          NodeAction.view
  1296  
  1297                      "device" ->
  1298                          NodeDevice.view
  1299  
  1300                      "msgService" ->
  1301                          NodeMessageService.view
  1302  
  1303                      "variable" ->
  1304                          NodeVariable.view
  1305  
  1306                      "signalGenerator" ->
  1307                          SignalGenerator.view
  1308  
  1309                      "file" ->
  1310                          File.view
  1311  
  1312                      "sync" ->
  1313                          NodeSync.view
  1314  
  1315                      "db" ->
  1316                          NodeDb.view
  1317  
  1318                      "particle" ->
  1319                          NodeParticle.view
  1320  
  1321                      "shelly" ->
  1322                          NodeShelly.view
  1323  
  1324                      "shellyIo" ->
  1325                          NodeShellyIO.view
  1326  
  1327                      "metrics" ->
  1328                          NodeMetrics.view
  1329  
  1330                      "networkManager" ->
  1331                          NodeNetworkManager.view
  1332  
  1333                      "ntp" ->
  1334                          NodeNTP.view
  1335  
  1336                      "networkManagerDevice" ->
  1337                          NodeNetworkManagerDevice.view
  1338  
  1339                      "networkManagerConn" ->
  1340                          NodeNetworkManagerConn.view
  1341  
  1342                      "update" ->
  1343                          NodeUpdate.view
  1344  
  1345                      _ ->
  1346                          NodeRaw.view
  1347  
  1348          background =
  1349              if node.expDetail then
  1350                  Style.colors.pale
  1351  
  1352              else
  1353                  Style.colors.none
  1354  
  1355          alignButton =
  1356              el [ alignTop, paddingEach { top = 10, right = 0, left = 0, bottom = 0 } ]
  1357      in
  1358      el
  1359          [ width fill
  1360          , paddingEach { top = 0, right = 0, bottom = 0, left = depth * 35 }
  1361          , Form.onEnterEsc (ApiPostPoints node.node.id) DiscardNodeOp
  1362          ]
  1363      <|
  1364          row [ spacing 6 ]
  1365              [ alignButton <|
  1366                  if not node.hasChildren then
  1367                      Icon.blank
  1368  
  1369                  else if node.expChildren then
  1370                      Button.arrowDown (ToggleExpChildren node.feID)
  1371  
  1372                  else
  1373                      Button.arrowRight (ToggleExpChildren node.feID)
  1374              , alignButton <|
  1375                  Button.dot (ToggleExpDetail node.feID)
  1376              , column
  1377                  [ spacing 6, padding 6, width fill, Background.color background ]
  1378                  [ nodeView
  1379                      { now = model.now
  1380                      , zone = model.zone
  1381                      , modified = node.mod
  1382                      , parent = Maybe.map .node parent
  1383                      , node = node.node
  1384                      , children = children
  1385                      , nodes = model.nodes
  1386                      , expDetail = node.expDetail
  1387                      , onEditNodePoint = EditNodePoint node.feID
  1388                      , onUploadFile = UploadFile node.node.id
  1389                      , copy = model.copyMove
  1390                      , scratch = model.scratch
  1391                      , onEditScratch = EditScratch
  1392                      }
  1393                  , viewIf viewRaw <|
  1394                      column [ spacing 10 ]
  1395                          [ Input.text []
  1396                              { onChange = UpdateNewPointType
  1397                              , text = model.addPoint.typ
  1398                              , placeholder = Nothing
  1399                              , label = Input.labelLeft [] <| text "New point type:"
  1400                              }
  1401                          , Input.text []
  1402                              { onChange = UpdateNewPointKey
  1403                              , text = model.addPoint.key
  1404                              , placeholder = Nothing
  1405                              , label = Input.labelLeft [] <| text "New point key:"
  1406                              }
  1407                          ]
  1408                  , viewIf node.mod <|
  1409                      Form.buttonRow <|
  1410                          [ Form.button
  1411                              { label = "save"
  1412                              , color = colors.blue
  1413                              , onPress = ApiPostPoints node.node.id
  1414                              }
  1415                          , Form.button
  1416                              { label = "discard"
  1417                              , color = colors.gray
  1418                              , onPress = DiscardEdits
  1419                              }
  1420                          ]
  1421                              ++ (if viewRaw then
  1422                                      [ Form.button
  1423                                          { label = "add point"
  1424                                          , color = colors.darkgreen
  1425                                          , onPress =
  1426                                              let
  1427                                                  key =
  1428                                                      if model.addPoint.key == "" then
  1429                                                          "0"
  1430  
  1431                                                      else
  1432                                                          model.addPoint.key
  1433                                              in
  1434                                              EditNodePoint node.feID
  1435                                                  [ Point model.addPoint.typ
  1436                                                      key
  1437                                                      model.now
  1438                                                      0
  1439                                                      ""
  1440                                                      0
  1441                                                  ]
  1442                                          }
  1443                                      ]
  1444  
  1445                                  else
  1446                                      []
  1447                                 )
  1448                  , if node.expDetail then
  1449                      let
  1450                          viewNodeOps =
  1451                              viewNodeOperations node msg
  1452  
  1453                          msg =
  1454                              Maybe.andThen
  1455                                  (\m ->
  1456                                      if m.feID == node.feID then
  1457                                          Just m.text
  1458  
  1459                                      else
  1460                                          Nothing
  1461                                  )
  1462                                  model.nodeMsg
  1463                      in
  1464                      case model.nodeOp of
  1465                          OpNone ->
  1466                              viewNodeOps
  1467  
  1468                          OpNodeToAdd add ->
  1469                              if add.feID == node.feID then
  1470                                  viewAddNode model.customNodeType node add
  1471  
  1472                              else
  1473                                  viewNodeOps
  1474  
  1475                          OpNodeMessage m ->
  1476                              if m.feID == node.feID then
  1477                                  viewMsgNode m
  1478  
  1479                              else
  1480                                  viewNodeOps
  1481  
  1482                          OpNodeDelete feID id parentId ->
  1483                              if feID == node.feID then
  1484                                  viewDeleteNode id parentId
  1485  
  1486                              else
  1487                                  viewNodeOps
  1488  
  1489                          OpNodePaste feID id ->
  1490                              if feID == node.feID then
  1491                                  viewPasteNode feID id model.copyMove
  1492  
  1493                              else
  1494                                  viewNodeOps
  1495  
  1496                    else
  1497                      Element.none
  1498                  ]
  1499              ]
  1500  
  1501  
  1502  nodeTypesThatHaveChildNodes : List String
  1503  nodeTypesThatHaveChildNodes =
  1504      [ Node.typeDevice
  1505      , Node.typeGroup
  1506      , Node.typeModbus
  1507      , Node.typeOneWire
  1508      , Node.typeSerialDev
  1509      , Node.typeCanBus
  1510      , Node.typeRule
  1511      , Node.typeNetworkManager
  1512      ]
  1513  
  1514  
  1515  viewNodeOperations : NodeView -> Maybe String -> Element Msg
  1516  viewNodeOperations node msg =
  1517      let
  1518          desc =
  1519              Point.getBestDesc node.node.points
  1520  
  1521          showNodeAdd =
  1522              List.member node.node.typ
  1523                  nodeTypesThatHaveChildNodes
  1524      in
  1525      column [ spacing 6 ]
  1526          [ row [ spacing 6 ]
  1527              [ viewIf showNodeAdd <|
  1528                  Button.plusCircle (AddNode node.feID node.node.id)
  1529              , Button.message (MsgNode node.feID node.node.id node.node.parent)
  1530              , Button.x (DeleteNode node.feID node.node.id node.node.parent)
  1531              , Button.copy (CopyNode node.feID node.node.id node.node.parent desc)
  1532              , Button.clipboard (PasteNode node.feID node.node.id)
  1533              , Button.list (ToggleRaw node.feID)
  1534              ]
  1535          , case msg of
  1536              Just m ->
  1537                  text m
  1538  
  1539              Nothing ->
  1540                  Element.none
  1541          ]
  1542  
  1543  
  1544  nodeDescUser : Element Msg
  1545  nodeDescUser =
  1546      row [] [ Icon.user, text "User" ]
  1547  
  1548  
  1549  nodeDescGroup : Element Msg
  1550  nodeDescGroup =
  1551      row [] [ Icon.users, text "Group" ]
  1552  
  1553  
  1554  nodeDescModbus : Element Msg
  1555  nodeDescModbus =
  1556      row [] [ Icon.bus, text "Modbus" ]
  1557  
  1558  
  1559  nodeDescModbusIO : Element Msg
  1560  nodeDescModbusIO =
  1561      row [] [ Icon.io, text "Modbus IO" ]
  1562  
  1563  
  1564  nodeDescSerialDev : Element Msg
  1565  nodeDescSerialDev =
  1566      row [] [ Icon.serialDev, text "Serial Device" ]
  1567  
  1568  
  1569  nodeDescCanBus : Element Msg
  1570  nodeDescCanBus =
  1571      row [] [ Icon.serialDev, text "CAN Bus" ]
  1572  
  1573  
  1574  nodeDescRule : Element Msg
  1575  nodeDescRule =
  1576      row [] [ Icon.list, text "Rule" ]
  1577  
  1578  
  1579  nodeDescMsgService : Element Msg
  1580  nodeDescMsgService =
  1581      row [] [ Icon.send, text "Messaging Service" ]
  1582  
  1583  
  1584  nodeDescDb : Element Msg
  1585  nodeDescDb =
  1586      row [] [ Icon.database, text "Database" ]
  1587  
  1588  
  1589  nodeDescParticle : Element Msg
  1590  nodeDescParticle =
  1591      row [] [ Icon.particle, text "Particle" ]
  1592  
  1593  
  1594  nodeDescShelly : Element Msg
  1595  nodeDescShelly =
  1596      row [] [ Icon.shelly, text "Shelly" ]
  1597  
  1598  
  1599  nodeDescVariable : Element Msg
  1600  nodeDescVariable =
  1601      row [] [ Icon.variable, text "Variable" ]
  1602  
  1603  
  1604  nodeDescSignalGenerator : Element Msg
  1605  nodeDescSignalGenerator =
  1606      row [] [ Icon.activity, text "Signal Generator" ]
  1607  
  1608  
  1609  nodeDescFile : Element Msg
  1610  nodeDescFile =
  1611      row [] [ Icon.file, text "File" ]
  1612  
  1613  
  1614  nodeDescSync : Element Msg
  1615  nodeDescSync =
  1616      row [] [ Icon.sync, text "sync" ]
  1617  
  1618  
  1619  nodeDescCondition : Element Msg
  1620  nodeDescCondition =
  1621      row [] [ Icon.check, text "Condition" ]
  1622  
  1623  
  1624  nodeDescAction : Element Msg
  1625  nodeDescAction =
  1626      row [] [ Icon.trendingUp, text "Action (rule active)" ]
  1627  
  1628  
  1629  nodeDescActionInactive : Element Msg
  1630  nodeDescActionInactive =
  1631      row [] [ Icon.trendingDown, text "Action (rule inactive)" ]
  1632  
  1633  
  1634  nodeDescMetrics : Element Msg
  1635  nodeDescMetrics =
  1636      row [] [ Icon.barChart, text "Metrics" ]
  1637  
  1638  
  1639  nodeDescUpdate : Element Msg
  1640  nodeDescUpdate =
  1641      row [] [ Icon.update, text "Update" ]
  1642  
  1643  
  1644  nodeDescNetworkManager : Element Msg
  1645  nodeDescNetworkManager =
  1646      row [] [ Icon.network, text "Network Manager" ]
  1647  
  1648  
  1649  nodeDescNetworkManagerConn : Element Msg
  1650  nodeDescNetworkManagerConn =
  1651      row [] [ Icon.cable, text "Connection" ]
  1652  
  1653  
  1654  nodeDescNTP : Element Msg
  1655  nodeDescNTP =
  1656      row [] [ Icon.clock, text "NTP" ]
  1657  
  1658  
  1659  viewAddNode : String -> NodeView -> NodeToAdd -> Element Msg
  1660  viewAddNode customNodeType parent add =
  1661      column [ spacing 10 ]
  1662          [ Input.radio [ spacing 6 ]
  1663              { onChange = SelectAddNodeType
  1664              , selected = add.typ
  1665              , label = Input.labelAbove [] (el [ padding 12 ] <| text "Select node type to add: ")
  1666              , options =
  1667                  (if parent.node.typ == Node.typeDevice then
  1668                      [ Input.option Node.typeUser nodeDescUser
  1669                      , Input.option Node.typeGroup nodeDescGroup
  1670                      , Input.option Node.typeRule nodeDescRule
  1671                      , Input.option Node.typeNetworkManager nodeDescNetworkManager
  1672                      , Input.option Node.typeNTP nodeDescNTP
  1673                      , Input.option Node.typeModbus nodeDescModbus
  1674                      , Input.option Node.typeSerialDev nodeDescSerialDev
  1675                      , Input.option Node.typeCanBus nodeDescCanBus
  1676                      , Input.option Node.typeMsgService nodeDescMsgService
  1677                      , Input.option Node.typeDb nodeDescDb
  1678                      , Input.option Node.typeParticle nodeDescParticle
  1679                      , Input.option Node.typeShelly nodeDescShelly
  1680                      , Input.option Node.typeVariable nodeDescVariable
  1681                      , Input.option Node.typeSignalGenerator nodeDescSignalGenerator
  1682                      , Input.option Node.typeFile nodeDescFile
  1683                      , Input.option Node.typeSync nodeDescSync
  1684                      , Input.option Node.typeMetrics nodeDescMetrics
  1685                      , Input.option Node.typeUpdate nodeDescUpdate
  1686                      ]
  1687  
  1688                   else
  1689                      []
  1690                  )
  1691                      ++ (if parent.node.typ == Node.typeGroup then
  1692                              [ Input.option Node.typeUser nodeDescUser
  1693                              , Input.option Node.typeGroup nodeDescGroup
  1694                              , Input.option Node.typeRule nodeDescRule
  1695                              , Input.option Node.typeModbus nodeDescModbus
  1696                              , Input.option Node.typeSerialDev nodeDescSerialDev
  1697                              , Input.option Node.typeCanBus nodeDescCanBus
  1698                              , Input.option Node.typeMsgService nodeDescMsgService
  1699                              , Input.option Node.typeDb nodeDescDb
  1700                              , Input.option Node.typeParticle nodeDescParticle
  1701                              , Input.option Node.typeShelly nodeDescShelly
  1702                              , Input.option Node.typeVariable nodeDescVariable
  1703                              , Input.option Node.typeSignalGenerator nodeDescSignalGenerator
  1704                              , Input.option Node.typeFile nodeDescFile
  1705                              ]
  1706  
  1707                          else
  1708                              []
  1709                         )
  1710                      ++ (if parent.node.typ == Node.typeModbus then
  1711                              [ Input.option Node.typeModbusIO nodeDescModbusIO ]
  1712  
  1713                          else
  1714                              []
  1715                         )
  1716                      ++ (if parent.node.typ == Node.typeRule then
  1717                              [ Input.option Node.typeCondition nodeDescCondition
  1718                              , Input.option Node.typeAction nodeDescAction
  1719                              , Input.option Node.typeActionInactive nodeDescActionInactive
  1720                              ]
  1721  
  1722                          else
  1723                              []
  1724                         )
  1725                      ++ (if parent.node.typ == Node.typeCanBus then
  1726                              [ Input.option Node.typeFile nodeDescFile ]
  1727  
  1728                          else
  1729                              []
  1730                         )
  1731                      ++ (if parent.node.typ == Node.typeNetworkManager then
  1732                              [ Input.option Node.typeNetworkManagerConn nodeDescNetworkManagerConn ]
  1733  
  1734                          else
  1735                              []
  1736                         )
  1737                      ++ (if parent.node.typ == Node.typeSerialDev then
  1738                              [ Input.option Node.typeFile nodeDescFile ]
  1739  
  1740                          else
  1741                              []
  1742                         )
  1743                      ++ [ Input.option "custom" <| text "Custom" ]
  1744              }
  1745          , viewIf (add.typ == Just "custom") <|
  1746              Input.text
  1747                  []
  1748                  { onChange = UpdateCustomNodeType
  1749                  , text = customNodeType
  1750                  , placeholder = Nothing
  1751                  , label = Input.labelLeft [] <| text "Custom node type:"
  1752                  }
  1753          , Form.buttonRow
  1754              [ case add.typ of
  1755                  Just _ ->
  1756                      Form.button
  1757                          { label = "add"
  1758                          , color = Style.colors.blue
  1759                          , onPress = ApiPostAddNode parent.feID
  1760                          }
  1761  
  1762                  Nothing ->
  1763                      Element.none
  1764              , Form.button
  1765                  { label = "cancel"
  1766                  , color = Style.colors.gray
  1767                  , onPress = DiscardNodeOp
  1768                  }
  1769              ]
  1770          ]
  1771  
  1772  
  1773  viewMsgNode : NodeMessage -> Element Msg
  1774  viewMsgNode msg =
  1775      el [ width fill, paddingEach { top = 10, right = 0, left = 0, bottom = 0 } ] <|
  1776          column
  1777              [ width fill, spacing 32 ]
  1778              [ Input.multiline [ width fill ]
  1779                  { onChange = UpdateMsg
  1780                  , text = msg.message
  1781                  , placeholder = Nothing
  1782                  , label = Input.labelAbove [] <| text "Send message to users:"
  1783                  , spellcheck = True
  1784                  }
  1785              , Form.buttonRow
  1786                  [ Form.button
  1787                      { label = "send now"
  1788                      , color = Style.colors.blue
  1789                      , onPress = ApiPostNotificationNode
  1790                      }
  1791                  , Form.button
  1792                      { label = "cancel"
  1793                      , color = Style.colors.gray
  1794                      , onPress = DiscardNodeOp
  1795                      }
  1796                  ]
  1797              ]
  1798  
  1799  
  1800  viewDeleteNode : String -> String -> Element Msg
  1801  viewDeleteNode id parent =
  1802      el [ paddingEach { top = 10, right = 0, left = 0, bottom = 0 } ] <|
  1803          row []
  1804              [ text "Delete this node?"
  1805              , Form.buttonRow
  1806                  [ Form.button
  1807                      { label = "yes"
  1808                      , color = colors.red
  1809                      , onPress = ApiDelete id parent
  1810                      }
  1811                  , Form.button
  1812                      { label = "no"
  1813                      , color = colors.gray
  1814                      , onPress = DiscardNodeOp
  1815                      }
  1816                  ]
  1817              ]
  1818  
  1819  
  1820  viewPasteNode : Int -> String -> CopyMove -> Element Msg
  1821  viewPasteNode feID dest copyMove =
  1822      let
  1823          cancelButton =
  1824              Form.buttonRow
  1825                  [ Form.button
  1826                      { label = "cancel"
  1827                      , color = colors.gray
  1828                      , onPress = DiscardNodeOp
  1829                      }
  1830                  ]
  1831  
  1832          moveButton op =
  1833              Form.button
  1834                  { label = "move"
  1835                  , color = colors.darkgreen
  1836                  , onPress = op
  1837                  }
  1838  
  1839          mirrorButton op =
  1840              Form.button
  1841                  { label = "mirror"
  1842                  , color = colors.blue
  1843                  , onPress = op
  1844                  }
  1845  
  1846          duplicateButton op =
  1847              Form.button
  1848                  { label = "duplicate"
  1849                  , color = colors.red
  1850                  , onPress = op
  1851                  }
  1852      in
  1853      el [ paddingEach { top = 10, right = 0, left = 0, bottom = 0 } ] <|
  1854          case copyMove of
  1855              CopyMoveNone ->
  1856                  row []
  1857                      [ text "Select node to copy/move first"
  1858                      , cancelButton
  1859                      ]
  1860  
  1861              Copy id src desc ->
  1862                  row [] <|
  1863                      if id == dest then
  1864                          [ text "Can't move/copy node to itself"
  1865                          , cancelButton
  1866                          ]
  1867  
  1868                      else if src == dest then
  1869                          [ text <| "Copy " ++ desc ++ " here?"
  1870                          , Form.buttonRow
  1871                              [ duplicateButton <| ApiPutDuplicateNode feID id dest
  1872                              , cancelButton
  1873                              ]
  1874                          ]
  1875  
  1876                      else
  1877                          [ text <| "Copy " ++ desc ++ " here?"
  1878                          , Form.buttonRow
  1879                              [ moveButton <| ApiPostMoveNode feID id src dest
  1880                              , mirrorButton <| ApiPutMirrorNode feID id dest
  1881                              , duplicateButton <| ApiPutDuplicateNode feID id dest
  1882                              , cancelButton
  1883                              ]
  1884                          ]
  1885  
  1886  
  1887  mergeNodesEdit : List (Tree NodeView) -> Maybe NodeEdit -> List (Tree NodeView)
  1888  mergeNodesEdit nodes nodeEdit =
  1889      case nodeEdit of
  1890          Just edit ->
  1891              List.map
  1892                  (Tree.map
  1893                      (\n ->
  1894                          if edit.feID == n.feID then
  1895                              let
  1896                                  node =
  1897                                      n.node
  1898                              in
  1899                              { n
  1900                                  | mod = True
  1901                                  , node =
  1902                                      { node
  1903                                          | points =
  1904                                              Point.updatePoints node.points edit.points
  1905                                      }
  1906                              }
  1907  
  1908                          else
  1909                              { n | mod = False }
  1910                      )
  1911                  )
  1912                  nodes
  1913  
  1914          Nothing ->
  1915              List.map (Tree.map (\n -> { n | mod = False })) nodes