NOISSUE - Remove Elm UI (#953)

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
This commit is contained in:
Manuel Imperiale
2019-11-19 09:31:41 +01:00
committed by Drasko DRASKOVIC
parent 5e35cbe06b
commit ac3ff5221a
27 changed files with 1 additions and 3320 deletions
-1
View File
@@ -1,7 +1,6 @@
.git
.github
build
ui
docker
docs
k8s
+1 -11
View File
@@ -37,7 +37,7 @@ endef
all: $(SERVICES) mqtt
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt ui
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt
clean:
rm -rf ${BUILD_DIR}
@@ -81,9 +81,6 @@ $(DOCKERS):
$(DOCKERS_DEV):
$(call make_docker_dev,$(@))
docker_ui:
$(MAKE) -C ui docker
docker_mqtt:
# MQTT Docker build must be done from root dir because it copies .proto files
ifeq ($(GOARCH), arm)
@@ -99,9 +96,6 @@ dockers: $(DOCKERS) docker_ui docker_mqtt
dockers_dev: $(DOCKERS_DEV)
ui:
$(MAKE) -C ui
mqtt:
cd mqtt/aedes && npm install
@@ -109,7 +103,6 @@ define docker_push
for svc in $(SERVICES); do \
docker push mainflux/$$svc:$(1); \
done
docker push mainflux/ui:$(1)
docker push mainflux/mqtt:$(1)
endef
@@ -136,9 +129,6 @@ rundev:
run:
docker-compose -f docker/docker-compose.yml -f docker/aedes.yml up
runui:
$(MAKE) -C ui run
runlora:
docker-compose \
-f docker/docker-compose.yml \
-2
View File
@@ -1,2 +0,0 @@
elm-stuff
main.js
-25
View File
@@ -1,25 +0,0 @@
all: dev
.PHONY: all docker
dev:
elm make src/Main.elm --output=main.js
prod:
elm make --optimize src/Main.elm --output=main.js
run:
elm reactor
docker:
ifeq ($(GOARCH), arm)
docker build --tag=mainflux/ui-arm -f docker/Dockerfile.arm .
else
docker build --tag=mainflux/ui -f docker/Dockerfile .
endif
clean:
rm -f main.js
mrproper: clean
rm -rf elm-stuff
-57
View File
@@ -1,57 +0,0 @@
# GUI for Mainflux in Elm
Dashboard made with [elm-bootstrap](http://elm-bootstrap.info/).
## Install
### Docker container GUI build
Install [Docker](https://docs.docker.com/install/) and [Docker
compose](https://docs.docker.com/compose/install/), `cd` to Mainflux root
directory and then
`docker-compose -f docker/docker-compose.yml up`
if you want to launch a whole Mainflux docker composition, or just
`docker-compose -f docker/docker-compose.yml up ui`
if you want to launch just GUI.
### Native GUI build
Install [Elm](https://guide.elm-lang.org/install.html) and then run the
following commands:
```
git clone https://github.com/mainflux/mainflux
cd mainflux/ui
make
```
This will produce `index.html` in the _ui_ directory. Still in the _mainflux/ui_
folder, enter
`make run`
and follow the instructions on screen.
**NB:** `make` does `elm make src/Main.elm --output=main.js` and `make run` executes `elm
reactor`. Cf. _Makefile_ for more options.
## Configuration
Open the _src/Env.elm_ file and edit the values of the `env` record.
## Contribute to the GUI development
Follow the instructions above to install and run GUI as a native build. In
_src/Env.elm_ change a `url` field value of the `elm` record to
`http://localhost:80/` (trailing slash `/` is mandatory). Instead of `make run`
you can install [elm-live](https://github.com/wking-io/elm-live) and execute
`elm-live src/Main.elm -- --output=main.js` to get a live reload when your `.Elm` files change.
Launch Mainflux without ui service, either natively or as a Docker composition.
If you have already launched Mainflux as a Docker composition, simply `cd` to
Mainflux folder and run `docker-compose -f docker/docker-compose.yml stop ui`.
Please follow the [guidelines for Mainflux
contributors](https://mainflux.readthedocs.io/en/latest/CONTRIBUTING/).
-112
View File
@@ -1,112 +0,0 @@
/*
* Copyright (c) Mainflux
* SPDX-License-Identifier: Apache-2.0
*/
@import url('https://fonts.googleapis.com/css?family=Roboto');
@import url('https://fonts.googleapis.com/css?family=Montserrat:400,600,800');
.btn-primary {
color: #fff;
background-color: #113f67;
border-color: #113f67;
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active,
.btn-primary.active,
.open>.dropdown-toggle.btn-primary {
color: #fff;
background-color: #408ab4;
border-color: #408ab4;
}
.btn-secondary {
color: #fff;
background-color: #408ab4;
border-color: #408ab4;
}
.btn-secondary:hover,
.btn-secondary:focus,
.btn-secondary:active,
.btn-secondary.active,
.open>.dropdown-toggle.btn-secondary {
color: #fff;
background-color: #113f67;
border-color: #113f67;
}
body {
background-color: #f3f3f4 !important;
font-family: 'Montserrat', sans-serif !important;
}
.title {
font-family: 'Montserrat', sans-serif;
font-weight: 800;
transform: scaleY(0.95);
text-align: center;
margin: 1rem 0;
}
.page-link {
color: #113f67;
}
.page-item.active .page-link {
background-color: #408ab4;
border-color: #408ab4;
}
ul.nav a:hover {
color: #fff !important;
background-color: #408ab4 !important;
cursor: pointer;
transition: color .15s ease-in-out, background-color .15s ease-in-out;
border-radius: 0;
}
.nav-pills .nav-link.active {
background-color: #408ab4 !important;
border-radius: 0;
}
.nav li a i {
margin-right: 10px;
font-size: 12px;
width: 25px;
height: 25px;
line-height: 25px;
text-align: center;
border-radius: 4px;
background: transparent;
}
.card {
margin-top: 30px;
margin-bottom: 30px;
border: none;
}
.card-header {
background-color: white;
border-bottom: none;
margin-bottom: none;
}
.table thead th {
border-top: none;
}
.table-hover tbody tr:hover>td {
cursor: pointer;
background: #408ab4;
color: white;
}
.table_header {
color: #408ab4;
font-weight: bold;
}
-27
View File
@@ -1,27 +0,0 @@
###
# Copyright (c) Mainflux
#
# Mainflux is licensed under an Apache license, version 2.0 license.
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
# See the included LICENSE file for more details.
###
# Stage 0, based on Node.js, to build and compile Elm app
FROM node:10.15.1-alpine as builder
WORKDIR /app
RUN npm install --unsafe-perm=true --allow-root -g elm@latest-0.19.0
COPY . /app
RUN elm make --optimize src/Main.elm --output=main.js
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
FROM nginx:1.14.2-alpine
COPY --from=builder /app/index.html /usr/share/nginx/html
COPY --from=builder /app/main.js /usr/share/nginx/html
COPY --from=builder /app/css/mainflux.css /usr/share/nginx/html/css/
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY src/Websocket.js /usr/share/nginx/html/src/
COPY docker/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
-27
View File
@@ -1,27 +0,0 @@
###
# Copyright (c) Mainflux
#
# Mainflux is licensed under an Apache license, version 2.0 license.
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
# See the included LICENSE file for more details.
###
# Stage 0, based on Node.js, to build and compile Elm app
FROM node:10.15.1-alpine as builder
WORKDIR /app
RUN npm install --unsafe-perm=true --allow-root -g elm@latest-0.19.0
COPY . /app
RUN elm make --optimize src/Main.elm --output=main.js
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
FROM arm32v7/nginx:1.16
COPY --from=builder /app/index.html /usr/share/nginx/html
COPY --from=builder /app/main.js /usr/share/nginx/html
COPY --from=builder /app/css/mainflux.css /usr/share/nginx/html/css/
COPY --from=builder /app/src/Websocket.js /usr/share/nginx/html/src/
COPY --from=builder /app/docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY docker/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
-9
View File
@@ -1,9 +0,0 @@
#!/bin/sh
if [ -n "$MF_UI_PORT" ]; then
sed -i -e "s/MF_UI_PORT/$MF_UI_PORT/" /etc/nginx/conf.d/default.conf
else
sed -i -e "s/MF_UI_PORT/3000/" /etc/nginx/conf.d/default.conf
fi
exec nginx -g "daemon off;"
-8
View File
@@ -1,8 +0,0 @@
server {
listen MF_UI_PORT;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
}
-30
View File
@@ -1,30 +0,0 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.2",
"elm/url": "1.0.0",
"elm-community/list-extra": "8.1.0",
"rundis/elm-bootstrap": "5.0.0"
},
"indirect": {
"avh4/elm-color": "1.0.0",
"elm/bytes": "1.0.7",
"elm/file": "1.0.1",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Main</title>
<link rel="stylesheet" href="css/mainflux.css">
<script src="main.js"></script>
</head>
<body>
<div id="elm"></div>
<script>
var app = Elm.Main.init({
node: document.getElementById('elm')
});
var MF = {}
</script>
<script src="src/Websocket.js"></script>
</body>
</html>
-469
View File
@@ -1,469 +0,0 @@
-- Copyright (c) Mainflux--
-- SPDX-License-Identifier: Apache-2.0
module Channel exposing (Channel, Model, Msg(..), initial, update, view)
import Bootstrap.Button as Button
import Bootstrap.Card as Card
import Bootstrap.Card.Block as Block
import Bootstrap.Form as Form
import Bootstrap.Form.Input as Input
import Bootstrap.Form.InputGroup as InputGroup
import Bootstrap.Grid as Grid
import Bootstrap.Grid.Col as Col
import Bootstrap.Modal as Modal
import Bootstrap.Table as Table
import Bootstrap.Utilities.Spacing as Spacing
import Dict
import Error
import Helpers exposing (faIcons, fontAwesome)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
import HttpMF exposing (paths)
import Json.Decode as D
import Json.Encode as E
import JsonMF exposing (..)
import ModalMF
import Url.Builder as B
query =
{ offset = 0
, limit = 10
}
type alias Channel =
{ name : Maybe String
, id : String
, metadata : Maybe JsonValue
}
emptyChannel =
Channel (Just "") "" (Just ValueNull)
type alias Channels =
{ list : List Channel
, total : Int
}
type alias Model =
{ name : String
, metadata : String
, offset : Int
, limit : Int
, response : String
, channels : Channels
, channel : Channel
, editMode : Bool
, provisionModalVisibility : Modal.Visibility
, editModalVisibility : Modal.Visibility
}
initial : Model
initial =
{ name = ""
, metadata = ""
, offset = query.offset
, limit = query.limit
, response = ""
, channels =
{ list = []
, total = 0
}
, channel = emptyChannel
, editMode = False
, provisionModalVisibility = Modal.hidden
, editModalVisibility = Modal.hidden
}
type Msg
= SubmitName String
| SubmitMetadata String
| ProvisionChannel
| ProvisionedChannel (Result Http.Error String)
| EditChannel
| UpdateChannel
| UpdatedChannel (Result Http.Error String)
| RetrieveChannel String
| RetrievedChannel (Result Http.Error Channel)
| RetrieveChannels
| RetrieveChannelsForThing String
| RetrievedChannels (Result Http.Error Channels)
| RemoveChannel String
| RemovedChannel (Result Http.Error String)
| SubmitPage Int
| ShowEditModal Channel
| CloseEditModal
| ShowProvisionModal
| ClosePorvisionModal
update : Msg -> Model -> String -> ( Model, Cmd Msg )
update msg model token =
case msg of
SubmitName name ->
( { model | name = name }, Cmd.none )
SubmitPage page ->
updateChannelList { model | offset = Helpers.pageToOffset page query.limit } token
SubmitMetadata metadata ->
( { model | metadata = metadata }, Cmd.none )
ProvisionChannel ->
( resetEdit model
, HttpMF.provision
(B.relative [ paths.channels ] [])
token
{ emptyChannel
| name = Just model.name
, metadata = stringToMaybeJsonValue model.metadata
}
channelEncoder
ProvisionedChannel
"/channels/"
)
ProvisionedChannel result ->
case result of
Ok channelid ->
updateChannelList
{ model
| channel = { emptyChannel | id = channelid }
, provisionModalVisibility = Modal.hidden
, editModalVisibility = Modal.shown
}
token
Err error ->
( { model | response = Error.handle error }, Cmd.none )
EditChannel ->
( { model
| editMode = True
, name = Helpers.parseString model.channel.name
, metadata = maybeJsonValueToString model.channel.metadata
}
, Cmd.none
)
UpdateChannel ->
( resetEdit { model | editMode = False }
, HttpMF.update
(B.relative [ paths.channels, model.channel.id ] [])
token
{ emptyChannel
| name = Just model.name
, metadata =
case stringToJsonValue model.metadata of
Ok jsonValue ->
Just jsonValue
Err err ->
model.channel.metadata
}
channelEncoder
UpdatedChannel
)
UpdatedChannel result ->
case result of
Ok statusCode ->
updateChannelList (resetEdit { model | response = statusCode }) token
Err error ->
( { model | response = Error.handle error }, Cmd.none )
RetrieveChannel channelid ->
( model
, HttpMF.retrieve
(B.relative [ paths.channels, channelid ] [])
token
RetrievedChannel
channelDecoder
)
RetrievedChannel result ->
case result of
Ok channel ->
( { model | channel = channel }, Cmd.none )
Err error ->
( { model | response = Error.handle error }, Cmd.none )
RetrieveChannels ->
( model
, HttpMF.retrieve
(B.relative [ paths.channels ] (Helpers.buildQueryParamList model.offset model.limit))
token
RetrievedChannels
channelsDecoder
)
RetrieveChannelsForThing thingid ->
( model
, HttpMF.retrieve
(B.relative [ paths.things, thingid, paths.channels ] (Helpers.buildQueryParamList model.offset model.limit))
token
RetrievedChannels
channelsDecoder
)
RetrievedChannels result ->
case result of
Ok channels ->
( { model | channels = channels }, Cmd.none )
Err error ->
( { model | response = Error.handle error }, Cmd.none )
RemoveChannel id ->
( resetEdit model
, HttpMF.remove
(B.relative [ paths.channels, id ] [])
token
RemovedChannel
)
RemovedChannel result ->
case result of
Ok statusCode ->
updateChannelList
{ model
| response = statusCode
, offset = Helpers.validateOffset model.offset model.channels.total query.limit
, editModalVisibility = Modal.hidden
}
token
Err error ->
( { model | response = Error.handle error }, Cmd.none )
ShowEditModal channel ->
( { model
| editModalVisibility = Modal.shown
, channel = channel
, editMode = False
}
, Cmd.none
)
CloseEditModal ->
( resetEdit { model | editModalVisibility = Modal.hidden }, Cmd.none )
ShowProvisionModal ->
( { model | provisionModalVisibility = Modal.shown }
, Cmd.none
)
ClosePorvisionModal ->
( resetEdit { model | provisionModalVisibility = Modal.hidden }, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
Grid.container []
(Helpers.appendIf (model.channels.total > model.limit)
[ Grid.row []
[ Grid.col []
[ Card.config []
|> Card.header []
[ Grid.row []
[ Grid.col [ Col.attrs [ align "left" ] ]
[ h3 [] [ div [ class "table_header" ] [ i [ style "margin-right" "15px", class faIcons.channels ] [], text "Channels" ] ]
]
, Grid.col [ Col.attrs [ align "right" ] ]
[ Button.button [ Button.secondary, Button.attrs [ align "right" ], Button.onClick ShowProvisionModal ] [ text "ADD" ]
]
]
]
|> Card.block []
[ Block.custom
(Table.table
{ options = [ Table.striped, Table.hover, Table.small ]
, thead = genTableHeader
, tbody = genTableBody model
}
)
]
|> Card.view
]
]
, provisionModal model
, editModal model
]
<|
Helpers.genPagination model.channels.total (Helpers.offsetToPage model.offset model.limit) SubmitPage
)
-- Channels table
genTableHeader : Table.THead Msg
genTableHeader =
Table.simpleThead
[ Table.th [] [ text "Name" ]
, Table.th [] [ text "ID" ]
]
genTableBody : Model -> Table.TBody Msg
genTableBody model =
Table.tbody []
(List.map
(\channel ->
Table.tr [ Table.rowAttr (onClick (ShowEditModal channel)) ]
[ Table.td [] [ text (Helpers.parseString channel.name) ]
, Table.td [] [ text channel.id ]
]
)
model.channels.list
)
-- Provision modal
provisionModal : Model -> Html Msg
provisionModal model =
Modal.config ClosePorvisionModal
|> Modal.large
|> Modal.hideOnBackdropClick True
|> Modal.h4 [] [ text "Add channel" ]
|> provisionModalBody model
|> Modal.view model.provisionModalVisibility
provisionModalBody : Model -> (Modal.Config Msg -> Modal.Config Msg)
provisionModalBody model =
Modal.body []
[ Grid.container []
[ ModalMF.modalForm
[ ModalMF.FormRecord "name" SubmitName model.name model.name
, ModalMF.FormRecord "metadata" SubmitMetadata model.metadata model.metadata
]
, ModalMF.provisionModalButtons ProvisionChannel ClosePorvisionModal
]
]
-- Edit modal
editModal : Model -> Html Msg
editModal model =
Modal.config CloseEditModal
|> Modal.large
|> Modal.hideOnBackdropClick True
|> Modal.h4 [] [ text (Helpers.parseString model.channel.name) ]
|> editModalBody model
|> Modal.view model.editModalVisibility
editModalBody : Model -> (Modal.Config Msg -> Modal.Config Msg)
editModalBody model =
Modal.body []
[ Grid.container []
[ Grid.row []
[ Grid.col []
[ editModalForm model
, ModalMF.modalDiv [ ( "id", model.channel.id ) ]
]
]
, ModalMF.editModalButtons model.editMode UpdateChannel EditChannel (ShowEditModal model.channel) (RemoveChannel model.channel.id) CloseEditModal
]
]
editModalForm : Model -> Html Msg
editModalForm model =
if model.editMode then
ModalMF.modalForm
[ ModalMF.FormRecord "name" SubmitName (Helpers.parseString model.channel.name) model.name
, ModalMF.FormRecord "metadata" SubmitMetadata (maybeJsonValueToString model.channel.metadata) model.metadata
]
else
ModalMF.modalDiv [ ( "name", Helpers.parseString model.channel.name ), ( "metadata", maybeJsonValueToString model.channel.metadata ) ]
-- JSON
channelDecoder : D.Decoder Channel
channelDecoder =
D.map3 Channel
(D.maybe (D.field "name" D.string))
(D.field "id" D.string)
(D.maybe (D.field "metadata" jsonValueDecoder))
channelsDecoder : D.Decoder Channels
channelsDecoder =
D.map2 Channels
(D.field "channels" (D.list channelDecoder))
(D.field "total" D.int)
channelEncoder : Channel -> E.Value
channelEncoder channel =
E.object
[ ( "name", E.string (Helpers.parseString channel.name) )
, ( "metadata", jsonValueEncoder (maybeJsonValueToJsonValue channel.metadata) )
]
-- HELPERS
resetEdit : Model -> Model
resetEdit model =
{ model | name = "", metadata = "" }
updateChannelList : Model -> String -> ( Model, Cmd Msg )
updateChannelList model token =
( model
, Cmd.batch
[ HttpMF.retrieve
(B.relative [ paths.channels ] (Helpers.buildQueryParamList model.offset model.limit))
token
RetrievedChannels
channelsDecoder
, HttpMF.retrieve
(B.relative [ paths.channels, model.channel.id ] [])
token
RetrievedChannel
channelDecoder
]
)
updateChannelListForThing : Model -> String -> String -> ( Model, Cmd Msg )
updateChannelListForThing model token thingid =
( model
, HttpMF.retrieve
(B.relative [ paths.things, thingid, paths.channels ] (Helpers.buildQueryParamList model.offset model.limit))
token
RetrievedChannels
channelsDecoder
)
-209
View File
@@ -1,209 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
port module Connection exposing (Model, Msg(..), initial, update, view)
import Bootstrap.Button as Button
import Bootstrap.Card as Card
import Bootstrap.Card.Block as Block
import Bootstrap.Form as Form
import Bootstrap.Form.Checkbox as Checkbox
import Bootstrap.Form.Input as Input
import Bootstrap.Grid as Grid
import Bootstrap.Grid.Col as Col
import Bootstrap.Table as Table
import Bootstrap.Text as Text
import Bootstrap.Utilities.Spacing as Spacing
import Channel
import Debug exposing (log)
import Error
import Helpers exposing (faIcons, fontAwesome)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
import HttpMF exposing (paths)
import Json.Decode as D
import Json.Encode as E
import List.Extra
import Ports exposing (..)
import Thing
import Url.Builder as B
type alias Model =
{ response : String
, things : Thing.Model
, channels : Channel.Model
, checkedThingsIds : List String
, checkedChannelsIds : List String
, websocketIn : List String
}
initial : Model
initial =
{ response = ""
, things = Thing.initial
, channels = Channel.initial
, checkedThingsIds = []
, checkedChannelsIds = []
, websocketIn = []
}
type Msg
= Connect
| Disconnect
| ThingMsg Thing.Msg
| ChannelMsg Channel.Msg
| GotResponse (Result Http.Error String)
| CheckThing ( String, String )
| CheckChannel String
resetChecked : Model -> Model
resetChecked model =
{ model | checkedThingsIds = [], checkedChannelsIds = [] }
isEmptyChecked : Model -> Bool
isEmptyChecked model =
List.isEmpty model.checkedThingsIds || List.isEmpty model.checkedChannelsIds
update : Msg -> Model -> String -> ( Model, Cmd Msg )
update msg model token =
case msg of
Connect ->
if isEmptyChecked model then
( model, Cmd.none )
else
( resetChecked model
, Cmd.batch (connect model.checkedThingsIds model.checkedChannelsIds "PUT" token)
)
Disconnect ->
if isEmptyChecked model then
( model, Cmd.none )
else
( resetChecked model
, Cmd.batch (connect model.checkedThingsIds model.checkedChannelsIds "DELETE" token)
)
GotResponse result ->
case result of
Ok statusCode ->
( { model | response = statusCode }, Cmd.none )
Err error ->
( { model | response = Error.handle error }, Cmd.none )
ThingMsg subMsg ->
let
( updatedThing, thingCmd ) =
Thing.update subMsg model.things token
in
( { model | things = updatedThing }, Cmd.map ThingMsg thingCmd )
ChannelMsg subMsg ->
let
( updatedChannel, channelCmd ) =
Channel.update subMsg model.channels token
in
( { model | channels = updatedChannel }, Cmd.map ChannelMsg channelCmd )
CheckThing thing ->
( { model
| checkedThingsIds = Helpers.checkEntity (Tuple.first thing) model.checkedThingsIds
}
, Cmd.none
)
CheckChannel id ->
( { model | checkedChannelsIds = Helpers.checkEntity id model.checkedChannelsIds }, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
Grid.container []
[ Grid.row []
[ Grid.col []
(Helpers.appendIf (model.things.things.total > model.things.limit)
[ Helpers.genCardConfig faIcons.things "Things" (genThingRows model.checkedThingsIds model.things.things.list) ]
(Html.map ThingMsg (Helpers.genPagination model.things.things.total (Helpers.offsetToPage model.things.offset model.things.limit) Thing.SubmitPage))
)
, Grid.col []
(Helpers.appendIf (model.channels.channels.total > model.channels.limit)
[ Helpers.genCardConfig faIcons.channels "Channels" (genChannelRows model.checkedChannelsIds model.channels.channels.list) ]
(Html.map ChannelMsg (Helpers.genPagination model.channels.channels.total (Helpers.offsetToPage model.channels.offset model.channels.limit) Channel.SubmitPage))
)
]
, Grid.row []
[ Grid.col [ Col.attrs [ align "left" ] ]
[ Form.form []
[ Button.button [ Button.success, Button.attrs [ Spacing.ml1 ], Button.onClick Connect ] [ text "Connect" ]
, Button.button [ Button.danger, Button.attrs [ Spacing.ml1 ], Button.onClick Disconnect ] [ text "Disconnect" ]
]
]
]
, Helpers.response model.response
]
genThingRows : List String -> List Thing.Thing -> List (Table.Row Msg)
genThingRows checkedThingsIds things =
List.map
(\thing ->
Table.tr []
[ Table.td [] [ text (" " ++ Helpers.parseString thing.name) ]
, Table.td [] [ text thing.id ]
, Table.td [] [ input [ type_ "checkbox", onClick (CheckThing ( thing.id, thing.key )), checked (Helpers.isChecked thing.id checkedThingsIds) ] [] ]
]
)
things
genChannelRows : List String -> List Channel.Channel -> List (Table.Row Msg)
genChannelRows checkedChannelsIds channels =
List.map
(\channel ->
Table.tr []
[ Table.td [] [ text (" " ++ Helpers.parseString channel.name) ]
, Table.td [] [ text channel.id ]
, Table.td [] [ input [ type_ "checkbox", onClick (CheckChannel channel.id), checked (Helpers.isChecked channel.id checkedChannelsIds) ] [] ]
]
)
channels
-- HTTP
connect : List String -> List String -> String -> String -> List (Cmd Msg)
connect checkedThingsIds checkedChannelsIds method token =
List.foldr (++)
[]
(List.map
(\thingid ->
List.map
(\channelid ->
HttpMF.request
(B.relative [ paths.channels, channelid, paths.things, thingid ] [])
method
token
Http.emptyBody
GotResponse
)
checkedChannelsIds
)
checkedThingsIds
)
-11
View File
@@ -1,11 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
module Env exposing (env)
env =
{ -- Leave empty to let browser prepend base URL to requests
url = ""
}
-26
View File
@@ -1,26 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
module Error exposing (handle)
import Http
handle : Http.Error -> String
handle error =
case error of
Http.BadUrl url ->
"Bad URL: " ++ url
Http.Timeout ->
"Timeout"
Http.NetworkError ->
"Network error"
Http.BadStatus code ->
"Bad status: " ++ String.fromInt code
Http.BadBody err ->
"Invalid response body"
-237
View File
@@ -1,237 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
module Helpers exposing (appendIf, buildQueryParamList, checkEntity, disableNext, faIcons, fontAwesome, genCardConfig, genOrderedList, genPagination, isChecked, offsetToPage, pageToOffset, parseString, resetList, response, validateInt, validateOffset)
import Bootstrap.Button as Button
import Bootstrap.Card as Card
import Bootstrap.Card.Block as Block
import Bootstrap.Grid as Grid
import Bootstrap.Grid.Col as Col
import Bootstrap.Grid.Row as Row
import Bootstrap.Table as Table
import Bootstrap.Utilities.Spacing as Spacing
import Html exposing (Html, a, div, hr, i, li, nav, node, p, strong, text, ul)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
import List.Extra
import Url.Builder as B
-- HTTP
response : String -> Html.Html msg
response resp =
if String.length resp > 0 then
Grid.row []
[ Grid.col []
[ hr [] []
, p [] [ text ("response: " ++ resp) ]
]
]
else
Grid.row []
[ Grid.col [] []
]
-- STRING
parseString : Maybe String -> String
parseString str =
case str of
Just s ->
s
Nothing ->
""
-- PAGINATION
buildQueryParamList : Int -> Int -> List B.QueryParameter
buildQueryParamList offset limit =
[ B.int "offset" offset, B.int "limit" limit ]
validateInt : String -> Int -> Int
validateInt string default =
case String.toInt string of
Just num ->
num
Nothing ->
default
pageToOffset : Int -> Int -> Int
pageToOffset page limit =
(page - 1) * limit
offsetToPage : Int -> Int -> Int
offsetToPage offset limit =
(offset // limit) + 1
validateOffset : Int -> Int -> Int -> Int
validateOffset offset total limit =
if offset >= (total - 1) then
Basics.max ((total - 1) - limit) 0
else
offset
disableNext : Int -> Int -> Bool
disableNext currPage total =
currPage == Basics.ceiling (Basics.toFloat total / 10)
genPagination : Int -> Int -> (Int -> msg) -> Html msg
genPagination total currPage msg =
let
pages =
List.range 1 (Basics.ceiling (Basics.toFloat total / 10))
cols =
nav []
[ ul [ class "pagination" ]
([ li [ classList [ ( "page-item", True ), ( "disabled", currPage == 1 ) ] ]
[ a
(appendIf (currPage > 1)
[ class "page-link" ]
(onClick (msg (currPage - 1)))
)
[ text "Previous" ]
]
]
++ List.map
(\page ->
li [ classList [ ( "page-item", True ), ( "active", currPage == page ) ] ] [ a [ class "page-link", onClick (msg page) ] [ text (String.fromInt page) ] ]
)
pages
++ [ li [ classList [ ( "page-item", True ), ( "disabled", disableNext currPage total ) ] ]
[ a
(appendIf
(not (disableNext currPage total))
[ class "page-link" ]
(onClick (msg (currPage + 1)))
)
[ text "Next" ]
]
]
)
]
in
Grid.row [] [ Grid.col [] [ cols ] ]
-- FONT-AWESOME
fontAwesome : Html msg
fontAwesome =
node "link"
[ rel "stylesheet"
, href "https://use.fontawesome.com/releases/v5.7.2/css/all.css"
, attribute "integrity" "sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr"
, attribute "crossorigin" "anonymous"
]
[]
faIcons =
{ provision = "fa fa-plus"
, edit = "fa fa-pen"
, remove = "fa fa-trash-alt"
, dashboard = "fas fa-chart-bar"
, things = "fas fa-sitemap"
, channels = "fas fa-broadcast-tower"
, connection = "fas fa-plug"
, messages = "far fa-paper-plane"
, version = "fa fa-code-branch"
, websocket = "fas fa-arrows-alt-v"
, send = "fas fa-arrow-up"
, receive = "fas fa-arrow-down"
}
-- TABLE
checkEntity : String -> List String -> List String
checkEntity id checkedEntitiesIds =
if List.member id checkedEntitiesIds then
List.Extra.remove id checkedEntitiesIds
else
id :: checkedEntitiesIds
isChecked : String -> List String -> Bool
isChecked id checkedEntitiesIds =
if List.member id checkedEntitiesIds then
True
else
False
appendIf : Bool -> List a -> a -> List a
appendIf flag list value =
if flag then
list ++ [ value ]
else
list
genCardConfig : String -> String -> List (Table.Row msg) -> Html msg
genCardConfig faClass title rows =
Card.config
[]
|> Card.headerH3 [] [ div [ class "table_header" ] [ i [ style "margin-right" "15px", class faClass ] [], text title ] ]
|> Card.block []
[ Block.custom
(Table.table
{ options = [ Table.striped, Table.hover, Table.small ]
, thead =
Table.simpleThead
[ Table.th [] [ text "Name" ]
, Table.th [] [ text "ID" ]
]
, tbody = Table.tbody [] <| rows
}
)
]
|> Card.view
genOrderedList : List String -> Html msg
genOrderedList strings =
Html.ol []
(strings
|> List.map
(\string -> Html.li [] [ Html.text string ])
)
resetList : List String -> Int -> List String
resetList strings limit =
if List.length strings >= limit then
[]
else
strings
-192
View File
@@ -1,192 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
module HttpMF exposing (baseURL, expectID, expectRetrieve, expectStatus, paths, provision, remove, request, retrieve, update, user, version)
import Dict
import Env exposing (env)
import Helpers
import Http
import Json.Decode as D
import Json.Encode as E
import Url.Builder as B
baseURL =
env.url
paths =
{ users = "users"
, tokens = "tokens"
, things = "things"
, channels = "channels"
, messages = "messages"
, version = "version"
}
-- EXPECT
expectStatus : (Result Http.Error String -> msg) -> Http.Expect msg
expectStatus toMsg =
Http.expectStringResponse toMsg <|
\resp ->
case resp of
Http.BadUrl_ u ->
Err (Http.BadUrl u)
Http.Timeout_ ->
Err Http.Timeout
Http.NetworkError_ ->
Err Http.NetworkError
Http.BadStatus_ metadata body ->
Err (Http.BadStatus metadata.statusCode)
Http.GoodStatus_ metadata _ ->
Ok (String.fromInt metadata.statusCode)
expectID : (Result Http.Error String -> msg) -> String -> Http.Expect msg
expectID toMsg prefix =
Http.expectStringResponse toMsg <|
\resp ->
case resp of
Http.BadUrl_ u ->
Err (Http.BadUrl u)
Http.Timeout_ ->
Err Http.Timeout
Http.NetworkError_ ->
Err Http.NetworkError
Http.BadStatus_ metadata body ->
Err (Http.BadStatus metadata.statusCode)
Http.GoodStatus_ metadata body ->
Ok <|
String.dropLeft (String.length prefix) <|
Helpers.parseString (Dict.get "location" metadata.headers)
expectRetrieve : (Result Http.Error a -> msg) -> D.Decoder a -> Http.Expect msg
expectRetrieve toMsg decoder =
Http.expectStringResponse toMsg <|
\resp ->
case resp of
Http.BadUrl_ u ->
Err (Http.BadUrl u)
Http.Timeout_ ->
Err Http.Timeout
Http.NetworkError_ ->
Err Http.NetworkError
Http.BadStatus_ metadata body ->
Err (Http.BadStatus metadata.statusCode)
Http.GoodStatus_ metadata body ->
case D.decodeString decoder body of
Ok value ->
Ok value
Err err ->
Err (Http.BadBody (D.errorToString err))
-- REQUEST
version : String -> (Result Http.Error String -> msg) -> D.Decoder String -> Cmd msg
version path msg decoder =
Http.get
{ url = baseURL ++ path
, expect = Http.expectJson msg decoder
}
user : String -> String -> String -> E.Value -> Http.Expect msg -> Cmd msg
user email password u value expect =
Http.post
{ url = baseURL ++ u
, body =
value |> Http.jsonBody
, expect = expect
}
request : String -> String -> String -> Http.Body -> (Result Http.Error String -> msg) -> Cmd msg
request path method token b msg =
Http.request
{ method = method
, headers = [ Http.header "Authorization" token ]
, url = baseURL ++ path
, body = b
, expect = expectStatus msg
, timeout = Nothing
, tracker = Nothing
}
retrieve : String -> String -> (Result Http.Error a -> msg) -> D.Decoder a -> Cmd msg
retrieve path token msg decoder =
Http.request
{ method = "GET"
, headers = [ Http.header "Authorization" token ]
, url = baseURL ++ path
, body = Http.emptyBody
, expect = expectRetrieve msg decoder
, timeout = Nothing
, tracker = Nothing
}
provision : String -> String -> entity -> (entity -> E.Value) -> (Result Http.Error String -> msg) -> String -> Cmd msg
provision path token e encoder msg prefix =
Http.request
{ method = "POST"
, headers = [ Http.header "Authorization" token ]
, url = baseURL ++ path
, body =
encoder e
|> Http.jsonBody
, expect = expectID msg prefix
, timeout = Nothing
, tracker = Nothing
}
update : String -> String -> entity -> (entity -> E.Value) -> (Result Http.Error String -> msg) -> Cmd msg
update path token e encoder msg =
Http.request
{ method = "PUT"
, headers = [ Http.header "Authorization" token ]
, url = baseURL ++ path
, body =
encoder e
|> Http.jsonBody
, expect = expectStatus msg
, timeout = Nothing
, tracker = Nothing
}
remove : String -> String -> (Result Http.Error String -> msg) -> Cmd msg
remove path token msg =
Http.request
{ method = "DELETE"
, headers = [ Http.header "Authorization" token ]
, url = baseURL ++ path
, body = Http.emptyBody
, expect = expectStatus msg
, timeout = Nothing
, tracker = Nothing
}
-105
View File
@@ -1,105 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
module JsonMF exposing (JsonValue(..), jsonValueDecoder, jsonValueEncoder, jsonValueToString, maybeJsonValueToJsonValue, maybeJsonValueToString, stringToJsonValue, stringToMaybeJsonValue)
import Json.Decode as D
import Json.Encode as E
-- JSONVALUE
type JsonValue
= ValueObject (List ( String, JsonValue ))
| ValueArray (List JsonValue)
| ValueString String
| ValueFloat Float
| ValueInt Int
| ValueBool Bool
| ValueNull
jsonValueDecoder : D.Decoder JsonValue
jsonValueDecoder =
D.oneOf
[ D.keyValuePairs (D.lazy (\_ -> jsonValueDecoder)) |> D.map ValueObject
, D.list (D.lazy (\_ -> jsonValueDecoder)) |> D.map ValueArray
, D.int |> D.map ValueInt
, D.float |> D.map ValueFloat
, D.bool |> D.map ValueBool
, D.string |> D.map ValueString
, D.null "" |> D.map (\_ -> ValueNull)
]
stringToJsonValue : String -> Result D.Error JsonValue
stringToJsonValue jsonString =
D.decodeString jsonValueDecoder jsonString
jsonValueEncoder : JsonValue -> E.Value
jsonValueEncoder json =
case json of
ValueObject dict ->
dict
|> List.map
(\( k, v ) ->
( k, jsonValueEncoder v )
)
|> E.object
ValueArray array ->
array
|> E.list jsonValueEncoder
ValueString str ->
E.string str
ValueFloat number ->
E.float number
ValueInt number ->
E.int number
ValueBool bool ->
E.bool bool
ValueNull ->
E.null
jsonValueToString : JsonValue -> String
jsonValueToString jsonValue =
jsonValue |> jsonValueEncoder |> E.encode 1
-- String -> Maybe JsonValue -> JsonValue
stringToMaybeJsonValue : String -> Maybe JsonValue
stringToMaybeJsonValue string =
case stringToJsonValue string of
Ok jsonValue ->
Just jsonValue
Err err ->
Just ValueNull
maybeJsonValueToJsonValue : Maybe JsonValue -> JsonValue
maybeJsonValueToJsonValue maybeJsonValue =
case maybeJsonValue of
Just jsonValue ->
jsonValue
Nothing ->
ValueNull
maybeJsonValueToString : Maybe JsonValue -> String
maybeJsonValueToString maybeJsonValue =
jsonValueToString (maybeJsonValueToJsonValue maybeJsonValue)
-431
View File
@@ -1,431 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
port module Main exposing (Model, Msg(..), init, main, subscriptions, update, view)
import Bootstrap.Button as Button
import Bootstrap.ButtonGroup as ButtonGroup
import Bootstrap.CDN as CDN
import Bootstrap.Card as Card
import Bootstrap.Card.Block as Block
import Bootstrap.Form as Form
import Bootstrap.Form.Checkbox as Checkbox
import Bootstrap.Form.Fieldset as Fieldset
import Bootstrap.Form.Input as Input
import Bootstrap.Form.Radio as Radio
import Bootstrap.Form.Select as Select
import Bootstrap.Form.Textarea as Textarea
import Bootstrap.Grid as Grid
import Bootstrap.Grid.Col as Col
import Bootstrap.Grid.Row as Row
import Bootstrap.Text as Text
import Bootstrap.Utilities.Spacing as Spacing
import Browser
import Browser.Navigation as Nav
import Channel
import Connection
import Debug exposing (log)
import Error
import Helpers exposing (faIcons, fontAwesome)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
import Json.Decode exposing (Decoder, field, string)
import Json.Encode as Encode
import Message
import Thing
import Url
import Url.Parser as UrlParser exposing ((</>))
import User
import Version
-- MAIN
main : Program () Model Msg
main =
Browser.application
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
-- MODEL
type alias Model =
{ key : Nav.Key
, user : User.Model
, dashboard : Version.Model
, channel : Channel.Model
, thing : Thing.Model
, connection : Connection.Model
, message : Message.Model
, view : String
}
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init _ url key =
( Model key
User.initial
Version.initial
Channel.initial
Thing.initial
Connection.initial
Message.initial
(parse url)
, Cmd.none
)
-- URL PARSER
type alias Route =
( String, Maybe String )
parse : Url.Url -> String
parse url =
UrlParser.parse
(UrlParser.map Tuple.pair (UrlParser.string </> UrlParser.fragment identity))
url
|> (\route ->
case route of
Just r ->
Tuple.first r
Nothing ->
""
)
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
| UserMsg User.Msg
| VersionMsg Version.Msg
| ChannelMsg Channel.Msg
| ThingMsg Thing.Msg
| ConnectionMsg Connection.Msg
| MessageMsg Message.Msg
| Version
| Channels
| Things
| Connection
| Messages
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Cmd.none )
UrlChanged url ->
( { model | view = parse url }
, Cmd.none
)
UserMsg subMsg ->
updateUser model subMsg
VersionMsg subMsg ->
updateVersion model subMsg
ChannelMsg subMsg ->
updateChannel model subMsg
ThingMsg subMsg ->
updateThing model subMsg
ConnectionMsg subMsg ->
updateConnection model subMsg
MessageMsg subMsg ->
updateMessage model subMsg
Version ->
( { model | view = "dashboard" }, Cmd.none )
Things ->
( { model | view = "things" }, Cmd.none )
Channels ->
( { model | view = "channels" }, Cmd.none )
Connection ->
( { model | view = "connection" }
, Cmd.batch
[ Tuple.second (updateConnection model (Connection.ThingMsg Thing.RetrieveThings))
, Tuple.second (updateConnection model (Connection.ChannelMsg Channel.RetrieveChannels))
]
)
Messages ->
updateMessage { model | view = "messages" } (Message.ThingMsg Thing.RetrieveThings)
updateUser : Model -> User.Msg -> ( Model, Cmd Msg )
updateUser model msg =
let
( updatedUser, userCmd ) =
User.update msg model.user
in
case msg of
User.GotToken _ ->
if String.length updatedUser.token > 0 then
logIn { model | view = "dashboard" } updatedUser Version.GetVersion Thing.RetrieveThings Channel.RetrieveChannels
else
( { model | user = updatedUser }, Cmd.map UserMsg userCmd )
_ ->
( { model | user = updatedUser }, Cmd.map UserMsg userCmd )
logIn : Model -> User.Model -> Version.Msg -> Thing.Msg -> Channel.Msg -> ( Model, Cmd Msg )
logIn model user dashboardMsg thingMsg channelMsg =
let
( updatedVersion, dashboardCmd ) =
Version.update dashboardMsg model.dashboard
( updatedThing, thingCmd ) =
Thing.update thingMsg model.thing user.token
( updatedChannel, channelCmd ) =
Channel.update channelMsg model.channel user.token
in
( { model | user = user }
, Cmd.batch
[ Cmd.map VersionMsg dashboardCmd
, Cmd.map ThingMsg thingCmd
, Cmd.map ChannelMsg channelCmd
]
)
updateVersion : Model -> Version.Msg -> ( Model, Cmd Msg )
updateVersion model msg =
let
( updatedVersion, dashboardCmd ) =
Version.update msg model.dashboard
in
( { model | dashboard = updatedVersion }, Cmd.map VersionMsg dashboardCmd )
updateThing : Model -> Thing.Msg -> ( Model, Cmd Msg )
updateThing model msg =
let
( updatedThing, thingCmd ) =
Thing.update msg model.thing model.user.token
in
( { model | thing = updatedThing }, Cmd.map ThingMsg thingCmd )
updateChannel : Model -> Channel.Msg -> ( Model, Cmd Msg )
updateChannel model msg =
let
( updatedChannel, channelCmd ) =
Channel.update msg model.channel model.user.token
in
( { model | channel = updatedChannel }, Cmd.map ChannelMsg channelCmd )
updateConnection : Model -> Connection.Msg -> ( Model, Cmd Msg )
updateConnection model msg =
let
( updatedConnection, connectionCmd ) =
Connection.update msg model.connection model.user.token
in
( { model | connection = updatedConnection }, Cmd.map ConnectionMsg connectionCmd )
updateMessage : Model -> Message.Msg -> ( Model, Cmd Msg )
updateMessage model msg =
let
( updatedMessage, messageCmd ) =
Message.update msg model.message model.user.token
in
( { model | message = updatedMessage }, Cmd.map MessageMsg messageCmd )
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Sub.map UserMsg (User.subscriptions model.user)
, Sub.map MessageMsg (Message.subscriptions model.message)
]
-- VIEW
mfStylesheet : Html msg
mfStylesheet =
node "link"
[ rel "stylesheet"
, href "./css/mainflux.css"
]
[]
view : Model -> Browser.Document Msg
view model =
{ title = "Mainflux"
, body =
let
buttonAttrs =
Button.attrs [ style "text-align" "left" ]
loggedIn =
User.loggedIn model.user
menu =
if loggedIn then
[ ul [ class "nav-pills flex-column nav" ]
[ menuItem "Dashboard" Version faIcons.dashboard (model.view == "dashboard")
, menuItem "Things" Things faIcons.things (model.view == "things")
, menuItem "Channels" Channels faIcons.channels (model.view == "channels")
, menuItem "Connection" Connection faIcons.connection (model.view == "connection")
, menuItem "Messages" Messages faIcons.messages (model.view == "messages")
]
]
else
[]
header =
if loggedIn then
Html.map UserMsg (User.view model.user)
else
Grid.container [] []
content =
if loggedIn then
case model.view of
"dashboard" ->
dashboard model
"channels" ->
Html.map ChannelMsg (Channel.view model.channel)
"things" ->
Html.map ThingMsg (Thing.view model.thing)
"connection" ->
Html.map ConnectionMsg (Connection.view model.connection)
"messages" ->
Html.map MessageMsg (Message.view model.message)
_ ->
dashboard model
else
Html.map UserMsg (User.view model.user)
in
[ Grid.containerFluid []
[ CDN.stylesheet -- creates an inline style node with the Bootstrap CSS
, mfStylesheet
, fontAwesome
, Grid.row [ Row.attrs [ style "height" "100vh" ] ]
[ Grid.col
[ Col.attrs
[ style "background-color" "#113f67"
, style "padding" "0"
, style "color" "white"
]
]
[ Grid.row []
[ Grid.col
[ Col.attrs [] ]
[ h3 [ class "title" ] [ text "MAINFLUX" ] ]
]
, Grid.row []
[ Grid.col
[]
menu
]
]
, Grid.col
[ Col.xs10
, Col.attrs []
]
[ header
, Grid.row []
[ Grid.col
[ Col.attrs [] ]
[ content ]
]
]
]
]
]
}
dashboard : Model -> Html Msg
dashboard model =
Grid.container
[]
[ Grid.row []
[ Grid.col []
[ Card.deck (cardList model)
]
]
]
cardList : Model -> List (Card.Config Msg)
cardList model =
[ Card.config []
|> Card.headerH3 [] [ div [ class "table_header" ] [ i [ style "margin-right" "15px", class faIcons.version ] [], text "Version" ] ]
|> Card.block []
[ Block.titleH4 [] [ text model.dashboard.version ] ]
, Card.config []
|> Card.headerH3 [] [ div [ class "table_header" ] [ i [ style "margin-right" "15px", class faIcons.things ] [], text "Things" ] ]
|> Card.block []
[ Block.titleH4 [] [ text (String.fromInt model.thing.things.total) ]
, Block.custom <|
Button.button [ Button.secondary, Button.onClick Things ] [ text "Manage things" ]
]
, Card.config []
|> Card.headerH3 [] [ div [ class "table_header" ] [ i [ style "margin-right" "15px", class faIcons.channels ] [], text "Channels" ] ]
|> Card.block []
[ Block.titleH4 [] [ text (String.fromInt model.channel.channels.total) ]
, Block.custom <|
Button.button [ Button.secondary, Button.onClick Channels ] [ text "Manage channels" ]
]
]
menuItem : String -> Msg -> String -> Bool -> Html Msg
menuItem name msg icon active =
li [ class "nav-item", class "text-left" ] [ a [ onClick msg, classList [ ( "nav-link", True ), ( "active", active ) ] ] [ i [ class icon ] [], text name ] ]
-342
View File
@@ -1,342 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
port module Message exposing (Model, Msg(..), initial, subscriptions, update, view)
import Bootstrap.Button as Button
import Bootstrap.Card as Card
import Bootstrap.Card.Block as Block
import Bootstrap.Form as Form
import Bootstrap.Form.Checkbox as Checkbox
import Bootstrap.Form.Input as Input
import Bootstrap.Form.Radio as Radio
import Bootstrap.Grid as Grid
import Bootstrap.Grid.Col as Col
import Bootstrap.Table as Table
import Bootstrap.Utilities.Spacing as Spacing
import Channel
import Debug exposing (log)
import Error
import Helpers exposing (faIcons, fontAwesome)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
import HttpMF exposing (paths)
import Json.Decode as D
import Json.Encode as E
import List.Extra
import Ports exposing (..)
import Thing
import Url.Builder as B
type alias CheckedChannel =
{ id : String
, ws : Int
}
type alias Model =
{ message : String
, thingkey : String
, response : String
, things : Thing.Model
, channels : Channel.Model
, thingid : String
, checkedChannelsIds : List String
, checkedChannelsIdsWs : List String
, websocketIn : List String
}
initial : Model
initial =
{ message = ""
, thingkey = ""
, response = ""
, things = Thing.initial
, channels = Channel.initial
, thingid = ""
, checkedChannelsIds = []
, checkedChannelsIdsWs = []
, websocketIn = []
}
type Msg
= SubmitMessage String
| SendMessage
| Listen
| Stop
| WebsocketIn String
| RetrievedWebsockets E.Value
| SentMessage (Result Http.Error String)
| ThingMsg Thing.Msg
| ChannelMsg Channel.Msg
| SelectThing String String Channel.Msg
| CheckChannel String
resetSent : Model -> Model
resetSent model =
{ model | message = "", thingkey = "", response = "", thingid = "" }
update : Msg -> Model -> String -> ( Model, Cmd Msg )
update msg model token =
case msg of
SubmitMessage message ->
( { model | message = message }, Cmd.none )
SendMessage ->
( model
, Cmd.batch
(List.map
(\channelid -> send channelid model.thingkey model.message)
model.checkedChannelsIds
)
)
Listen ->
if List.isEmpty model.checkedChannelsIds then
( model, Cmd.none )
else
( model, Cmd.batch <| ws connectWebsocket model )
Stop ->
if List.isEmpty model.checkedChannelsIds then
( model, Cmd.none )
else
( model, Cmd.batch <| ws disconnectWebsocket model )
SentMessage result ->
case result of
Ok statusCode ->
( { model | response = statusCode }, Cmd.none )
Err error ->
( { model | response = Error.handle error }, Cmd.none )
WebsocketIn data ->
( { model | websocketIn = data :: model.websocketIn }, Cmd.none )
RetrievedWebsockets wssList ->
case D.decodeValue websocketsQueryDecoder wssList of
Ok wssL ->
if List.isEmpty wssL then
( model, Cmd.none )
else
let
l =
List.map
(\wss ->
channelIdFromUrl wss.url
)
wssL
in
( { model | checkedChannelsIdsWs = l }, Cmd.none )
Err _ ->
( model, Cmd.none )
ThingMsg subMsg ->
updateThing model subMsg token
ChannelMsg subMsg ->
updateChannel model subMsg token
SelectThing thingid thingkey channelMsg ->
updateChannel { model | thingid = thingid, thingkey = thingkey, checkedChannelsIds = [], checkedChannelsIdsWs = [] } (Channel.RetrieveChannelsForThing thingid) token
CheckChannel id ->
( { model | checkedChannelsIds = Helpers.checkEntity id model.checkedChannelsIds }, Cmd.none )
retrieveWebsocketsForThing : List Channel.Channel -> String -> Cmd Msg
retrieveWebsocketsForThing channels thingkey =
let
wssList =
List.map
(\channel ->
Websocket channel.id thingkey ""
)
channels
in
queryWebsockets (websocketsEncoder wssList)
updateThing : Model -> Thing.Msg -> String -> ( Model, Cmd Msg )
updateThing model msg token =
let
( updatedThing, thingCmd ) =
Thing.update msg model.things token
in
( { model | things = updatedThing }
, Cmd.map ThingMsg thingCmd
)
updateChannel : Model -> Channel.Msg -> String -> ( Model, Cmd Msg )
updateChannel model msg token =
let
( updatedChannel, channelCmd ) =
Channel.update msg model.channels token
checkedChannels =
updatedChannel.channels.list
in
( { model | channels = updatedChannel }
, Cmd.map ChannelMsg channelCmd
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ websocketIn WebsocketIn
, retrieveWebsockets RetrievedWebsockets
]
-- VIEW
view : Model -> Html Msg
view model =
Grid.container []
[ Grid.row []
[ Grid.col []
(Helpers.appendIf (model.things.things.total > model.things.limit)
[ Helpers.genCardConfig faIcons.things "Things" (genThingRows model) ]
(Html.map ThingMsg (Helpers.genPagination model.things.things.total (Helpers.offsetToPage model.things.offset model.things.limit) Thing.SubmitPage))
)
, Grid.col []
(Helpers.appendIf (model.channels.channels.total > model.channels.limit)
[ Helpers.genCardConfig faIcons.channels "Channels" (genChannelRows model) ]
(Html.map ChannelMsg (Helpers.genPagination model.channels.channels.total (Helpers.offsetToPage model.channels.offset model.channels.limit) Channel.SubmitPage))
)
]
, Grid.row []
[ Grid.col []
[ Card.config []
|> Card.headerH3 []
[ Grid.row []
[ Grid.col []
[ div [ class "table_header" ]
[ i [ style "margin-right" "15px", class faIcons.send ] []
, text "HTTP"
]
]
, Grid.col [ Col.attrs [ align "right" ] ]
[ Form.group []
[ Button.button [ Button.secondary, Button.attrs [ Spacing.ml1 ], Button.onClick SendMessage ] [ text "Send" ]
]
]
]
]
|> Card.block []
[ Block.custom
(Grid.row []
[ Grid.col [] [ Input.text [ Input.id "message", Input.onInput SubmitMessage ] ]
]
)
]
|> Card.view
]
, Grid.col []
[ Card.config []
|> Card.headerH3 []
[ Grid.row []
[ Grid.col []
[ div [ class "table_header" ]
[ i [ style "margin-right" "15px", class faIcons.receive ] []
, text "WS"
]
]
, Grid.col [ Col.attrs [ align "right" ] ]
[ Form.form []
[ Form.group []
[ Button.button [ Button.secondary, Button.attrs [ Spacing.ml1 ], Button.onClick Listen ] [ text "Listen" ]
, Button.button [ Button.secondary, Button.attrs [ Spacing.ml1 ], Button.onClick Stop ] [ text "Stop" ]
]
]
]
]
]
|> Card.block []
[ Block.custom
(Helpers.genOrderedList model.websocketIn)
]
|> Card.view
]
]
, Helpers.response model.response
]
genThingRows : Model -> List (Table.Row Msg)
genThingRows model =
List.map
(\thing ->
Table.tr []
[ Table.td [] [ label [] [ text (Helpers.parseString thing.name) ] ]
, Table.td [] [ text thing.id ]
, Table.td [] [ input [ type_ "radio", onClick (SelectThing thing.id thing.key (Channel.RetrieveChannelsForThing thing.id)), name "things" ] [] ]
]
)
model.things.things.list
genChannelRows : Model -> List (Table.Row Msg)
genChannelRows model =
List.map
(\channel ->
Table.tr []
[ Table.td [] [ text (" " ++ Helpers.parseString channel.name) ]
, Table.td [] [ text (channel.id ++ isInList channel.id model.checkedChannelsIdsWs) ]
, Table.td [] [ input [ type_ "checkbox", onClick (CheckChannel channel.id), checked (Helpers.isChecked channel.id model.checkedChannelsIds) ] [] ]
]
)
model.channels.channels.list
isInList : String -> List String -> String
isInList id idList =
if List.member id idList then
" *WS*"
else
""
-- HTTP
send : String -> String -> String -> Cmd Msg
send channelid thingkey message =
HttpMF.request
(B.relative [ "http", paths.channels, channelid, paths.messages ] [])
"POST"
thingkey
(Http.stringBody "application/json" message)
SentMessage
ws : (E.Value -> Cmd Msg) -> Model -> List (Cmd Msg)
ws command model =
List.map
(\channelid ->
command <| websocketEncoder (Websocket channelid model.thingkey "")
)
model.checkedChannelsIds
-94
View File
@@ -1,94 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
module ModalMF exposing (FormRecord, editModalButtons, modalDiv, modalForm, provisionModalButtons)
import Bootstrap.Button as Button
import Bootstrap.Form as Form
import Bootstrap.Form.Input as Input
import Bootstrap.Grid as Grid
import Bootstrap.Grid.Col as Col
import Bootstrap.Grid.Row as Row
import Bootstrap.Utilities.Spacing as Spacing
import Dict
import Helpers
import Html exposing (Html, div, hr, node, p, strong, text)
import Html.Attributes exposing (..)
button type_ msg txt =
Button.button [ type_, Button.attrs [ Spacing.ml1 ], Button.onClick msg ] [ text txt ]
modalDiv paragraphList =
div []
(List.map
(\paragraph ->
p []
[ strong [] [ text (Tuple.first paragraph ++ ": ") ]
, text (Tuple.second paragraph)
]
)
paragraphList
)
type alias FormRecord msg =
{ text : String
, msg : String -> msg
, placeholder : String
, value : String
}
modalForm : List (FormRecord msg) -> Html msg
modalForm formList =
Form.form []
(List.map
(\form ->
Form.group []
[ Form.label [] [ strong [] [ text form.text ] ]
, Input.text [ Input.onInput form.msg, Input.attrs [ placeholder form.placeholder, value form.value ] ]
]
)
formList
)
editModalButtons mode updateMsg editMsg cancelMsg deleteMsg closeMsg =
let
lButton1 =
if mode then
button Button.outlinePrimary updateMsg "UPDATE"
else
button Button.outlinePrimary editMsg "EDIT"
lButton2 =
if mode then
button Button.outlineDanger cancelMsg "CANCEL"
else
button Button.outlineDanger deleteMsg "DELETE"
in
Grid.row []
[ Grid.col [ Col.attrs [ align "left" ] ]
[ lButton1
, lButton2
]
, Grid.col [ Col.attrs [ align "right" ] ]
[ button Button.outlineSecondary closeMsg "CLOSE"
]
]
provisionModalButtons provisionMsg closeMsg =
Grid.row []
[ Grid.col [ Col.attrs [ align "left" ] ]
[ button Button.outlinePrimary provisionMsg "ADD"
]
, Grid.col [ Col.attrs [ align "right" ] ]
[ button Button.outlineSecondary closeMsg "CLOSE"
]
]
-88
View File
@@ -1,88 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
port module Ports exposing (Websocket, WebsocketQuery, channelIdFromUrl, connectWebsocket, disconnectWebsocket, queryWebsocket, queryWebsockets, retrieveWebsocket, retrieveWebsockets, websocketEncoder, websocketIn, websocketOut, websocketQueryDecoder, websocketsEncoder, websocketsQueryDecoder)
import Json.Decode as D
import Json.Encode as E
port connectWebsocket : E.Value -> Cmd msg
port disconnectWebsocket : E.Value -> Cmd msg
port websocketIn : (String -> msg) -> Sub msg
port websocketOut : E.Value -> Cmd msg
port queryWebsocket : E.Value -> Cmd msg
port retrieveWebsocket : (E.Value -> msg) -> Sub msg
port queryWebsockets : E.Value -> Cmd msg
port retrieveWebsockets : (E.Value -> msg) -> Sub msg
-- JSON
type alias WebsocketQuery =
{ url : String
, readyState : Int
}
type alias Websocket =
{ channelid : String
, thingkey : String
, message : String
}
websocketQueryDecoder : D.Decoder WebsocketQuery
websocketQueryDecoder =
D.map2 WebsocketQuery
(D.field "url" D.string)
(D.field "readyState" D.int)
websocketsQueryDecoder : D.Decoder (List WebsocketQuery)
websocketsQueryDecoder =
D.list websocketQueryDecoder
websocketEncoder : Websocket -> E.Value
websocketEncoder ws =
E.object
[ ( "channelid", E.string ws.channelid )
, ( "thingkey", E.string ws.thingkey )
, ( "message", E.string ws.message )
]
websocketsEncoder : List Websocket -> E.Value
websocketsEncoder wss =
E.list websocketEncoder wss
channelIdFromUrl : String -> String
channelIdFromUrl url =
let
start =
String.length "wss://localhost/ws/channels/"
end =
String.length "wss://localhost/ws/channels/"
+ String.length "0522c54b-5b00-4aab-a2b0-6e3e54320995"
in
String.slice start end url
-457
View File
@@ -1,457 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
module Thing exposing (Model, Msg(..), Thing, initial, update, view)
import Bootstrap.Button as Button
import Bootstrap.ButtonGroup as ButtonGroup
import Bootstrap.Card as Card
import Bootstrap.Card.Block as Block
import Bootstrap.Dropdown as Dropdown
import Bootstrap.Form as Form
import Bootstrap.Form.Input as Input
import Bootstrap.Grid as Grid
import Bootstrap.Grid.Col as Col
import Bootstrap.Grid.Row as Row
import Bootstrap.Modal as Modal
import Bootstrap.Table as Table
import Bootstrap.Utilities.Spacing as Spacing
import Debug exposing (log)
import Dict
import Error
import Helpers exposing (faIcons, fontAwesome)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
import HttpMF exposing (paths)
import Json.Decode as D
import Json.Encode as E
import JsonMF exposing (..)
import ModalMF
import Url.Builder as B
query =
{ offset = 0
, limit = 10
}
type alias Thing =
{ name : Maybe String
, id : String
, key : String
, metadata : Maybe JsonValue
}
emptyThing =
Thing (Just "") "" "" (Just ValueNull)
type alias Things =
{ list : List Thing
, total : Int
}
type alias Model =
{ name : String
, metadata : String
, offset : Int
, limit : Int
, response : String
, things : Things
, thing : Thing
, location : String
, editMode : Bool
, provisionModalVisibility : Modal.Visibility
, editModalVisibility : Modal.Visibility
}
initial : Model
initial =
{ name = ""
, metadata = ""
, offset = query.offset
, limit = query.limit
, response = ""
, things =
{ list = []
, total = 0
}
, thing = emptyThing
, location = ""
, editMode = False
, provisionModalVisibility = Modal.hidden
, editModalVisibility = Modal.hidden
}
type Msg
= SubmitName String
| SubmitMetadata String
| ProvisionThing
| ProvisionedThing (Result Http.Error String)
| EditThing
| UpdateThing
| UpdatedThing (Result Http.Error String)
| RetrieveThing String
| RetrievedThing (Result Http.Error Thing)
| RetrieveThings
| RetrievedThings (Result Http.Error Things)
| RemoveThing String
| RemovedThing (Result Http.Error String)
| SubmitPage Int
| ClosePorvisionModal
| CloseEditModal
| ShowProvisionModal
| ShowEditModal Thing
update : Msg -> Model -> String -> ( Model, Cmd Msg )
update msg model token =
case msg of
SubmitName name ->
( { model | name = name }, Cmd.none )
SubmitMetadata metadata ->
( { model | metadata = metadata }, Cmd.none )
SubmitPage page ->
updateThingList { model | offset = Helpers.pageToOffset page query.limit } token
ProvisionThing ->
( resetEdit model
, HttpMF.provision
(B.relative [ paths.things ] [])
token
{ emptyThing
| name = Just model.name
, metadata = stringToMaybeJsonValue model.metadata
}
thingEncoder
ProvisionedThing
"/things/"
)
ProvisionedThing result ->
case result of
Ok thingid ->
updateThingList
{ model
| thing = { emptyThing | id = thingid }
, provisionModalVisibility = Modal.hidden
, editModalVisibility = Modal.shown
}
token
Err error ->
( { model | response = Error.handle error }, Cmd.none )
EditThing ->
( { model
| editMode = True
, name = Helpers.parseString model.thing.name
, metadata = maybeJsonValueToString model.thing.metadata
}
, Cmd.none
)
UpdateThing ->
( resetEdit { model | editMode = False }
, HttpMF.update
(B.relative [ paths.things, model.thing.id ] [])
token
{ emptyThing
| name = Just model.name
, metadata =
case stringToJsonValue model.metadata of
Ok jsonValue ->
Just jsonValue
Err err ->
model.thing.metadata
}
thingEncoder
UpdatedThing
)
UpdatedThing result ->
case result of
Ok statusCode ->
updateThingList (resetEdit { model | response = statusCode }) token
Err error ->
( { model | response = Error.handle error }, Cmd.none )
RetrieveThing thingid ->
( model
, HttpMF.retrieve
(B.relative [ paths.things, thingid ] [])
token
RetrievedThing
thingDecoder
)
RetrievedThing result ->
case result of
Ok thing ->
( { model | thing = thing }, Cmd.none )
Err error ->
( { model | response = Error.handle error }, Cmd.none )
RetrieveThings ->
( model
, HttpMF.retrieve
(B.relative [ paths.things ] (Helpers.buildQueryParamList model.offset model.limit))
token
RetrievedThings
thingsDecoder
)
RetrievedThings result ->
case result of
Ok things ->
( { model | things = things }, Cmd.none )
Err error ->
( { model | response = Error.handle error }, Cmd.none )
RemoveThing id ->
( model
, HttpMF.remove
(B.relative [ paths.things, id ] [])
token
RemovedThing
)
RemovedThing result ->
case result of
Ok statusCode ->
updateThingList
{ model
| response = statusCode
, offset = Helpers.validateOffset model.offset model.things.total query.limit
, editModalVisibility = Modal.hidden
}
token
Err error ->
( { model | response = Error.handle error }, Cmd.none )
ClosePorvisionModal ->
( resetEdit { model | provisionModalVisibility = Modal.hidden }, Cmd.none )
CloseEditModal ->
( resetEdit { model | editModalVisibility = Modal.hidden }, Cmd.none )
ShowProvisionModal ->
( { model | provisionModalVisibility = Modal.shown }
, Cmd.none
)
ShowEditModal thing ->
( { model
| editModalVisibility = Modal.shown
, thing = thing
, editMode = False
}
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
Grid.container []
(Helpers.appendIf (model.things.total > model.limit)
[ genTable model, provisionModal model, editModal model ]
(Helpers.genPagination model.things.total (Helpers.offsetToPage model.offset model.limit) SubmitPage)
)
-- Things table
genTable : Model -> Html Msg
genTable model =
Grid.row []
[ Grid.col []
[ Card.config []
|> Card.header []
[ Grid.row []
[ Grid.col [ Col.attrs [ align "left" ] ]
[ h3 [] [ div [ class "table_header" ] [ i [ style "margin-right" "15px", class faIcons.things ] [], text "Things" ] ]
]
, Grid.col [ Col.attrs [ align "right" ] ]
[ Button.button [ Button.secondary, Button.attrs [ align "right" ], Button.onClick ShowProvisionModal ] [ text "ADD" ]
]
]
]
|> Card.block []
[ Block.custom
(Table.table
{ options = [ Table.striped, Table.hover, Table.small ]
, thead = genTableHeader
, tbody = genTableBody model
}
)
]
|> Card.view
]
]
genTableHeader : Table.THead Msg
genTableHeader =
Table.simpleThead
[ Table.th [] [ text "Name" ]
, Table.th [] [ text "ID" ]
]
genTableBody : Model -> Table.TBody Msg
genTableBody model =
Table.tbody []
(List.map
(\thing ->
Table.tr [ Table.rowAttr (onClick (ShowEditModal thing)) ]
[ Table.td [] [ text (Helpers.parseString thing.name) ]
, Table.td [] [ text thing.id ]
]
)
model.things.list
)
provisionModal : Model -> Html Msg
provisionModal model =
Modal.config ClosePorvisionModal
|> Modal.large
|> Modal.hideOnBackdropClick True
|> Modal.h4 [] [ text "Add thing" ]
|> provisionModalBody model
|> Modal.view model.provisionModalVisibility
provisionModalBody : Model -> (Modal.Config Msg -> Modal.Config Msg)
provisionModalBody model =
Modal.body []
[ Grid.container []
[ Grid.row [] [ Grid.col [] [ provisionModalForm model ] ]
, ModalMF.provisionModalButtons ProvisionThing ClosePorvisionModal
]
]
provisionModalForm : Model -> Html Msg
provisionModalForm model =
ModalMF.modalForm
[ ModalMF.FormRecord "name" SubmitName model.name model.name
, ModalMF.FormRecord "metadata" SubmitMetadata model.metadata model.metadata
]
-- Edit modal
editModal : Model -> Html Msg
editModal model =
Modal.config CloseEditModal
|> Modal.large
|> Modal.hideOnBackdropClick True
|> Modal.h4 [] [ text (Helpers.parseString model.thing.name) ]
|> editModalBody model
|> Modal.view model.editModalVisibility
editModalBody : Model -> (Modal.Config Msg -> Modal.Config Msg)
editModalBody model =
Modal.body []
[ Grid.container []
[ Grid.row []
[ Grid.col []
[ editModalForm model
, ModalMF.modalDiv [ ( "id", model.thing.id ), ( "key", model.thing.key ) ]
]
]
, ModalMF.editModalButtons model.editMode UpdateThing EditThing (ShowEditModal model.thing) (RemoveThing model.thing.id) CloseEditModal
]
]
editModalForm : Model -> Html Msg
editModalForm model =
if model.editMode then
ModalMF.modalForm
[ ModalMF.FormRecord "name" SubmitName (Helpers.parseString model.thing.name) model.name
, ModalMF.FormRecord "metadata" SubmitMetadata (maybeJsonValueToString model.thing.metadata) model.metadata
]
else
ModalMF.modalDiv [ ( "name", Helpers.parseString model.thing.name ), ( "metadata", maybeJsonValueToString model.thing.metadata ) ]
-- JSON
thingDecoder : D.Decoder Thing
thingDecoder =
D.map4 Thing
(D.maybe (D.field "name" D.string))
(D.field "id" D.string)
(D.field "key" D.string)
(D.maybe (D.field "metadata" jsonValueDecoder))
thingsDecoder : D.Decoder Things
thingsDecoder =
D.map2 Things
(D.field "things" (D.list thingDecoder))
(D.field "total" D.int)
thingEncoder : Thing -> E.Value
thingEncoder thing =
E.object
[ ( "name", E.string (Helpers.parseString thing.name) )
, ( "metadata", jsonValueEncoder (maybeJsonValueToJsonValue thing.metadata) )
]
-- HELPERS
resetEdit : Model -> Model
resetEdit model =
{ model | name = "", metadata = "" }
updateThingList : Model -> String -> ( Model, Cmd Msg )
updateThingList model token =
( model
, Cmd.batch
[ HttpMF.retrieve
(B.relative [ paths.things ] (Helpers.buildQueryParamList model.offset model.limit))
token
RetrievedThings
thingsDecoder
, HttpMF.retrieve
(B.relative [ paths.things, model.thing.id ] [])
token
RetrievedThing
thingDecoder
]
)
-196
View File
@@ -1,196 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
module User exposing (Model, Msg(..), initial, loggedIn, subscriptions, update, view)
import Bootstrap.Button as Button
import Bootstrap.Dropdown as Dropdown
import Bootstrap.Form as Form
import Bootstrap.Form.Input as Input
import Bootstrap.Grid as Grid
import Bootstrap.Grid.Col as Col
import Bootstrap.Utilities.Spacing as Spacing
import Error
import Helpers
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http
import HttpMF exposing (baseURL, paths)
import Json.Decode as D
import Json.Encode as E
import Url.Builder as B
type alias Model =
{ email : String
, password : String
, token : String
, response : String
, dropState : Dropdown.State
}
initial : Model
initial =
{ email = ""
, password = ""
, token = ""
, response = ""
, dropState = Dropdown.initialState
}
type Msg
= SubmitEmail String
| SubmitPassword String
| Create
| Created (Result Http.Error String)
| GetToken
| GotToken (Result Http.Error String)
| DropState Dropdown.State
| LogOut
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SubmitEmail email ->
( { model | email = email }, Cmd.none )
SubmitPassword password ->
( { model | password = password }, Cmd.none )
Create ->
( model
, HttpMF.user
model.email
model.password
(B.relative [ paths.users ] [])
(encode (User model.email model.password))
(HttpMF.expectStatus Created)
)
Created result ->
case result of
Ok statusCode ->
( { model | response = statusCode }, Cmd.none )
Err error ->
( { model | response = Error.handle error }, Cmd.none )
GetToken ->
( model
, HttpMF.user
model.email
model.password
(B.relative [ paths.tokens ] [])
(encode (User model.email model.password))
(HttpMF.expectRetrieve
GotToken
(D.field "token" D.string)
)
)
GotToken result ->
case result of
Ok token ->
( { model | token = token, response = "" }, Cmd.none )
Err error ->
( { model | token = "", response = Error.handle error }, Cmd.none )
DropState state ->
( { model | dropState = state }, Cmd.none )
LogOut ->
( { model | email = "", password = "", token = "", response = "" }, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
if loggedIn model then
Grid.row []
[ Grid.col [ Col.attrs [ align "right" ] ]
[ Dropdown.dropdown
model.dropState
{ options = []
, toggleMsg = DropState
, toggleButton =
Dropdown.toggle [ Button.warning ] [ text model.email ]
, items =
[ Dropdown.buttonItem [ onClick LogOut ] [ text "logout" ]
]
}
]
]
else
Grid.container []
[ Grid.row []
[ Grid.col []
[ Form.form []
[ Form.group []
[ Form.label [ for "email" ] [ text "Email address" ]
, Input.email [ Input.id "email", Input.onInput SubmitEmail ]
]
, Form.group []
[ Form.label [ for "pwd" ] [ text "Password" ]
, Input.password [ Input.id "pwd", Input.onInput SubmitPassword ]
]
, Button.button [ Button.primary, Button.attrs [ Spacing.ml1 ], Button.onClick Create ] [ text "Register" ]
, Button.button [ Button.primary, Button.attrs [ Spacing.ml1 ], Button.onClick GetToken ] [ text "Log in" ]
]
]
]
, Helpers.response model.response
]
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Dropdown.subscriptions model.dropState DropState ]
-- JSON
type alias User =
{ email : String
, password : String
}
encode : User -> E.Value
encode user =
E.object
[ ( "email", E.string user.email )
, ( "password", E.string user.password )
]
decoder : D.Decoder User
decoder =
D.map2 User
(D.field "email" D.string)
(D.field "password" D.string)
-- HTTP
loggedIn : Model -> Bool
loggedIn model =
if String.length model.token > 0 then
True
else
False
-48
View File
@@ -1,48 +0,0 @@
-- Copyright (c) Mainflux
-- SPDX-License-Identifier: Apache-2.0
module Version exposing (Model, Msg(..), initial, update)
import Error
import Html exposing (..)
import Html.Attributes exposing (..)
import Http
import HttpMF exposing (paths)
import Json.Decode as D
import Json.Encode as E
import Url.Builder as B
type alias Model =
{ version : String }
initial : Model
initial =
{ version = "" }
type Msg
= GetVersion
| GotVersion (Result Http.Error String)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GetVersion ->
( model
, HttpMF.version
(B.relative [ paths.version ] [])
GotVersion
(D.field "version" D.string)
)
GotVersion result ->
case result of
Ok version ->
( { model | version = version }, Cmd.none )
Err error ->
( { model | version = Error.handle error }, Cmd.none )
-86
View File
@@ -1,86 +0,0 @@
var wss = new Object();
MF.log = function(msg) {
console.log(msg);
app.ports.websocketIn.send(msg);
}
MF.url = function(data) {
var wsProtocol = 'ws';
if (window.location.protocol == 'https:') {
wsProtocol = 'wss'
}
return wsProtocol + '://' + window.location.hostname + '/ws/channels/' + data.channelid + '/messages?authorization=' + data.thingkey
}
app.ports.connectWebsocket.subscribe(function(data) {
var url = MF.url(data);
if (wss[url]) {
MF.log('Websocket already open. URL: ' + url );
return;
}
var ws = new WebSocket(url);
ws.onopen = function (event) {
MF.log('Websocket opened. URL: ' + url);
wss[url] = ws;
}
ws.onerror = function (event) {
console.log(event);
}
ws.onmessage = function(message) {
app.ports.websocketIn.send(JSON.stringify({data: message.data, timestamp: message.timeStamp}));
};
ws.onclose = function (event) {
MF.log('Websocket closed. URL: ' + url);
delete wss[ws.url];
};
});
if (typeof app.ports.websocketOut !== 'undefined') {
app.ports.websocketOut.subscribe(function(data) {
var url = MF.url(data);
if (wss[url]) {
wss[url].send(data.message);
} else {
MF.log('Message not sent. Websocket is not open. URL: ' + url);
}
})
}
app.ports.disconnectWebsocket.subscribe(function(data) {
var url = MF.url(data);
if (wss[url]) {
wss[url].close();
} else {
MF.log('Websocket not disconnected. Websocket is not open. URL: ' + url);
}
})
if (typeof app.ports.queryWebsocket !== 'undefined') {
app.ports.queryWebsocket.subscribe(function(data) {
var url = MF.url(data);
if (wss[url]) {
app.ports.retrieveWebsocket.send({url: url, readyState : wss[url].readyState});
} else {
app.ports.retrieveWebsocket.send({url: '', readyState : -1})
}
})
}
if (typeof app.ports.queryWebsockets !== 'undefined') {
app.ports.queryWebsockets.subscribe(function(data) {
var wssList = []
data.forEach(function(item, index){
var url = MF.url(item);
if (wss[url]) {
wssList.push({url: url, readyState : wss[url].readyState})
}
})
app.ports.retrieveWebsockets.send(wssList);
})
}