mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 06:50:18 +00:00
NOISSUE - Remove Elm UI (#953)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
This commit is contained in:
committed by
Drasko DRASKOVIC
parent
5e35cbe06b
commit
ac3ff5221a
@@ -1,7 +1,6 @@
|
||||
.git
|
||||
.github
|
||||
build
|
||||
ui
|
||||
docker
|
||||
docs
|
||||
k8s
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
elm-stuff
|
||||
main.js
|
||||
-25
@@ -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
|
||||
@@ -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/).
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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;"
|
||||
@@ -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
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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 = ""
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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 ] ]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
]
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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 )
|
||||
@@ -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);
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user