mirror of
https://github.com/syncthing/syncthing.git
synced 2025-12-23 14:08:06 -05:00
chore: remove abandoned next-gen-gui experiment (#10004)
It didn't go anywhere, it adds no value where it is.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,5 +17,4 @@ deb
|
|||||||
*.bz2
|
*.bz2
|
||||||
/repos
|
/repos
|
||||||
/proto/scripts/protoc-gen-gosyncthing
|
/proto/scripts/protoc-gen-gosyncthing
|
||||||
/gui/next-gen-gui
|
|
||||||
/compat.json
|
/compat.json
|
||||||
|
|||||||
82
build.go
82
build.go
@@ -38,27 +38,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
goarch string
|
goarch string
|
||||||
goos string
|
goos string
|
||||||
noupgrade bool
|
noupgrade bool
|
||||||
version string
|
version string
|
||||||
goCmd string
|
goCmd string
|
||||||
race bool
|
race bool
|
||||||
debug = os.Getenv("BUILDDEBUG") != ""
|
debug = os.Getenv("BUILDDEBUG") != ""
|
||||||
extraTags string
|
extraTags string
|
||||||
installSuffix string
|
installSuffix string
|
||||||
pkgdir string
|
pkgdir string
|
||||||
cc string
|
cc string
|
||||||
run string
|
run string
|
||||||
benchRun string
|
benchRun string
|
||||||
buildOut string
|
buildOut string
|
||||||
debugBinary bool
|
debugBinary bool
|
||||||
coverage bool
|
coverage bool
|
||||||
long bool
|
long bool
|
||||||
timeout = "120s"
|
timeout = "120s"
|
||||||
longTimeout = "600s"
|
longTimeout = "600s"
|
||||||
numVersions = 5
|
numVersions = 5
|
||||||
withNextGenGUI = os.Getenv("BUILD_NEXT_GEN_GUI") != ""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type target struct {
|
type target struct {
|
||||||
@@ -380,7 +379,6 @@ func parseFlags() {
|
|||||||
flag.IntVar(&numVersions, "num-versions", numVersions, "Number of versions for changelog command")
|
flag.IntVar(&numVersions, "num-versions", numVersions, "Number of versions for changelog command")
|
||||||
flag.StringVar(&run, "run", "", "Specify which tests to run")
|
flag.StringVar(&run, "run", "", "Specify which tests to run")
|
||||||
flag.StringVar(&benchRun, "bench", "", "Specify which benchmarks to run")
|
flag.StringVar(&benchRun, "bench", "", "Specify which benchmarks to run")
|
||||||
flag.BoolVar(&withNextGenGUI, "with-next-gen-gui", withNextGenGUI, "Also build 'newgui'")
|
|
||||||
flag.StringVar(&buildOut, "build-out", "", "Set the '-o' value for 'go build'")
|
flag.StringVar(&buildOut, "build-out", "", "Set the '-o' value for 'go build'")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
}
|
}
|
||||||
@@ -453,10 +451,6 @@ func benchArgs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func install(target target, tags []string) {
|
func install(target target, tags []string) {
|
||||||
if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
|
|
||||||
log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
|
|
||||||
}
|
|
||||||
|
|
||||||
lazyRebuildAssets()
|
lazyRebuildAssets()
|
||||||
|
|
||||||
tags = append(target.tags, tags...)
|
tags = append(target.tags, tags...)
|
||||||
@@ -486,10 +480,6 @@ func install(target target, tags []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func build(target target, tags []string) {
|
func build(target target, tags []string) {
|
||||||
if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
|
|
||||||
log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
|
|
||||||
}
|
|
||||||
|
|
||||||
lazyRebuildAssets()
|
lazyRebuildAssets()
|
||||||
tags = append(target.tags, tags...)
|
tags = append(target.tags, tags...)
|
||||||
|
|
||||||
@@ -823,43 +813,11 @@ func lazyRebuildAssets() {
|
|||||||
shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
|
shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
|
||||||
shouldRebuildAssets("cmd/infra/strelaypoolsrv/auto/gui.files.go", "cmd/infra/strelaypoolsrv/gui")
|
shouldRebuildAssets("cmd/infra/strelaypoolsrv/auto/gui.files.go", "cmd/infra/strelaypoolsrv/gui")
|
||||||
|
|
||||||
if withNextGenGUI {
|
|
||||||
shouldRebuild = buildNextGenGUI() || shouldRebuild
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldRebuild {
|
if shouldRebuild {
|
||||||
rebuildAssets()
|
rebuildAssets()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildNextGenGUI() bool {
|
|
||||||
// Check if we need to run the npm process, and if so also set the flag
|
|
||||||
// to rebuild Go assets afterwards. The index.html is regenerated every
|
|
||||||
// time by the build process. This assumes the new GUI ends up in
|
|
||||||
// next-gen-gui/dist/next-gen-gui.
|
|
||||||
|
|
||||||
if !shouldRebuildAssets("gui/next-gen-gui/index.html", "next-gen-gui") {
|
|
||||||
// The GUI is up to date.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
runPrintInDir("next-gen-gui", "npm", "install")
|
|
||||||
runPrintInDir("next-gen-gui", "npm", "run", "build", "--", "--prod", "--subresource-integrity")
|
|
||||||
|
|
||||||
rmr("gui/tech-ui")
|
|
||||||
|
|
||||||
for _, src := range listFiles("next-gen-gui/dist") {
|
|
||||||
rel, _ := filepath.Rel("next-gen-gui/dist", src)
|
|
||||||
dst := filepath.Join("gui", rel)
|
|
||||||
if err := copyFile(src, dst, 0o644); err != nil {
|
|
||||||
fmt.Println("copy:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldRebuildAssets(target, srcdir string) bool {
|
func shouldRebuildAssets(target, srcdir string) bool {
|
||||||
info, err := os.Stat(target)
|
info, err := os.Stat(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
# 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
|
|
||||||
46
next-gen-gui/.gitignore
vendored
46
next-gen-gui/.gitignore
vendored
@@ -1,46 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# 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:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
||||||
"version": 1,
|
|
||||||
"newProjectRoot": "projects",
|
|
||||||
"projects": {
|
|
||||||
"next-gen-gui": {
|
|
||||||
"projectType": "application",
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"style": "scss"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "",
|
|
||||||
"sourceRoot": "src",
|
|
||||||
"prefix": "app",
|
|
||||||
"architect": {
|
|
||||||
"build": {
|
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/next-gen-gui",
|
|
||||||
"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": "next-gen-gui:build"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"browserTarget": "next-gen-gui:build:production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"extract-i18n": {
|
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
|
||||||
"options": {
|
|
||||||
"browserTarget": "next-gen-gui: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": "next-gen-gui:serve"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"devServerTarget": "next-gen-gui:serve:production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
"defaultProject": "next-gen-gui"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# 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'.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// @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 } }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../out-tsc/e2e",
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"types": [
|
|
||||||
"jasmine",
|
|
||||||
"jasminewd2",
|
|
||||||
"node"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Karma configuration file, see link for more information
|
|
||||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
basePath: '',
|
|
||||||
frameworks: ['jasmine', '@angular-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
|
|
||||||
});
|
|
||||||
};
|
|
||||||
37977
next-gen-gui/package-lock.json
generated
37977
next-gen-gui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "next-gen-gui",
|
|
||||||
"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.
|
Before Width: | Height: | Size: 734 KiB |
@@ -1,9 +0,0 @@
|
|||||||
import { environment } from '../environments/environment'
|
|
||||||
|
|
||||||
export const deviceID = (): String => {
|
|
||||||
// keep consistent with ShortIDStringLength in lib/protocol/deviceid.go
|
|
||||||
return environment.production ? globalThis.metadata['deviceIDShort'] : '1234567';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const apiURL: String = '/'
|
|
||||||
export const apiRetry: number = 3;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
|
|
||||||
const routes: Routes = [];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forRoot(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class AppRoutingModule { }
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<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>
|
|
||||||
<button mat-stroked-button (click)="restoreDefaultTheme()">Restore default theme</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<app-dashboard></app-dashboard>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
.header {
|
|
||||||
margin: 15px 3vw 12px 3vw;
|
|
||||||
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-form-field {
|
|
||||||
font-size: 14px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { SystemConfigService } from './services/system-config.service';
|
|
||||||
import { MessageService } from './services/message.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
templateUrl: './app.component.html',
|
|
||||||
styleUrls: ['./app.component.scss']
|
|
||||||
})
|
|
||||||
export class AppComponent {
|
|
||||||
constructor(
|
|
||||||
private systemConfigService: SystemConfigService,
|
|
||||||
private messageService: MessageService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
restoreDefaultTheme(): void {
|
|
||||||
this.systemConfigService
|
|
||||||
.setGUITheme('default')
|
|
||||||
.subscribe(() => {
|
|
||||||
this.messageService.add('The default GUI theme has been selected. Please hit "Reload" in your browser.')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
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 { }
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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() { }
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<div fxLayout="row" fxLayoutAlign="space-between start" [ngClass]="(_selected)?'item selected':'item'">
|
|
||||||
<div><a href="#">{{state}}</a>: </div>
|
|
||||||
<div>{{count}}</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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() { }
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
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),
|
|
||||||
() => {
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// 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';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Completion {
|
|
||||||
completion: number;
|
|
||||||
globalBytes: number;
|
|
||||||
needBytes: number;
|
|
||||||
needDeletes: number;
|
|
||||||
needItems: number;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<!--<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="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>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.progress {
|
|
||||||
margin: 0 3vw 0 3vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-container {
|
|
||||||
margin: 10px calc(10px + 3.3vw);
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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 cacheable.
|
|
||||||
if (!isCacheable(req)) { return next.handle(req); }
|
|
||||||
|
|
||||||
const cachedResponse = this.cache.get(req);
|
|
||||||
return cachedResponse ?
|
|
||||||
of(cachedResponse) : sendRequest(req, next, this.cache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Is this request cacheable? */
|
|
||||||
function isCacheable(req: HttpRequest<any>) {
|
|
||||||
// Only GET requests are cacheable
|
|
||||||
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.
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
/* "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 },
|
|
||||||
];
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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() { }
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
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() { }
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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
|
|
||||||
}]*/;
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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": []
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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 : "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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 development
|
|
||||||
// in-memory-web-api returns arrays
|
|
||||||
if (!environment.production) {
|
|
||||||
const a: any = res as any;
|
|
||||||
if (a.length > 0) {
|
|
||||||
res = res[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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 development
|
|
||||||
// in-memory-web-api returns arrays
|
|
||||||
if (!environment.production) {
|
|
||||||
const a: any = res as any;
|
|
||||||
if (a.length > 0) {
|
|
||||||
res = res[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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() { }
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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 = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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';
|
|
||||||
private guiConfigUrl = environment.production ? apiURL + 'rest/config/gui' : 'api/config/gui';
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
setGUITheme(theme: String): Observable<any> {
|
|
||||||
return this.http.patch(this.guiConfigUrl, { theme: theme })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user