mirror of
https://github.com/mainflux/mainflux.git
synced 2025-04-27 13:48:49 +08:00
MF-244 - Integrate Dashflux (#258)
This commit is contained in:
parent
db6165aa6b
commit
ff30957614
60
dashflux/.angular-cli.json
Normal file
60
dashflux/.angular-cli.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"name": "mainflux-ui"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.ico"
|
||||
],
|
||||
"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": {}
|
||||
}
|
||||
}
|
13
dashflux/.editorconfig
Normal file
13
dashflux/.editorconfig
Normal file
@ -0,0 +1,13 @@
|
||||
# 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
Normal file
58
dashflux/.gitignore
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
# 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
Normal file
201
dashflux/LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
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.
|
96
dashflux/README.md
Normal file
96
dashflux/README.md
Normal file
@ -0,0 +1,96 @@
|
||||
# Mainflux UI Dashboad
|
||||
|
||||
[](LICENSE)
|
||||
[][gitter]
|
||||
|
||||
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.
|
||||
|
||||
### Details
|
||||
To find out more about the Mainflux IoT, please see our [wiki][wiki].
|
||||
|
||||
### 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) Newst version with Webpack integration
|
||||
- - Depending on how Node is configured on your machine, you may need to run `sudo npm install
|
||||
- [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 localy, [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.
|
||||
|
||||
## Get Started
|
||||
|
||||
Clone this repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mainflux/dashflux
|
||||
```
|
||||
|
||||
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 the appropriate endpoint URL's in environment.ts (for local development will probably be 0.0.0.0:<port>)
|
||||
|
||||
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.**
|
||||
|
||||
### Docker
|
||||
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:
|
||||
```
|
||||
docker build -f docker/Dockerfile -t dashflux:dev --build-arg env=dev .
|
||||
```
|
||||
|
||||
Build image using the **production** environment:
|
||||
```
|
||||
docker build -t dashflux:prod .
|
||||
```
|
||||
You can test image running
|
||||
```
|
||||
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.
|
||||
|
||||
|
||||
### Development
|
||||
- Follow angular-cli [documentation](https://github.com/angular/angular-cli)
|
||||
- Follow [official angular style guide](https://angular.io/styleguide)
|
||||
|
||||
### Community
|
||||
#### Mailing lists
|
||||
Visit [mainflux][google-group] official mailing list.
|
||||
|
||||
#### IRC
|
||||
[Mainflux Gitter][gitter]
|
||||
|
||||
#### Twitter
|
||||
[@mainflux][twitter]
|
||||
|
||||
### License
|
||||
[Apache License, version 2.0](LICENSE)
|
||||
|
||||
[wiki]: https://github.com/Mainflux/mainflux/wiki
|
||||
[google-group]: https://groups.google.com/forum/#!forum/mainflux
|
||||
[twitter]: https://twitter.com/mainflux
|
||||
[gitter]: https://gitter.im/Mainflux/mainflux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
1
dashflux/docker/.dockerignore
Normal file
1
dashflux/docker/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
13
dashflux/docker/Dockerfile
Normal file
13
dashflux/docker/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
# Stage 0, based on Node.js, to build and compile Angular
|
||||
FROM node:8.6 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
|
||||
COPY --from=node /app/dist/ /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
8
dashflux/docker/nginx.conf
Normal file
8
dashflux/docker/nginx.conf
Normal file
@ -0,0 +1,8 @@
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
14
dashflux/e2e/app.e2e-spec.ts
Normal file
14
dashflux/e2e/app.e2e-spec.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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!');
|
||||
});
|
||||
});
|
11
dashflux/e2e/app.po.ts
Normal file
11
dashflux/e2e/app.po.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
14
dashflux/e2e/tsconfig.e2e.json
Normal file
14
dashflux/e2e/tsconfig.e2e.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
33
dashflux/karma.conf.js
Normal file
33
dashflux/karma.conf.js
Normal file
@ -0,0 +1,33 @@
|
||||
// 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
Normal file
10394
dashflux/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
dashflux/package.json
Normal file
57
dashflux/package.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
28
dashflux/protractor.conf.js
Normal file
28
dashflux/protractor.conf.js
Normal file
@ -0,0 +1,28 @@
|
||||
// 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 } }));
|
||||
}
|
||||
};
|
9
dashflux/proxy-config.json
Normal file
9
dashflux/proxy-config.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"/api/*": {
|
||||
"target": "http://localhost:8180",
|
||||
"secure":false,
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
}
|
||||
}
|
28
dashflux/src/app/app-routing.module.ts
Normal file
28
dashflux/src/app/app-routing.module.ts
Normal file
@ -0,0 +1,28 @@
|
||||
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 { ClientsComponent } from './components/clients/clients.component';
|
||||
|
||||
const routes: Route[] = [
|
||||
{ path: '', redirectTo: 'clients', pathMatch: 'full'},
|
||||
{ path: 'login', component: LoginComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'signup', component: SignupComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'clients', component: ClientsComponent, canActivate: [ProtectedGuard]},
|
||||
{ path: 'channels', component: ChannelsComponent, canActivate: [ProtectedGuard]}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forRoot(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule { }
|
15
dashflux/src/app/app.component.html
Normal file
15
dashflux/src/app/app.component.html
Normal file
@ -0,0 +1,15 @@
|
||||
<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]="['/clients']" mat-button>Clients</a>
|
||||
<a [routerLink]="['/channels']" mat-button>Channels</a>
|
||||
<a (click)="logout()" mat-button>Logout</a>
|
||||
</ng-container>
|
||||
|
||||
</mat-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
6
dashflux/src/app/app.component.scss
Normal file
6
dashflux/src/app/app.component.scss
Normal file
@ -0,0 +1,6 @@
|
||||
.loading {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
}
|
40
dashflux/src/app/app.component.spec.ts
Normal file
40
dashflux/src/app/app.component.spec.ts
Normal file
@ -0,0 +1,40 @@
|
||||
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 { ClientsService } from './core/services/clients/clients.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,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
});
|
33
dashflux/src/app/app.component.ts
Normal file
33
dashflux/src/app/app.component.ts
Normal file
@ -0,0 +1,33 @@
|
||||
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();
|
||||
}
|
||||
}
|
90
dashflux/src/app/app.module.ts
Normal file
90
dashflux/src/app/app.module.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import './rxjs-extensions.ts';
|
||||
import 'hammerjs';
|
||||
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
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 { AddChannelDialogComponent } from './components/channels/add-channel-dialog/add-channel-dialog.component';
|
||||
import { ChannelsComponent } from './components/channels/channels.component';
|
||||
import { AddClientDialogComponent } from './components/clients/add-client-dialog/add-client-dialog.component';
|
||||
import { ClientsComponent } from './components/clients/clients.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 { ClientsService } from './core/services/clients/clients.service';
|
||||
import { MockAuthService } from './core/services/mock-auth.service';
|
||||
import { MockChannelsService } from './core/services/mock-channels.service';
|
||||
import { MockClientsService } from './core/services/mock-clients.service';
|
||||
import { ChannelsStore } from './core/store/channels.store';
|
||||
import { ClientsStore } from './core/store/clients.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,
|
||||
ClientsComponent,
|
||||
ChannelsComponent,
|
||||
SignupComponent,
|
||||
LoginComponent,
|
||||
AddClientDialogComponent,
|
||||
ConfirmationDialogComponent,
|
||||
AddChannelDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
AuthModule,
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
MaterialModule,
|
||||
FlexLayoutModule,
|
||||
ReactiveFormsModule,
|
||||
MobxAngularModule,
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
ClientsStore,
|
||||
ChannelsStore,
|
||||
AuthStore,
|
||||
MockAuthService,
|
||||
MockClientsService,
|
||||
MockChannelsService,
|
||||
ClientsService,
|
||||
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 },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
entryComponents: [
|
||||
AddClientDialogComponent,
|
||||
AddChannelDialogComponent,
|
||||
ConfirmationDialogComponent
|
||||
]
|
||||
})
|
||||
export class AppModule { }
|
28
dashflux/src/app/components/auth/login/login.component.html
Normal file
28
dashflux/src/app/components/auth/login/login.component.html
Normal file
@ -0,0 +1,28 @@
|
||||
<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>
|
@ -0,0 +1,3 @@
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
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();
|
||||
}));
|
||||
});
|
42
dashflux/src/app/components/auth/login/login.component.ts
Normal file
42
dashflux/src/app/components/auth/login/login.component.ts
Normal file
@ -0,0 +1,42 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<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>
|
@ -0,0 +1,3 @@
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
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();
|
||||
});
|
||||
});
|
55
dashflux/src/app/components/auth/signup/signup.component.ts
Normal file
55
dashflux/src/app/components/auth/signup/signup.component.ts
Normal file
@ -0,0 +1,55 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<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 clients" formControlName="connected" [compareWith]="compareFunction" multiple>
|
||||
<mat-option *ngFor="let client of clientsStore.clients" [value]="client">{{client.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>
|
@ -0,0 +1,56 @@
|
||||
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 { ClientsService } from '../../../core/services/clients/clients.service';
|
||||
import { AddChannelDialogComponent } from './add-channel-dialog.component';
|
||||
import { ClientsStore } from '../../../core/store/clients.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
describe('AddChannelDialogComponent', () => {
|
||||
let component: AddChannelDialogComponent;
|
||||
let fixture: ComponentFixture<AddChannelDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddChannelDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
ClientsStore,
|
||||
UiStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddChannelDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,52 @@
|
||||
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 { ClientsStore } from '../../../core/store/clients.store';
|
||||
import { Channel, Client } from '../../../core/store/models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-channel-dialog',
|
||||
templateUrl: './add-channel-dialog.component.html',
|
||||
styleUrls: ['./add-channel-dialog.component.scss']
|
||||
})
|
||||
export class AddChannelDialogComponent implements OnInit {
|
||||
addChannelForm: FormGroup;
|
||||
@Output() submit: EventEmitter<Channel> = new EventEmitter<Channel>();
|
||||
editMode: boolean;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<AddChannelDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Channel,
|
||||
public clientsStore: ClientsStore,
|
||||
) { }
|
||||
|
||||
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: Client, obj2: Client) {
|
||||
return obj1.id === obj2.id;
|
||||
}
|
||||
}
|
49
dashflux/src/app/components/channels/channels.component.html
Normal file
49
dashflux/src/app/components/channels/channels.component.html
Normal file
@ -0,0 +1,49 @@
|
||||
<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>
|
10
dashflux/src/app/components/channels/channels.component.scss
Normal file
10
dashflux/src/app/components/channels/channels.component.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.addButton {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
/* List container */
|
||||
.channel-list-container {
|
||||
width: 100%;
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
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 { ClientsService } from '../../core/services/clients/clients.service';
|
||||
import { ChannelsComponent } from './channels.component';
|
||||
import { ClientsStore } from '../../core/store/clients.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: ClientsStore,
|
||||
useClass: class {
|
||||
getClients = jasmine.createSpy('getClients');
|
||||
}
|
||||
},
|
||||
UiStore,
|
||||
{
|
||||
provide: ChannelsStore,
|
||||
useClass: class {
|
||||
getChannels = jasmine.createSpy('getChannels');
|
||||
}
|
||||
},
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
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();
|
||||
});
|
||||
});
|
62
dashflux/src/app/components/channels/channels.component.ts
Normal file
62
dashflux/src/app/components/channels/channels.component.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
import { ClientsStore } from '../../core/store/clients.store';
|
||||
import { Channel } from '../../core/store/models';
|
||||
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { AddChannelDialogComponent } from './add-channel-dialog/add-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 clientsStore: ClientsStore,
|
||||
public channelsStore: ChannelsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.channelsStore.getChannels();
|
||||
this.clientsStore.getClients();
|
||||
}
|
||||
|
||||
addChannel() {
|
||||
const dialogRef = this.dialog.open(AddChannelDialogComponent);
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((channel: Channel) => {
|
||||
this.channelsStore.addChannel(channel);
|
||||
});
|
||||
}
|
||||
|
||||
editChannel(channel: Channel) {
|
||||
const dialogRef = this.dialog.open(AddChannelDialogComponent, {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<form [formGroup]="addClientForm" (ngSubmit)="onAddClient()">
|
||||
<h1 mat-dialog-title>Add client</h1>
|
||||
<div mat-dialog-content fxLayout="column">
|
||||
<input type="hidden" formControlName="id">
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Client type" formControlName="type">
|
||||
<mat-option value="app">
|
||||
App
|
||||
</mat-option>
|
||||
<mat-option value="device">
|
||||
Device
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="addClientForm.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="addClientForm.get('name').errors?.required">
|
||||
Name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea formControlName="payload" matInput placeholder="Payload" 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]="addClientForm.invalid">Ok</button>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,52 @@
|
||||
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 { ClientsService } from '../../../core/services/clients/clients.service';
|
||||
import { AddClientDialogComponent } from './add-client-dialog.component';
|
||||
|
||||
describe('AddClientDialogComponent', () => {
|
||||
let component: AddClientDialogComponent;
|
||||
let fixture: ComponentFixture<AddClientDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddClientDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddClientDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
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 { Client } from '../../../core/store/models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-client-dialog',
|
||||
templateUrl: './add-client-dialog.component.html',
|
||||
styleUrls: ['./add-client-dialog.component.scss']
|
||||
})
|
||||
export class AddClientDialogComponent implements OnInit {
|
||||
addClientForm: FormGroup;
|
||||
@Output() submit: EventEmitter<Client> = new EventEmitter<Client>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<AddClientDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Client
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.addClientForm = this.fb.group(
|
||||
{
|
||||
id: [''],
|
||||
type: ['', [Validators.required]],
|
||||
name: ['', [Validators.required, Validators.minLength(5)]],
|
||||
payload: ['']
|
||||
}
|
||||
);
|
||||
|
||||
if (this.data) {
|
||||
this.addClientForm.patchValue(this.data);
|
||||
this.addClientForm.get('payload').patchValue(JSON.stringify(this.data.payload));
|
||||
}
|
||||
}
|
||||
|
||||
onAddClient() {
|
||||
const client = this.addClientForm.value;
|
||||
this.submit.emit(client);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
51
dashflux/src/app/components/clients/clients.component.html
Normal file
51
dashflux/src/app/components/clients/clients.component.html
Normal file
@ -0,0 +1,51 @@
|
||||
<div class="clientsContainer" fxLayout="row" fxLayoutWrap>
|
||||
<ng-container [ngSwitch]="clientsStore.clients?.length > 0">
|
||||
<ng-container *ngSwitchCase="true">
|
||||
<!-- List -->
|
||||
<div class="client-list-container">
|
||||
<mat-table #table [dataSource]="clientsStore.clients.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="payload">
|
||||
<mat-header-cell fxFlex="35%" *matHeaderCellDef mat-sort-header>Payload</mat-header-cell>
|
||||
<mat-cell fxFlex="35%" *matCellDef="let row"> {{row.payload | 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)="editClient(row)">
|
||||
<mat-icon aria-label="Delete">edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="accent" (click)="deleteClient(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)="addClient()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
10
dashflux/src/app/components/clients/clients.component.scss
Normal file
10
dashflux/src/app/components/clients/clients.component.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.addButton {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
/* List container */
|
||||
.client-list-container {
|
||||
width: 100%;
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
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 { ClientsService } from '../../core/services/clients/clients.service';
|
||||
import { ClientsComponent } from './clients.component';
|
||||
import { UiStore } from '../../core/store/ui.store';
|
||||
import { ClientsStore } from '../../core/store/clients.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
describe('ClientsComponent', () => {
|
||||
let component: ClientsComponent;
|
||||
let fixture: ComponentFixture<ClientsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ClientsComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
{
|
||||
provide: ClientsStore,
|
||||
useClass: class {
|
||||
getClients = jasmine.createSpy('getClients');
|
||||
}
|
||||
},
|
||||
ChannelsStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ClientsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
63
dashflux/src/app/components/clients/clients.component.ts
Normal file
63
dashflux/src/app/components/clients/clients.component.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { toJS } from 'mobx';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Client } from '../../core/store/models';
|
||||
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { AddClientDialogComponent } from './add-client-dialog/add-client-dialog.component';
|
||||
import { ClientsStore } from '../../core/store/clients.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clients',
|
||||
templateUrl: './clients.component.html',
|
||||
styleUrls: ['./clients.component.scss']
|
||||
})
|
||||
export class ClientsComponent implements OnInit {
|
||||
clients: Observable<Client[]>;
|
||||
displayedColumns = ['id', 'name', 'type', 'payload', 'actions'];
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
public clientsStore: ClientsStore,
|
||||
public channelsStore: ChannelsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.clientsStore.getClients();
|
||||
this.channelsStore.getChannels();
|
||||
}
|
||||
|
||||
addClient() {
|
||||
const dialogRef = this.dialog.open(AddClientDialogComponent);
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((client: Client) => {
|
||||
this.clientsStore.addClient(client);
|
||||
});
|
||||
}
|
||||
|
||||
editClient(client: Client) {
|
||||
const dialogRef = this.dialog.open(AddClientDialogComponent, {
|
||||
data: client
|
||||
});
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((editedClient: Client) => {
|
||||
this.clientsStore.editClient(toJS(editedClient));
|
||||
});
|
||||
}
|
||||
|
||||
deleteClient(client: Client) {
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
question: 'Are you sure you want to delete the client?'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.clientsStore.deleteClient(toJS(client));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<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>
|
@ -0,0 +1,40 @@
|
||||
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();
|
||||
});
|
||||
});
|
@ -0,0 +1,16 @@
|
||||
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() {
|
||||
}
|
||||
|
||||
}
|
71
dashflux/src/app/core/material/material.module.ts
Normal file
71
dashflux/src/app/core/material/material.module.ts
Normal file
@ -0,0 +1,71 @@
|
||||
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 { }
|
144
dashflux/src/app/core/services/auth/authentication.service.ts
Normal file
144
dashflux/src/app/core/services/auth/authentication.service.ts
Normal file
@ -0,0 +1,144 @@
|
||||
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);
|
||||
}
|
||||
}
|
52
dashflux/src/app/core/services/auth/token-storage.service.ts
Normal file
52
dashflux/src/app/core/services/auth/token-storage.service.ts
Normal file
@ -0,0 +1,52 @@
|
||||
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');
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
103
dashflux/src/app/core/services/channels/channels.service.ts
Normal file
103
dashflux/src/app/core/services/channels/channels.service.ts
Normal file
@ -0,0 +1,103 @@
|
||||
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, Client } 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
|
||||
};
|
||||
|
||||
return this.http.post(environment.channelsUrl, payload, { observe: 'response' })
|
||||
.switchMap((res) => {
|
||||
const id = this.getChannelIdFrom(res);
|
||||
return forkJoin(this.createClientConnectRequests(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 clientsToAdd = this.getClientsToAdd(channelFormData, channel);
|
||||
if (clientsToAdd.length) {
|
||||
return forkJoin(this.createClientConnectRequests(channel.id, clientsToAdd));
|
||||
} else {
|
||||
return Observable.of([]);
|
||||
}
|
||||
}).switchMap(() => {
|
||||
const clientsToDelete = this.getClientsToDelete(channelFormData, channel);
|
||||
console.log(clientsToDelete);
|
||||
if (clientsToDelete.length) {
|
||||
return forkJoin(this.createClientDisconnectRequests(channel.id, clientsToDelete));
|
||||
} else {
|
||||
return Observable.of([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getClientsToDelete(channelFormData: Channel, channel: Channel) {
|
||||
return channel.connected.filter(client => {
|
||||
return channelFormData.connected.find(cl => cl.id === client.id) === undefined;
|
||||
});
|
||||
}
|
||||
|
||||
getClientsToAdd(channelFormData: Channel, channel: Channel) {
|
||||
return channelFormData.connected.filter(client => {
|
||||
return channel.connected.find(cl => cl.id === client.id) === undefined;
|
||||
});
|
||||
}
|
||||
|
||||
createClientConnectRequests(channelId: string , connected: Client[]) {
|
||||
return connected.map((connection) => {
|
||||
return this.http.put(environment.channelsUrl + '/' + channelId + '/clients/' + connection.id, {});
|
||||
});
|
||||
}
|
||||
|
||||
createClientDisconnectRequests(channelId: string , connected: Client[]) {
|
||||
return connected.map((connection) => {
|
||||
return this.http.delete(environment.channelsUrl + '/' + channelId + '/clients/' + connection.id, {});
|
||||
});
|
||||
}
|
||||
}
|
27
dashflux/src/app/core/services/clients/clients.service.ts
Normal file
27
dashflux/src/app/core/services/clients/clients.service.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { Client } from '../../store/models';
|
||||
|
||||
@Injectable()
|
||||
export class ClientsService {
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
getClients() {
|
||||
return this.http.get(environment.clientsUrl);
|
||||
}
|
||||
|
||||
addClient(client: Client) {
|
||||
return this.http.post(environment.clientsUrl, client);
|
||||
}
|
||||
|
||||
deleteClient(client: Client) {
|
||||
return this.http.delete(environment.clientsUrl + '/' + client.id);
|
||||
}
|
||||
|
||||
editClient(client: Client) {
|
||||
return this.http.put(environment.clientsUrl + '/' + client.id, client);
|
||||
}
|
||||
}
|
46
dashflux/src/app/core/services/mock-auth.service.ts
Normal file
46
dashflux/src/app/core/services/mock-auth.service.ts
Normal file
@ -0,0 +1,46 @@
|
||||
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'));
|
||||
}
|
||||
}
|
27
dashflux/src/app/core/services/mock-channels.service.ts
Normal file
27
dashflux/src/app/core/services/mock-channels.service.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Client } 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: Client) {
|
||||
MOCK_CHANNELS.channels.push(client);
|
||||
return Observable.of(1).delay(1000);
|
||||
}
|
||||
}
|
19
dashflux/src/app/core/services/mock-clients.service.ts
Normal file
19
dashflux/src/app/core/services/mock-clients.service.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Client } from '../store/models';
|
||||
|
||||
let MOCK_CLIENTS = [
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class MockClientsService {
|
||||
getClients() {
|
||||
return Observable.of(MOCK_CLIENTS).delay(1000);
|
||||
}
|
||||
|
||||
addClient(client: Client) {
|
||||
MOCK_CLIENTS.push(client);
|
||||
return Observable.of(1).delay(1000);
|
||||
}
|
||||
}
|
178
dashflux/src/app/core/store/auth.store.spec.ts
Normal file
178
dashflux/src/app/core/store/auth.store.spec.ts
Normal file
@ -0,0 +1,178 @@
|
||||
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 { ClientsService } from '../services/clients/clients.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,
|
||||
ClientsService,
|
||||
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 /clients 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(['/clients']);
|
||||
}));
|
||||
|
||||
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']);
|
||||
}));
|
||||
});
|
||||
});
|
63
dashflux/src/app/core/store/auth.store.ts
Normal file
63
dashflux/src/app/core/store/auth.store.ts
Normal file
@ -0,0 +1,63 @@
|
||||
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(['/clients']);
|
||||
},
|
||||
(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']);
|
||||
}
|
||||
}
|
247
dashflux/src/app/core/store/channels.store.spec.ts
Normal file
247
dashflux/src/app/core/store/channels.store.spec.ts
Normal file
@ -0,0 +1,247 @@
|
||||
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();
|
||||
}));
|
||||
});
|
||||
});
|
68
dashflux/src/app/core/store/channels.store.ts
Normal file
68
dashflux/src/app/core/store/channels.store.ts
Normal file
@ -0,0 +1,68 @@
|
||||
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;
|
||||
this.channelsService.addChannel(channel)
|
||||
.subscribe(() => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
260
dashflux/src/app/core/store/clients.store.spec.ts
Normal file
260
dashflux/src/app/core/store/clients.store.spec.ts
Normal file
@ -0,0 +1,260 @@
|
||||
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 { ClientsService } from '../services/clients/clients.service';
|
||||
import { ClientsStore } from './clients.store';
|
||||
import { Client } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
describe('ClientsStore', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
providers: [
|
||||
ClientsStore,
|
||||
UiStore,
|
||||
ClientsService,
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ClientsStore], (clientsStore: ClientsStore) => {
|
||||
expect(clientsStore).toBeTruthy();
|
||||
}));
|
||||
|
||||
describe('getClients', () => {
|
||||
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const getClients = spyOn(clientsService, 'getClients').and.returnValue({ subscribe: () => { } });
|
||||
|
||||
clientsStore.getClients();
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful get', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const getClients = spyOn(clientsService, 'getClients').and.returnValue(Observable.of(true));
|
||||
|
||||
clientsStore.getClients();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the clients property to the returned clients from the service', inject([ClientsStore, ClientsService],
|
||||
(clientsStore: ClientsStore, clientsService: ClientsService) => {
|
||||
const serviceReturnValue = { clients: [] };
|
||||
const getChannels = spyOn(clientsService, 'getClients').and.returnValue(Observable.of(serviceReturnValue));
|
||||
|
||||
clientsStore.getClients();
|
||||
|
||||
expect(toJS(clientsStore.clients)).toEqual(serviceReturnValue.clients);
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed get', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const getClients = spyOn(clientsService, 'getClients').and.returnValue(Observable.throw(''));
|
||||
|
||||
clientsStore.getClients();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('addClient', () => {
|
||||
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const addClient = spyOn(clientsService, 'addClient').and.returnValue({ subscribe: () => { } });
|
||||
const newClient: Client = {
|
||||
name: 'new client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.addClient(newClient);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const addClient = spyOn(clientsService, 'addClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
const newClient: Client = {
|
||||
name: 'new client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.addClient(newClient);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the ClientsStore.getClients after successful add', inject([ClientsStore, ClientsService],
|
||||
(clientsStore: ClientsStore, clientsService: ClientsService) => {
|
||||
const addClient = spyOn(clientsService, 'addClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const newClient: Client = {
|
||||
name: 'new client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.addClient(newClient);
|
||||
|
||||
expect(storeGetClientsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed add', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const addClient = spyOn(clientsService, 'addClient').and.returnValue(Observable.throw(''));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const newClient: Client = {
|
||||
name: 'new client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.addClient(newClient);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('editClient', () => {
|
||||
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const editClient = spyOn(clientsService, 'editClient').and.returnValue({ subscribe: () => { } });
|
||||
const editedClient: Client = {
|
||||
name: 'edited client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.editClient(editedClient);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const editClient = spyOn(clientsService, 'editClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
const editedClient: Client = {
|
||||
name: 'edited client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.editClient(editedClient);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the ClientsStore.getChannels after successful edit', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const editClient = spyOn(clientsService, 'editClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const editedClient: Client = {
|
||||
name: 'edited client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
|
||||
clientsStore.editClient(editedClient);
|
||||
|
||||
expect(storeGetClientsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed edit', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const editClient = spyOn(clientsService, 'editClient').and.returnValue(Observable.throw(''));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const editedClient: Client = {
|
||||
name: 'edited client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.editClient(editedClient);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('deleteClient', () => {
|
||||
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue({ subscribe: () => { } });
|
||||
const clientToBeDeleted: Client = {
|
||||
name: 'clientToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
clientsStore.deleteClient(clientToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
const clientToBeDeleted: Client = {
|
||||
name: 'clientToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
|
||||
clientsStore.deleteClient(clientToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the clientsStore.getChannels after successful add', inject([ClientsStore, ClientsService],
|
||||
(clientsStore: ClientsStore, clientsService: ClientsService) => {
|
||||
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const clientToBeDeleted: Client = {
|
||||
name: 'clientToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
clientsStore.deleteClient(clientToBeDeleted);
|
||||
|
||||
expect(storeGetClientsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed delete', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue(Observable.throw(''));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
const clientToBeDeleted: Client = {
|
||||
name: 'clientToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
|
||||
clientsStore.deleteClient(clientToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
});
|
64
dashflux/src/app/core/store/clients.store.ts
Normal file
64
dashflux/src/app/core/store/clients.store.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { action, observable } from 'mobx';
|
||||
|
||||
import { ClientsService } from '../services/clients/clients.service';
|
||||
import { Client } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
@Injectable()
|
||||
export class ClientsStore {
|
||||
@observable clients: Client[] = [];
|
||||
|
||||
constructor(
|
||||
private uiState: UiStore,
|
||||
private clientsService: ClientsService,
|
||||
) { }
|
||||
|
||||
@action
|
||||
getClients() {
|
||||
this.uiState.loading = true;
|
||||
this.clientsService.getClients()
|
||||
.subscribe((payload: any) => {
|
||||
this.uiState.loading = false;
|
||||
this.clients = payload.clients;
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
addClient(client: Client) {
|
||||
this.uiState.loading = true;
|
||||
this.clientsService.addClient(client)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getClients();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
editClient(client: Client) {
|
||||
this.uiState.loading = true;
|
||||
this.clientsService.editClient(client)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getClients();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
deleteClient(client: Client) {
|
||||
this.uiState.loading = true;
|
||||
this.clientsService.deleteClient(client)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getClients();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
17
dashflux/src/app/core/store/models.ts
Normal file
17
dashflux/src/app/core/store/models.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export interface Channel {
|
||||
id?: '';
|
||||
name: string;
|
||||
connected: Client[];
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
id?: '';
|
||||
type: string;
|
||||
name: string;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
57
dashflux/src/app/core/store/ui.store.spec.ts
Normal file
57
dashflux/src/app/core/store/ui.store.spec.ts
Normal file
@ -0,0 +1,57 @@
|
||||
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 { ClientsService } from '../services/clients/clients.service';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
describe('State', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
UiStore,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
ClientsService,
|
||||
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']);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
23
dashflux/src/app/core/store/ui.store.ts
Normal file
23
dashflux/src/app/core/store/ui.store.ts
Normal file
@ -0,0 +1,23 @@
|
||||
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']);
|
||||
}
|
||||
}
|
19
dashflux/src/app/rxjs-extensions.ts
Normal file
19
dashflux/src/app/rxjs-extensions.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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';
|
0
dashflux/src/assets/.gitkeep
Normal file
0
dashflux/src/assets/.gitkeep
Normal file
3
dashflux/src/environments/environment.prod.ts
Normal file
3
dashflux/src/environments/environment.prod.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
12
dashflux/src/environments/environment.ts
Normal file
12
dashflux/src/environments/environment.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// 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: '/api/users',
|
||||
loginUrl: '/api/tokens',
|
||||
clientsUrl: '/api/clients',
|
||||
channelsUrl: '/api/channels'
|
||||
};
|
BIN
dashflux/src/favicon.ico
Normal file
BIN
dashflux/src/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
15
dashflux/src/index.html
Normal file
15
dashflux/src/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!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.ico">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
12
dashflux/src/main.ts
Normal file
12
dashflux/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
||||
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));
|
76
dashflux/src/polyfills.ts
Normal file
76
dashflux/src/polyfills.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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
dashflux/src/styles.scss
Normal file
1
dashflux/src/styles.scss
Normal file
@ -0,0 +1 @@
|
||||
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
|
32
dashflux/src/test.ts
Normal file
32
dashflux/src/test.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// 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();
|
14
dashflux/src/tsconfig.app.json
Normal file
14
dashflux/src/tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"baseUrl": "./",
|
||||
"module": "es2015",
|
||||
"types": []
|
||||
},
|
||||
"include": [ "../node_modules/ngx-auth/**/*", "**/*"],
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
28
dashflux/src/tsconfig.spec.json
Normal file
28
dashflux/src/tsconfig.spec.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"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
Normal file
5
dashflux/src/typings.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/* SystemJS module definition */
|
||||
declare var module: NodeModule;
|
||||
interface NodeModule {
|
||||
id: string;
|
||||
}
|
19
dashflux/tsconfig.json
Normal file
19
dashflux/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
141
dashflux/tslint.json
Normal file
141
dashflux/tslint.json
Normal file
@ -0,0 +1,141 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user