newgui: Merge separate repo into syncthing/syncthing
Co-authored-by: Audrius Butkevicius <audrius.butkevicius@gmail.com> Co-authored-by: Simon Frei <freisim93@gmail.com>
This commit is contained in:
commit
0471daf771
|
@ -0,0 +1,13 @@
|
||||||
|
# Editor configuration, see https://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
|
|
@ -0,0 +1,46 @@
|
||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
# Only exists if Bazel was run
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# profiling files
|
||||||
|
chrome-profiler-events*.json
|
||||||
|
speed-measure-plugin*.json
|
||||||
|
|
||||||
|
# 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
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
|
@ -0,0 +1,373 @@
|
||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Syncthing Tech UI
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This is a very bare bones read-only GUI for viewing the status of large
|
||||||
|
setups. Download a [release
|
||||||
|
zip](https://github.com/kastelo/syncthing-tech-ui/releases) and unpack it
|
||||||
|
into the GUI override directory (assuming default Linux setup):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd ~/.config/syncthing
|
||||||
|
$ mkdir -p gui/default
|
||||||
|
$ cd gui/default
|
||||||
|
$ unzip ~/tech-ui-v1.0.0.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Then load the GUI via http://localhost:8384/tech-ui/ or similar. You should see something like this:
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `npm run serve` for a dev server. Navigate to `http://localhost:4200/`. The
|
||||||
|
app will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Production server
|
||||||
|
|
||||||
|
In production we serve the UI through Syncthing itself. The easiest way to
|
||||||
|
do that is to simply put the built assets in the `gui` subdirectory of
|
||||||
|
Syncthing's config directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run build -- --prod
|
||||||
|
$ rsync -va --delete dist/tech-ui/ ~/.config/syncthing/gui/default/tech-ui/
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust for your actual Syncthing config dir if different. Navigate to
|
||||||
|
`http://localhost:8384/tech-ui/`.
|
||||||
|
|
||||||
|
Another option is to start Syncthing with the STGUIASSETS environment
|
||||||
|
variable pointing to the distribution directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run build -- --prod
|
||||||
|
$ ln -sf . dist/default
|
||||||
|
$ export STGUIASSETS=$(pwd)/dist
|
||||||
|
$ syncthing
|
||||||
|
```
|
||||||
|
|
||||||
|
The magic is symlink is because Syncthing will look for the GUI in the
|
||||||
|
`default` subdirectory. Navigate to `http://localhost:8384/tech-ui/`.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You
|
||||||
|
can also use `ng generate
|
||||||
|
directive|pipe|service|class|guard|interface|enum|module`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MPLv2
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
Copyright (c) 2020 The Syncthing Authors
|
|
@ -0,0 +1,128 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"tech-ui": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/tech-ui",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"aot": true,
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"extractCss": true,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "tech-ui:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "tech-ui:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "tech-ui:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"karmaConfig": "karma.conf.js",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": [
|
||||||
|
"tsconfig.app.json",
|
||||||
|
"tsconfig.spec.json",
|
||||||
|
"e2e/tsconfig.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
|
"options": {
|
||||||
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
|
"devServerTarget": "tech-ui:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "tech-ui:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
"defaultProject": "tech-ui"
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
> 0.5%
|
||||||
|
last 2 versions
|
||||||
|
Firefox ESR
|
||||||
|
not dead
|
||||||
|
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
|
@ -0,0 +1,32 @@
|
||||||
|
// @ts-check
|
||||||
|
// Protractor configuration file, see link for more information
|
||||||
|
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||||
|
|
||||||
|
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type { import("protractor").Config }
|
||||||
|
*/
|
||||||
|
exports.config = {
|
||||||
|
allScriptsTimeout: 11000,
|
||||||
|
specs: [
|
||||||
|
'./src/**/*.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: require('path').join(__dirname, './tsconfig.json')
|
||||||
|
});
|
||||||
|
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { AppPage } from './app.po';
|
||||||
|
import { browser, logging } from 'protractor';
|
||||||
|
|
||||||
|
describe('workspace-project App', () => {
|
||||||
|
let page: AppPage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new AppPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display welcome message', () => {
|
||||||
|
page.navigateTo();
|
||||||
|
expect(page.getTitleText()).toEqual('tech-ui app is running!');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Assert that there are no errors emitted from the browser
|
||||||
|
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||||
|
expect(logs).not.toContain(jasmine.objectContaining({
|
||||||
|
level: logging.Level.SEVERE,
|
||||||
|
} as logging.Entry));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { browser, by, element } from 'protractor';
|
||||||
|
|
||||||
|
export class AppPage {
|
||||||
|
navigateTo(): Promise<unknown> {
|
||||||
|
return browser.get(browser.baseUrl) as Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitleText(): Promise<string> {
|
||||||
|
return element(by.css('app-root .content span')).getText() as Promise<string>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/e2e",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"types": [
|
||||||
|
"jasmine",
|
||||||
|
"jasminewd2",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 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-devkit/build-angular'],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-jasmine-html-reporter'),
|
||||||
|
require('karma-coverage-istanbul-reporter'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
client: {
|
||||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
|
},
|
||||||
|
coverageIstanbulReporter: {
|
||||||
|
dir: require('path').join(__dirname, './coverage/tech-ui'),
|
||||||
|
reports: ['html', 'lcovonly', 'text-summary'],
|
||||||
|
fixWebpackSourcePaths: true
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'kjhtml'],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Chrome'],
|
||||||
|
singleRun: false,
|
||||||
|
restartOnFileChange: true
|
||||||
|
});
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"name": "tech-ui",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"test": "ng test",
|
||||||
|
"lint": "ng lint",
|
||||||
|
"e2e": "ng e2e"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^9.1.0",
|
||||||
|
"@angular/cdk": "^9.2.0",
|
||||||
|
"@angular/common": "^9.1.0",
|
||||||
|
"@angular/compiler": "^9.1.0",
|
||||||
|
"@angular/core": "^9.1.0",
|
||||||
|
"@angular/flex-layout": "^9.0.0-beta.29",
|
||||||
|
"@angular/forms": "^9.1.0",
|
||||||
|
"@angular/material": "^9.2.0",
|
||||||
|
"@angular/platform-browser": "^9.1.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^9.1.0",
|
||||||
|
"@angular/router": "^9.1.0",
|
||||||
|
"angular-in-memory-web-api": "^0.10.0",
|
||||||
|
"chart.js": "^2.9.3",
|
||||||
|
"component": "^1.1.0",
|
||||||
|
"rxjs": "^6.5.5",
|
||||||
|
"tslib": "^1.10.0",
|
||||||
|
"zone.js": "^0.10.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^0.901.0",
|
||||||
|
"@angular/cli": "^9.1.0",
|
||||||
|
"@angular/compiler-cli": "^9.1.0",
|
||||||
|
"@angular/language-service": "^9.1.0",
|
||||||
|
"@types/jasmine": "^3.5.10",
|
||||||
|
"@types/jasminewd2": "~2.0.3",
|
||||||
|
"@types/node": "^12.12.34",
|
||||||
|
"codelyzer": "^5.2.2",
|
||||||
|
"jasmine-core": "~3.5.0",
|
||||||
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
|
"karma": "~4.3.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage-istanbul-reporter": "~2.1.0",
|
||||||
|
"karma-jasmine": "~2.0.1",
|
||||||
|
"karma-jasmine-html-reporter": "^1.5.3",
|
||||||
|
"protractor": "~5.4.3",
|
||||||
|
"ts-node": "~8.3.0",
|
||||||
|
"tslint": "~5.18.0",
|
||||||
|
"typescript": "~3.7.5"
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 734 KiB |
|
@ -0,0 +1,9 @@
|
||||||
|
import { environment } from '../environments/environment'
|
||||||
|
|
||||||
|
export const deviceID = (): String => {
|
||||||
|
const dID: String = environment.production ? globalThis.metadata['deviceID'] : '12345';
|
||||||
|
return dID.substring(0, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiURL: String = '/'
|
||||||
|
export const apiRetry: number = 3;
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
|
||||||
|
const routes: Routes = [];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
|
@ -0,0 +1 @@
|
||||||
|
<app-dashboard></app-dashboard>
|
|
@ -0,0 +1,9 @@
|
||||||
|
/* Structure */
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-form-field {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { TestBed, async } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
constructor() { }
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatListModule } from '@angular/material/list'
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||||
|
|
||||||
|
import { httpInterceptorProviders } from './http-interceptors';
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
import { StatusListComponent } from './lists/status-list/status-list.component';
|
||||||
|
import { DeviceListComponent } from './lists/device-list/device-list.component';
|
||||||
|
import { DonutChartComponent } from './charts/donut-chart/donut-chart.component';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
import { ListToggleComponent } from './list-toggle/list-toggle.component';
|
||||||
|
|
||||||
|
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
|
||||||
|
import { InMemoryConfigDataService } from './services/in-memory-config-data.service';
|
||||||
|
|
||||||
|
import { deviceID } from './api-utils';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
import { ChartItemComponent } from './charts/chart-item/chart-item.component';
|
||||||
|
import { ChartComponent } from './charts/chart/chart.component';
|
||||||
|
import { FolderListComponent } from './lists/folder-list/folder-list.component';
|
||||||
|
import { DialogComponent } from './dialog/dialog.component';
|
||||||
|
import { CardComponent, CardTitleComponent, CardContentComponent } from './card/card.component';
|
||||||
|
import { TrimPipe } from './trim.pipe';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
StatusListComponent,
|
||||||
|
DeviceListComponent,
|
||||||
|
ListToggleComponent,
|
||||||
|
DashboardComponent,
|
||||||
|
DonutChartComponent,
|
||||||
|
ChartComponent,
|
||||||
|
ChartItemComponent,
|
||||||
|
FolderListComponent,
|
||||||
|
DialogComponent,
|
||||||
|
CardComponent,
|
||||||
|
CardTitleComponent,
|
||||||
|
CardContentComponent,
|
||||||
|
TrimPipe,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatPaginatorModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatButtonToggleModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatListModule,
|
||||||
|
MatButtonModule,
|
||||||
|
FlexLayoutModule,
|
||||||
|
HttpClientModule,
|
||||||
|
HttpClientXsrfModule.withOptions({
|
||||||
|
headerName: 'X-CSRF-Token-' + deviceID(),
|
||||||
|
cookieName: 'CSRF-Token-' + deviceID(),
|
||||||
|
}),
|
||||||
|
environment.production ?
|
||||||
|
[] : HttpClientInMemoryWebApiModule.forRoot(InMemoryConfigDataService,
|
||||||
|
{ dataEncapsulation: false, delay: 10 }),
|
||||||
|
],
|
||||||
|
providers: [httpInterceptorProviders],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class AppModule { }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Import theming functions
|
||||||
|
@import '~@angular/material/theming';
|
||||||
|
|
||||||
|
@mixin tui-card-theme($theme) {
|
||||||
|
// Extract the palettes you need from the theme definition.
|
||||||
|
$primary: map-get($theme, primary);
|
||||||
|
$accent: map-get($theme, accent);
|
||||||
|
$background: map-get($theme, background);
|
||||||
|
$foreground: map-get($theme, foreground);
|
||||||
|
|
||||||
|
.tui-card {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-card-title {
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
font-size: mat-font-size($tech-ui-typography, subheading-2);
|
||||||
|
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
|
||||||
|
color: mat-color($primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-card-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-button-toggle .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.tui-card {
|
||||||
|
background-color: map_get($mat-grey, 800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CardComponent } from './card.component';
|
||||||
|
|
||||||
|
describe('CardComponent', () => {
|
||||||
|
let component: CardComponent;
|
||||||
|
let fixture: ComponentFixture<CardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ CardComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { cardElevation } from '../style';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-card',
|
||||||
|
template: '<div class="{{elevation}} tui-card"><ng-content></ng-content></div>',
|
||||||
|
styleUrls: ['./card.component.scss']
|
||||||
|
})
|
||||||
|
export class CardComponent implements OnInit {
|
||||||
|
elevation: string = cardElevation;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-card-title',
|
||||||
|
template: '<div class="tui-card-title"><ng-content></ng-content></div>',
|
||||||
|
styleUrls: ['./card.component.scss']
|
||||||
|
})
|
||||||
|
export class CardTitleComponent {
|
||||||
|
constructor() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-card-content',
|
||||||
|
template: '<div class="tui-card-content"><ng-content></ng-content></div>',
|
||||||
|
styleUrls: ['./card.component.scss']
|
||||||
|
})
|
||||||
|
export class CardContentComponent {
|
||||||
|
constructor() { }
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<div fxLayout="row" fxLayoutAlign="space-between start" [ngClass]="(_selected)?'item selected':'item'">
|
||||||
|
<div><a href="#">{{state}}</a>: </div>
|
||||||
|
<div>{{count}}</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,27 @@
|
||||||
|
@mixin chart-item-theme($theme) {
|
||||||
|
.item {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px 7px 3px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: #DDDDDD;
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
.selected a {
|
||||||
|
color: #303030;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.selected {
|
||||||
|
background-color: map_get($mat-grey, 900);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChartItemComponent } from './chart-item.component';
|
||||||
|
|
||||||
|
describe('ChartItemComponent', () => {
|
||||||
|
let component: ChartItemComponent;
|
||||||
|
let fixture: ComponentFixture<ChartItemComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ ChartItemComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ChartItemComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-item',
|
||||||
|
templateUrl: './chart-item.component.html',
|
||||||
|
styleUrls: ['./chart-item.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartItemComponent {
|
||||||
|
@Input() state: string;
|
||||||
|
@Input() count: number;
|
||||||
|
@Input('selected')
|
||||||
|
set selected(s: boolean) {
|
||||||
|
this._selected = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selected: boolean = true;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<app-card>
|
||||||
|
<app-card-title>{{title | uppercase}}</app-card-title>
|
||||||
|
<app-card-content>
|
||||||
|
<div fxLayout="row" fxLayoutAlign="space-between stretch">
|
||||||
|
<app-donut-chart [elementID]="chartID" fxFlex="30" [title]="title" (stateEvent)="onItemSelect($event)">
|
||||||
|
</app-donut-chart>
|
||||||
|
<div class=" items" fxLayout="column" fxLayoutAlign="start end" fxFlex="70">
|
||||||
|
<app-chart-item *ngFor="let state of states" (click)="onItemSelect(state)" [state]="state.label"
|
||||||
|
[count]="state.count" [selected]="state.selected">
|
||||||
|
</app-chart-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-card-content>
|
||||||
|
</app-card>
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChartComponent } from './chart.component';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
class MockService {
|
||||||
|
getEach() {
|
||||||
|
// unimplemented
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ChartComponent', () => {
|
||||||
|
let component: ChartComponent;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [ChartComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
component = TestBed.inject(ChartComponent);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { Component, OnInit, ViewChild, Input, Type } from '@angular/core';
|
||||||
|
import Folder from '../../folder'
|
||||||
|
import { FolderService } from 'src/app/services/folder.service';
|
||||||
|
import { DonutChartComponent } from '../donut-chart/donut-chart.component';
|
||||||
|
import { DeviceService } from 'src/app/services/device.service';
|
||||||
|
import Device from 'src/app/device';
|
||||||
|
import { StType } from '../../type';
|
||||||
|
import { FilterService } from 'src/app/services/filter.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
|
||||||
|
export interface ChartItemState {
|
||||||
|
label: string,
|
||||||
|
count: number,
|
||||||
|
color: string,
|
||||||
|
selected: boolean,
|
||||||
|
}
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart',
|
||||||
|
templateUrl: './chart.component.html',
|
||||||
|
styleUrls: ['./chart.component.scss']
|
||||||
|
})
|
||||||
|
|
||||||
|
export class ChartComponent implements OnInit {
|
||||||
|
@ViewChild(DonutChartComponent) donutChart: DonutChartComponent;
|
||||||
|
@Input() type: StType;
|
||||||
|
title: string;
|
||||||
|
chartID: string;
|
||||||
|
states: ChartItemState[] = [];
|
||||||
|
|
||||||
|
private observer: Observable<any>;
|
||||||
|
private activeChartState: ChartItemState;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private folderService: FolderService,
|
||||||
|
private deviceService: DeviceService,
|
||||||
|
private filterService: FilterService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
onItemSelect(s: ChartItemState) {
|
||||||
|
// Send chart item state to filter
|
||||||
|
this.filterService.changeFilter({ type: this.type, text: s.label });
|
||||||
|
|
||||||
|
// Deselect all other items
|
||||||
|
this.states.forEach(s => {
|
||||||
|
s.selected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select item only
|
||||||
|
if (s !== this.activeChartState) {
|
||||||
|
s.selected = true;
|
||||||
|
this.activeChartState = s;
|
||||||
|
} else {
|
||||||
|
this.activeChartState = null;
|
||||||
|
this.filterService.changeFilter({ type: this.type, text: "" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
switch (this.type) {
|
||||||
|
case StType.Folder:
|
||||||
|
this.title = "Folders";
|
||||||
|
this.chartID = 'foldersChart';
|
||||||
|
this.observer = this.folderService.folderAdded$;
|
||||||
|
break;
|
||||||
|
case StType.Device:
|
||||||
|
this.title = "Devices";
|
||||||
|
this.chartID = 'devicesChart';
|
||||||
|
this.observer = this.deviceService.deviceAdded$;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
let totalCount: number = 0;
|
||||||
|
this.observer.subscribe(
|
||||||
|
t => {
|
||||||
|
// Count the number of folders and set chart
|
||||||
|
totalCount++;
|
||||||
|
this.donutChart.count = totalCount;
|
||||||
|
|
||||||
|
// Get StateType and convert to string
|
||||||
|
const stateType = t.stateType;
|
||||||
|
const state = t.state;
|
||||||
|
let color;
|
||||||
|
switch (this.type) {
|
||||||
|
case StType.Folder:
|
||||||
|
color = Folder.stateTypeToColor(t.stateType);
|
||||||
|
break;
|
||||||
|
case StType.Device:
|
||||||
|
color = Device.stateTypeToColor(stateType);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if state exists
|
||||||
|
let found: boolean = false;
|
||||||
|
this.states.forEach(s => {
|
||||||
|
if (s.label === state) {
|
||||||
|
s.count = s.count + 1;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
this.states.push({ label: state, count: 1, color: color, selected: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.donutChart.updateData(this.states);
|
||||||
|
},
|
||||||
|
err => console.error('Observer got an error: ' + err),
|
||||||
|
() => {
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id={{elementID}} width="100px" height="100px"></canvas>
|
||||||
|
<div class="center" fxLayout="column" fxLayoutAlign="center center">
|
||||||
|
<div class="{{_countClass}}">{{_count}}</div>
|
||||||
|
<div class="title">{{title}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,48 @@
|
||||||
|
.chart-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; bottom: 0; right: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
overflow: auto;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: calc(0.5rem + 0.625vw);
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-total {
|
||||||
|
font-size: calc(1rem + 0.625vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-count-total {
|
||||||
|
font-size: calc(0.5rem + 0.625vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.count-total {
|
||||||
|
font-size: calc(1.00rem + 0.625vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 800px) and (max-width: 1000px) {
|
||||||
|
.title {
|
||||||
|
font-size: calc(0.35rem + 0.625vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-total {
|
||||||
|
font-size: calc(1.35rem + 0.625vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width:1000px) {
|
||||||
|
.title {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DonutChartComponent } from './donut-chart.component';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
describe('DonutChartComponent', () => {
|
||||||
|
let component: DonutChartComponent;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [DonutChartComponent],
|
||||||
|
providers: [DonutChartComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
component = TestBed.inject(DonutChartComponent);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { Chart } from 'chart.js'
|
||||||
|
import { tooltip } from '../tooltip'
|
||||||
|
import { FilterService } from 'src/app/services/filter.service';
|
||||||
|
import { ChartItemState } from '../chart/chart.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-donut-chart',
|
||||||
|
templateUrl: './donut-chart.component.html',
|
||||||
|
styleUrls: ['./donut-chart.component.scss']
|
||||||
|
})
|
||||||
|
export class DonutChartComponent {
|
||||||
|
@Input() elementID: string;
|
||||||
|
@Input() title: number;
|
||||||
|
@Output() stateEvent = new EventEmitter<ChartItemState>();;
|
||||||
|
|
||||||
|
_count: number;
|
||||||
|
_countClass = "count-total";
|
||||||
|
set count(n: number) {
|
||||||
|
if (n >= 1000) { // use a smaller font
|
||||||
|
this._countClass = "large-count-total"
|
||||||
|
}
|
||||||
|
this._count = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canvas: any;
|
||||||
|
private ctx: any;
|
||||||
|
private chart: Chart;
|
||||||
|
private states: ChartItemState[];
|
||||||
|
|
||||||
|
constructor(private filterService: FilterService) { }
|
||||||
|
|
||||||
|
updateData(states: ChartItemState[]): void {
|
||||||
|
this.states = states;
|
||||||
|
// Using object destructuring
|
||||||
|
for (let i = 0; i < states.length; i++) {
|
||||||
|
let s = states[i];
|
||||||
|
this.chart.data.labels[i] = s.label;
|
||||||
|
this.chart.data.datasets[0].data[i] = s.count;
|
||||||
|
this.chart.data.datasets[0].backgroundColor[i] = s.color;
|
||||||
|
}
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllData(withAnimation: boolean): void {
|
||||||
|
this.chart.data.labels.pop();
|
||||||
|
this.chart.data.datasets.forEach((dataset) => {
|
||||||
|
dataset.data = [];
|
||||||
|
});
|
||||||
|
this.chart.update(withAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.canvas = document.getElementById(this.elementID);
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.chart = new Chart(this.ctx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
datasets: [{
|
||||||
|
data: [],
|
||||||
|
backgroundColor: [],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
cutoutPercentage: 77,
|
||||||
|
responsive: true,
|
||||||
|
onClick: (e) => {
|
||||||
|
var activePoints = this.chart.getElementsAtEvent(e);
|
||||||
|
if (activePoints.length > 0) {
|
||||||
|
const index = activePoints[0]["_index"];
|
||||||
|
this.stateEvent.emit(this.states[index]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
// Disable the on-canvas tooltip
|
||||||
|
enabled: false,
|
||||||
|
custom: tooltip(),
|
||||||
|
},
|
||||||
|
animation: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Adapted from https://www.chartjs.org/samples/latest/tooltips/custom-pie.html
|
||||||
|
export let tooltip: () => (tooltip: any) => void =
|
||||||
|
function (): (tooltip: any) => void {
|
||||||
|
return function (tooltip: any): void {
|
||||||
|
// Tooltip Element
|
||||||
|
const tooltipEl = document.getElementById('chartjs-tooltip');
|
||||||
|
|
||||||
|
// Hide if no tooltip
|
||||||
|
if (tooltip.opacity === 0) {
|
||||||
|
tooltipEl.style.opacity = '0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set caret Position
|
||||||
|
tooltipEl.classList.remove('above', 'below', 'no-transform');
|
||||||
|
if (tooltip.yAlign) {
|
||||||
|
tooltipEl.classList.add(tooltip.yAlign);
|
||||||
|
} else {
|
||||||
|
tooltipEl.classList.add('no-transform');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBody(bodyItem) {
|
||||||
|
return bodyItem.lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Text
|
||||||
|
if (tooltip.body) {
|
||||||
|
let titleLines = tooltip.title || [];
|
||||||
|
const bodyLines = tooltip.body.map(getBody);
|
||||||
|
|
||||||
|
let innerHtml = '<thead>';
|
||||||
|
|
||||||
|
titleLines.forEach(function (title) {
|
||||||
|
innerHtml += '<tr><th>' + title + '</th></tr>';
|
||||||
|
});
|
||||||
|
innerHtml += '</thead><tbody>';
|
||||||
|
|
||||||
|
bodyLines.forEach(function (body, i) {
|
||||||
|
let colors = tooltip.labelColors[i];
|
||||||
|
let style = 'background:' + colors.backgroundColor;
|
||||||
|
style += '; border-color:' + colors.borderColor;
|
||||||
|
style += '; border-width: 2px';
|
||||||
|
let span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
|
||||||
|
innerHtml += '<tr><td>' + span + body + '</td></tr>';
|
||||||
|
});
|
||||||
|
innerHtml += '</tbody>';
|
||||||
|
|
||||||
|
let tableRoot = tooltipEl.querySelector('table');
|
||||||
|
tableRoot.innerHTML = innerHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
var position = this._chart.canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Display, position, and set styles for font
|
||||||
|
tooltipEl.style.opacity = '1';
|
||||||
|
tooltipEl.style.position = 'absolute';
|
||||||
|
tooltipEl.style.left = position.left + window.pageXOffset + tooltip.caretX + 'px';
|
||||||
|
tooltipEl.style.top = position.top + window.pageYOffset + tooltip.caretY + 'px';
|
||||||
|
tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px';
|
||||||
|
tooltipEl.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface Completion {
|
||||||
|
completion: number;
|
||||||
|
globalBytes: number;
|
||||||
|
needBytes: number;
|
||||||
|
needDeletes: number;
|
||||||
|
needItems: number;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
export interface SystemConnections {
|
||||||
|
connections: { deviceId?: Connection };
|
||||||
|
total: Connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Connection {
|
||||||
|
address: string;
|
||||||
|
at: string;
|
||||||
|
clientVersion: string;
|
||||||
|
connected: boolean;
|
||||||
|
crypto: string;
|
||||||
|
inBytesTotal: number;
|
||||||
|
outBytesTotal: number;
|
||||||
|
paused: boolean;
|
||||||
|
type: string;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!--<div class="grid-container" gdAreas="header header | folders devices | status-list status-list | footer footer"
|
||||||
|
gdGap="16px" gdRows="auto auto auto"> -->
|
||||||
|
<!--<div class="grid-container" fxLayout="row" fxLayoutGap="16px grid" fxLayoutAlign="stretch">-->
|
||||||
|
<div class="header" fxLayout="row" fxLayoutAlign="space-between center">
|
||||||
|
<div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="start center">
|
||||||
|
<img src="assets/logo-horizontal.svg" width="150px" />
|
||||||
|
<span>Tech UI</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress">
|
||||||
|
<mat-progress-bar mode="determinate" value="{{progressValue}}" [@progressBar]="isLoading ? 'start' : 'done'">
|
||||||
|
</mat-progress-bar>
|
||||||
|
</div>
|
||||||
|
<div fxLayout="column" fxLayoutGap="16px" class="grid-container" [@loading]="isLoading ? 'start' : 'done'">
|
||||||
|
<div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="space-between stretch">
|
||||||
|
<app-chart [type]=folderChart fxFlex="50"></app-chart>
|
||||||
|
<app-chart [type]=deviceChart fxFlex="50"></app-chart>
|
||||||
|
</div>
|
||||||
|
<app-status-list gdArea="status-list"></app-status-list>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
|
@ -0,0 +1,13 @@
|
||||||
|
.header {
|
||||||
|
margin: 15px 3vw 12px 3vw;
|
||||||
|
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
margin: 0 3vw 0 3vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
margin: 10px calc(10px + 3.3vw);
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DashboardComponent } from './dashboard.component';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
describe('DashboardComponent', () => {
|
||||||
|
let component: DashboardComponent;
|
||||||
|
let fixture: ComponentFixture<DashboardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [DashboardComponent],
|
||||||
|
imports: [
|
||||||
|
HttpClientModule,
|
||||||
|
NoopAnimationsModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
],
|
||||||
|
providers: [DashboardComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
component = TestBed.inject(DashboardComponent);
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DashboardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compile', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { Component, OnInit, AfterViewInit, ViewChild, AfterViewChecked } from '@angular/core';
|
||||||
|
import {
|
||||||
|
trigger,
|
||||||
|
state,
|
||||||
|
style,
|
||||||
|
animate,
|
||||||
|
transition,
|
||||||
|
} from '@angular/animations';
|
||||||
|
import { SystemConfigService } from '../services/system-config.service';
|
||||||
|
import { StType } from '../type';
|
||||||
|
import { FilterService } from '../services/filter.service';
|
||||||
|
import { ProgressService } from '../services/progress.service';
|
||||||
|
import { MatProgressBar } from '@angular/material/progress-bar';
|
||||||
|
import { MessageService } from '../services/message.service';
|
||||||
|
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { DialogComponent } from '../dialog/dialog.component';
|
||||||
|
import { FolderService } from '../services/folder.service';
|
||||||
|
import { DeviceService } from '../services/device.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
templateUrl: './dashboard.component.html',
|
||||||
|
styleUrls: ['./dashboard.component.scss'],
|
||||||
|
providers: [FilterService],
|
||||||
|
animations: [
|
||||||
|
trigger('loading', [
|
||||||
|
state('start', style({
|
||||||
|
marginTop: '20px',
|
||||||
|
})),
|
||||||
|
state('done', style({
|
||||||
|
marginTop: '0px',
|
||||||
|
})),
|
||||||
|
transition('start => done', [
|
||||||
|
animate('0.2s 0.2s')
|
||||||
|
]),
|
||||||
|
transition('done => start', [
|
||||||
|
animate('0.2s 0.2s')
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
trigger('progressBar', [
|
||||||
|
state('start', style({
|
||||||
|
opacity: 100,
|
||||||
|
visibility: 'visible'
|
||||||
|
})),
|
||||||
|
state('done', style({
|
||||||
|
opacity: 0,
|
||||||
|
visibility: 'hidden'
|
||||||
|
})),
|
||||||
|
transition('start => done', [
|
||||||
|
animate('0.35s')
|
||||||
|
]),
|
||||||
|
transition('done => start', [
|
||||||
|
animate('0.35s')
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class DashboardComponent implements OnInit, AfterViewInit {
|
||||||
|
@ViewChild(MatProgressBar) progressBar: MatProgressBar;
|
||||||
|
folderChart: StType = StType.Folder;
|
||||||
|
deviceChart: StType = StType.Device;
|
||||||
|
progressValue: number = 0;
|
||||||
|
isLoading = true;
|
||||||
|
private dialogRef: MatDialogRef<DialogComponent>;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private systemConfigService: SystemConfigService,
|
||||||
|
private folderService: FolderService,
|
||||||
|
private deviceService: DeviceService,
|
||||||
|
private progressService: ProgressService,
|
||||||
|
private messageService: MessageService,
|
||||||
|
public dialog: MatDialog
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Request data from Rest API
|
||||||
|
this.systemConfigService.getSystemConfig().subscribe(
|
||||||
|
_ => {
|
||||||
|
// Request devices and folders for charts and lists
|
||||||
|
this.folderService.requestFolders();
|
||||||
|
this.deviceService.requestDevices();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
// Listen for progress service changes
|
||||||
|
let t = setInterval(() => {
|
||||||
|
if (this.progressService.isComplete()) {
|
||||||
|
clearInterval(t);
|
||||||
|
this.progressValue = 100;
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
this.progressValue = this.progressService.percentValue;
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Listen for messages from other services/components
|
||||||
|
this.messageService.messageAdded$
|
||||||
|
.subscribe(
|
||||||
|
_ => {
|
||||||
|
// Open dialog
|
||||||
|
if (!this.dialogRef)
|
||||||
|
this.dialogRef = this.dialog.open(DialogComponent);
|
||||||
|
|
||||||
|
this.dialogRef.afterClosed().subscribe(
|
||||||
|
_ => {
|
||||||
|
this.dialogRef = null;
|
||||||
|
this.messageService.clear();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { colors } from './style';
|
||||||
|
import Folder from './folder';
|
||||||
|
import { Completion } from './completion';
|
||||||
|
|
||||||
|
interface Device {
|
||||||
|
deviceID: string;
|
||||||
|
name: string;
|
||||||
|
stateType: Device.StateType;
|
||||||
|
state: string;
|
||||||
|
paused: boolean;
|
||||||
|
connected: boolean;
|
||||||
|
completion: Completion;
|
||||||
|
used: boolean; // indicates if a folder is using the device
|
||||||
|
folders: Folder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Device {
|
||||||
|
export enum StateType {
|
||||||
|
Insync = 1,
|
||||||
|
UnusedInsync,
|
||||||
|
Unknown,
|
||||||
|
Syncing,
|
||||||
|
Paused,
|
||||||
|
UnusedPaused,
|
||||||
|
Disconnected,
|
||||||
|
UnusedDisconnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stateTypeToString(s: StateType): string {
|
||||||
|
switch (s) {
|
||||||
|
case StateType.Insync:
|
||||||
|
return 'Up to Date';
|
||||||
|
case StateType.UnusedInsync:
|
||||||
|
return 'Connected (Unused)';
|
||||||
|
case StateType.Unknown:
|
||||||
|
return 'Unknown';
|
||||||
|
case StateType.Syncing:
|
||||||
|
return 'Syncing';
|
||||||
|
case StateType.Paused:
|
||||||
|
return 'Paused';
|
||||||
|
case StateType.UnusedPaused:
|
||||||
|
return 'Paused (Unused)';
|
||||||
|
case StateType.Disconnected:
|
||||||
|
return 'Disconnected';
|
||||||
|
case StateType.UnusedDisconnected:
|
||||||
|
return 'Disconnected (Unused)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stateTypeToColor looks up a hex color string based on StateType
|
||||||
|
* @param s StateType
|
||||||
|
*/
|
||||||
|
export function stateTypeToColor(s: StateType): string {
|
||||||
|
switch (s) {
|
||||||
|
case StateType.Insync:
|
||||||
|
return colors.get("blue");
|
||||||
|
case StateType.UnusedInsync:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.Unknown:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.Syncing:
|
||||||
|
return colors.get("green");
|
||||||
|
case StateType.Paused:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.UnusedPaused:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.Disconnected:
|
||||||
|
return colors.get("yellow");
|
||||||
|
case StateType.UnusedDisconnected:
|
||||||
|
return colors.get("grey");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStateType(d: Device): StateType {
|
||||||
|
// StateType Unknown is set in DeviceService
|
||||||
|
if (d.stateType === StateType.Unknown) {
|
||||||
|
return StateType.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.paused) {
|
||||||
|
return d.used ? StateType.Paused : StateType.UnusedPaused;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.connected) {
|
||||||
|
if (d.completion.completion === 100) {
|
||||||
|
return d.used ? StateType.Insync : StateType.UnusedInsync;
|
||||||
|
} else {
|
||||||
|
return StateType.Syncing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.used ? StateType.Disconnected : StateType.UnusedDisconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recalcCompletion(d: Device) {
|
||||||
|
if (!d || !d.completion || !d.folders) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var total = 0, needed = 0, deletes = 0, items = 0;
|
||||||
|
d.folders.forEach(folder => {
|
||||||
|
if (!folder || !folder.completion)
|
||||||
|
return
|
||||||
|
needed += folder.completion.needBytes;
|
||||||
|
items += folder.completion.needItems;
|
||||||
|
deletes += folder.completion.needDeletes;
|
||||||
|
});
|
||||||
|
if (total == 0) {
|
||||||
|
d.completion.completion = 100;
|
||||||
|
d.completion.needBytes = 0;
|
||||||
|
d.completion.needItems = 0;
|
||||||
|
} else {
|
||||||
|
d.completion.completion = Math.floor(100 * (1 - needed / total));
|
||||||
|
d.completion.needBytes = needed;
|
||||||
|
d.completion.needItems = items + deletes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needed == 0 && deletes > 0) {
|
||||||
|
// We don't need any data, but we have deletes that we need
|
||||||
|
// to do. Drop down the completion percentage to indicate
|
||||||
|
// that we have stuff to do.
|
||||||
|
d.completion.completion = 95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Device;
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div mat-dialog-content fxLayout="column" fxLayoutAlign="space-between center">
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item *ngFor='let message of messageService.messages'>{{message}}</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
<button mat-stroked-button [mat-dialog-close] cdkFocusInitial>Close</button>
|
||||||
|
</div>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DialogComponent } from './dialog.component';
|
||||||
|
|
||||||
|
describe('DialogComponent', () => {
|
||||||
|
let component: DialogComponent;
|
||||||
|
let fixture: ComponentFixture<DialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ DialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MessageService } from '../services/message.service';
|
||||||
|
|
||||||
|
export interface DialogData {
|
||||||
|
message: 'example message';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dialog',
|
||||||
|
templateUrl: './dialog.component.html',
|
||||||
|
styleUrls: ['./dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class DialogComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<DialogComponent>,
|
||||||
|
public messageService: MessageService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void { }
|
||||||
|
|
||||||
|
onNoClick(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,200 @@
|
||||||
|
import Device from './device';
|
||||||
|
import { colors } from './style';
|
||||||
|
import { Completion } from './completion';
|
||||||
|
|
||||||
|
interface Folder {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
devices: Device[];
|
||||||
|
status: Folder.Status;
|
||||||
|
stateType: Folder.StateType;
|
||||||
|
state: string;
|
||||||
|
paused: boolean;
|
||||||
|
completion: Completion;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Folder {
|
||||||
|
export enum StateType {
|
||||||
|
Paused = 1,
|
||||||
|
Unknown,
|
||||||
|
Unshared,
|
||||||
|
WaitingToScan,
|
||||||
|
Stopped,
|
||||||
|
Scanning,
|
||||||
|
Idle,
|
||||||
|
LocalAdditions,
|
||||||
|
WaitingToSync,
|
||||||
|
PreparingToSync,
|
||||||
|
Syncing,
|
||||||
|
OutOfSync,
|
||||||
|
FailedItems,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stateTypeToString returns a string representation of
|
||||||
|
* the StateType enum
|
||||||
|
* @param s StateType
|
||||||
|
*/
|
||||||
|
export function stateTypeToString(s: StateType): string {
|
||||||
|
switch (s) {
|
||||||
|
case StateType.Paused:
|
||||||
|
return 'Paused';
|
||||||
|
case StateType.Unknown:
|
||||||
|
return 'Unknown';
|
||||||
|
case StateType.Unshared:
|
||||||
|
return 'Unshared';
|
||||||
|
case StateType.WaitingToSync:
|
||||||
|
return 'Waiting to Sync';
|
||||||
|
case StateType.Stopped:
|
||||||
|
return 'Stopped';
|
||||||
|
case StateType.Scanning:
|
||||||
|
return 'Scanning';
|
||||||
|
case StateType.Idle:
|
||||||
|
return 'Up to Date';
|
||||||
|
case StateType.LocalAdditions:
|
||||||
|
return 'Local Additions';
|
||||||
|
case StateType.WaitingToScan:
|
||||||
|
return 'Waiting to Scan';
|
||||||
|
case StateType.PreparingToSync:
|
||||||
|
return 'Preparing to Sync';
|
||||||
|
case StateType.Syncing:
|
||||||
|
return 'Syncing';
|
||||||
|
case StateType.OutOfSync:
|
||||||
|
return 'Out of Sync';
|
||||||
|
case StateType.FailedItems:
|
||||||
|
return 'Failed Items';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stateTypeToColor looks up a hex color string based on StateType
|
||||||
|
* @param s StateType
|
||||||
|
*/
|
||||||
|
export function stateTypeToColor(s: StateType): string {
|
||||||
|
switch (s) {
|
||||||
|
case StateType.Paused:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.Unknown:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.Unshared:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.WaitingToSync:
|
||||||
|
return colors.get("yellow");
|
||||||
|
case StateType.Stopped:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.Scanning:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.Idle:
|
||||||
|
return colors.get("blue");
|
||||||
|
case StateType.LocalAdditions:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.WaitingToScan:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.PreparingToSync:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.Syncing:
|
||||||
|
return colors.get("green");
|
||||||
|
case StateType.OutOfSync:
|
||||||
|
return colors.get("grey");
|
||||||
|
case StateType.FailedItems:
|
||||||
|
return colors.get("red");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getStateType looks at a folder and determines the correct
|
||||||
|
* StateType to return
|
||||||
|
*
|
||||||
|
* Possible state values from API
|
||||||
|
* "idle", "scanning", "scan-waiting", "sync-waiting", "sync-preparing"
|
||||||
|
* "syncing", "error", "unknown"
|
||||||
|
*
|
||||||
|
* @param f Folder
|
||||||
|
*/
|
||||||
|
export function getStateType(f: Folder): StateType {
|
||||||
|
if (f.paused) {
|
||||||
|
return StateType.Paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!f.status || (Object.keys(f.status).length === 0)) {
|
||||||
|
return StateType.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs: Folder.Status = f.status;
|
||||||
|
const state: string = fs.state;
|
||||||
|
|
||||||
|
// Match API string to StateType
|
||||||
|
switch (state) {
|
||||||
|
case "idle":
|
||||||
|
return StateType.Idle;
|
||||||
|
case "scanning":
|
||||||
|
return StateType.Scanning;
|
||||||
|
case "scan-waiting":
|
||||||
|
return StateType.WaitingToScan;
|
||||||
|
case "sync-waiting":
|
||||||
|
return StateType.WaitingToSync;
|
||||||
|
case "sync-preparing":
|
||||||
|
return StateType.PreparingToSync;
|
||||||
|
case "syncing":
|
||||||
|
return StateType.Syncing;
|
||||||
|
case "error":
|
||||||
|
// legacy, the state is called "stopped" in the gui
|
||||||
|
return StateType.Stopped;
|
||||||
|
case "unknown":
|
||||||
|
return StateType.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.needTotalItems > 0) {
|
||||||
|
return StateType.OutOfSync;
|
||||||
|
}
|
||||||
|
if (fs.pullErrors > 0) {
|
||||||
|
return StateType.FailedItems;
|
||||||
|
}
|
||||||
|
if (fs.receiveOnlyTotalItems > 0) {
|
||||||
|
return StateType.LocalAdditions;
|
||||||
|
}
|
||||||
|
if (f.devices.length <= 1) {
|
||||||
|
return StateType.Unshared;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StateType.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Status {
|
||||||
|
globalBytes: number;
|
||||||
|
globalDeleted: number;
|
||||||
|
globalDirectories: number;
|
||||||
|
globalFiles: number;
|
||||||
|
globalSymlinks: number;
|
||||||
|
globalTotalItems: number;
|
||||||
|
ignorePatterns: boolean;
|
||||||
|
inSyncBytes: number;
|
||||||
|
inSyncFiles: number;
|
||||||
|
invalid: string;
|
||||||
|
localBytes: number;
|
||||||
|
localDeleted: number;
|
||||||
|
localDirectories: number;
|
||||||
|
localFiles: number;
|
||||||
|
localSymlinks: number;
|
||||||
|
needBytes: number;
|
||||||
|
needDeletes: number;
|
||||||
|
needDirectories: number;
|
||||||
|
needFiles: number;
|
||||||
|
needSymlinks: number;
|
||||||
|
needTotalItems: number;
|
||||||
|
pullErrors: number;
|
||||||
|
receiveOnlyChangedBytes: number;
|
||||||
|
receiveOnlyChangedDeletes: number;
|
||||||
|
receiveOnlyChangedDirectories: number;
|
||||||
|
receiveOnlyChangedFiles: number;
|
||||||
|
receiveOnlyChangedSymlinks: number;
|
||||||
|
receiveOnlyTotalItems: number;
|
||||||
|
sequence: number;
|
||||||
|
state: string;
|
||||||
|
stateChanged: string;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Folder;
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CachingInterceptor } from './caching.interceptor';
|
||||||
|
|
||||||
|
describe('CachingInterceptor', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
CachingInterceptor
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const interceptor: CachingInterceptor = TestBed.inject(CachingInterceptor);
|
||||||
|
expect(interceptor).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
HttpRequest,
|
||||||
|
HttpHandler,
|
||||||
|
HttpEvent,
|
||||||
|
HttpInterceptor,
|
||||||
|
HttpHeaders,
|
||||||
|
HttpResponse
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { RequestCacheService } from '../services/request-cache.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CachingInterceptor implements HttpInterceptor {
|
||||||
|
constructor(private cache: RequestCacheService) { }
|
||||||
|
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler) {
|
||||||
|
// continue if not cachable.
|
||||||
|
if (!isCachable(req)) { return next.handle(req); }
|
||||||
|
|
||||||
|
const cachedResponse = this.cache.get(req);
|
||||||
|
return cachedResponse ?
|
||||||
|
of(cachedResponse) : sendRequest(req, next, this.cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Is this request cachable? */
|
||||||
|
function isCachable(req: HttpRequest<any>) {
|
||||||
|
// Only GET requests are cachable
|
||||||
|
return req.method === 'GET';
|
||||||
|
/*
|
||||||
|
return req.method === 'GET' &&
|
||||||
|
-1 < req.url.indexOf("url");
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server response observable by sending request to `next()`.
|
||||||
|
* Will add the response to the cache on the way out.
|
||||||
|
*/
|
||||||
|
function sendRequest(
|
||||||
|
req: HttpRequest<any>,
|
||||||
|
next: HttpHandler,
|
||||||
|
cache: RequestCacheService): Observable<HttpEvent<any>> {
|
||||||
|
|
||||||
|
// No headers allowed in npm search request
|
||||||
|
const noHeaderReq = req.clone({ headers: new HttpHeaders() });
|
||||||
|
|
||||||
|
return next.handle(noHeaderReq).pipe(
|
||||||
|
tap(event => {
|
||||||
|
// There may be other events besides the response.
|
||||||
|
if (event instanceof HttpResponse) {
|
||||||
|
// cache.put(req, event); // Update the cache.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CSRFInterceptor } from './csrf.interceptor';
|
||||||
|
|
||||||
|
describe('CsrfInterceptor', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
CSRFInterceptor
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const interceptor: CSRFInterceptor = TestBed.inject(CSRFInterceptor);
|
||||||
|
expect(interceptor).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { deviceID } from '../api-utils';
|
||||||
|
import {
|
||||||
|
HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders
|
||||||
|
} from '@angular/common/http';
|
||||||
|
|
||||||
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CSRFInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
constructor(private cookieService: CookieService) { }
|
||||||
|
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler) {
|
||||||
|
const dID: String = deviceID();
|
||||||
|
const csrfCookie = 'CSRF-Token-' + dID
|
||||||
|
|
||||||
|
// Clone the request and replace the original headers with
|
||||||
|
// cloned headers, updated with the CSRF information.
|
||||||
|
const csrfReq = req.clone({
|
||||||
|
headers: req.headers.set('X-CSRF-Token-' + dID,
|
||||||
|
this.cookieService.getCookie(csrfCookie))
|
||||||
|
});
|
||||||
|
|
||||||
|
// send cloned request with header to the next handler.
|
||||||
|
return next.handle(csrfReq);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ErrorInterceptor } from './error.interceptor';
|
||||||
|
|
||||||
|
describe('ErrorInterceptor', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
ErrorInterceptor
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const interceptor: ErrorInterceptor = TestBed.inject(ErrorInterceptor);
|
||||||
|
expect(interceptor).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
HttpRequest,
|
||||||
|
HttpHandler,
|
||||||
|
HttpEvent,
|
||||||
|
HttpInterceptor,
|
||||||
|
HttpErrorResponse
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import { Observable, throwError } from 'rxjs';
|
||||||
|
import { apiRetry } from '../api-utils';
|
||||||
|
import { retry, catchError } from 'rxjs/operators';
|
||||||
|
import { MessageService } from '../services/message.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ErrorInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
constructor(private messageService: MessageService) { }
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
return next.handle(request)
|
||||||
|
.pipe(
|
||||||
|
retry(apiRetry),
|
||||||
|
catchError((error: HttpErrorResponse) => {
|
||||||
|
let errorMsg: string;
|
||||||
|
if (error.error instanceof ErrorEvent) {
|
||||||
|
// Client side
|
||||||
|
errorMsg = `Error: ${error.error.message}`;
|
||||||
|
} else {
|
||||||
|
// Server side
|
||||||
|
errorMsg = `Error Status: ${error.status}\nMessage: ${error.message}`;
|
||||||
|
}
|
||||||
|
console.log(errorMsg);
|
||||||
|
|
||||||
|
this.messageService.add(errorMsg);
|
||||||
|
return throwError(errorMsg);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/* "Barrel" of Http Interceptors */
|
||||||
|
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { CSRFInterceptor } from './csrf.interceptor';
|
||||||
|
import { CachingInterceptor } from './caching.interceptor';
|
||||||
|
import { ErrorInterceptor } from './error.interceptor';
|
||||||
|
|
||||||
|
/** Http interceptor providers in outside-in order */
|
||||||
|
export const httpInterceptorProviders = [
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true },
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
|
||||||
|
// CSRFInterceptor needs to be last
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true },
|
||||||
|
];
|
|
@ -0,0 +1,4 @@
|
||||||
|
<mat-button-toggle-group class="tui-button-toggle" name="fontStyle" aria-label="Font Style" value="folders">
|
||||||
|
<mat-button-toggle value="folders" (click)="onSelect(listType.Folder)">Folders</mat-button-toggle>
|
||||||
|
<mat-button-toggle value="devices" (click)="onSelect(listType.Device)">Devices</mat-button-toggle>
|
||||||
|
</mat-button-toggle-group>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ListToggleComponent } from './list-toggle.component';
|
||||||
|
|
||||||
|
describe('ListToggleComponent', () => {
|
||||||
|
let component: ListToggleComponent;
|
||||||
|
let fixture: ComponentFixture<ListToggleComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ListToggleComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ListToggleComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
|
||||||
|
import { StType } from '../type';
|
||||||
|
import { MatButtonToggleGroup } from '@angular/material/button-toggle';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-list-toggle',
|
||||||
|
templateUrl: './list-toggle.component.html',
|
||||||
|
styleUrls: ['./list-toggle.component.scss']
|
||||||
|
})
|
||||||
|
|
||||||
|
export class ListToggleComponent implements OnInit {
|
||||||
|
@ViewChild(MatButtonToggleGroup) group: MatButtonToggleGroup;
|
||||||
|
public listType = StType;
|
||||||
|
// public toggleValue: string = "folders";
|
||||||
|
@Output() listTypeEvent = new EventEmitter<StType>();
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(t: StType): void {
|
||||||
|
this.listTypeEvent.emit(t);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Filter</mat-label>
|
||||||
|
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Up to Date">
|
||||||
|
</mat-form-field>
|
||||||
|
<table mat-table class="full-width-table" matSort aria-label="Devices" multiTemplateDataRows>
|
||||||
|
<ng-container matColumnDef="{{column}}" *ngFor="let column of displayedColumns">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> {{column}} </th>
|
||||||
|
<td mat-cell *matCellDef="let device"> {{device[column]}} </td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="expandedDetail">
|
||||||
|
<td mat-cell *matCellDef="let device" [attr.colspan]="displayedColumns.length">
|
||||||
|
<div class="table-detail" [@detailExpand]="device == expandedDevice ? 'expanded' : 'collapsed'">
|
||||||
|
<div class="detail-items">
|
||||||
|
<span>Folders: </span>
|
||||||
|
<span class="item-name" *ngFor="let folder of device.folders">{{folder.label | trim}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let device; columns: displayedColumns;" class="table-row"
|
||||||
|
[class.expanded-row]="expandedDevice === device"
|
||||||
|
(click)="expandedDevice = expandedDevice === device ? null : device">
|
||||||
|
</tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<mat-paginator #paginator [length]="dataSource?.data.length" [pageIndex]="0" [pageSize]="25"
|
||||||
|
[pageSizeOptions]="[25, 50, 100, 250]">
|
||||||
|
</mat-paginator>
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
|
||||||
|
import { DeviceListComponent } from './device-list.component';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { ChangeDetectorRef } from '@angular/core';
|
||||||
|
|
||||||
|
describe('DeviceListComponent', () => {
|
||||||
|
let component: DeviceListComponent;
|
||||||
|
let fixture: ComponentFixture<DeviceListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [DeviceListComponent],
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [DeviceListComponent, ChangeDetectorRef]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
component = TestBed.inject(DeviceListComponent);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should compile', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { AfterViewInit, Component, OnInit, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
|
import { MatSort } from '@angular/material/sort';
|
||||||
|
import { MatTable, MatTableDataSource } from '@angular/material/table';
|
||||||
|
|
||||||
|
import Device from '../../device';
|
||||||
|
import { SystemConfigService } from '../../services/system-config.service';
|
||||||
|
import { FilterService } from 'src/app/services/filter.service';
|
||||||
|
import { StType } from 'src/app/type';
|
||||||
|
import { MatInput } from '@angular/material/input';
|
||||||
|
import { DeviceService } from 'src/app/services/device.service';
|
||||||
|
import { trigger, state, style, transition, animate } from '@angular/animations';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-device-list',
|
||||||
|
templateUrl: './device-list.component.html',
|
||||||
|
styleUrls: ['../status-list/status-list.component.scss'],
|
||||||
|
animations: [
|
||||||
|
trigger('detailExpand', [
|
||||||
|
state('collapsed', style({ height: '0px', minHeight: '0' })),
|
||||||
|
state('expanded', style({ height: '*' })),
|
||||||
|
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DeviceListComponent implements AfterViewInit, OnInit, OnDestroy {
|
||||||
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
@ViewChild(MatTable) table: MatTable<Device>;
|
||||||
|
@ViewChild(MatInput) input: MatInput;
|
||||||
|
dataSource: MatTableDataSource<Device>;
|
||||||
|
|
||||||
|
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||||
|
displayedColumns = ['deviceID', 'name', 'state'];
|
||||||
|
expandedDevice: Device | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private deviceService: DeviceService,
|
||||||
|
private filterService: FilterService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) { };
|
||||||
|
|
||||||
|
applyFilter(event: Event) {
|
||||||
|
// Set previous filter value
|
||||||
|
const filterValue = (event.target as HTMLInputElement).value;
|
||||||
|
this.filterService.previousInputs.set(StType.Device, filterValue);
|
||||||
|
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.dataSource = new MatTableDataSource();
|
||||||
|
this.dataSource.data = [];
|
||||||
|
|
||||||
|
// Replace all data when requests are finished
|
||||||
|
this.deviceService.devicesUpdated$.subscribe(
|
||||||
|
devices => {
|
||||||
|
this.dataSource.data = devices;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add device as they come in
|
||||||
|
let devices: Device[] = [];
|
||||||
|
this.deviceService.deviceAdded$.subscribe(
|
||||||
|
device => {
|
||||||
|
devices.push(device);
|
||||||
|
this.dataSource.data = devices;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
this.table.dataSource = this.dataSource;
|
||||||
|
|
||||||
|
const changeText = (text: string) => {
|
||||||
|
this.dataSource.filter = text.trim().toLowerCase();
|
||||||
|
this.input.value = text;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set previous value
|
||||||
|
changeText(this.filterService.previousInputs.get(StType.Device));
|
||||||
|
|
||||||
|
// Listen for filter changes from other components
|
||||||
|
this.filterService.filterChanged$
|
||||||
|
.subscribe(
|
||||||
|
input => {
|
||||||
|
if (input.type === StType.Device) {
|
||||||
|
changeText(input.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() { }
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Filter</mat-label>
|
||||||
|
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Up to Date">
|
||||||
|
</mat-form-field>
|
||||||
|
<table mat-table class="full-width-table" matSort aria-label="Folders" multiTemplateDataRows>
|
||||||
|
<ng-container matColumnDef="{{column}}" *ngFor="let column of displayedColumns">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> {{column}} </th>
|
||||||
|
<td mat-cell *matCellDef="let folder"> {{folder[column]}} </td>
|
||||||
|
</ng-container>
|
||||||
|
<!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
|
||||||
|
<ng-container matColumnDef="expandedDetail">
|
||||||
|
<td mat-cell *matCellDef="let folder" [attr.colspan]="displayedColumns.length">
|
||||||
|
<div class="table-detail" [@detailExpand]="folder == expandedFolder ? 'expanded' : 'collapsed'">
|
||||||
|
<div class="detail-items">
|
||||||
|
<span>Shared with: </span>
|
||||||
|
<span class="item-name" *ngFor="let device of folder.devices">{{device.name}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let folder; columns: displayedColumns;" class="table-row"
|
||||||
|
[class.expanded-row]="expandedFolder === folder"
|
||||||
|
(click)="expandedFolder = expandedFolder === folder ? null : folder">
|
||||||
|
</tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<mat-paginator #paginator [length]="dataSource?.data.length" [pageIndex]="0" [pageSize]="25"
|
||||||
|
[pageSizeOptions]="[25, 50, 100, 250]">
|
||||||
|
</mat-paginator>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FolderListComponent } from './folder-list.component';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { ChangeDetectorRef } from '@angular/core';
|
||||||
|
|
||||||
|
describe('FolderListComponent', () => {
|
||||||
|
let component: FolderListComponent;
|
||||||
|
let fixture: ComponentFixture<FolderListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [FolderListComponent],
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [FolderListComponent, ChangeDetectorRef]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
component = TestBed.inject(FolderListComponent);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { AfterViewInit, Component, OnInit, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
|
import { MatSort } from '@angular/material/sort';
|
||||||
|
import { MatTable, MatTableDataSource } from '@angular/material/table';
|
||||||
|
|
||||||
|
import Folder from '../../folder';
|
||||||
|
import { SystemConfigService } from '../../services/system-config.service';
|
||||||
|
import { FilterService } from 'src/app/services/filter.service';
|
||||||
|
import { StType } from 'src/app/type';
|
||||||
|
import { MatInput } from '@angular/material/input';
|
||||||
|
import { FolderService } from 'src/app/services/folder.service';
|
||||||
|
import { trigger, state, style, transition, animate } from '@angular/animations';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-folder-list',
|
||||||
|
templateUrl: './folder-list.component.html',
|
||||||
|
styleUrls: ['../status-list/status-list.component.scss'],
|
||||||
|
animations: [
|
||||||
|
trigger('detailExpand', [
|
||||||
|
state('collapsed', style({ height: '0px', minHeight: '0' })),
|
||||||
|
state('expanded', style({ height: '*' })),
|
||||||
|
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class FolderListComponent implements AfterViewInit, OnInit, OnDestroy {
|
||||||
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
@ViewChild(MatTable) table: MatTable<Folder>;
|
||||||
|
@ViewChild(MatInput) input: MatInput;
|
||||||
|
dataSource: MatTableDataSource<Folder>;
|
||||||
|
|
||||||
|
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||||
|
displayedColumns = [
|
||||||
|
"id",
|
||||||
|
"label",
|
||||||
|
"path",
|
||||||
|
"state"
|
||||||
|
];
|
||||||
|
|
||||||
|
expandedFolder: Folder | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private folderService: FolderService,
|
||||||
|
private filterService: FilterService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) {
|
||||||
|
};
|
||||||
|
|
||||||
|
applyFilter(event: Event) {
|
||||||
|
const filterValue = (event.target as HTMLInputElement).value;
|
||||||
|
this.filterService.previousInputs.set(StType.Folder, filterValue);
|
||||||
|
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.dataSource = new MatTableDataSource();
|
||||||
|
this.dataSource.data = [];
|
||||||
|
|
||||||
|
// Replace all data when requests are finished
|
||||||
|
this.folderService.foldersUpdated$.subscribe(
|
||||||
|
folders => {
|
||||||
|
this.dataSource.data = folders;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add device as they come in
|
||||||
|
let folders: Folder[] = [];
|
||||||
|
this.folderService.folderAdded$.subscribe(
|
||||||
|
folder => {
|
||||||
|
folders.push(folder);
|
||||||
|
this.dataSource.data = folders;
|
||||||
|
}
|
||||||
|
);;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
this.table.dataSource = this.dataSource;
|
||||||
|
|
||||||
|
const changeText = (text: string) => {
|
||||||
|
this.dataSource.filter = text.trim().toLowerCase();
|
||||||
|
this.input.value = text;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set previous value
|
||||||
|
changeText(this.filterService.previousInputs.get(StType.Folder));
|
||||||
|
|
||||||
|
// Listen for filter changes from other components
|
||||||
|
this.filterService.filterChanged$
|
||||||
|
.subscribe(
|
||||||
|
input => {
|
||||||
|
if (input.type === StType.Folder) {
|
||||||
|
changeText(input.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() { }
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<app-card class="status-list">
|
||||||
|
<div fxLayout="row" fxLayoutAlign="space-between start">
|
||||||
|
<app-card-title>{{title | uppercase}}</app-card-title>
|
||||||
|
<app-list-toggle (listTypeEvent)="onToggle($event)" class="tui-card-toggle"></app-list-toggle>
|
||||||
|
</div>
|
||||||
|
<app-card-content>
|
||||||
|
<app-folder-list *ngIf="currentListType===listType.Folder"></app-folder-list>
|
||||||
|
<app-device-list *ngIf="currentListType===listType.Device"> </app-device-list>
|
||||||
|
</app-card-content>
|
||||||
|
</app-card>
|
|
@ -0,0 +1,70 @@
|
||||||
|
.status-list .tui-card-toggle {
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-form-field {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.detail-row {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.table-row:not(.expanded-row):hover {
|
||||||
|
background: whitesmoke;
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.table-row:not(.expanded-row):active {
|
||||||
|
background: #DDDDDD;
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-row {
|
||||||
|
background: #DDDDDD;
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row td {
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-detail {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-items {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide empty name
|
||||||
|
.item-name:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name:not(:last-child):after {
|
||||||
|
content: ", ";
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
tr.table-row:not(.expanded-row):hover {
|
||||||
|
background: #212121;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.table-row:not(.expanded-row):active {
|
||||||
|
background: #212121;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-row {
|
||||||
|
background: #212121;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { StatusListComponent } from './status-list.component';
|
||||||
|
|
||||||
|
describe('StatusListComponent', () => {
|
||||||
|
let component: StatusListComponent;
|
||||||
|
let fixture: ComponentFixture<StatusListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [StatusListComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(StatusListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Component, ViewChild, AfterViewInit, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { StType } from '../../type';
|
||||||
|
import { cardElevation } from '../../style';
|
||||||
|
import { FilterService } from 'src/app/services/filter.service';
|
||||||
|
import { ListToggleComponent } from 'src/app/list-toggle/list-toggle.component';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-status-list',
|
||||||
|
templateUrl: './status-list.component.html',
|
||||||
|
styleUrls: ['./status-list.component.scss']
|
||||||
|
})
|
||||||
|
export class StatusListComponent {
|
||||||
|
@ViewChild(ListToggleComponent) toggle: ListToggleComponent;
|
||||||
|
currentListType: StType = StType.Folder;
|
||||||
|
listType = StType; // used in html
|
||||||
|
elevation: string = cardElevation;
|
||||||
|
title: string = 'Status';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private filterService: FilterService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
// Listen for filter changes from other components
|
||||||
|
this.filterService.filterChanged$.subscribe(
|
||||||
|
input => {
|
||||||
|
this.currentListType = input.type;
|
||||||
|
|
||||||
|
switch (input.type) {
|
||||||
|
case StType.Folder:
|
||||||
|
this.toggle.group.value = "folders";
|
||||||
|
break;
|
||||||
|
case StType.Device:
|
||||||
|
this.toggle.group.value = "devices";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cdr.detectChanges(); // manually detect changes
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggle(t: StType) {
|
||||||
|
this.currentListType = t;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
export const dbCompletion =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"device": "YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC",
|
||||||
|
"completion": 100,
|
||||||
|
"globalBytes": 156793013575,
|
||||||
|
"needBytes": 0,
|
||||||
|
"needDeletes": 0,
|
||||||
|
"needItems": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"completion": 80,
|
||||||
|
"globalBytes": 3013575,
|
||||||
|
"needBytes": 100,
|
||||||
|
"needDeletes": 0,
|
||||||
|
"needItems": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"completion": 100,
|
||||||
|
"globalBytes": 156793013575,
|
||||||
|
"needBytes": 0,
|
||||||
|
"needDeletes": 0
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,46 @@
|
||||||
|
export const dbStatus =
|
||||||
|
[
|
||||||
|
{ "folder": "GXWxf-3zgnU", "state": "active" },
|
||||||
|
{ "folder": "Tyeho-ncvqp", "state": "idle" },
|
||||||
|
{ "folder": "Ihpqp-3zgnq", "state": "idle" },
|
||||||
|
{ "folder": "Abqqp-3zgnU", "state": "idle" },
|
||||||
|
{ "folder": "Bawer-3zgnU", "state": "idle" },
|
||||||
|
{ "folder": "Zpohq-3zgnU", "state": "idle" },
|
||||||
|
{ "folder": "Lkmbn-3zgnU", "state": "idle" },
|
||||||
|
{ "folder": "Poqff-3zgnU", "state": "idle" }
|
||||||
|
]
|
||||||
|
/*[{
|
||||||
|
"folder": "GXWxf-3zgnU",
|
||||||
|
"globalBytes": 0,
|
||||||
|
"globalDeleted": 0,
|
||||||
|
"globalDirectories": 0,
|
||||||
|
"globalFiles": 0,
|
||||||
|
"globalSymlinks": 0,
|
||||||
|
"globalTotalItems": 0,
|
||||||
|
"ignorePatterns": false,
|
||||||
|
"inSyncBytes": 0,
|
||||||
|
"inSyncFiles": 0,
|
||||||
|
"invalid": "",
|
||||||
|
"localBytes": 0,
|
||||||
|
"localDeleted": 0,
|
||||||
|
"localDirectories": 0,
|
||||||
|
"localFiles": 0,
|
||||||
|
"localSymlinks": 0,
|
||||||
|
"localTotalItems": 0,
|
||||||
|
"needBytes": 0,
|
||||||
|
"needDeletes": 0,
|
||||||
|
"needDirectories": 0,
|
||||||
|
"needFiles": 0,
|
||||||
|
"needSymlinks": 0,
|
||||||
|
"needTotalItems": 0,
|
||||||
|
"pullErrors": 0,
|
||||||
|
"receiveOnlyChangedBytes": 0,
|
||||||
|
"receiveOnlyChangedDeletes": 0,
|
||||||
|
"receiveOnlyChangedDirectories": 0,
|
||||||
|
"receiveOnlyChangedFiles": 0,
|
||||||
|
"receiveOnlyChangedSymlinks": 0,
|
||||||
|
"sequence": 0,
|
||||||
|
"state": "idle",
|
||||||
|
"stateChanged": "2018-08-08T07:04:57.301064781+02:00",
|
||||||
|
"version": 0
|
||||||
|
}]*/;
|
|
@ -0,0 +1,67 @@
|
||||||
|
export const config = {
|
||||||
|
"version": 15,
|
||||||
|
"folders": [
|
||||||
|
{ "id": "GXWxf-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC" }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
|
||||||
|
{ "id": "Tyeho-ncvqp", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
|
||||||
|
{ "id": "Ihpqp-3zgnq", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
|
||||||
|
{ "id": "Abqqp-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
|
||||||
|
{ "id": "Bawer-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
|
||||||
|
{ "id": "Zpohq-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
|
||||||
|
{ "id": "Lkmbn-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
|
||||||
|
{ "id": "Poqff-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
|
||||||
|
],
|
||||||
|
"devices": [
|
||||||
|
{ "deviceID": "YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC", "name": "Laptop", "addresses": ["dynamic", "tcp://192.168.1.2:22000"], "compression": "metadata", "certName": "", "introducer": false },
|
||||||
|
{ "deviceID": "...", "name": "Server", "addresses": ["dynamic", "tcp://192.168.1.3:22000"], "compression": "metadata", "certName": "", "introducer": false },
|
||||||
|
],
|
||||||
|
"gui": {
|
||||||
|
"enabled": true,
|
||||||
|
"address": "127.0.0.1:8384",
|
||||||
|
"user": "Username",
|
||||||
|
"password": "$2a$10$ZFws69T4FlvWwsqeIwL.TOo5zOYqsa/.TxlUnsGYS.j3JvjFTmxo6",
|
||||||
|
"useTLS": false,
|
||||||
|
"apiKey": "pGahcht56664QU5eoFQW6szbEG6Ec2Cr",
|
||||||
|
"insecureAdminAccess": false,
|
||||||
|
"theme": "default"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"listenAddresses": [
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"globalAnnounceServers": [
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"globalAnnounceEnabled": true,
|
||||||
|
"localAnnounceEnabled": true,
|
||||||
|
"localAnnouncePort": 21027,
|
||||||
|
"localAnnounceMCAddr": "[ff12::8384]:21027",
|
||||||
|
"maxSendKbps": 0,
|
||||||
|
"maxRecvKbps": 0,
|
||||||
|
"reconnectionIntervalS": 60,
|
||||||
|
"relaysEnabled": true,
|
||||||
|
"relayReconnectIntervalM": 10,
|
||||||
|
"startBrowser": false,
|
||||||
|
"natEnabled": true,
|
||||||
|
"natLeaseMinutes": 60,
|
||||||
|
"natRenewalMinutes": 30,
|
||||||
|
"natTimeoutSeconds": 10,
|
||||||
|
"urAccepted": -1,
|
||||||
|
"urUniqueId": "",
|
||||||
|
"urURL": "https://data.syncthing.net/newdata",
|
||||||
|
"urPostInsecurely": false,
|
||||||
|
"urInitialDelayS": 1800,
|
||||||
|
"restartOnWakeup": true,
|
||||||
|
"autoUpgradeIntervalH": 12,
|
||||||
|
"keepTemporariesH": 24,
|
||||||
|
"cacheIgnoredFiles": false,
|
||||||
|
"progressUpdateIntervalS": 5,
|
||||||
|
"limitBandwidthInLan": false,
|
||||||
|
"minHomeDiskFreePct": 1,
|
||||||
|
"releasesURL": "https://upgrades.syncthing.net/meta.json",
|
||||||
|
"alwaysLocalNets": [],
|
||||||
|
"overwriteRemoteDeviceNamesOnConnect": false,
|
||||||
|
"tempIndexMinBlocks": 10
|
||||||
|
},
|
||||||
|
"ignoredDevices": [],
|
||||||
|
"ignoredFolders": []
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
export const connections = {
|
||||||
|
"total": {
|
||||||
|
"paused": false,
|
||||||
|
"clientVersion": "",
|
||||||
|
"at": "2015-11-07T17:29:47.691637262+01:00",
|
||||||
|
"connected": false,
|
||||||
|
"inBytesTotal": 1479,
|
||||||
|
"type": "",
|
||||||
|
"outBytesTotal": 1318,
|
||||||
|
"address": ""
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC": {
|
||||||
|
"connected": true,
|
||||||
|
"inBytesTotal": 556,
|
||||||
|
"paused": false,
|
||||||
|
"at": "2015-11-07T17:29:47.691548971+01:00",
|
||||||
|
"clientVersion": "v0.12.1",
|
||||||
|
"address": "127.0.0.1:22002",
|
||||||
|
"type": "TCP (Client)",
|
||||||
|
"outBytesTotal": 550
|
||||||
|
},
|
||||||
|
"DOVII4U-SQEEESM-VZ2CVTC-CJM4YN5-QNV7DCU-5U3ASRL-YVFG6TH-W5DV5AA": {
|
||||||
|
"outBytesTotal": 0,
|
||||||
|
"type": "",
|
||||||
|
"address": "",
|
||||||
|
"at": "0001-01-01T00:00:00Z",
|
||||||
|
"clientVersion": "",
|
||||||
|
"paused": false,
|
||||||
|
"inBytesTotal": 0,
|
||||||
|
"connected": false
|
||||||
|
},
|
||||||
|
"UYGDMA4-TPHOFO5-2VQYDCC-7CWX7XW-INZINQT-LE4B42N-4JUZTSM-IWCSXA4": {
|
||||||
|
"address": "",
|
||||||
|
"type": "",
|
||||||
|
"outBytesTotal": 0,
|
||||||
|
"connected": false,
|
||||||
|
"inBytesTotal": 0,
|
||||||
|
"paused": false,
|
||||||
|
"at": "0001-01-01T00:00:00Z",
|
||||||
|
"clientVersion": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
export const systemStatus = {
|
||||||
|
"alloc": 30618136,
|
||||||
|
"connectionServiceStatus": {
|
||||||
|
"dynamic+https://relays.syncthing.net/endpoint": {
|
||||||
|
"error": null,
|
||||||
|
"lanAddresses": [
|
||||||
|
"relay://23.92.71.120:443/?id=53STGR7-YBM6FCX-PAZ2RHM-YPY6OEJ-WYHVZO7-PCKQRCK-PZLTP7T-434XCAD&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070&providedBy=canton7"
|
||||||
|
],
|
||||||
|
"wanAddresses": [
|
||||||
|
"relay://23.92.71.120:443/?id=53STGR7-YBM6FCX-PAZ2RHM-YPY6OEJ-WYHVZO7-PCKQRCK-PZLTP7T-434XCAD&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070&providedBy=canton7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tcp://0.0.0.0:22000": {
|
||||||
|
"error": null,
|
||||||
|
"lanAddresses": [
|
||||||
|
"tcp://0.0.0.0:22000"
|
||||||
|
],
|
||||||
|
"wanAddresses": [
|
||||||
|
"tcp://0.0.0.0:22000"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cpuPercent": 0,
|
||||||
|
"discoveryEnabled": true,
|
||||||
|
"discoveryErrors": {
|
||||||
|
"global@https://discovery-v4-1.syncthing.net/v2/": "500 Internal Server Error",
|
||||||
|
"global@https://discovery-v4-2.syncthing.net/v2/": "Post https://discovery-v4-2.syncthing.net/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)",
|
||||||
|
"global@https://discovery-v4-3.syncthing.net/v2/": "Post https://discovery-v4-3.syncthing.net/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)",
|
||||||
|
"global@https://discovery-v6-1.syncthing.net/v2/": "Post https://discovery-v6-1.syncthing.net/v2/: dial tcp [2001:470:28:4d6::5]:443: connect: no route to host",
|
||||||
|
"global@https://discovery-v6-2.syncthing.net/v2/": "Post https://discovery-v6-2.syncthing.net/v2/: dial tcp [2604:a880:800:10::182:a001]:443: connect: no route to host",
|
||||||
|
"global@https://discovery-v6-3.syncthing.net/v2/": "Post https://discovery-v6-3.syncthing.net/v2/: dial tcp [2400:6180:0:d0::d9:d001]:443: connect: no route to host"
|
||||||
|
},
|
||||||
|
"discoveryMethods": 8,
|
||||||
|
"goroutines": 49,
|
||||||
|
"lastDialStatus": {
|
||||||
|
"tcp://10.20.30.40": {
|
||||||
|
"when": "2019-05-16T07:41:23Z",
|
||||||
|
"error": "dial tcp 10.20.30.40:22000: i/o timeout"
|
||||||
|
},
|
||||||
|
"tcp://172.16.33.3:22000": {
|
||||||
|
"when": "2019-05-16T07:40:43Z",
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
"tcp://83.233.120.221:22000": {
|
||||||
|
"when": "2019-05-16T07:41:13Z",
|
||||||
|
"error": "dial tcp 83.233.120.221:22000: connect: connection refused"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"myID": "YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC",
|
||||||
|
"pathSeparator": "/",
|
||||||
|
"startTime": "2016-06-06T19:41:43.039284753+02:00",
|
||||||
|
"sys": 42092792,
|
||||||
|
"themes": [
|
||||||
|
"default",
|
||||||
|
"dark"
|
||||||
|
],
|
||||||
|
"tilde": "/Users/jb",
|
||||||
|
"uptime": 2635
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CookieService } from './cookie.service';
|
||||||
|
|
||||||
|
describe('CookieService', () => {
|
||||||
|
let service: CookieService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(CookieService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CookieService {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
getCookie(name: string): string {
|
||||||
|
let ca: Array<string> = document.cookie.split(';');
|
||||||
|
let caLen: number = ca.length;
|
||||||
|
let cookieName = `${name}=`;
|
||||||
|
let c: string;
|
||||||
|
|
||||||
|
for (let i: number = 0; i < caLen; i += 1) {
|
||||||
|
c = ca[i].replace(/^\s+/g, '');
|
||||||
|
if (c.indexOf(cookieName) == 0) {
|
||||||
|
return c.substring(cookieName.length, c.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCookie(name): void {
|
||||||
|
this.setCookie(name, "", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCookie(name: string, value: string, expireDays: number, path: string = ""): void {
|
||||||
|
let d: Date = new Date();
|
||||||
|
d.setTime(d.getTime() + expireDays * 24 * 60 * 60 * 1000);
|
||||||
|
let expires: string = "expires=" + d.toUTCString();
|
||||||
|
document.cookie = name + "=" + value + "; " + expires + (path.length > 0 ? "; path=" + path : "");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DbCompletionService } from './db-completion.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
describe('DbCompletionService', () => {
|
||||||
|
let service: DbCompletionService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [DbCompletionService]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(DbCompletionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { apiURL } from '../api-utils';
|
||||||
|
import { Completion } from '../completion';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { StType } from '../type';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DbCompletionService {
|
||||||
|
private dbStatusUrl = environment.production ? apiURL + 'rest/db/completion' : 'api/dbCompletion';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
getCompletion(type: StType, id: string): Observable<Completion> {
|
||||||
|
let httpOptions: { params: HttpParams };
|
||||||
|
if (id) {
|
||||||
|
switch (type) {
|
||||||
|
case StType.Device:
|
||||||
|
httpOptions = {
|
||||||
|
params: new HttpParams().set('device', id)
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case StType.Folder:
|
||||||
|
httpOptions = {
|
||||||
|
params: new HttpParams().set('folder', id)
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else { }
|
||||||
|
|
||||||
|
return this.http
|
||||||
|
.get<Completion>(this.dbStatusUrl, httpOptions)
|
||||||
|
.pipe(
|
||||||
|
map(res => {
|
||||||
|
// Remove from array in developement
|
||||||
|
// in-memory-web-api returns arrays
|
||||||
|
if (!environment.production) {
|
||||||
|
const a: any = res as any;
|
||||||
|
if (a.length > 0) {
|
||||||
|
res = res[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DbStatusService } from './db-status.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
describe('DbStatusService', () => {
|
||||||
|
let service: DbStatusService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [DbStatusService]
|
||||||
|
});
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(DbStatusService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { environment } from '../../environments/environment'
|
||||||
|
import { apiURL } from '../api-utils'
|
||||||
|
import Folder from '../folder'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DbStatusService {
|
||||||
|
private dbStatusUrl = environment.production ? apiURL + 'rest/db/status' : 'api/dbStatus';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
getFolderStatus(id: string): Observable<Folder.Status> {
|
||||||
|
let httpOptions: { params: HttpParams };
|
||||||
|
if (id) {
|
||||||
|
httpOptions = {
|
||||||
|
params: new HttpParams().set('folder', id)
|
||||||
|
};
|
||||||
|
} else { }
|
||||||
|
|
||||||
|
return this.http
|
||||||
|
.get<Folder.Status>(this.dbStatusUrl, httpOptions)
|
||||||
|
.pipe(
|
||||||
|
map(res => {
|
||||||
|
// Remove from array in developement
|
||||||
|
// in-memory-web-api returns arrays
|
||||||
|
if (!environment.production) {
|
||||||
|
const a: any = res as any;
|
||||||
|
if (a.length > 0) {
|
||||||
|
res = res[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DeviceService } from './device.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
describe('DeviceService', () => {
|
||||||
|
let service: DeviceService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [DeviceService]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(DeviceService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import Device from '../device';
|
||||||
|
import { Observable, Subscriber, ReplaySubject, Subject } from 'rxjs';
|
||||||
|
import { SystemConfigService } from './system-config.service';
|
||||||
|
import { SystemConnectionsService } from './system-connections.service';
|
||||||
|
import { DbCompletionService } from './db-completion.service';
|
||||||
|
import { SystemConnections } from '../connections';
|
||||||
|
import { SystemStatusService } from './system-status.service';
|
||||||
|
import { ProgressService } from './progress.service';
|
||||||
|
import { StType } from '../type';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DeviceService {
|
||||||
|
private devices: Device[];
|
||||||
|
private sysConns: SystemConnections;
|
||||||
|
private devicesSubject: ReplaySubject<Device[]> = new ReplaySubject(1);
|
||||||
|
devicesUpdated$ = this.devicesSubject.asObservable();
|
||||||
|
private thisDevice: Device;
|
||||||
|
|
||||||
|
private deviceAddedSource = new Subject<Device>();
|
||||||
|
deviceAdded$ = this.deviceAddedSource.asObservable();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private systemConfigService: SystemConfigService,
|
||||||
|
private systemConnectionsService: SystemConnectionsService,
|
||||||
|
private dbCompletionService: DbCompletionService,
|
||||||
|
private systemStatusService: SystemStatusService,
|
||||||
|
private progressService: ProgressService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
getDeviceStatusInOrder(startIndex: number) {
|
||||||
|
// Return if there aren't any device at the index
|
||||||
|
if (startIndex >= (this.devices.length)) {
|
||||||
|
this.devicesSubject.next(this.devices);
|
||||||
|
// this.devicesSubject.complete();
|
||||||
|
// this.deviceAddedSource.complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const device: Device = this.devices[startIndex];
|
||||||
|
startIndex = startIndex + 1;
|
||||||
|
|
||||||
|
// Check if device in the connections
|
||||||
|
if (this.sysConns.connections[device.deviceID] === undefined) {
|
||||||
|
device.stateType = Device.StateType.Unknown;
|
||||||
|
} else {
|
||||||
|
// Set connected
|
||||||
|
device.connected = this.sysConns.connections[device.deviceID].connected;
|
||||||
|
|
||||||
|
// TODO ? temporarily set to connected
|
||||||
|
if (device.deviceID === this.thisDevice.deviceID) {
|
||||||
|
device.connected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dbCompletionService.getCompletion(StType.Device, device.deviceID).subscribe(
|
||||||
|
c => {
|
||||||
|
device.completion = c;
|
||||||
|
Device.recalcCompletion(device);
|
||||||
|
device.stateType = Device.getStateType(device);
|
||||||
|
device.state = Device.stateTypeToString(device.stateType);
|
||||||
|
|
||||||
|
this.deviceAddedSource.next(device);
|
||||||
|
this.progressService.addToProgress(1);
|
||||||
|
|
||||||
|
// recursively get the status of the next device
|
||||||
|
this.getDeviceStatusInOrder(startIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getEach() returns each device
|
||||||
|
*/
|
||||||
|
requestDevices() {
|
||||||
|
this.systemConfigService.getDevices().subscribe(
|
||||||
|
devices => {
|
||||||
|
this.devices = devices;
|
||||||
|
|
||||||
|
// First check to see which device is local 'thisDevice'
|
||||||
|
this.systemStatusService.getSystemStatus().subscribe(
|
||||||
|
status => {
|
||||||
|
this.devices.forEach(device => {
|
||||||
|
if (device.deviceID === status.myID) {
|
||||||
|
// TODO Determine if it should ignore thisDevice
|
||||||
|
this.thisDevice = device;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check folder devices to see if the device is used
|
||||||
|
this.systemConfigService.getFolders().subscribe(
|
||||||
|
folders => {
|
||||||
|
// Loop through all folder devices to see if the device is used
|
||||||
|
this.devices.forEach(device => {
|
||||||
|
// Alloc array if needed
|
||||||
|
if (!device.folders) {
|
||||||
|
device.folders = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.forEach(folder => {
|
||||||
|
folder.devices.forEach(fdevice => {
|
||||||
|
if (device.deviceID === fdevice.deviceID) {
|
||||||
|
// The device is used by a folder
|
||||||
|
device.used = true;
|
||||||
|
|
||||||
|
// Add a reference to the folder to the device
|
||||||
|
device.folders.push(folder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// See if the connection is connected or undefined
|
||||||
|
this.systemConnectionsService.getSystemConnections().subscribe(
|
||||||
|
c => {
|
||||||
|
this.sysConns = c;
|
||||||
|
|
||||||
|
// Synchronously get the status of each device
|
||||||
|
this.getDeviceStatusInOrder(0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FilterService } from './filter.service';
|
||||||
|
|
||||||
|
describe('FilterService', () => {
|
||||||
|
let service: FilterService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(FilterService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { StType } from '../type';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
export interface FilterInput {
|
||||||
|
type: StType;
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class FilterService {
|
||||||
|
previousInputs = new Map<StType, string>(
|
||||||
|
[
|
||||||
|
[StType.Folder, ""],
|
||||||
|
[StType.Device, ""],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
private filterChangeSource = new Subject<FilterInput>();
|
||||||
|
filterChanged$ = this.filterChangeSource.asObservable();
|
||||||
|
|
||||||
|
changeFilter(input: FilterInput) {
|
||||||
|
this.previousInputs.set(input.type, input.text)
|
||||||
|
this.filterChangeSource.next(input);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FolderService } from './folder.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
describe('FolderService', () => {
|
||||||
|
let service: FolderService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [FolderService]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(FolderService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { SystemConfigService } from './system-config.service';
|
||||||
|
import { Observable, Subscriber, Subject, ReplaySubject } from 'rxjs';
|
||||||
|
import Folder from '../folder';
|
||||||
|
import { DbStatusService } from './db-status.service';
|
||||||
|
import { ProgressService } from './progress.service';
|
||||||
|
import { DbCompletionService } from './db-completion.service';
|
||||||
|
import { StType } from '../type';
|
||||||
|
import { DeviceService } from './device.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class FolderService {
|
||||||
|
private folders: Folder[];
|
||||||
|
private foldersSubject: ReplaySubject<Folder[]> = new ReplaySubject(1);
|
||||||
|
foldersUpdated$ = this.foldersSubject.asObservable();
|
||||||
|
private folderAddedSource = new Subject<Folder>();
|
||||||
|
folderAdded$ = this.folderAddedSource.asObservable();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private systemConfigService: SystemConfigService,
|
||||||
|
private deviceService: DeviceService,
|
||||||
|
private dbStatusService: DbStatusService,
|
||||||
|
private dbCompletionService: DbCompletionService,
|
||||||
|
private progressService: ProgressService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
getFolderStatusInOrder(startIndex: number) {
|
||||||
|
// Return if there aren't any folders at the index
|
||||||
|
if (startIndex >= (this.folders.length)) {
|
||||||
|
this.foldersSubject.next(this.folders);
|
||||||
|
// this.folderAddedSource.complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const folder: Folder = this.folders[startIndex];
|
||||||
|
startIndex = startIndex + 1;
|
||||||
|
|
||||||
|
// Folder devices array only has deviceID
|
||||||
|
// and we want all the device info
|
||||||
|
this.systemConfigService.getDevices().subscribe(
|
||||||
|
devices => {
|
||||||
|
devices.forEach(device => {
|
||||||
|
// Update any device this folder
|
||||||
|
// has reference to
|
||||||
|
folder.devices.forEach((folderDevice, index) => {
|
||||||
|
if (folderDevice.deviceID === device.deviceID) {
|
||||||
|
console.log("find device match?", device.name)
|
||||||
|
folder.devices[index] = device;
|
||||||
|
|
||||||
|
console.log("update?", folder.devices);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gather the folder information from the status and
|
||||||
|
// completion services
|
||||||
|
this.dbStatusService.getFolderStatus(folder.id).subscribe(
|
||||||
|
status => {
|
||||||
|
folder.status = status;
|
||||||
|
|
||||||
|
this.dbCompletionService.getCompletion(StType.Folder, folder.id).subscribe(
|
||||||
|
c => {
|
||||||
|
folder.completion = c;
|
||||||
|
folder.stateType = Folder.getStateType(folder);
|
||||||
|
folder.state = Folder.stateTypeToString(folder.stateType);
|
||||||
|
|
||||||
|
this.folderAddedSource.next(folder);
|
||||||
|
this.progressService.addToProgress(1);
|
||||||
|
|
||||||
|
// Now that we have all the folder information
|
||||||
|
// recursively get the status of the next folder
|
||||||
|
this.getFolderStatusInOrder(startIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* requestFolders() requests each folder and uses db status service to
|
||||||
|
* set all their statuses and db completion service to find
|
||||||
|
* completion in order. Updating folderAdded$ and foldersUpdate$
|
||||||
|
* observers
|
||||||
|
*/
|
||||||
|
requestFolders() {
|
||||||
|
this.systemConfigService.getFolders().subscribe(
|
||||||
|
folders => {
|
||||||
|
this.folders = folders;
|
||||||
|
|
||||||
|
// Synchronously get the status of each folder
|
||||||
|
this.getFolderStatusInOrder(0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { InMemoryConfigDataService } from './in-memory-config-data.service';
|
||||||
|
|
||||||
|
describe('InMemoryDataService', () => {
|
||||||
|
let service: InMemoryConfigDataService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(InMemoryConfigDataService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { config } from '../mocks/mock-system-config';
|
||||||
|
import { dbStatus } from '../mocks/mock-db-status';
|
||||||
|
import { connections } from '../mocks/mock-system-connections';
|
||||||
|
import { dbCompletion } from '../mocks/mock-db-completion';
|
||||||
|
import { systemStatus } from '../mocks/mock-system-status';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class InMemoryConfigDataService {
|
||||||
|
createDb() {
|
||||||
|
return { config, dbStatus, connections, dbCompletion, systemStatus };
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MessageService } from './message.service';
|
||||||
|
|
||||||
|
describe('MessageService', () => {
|
||||||
|
let service: MessageService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(MessageService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MessageService {
|
||||||
|
messages: string[] = [];
|
||||||
|
private messageAddedSource = new Subject<string>();
|
||||||
|
messageAdded$ = this.messageAddedSource.asObservable();
|
||||||
|
|
||||||
|
add(message: string) {
|
||||||
|
this.messages.push(message);
|
||||||
|
this.messageAddedSource.next(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProgressService } from './progress.service';
|
||||||
|
import { stringToKeyValue } from '@angular/flex-layout/extended/typings/style/style-transforms';
|
||||||
|
|
||||||
|
describe('ProgressService', () => {
|
||||||
|
let service: ProgressService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ProgressService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('#percentValue should return 0 - 100', () => {
|
||||||
|
interface iTest {
|
||||||
|
total: number,
|
||||||
|
progress: number,
|
||||||
|
expected: number,
|
||||||
|
}
|
||||||
|
const tests: Map<string, iTest> = new Map([
|
||||||
|
["default", { total: 0, progress: 0, expected: 0 }],
|
||||||
|
["NaN return 0", { total: 0, progress: 100, expected: 0 }],
|
||||||
|
["greater than 100 return 100", { total: 10, progress: 100, expected: 100 }],
|
||||||
|
["valid", { total: 100, progress: 100, expected: 100 }],
|
||||||
|
["valid", { total: 100, progress: 50, expected: 50 }],
|
||||||
|
["test floor", { total: 133, progress: 41, expected: 30 }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
service = new ProgressService();
|
||||||
|
for (let test of tests.values()) {
|
||||||
|
service.total = test.total;
|
||||||
|
service.updateProgress(test.progress);
|
||||||
|
expect(service.percentValue).toBe(test.expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ProgressService {
|
||||||
|
private progress: number = 0;
|
||||||
|
private _total: number = 0;
|
||||||
|
set total(t: number) {
|
||||||
|
this._total = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
get percentValue(): number {
|
||||||
|
let p: number = Math.floor((this.progress / this._total) * 100);
|
||||||
|
if (p < 0 || isNaN(p) || p === Infinity) {
|
||||||
|
p = 0;
|
||||||
|
} else if (p > 100) {
|
||||||
|
p = 100;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
addToProgress(n: number) {
|
||||||
|
if (n < 0 || isNaN(n) || n === Infinity) {
|
||||||
|
n = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progress += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(n: number) {
|
||||||
|
if (n < 0 || isNaN(n) || n === Infinity) {
|
||||||
|
n = 0
|
||||||
|
} else if (n > 100) {
|
||||||
|
n = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progress = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
isComplete(): boolean {
|
||||||
|
if (this.progress >= this._total && this.progress > 0 && this._total > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RequestCacheService } from './request-cache.service';
|
||||||
|
|
||||||
|
describe('RequestCacheService', () => {
|
||||||
|
let service: RequestCacheService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(RequestCacheService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpResponse, HttpRequest } from '@angular/common/http';
|
||||||
|
|
||||||
|
export interface RequestCacheEntry {
|
||||||
|
url: string;
|
||||||
|
response: HttpResponse<any>;
|
||||||
|
lastRead: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxAge = 30000; // milliseconds
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RequestCacheService {
|
||||||
|
private cache: Map<string, RequestCacheEntry> = new Map();
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
get(req: HttpRequest<any>): HttpResponse<any> | undefined {
|
||||||
|
const url = req.urlWithParams;
|
||||||
|
const cached = this.cache.get(url);
|
||||||
|
|
||||||
|
if (!cached) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpired = cached.lastRead < (Date.now() - maxAge);
|
||||||
|
return isExpired ? undefined : cached.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
put(req: HttpRequest<any>, response: HttpResponse<any>): void {
|
||||||
|
const url = req.urlWithParams;
|
||||||
|
|
||||||
|
const entry = { url, response, lastRead: Date.now() };
|
||||||
|
this.cache.set(url, entry);
|
||||||
|
|
||||||
|
// Remove expired cache entries
|
||||||
|
const expired = Date.now() - maxAge;
|
||||||
|
this.cache.forEach(entry => {
|
||||||
|
if (entry.lastRead < expired) {
|
||||||
|
this.cache.delete(entry.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAll(): void {
|
||||||
|
this.cache = new Map();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SystemConfigService } from './system-config.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
describe('SystemConfigService', () => {
|
||||||
|
let service: SystemConfigService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [SystemConfigService]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(SystemConfigService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Observable, ReplaySubject } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import Folder from '../folder';
|
||||||
|
import Device from '../device';
|
||||||
|
import { environment } from '../../environments/environment'
|
||||||
|
import { apiURL } from '../api-utils'
|
||||||
|
import { ProgressService } from './progress.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class SystemConfigService {
|
||||||
|
private folders: Folder[];
|
||||||
|
private devices: Device[];
|
||||||
|
private foldersSubject: ReplaySubject<Folder[]> = new ReplaySubject(1);
|
||||||
|
private devicesSubject: ReplaySubject<Device[]> = new ReplaySubject(1);
|
||||||
|
|
||||||
|
private systemConfigUrl = environment.production ? apiURL + 'rest/system/config' : 'api/config';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private progressService: ProgressService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
getSystemConfig(): Observable<any> {
|
||||||
|
return this.http
|
||||||
|
.get(this.systemConfigUrl)
|
||||||
|
.pipe(
|
||||||
|
map(res => {
|
||||||
|
this.folders = res['folders'];
|
||||||
|
this.devices = res['devices'];
|
||||||
|
|
||||||
|
// Set the total for the progress service
|
||||||
|
this.progressService.total = this.folders.length + this.devices.length;
|
||||||
|
|
||||||
|
this.foldersSubject.next(this.folders);
|
||||||
|
this.devicesSubject.next(this.devices);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFolders(): Observable<Folder[]> {
|
||||||
|
return this.foldersSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDevices(): Observable<Device[]> {
|
||||||
|
return this.devicesSubject.asObservable();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SystemConnectionsService } from './system-connections.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
describe('SystemConnectionsService', () => {
|
||||||
|
let service: SystemConnectionsService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [SystemConnectionsService]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(SystemConnectionsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { apiURL } from '../api-utils';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { SystemConnections } from '../connections';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class SystemConnectionsService {
|
||||||
|
private systemConfigUrl = environment.production ? apiURL + 'rest/system/connections' : 'api/connections';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
getSystemConnections(): Observable<SystemConnections> {
|
||||||
|
return this.http
|
||||||
|
.get<SystemConnections>(this.systemConfigUrl)
|
||||||
|
.pipe(
|
||||||
|
map(res => {
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SystemStatusService } from './system-status.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
describe('SystemStatusService', () => {
|
||||||
|
let service: SystemStatusService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientModule],
|
||||||
|
providers: [SystemStatusService]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(SystemStatusService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue