github.com/simpleiot/simpleiot@v0.18.3/frontend/src/UI/NodeInputs.elm (about)

     1  module UI.NodeInputs exposing
     2      ( NodeInputOptions
     3      , nodeButtonActionText
     4      , nodeCheckboxInput
     5      , nodeCounterWithReset
     6      , nodeKeyValueInput
     7      , nodeListInput
     8      , nodeNumberInput
     9      , nodeOnOffInput
    10      , nodeOptionInput
    11      , nodePasteButton
    12      , nodeTextInput
    13      , nodeTimeDateInput
    14      )
    15  
    16  import Api.Node exposing (Node)
    17  import Api.Point as Point exposing (Point)
    18  import Color
    19  import Element exposing (..)
    20  import Element.Border as Border
    21  import Element.Font as Font
    22  import Element.Input as Input
    23  import List.Extra
    24  import Round
    25  import Svg as S
    26  import Svg.Attributes as Sa
    27  import Time
    28  import Time.Extra
    29  import UI.Button
    30  import UI.Form as Form
    31  import UI.Sanitize as Sanitize
    32  import UI.Style as Style
    33  import UI.ViewIf exposing (viewIf)
    34  import Utils.Time exposing (scheduleToLocal, scheduleToUTC)
    35  
    36  
    37  type alias NodeInputOptions msg =
    38      { onEditNodePoint : List Point -> msg
    39      , onEditScratch : String -> msg
    40      , node : Node
    41      , now : Time.Posix
    42      , zone : Time.Zone
    43      , labelWidth : Int
    44      , scratch : String
    45      }
    46  
    47  
    48  nodeTextInput :
    49      NodeInputOptions msg
    50      -> String
    51      -> String
    52      -> String
    53      -> String
    54      -> Element msg
    55  nodeTextInput o key typ lbl placeholder =
    56      let
    57          textRaw =
    58              Point.getText o.node.points typ key
    59      in
    60      Input.text
    61          []
    62          { onChange =
    63              \d ->
    64                  o.onEditNodePoint [ Point typ key o.now 0 d 0 ]
    65          , text =
    66              if textRaw == "123BLANK123" then
    67                  ""
    68  
    69              else
    70                  let
    71                      v =
    72                          Point.getValue o.node.points typ key
    73                  in
    74                  if v /= 0 then
    75                      ""
    76  
    77                  else
    78                      textRaw
    79          , placeholder = Just <| Input.placeholder [] <| text placeholder
    80          , label =
    81              if lbl == "" then
    82                  Input.labelHidden ""
    83  
    84              else
    85                  Input.labelLeft [ width (px o.labelWidth) ] <| el [ alignRight ] <| text <| lbl ++ ":"
    86          }
    87  
    88  
    89  nodeTimeDateInput : NodeInputOptions msg -> Int -> Element msg
    90  nodeTimeDateInput o labelWidth =
    91      let
    92          zoneOffset =
    93              Time.Extra.toOffset o.zone o.now
    94  
    95          sModel =
    96              pointsToSchedule o.node.points
    97  
    98          sLocal =
    99              checkScheduleToLocal zoneOffset sModel
   100  
   101          sendTime updateSchedule tm =
   102              let
   103                  tmClean =
   104                      Sanitize.time tm
   105              in
   106              updateSchedule sLocal tmClean
   107                  |> checkScheduleToUTC zoneOffset
   108                  |> scheduleToPoints o.now
   109                  |> o.onEditNodePoint
   110  
   111          updateDate index dUpdate =
   112              let
   113                  dClean =
   114                      Sanitize.date dUpdate
   115  
   116                  updatedDates =
   117                      List.Extra.setAt index dClean sLocal.dates
   118  
   119                  sUpdate =
   120                      { sLocal | dates = updatedDates }
   121              in
   122              sUpdate
   123                  |> checkScheduleToUTC zoneOffset
   124                  |> scheduleToPoints o.now
   125                  |> o.onEditNodePoint
   126  
   127          deleteDate index =
   128              let
   129                  updatedDates =
   130                      List.Extra.removeAt index sLocal.dates
   131  
   132                  sUpdate =
   133                      { sLocal | dates = updatedDates }
   134              in
   135              sUpdate
   136                  |> checkScheduleToUTC zoneOffset
   137                  |> scheduleToPoints o.now
   138                  |> o.onEditNodePoint
   139  
   140          weekdaysChecked =
   141              List.foldl
   142                  (\w ret ->
   143                      (w /= 0) || ret
   144                  )
   145                  False
   146                  sLocal.weekdays
   147  
   148          weekdayCheckboxInput index label =
   149              Input.checkbox []
   150                  { onChange =
   151                      \d ->
   152                          updateScheduleWkday sLocal index d
   153                              |> checkScheduleToUTC zoneOffset
   154                              |> scheduleToPoints o.now
   155                              |> o.onEditNodePoint
   156                  , checked = List.member index sLocal.weekdays
   157                  , icon = Input.defaultCheckbox
   158                  , label = Input.labelAbove [] <| text label
   159                  }
   160  
   161          dateCount =
   162              List.length sLocal.dates
   163      in
   164      column [ spacing 5 ]
   165          [ if dateCount <= 0 then
   166              wrappedRow
   167                  [ spacing 20
   168                  , paddingEach { top = 0, right = 0, bottom = 5, left = 0 }
   169                  ]
   170                  -- here, number matches Go Weekday definitions
   171                  -- https://pkg.go.dev/time#Weekday
   172                  [ el [ width <| px (o.labelWidth - 120) ] none
   173                  , text "Weekdays:"
   174                  , weekdayCheckboxInput 0 "S"
   175                  , weekdayCheckboxInput 1 "M"
   176                  , weekdayCheckboxInput 2 "T"
   177                  , weekdayCheckboxInput 3 "W"
   178                  , weekdayCheckboxInput 4 "T"
   179                  , weekdayCheckboxInput 5 "F"
   180                  , weekdayCheckboxInput 6 "S"
   181                  ]
   182  
   183            else
   184              none
   185          , Input.text
   186              []
   187              { label = Input.labelLeft [ width (px labelWidth) ] <| el [ alignRight ] <| text <| "Start time:"
   188              , onChange = sendTime (\sched tm -> { sched | startTime = tm })
   189              , text = sLocal.startTime
   190              , placeholder = Nothing
   191              }
   192          , Input.text
   193              []
   194              { label = Input.labelLeft [ width (px labelWidth) ] <| el [ alignRight ] <| text <| "End time:"
   195              , onChange = sendTime (\sched tm -> { sched | endTime = tm })
   196              , text = sLocal.endTime
   197              , placeholder = Nothing
   198              }
   199          , if not weekdaysChecked then
   200              let
   201                  dateCountS =
   202                      String.fromInt dateCount
   203              in
   204              column []
   205                  [ el [ Element.paddingEach { top = 0, bottom = 0, right = 0, left = labelWidth - 59 } ] <| text "Dates:"
   206                  , column [ spacing 5 ] <|
   207                      List.indexedMap
   208                          (\i date ->
   209                              row [ spacing 10 ]
   210                                  [ Input.text []
   211                                      { label = Input.labelLeft [ width (px labelWidth) ] <| text ""
   212                                      , onChange = updateDate i
   213                                      , text = date
   214                                      , placeholder = Nothing
   215                                      }
   216                                  , UI.Button.x <| deleteDate i
   217                                  ]
   218                          )
   219                          sLocal.dates
   220                  , el [ Element.paddingEach { top = 6, bottom = 0, right = 0, left = labelWidth - 59 } ] <|
   221                      Form.button
   222                          { label = "Add Date"
   223                          , color = Style.colors.blue
   224                          , onPress =
   225                              o.onEditNodePoint
   226                                  [ { typ = Point.typeDate
   227                                    , key = dateCountS
   228                                    , text = ""
   229                                    , time = o.now
   230                                    , tombstone = 0
   231                                    , value = 0
   232                                    }
   233                                  ]
   234                          }
   235                  ]
   236  
   237            else
   238              none
   239          ]
   240  
   241  
   242  pointsToSchedule : List Point -> Utils.Time.Schedule
   243  pointsToSchedule points =
   244      let
   245          start =
   246              Point.getText points Point.typeStart ""
   247  
   248          end =
   249              Point.getText points Point.typeEnd ""
   250  
   251          weekdays =
   252              List.filter
   253                  (\d ->
   254                      let
   255                          dString =
   256                              String.fromInt d
   257  
   258                          p =
   259                              Point.getValue points Point.typeWeekday dString
   260                      in
   261                      p == 1
   262                  )
   263                  [ 0, 1, 2, 3, 4, 5, 6 ]
   264  
   265          datePoints =
   266              List.filter
   267                  (\p ->
   268                      p.typ == Point.typeDate && p.tombstone == 0
   269                  )
   270                  points
   271                  |> List.sortWith Point.sort
   272  
   273          dates =
   274              List.map (\p -> p.text) datePoints
   275      in
   276      { startTime = start
   277      , endTime = end
   278      , weekdays = weekdays
   279      , dates = dates
   280      , dateCount = List.length datePoints
   281      }
   282  
   283  
   284  scheduleToPoints : Time.Posix -> Utils.Time.Schedule -> List Point
   285  scheduleToPoints now sched =
   286      [ Point Point.typeStart "0" now 0 sched.startTime 0
   287      , Point Point.typeEnd "0" now 0 sched.endTime 0
   288      ]
   289          ++ List.map
   290              (\wday ->
   291                  if List.member wday sched.weekdays then
   292                      Point Point.typeWeekday (String.fromInt wday) now 1 "" 0
   293  
   294                  else
   295                      Point Point.typeWeekday (String.fromInt wday) now 0 "" 0
   296              )
   297              [ 0, 1, 2, 3, 4, 5, 6 ]
   298          ++ List.indexedMap
   299              (\i d -> Point Point.typeDate (String.fromInt i) now 0 d 0)
   300              sched.dates
   301          ++ (if List.length sched.dates < sched.dateCount then
   302                  -- some dates have been deleted, so send some tombstone points to fill out array length
   303                  List.map
   304                      (\i ->
   305                          Point Point.typeDate (String.fromInt i) now 0 "" 1
   306                      )
   307                      (List.range (List.length sched.dates) (sched.dateCount - 1))
   308  
   309              else
   310                  []
   311             )
   312  
   313  
   314  
   315  -- only convert to utc if both times and all dates are valid
   316  
   317  
   318  checkScheduleToUTC : Int -> Utils.Time.Schedule -> Utils.Time.Schedule
   319  checkScheduleToUTC offset sched =
   320      if validHM sched.startTime && validHM sched.endTime && validDates sched.dates then
   321          scheduleToUTC offset sched
   322  
   323      else
   324          sched
   325  
   326  
   327  updateScheduleWkday : Utils.Time.Schedule -> Int -> Bool -> Utils.Time.Schedule
   328  updateScheduleWkday sched index checked =
   329      let
   330          weekdays =
   331              if checked then
   332                  if List.member index sched.weekdays then
   333                      sched.weekdays
   334  
   335                  else
   336                      index :: sched.weekdays
   337  
   338              else
   339                  List.Extra.remove index sched.weekdays
   340      in
   341      { sched | weekdays = List.sort weekdays }
   342  
   343  
   344  
   345  -- only convert to local if both times are valid
   346  
   347  
   348  checkScheduleToLocal : Int -> Utils.Time.Schedule -> Utils.Time.Schedule
   349  checkScheduleToLocal offset sched =
   350      if validHM sched.startTime && validHM sched.endTime && validDates sched.dates then
   351          scheduleToLocal offset sched
   352  
   353      else
   354          sched
   355  
   356  
   357  validHM : String -> Bool
   358  validHM t =
   359      case Sanitize.parseHM t of
   360          Just _ ->
   361              True
   362  
   363          Nothing ->
   364              False
   365  
   366  
   367  validDate : String -> Bool
   368  validDate d =
   369      case Sanitize.parseDate d of
   370          Just _ ->
   371              True
   372  
   373          Nothing ->
   374              False
   375  
   376  
   377  validDates : List String -> Bool
   378  validDates dates =
   379      List.foldl
   380          (\d ret ->
   381              if not ret then
   382                  ret
   383  
   384              else if d == "" then
   385                  True
   386  
   387              else
   388                  validDate d
   389          )
   390          True
   391          dates
   392  
   393  
   394  nodeButtonActionText :
   395      NodeInputOptions msg
   396      -> String
   397      -> String
   398      -> String
   399      -> String
   400      -> Color
   401      -> Element msg
   402  nodeButtonActionText o key typ value lbl color =
   403      let
   404          sendText =
   405              o.onEditNodePoint
   406                  [ { typ = typ
   407                    , key = key
   408                    , text = value
   409                    , time = o.now
   410                    , tombstone = 0
   411                    , value = 0
   412                    }
   413                  ]
   414      in
   415      Form.button
   416          { label = lbl
   417          , color = color
   418          , onPress = sendText
   419          }
   420  
   421  
   422  nodeCheckboxInput :
   423      NodeInputOptions msg
   424      -> String
   425      -> String
   426      -> String
   427      -> Element msg
   428  nodeCheckboxInput o key typ lbl =
   429      Input.checkbox
   430          []
   431          { onChange =
   432              \d ->
   433                  let
   434                      v =
   435                          if d then
   436                              1.0
   437  
   438                          else
   439                              0.0
   440                  in
   441                  o.onEditNodePoint
   442                      [ Point typ key o.now v "" 0 ]
   443          , checked =
   444              Point.getValue o.node.points typ key == 1
   445          , icon = Input.defaultCheckbox
   446          , label =
   447              if lbl /= "" then
   448                  Input.labelLeft [ width (px o.labelWidth) ] <|
   449                      el [ alignRight ] <|
   450                          text <|
   451                              lbl
   452                                  ++ ":"
   453  
   454              else
   455                  Input.labelHidden ""
   456          }
   457  
   458  
   459  nodeNumberInput :
   460      NodeInputOptions msg
   461      -> String
   462      -> String
   463      -> String
   464      -> Element msg
   465  nodeNumberInput o key typ lbl =
   466      let
   467          pMaybe =
   468              Point.get o.node.points typ key
   469  
   470          currentValue =
   471              case pMaybe of
   472                  Just p ->
   473                      if p.text /= "" then
   474                          if p.text == Point.blankMajicValue || p.text == "blank" then
   475                              ""
   476  
   477                          else if p.text == "-" then
   478                              "-"
   479  
   480                          else
   481                              Sanitize.float p.text
   482  
   483                      else
   484                          String.fromFloat (Round.roundNum 6 p.value)
   485  
   486                  Nothing ->
   487                      ""
   488  
   489          currentValueF =
   490              case pMaybe of
   491                  Just p ->
   492                      p.value
   493  
   494                  Nothing ->
   495                      0
   496      in
   497      Input.text
   498          []
   499          { onChange =
   500              \d ->
   501                  let
   502                      dCheck =
   503                          if d == "" then
   504                              Point.blankMajicValue
   505  
   506                          else if d == "-" then
   507                              "-"
   508  
   509                          else
   510                              case String.toFloat d of
   511                                  Just _ ->
   512                                      d
   513  
   514                                  Nothing ->
   515                                      currentValue
   516  
   517                      v =
   518                          if dCheck == Point.blankMajicValue || dCheck == "-" then
   519                              0
   520  
   521                          else
   522                              Maybe.withDefault currentValueF <| String.toFloat dCheck
   523                  in
   524                  o.onEditNodePoint
   525                      [ Point typ key o.now v dCheck 0 ]
   526          , text = currentValue
   527          , placeholder = Nothing
   528          , label =
   529              if lbl == "" then
   530                  Input.labelHidden ""
   531  
   532              else
   533                  Input.labelLeft [ width (px o.labelWidth) ] <| el [ alignRight ] <| text <| lbl ++ ":"
   534          }
   535  
   536  
   537  nodeOptionInput :
   538      NodeInputOptions msg
   539      -> String
   540      -> String
   541      -> String
   542      -> List ( String, String )
   543      -> Element msg
   544  nodeOptionInput o key typ lbl options =
   545      Input.radio
   546          [ spacing 6 ]
   547          { onChange =
   548              \sel ->
   549                  o.onEditNodePoint
   550                      [ Point typ key o.now 0 sel 0 ]
   551          , label =
   552              Input.labelLeft [ padding 12, width (px o.labelWidth) ] <|
   553                  el [ alignRight ] <|
   554                      text <|
   555                          lbl
   556                              ++ ":"
   557          , selected = Just <| Point.getText o.node.points typ key
   558          , options =
   559              List.map
   560                  (\opt ->
   561                      Input.option (Tuple.first opt) (text (Tuple.second opt))
   562                  )
   563                  options
   564          }
   565  
   566  
   567  nodeCounterWithReset :
   568      NodeInputOptions msg
   569      -> String
   570      -> String
   571      -> String
   572      -> String
   573      -> Element msg
   574  nodeCounterWithReset o key typ pointResetName lbl =
   575      let
   576          currentValue =
   577              Point.getValue o.node.points typ key
   578  
   579          currentResetValue =
   580              Point.getValue o.node.points pointResetName key /= 0
   581      in
   582      row [ spacing 20 ]
   583          [ el [ width (px o.labelWidth) ] <|
   584              el [ alignRight ] <|
   585                  text <|
   586                      lbl
   587                          ++ ": "
   588                          ++ String.fromFloat currentValue
   589          , Input.checkbox []
   590              { onChange =
   591                  \v ->
   592                      let
   593                          vFloat =
   594                              if v then
   595                                  1.0
   596  
   597                              else
   598                                  0
   599                      in
   600                      o.onEditNodePoint [ Point pointResetName key o.now vFloat "" 0 ]
   601              , icon = Input.defaultCheckbox
   602              , checked = currentResetValue
   603              , label =
   604                  Input.labelLeft [] (text "reset")
   605              }
   606          ]
   607  
   608  
   609  nodeOnOffInput :
   610      NodeInputOptions msg
   611      -> String
   612      -> String
   613      -> String
   614      -> String
   615      -> Element msg
   616  nodeOnOffInput o key typ pointSetName lbl =
   617      let
   618          currentValue =
   619              Point.getValue o.node.points typ key
   620  
   621          currentSetValue =
   622              Point.getValue o.node.points pointSetName key
   623  
   624          fill =
   625              if currentSetValue == 0 then
   626                  Color.rgb 0.5 0.5 0.5
   627  
   628              else
   629                  Color.rgb255 50 100 150
   630  
   631          fillS =
   632              Color.toCssString fill
   633  
   634          offset =
   635              if currentSetValue == 0 then
   636                  3
   637  
   638              else
   639                  3 + 24
   640  
   641          newValue =
   642              if currentSetValue == 0 then
   643                  1
   644  
   645              else
   646                  0
   647      in
   648      row [ spacing 10 ]
   649          [ el [ width (px o.labelWidth) ] <| el [ alignRight ] <| text <| lbl ++ ":"
   650          , Input.button
   651              []
   652              { onPress = Just <| o.onEditNodePoint [ Point pointSetName key o.now newValue "" 0 ]
   653              , label =
   654                  el [ width (px 100) ] <|
   655                      html <|
   656                          S.svg [ Sa.viewBox "0 0 48 24" ]
   657                              [ S.rect
   658                                  [ Sa.x "0"
   659                                  , Sa.y "0"
   660                                  , Sa.width "48"
   661                                  , Sa.height "24"
   662                                  , Sa.ry "3"
   663                                  , Sa.rx "3"
   664                                  , Sa.fill fillS
   665                                  ]
   666                                <|
   667                                  if currentValue /= currentSetValue then
   668                                      let
   669                                          fillFade =
   670                                              if currentSetValue == 0 then
   671                                                  Color.rgb 0.9 0.9 0.9
   672  
   673                                              else
   674                                                  Color.rgb255 150 200 255
   675  
   676                                          fillFadeS =
   677                                              Color.toCssString fillFade
   678                                      in
   679                                      [ S.animate
   680                                          [ Sa.attributeName "fill"
   681                                          , Sa.dur "2s"
   682                                          , Sa.repeatCount "indefinite"
   683                                          , Sa.values <|
   684                                              fillFadeS
   685                                                  ++ ";"
   686                                                  ++ fillS
   687                                                  ++ ";"
   688                                                  ++ fillFadeS
   689                                          ]
   690                                          []
   691                                      ]
   692  
   693                                  else
   694                                      []
   695                              , S.rect
   696                                  [ Sa.x (String.fromFloat offset)
   697                                  , Sa.y "3"
   698                                  , Sa.width "18"
   699                                  , Sa.height "18"
   700                                  , Sa.ry "3"
   701                                  , Sa.rx "3"
   702                                  , Sa.fill (Color.toCssString Color.white)
   703                                  ]
   704                                  []
   705                              ]
   706              }
   707          ]
   708  
   709  
   710  nodePasteButton :
   711      NodeInputOptions msg
   712      -> Element msg
   713      -> String
   714      -> String
   715      -> Element msg
   716  nodePasteButton o label typ value =
   717      row [ spacing 10, paddingEach { top = 0, bottom = 0, right = 0, left = 75 } ]
   718          [ UI.Button.clipboard <| o.onEditNodePoint [ Point typ "0" o.now 0 value 0 ]
   719          , label
   720          ]
   721  
   722  
   723  nodeListInput : NodeInputOptions msg -> String -> String -> String -> Element msg
   724  nodeListInput o typ label buttonLabel =
   725      let
   726          entries =
   727              Point.getTextArray o.node.points typ
   728  
   729          entriesArrayCount =
   730              List.Extra.count
   731                  (\p ->
   732                      p.typ == typ
   733                  )
   734                  o.node.points
   735  
   736          entriesArrayCountS =
   737              String.fromInt entriesArrayCount
   738  
   739          entriesToPoints es =
   740              List.indexedMap
   741                  (\i s ->
   742                      Point typ (String.fromInt i) o.now 0 s 0
   743                  )
   744                  es
   745                  ++ List.map
   746                      (\i ->
   747                          Point typ (String.fromInt i) o.now 0 "" 1
   748                      )
   749                      (List.range (List.length es) (entriesArrayCount - 1))
   750  
   751          updateEntry i update =
   752              List.Extra.setAt i update entries |> entriesToPoints |> o.onEditNodePoint
   753  
   754          deleteEntry i =
   755              List.Extra.removeAt i entries |> entriesToPoints |> o.onEditNodePoint
   756      in
   757      column [ centerX, spacing 5 ] <|
   758          (el [ Font.bold, centerX, Element.paddingXY 0 6 ] <|
   759              Element.text label
   760          )
   761              :: List.indexedMap
   762                  (\i s ->
   763                      row [ spacing 10 ]
   764                          [ Input.text []
   765                              { label = Input.labelHidden "entry name"
   766                              , onChange = updateEntry i
   767                              , text = s
   768                              , placeholder = Nothing
   769                              }
   770                          , UI.Button.x <| deleteEntry i
   771                          ]
   772                  )
   773                  entries
   774              ++ [ el [ Element.paddingXY 0 6, centerX ] <|
   775                      Form.button
   776                          { label = buttonLabel
   777                          , color = Style.colors.blue
   778                          , onPress =
   779                              o.onEditNodePoint
   780                                  [ { typ = typ
   781                                    , key = entriesArrayCountS
   782                                    , text = ""
   783                                    , time = o.now
   784                                    , tombstone = 0
   785                                    , value = 0
   786                                    }
   787                                  ]
   788                          }
   789                 ]
   790  
   791  
   792  type alias Edges =
   793      { top : Int
   794      , right : Int
   795      , bottom : Int
   796      , left : Int
   797      }
   798  
   799  
   800  edges : Edges
   801  edges =
   802      { top = 0
   803      , right = 0
   804      , bottom = 0
   805      , left = 0
   806      }
   807  
   808  
   809  nodeKeyValueInput : NodeInputOptions msg -> String -> String -> String -> Element msg
   810  nodeKeyValueInput o typ label buttonLabel =
   811      let
   812          points =
   813              Point.getAll o.node.points typ |> Point.filterDeleted |> List.sortWith Point.sort
   814  
   815          newKV =
   816              o.onEditNodePoint
   817                  [ { typ = typ
   818                    , key = o.scratch
   819                    , text = ""
   820                    , time = o.now
   821                    , tombstone = 0
   822                    , value = 0
   823                    }
   824                  ]
   825      in
   826      column [ centerX, spacing 5 ]
   827          [ viewIf (List.length points > 0) <| keyValues o typ label points
   828          , row [ spacing 10 ]
   829              [ el [ Element.paddingEach { edges | left = 40 }, centerX ] <|
   830                  text "Add: "
   831              , Input.text [ Form.onEnter newKV ]
   832                  { label = Input.labelHidden "key"
   833                  , onChange = o.onEditScratch
   834                  , text = o.scratch
   835                  , placeholder = Just <| Input.placeholder [] <| text "new key"
   836                  }
   837              , Form.button
   838                  { label = buttonLabel
   839                  , color = Style.colors.blue
   840                  , onPress = newKV
   841                  }
   842              ]
   843          ]
   844  
   845  
   846  keyValues : NodeInputOptions msg -> String -> String -> List Point -> Element msg
   847  keyValues o typ label points =
   848      let
   849          deleteEntry key =
   850              o.onEditNodePoint
   851                  [ { typ = typ
   852                    , key = key
   853                    , text = ""
   854                    , time = o.now
   855                    , tombstone = 1
   856                    , value = 0
   857                    }
   858                  ]
   859      in
   860      column [ centerX, spacing 5 ]
   861          [ el [ Font.bold, centerX, Element.paddingXY 0 6 ] <|
   862              Element.text label
   863          , table [ padding 7 ]
   864              { data = points
   865              , columns =
   866                  let
   867                      cell =
   868                          el [ paddingXY 15 5, Border.width 0, centerY ]
   869                  in
   870                  [ { header = cell <| el [ Font.bold, centerX ] <| text "Key"
   871                    , width = shrink
   872                    , view = \p -> cell <| text <| p.key ++ ":"
   873                    }
   874                  , { header = cell <| el [ Font.bold, centerX ] <| text "Value"
   875                    , width = fill
   876                    , view = \p -> cell <| nodeTextInput o p.key typ "" "value"
   877                    }
   878                  , { header = cell <| el [ Font.bold, centerX ] <| text "Delete"
   879                    , width = shrink
   880                    , view = \p -> cell <| el [ centerX ] <| UI.Button.x <| deleteEntry p.key
   881                    }
   882                  ]
   883              }
   884          ]