mirror of
https://github.com/mainflux/mainflux.git
synced 2025-04-24 13:48:49 +08:00
MF-571 - Elm UI (#632)
* Initial commit Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add user create form Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add Makefile and README Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove elm-stuff Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add url parsing Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add channels retrive and remove Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Refactor request f and add f annotations Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add things provision Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add thing retrieve and remove Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Rename Channel.elm methods Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Fix trailing slash and add url constants Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add module User with separate model, view and update Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Modularize channels Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Modularize version and things Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Refactor module methods Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Modularize messaging Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add channel pagination Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Fix initial channel model Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add access control (connect and disconnect things from channel) Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add pagination to Things Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Using Url.Builder in connection and messaging Rename Acces module to Connection Rename attribute in Bootstrap code Remove debug logging Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add login Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add dynamic menu Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Fix empty name channel/thing bug Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove menu from login screen and add response helper Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add version, channels, things direct display Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add channels table Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Change var names and add refreshChannelList helper Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add Things list Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Fix response behavior Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove version page button Signed-off-by: drasko <drasko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Move everything to ui folder Signed-off-by: drasko <drasko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove dashflux Signed-off-by: drasko <drasko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * MF-571 Add things and channels lists to Connection module (#580) * Add thing and channel lists to Connection module Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Fix form reset bug & remember checkbox status in Connection module Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * MF-571 - Add batch things-channels connection modification (#588) * Add batch things-channels connection modification Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove unused token arg and reposition buttons Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * NOISSUE - messaging in UI (#578) * Add thing and channel lists to Connection module Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * init commit Signed-off-by: Ivan Milošević <iva@blokovi.com> * list channels and things Signed-off-by: Ivan Milošević <iva@blokovi.com> * list channels with radio buttons Signed-off-by: Ivan Milošević <iva@blokovi.com> * Listing channels for selected thing Signed-off-by: Ivan Milošević <iva@blokovi.com> * fix list channels for selected thing Signed-off-by: Ivan Milošević <iva@blokovi.com> * Use records instead of hardcode values Use genFormField from Helpers Remove dead comments Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * NOISSUE - Add Docker build for UI (#582) * Add Docker build for UI Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com> * Fix entrypoint and ports Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com> * Add NginX server. Fix port. Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * NOISSUE - Fix layout and add custom CSS (#593) * Fix layout and add custom CSS Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com> * Remove comments, add copyright headers Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com> * Add newline at the end of the css file Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * NOISSUE -update message module (#599) * Update message module Sending message as selected thing to multiple channels Signed-off-by: Ivan Milošević <iva@blokovi.com> * Rename token to thingkey Signed-off-by: Ivan Milošević <iva@blokovi.com> * Reset model on send message and reset list of channels on selecting thing Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove logout button and add logout header (#604) * Remove logout button and add logout header * Removed dead code. Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * MF-571 - Pagination (#606) * Add total # things to Thing model Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add basic pagination for Thing Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add pagination helper function & Channel pagination Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * NOISSUE - Add bootstrap cards in version view (#607) * Add total # things to Thing model Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add basic pagination for Thing Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add bootstrap cards in version view Signed-off-by: Ivan Milošević <iva@blokovi.com> * Add pagination helper function & Channel pagination Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * modify styling Signed-off-by: Ivan Milošević <iva@blokovi.com> * Styling Signed-off-by: Ivan Milošević <iva@blokovi.com> * Mainflux logo Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Revert "NOISSUE - Add bootstrap cards in version view (#607)" This reverts commit a9a4dd7c730de453c45b64b51712c81e9befbeec. Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Pagination (#613) Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * MF-571 - Add dashboard (#614) * Add Dashboard module and simplify Main module Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove unused code and Version module Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add helper funcs to Main.elm update method Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Improve Main and Message module organization Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add dashboard Things and Channels buttons Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add Modal to edit thing Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add RemoveThing msg and AwesomeFont Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Unify provision and retrieve request Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Reorganize code by making funcs and code repositioning Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add fon-awesome 5.* support Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add metadata edit Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove table header add thing and removed dead code Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * MF-571 - UI modal buttons (#617) * Add Dashboard module and simplify Main module Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove unused code and Version module Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add helper funcs to Main.elm update method Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Improve Main and Message module organization Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add dashboard Things and Channels buttons Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add Modal to edit thing Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add RemoveThing msg and AwesomeFont Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Unify provision and retrieve request Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Reorganize code by making funcs and code repositioning Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add fon-awesome 5.* support Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add metadata edit Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove table header add thing and removed dead code Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add complete Thing edit modal buttons Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add card style to channels (#618) Add dropdown user menu Add font awesome to main menu Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * MF-571 - UI Thing module with modals (#620) * Add Thing provision modal Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add app/dev dropdown to Thing provision modal Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Refactor Thing to have separate Update and Provision msg handling Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove fontAwesome import and fix Dict import Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * MF-571 - UI channel modals (#621) * Add Thing provision modal Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add app/dev dropdown to Thing provision modal Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Refactor Thing to have separate Update and Provision msg handling Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove fontAwesome import and fix Dict import Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add provision and edit modals to Channel Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add HttpMF http custom library Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add ModalMF Bootstrap modal custom library Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Moved all http funcs to HttpMF Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Moved expect funcs from Connection and Message to HttpMF Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add cards and change tables (#622) * Change type of tables Add tables to cards Colorize buttons Signed-off-by: Ivan Milošević <iva@blokovi.com> * remove comments Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * MF-571 - Fix user login and refactor User (#623) * Fix user login and refactor User Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Remove redundant Grid.row nesting Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Fix links in README (#624) Signed-off-by: Ivan Milošević <iva@blokovi.com> Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Fix ui/Makefile and ui/README.md Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add copyright header to .elm files Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Fix missing README.md after migration Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Fix typo Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Add install options to README.md Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com> * Rename Url to URL and Id to ID Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
This commit is contained in:
parent
dbd8f9be23
commit
3255f32a9c
@ -1,7 +1,7 @@
|
||||
.git
|
||||
.github
|
||||
build
|
||||
dashflux
|
||||
ui
|
||||
docker
|
||||
docs
|
||||
k8s
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
build
|
||||
|
||||
site/
|
||||
|
||||
|
26
Makefile
26
Makefile
@ -1,3 +1,8 @@
|
||||
## Copyright (c) 2015-2019
|
||||
## Mainflux
|
||||
##
|
||||
## SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http normalizer ws coap lora influxdb-writer influxdb-reader mongodb-writer mongodb-reader cassandra-writer cassandra-reader cli bootstrap
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
@ -19,7 +24,7 @@ endef
|
||||
|
||||
all: $(SERVICES) mqtt
|
||||
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt ui
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
@ -57,15 +62,23 @@ $(SERVICES):
|
||||
$(DOCKERS):
|
||||
$(call make_docker,$(@))
|
||||
|
||||
dockers: $(DOCKERS)
|
||||
docker build --tag=mainflux/dashflux -f dashflux/docker/Dockerfile dashflux
|
||||
docker_ui:
|
||||
$(MAKE) -C ui docker
|
||||
|
||||
docker_mqtt:
|
||||
# MQTT Docker build must be done from root dir because it copies .proto files
|
||||
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile .
|
||||
|
||||
dockers: $(DOCKERS) docker_ui docker_mqtt
|
||||
|
||||
$(DOCKERS_DEV):
|
||||
$(call make_docker_dev,$(@))
|
||||
|
||||
dockers_dev: $(DOCKERS_DEV)
|
||||
|
||||
ui:
|
||||
$(MAKE) -C ui
|
||||
|
||||
mqtt:
|
||||
cd mqtt && npm install
|
||||
|
||||
@ -73,7 +86,7 @@ define docker_push
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc:$(1); \
|
||||
done
|
||||
docker push mainflux/dashflux:$(1)
|
||||
docker push mainflux/ui:$(1)
|
||||
docker push mainflux/mqtt:$(1)
|
||||
endef
|
||||
|
||||
@ -90,7 +103,7 @@ release:
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
done
|
||||
docker tag mainflux/dashflux mainflux/dashflux:$(version)
|
||||
docker tag mainflux/ui mainflux/ui:$(version)
|
||||
docker tag mainflux/mqtt mainflux/mqtt:$(version)
|
||||
$(call docker_push,$(version))
|
||||
|
||||
@ -100,6 +113,9 @@ rundev:
|
||||
run:
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
|
||||
runui:
|
||||
$(MAKE) -C ui run
|
||||
|
||||
runlora:
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
docker-compose -f docker/addons/influxdb-writer/docker-compose.yml up -d
|
||||
|
@ -1,60 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"name": "mainflux-ui"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.png"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "app",
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"project": "src/tsconfig.app.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "src/tsconfig.spec.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"component": {}
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
58
dashflux/.gitignore
vendored
58
dashflux/.gitignore
vendored
@ -1,58 +0,0 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/bower_components
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
/.vscode
|
||||
/out-tsc
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage/*
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
/e2e/*.map
|
||||
|
||||
#System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
#json-server database
|
||||
db.json
|
||||
db.*
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
201
dashflux/LICENSE
201
dashflux/LICENSE
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,73 +0,0 @@
|
||||
# Mainflux UI Dashboad
|
||||
|
||||
Mainflux UI, dashboard for [Mainflux](https://github.com/mainflux/mainflux) Industrial IoT Messaging and Device Management Server.
|
||||
|
||||
> **N.B.** Mainflux UI service is WIP and not suitable for deployment at this moment. You are welcome to contribute and improve it.
|
||||
> ## Development
|
||||
>- Follow angular-cli [documentation](https://github.com/angular/angular-cli)
|
||||
>- Follow [official angular style guide](https://angular.io/styleguide)
|
||||
|
||||
## Requirements
|
||||
|
||||
You'll need the following software installed to get started.
|
||||
|
||||
- [Node](https://nodejs.org/en/) 6 or higher, we recommend current LTS version, together with NPM 3 or higher.
|
||||
- [Angular-cli](https://github.com/angular/angular-cli) Newest version with Webpack integration
|
||||
- - Depending on how Node is configured on your machine, you may need to run installation command with `sudo`
|
||||
- [Git](http://git-scm.com/downloads): Use the installer for your OS.
|
||||
- Windows users can also try [Git for Windows](http://git-for-windows.github.io/).
|
||||
- For local Development with [Mainflux composition](https://github.com/mainflux/mainflux) running locally, [Chrome extension for Cross origin](https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi?utm_source=chrome-app-launcher-info-dialog) is required. Because composition is running on different port then our Angular app, we have cross origin.
|
||||
|
||||
## Configuration
|
||||
|
||||
Change into the directory.
|
||||
|
||||
```bash
|
||||
cd dashflux
|
||||
```
|
||||
|
||||
Install the dependencies. If you're running Mac OS or Linux, you may need to run `sudo npm install` instead, depending on how your machine is configured.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Set appropriate endpoint URLs in **environment.ts** (for local development will probably be 0.0.0.0:<_port_>) or **environment.prod.ts** for production.
|
||||
|
||||
To start the server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
This will run and assemble our app.
|
||||
**Now go to `localhost:4200` in your browser to see it in action.**
|
||||
|
||||
## Deployment
|
||||
|
||||
Dashflux is distributed as Docker container. We use nginx to serve dashflux from docker container, supporting environments using docker multi-stage builds.
|
||||
Dashflux docker image is available on [Dockerhub mainflux/dashflux](https://hub.docker.com/r/mainflux/dashflux/)
|
||||
|
||||
If you want to build image locally, you can build image using the **development** environment:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile -t dashflux:dev --build-arg env=dev .
|
||||
```
|
||||
|
||||
Build image using the **production** environment:
|
||||
|
||||
```bash
|
||||
docker build -t dashflux:prod -f ./docker/Dockerfile .
|
||||
```
|
||||
|
||||
**Note:** before running *docker build* command, please make sure appropriate endpoint URLs in *environment.ts* or *environment.prod.ts* are set up.
|
||||
|
||||
You can test image running
|
||||
|
||||
```bash
|
||||
docker run -p 80:80 dashflux:dev
|
||||
```
|
||||
|
||||
This will run dashflux in docker container.
|
||||
|
||||
Now go to `http://localhost` in your browser to see it in action.
|
@ -1 +0,0 @@
|
||||
node_modules
|
@ -1,13 +0,0 @@
|
||||
# Stage 0, based on Node.js, to build and compile Angular
|
||||
FROM node:9.5 as node
|
||||
WORKDIR /app
|
||||
COPY package.json /app/
|
||||
RUN npm install
|
||||
COPY ./ /app/
|
||||
ARG env=prod
|
||||
RUN npm run build -- --prod --environment $env
|
||||
|
||||
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
|
||||
FROM nginx:1.13-alpine
|
||||
COPY --from=node /app/dist/ /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
@ -1,14 +0,0 @@
|
||||
import { AppPage } from './app.po';
|
||||
|
||||
describe('mainflux-ui App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getParagraphText()).toEqual('Welcome to app!');
|
||||
});
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
],
|
||||
client:{
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
reports: [ 'html', 'lcovonly' ],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
angularCli: {
|
||||
environment: 'dev'
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['ChromeHeadless'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
10394
dashflux/package-lock.json
generated
10394
dashflux/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,57 +0,0 @@
|
||||
{
|
||||
"name": "mainflux-ui",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache License, version 2.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy-config.json",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^5.2.4",
|
||||
"@angular/common": "^5.2.4",
|
||||
"@angular/compiler": "^5.2.4",
|
||||
"@angular/core": "^5.2.4",
|
||||
"@angular/flex-layout": "2.0.0-beta.10-4905443",
|
||||
"@angular/forms": "^5.2.4",
|
||||
"@angular/http": "^5.2.4",
|
||||
"@angular/platform-browser": "^5.2.4",
|
||||
"@angular/platform-browser-dynamic": "^5.2.4",
|
||||
"@angular/router": "^5.2.4",
|
||||
"core-js": "^2.4.1",
|
||||
"mobx": "^3.5.1",
|
||||
"mobx-angular": "^2.1.1",
|
||||
"ngx-auth": "^2.2.0",
|
||||
"rxjs": "^5.5.2",
|
||||
"zone.js": "^0.8.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cdk": "^5.2.0",
|
||||
"@angular/cli": "^1.7.3",
|
||||
"@angular/compiler-cli": "^5.2.4",
|
||||
"@angular/language-service": "^5.2.4",
|
||||
"@angular/material": "^5.2.0",
|
||||
"@types/jasmine": "~2.5.53",
|
||||
"@types/jasminewd2": "~2.0.2",
|
||||
"@types/node": "^6.0.100",
|
||||
"codelyzer": "~3.2.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"jasmine-core": "~2.5.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
"karma-chrome-launcher": "~2.1.1",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~3.2.0",
|
||||
"tslint": "~5.7.0",
|
||||
"typescript": "~2.4.2"
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./e2e/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: 'e2e/tsconfig.e2e.json'
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"/api/*": {
|
||||
"target": "http://localhost:8180",
|
||||
"secure":false,
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Route, RouterModule } from '@angular/router';
|
||||
import { ProtectedGuard, PublicGuard } from 'ngx-auth';
|
||||
|
||||
import { LoginComponent } from './components/auth/login/login.component';
|
||||
import { SignupComponent } from './components/auth/signup/signup.component';
|
||||
import { ChannelsComponent } from './components/channels/channels.component';
|
||||
import { ThingsComponent } from './components/things/things.component';
|
||||
|
||||
const routes: Route[] = [
|
||||
{ path: '', redirectTo: 'things', pathMatch: 'full'},
|
||||
{ path: 'login', component: LoginComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'signup', component: SignupComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'things', component: ThingsComponent, canActivate: [ProtectedGuard]},
|
||||
{ path: 'channels', component: ChannelsComponent, canActivate: [ProtectedGuard]}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forRoot(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule { }
|
@ -1,22 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<div *mobxAutorun>
|
||||
<div class="loading" *ngIf="uiStore.loading" fxLayoutAlign="center center">
|
||||
<mat-progress-spinner [mode]="'indeterminate'"></mat-progress-spinner>
|
||||
</div>
|
||||
<mat-toolbar color="primary" class="mat-elevation-z6">
|
||||
<h1 fxFlex>Mainflux</h1>
|
||||
<ng-container *ngIf="authStore.isAuthenticated">
|
||||
<a [routerLink]="['/things']" mat-button>Things</a>
|
||||
<a [routerLink]="['/channels']" mat-button>Channels</a>
|
||||
<a (click)="logout()" mat-button>Logout</a>
|
||||
</ng-container>
|
||||
|
||||
</mat-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
@ -1,13 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { MaterialModule } from './core/material/material.module';
|
||||
import { AuthenticationService } from './core/services/auth/authentication.service';
|
||||
import { TokenStorage } from './core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from './core/services/channels/channels.service';
|
||||
import { ThingsService } from './core/services/things/things.service';
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { reaction } from 'mobx';
|
||||
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(
|
||||
private snackBar: MatSnackBar,
|
||||
public uiStore: UiStore,
|
||||
public authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
reaction(() => this.authStore.authError, (authError) => {
|
||||
if (authError) {
|
||||
this.snackBar.open(authError, '', {
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authStore.logout();
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import './rxjs-extensions.ts';
|
||||
import 'hammerjs';
|
||||
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MobxAngularModule } from 'mobx-angular';
|
||||
import { AUTH_SERVICE, AuthModule, PROTECTED_FALLBACK_PAGE_URI, PUBLIC_FALLBACK_PAGE_URI } from 'ngx-auth';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { LoginComponent } from './components/auth/login/login.component';
|
||||
import { SignupComponent } from './components/auth/signup/signup.component';
|
||||
import { ChannelDialogComponent } from './components/channels/channel-dialog/channel-dialog.component';
|
||||
import { ChannelsComponent } from './components/channels/channels.component';
|
||||
import { ThingDialogComponent } from './components/things/thing-dialog/thing-dialog.component';
|
||||
import { ThingsComponent } from './components/things/things.component';
|
||||
import { ConfirmationDialogComponent } from './components/shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { MaterialModule } from './core/material/material.module';
|
||||
import { AuthenticationService } from './core/services/auth/authentication.service';
|
||||
import { TokenStorage } from './core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from './core/services/channels/channels.service';
|
||||
import { ThingsService } from './core/services/things/things.service';
|
||||
import { MockAuthService } from './core/services/mock-auth.service';
|
||||
import { MockChannelsService } from './core/services/mock-channels.service';
|
||||
import { MockThingsService } from './core/services/mock-things.service';
|
||||
import { ChannelsStore } from './core/store/channels.store';
|
||||
import { ThingsStore } from './core/store/things.store';
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
import { UnauthorizedInterceptor } from './core/services/auth/unauthorized.interceptor';
|
||||
|
||||
export function factory(authenticationService: AuthenticationService) {
|
||||
return authenticationService;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ThingsComponent,
|
||||
ChannelsComponent,
|
||||
SignupComponent,
|
||||
LoginComponent,
|
||||
ThingDialogComponent,
|
||||
ConfirmationDialogComponent,
|
||||
ChannelDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
AuthModule,
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
MaterialModule,
|
||||
FlexLayoutModule,
|
||||
ReactiveFormsModule,
|
||||
MobxAngularModule,
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
ThingsStore,
|
||||
ChannelsStore,
|
||||
AuthStore,
|
||||
MockAuthService,
|
||||
MockThingsService,
|
||||
MockChannelsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
{ provide: PROTECTED_FALLBACK_PAGE_URI, useValue: '/' },
|
||||
{ provide: PUBLIC_FALLBACK_PAGE_URI, useValue: '/login' },
|
||||
{
|
||||
provide: AUTH_SERVICE,
|
||||
deps: [AuthenticationService],
|
||||
useFactory: factory
|
||||
},
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true },
|
||||
{ provide: APP_BASE_HREF, useValue: '/app/' }
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
entryComponents: [
|
||||
ThingDialogComponent,
|
||||
ChannelDialogComponent,
|
||||
ConfirmationDialogComponent
|
||||
]
|
||||
})
|
||||
export class AppModule { }
|
@ -1,35 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<div class="container" style="height: 80vh" fxLayoutAlign="center center">
|
||||
<form fxLayout="column" [formGroup]="loginForm" (ngSubmit)="login()" fxFlex="30%" fxFlex.sm="70%" fxFlex.xs="90%">
|
||||
<mat-card>
|
||||
<mat-card-title>Login</mat-card-title>
|
||||
<mat-card-content class="loginCard" fxLayout="column" fxLayoutAlign="space-evenly">
|
||||
<mat-form-field fxFlex="40%">
|
||||
<input type="text" class="emailInput" required matInput placeholder="Email" formControlName="email"/>
|
||||
<mat-error *ngIf="loginForm.get('email').errors?.required">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="loginForm.get('email').errors?.email">
|
||||
Email must be valid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%">
|
||||
<input type="password" class="passwordInput" required matInput placeholder="Password" formControlName="password" />
|
||||
<mat-error *ngIf="loginForm.get('password').errors?.required">
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions fxLayout="column">
|
||||
<button type="submit" [disabled]="loginForm.invalid" mat-raised-button color="primary" class="loginButton">Login</button>
|
||||
<button mat-button (click)="signup()" class="signupButton">Sign Up with Email</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
</div>
|
@ -1,10 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { MaterialModule } from '../../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
|
||||
import { LoginComponent } from './login.component';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call the store goToSignup when clicked on Sign Up with Email', inject([UiStore], (store: UiStore) => {
|
||||
const signupButton = fixture.debugElement.nativeElement.querySelector('.signupButton');
|
||||
const signupSpy = spyOn(store, 'goToSignup').and.stub();
|
||||
|
||||
signupButton.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(signupSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
loginForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private uiStore: UiStore,
|
||||
private authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.loginForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required]],
|
||||
});
|
||||
}
|
||||
|
||||
login() {
|
||||
this.authStore.login(this.getUserDataFromForm());
|
||||
}
|
||||
|
||||
signup() {
|
||||
this.uiStore.goToSignup();
|
||||
}
|
||||
|
||||
getUserDataFromForm() {
|
||||
return {
|
||||
email: this.loginForm.get('email').value,
|
||||
password: this.loginForm.get('password').value
|
||||
};
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<div class="container" style="height: 80vh" fxLayoutAlign="center center">
|
||||
<form fxLayout="column" [formGroup]="signupForm" (ngSubmit)="signup()" fxFlex="30%" fxFlex.sm="70%" fxFlex.xs="90%">
|
||||
<mat-card>
|
||||
<mat-card-title>Signup</mat-card-title>
|
||||
<mat-card-content class="loginCard" fxLayout="column" fxLayoutAlign="space-evenly">
|
||||
<mat-form-field fxFlex="40%">
|
||||
<input type="text" required matInput placeholder="Email" formControlName="email"/>
|
||||
<mat-error *ngIf="signupForm.get('email').errors?.required">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="signupForm.get('email').errors?.email">
|
||||
Email must be valid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%" formGroupName="passwords">
|
||||
<input type="password" required matInput placeholder="Password" formControlName="password" />
|
||||
<mat-error *ngIf="signupForm.get('passwords.password').errors?.required">
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%" formGroupName="passwords">
|
||||
<input type="password" required matInput placeholder="Repeat password" formControlName="repeatPassword" />
|
||||
<mat-error *ngIf="signupForm.get('passwords.repeatPassword').errors?.required">
|
||||
Repeat the password
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions fxLayout="column">
|
||||
<button type="submit" [disabled]="signupForm.invalid" mat-raised-button color="primary" class="large-button">Signup</button>
|
||||
<a mat-button (click)="login()">Login</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
</div>
|
@ -1,10 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { MaterialModule } from '../../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
|
||||
import { SignupComponent } from './signup.component';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
describe('SignupComponent', () => {
|
||||
let component: SignupComponent;
|
||||
let fixture: ComponentFixture<SignupComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SignupComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SignupComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-signup',
|
||||
templateUrl: './signup.component.html',
|
||||
styleUrls: ['./signup.component.scss']
|
||||
})
|
||||
export class SignupComponent implements OnInit {
|
||||
signupForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private uiStore: UiStore,
|
||||
private authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.signupForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
passwords: this.fb.group({
|
||||
password: ['', [Validators.required]],
|
||||
repeatPassword: ['', [Validators.required]]
|
||||
}, { validator: this.comparePasswords })
|
||||
});
|
||||
}
|
||||
|
||||
comparePasswords(c: AbstractControl): { [key: string]: boolean } {
|
||||
const pass = c.get('password');
|
||||
const repeatPassword = c.get('repeatPassword');
|
||||
|
||||
if (pass.value !== repeatPassword.value) {
|
||||
return { 'match': true };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
signup() {
|
||||
this.authStore.signup(this.getUserDataFromForm());
|
||||
}
|
||||
|
||||
login() {
|
||||
this.uiStore.goToLogin();
|
||||
}
|
||||
|
||||
getUserDataFromForm() {
|
||||
return {
|
||||
email: this.signupForm.get('email').value,
|
||||
password: this.signupForm.get('passwords.password').value
|
||||
};
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<form [formGroup]="addChannelForm" (ngSubmit)="onAddChannel()">
|
||||
<h1 *ngIf="!editMode" mat-dialog-title>Add channel</h1>
|
||||
<h1 *ngIf="editMode" mat-dialog-title>Edit channel</h1>
|
||||
<div mat-dialog-content fxLayout="column">
|
||||
<input type="hidden" formControlName="id">
|
||||
<mat-form-field>
|
||||
<input matInput tabindex="1" placeholder="Name" formControlName="name" />
|
||||
<mat-error *ngIf="addChannelForm.get('name').errors?.required">
|
||||
Name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Connected things" formControlName="connected" [compareWith]="compareFunction" multiple>
|
||||
<mat-option *ngFor="let thing of thingsStore.things" [value]="thing">{{thing.name}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addChannelForm.invalid">Ok</button>
|
||||
</div>
|
||||
</form>
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { MaterialModule } from '../../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../../../core/services/channels/channels.service';
|
||||
import { ThingsService } from '../../../core/services/things/things.service';
|
||||
import { ChannelDialogComponent } from './channel-dialog.component';
|
||||
import { ThingsStore } from '../../../core/store/things.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
describe('ChannelDialogComponent', () => {
|
||||
let component: ChannelDialogComponent;
|
||||
let fixture: ComponentFixture<ChannelDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ChannelDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
ThingsStore,
|
||||
UiStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ChannelDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { toJS } from 'mobx';
|
||||
|
||||
import { ThingsStore } from '../../../core/store/things.store';
|
||||
import { Channel, Thing } from '../../../core/store/models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channel-dialog',
|
||||
templateUrl: './channel-dialog.component.html',
|
||||
styleUrls: ['./channel-dialog.component.scss']
|
||||
})
|
||||
export class ChannelDialogComponent implements OnInit {
|
||||
addChannelForm: FormGroup;
|
||||
@Output() submit: EventEmitter<Channel> = new EventEmitter<Channel>();
|
||||
editMode: boolean;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<ChannelDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Channel,
|
||||
public thingsStore: ThingsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.addChannelForm = this.fb.group(
|
||||
{
|
||||
id: [''],
|
||||
name: [''],
|
||||
connected: [[]]
|
||||
}
|
||||
);
|
||||
|
||||
if (this.data) {
|
||||
this.editMode = true;
|
||||
this.addChannelForm.patchValue(toJS(this.data));
|
||||
} else {
|
||||
this.editMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
onAddChannel() {
|
||||
const channel = this.addChannelForm.value;
|
||||
this.submit.emit(channel);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
compareFunction(obj1: Thing, obj2: Thing) {
|
||||
return obj1.id === obj2.id;
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<div class="channelsContainer" fxLayout="row" fxLayoutWrap>
|
||||
<ng-container [ngSwitch]="channelsStore.channels?.length > 0">
|
||||
<ng-container *ngSwitchCase="true">
|
||||
<!-- List -->
|
||||
<div class="channel-list-container">
|
||||
<mat-table #table [dataSource]="channelsStore.channels.toJS()" matSort class="mat-cell">
|
||||
<!-- Columns -->
|
||||
<ng-container matColumnDef="id">
|
||||
<mat-header-cell fxFlex="32%" *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
|
||||
<mat-cell fxFlex="32%" *matCellDef="let row" >{{row.id}}</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell fxFlex="30%" *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell fxFlex="30%" *matCellDef="let row"> {{row.name}}</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="connected">
|
||||
<mat-header-cell fxFlex="30%" *matHeaderCellDef mat-sort-header>Connected</mat-header-cell>
|
||||
<mat-cell fxFlex="30%" *matCellDef="let row">
|
||||
<li *ngFor="let item of row.connected"> {{item.id}} </li>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Actions -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell fxFlex="8%" *matHeaderCellDef mat-sort-header></mat-header-cell>
|
||||
<mat-cell fxFlex="8%" *matCellDef="let row" >
|
||||
<button mat-icon-button color="accent" (click)="editChannel(row)">
|
||||
<mat-icon aria-label="Delete">edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="accent" (click)="deleteChannel(row)">
|
||||
<mat-icon aria-label="Delete">delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchDefault>
|
||||
<h3>It looks like you don't have any channels in your account.</h3>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<button mat-mini-fab class="addButton" (click)="addChannel()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
.addButton {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
/* List container */
|
||||
.channel-list-container {
|
||||
width: 100%;
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { MaterialModule } from '../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../../core/services/channels/channels.service';
|
||||
import { ThingsService } from '../../core/services/things/things.service';
|
||||
import { ChannelsComponent } from './channels.component';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { UiStore } from '../../core/store/ui.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
describe('ChannelsComponent', () => {
|
||||
let component: ChannelsComponent;
|
||||
let fixture: ComponentFixture<ChannelsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ChannelsComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: ThingsStore,
|
||||
useClass: class {
|
||||
getThings = jasmine.createSpy('getThings');
|
||||
}
|
||||
},
|
||||
UiStore,
|
||||
{
|
||||
provide: ChannelsStore,
|
||||
useClass: class {
|
||||
getChannels = jasmine.createSpy('getChannels');
|
||||
}
|
||||
},
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ChannelsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { Channel } from '../../core/store/models';
|
||||
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { ChannelDialogComponent } from './channel-dialog/channel-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channels',
|
||||
templateUrl: './channels.component.html',
|
||||
styleUrls: ['./channels.component.scss'],
|
||||
})
|
||||
export class ChannelsComponent implements OnInit {
|
||||
channels: Observable<Channel[]>;
|
||||
displayedColumns = ['id', 'name', 'connected', 'actions'];
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
public thingsStore: ThingsStore,
|
||||
public channelsStore: ChannelsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.channelsStore.getChannels();
|
||||
this.thingsStore.getThings();
|
||||
}
|
||||
|
||||
addChannel() {
|
||||
const dialogRef = this.dialog.open(ChannelDialogComponent);
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((channel: Channel) => {
|
||||
this.channelsStore.addChannel(channel);
|
||||
});
|
||||
}
|
||||
|
||||
editChannel(channel: Channel) {
|
||||
const dialogRef = this.dialog.open(ChannelDialogComponent, {
|
||||
data: channel
|
||||
});
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((editedChannel: Channel) => {
|
||||
this.channelsStore.editChannel(editedChannel);
|
||||
});
|
||||
}
|
||||
|
||||
deleteChannel(channel: Channel) {
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
question: 'Are you sure you want to delete the channel?'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.channelsStore.deleteChannel(channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<h1 mat-dialog-title>{{data.question}}</h1>
|
||||
<div mat-dialog-actions fxLayoutAlign="end end">
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button mat-raised-button color="primary" [mat-dialog-close]="true">Ok</button>
|
||||
</div>
|
@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { MaterialModule } from '../../../core/material/material.module';
|
||||
import { ConfirmationDialogComponent } from './confirmation-dialog.component';
|
||||
|
||||
describe('ConfirmationDialogComponent', () => {
|
||||
let component: ConfirmationDialogComponent;
|
||||
let fixture: ComponentFixture<ConfirmationDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ConfirmationDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConfirmationDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirmation-dialog',
|
||||
templateUrl: './confirmation-dialog.component.html',
|
||||
styleUrls: ['./confirmation-dialog.component.scss']
|
||||
})
|
||||
export class ConfirmationDialogComponent implements OnInit {
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<form [formGroup]="addThingForm" (ngSubmit)="onAddThing()">
|
||||
<h1 mat-dialog-title>Add thing</h1>
|
||||
<div mat-dialog-content fxLayout="column">
|
||||
<input type="hidden" formControlName="id">
|
||||
<mat-form-field *ngIf="!editMode">
|
||||
<mat-select placeholder="Thing type" formControlName="type">
|
||||
<mat-option value="app">
|
||||
App
|
||||
</mat-option>
|
||||
<mat-option value="device">
|
||||
Device
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="addThingForm.get('type').errors?.required">
|
||||
Type is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput tabindex="1" placeholder="Name" formControlName="name" />
|
||||
<mat-error *ngIf="addThingForm.get('name').errors?.required">
|
||||
Name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea formControlName="metadata" matInput placeholder="Metadata" matTextareaAutosize matAutosizeMinRows="2"
|
||||
matAutosizeMaxRows="5"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addThingForm.invalid">Ok</button>
|
||||
</div>
|
||||
</form>
|
@ -1,52 +0,0 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { MaterialModule } from '../../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../../../core/services/channels/channels.service';
|
||||
import { ThingsService } from '../../../core/services/things/things.service';
|
||||
import { ThingDialogComponent } from './thing-dialog.component';
|
||||
|
||||
describe('ThingDialogComponent', () => {
|
||||
let component: ThingDialogComponent;
|
||||
let fixture: ComponentFixture<ThingDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ThingDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ThingDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
|
||||
import { Thing } from '../../../core/store/models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-thing-dialog',
|
||||
templateUrl: './thing-dialog.component.html',
|
||||
styleUrls: ['./thing-dialog.component.scss']
|
||||
})
|
||||
export class ThingDialogComponent implements OnInit {
|
||||
addThingForm: FormGroup;
|
||||
@Output() submit: EventEmitter<Thing> = new EventEmitter<Thing>();
|
||||
editMode: boolean;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<ThingDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Thing
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.addThingForm = this.fb.group(
|
||||
{
|
||||
id: null,
|
||||
type: ['', [Validators.required]],
|
||||
name: ['', [Validators.required, Validators.minLength(5)]],
|
||||
metadata: ['']
|
||||
}
|
||||
);
|
||||
|
||||
if (this.data) {
|
||||
this.editMode = true;
|
||||
this.addThingForm.patchValue(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
onAddThing() {
|
||||
const thing = this.addThingForm.value;
|
||||
this.submit.emit(thing);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<div class="thingsContainer" fxLayout="row" fxLayoutWrap>
|
||||
<ng-container [ngSwitch]="thingsStore.things?.length > 0">
|
||||
<ng-container *ngSwitchCase="true">
|
||||
<!-- List -->
|
||||
<div class="things-list-container">
|
||||
<mat-table #table [dataSource]="thingsStore.things.toJS()" matSort class="mat-cell">
|
||||
<!-- Columns -->
|
||||
<ng-container matColumnDef="id">
|
||||
<mat-header-cell fxFlex="32%" *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
|
||||
<mat-cell fxFlex="32%" *matCellDef="let row" >{{row.id}}</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell fxFlex="15%" *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell fxFlex="15%" *matCellDef="let row"> {{row.name}}</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="type">
|
||||
<mat-header-cell fxFlex="10%" *matHeaderCellDef mat-sort-header>Type</mat-header-cell>
|
||||
<mat-cell fxFlex="10%" *matCellDef="let row"> {{row.type}}</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="metadata">
|
||||
<mat-header-cell fxFlex="35%" *matHeaderCellDef mat-sort-header>Metadata</mat-header-cell>
|
||||
<mat-cell fxFlex="35%" *matCellDef="let row"> {{row.metadata | json}}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Actions -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell fxFlex="8%" *matHeaderCellDef mat-sort-header></mat-header-cell>
|
||||
<mat-cell fxFlex="8%" *matCellDef="let row" >
|
||||
<button mat-icon-button color="accent" (click)="editThing(row)">
|
||||
<mat-icon aria-label="Delete">edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="accent" (click)="deleteThing(row)">
|
||||
<mat-icon aria-label="Delete">delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<h3 >It looks like you don't have any device in your account.</h3>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<button mat-mini-fab class="addButton" (click)="addThing()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
.addButton {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
/* List container */
|
||||
.things-list-container {
|
||||
width: 100%;
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { MaterialModule } from '../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../../core/services/channels/channels.service';
|
||||
import { ThingsService } from '../../core/services/things/things.service';
|
||||
import { ThingsComponent } from './things.component';
|
||||
import { UiStore } from '../../core/store/ui.store';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
describe('ThingsComponent', () => {
|
||||
let component: ThingsComponent;
|
||||
let fixture: ComponentFixture<ThingsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ThingsComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
{
|
||||
provide: ThingsStore,
|
||||
useClass: class {
|
||||
getThings = jasmine.createSpy('getThings');
|
||||
}
|
||||
},
|
||||
ChannelsStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ThingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { toJS } from 'mobx';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Thing } from '../../core/store/models';
|
||||
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { ThingDialogComponent } from './thing-dialog/thing-dialog.component';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-things',
|
||||
templateUrl: './things.component.html',
|
||||
styleUrls: ['./things.component.scss']
|
||||
})
|
||||
export class ThingsComponent implements OnInit {
|
||||
things: Observable<Thing[]>;
|
||||
displayedColumns = ['id', 'name', 'type', 'metadata', 'actions'];
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
public thingsStore: ThingsStore,
|
||||
public channelsStore: ChannelsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.thingsStore.getThings();
|
||||
this.channelsStore.getChannels();
|
||||
}
|
||||
|
||||
addThing() {
|
||||
const dialogRef = this.dialog.open(ThingDialogComponent);
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((thing: Thing) => {
|
||||
this.thingsStore.addThing(thing);
|
||||
});
|
||||
}
|
||||
|
||||
editThing(thing: Thing) {
|
||||
const dialogRef = this.dialog.open(ThingDialogComponent, {
|
||||
data: thing
|
||||
});
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((editedThing: Thing) => {
|
||||
this.thingsStore.editThing(toJS(editedThing));
|
||||
});
|
||||
}
|
||||
|
||||
deleteThing(thing: Thing) {
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
question: 'Are you sure you want to delete the thing?'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.thingsStore.deleteThing(toJS(thing));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import {
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatDialogModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatMenuModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
MatSliderModule,
|
||||
MatSlideToggleModule,
|
||||
MatSnackBarModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
MatTooltipModule,
|
||||
MatTableModule,
|
||||
} from '@angular/material';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatMenuModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
MatSnackBarModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
MatTooltipModule,
|
||||
MatCheckboxModule,
|
||||
MatSliderModule,
|
||||
MatSlideToggleModule,
|
||||
MatChipsModule,
|
||||
MatTableModule
|
||||
],
|
||||
exports: [
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatMenuModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
MatSnackBarModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
MatTooltipModule,
|
||||
MatCheckboxModule,
|
||||
MatSliderModule,
|
||||
MatSlideToggleModule,
|
||||
MatChipsModule,
|
||||
MatTableModule
|
||||
],
|
||||
})
|
||||
export class MaterialModule { }
|
@ -1,151 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import '../../../rxjs-extensions';
|
||||
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AuthService } from 'ngx-auth';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { TokenStorage } from './token-storage.service';
|
||||
|
||||
|
||||
interface AccessData {
|
||||
token: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticationService implements AuthService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private tokenStorage: TokenStorage
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Check, if user already authorized.
|
||||
* @description Should return Observable with true or false values
|
||||
* @returns {Observable<boolean>}
|
||||
* @memberOf AuthService
|
||||
*/
|
||||
public isAuthorized(): Observable<boolean> {
|
||||
return this.tokenStorage
|
||||
.getAccessToken()
|
||||
.map(token => !!token);
|
||||
}
|
||||
|
||||
public getHeaders(token) {
|
||||
return {
|
||||
'Authorization': token
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token
|
||||
* @description Should return access token in Observable from e.g.
|
||||
* localStorage
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
public getAccessToken(): Observable<string> {
|
||||
return this.tokenStorage.getAccessToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function, that should perform refresh token verifyTokenRequest
|
||||
* @description Should be successfully completed so interceptor
|
||||
* can execute pending requests or retry original one
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
public refreshToken(): Observable<AccessData> {
|
||||
return this.tokenStorage
|
||||
.getRefreshToken()
|
||||
.switchMap((refreshToken: string) => {
|
||||
return this.http.post(`http://localhost:3000/refresh`, { refreshToken });
|
||||
})
|
||||
.do(this.saveAccessData.bind(this))
|
||||
.catch((err) => {
|
||||
this.logout();
|
||||
|
||||
return Observable.throw(err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function, checks response of failed request to determine,
|
||||
* whether token be refreshed or not.
|
||||
* @description Essentialy checks status
|
||||
* @param {Response} response
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public refreshShouldHappen(response: HttpErrorResponse): boolean {
|
||||
return response.status === 401;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that outgoing request is refresh-token,
|
||||
* so interceptor won't intercept this request
|
||||
* @param {string} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public verifyTokenRequest(url: string): boolean {
|
||||
return url.endsWith('/refresh');
|
||||
}
|
||||
|
||||
/**
|
||||
* EXTRA AUTH METHODS
|
||||
*/
|
||||
|
||||
public login(payload): Observable<any> {
|
||||
return this.http.post(environment.loginUrl, payload)
|
||||
.do((tokens: AccessData) => this.saveAccessData(tokens))
|
||||
.catch((error) => {
|
||||
let message = '';
|
||||
|
||||
if (error.status === 403) {
|
||||
message = 'Wrong password or email';
|
||||
} else {
|
||||
message = 'Server side error';
|
||||
}
|
||||
|
||||
return Observable.throw(message);
|
||||
});
|
||||
}
|
||||
|
||||
public signup(payload): Observable<any> {
|
||||
return this.http.post(environment.signupUrl, payload)
|
||||
.catch((error) => {
|
||||
let message = '';
|
||||
if (error.status === 409) {
|
||||
message = 'Already existing email address';
|
||||
} else {
|
||||
message = 'Server side error';
|
||||
}
|
||||
|
||||
return Observable.throw(message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
public logout(): void {
|
||||
this.tokenStorage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save access data in the storage
|
||||
*
|
||||
* @private
|
||||
* @param {AccessData} data
|
||||
*/
|
||||
private saveAccessData({ token }: AccessData) {
|
||||
this.tokenStorage
|
||||
.setAccessToken(token);
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
@Injectable()
|
||||
export class TokenStorage {
|
||||
|
||||
/**
|
||||
* Get access token
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
public getAccessToken(): Observable<string> {
|
||||
const token: string = <string>localStorage.getItem('accessToken');
|
||||
return Observable.of(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get refresh token
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
public getRefreshToken(): Observable<string> {
|
||||
const token: string = <string>localStorage.getItem('refreshToken');
|
||||
return Observable.of(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set access token
|
||||
* @returns {TokenStorage}
|
||||
*/
|
||||
public setAccessToken(token: string): TokenStorage {
|
||||
localStorage.setItem('accessToken', token);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set refresh token
|
||||
* @returns {TokenStorage}
|
||||
*/
|
||||
public setRefreshToken(token: string): TokenStorage {
|
||||
localStorage.setItem('refreshToken', token);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tokens
|
||||
*/
|
||||
public clear() {
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/do';
|
||||
|
||||
import { AuthStore } from '../../store/auth.store';
|
||||
|
||||
@Injectable()
|
||||
export class UnauthorizedInterceptor implements HttpInterceptor {
|
||||
constructor(
|
||||
private authStore: AuthStore
|
||||
) {}
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(req).do(event => {}, err => {
|
||||
if (err instanceof HttpErrorResponse && err.status === 403) {
|
||||
this.authStore.logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { HttpClient, HttpResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { forkJoin } from 'rxjs/observable/forkJoin';
|
||||
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { Channel, Thing } from '../../store/models';
|
||||
|
||||
interface ChannelsPayload {
|
||||
channels: Channel[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ChannelsService {
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
getChannels() {
|
||||
return this.http.get(environment.channelsUrl).switchMap((payload: ChannelsPayload) => {
|
||||
const allChannels = forkJoin(this.createChannelsRequests(payload.channels));
|
||||
return allChannels;
|
||||
}).switchMap((responses: Channel[]) => {
|
||||
responses.forEach(channel => {
|
||||
channel.connected = channel.connected ? channel.connected : [];
|
||||
});
|
||||
return Observable.of(responses);
|
||||
});
|
||||
}
|
||||
|
||||
createChannelsRequests(channels) {
|
||||
return channels.map((channel => this.http.get(environment.channelsUrl + '/' + channel.id)));
|
||||
}
|
||||
|
||||
addChannel(channel: Channel) {
|
||||
const payload = {
|
||||
name: channel.name
|
||||
};
|
||||
|
||||
if (!channel.connected.length) {
|
||||
console.log('send add ch');
|
||||
return this.http.post(environment.channelsUrl, payload);
|
||||
}
|
||||
return this.http.post(environment.channelsUrl, payload, { observe: 'response' })
|
||||
.switchMap((res) => {
|
||||
const id = this.getChannelIdFrom(res);
|
||||
return forkJoin(this.createThingsConnectRequests(id, channel.connected));
|
||||
});
|
||||
}
|
||||
|
||||
private getChannelIdFrom(res: HttpResponse<Object>) {
|
||||
const location = res.headers.get('Location');
|
||||
return location.replace('/channels/', '');
|
||||
}
|
||||
|
||||
deleteChannel(channel: Channel) {
|
||||
return this.http.delete(environment.channelsUrl + '/' + channel.id);
|
||||
}
|
||||
|
||||
editChannel(channelFormData: Channel, channel: Channel) {
|
||||
const payload = {
|
||||
name: channelFormData.name
|
||||
};
|
||||
|
||||
const editChannel = this.http.put(environment.channelsUrl + '/' + channel.id, payload);
|
||||
|
||||
return editChannel.switchMap(() => {
|
||||
const thingsToAdd = this.getThingsToAdd(channelFormData, channel);
|
||||
if (thingsToAdd.length) {
|
||||
return forkJoin(this.createThingsConnectRequests(channel.id, thingsToAdd));
|
||||
} else {
|
||||
return Observable.of([]);
|
||||
}
|
||||
}).switchMap(() => {
|
||||
const thingsToDelete = this.getThingsToDelete(channelFormData, channel);
|
||||
console.log(thingsToDelete);
|
||||
if (thingsToDelete.length) {
|
||||
return forkJoin(this.createThingsDisconnectRequests(channel.id, thingsToDelete));
|
||||
} else {
|
||||
return Observable.of([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getThingsToDelete(channelFormData: Channel, channel: Channel) {
|
||||
return channel.connected.filter(thing => {
|
||||
return channelFormData.connected.find(th => th.id === thing.id) === undefined;
|
||||
});
|
||||
}
|
||||
|
||||
getThingsToAdd(channelFormData: Channel, channel: Channel) {
|
||||
return channelFormData.connected.filter(thing => {
|
||||
return channel.connected.find(th => th.id === thing.id) === undefined;
|
||||
});
|
||||
}
|
||||
|
||||
createThingsConnectRequests(channelId: string , connected: Thing[]) {
|
||||
return connected.map((connection) => {
|
||||
return this.http.put(environment.channelsUrl + '/' + channelId + '/things/' + connection.id, {});
|
||||
});
|
||||
}
|
||||
|
||||
createThingsDisconnectRequests(channelId: string , connected: Thing[]) {
|
||||
return connected.map((connection) => {
|
||||
return this.http.delete(environment.channelsUrl + '/' + channelId + '/things/' + connection.id, {});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { User } from '../store/models';
|
||||
|
||||
export const MOCK_USER = {
|
||||
email: 'asdf@asdf.com',
|
||||
password: 'asdf'
|
||||
};
|
||||
|
||||
export const EXISTING_USER = {
|
||||
email: '1234@1234.com',
|
||||
password: '1234'
|
||||
};
|
||||
|
||||
export const INVALID_CREDENTIALS_USER = {
|
||||
email: 'pera@pera.com',
|
||||
password: 'pera'
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MockAuthService {
|
||||
public signup(user: User): Observable<User> {
|
||||
if (user.email === MOCK_USER.email) {
|
||||
return Observable.of(user).delay(5000);
|
||||
}
|
||||
|
||||
if (user.email === EXISTING_USER.email) {
|
||||
return Observable.throw(new Error('User with email already exists.'));
|
||||
}
|
||||
|
||||
return Observable.throw(new Error('Failed connecting to server.'));
|
||||
}
|
||||
|
||||
public login(user: User): Observable<User> {
|
||||
if (user.email === MOCK_USER.email) {
|
||||
return Observable.of(user).delay(1000);
|
||||
}
|
||||
|
||||
if (user.email === INVALID_CREDENTIALS_USER.email) {
|
||||
return Observable.throw(new Error('Invalid credentials'));
|
||||
}
|
||||
|
||||
return Observable.throw(new Error('Cannot connect to server'));
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Thing } from '../store/models';
|
||||
|
||||
const MOCK_CHANNELS = {
|
||||
channels: [
|
||||
{
|
||||
name: 'pera'
|
||||
},
|
||||
{
|
||||
name: 'dzoni'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MockChannelsService {
|
||||
getChannels() {
|
||||
return Observable.of(MOCK_CHANNELS).delay(1000);
|
||||
}
|
||||
|
||||
addChannel(client: Thing) {
|
||||
MOCK_CHANNELS.channels.push(client);
|
||||
return Observable.of(1).delay(1000);
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Thing } from '../store/models';
|
||||
|
||||
const MOCK_THINGS = [
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class MockThingsService {
|
||||
getThings() {
|
||||
return Observable.of(MOCK_THINGS).delay(1000);
|
||||
}
|
||||
|
||||
addThing(thing: Thing) {
|
||||
MOCK_THINGS.push(thing);
|
||||
return Observable.of(1).delay(1000);
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { Thing } from '../../store/models';
|
||||
|
||||
@Injectable()
|
||||
export class ThingsService {
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
getThings() {
|
||||
return this.http.get(environment.thingsUrl);
|
||||
}
|
||||
|
||||
addThing(thing: Thing) {
|
||||
return this.http.post(environment.thingsUrl, thing);
|
||||
}
|
||||
|
||||
deleteThing(thing: Thing) {
|
||||
return this.http.delete(environment.thingsUrl + '/' + thing.id);
|
||||
}
|
||||
|
||||
editThing(thing: Thing) {
|
||||
return this.http.put(environment.thingsUrl + '/' + thing.id, thing);
|
||||
}
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { AuthenticationService } from '../services/auth/authentication.service';
|
||||
import { TokenStorage } from '../services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../services/channels/channels.service';
|
||||
import { ThingsService } from '../services/things/things.service';
|
||||
import { AuthStore } from './auth.store';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
describe('AuthStore', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
providers: [
|
||||
AuthStore,
|
||||
UiStore,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([AuthStore], (authStore: AuthStore) => {
|
||||
expect(authStore).toBeTruthy();
|
||||
}));
|
||||
|
||||
describe('login', () => {
|
||||
const user = {
|
||||
email: 'user@user.com',
|
||||
password: 'userPassword',
|
||||
};
|
||||
|
||||
it('should set the loading flag to true before calling service', inject([AuthStore, UiStore, AuthenticationService],
|
||||
(authStore: AuthStore, uiStore: UiStore, authService: AuthenticationService) => {
|
||||
const spy = spyOn(authService, 'login').and.returnValue({ subscribe: () => { } });
|
||||
|
||||
authStore.login(user);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the isAuthenticated flag to true when successfully authenticated', inject([AuthStore, AuthenticationService, Router],
|
||||
(authStore: AuthStore, authService: AuthenticationService, router: Router) => {
|
||||
const spy = spyOn(authService, 'login').and.returnValue(Observable.of(true));
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
|
||||
authStore.login(user);
|
||||
|
||||
expect(authStore.isAuthenticated).toBeTruthy();
|
||||
}));
|
||||
|
||||
|
||||
it('should navigate to /things when successfully authenticated', inject([AuthStore, AuthenticationService, Router],
|
||||
(authStore: AuthStore, authService: AuthenticationService, router: Router) => {
|
||||
const spy = spyOn(authService, 'login').and.returnValue(Observable.of(true));
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
|
||||
authStore.login(user);
|
||||
|
||||
expect(routerSpy).toHaveBeenCalledWith(['/things']);
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false when successfully authenticated',
|
||||
inject([AuthStore, UiStore, AuthenticationService, Router],
|
||||
(authStore: AuthStore, uiStore: UiStore, authService: AuthenticationService, router: Router) => {
|
||||
const spy = spyOn(authService, 'login').and.returnValue(Observable.of(true));
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
|
||||
authStore.login(user);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false when authentication failed', inject([AuthStore, UiStore, AuthenticationService],
|
||||
(authStore: AuthStore, uiStore: UiStore, authService: AuthenticationService) => {
|
||||
const spy = spyOn(authService, 'login').and.returnValue(Observable.throw({ status: 403 }));
|
||||
|
||||
authStore.login(user);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the authError to authentication error', inject([AuthStore, AuthenticationService],
|
||||
(authStore: AuthStore, authService: AuthenticationService) => {
|
||||
const spy = spyOn(authService, 'login').and.returnValue(Observable.throw('Auth failed'));
|
||||
|
||||
authStore.login(user);
|
||||
|
||||
expect(authStore.authError).toEqual('Auth failed');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('signup', () => {
|
||||
const user = {
|
||||
email: 'user@user.com',
|
||||
password: 'userPassword',
|
||||
};
|
||||
|
||||
it('should set the loading flag to true before calling service', inject([AuthStore, UiStore, AuthenticationService],
|
||||
(authStore: AuthStore, uiStore: UiStore, authService: AuthenticationService) => {
|
||||
const spy = spyOn(authService, 'signup').and.returnValue({ subscribe: () => { } });
|
||||
|
||||
authStore.signup(user);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should call the login when signup successfull', inject([AuthStore, AuthenticationService, Router],
|
||||
(authStore: AuthStore, authService: AuthenticationService, router: Router) => {
|
||||
const spy = spyOn(authService, 'signup').and.returnValue(Observable.of(true));
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
const loginSpy = spyOn(authStore, 'login').and.stub();
|
||||
|
||||
authStore.signup(user);
|
||||
|
||||
expect(loginSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false when signup failed', inject([AuthStore, UiStore, AuthenticationService],
|
||||
(authStore: AuthStore, uiStore: UiStore, authService: AuthenticationService) => {
|
||||
const spy = spyOn(authService, 'signup').and.returnValue(Observable.throw('Signup failed'));
|
||||
|
||||
authStore.signup(user);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the authError to signup error', inject([AuthStore, AuthenticationService],
|
||||
(authStore: AuthStore, authService: AuthenticationService) => {
|
||||
const spy = spyOn(authService, 'signup').and.returnValue(Observable.throw('Signup failed'));
|
||||
|
||||
authStore.signup(user);
|
||||
|
||||
expect(authStore.authError).toEqual('Signup failed');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should call the authentication service logout', inject([AuthStore, AuthenticationService, Router],
|
||||
(authStore: AuthStore, authService: AuthenticationService, router: Router) => {
|
||||
const spy = spyOn(authService, 'logout');
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
|
||||
authStore.logout();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the isAuthenticated flag to false', inject([AuthStore, AuthenticationService, Router],
|
||||
(authStore: AuthStore, authService: AuthenticationService, router: Router) => {
|
||||
const spy = spyOn(authService, 'logout');
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
|
||||
authStore.logout();
|
||||
|
||||
expect(authStore.isAuthenticated).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should navigate to /login', inject([AuthStore, AuthenticationService, Router],
|
||||
(authStore: AuthStore, authService: AuthenticationService, router: Router) => {
|
||||
const spy = spyOn(authService, 'logout');
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
|
||||
authStore.logout();
|
||||
|
||||
expect(routerSpy).toHaveBeenCalledWith(['/login']);
|
||||
}));
|
||||
});
|
||||
});
|
@ -1,63 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { action, observable } from 'mobx';
|
||||
|
||||
import { AuthenticationService } from '../services/auth/authentication.service';
|
||||
import { User } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
@Injectable()
|
||||
export class AuthStore {
|
||||
@observable isAuthenticated = false;
|
||||
@observable authError = '';
|
||||
|
||||
constructor(
|
||||
private authenticationService: AuthenticationService,
|
||||
private uiState: UiStore,
|
||||
private router: Router,
|
||||
) {
|
||||
this.authenticationService.isAuthorized().subscribe(
|
||||
(isAuthenticated) => {
|
||||
this.isAuthenticated = isAuthenticated;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
login(user: User) {
|
||||
this.uiState.loading = true;
|
||||
|
||||
this.authenticationService.login(user)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.isAuthenticated = true;
|
||||
this.router.navigate(['/things']);
|
||||
},
|
||||
(error) => {
|
||||
this.uiState.loading = false;
|
||||
this.authError = error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
signup(user: User) {
|
||||
this.uiState.loading = true;
|
||||
|
||||
this.authenticationService.signup(user)
|
||||
.subscribe(() => {
|
||||
this.login(user);
|
||||
},
|
||||
(error) => {
|
||||
this.uiState.loading = false;
|
||||
this.authError = error;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
logout() {
|
||||
this.authenticationService.logout();
|
||||
this.isAuthenticated = false;
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
@ -1,247 +0,0 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { toJS } from 'mobx';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { ChannelsService } from '../services/channels/channels.service';
|
||||
import { ChannelsStore } from './channels.store';
|
||||
import { Channel } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
describe('ChannelsStore', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
providers: [
|
||||
ChannelsStore,
|
||||
UiStore,
|
||||
ChannelsService,
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ChannelsStore], (channelsStore: ChannelsStore) => {
|
||||
expect(channelsStore).toBeTruthy();
|
||||
}));
|
||||
|
||||
describe('getChannels', () => {
|
||||
it('should set the loading flag to true before service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const getChannels = spyOn(channelsService, 'getChannels').and.returnValue({ subscribe: () => { } });
|
||||
|
||||
channelsStore.getChannels();
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful get', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const getChannels = spyOn(channelsService, 'getChannels').and.returnValue(Observable.of(true));
|
||||
|
||||
channelsStore.getChannels();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the channels property to the returned channels from the service', inject([ChannelsStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, channelsService: ChannelsService) => {
|
||||
const serviceReturnValue = [];
|
||||
const getChannels = spyOn(channelsService, 'getChannels').and.returnValue(Observable.of(serviceReturnValue));
|
||||
|
||||
channelsStore.getChannels();
|
||||
|
||||
expect(toJS(channelsStore.channels)).toEqual(serviceReturnValue);
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed get', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const getChannels = spyOn(channelsService, 'getChannels').and.returnValue(Observable.throw(''));
|
||||
|
||||
channelsStore.getChannels();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('addChannel', () => {
|
||||
it('should set the loading flag to true before service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const addChannel = spyOn(channelsService, 'addChannel').and.returnValue({ subscribe: () => { } });
|
||||
const newChannel: Channel = {
|
||||
name: 'new channel',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
channelsStore.addChannel(newChannel);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const addChannel = spyOn(channelsService, 'addChannel').and.returnValue(Observable.of(true));
|
||||
const storeGetChannelsSpy = spyOn(channelsStore, 'getChannels').and.stub();
|
||||
const newChannel: Channel = {
|
||||
name: 'new channel',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
channelsStore.addChannel(newChannel);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const addChannel = spyOn(channelsService, 'addChannel').and.returnValue(Observable.throw(''));
|
||||
const storeGetChannelsSpy = spyOn(channelsStore, 'getChannels').and.stub();
|
||||
const newChannel: Channel = {
|
||||
name: 'new channel',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
channelsStore.addChannel(newChannel);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the channelsStore.getChannels after successful add', inject([ChannelsStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, channelsService: ChannelsService) => {
|
||||
const addChannel = spyOn(channelsService, 'addChannel').and.returnValue(Observable.of(true));
|
||||
const storeGetChannelsSpy = spyOn(channelsStore, 'getChannels').and.stub();
|
||||
|
||||
const newChannel: Channel = {
|
||||
name: 'new channel',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
channelsStore.addChannel(newChannel);
|
||||
|
||||
expect(storeGetChannelsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('editChannel', () => {
|
||||
it('should set the loading flag to true before service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const addChannel = spyOn(channelsService, 'editChannel').and.returnValue({ subscribe: () => { } });
|
||||
const editedChannel: Channel = {
|
||||
name: 'edited channel',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
channelsStore.editChannel(editedChannel);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const editChannel = spyOn(channelsService, 'editChannel').and.returnValue(Observable.of(true));
|
||||
const storeGetChannelsSpy = spyOn(channelsStore, 'getChannels').and.stub();
|
||||
const editedChannel: Channel = {
|
||||
name: 'edited channel',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
|
||||
channelsStore.editChannel(editedChannel);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const editChannel = spyOn(channelsService, 'editChannel').and.returnValue(Observable.throw(''));
|
||||
const storeGetChannelsSpy = spyOn(channelsStore, 'getChannels').and.stub();
|
||||
const editedChannel: Channel = {
|
||||
name: 'edited channel',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
|
||||
channelsStore.editChannel(editedChannel);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the channelsStore.getChannels after successful add', inject([ChannelsStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, channelsService: ChannelsService) => {
|
||||
const editChannel = spyOn(channelsService, 'editChannel').and.returnValue(Observable.of(true));
|
||||
const storeGetChannelsSpy = spyOn(channelsStore, 'getChannels').and.stub();
|
||||
|
||||
const editedChannel: Channel = {
|
||||
name: 'edited channel',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
channelsStore.editChannel(editedChannel);
|
||||
|
||||
expect(storeGetChannelsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('deleteChannel', () => {
|
||||
it('should set the loading flag to true before service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const deleteChannel = spyOn(channelsService, 'deleteChannel').and.returnValue({ subscribe: () => { } });
|
||||
const channelToBeDeleted: Channel = {
|
||||
name: 'channelToBeDeleted',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
channelsStore.deleteChannel(channelToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const deleteChannel = spyOn(channelsService, 'deleteChannel').and.returnValue(Observable.of(true));
|
||||
const storeGetChannelsSpy = spyOn(channelsStore, 'getChannels').and.stub();
|
||||
const channelToBeDeleted: Channel = {
|
||||
name: 'channelToBeDeleted',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
|
||||
channelsStore.deleteChannel(channelToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed service call', inject([ChannelsStore, UiStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, uiStore: UiStore, channelsService: ChannelsService) => {
|
||||
const deleteChannel = spyOn(channelsService, 'deleteChannel').and.returnValue(Observable.throw(''));
|
||||
const storeGetChannelsSpy = spyOn(channelsStore, 'getChannels').and.stub();
|
||||
const channelToBeDeleted: Channel = {
|
||||
name: 'channelToBeDeleted',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
|
||||
channelsStore.deleteChannel(channelToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the channelsStore.getChannels after successful add', inject([ChannelsStore, ChannelsService],
|
||||
(channelsStore: ChannelsStore, channelsService: ChannelsService) => {
|
||||
const deleteChannel = spyOn(channelsService, 'deleteChannel').and.returnValue(Observable.of(true));
|
||||
const storeGetChannelsSpy = spyOn(channelsStore, 'getChannels').and.stub();
|
||||
|
||||
const channelToBeDeleted: Channel = {
|
||||
name: 'channelToBeDeleted',
|
||||
connected: [],
|
||||
};
|
||||
|
||||
channelsStore.deleteChannel(channelToBeDeleted);
|
||||
|
||||
expect(storeGetChannelsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
@ -1,70 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { action, observable } from 'mobx';
|
||||
|
||||
import { ChannelsService } from '../services/channels/channels.service';
|
||||
import { Channel } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
@Injectable()
|
||||
export class ChannelsStore {
|
||||
@observable channels: Channel[] = [];
|
||||
|
||||
constructor(
|
||||
private uiState: UiStore,
|
||||
private channelsService: ChannelsService,
|
||||
) { }
|
||||
|
||||
@action
|
||||
getChannels() {
|
||||
this.uiState.loading = true;
|
||||
this.channelsService.getChannels()
|
||||
.subscribe((payload: any) => {
|
||||
this.uiState.loading = false;
|
||||
this.channels = payload;
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
addChannel(channel: Channel) {
|
||||
this.uiState.loading = true;
|
||||
console.log('add ch');
|
||||
this.channelsService.addChannel(channel)
|
||||
.subscribe(resp => {
|
||||
console.log(resp);
|
||||
this.uiState.loading = false;
|
||||
this.getChannels();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
editChannel(editedChannel: Channel) {
|
||||
this.uiState.loading = true;
|
||||
this.channelsService.editChannel(editedChannel, this.getChannelById(editedChannel.id))
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getChannels();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
private getChannelById(id: string) {
|
||||
return this.channels.find(ch => ch.id === id);
|
||||
}
|
||||
|
||||
@action
|
||||
deleteChannel(channel: Channel) {
|
||||
this.uiState.loading = true;
|
||||
this.channelsService.deleteChannel(channel)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getChannels();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
export interface Channel {
|
||||
id?: '';
|
||||
name: string;
|
||||
connected: Thing[];
|
||||
}
|
||||
|
||||
export interface Thing {
|
||||
id?: '';
|
||||
type: string;
|
||||
name: string;
|
||||
metadata: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
@ -1,256 +0,0 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { toJS } from 'mobx';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { ThingsService } from '../services/things/things.service';
|
||||
import { ThingsStore } from './things.store';
|
||||
import { Thing } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
describe('ThingsStore', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
providers: [
|
||||
ThingsStore,
|
||||
UiStore,
|
||||
ThingsService,
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ThingsStore], (thingsStore: ThingsStore) => {
|
||||
expect(thingsStore).toBeTruthy();
|
||||
}));
|
||||
|
||||
describe('getThings', () => {
|
||||
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const getThings = spyOn(thingsService, 'getThings').and.returnValue({ subscribe: () => { } });
|
||||
|
||||
thingsStore.getThings();
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful get', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const getThings = spyOn(thingsService, 'getThings').and.returnValue(Observable.of(true));
|
||||
|
||||
thingsStore.getThings();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the things property to the returned things from the service', inject([ThingsStore, ThingsService],
|
||||
(thingsStore: ThingsStore, thingsService: ThingsService) => {
|
||||
const serviceReturnValue = { things: [] };
|
||||
const getChannels = spyOn(thingsService, 'getThings').and.returnValue(Observable.of(serviceReturnValue));
|
||||
|
||||
thingsStore.getThings();
|
||||
|
||||
expect(toJS(thingsStore.things)).toEqual(serviceReturnValue.things);
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed get', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const getThings = spyOn(thingsService, 'getThings').and.returnValue(Observable.throw(''));
|
||||
|
||||
thingsStore.getThings();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('addThing', () => {
|
||||
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const addThing = spyOn(thingsService, 'addThing').and.returnValue({ subscribe: () => { } });
|
||||
const newThing: Thing = {
|
||||
name: 'new thing',
|
||||
type: 'app',
|
||||
metadata: '',
|
||||
};
|
||||
|
||||
thingsStore.addThing(newThing);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const addThing = spyOn(thingsService, 'addThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const newThing: Thing = {
|
||||
name: 'new thing',
|
||||
type: 'app',
|
||||
metadata: '',
|
||||
};
|
||||
|
||||
thingsStore.addThing(newThing);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the thingsStore.getThings after successful add', inject([ThingsStore, ThingsService],
|
||||
(thingsStore: ThingsStore, thingsService: ThingsService) => {
|
||||
const addThing = spyOn(thingsService, 'addThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const newThing: Thing = {
|
||||
name: 'new thing',
|
||||
type: 'app',
|
||||
metadata: '',
|
||||
};
|
||||
|
||||
thingsStore.addThing(newThing);
|
||||
|
||||
expect(storeGetThingsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed add', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const addThing = spyOn(thingsService, 'addThing').and.returnValue(Observable.throw(''));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
|
||||
const newThing: Thing = {
|
||||
name: 'new thing',
|
||||
type: 'app',
|
||||
metadata: '',
|
||||
};
|
||||
|
||||
thingsStore.addThing(newThing);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('editThing', () => {
|
||||
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const editThing = spyOn(thingsService, 'editThing').and.returnValue({ subscribe: () => { } });
|
||||
const editedThing: Thing = {
|
||||
name: 'edited thing',
|
||||
type: 'app',
|
||||
metadata: '',
|
||||
};
|
||||
|
||||
thingsStore.editThing(editedThing);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const editThing = spyOn(thingsService, 'editThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const editedThing: Thing = {
|
||||
name: 'edited thing',
|
||||
type: 'app',
|
||||
metadata: '',
|
||||
};
|
||||
|
||||
thingsStore.editThing(editedThing);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the thingsStore.getChannels after successful edit', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const editThing = spyOn(thingsService, 'editThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
|
||||
const editedThing: Thing = {
|
||||
name: 'edited thing',
|
||||
type: 'app',
|
||||
metadata: '',
|
||||
};
|
||||
|
||||
thingsStore.editThing(editedThing);
|
||||
|
||||
expect(storeGetThingsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed edit', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const editThing = spyOn(thingsService, 'editThing').and.returnValue(Observable.throw(''));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const editedThing: Thing = {
|
||||
name: 'edited thing',
|
||||
type: 'app',
|
||||
metadata: '',
|
||||
};
|
||||
|
||||
thingsStore.editThing(editedThing);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('deleteThing', () => {
|
||||
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue({ subscribe: () => { } });
|
||||
const thingToBeDeleted: Thing = {
|
||||
name: 'thingToBeDeleted',
|
||||
type: 'app',
|
||||
metadata: ''
|
||||
};
|
||||
|
||||
thingsStore.deleteThing(thingToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const thingToBeDeleted: Thing = {
|
||||
name: 'thingToBeDeleted',
|
||||
type: 'app',
|
||||
metadata: ''
|
||||
};
|
||||
|
||||
|
||||
thingsStore.deleteThing(thingToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the thingsStore.getChannels after successful add', inject([ThingsStore, ThingsService],
|
||||
(thingsStore: ThingsStore, thingsService: ThingsService) => {
|
||||
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
|
||||
const thingToBeDeleted: Thing = {
|
||||
name: 'thingToBeDeleted',
|
||||
type: 'app',
|
||||
metadata: ''
|
||||
};
|
||||
|
||||
thingsStore.deleteThing(thingToBeDeleted);
|
||||
|
||||
expect(storeGetThingsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed delete', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue(Observable.throw(''));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const thingToBeDeleted: Thing = {
|
||||
name: 'thingToBeDeleted',
|
||||
type: 'app',
|
||||
metadata: ''
|
||||
};
|
||||
|
||||
thingsStore.deleteThing(thingToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
});
|
@ -1,64 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { action, observable } from 'mobx';
|
||||
|
||||
import { ThingsService } from '../services/things/things.service';
|
||||
import { Thing } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
@Injectable()
|
||||
export class ThingsStore {
|
||||
@observable things: Thing[] = [];
|
||||
|
||||
constructor(
|
||||
private uiState: UiStore,
|
||||
private thingsService: ThingsService,
|
||||
) { }
|
||||
|
||||
@action
|
||||
getThings() {
|
||||
this.uiState.loading = true;
|
||||
this.thingsService.getThings()
|
||||
.subscribe((payload: any) => {
|
||||
this.uiState.loading = false;
|
||||
this.things = payload.things;
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
addThing(thing: Thing) {
|
||||
this.uiState.loading = true;
|
||||
this.thingsService.addThing(thing)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getThings();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
editThing(thing: Thing) {
|
||||
this.uiState.loading = true;
|
||||
this.thingsService.editThing(thing)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getThings();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
deleteThing(thing: Thing) {
|
||||
this.uiState.loading = true;
|
||||
this.thingsService.deleteThing(thing)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getThings();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { AuthenticationService } from '../services/auth/authentication.service';
|
||||
import { TokenStorage } from '../services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../services/channels/channels.service';
|
||||
import { ThingsService } from '../services/things/things.service';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
describe('State', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
UiStore,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([UiStore], (uiStore: UiStore) => {
|
||||
expect(uiStore).toBeTruthy();
|
||||
}));
|
||||
|
||||
describe('goToSignup', () => {
|
||||
it('should navigate to /signup', inject([UiStore, Router],
|
||||
(uiStore: UiStore, router: Router) => {
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
|
||||
uiStore.goToSignup();
|
||||
|
||||
expect(routerSpy).toHaveBeenCalledWith(['/signup']);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('goToLogin', () => {
|
||||
it('should navigate to /login', inject([UiStore, Router],
|
||||
(uiStore: UiStore, router: Router) => {
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
|
||||
uiStore.goToLogin();
|
||||
|
||||
expect(routerSpy).toHaveBeenCalledWith(['/login']);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { action, observable } from 'mobx';
|
||||
|
||||
@Injectable()
|
||||
export class UiStore {
|
||||
@observable loading = false;
|
||||
|
||||
constructor(
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
|
||||
@action
|
||||
goToSignup() {
|
||||
this.router.navigate(['/signup']);
|
||||
}
|
||||
|
||||
@action
|
||||
goToLogin() {
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import 'rxjs/add/observable/combineLatest';
|
||||
import 'rxjs/add/observable/interval';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/observable/from';
|
||||
import 'rxjs/add/observable/throw';
|
||||
import 'rxjs/add/operator/concatMap';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/debounceTime';
|
||||
import 'rxjs/add/operator/delay';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/finally';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/mergeMap';
|
||||
import 'rxjs/add/operator/retry';
|
||||
import 'rxjs/add/operator/startWith';
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
import 'rxjs/add/operator/take';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import 'rxjs/add/operator/withLatestFrom';
|
@ -1,7 +0,0 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
signupUrl: '/users',
|
||||
loginUrl: '/tokens',
|
||||
thingsUrl: '/things',
|
||||
channelsUrl: '/channels'
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
// The file contents for the current environment will overwrite these during build.
|
||||
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
|
||||
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
|
||||
// The list of which env maps to which file can be found in `.angular-cli.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
signupUrl: '/users',
|
||||
loginUrl: '/tokens',
|
||||
thingsUrl: '/things',
|
||||
channelsUrl: '/channels'
|
||||
};
|
Binary file not shown.
Before Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
@ -1,15 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mainflux UI</title>
|
||||
<base href="/">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.png">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +0,0 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.log(err));
|
@ -1,76 +0,0 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
||||
// import 'core-js/es6/symbol';
|
||||
// import 'core-js/es6/object';
|
||||
// import 'core-js/es6/function';
|
||||
// import 'core-js/es6/parse-int';
|
||||
// import 'core-js/es6/parse-float';
|
||||
// import 'core-js/es6/number';
|
||||
// import 'core-js/es6/math';
|
||||
// import 'core-js/es6/string';
|
||||
// import 'core-js/es6/date';
|
||||
// import 'core-js/es6/array';
|
||||
// import 'core-js/es6/regexp';
|
||||
// import 'core-js/es6/map';
|
||||
// import 'core-js/es6/weak-map';
|
||||
// import 'core-js/es6/set';
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/** IE10 and IE11 requires the following for the Reflect API. */
|
||||
// import 'core-js/es6/reflect';
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
|
||||
import 'core-js/es7/reflect';
|
||||
|
||||
|
||||
/**
|
||||
* Required to support Web Animations `@angular/platform-browser/animations`.
|
||||
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
|
||||
**/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Date, currency, decimal and percent pipes.
|
||||
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
|
||||
*/
|
||||
// import 'intl'; // Run `npm install --save intl`.
|
||||
/**
|
||||
* Need to import at least one locale-data with intl.
|
||||
*/
|
||||
// import 'intl/locale-data/jsonp/en';
|
@ -1,8 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
|
@ -1,32 +0,0 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/long-stack-trace-zone';
|
||||
import 'zone.js/dist/proxy.js';
|
||||
import 'zone.js/dist/sync-test';
|
||||
import 'zone.js/dist/jasmine-patch';
|
||||
import 'zone.js/dist/async-test';
|
||||
import 'zone.js/dist/fake-async-test';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
|
||||
declare const __karma__: any;
|
||||
declare const require: any;
|
||||
|
||||
// Prevent Karma from running prematurely.
|
||||
__karma__.loaded = function () {};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
||||
// Finally, start Karma to run the tests.
|
||||
__karma__.start();
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"baseUrl": "./",
|
||||
"module": "es2015",
|
||||
"types": []
|
||||
},
|
||||
"include": [ "../node_modules/ngx-auth/**/*", "**/*"],
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"outDir": "../out-tsc/spec",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"baseUrl": "",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"test.ts"
|
||||
],
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
5
dashflux/src/typings.d.ts
vendored
5
dashflux/src/typings.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
/* SystemJS module definition */
|
||||
declare var module: NodeModule;
|
||||
interface NodeModule {
|
||||
id: string;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
{
|
||||
"rulesDirectory": [
|
||||
"node_modules/codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"arrow-return-shorthand": true,
|
||||
"callable-types": true,
|
||||
"class-name": true,
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"curly": true,
|
||||
"eofline": true,
|
||||
"forin": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs",
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
],
|
||||
"interface-over-type-literal": true,
|
||||
"label-position": true,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-access": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-arg": true,
|
||||
"no-bitwise": true,
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-duplicate-super": true,
|
||||
"no-empty": false,
|
||||
"no-empty-interface": true,
|
||||
"no-eval": true,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-misused-new": true,
|
||||
"no-non-null-assertion": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-string-literal": false,
|
||||
"no-string-throw": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unnecessary-initializer": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-var-keyword": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-catch",
|
||||
"check-else",
|
||||
"check-whitespace"
|
||||
],
|
||||
"prefer-const": true,
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"radix": true,
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"typeof-compare": true,
|
||||
"unified-signatures": true,
|
||||
"variable-name": false,
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
],
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"app",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"app",
|
||||
"kebab-case"
|
||||
],
|
||||
"use-input-property-decorator": true,
|
||||
"use-output-property-decorator": true,
|
||||
"use-host-property-decorator": true,
|
||||
"no-input-rename": true,
|
||||
"no-output-rename": true,
|
||||
"use-life-cycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"component-class-suffix": true,
|
||||
"directive-class-suffix": true,
|
||||
"invoke-injectable": true
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ networks:
|
||||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:1.13-alpine
|
||||
image: nginx:1.14.2-alpine
|
||||
container_name: mainflux-nginx
|
||||
restart: on-failure
|
||||
volumes:
|
||||
@ -131,9 +131,9 @@ services:
|
||||
networks:
|
||||
- mainflux-base-net
|
||||
|
||||
dashflux:
|
||||
image: mainflux/dashflux:latest
|
||||
container_name: mainflux-dashflux
|
||||
ui:
|
||||
image: mainflux/ui:latest
|
||||
container_name: mainflux-ui
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
@ -230,7 +230,7 @@ http {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://dashflux:3000/;
|
||||
proxy_pass http://ui:3000/;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
|
@ -1,5 +1,4 @@
|
||||
FROM node:boron-alpine
|
||||
MAINTAINER Mainflux
|
||||
FROM node:10.15.1-alpine
|
||||
|
||||
COPY *.proto mqtt/* ./
|
||||
|
||||
|
@ -1,3 +1,8 @@
|
||||
// Copyright (c) 2015-2019
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
|
||||
var http = require('http'),
|
||||
|
2
ui/.dockerignore
Normal file
2
ui/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
elm-stuff
|
||||
index.html
|
21
ui/Makefile
Normal file
21
ui/Makefile
Normal file
@ -0,0 +1,21 @@
|
||||
all: dev
|
||||
|
||||
.PHONY: all docker
|
||||
|
||||
dev:
|
||||
elm make src/Main.elm
|
||||
|
||||
prod:
|
||||
elm make --optimize src/Main.elm
|
||||
|
||||
run:
|
||||
elm reactor
|
||||
|
||||
docker:
|
||||
docker build --tag=mainflux/ui -f docker/Dockerfile .
|
||||
|
||||
clean:
|
||||
rm -f index.html
|
||||
|
||||
mrproper: clean
|
||||
rm -rf elm-stuff
|
60
ui/README.md
Normal file
60
ui/README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# GUI for Mainflux in Elm
|
||||
Dashboard made with [elm-bootstrap](http://elm-bootstrap.info/).
|
||||
|
||||
## Install
|
||||
|
||||
### Install GUI as a part of Mainflux build
|
||||
|
||||
Install Elm (https://guide.elm-lang.org/install.html) and then
|
||||
|
||||
```
|
||||
git clone https://github.com/mainflux/mainflux
|
||||
cd mainflux/ui
|
||||
make
|
||||
```
|
||||
|
||||
This will produce `index.html` in the _ui_ directory. In order to use it, `cd`
|
||||
to _ui_ and do
|
||||
|
||||
`make run`
|
||||
|
||||
### Build a standalone native GUI
|
||||
|
||||
Install Elm (https://guide.elm-lang.org/install.html), `cd` to _ui_ and then
|
||||
|
||||
`elm make --optimize src/Main.elm`
|
||||
|
||||
This will produce `index.html` in the _ui_ directory. In order to use it do
|
||||
|
||||
`make run`
|
||||
|
||||
### About Elm `make`
|
||||
|
||||
`make` does `elm make src/Main.elm`.
|
||||
|
||||
`make run` just executes `elm reactor`. You can execute `elm reactor` in other
|
||||
terminal window and keep it running, and then see changes as you change-compile
|
||||
in the first window. You can even use something as
|
||||
[entr](http://eradman.com/entrproject/) to have your source compiled
|
||||
automatically when you change and save some files.
|
||||
|
||||
### Build as a part of Docker composition
|
||||
|
||||
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.
|
||||
|
||||
### Contribute to the GUI development
|
||||
|
||||
Install GUI as a part of Mainflux build or as a a standalone native GUI and run
|
||||
it. Launch Mainflux without ui service, either natively or as a Docker
|
||||
composition. Follow the guidelines for Mainflux contributors found here
|
||||
https://mainflux.readthedocs.io/en/latest/CONTRIBUTING/.
|
29
ui/css/mainflux.css
Normal file
29
ui/css/mainflux.css
Normal file
@ -0,0 +1,29 @@
|
||||
/* Copyright (c) 2019
|
||||
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;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f3f3f4 !important;
|
||||
font-family: 'Roboto', sans-serif !important;
|
||||
}
|
||||
.title{
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 800;
|
||||
transform: scaleY(0.95);
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
21
ui/docker/Dockerfile
Normal file
21
ui/docker/Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
###
|
||||
# Copyright (c) 2015-2019 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
|
||||
|
||||
COPY . /app
|
||||
RUN elm make --optimize src/Main.elm
|
||||
|
||||
# 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 docker/nginx.conf /etc/nginx/conf.d/default.conf
|
@ -5,4 +5,4 @@ server {
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
||||
}
|
30
ui/elm.json
Normal file
30
ui/elm.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"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": {}
|
||||
}
|
||||
}
|
480
ui/src/Channel.elm
Normal file
480
ui/src/Channel.elm
Normal file
@ -0,0 +1,480 @@
|
||||
-- Copyright (c) 2019
|
||||
-- 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)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import Http
|
||||
import HttpMF
|
||||
import Json.Decode as D
|
||||
import Json.Encode as E
|
||||
import ModalMF
|
||||
import Url.Builder as B
|
||||
|
||||
|
||||
url =
|
||||
{ base = "http://localhost"
|
||||
, thingsPath = [ "things" ]
|
||||
, channelsPath = [ "channels" ]
|
||||
}
|
||||
|
||||
|
||||
query =
|
||||
{ offset = 0
|
||||
, limit = 10
|
||||
}
|
||||
|
||||
|
||||
type alias Channel =
|
||||
{ name : Maybe String
|
||||
, id : String
|
||||
, metadata : Maybe String
|
||||
}
|
||||
|
||||
|
||||
emptyChannel =
|
||||
Channel (Just "") "" (Just "")
|
||||
|
||||
|
||||
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.crossOrigin url.base url.channelsPath [])
|
||||
token
|
||||
{ emptyChannel
|
||||
| name = Just model.name
|
||||
, metadata = Just 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 = Helpers.parseString model.channel.metadata
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
UpdateChannel ->
|
||||
( resetEdit { model | editMode = False }
|
||||
, HttpMF.update
|
||||
(B.crossOrigin url.base (List.append url.channelsPath [ model.channel.id ]) [])
|
||||
token
|
||||
{ emptyChannel
|
||||
| name = Just model.name
|
||||
, metadata = Just model.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.crossOrigin url.base (List.append url.channelsPath [ 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.crossOrigin url.base
|
||||
url.channelsPath
|
||||
(Helpers.buildQueryParamList model.offset model.limit)
|
||||
)
|
||||
token
|
||||
RetrievedChannels
|
||||
channelsDecoder
|
||||
)
|
||||
|
||||
RetrieveChannelsForThing thingid ->
|
||||
( model
|
||||
, HttpMF.retrieve
|
||||
(B.crossOrigin url.base
|
||||
(url.thingsPath ++ [ thingid ] ++ url.channelsPath)
|
||||
(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.crossOrigin url.base (List.append url.channelsPath [ 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 []
|
||||
[ Grid.row []
|
||||
[ Grid.col [ Col.attrs [ align "right" ] ]
|
||||
[ Button.button [ Button.outlinePrimary, Button.attrs [ Spacing.ml1, align "right" ], Button.onClick ShowProvisionModal ] [ text "ADD" ]
|
||||
]
|
||||
]
|
||||
, Grid.row []
|
||||
[ Grid.col []
|
||||
[ Card.config []
|
||||
|> Card.headerH3 [] [ text "Channels" ]
|
||||
|> Card.block []
|
||||
[ Block.custom
|
||||
(Table.table
|
||||
{ options = [ Table.striped, Table.hover, Table.small ]
|
||||
, thead = genTableHeader
|
||||
, tbody = genTableBody model
|
||||
}
|
||||
)
|
||||
]
|
||||
|> Card.view
|
||||
]
|
||||
]
|
||||
, Helpers.genPagination model.channels.total SubmitPage
|
||||
, provisionModal model
|
||||
, editModal model
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- 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 (Helpers.parseString model.channel.metadata) model.metadata
|
||||
]
|
||||
|
||||
else
|
||||
ModalMF.modalDiv [ ( "name", Helpers.parseString model.channel.name ), ( "metadata", Helpers.parseString 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" D.string))
|
||||
|
||||
|
||||
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", E.string (Helpers.parseString 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.crossOrigin url.base
|
||||
url.channelsPath
|
||||
(Helpers.buildQueryParamList model.offset model.limit)
|
||||
)
|
||||
token
|
||||
RetrievedChannels
|
||||
channelsDecoder
|
||||
, HttpMF.retrieve
|
||||
(B.crossOrigin url.base (List.append url.channelsPath [ model.channel.id ]) [])
|
||||
token
|
||||
RetrievedChannel
|
||||
channelDecoder
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
updateChannelListForThing : Model -> String -> String -> ( Model, Cmd Msg )
|
||||
updateChannelListForThing model token thingid =
|
||||
( model
|
||||
, HttpMF.retrieve
|
||||
(buildUrl (url.thingsPath ++ [ thingid ] ++ url.channelsPath) model.offset model.limit)
|
||||
token
|
||||
RetrievedChannels
|
||||
channelsDecoder
|
||||
)
|
||||
|
||||
|
||||
buildUrl : List String -> Int -> Int -> String
|
||||
buildUrl path offset limit =
|
||||
B.crossOrigin url.base
|
||||
path
|
||||
(Helpers.buildQueryParamList offset limit)
|
224
ui/src/Connection.elm
Normal file
224
ui/src/Connection.elm
Normal file
@ -0,0 +1,224 @@
|
||||
-- Copyright (c) 2019
|
||||
-- Mainflux
|
||||
--
|
||||
-- SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
|
||||
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.Table as Table
|
||||
import Bootstrap.Text as Text
|
||||
import Bootstrap.Utilities.Spacing as Spacing
|
||||
import Channel
|
||||
import Debug exposing (log)
|
||||
import Error
|
||||
import Helpers
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import Http
|
||||
import HttpMF
|
||||
import List.Extra
|
||||
import Thing
|
||||
import Url.Builder as B
|
||||
|
||||
|
||||
url =
|
||||
{ base = "http://localhost"
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ response : String
|
||||
, things : Thing.Model
|
||||
, channels : Channel.Model
|
||||
, checkedThingsIds : List String
|
||||
, checkedChannelsIds : List String
|
||||
}
|
||||
|
||||
|
||||
initial : Model
|
||||
initial =
|
||||
{ response = ""
|
||||
, things = Thing.initial
|
||||
, channels = Channel.initial
|
||||
, checkedThingsIds = []
|
||||
, checkedChannelsIds = []
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= Connect
|
||||
| Disconnect
|
||||
| ThingMsg Thing.Msg
|
||||
| ChannelMsg Channel.Msg
|
||||
| GotResponse (Result Http.Error String)
|
||||
| CheckThing String
|
||||
| CheckChannel String
|
||||
|
||||
|
||||
update : Msg -> Model -> String -> ( Model, Cmd Msg )
|
||||
update msg model token =
|
||||
case msg of
|
||||
Connect ->
|
||||
if List.isEmpty model.checkedThingsIds || List.isEmpty model.checkedChannelsIds then
|
||||
( model, Cmd.none )
|
||||
|
||||
else
|
||||
( { model | checkedThingsIds = [], checkedChannelsIds = [] }
|
||||
, Cmd.batch (connect model.checkedThingsIds model.checkedChannelsIds "PUT" token)
|
||||
)
|
||||
|
||||
Disconnect ->
|
||||
if List.isEmpty model.checkedThingsIds || List.isEmpty model.checkedChannelsIds then
|
||||
( model, Cmd.none )
|
||||
|
||||
else
|
||||
( { model | checkedThingsIds = [], checkedChannelsIds = [] }
|
||||
, 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 id ->
|
||||
( { model | checkedThingsIds = Helpers.checkEntity id 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 []
|
||||
[ Card.config
|
||||
[]
|
||||
|> Card.headerH3 [] [ text "Things" ]
|
||||
|> 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 [] <| genThingRows model.checkedThingsIds model.things.things.list
|
||||
}
|
||||
)
|
||||
]
|
||||
|> Card.view
|
||||
, Html.map ThingMsg (Helpers.genPagination model.things.things.total Thing.SubmitPage)
|
||||
]
|
||||
, Grid.col []
|
||||
[ Card.config
|
||||
[]
|
||||
|> Card.headerH3 [] [ text "Channels" ]
|
||||
|> 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 [] <| genChannelRows model.checkedChannelsIds model.channels.channels.list
|
||||
}
|
||||
)
|
||||
]
|
||||
|> Card.view
|
||||
, Html.map ChannelMsg (Helpers.genPagination model.channels.channels.total Channel.SubmitPage)
|
||||
]
|
||||
]
|
||||
, Grid.row []
|
||||
[ Grid.col []
|
||||
[ 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 [] [ input [ type_ "checkbox", onClick (CheckThing thing.id), checked (Helpers.isChecked thing.id checkedThingsIds) ] [], text (" " ++ Helpers.parseString thing.name) ]
|
||||
, Table.td [] [ text thing.id ]
|
||||
]
|
||||
)
|
||||
things
|
||||
|
||||
|
||||
genChannelRows : List String -> List Channel.Channel -> List (Table.Row Msg)
|
||||
genChannelRows checkedChannelsIds channels =
|
||||
List.map
|
||||
(\channel ->
|
||||
Table.tr []
|
||||
[ Table.td [] [ input [ type_ "checkbox", onClick (CheckChannel channel.id), checked (Helpers.isChecked channel.id checkedChannelsIds) ] [], text (" " ++ Helpers.parseString channel.name) ]
|
||||
, Table.td [] [ text channel.id ]
|
||||
]
|
||||
)
|
||||
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.crossOrigin url.base [ "channels", channelId, "things", thingId ] [])
|
||||
method
|
||||
token
|
||||
Http.emptyBody
|
||||
GotResponse
|
||||
)
|
||||
checkedChannelsIds
|
||||
)
|
||||
checkedThingsIds
|
||||
)
|
28
ui/src/Error.elm
Normal file
28
ui/src/Error.elm
Normal file
@ -0,0 +1,28 @@
|
||||
-- Copyright (c) 2019
|
||||
-- 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 ->
|
||||
err
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user