Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c0b2d93e3 | ||
|
|
6df8e78fa0 | ||
|
|
00954f4686 | ||
|
|
7840c7c0ec | ||
|
|
703a43a817 | ||
|
|
0794f97a50 | ||
|
|
e34fcfafa4 | ||
|
|
1edec8e222 | ||
|
|
c2c7f2d3b7 | ||
|
|
80081e337d | ||
|
|
c3149e2965 | ||
|
|
74f9d2e8c4 | ||
|
|
3f7603ec99 | ||
|
|
903665c07e | ||
|
|
c4533b6c2b | ||
|
|
3943946575 | ||
|
|
1fe730c4d2 | ||
|
|
11884281cb | ||
|
|
3cb9de9710 | ||
|
|
88680e15c6 | ||
|
|
ea4572c691 | ||
|
|
27de381f5e | ||
|
|
483acc8858 | ||
|
|
fda08fb4dd | ||
|
|
182d1e8eff | ||
|
|
10d354a550 | ||
|
|
97169aecea | ||
|
|
06be5d3299 | ||
|
|
14d6a60fb0 | ||
|
|
069a3a2af2 | ||
|
|
d8f796354d | ||
|
|
38b01c6dcc | ||
|
|
bea5e018b1 | ||
|
|
79139193e5 | ||
|
|
913ff61c83 | ||
|
|
4f9e150252 | ||
|
|
d989bb8921 | ||
|
|
19a15c030d | ||
|
|
463fc9b676 | ||
|
|
00a2871c50 | ||
|
|
bb6196aab7 | ||
|
|
36a88b0fd3 | ||
|
|
3b38c73950 | ||
|
|
3c2018889f | ||
|
|
fb29d6a42e | ||
|
|
fd0fdac309 | ||
|
|
a563522ec0 | ||
|
|
41ae76c10e | ||
|
|
f8438bfaac | ||
|
|
5027b8c600 | ||
|
|
a93776f907 | ||
|
|
4a4ece124f | ||
|
|
690458b270 | ||
|
|
1df45d04a1 | ||
|
|
4f60ccfda6 | ||
|
|
4e9e61fd28 | ||
|
|
63b64005e9 | ||
|
|
6e644b03fa | ||
|
|
789fc6064e | ||
|
|
6c604c46a2 | ||
|
|
472a632b96 | ||
|
|
ec2c2a955c | ||
|
|
4a980a9176 | ||
|
|
ad9f831b91 | ||
|
|
1516f90712 | ||
|
|
09ba050849 | ||
|
|
7106c997b6 | ||
|
|
d8241362e6 | ||
|
|
b32b2ef3ab | ||
|
|
5b0be8a652 | ||
|
|
db98e8c6b7 | ||
|
|
d5b172e25f | ||
|
|
6e73ea28ea | ||
|
|
16373774d3 | ||
|
|
22bc9ed266 | ||
|
|
79f85f1187 | ||
|
|
6a865b0827 | ||
|
|
aac2cc219b | ||
|
|
4437dd4c63 | ||
|
|
9db2938cf1 | ||
|
|
78fba9bada | ||
|
|
f907d99d8d | ||
|
|
a6af5ff82e | ||
|
|
00a6e2cc63 | ||
|
|
8ffbd09bd6 | ||
|
|
8c51c86ff1 | ||
|
|
da257201b2 | ||
|
|
c34b618a9a | ||
|
|
920304be99 | ||
|
|
86c1da13d3 | ||
|
|
b0f4573485 | ||
|
|
405b5d3cca | ||
|
|
a57741de61 | ||
|
|
b6d86d0566 | ||
|
|
898141a8fe | ||
|
|
1cfb2dbb25 | ||
|
|
feb00e9f5a | ||
|
|
fe42b01021 | ||
|
|
aad653bf0b | ||
|
|
82cbb38ea6 | ||
|
|
6b78772be7 | ||
|
|
8b8458832b | ||
|
|
6ff1b356d3 | ||
|
|
429d60dda8 | ||
|
|
bcecb28dc1 | ||
|
|
3aaab06db0 | ||
|
|
4a04b896dd | ||
|
|
a485de4b5f | ||
|
|
b359f24dbe | ||
|
|
a6623a539e | ||
|
|
217eb83985 | ||
|
|
1768964e0f | ||
|
|
1c00ab899a | ||
|
|
c10e4b2951 | ||
|
|
4841b3a65a | ||
|
|
d06e52d6e5 | ||
|
|
53164ac40d | ||
|
|
b3302fb84c | ||
|
|
0c0ca21ac8 | ||
|
|
3ba7ef8ca9 | ||
|
|
48516e468e | ||
|
|
b9337ae636 | ||
|
|
f547adb550 | ||
|
|
d7f578aa50 | ||
|
|
b672a5092b | ||
|
|
6d4e30ddee | ||
|
|
50a48c5dae | ||
|
|
86644cdb00 | ||
|
|
83f757ce27 | ||
|
|
38ece7462e | ||
|
|
526dcd4ac7 | ||
|
|
c81ad70e88 | ||
|
|
9691b4edb7 | ||
|
|
99a2ad747b | ||
|
|
6345ce97c7 | ||
|
|
29fe7a4fa5 | ||
|
|
cf69e02156 | ||
|
|
72ecfdff5c | ||
|
|
4dea0b828a | ||
|
|
40f559fbe5 | ||
|
|
0627fae904 | ||
|
|
ae127ccde2 | ||
|
|
83d4e80d52 | ||
|
|
42f6232e5f | ||
|
|
d9b18b920c | ||
|
|
9aafc19923 | ||
|
|
86fe6ca57b | ||
|
|
edd312017a |
@@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
|
.vscode
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: "Test build of image when Dockerfile is changed"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rebuild-container:
|
||||||
|
name: "Build image with cache"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
with:
|
||||||
|
platforms: linux/arm64
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
-
|
||||||
|
name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Build image
|
||||||
|
id: docker_build_amd64
|
||||||
|
uses: docker/build-push-action@v5.1.0
|
||||||
|
with:
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: public-pool-ui:amd64
|
||||||
|
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --force
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build:github
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
if: success()
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: dist/public-pool-ui
|
||||||
|
enable_jekyll: true
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Build and scan container for vulnerabilities with Trivy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '22 14 * * 0'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and scan images
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Build image from Dockerfile
|
||||||
|
uses: docker/build-push-action@v5.1.0
|
||||||
|
with:
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: ${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
|
||||||
|
-
|
||||||
|
name: Run Trivy vulnerability scanner against "latest" image
|
||||||
|
uses: aquasecurity/trivy-action@master
|
||||||
|
with:
|
||||||
|
image-ref: '${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest'
|
||||||
|
format: 'sarif'
|
||||||
|
output: 'trivy-results.sarif'
|
||||||
|
severity: 'CRITICAL,HIGH'
|
||||||
|
-
|
||||||
|
name: Upload "latest" Trivy scan results to GitHub Security tab
|
||||||
|
uses: github/codeql-action/upload-sarif@v2
|
||||||
|
with:
|
||||||
|
sarif_file: 'trivy-results.sarif'
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: "Update image and push to Github Packages and Docker Hub weekly"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 12 * * 1" # Run every Monday at noon.
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rebuild-container:
|
||||||
|
name: "Rebuild Container with the latest base image"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Prepare outputs
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
-
|
||||||
|
name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
with:
|
||||||
|
platforms: linux/arm64
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
-
|
||||||
|
name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3.0.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3.0.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
-
|
||||||
|
name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Get short SHA
|
||||||
|
id: get_short_sha
|
||||||
|
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
-
|
||||||
|
name: Build and push to Docker Hub and GitHub Packages Docker Registry
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v5.1.0
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository_owner }}/public-pool-ui:latest
|
||||||
|
ghcr.io/${{ github.repository_owner }}/public-pool-ui:${{ steps.get_short_sha.outputs.sha_short }}
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/public-pool-ui:${{ steps.get_short_sha.outputs.sha_short }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.source=${{ github.event.repository.html_url }}
|
||||||
|
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
|
||||||
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: "Update image when Dockerfile is changed"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rebuild-container:
|
||||||
|
name: "Rebuild Container with the latest base image"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Prepare outputs
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
-
|
||||||
|
name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
with:
|
||||||
|
platforms: linux/arm64
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
-
|
||||||
|
name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3.0.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3.0.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
-
|
||||||
|
name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Get short SHA
|
||||||
|
id: get_short_sha
|
||||||
|
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
-
|
||||||
|
name: Build and push to Docker Hub and GitHub Packages Docker Registry
|
||||||
|
uses: docker/build-push-action@v5.1.0
|
||||||
|
id: docker_build_push
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository_owner }}/public-pool-ui:latest
|
||||||
|
ghcr.io/${{ github.repository_owner }}/public-pool-ui:${{ steps.get_short_sha.outputs.sha_short }}
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/public-pool-ui:${{ steps.get_short_sha.outputs.sha_short }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.source=${{ github.event.repository.html_url }}
|
||||||
|
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
|
||||||
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
|
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/public-pool-ui:latest
|
||||||
|
cache-to: type=inline
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||||
|
"recommendations": ["angular.ng-template"]
|
||||||
|
}
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "ng serve",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:4200/",
|
||||||
|
"sourceMaps": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ng test",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: test",
|
||||||
|
"url": "http://localhost:9876/debug.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+42
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "start",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "(.*?)"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "test",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "(.*?)"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
+35
@@ -0,0 +1,35 @@
|
|||||||
|
############################
|
||||||
|
# Docker build environment #
|
||||||
|
############################
|
||||||
|
|
||||||
|
FROM node:lts-bookworm-slim AS build
|
||||||
|
|
||||||
|
# Upgrade all packages and install dependencies
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get upgrade -y
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
python3 \
|
||||||
|
build-essential \
|
||||||
|
&& apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build Public Pool UI using NPM
|
||||||
|
RUN npm i && npm run build
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Docker final environment #
|
||||||
|
############################
|
||||||
|
|
||||||
|
FROM caddy:alpine AS final
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
COPY --from=build /build/dist/public-pool-ui .
|
||||||
|
COPY docker/Caddyfile.tpl /etc/Caddyfile.tpl
|
||||||
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
CMD ["/bin/sh", "/entrypoint.sh"]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# PublicPoolUi
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.3.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Requires [Public-Pool](https://github.com/benjamin-wilson/public-pool) to be running
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Install pm2 (https://pm2.keymetrics.io/)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pm2 serve --spa dist/public-pool-ui/ 3335 --name ui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker build -t public-pool-ui .
|
||||||
|
$ docker run --name public-pool-ui --rm -p 8080:80 public-pool-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
From Docker commands, website will be accessible on [http://localhost:8080](http://localhost:8080). By default Caddy server listen on port 80, but we bind it to port 8080 which allows you to launch image without root permissions.
|
||||||
|
|
||||||
|
Available variables:
|
||||||
|
* `DOMAIN`: website domain (default: `localhost`)
|
||||||
|
* `LOGLEVEL`: loglevel in stdout (default: `INFO`)
|
||||||
|
* `LOGFORMAT`: log format in stdout (default: `json`)
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"public-pool-ui": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/public-pool-ui",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"stylePreprocessorOptions": {
|
||||||
|
"includePaths": [
|
||||||
|
"src/styles"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/chart.js/dist/chart.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"electron": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.electron.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "3mb",
|
||||||
|
"maximumError": "3mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "3mb",
|
||||||
|
"maximumError": "3mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "3mb",
|
||||||
|
"maximumError": "3mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "public-pool-ui:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "public-pool-ui:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "public-pool-ui:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 986 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
:80 {
|
||||||
|
root * /var/www/html
|
||||||
|
file_server
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format %%LOGFORMAT%%
|
||||||
|
level %%LOGLEVEL%%
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ ! -e "/etc/Caddyfile" ]; then
|
||||||
|
sed -i "s#%%LOGLEVEL%%#${LOGLEVEL:-INFO}#g" /etc/Caddyfile.tpl
|
||||||
|
sed -i "s#%%LOGFORMAT%%#${LOGFORMAT:-json}#g" /etc/Caddyfile.tpl
|
||||||
|
mv /etc/Caddyfile.tpl /etc/Caddyfile
|
||||||
|
else
|
||||||
|
rm -f /etc/Caddyfile.tpl
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting UI on port 80"
|
||||||
|
echo "Logs output: ${LOGLEVEL:-INFO} (${LOGFORMAT:-json})"
|
||||||
|
|
||||||
|
exec caddy run --config /etc/Caddyfile
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 293 B |
-18
@@ -1,18 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" data-critters-container>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Public Pool</title>
|
|
||||||
<base href="/">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
||||||
<style>:root{--surface-a:#1f2d40;--surface-b:#17212f;--surface-c:rgba(255, 255, 255, 0.03);--surface-d:#304562;--surface-e:#1f2d40;--surface-f:#1f2d40;--text-color:rgba(255, 255, 255, 0.87);--text-color-secondary:rgba(255, 255, 255, 0.6);--primary-color:#64B5F6;--primary-color-text:#212529;--font-family:-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;--surface-0:#17212f;--surface-50:#2e3744;--surface-100:#454d59;--surface-200:#5d646d;--surface-300:#747a82;--surface-400:#8b9097;--surface-500:#a2a6ac;--surface-600:#b9bcc1;--surface-700:#d1d3d5;--surface-800:#e8e9ea;--surface-900:#ffffff;--gray-50:#e8e9ea;--gray-100:#d1d3d5;--gray-200:#b9bcc1;--gray-300:#a2a6ac;--gray-400:#8b9097;--gray-500:#747a82;--gray-600:#5d646d;--gray-700:#454d59;--gray-800:#2e3744;--gray-900:#17212f;--content-padding:1rem;--inline-spacing:0.5rem;--border-radius:3px;--surface-ground:#17212f;--surface-section:#17212f;--surface-card:#1f2d40;--surface-overlay:#1f2d40;--surface-border:#304562;--surface-hover:rgba(255,255,255,.03);--maskbg:rgba(0, 0, 0, 0.4);--focus-ring:0 0 0 1px #93cbf9;color-scheme:light dark}*{box-sizing:border-box}:root{--blue-50:#f4fafe;--blue-100:#cae6fc;--blue-200:#a0d2fa;--blue-300:#75bef8;--blue-400:#4baaf5;--blue-500:#2196f3;--blue-600:#1c80cf;--blue-700:#1769aa;--blue-800:#125386;--blue-900:#0d3c61;--green-50:#f6fbf6;--green-100:#d4ecd5;--green-200:#b2ddb4;--green-300:#90cd93;--green-400:#6ebe71;--green-500:#4caf50;--green-600:#419544;--green-700:#357b38;--green-800:#2a602c;--green-900:#1e4620;--yellow-50:#fffcf5;--yellow-100:#fef0cd;--yellow-200:#fde4a5;--yellow-300:#fdd87d;--yellow-400:#fccc55;--yellow-500:#fbc02d;--yellow-600:#d5a326;--yellow-700:#b08620;--yellow-800:#8a6a19;--yellow-900:#644d12;--cyan-50:#f2fcfd;--cyan-100:#c2eff5;--cyan-200:#91e2ed;--cyan-300:#61d5e4;--cyan-400:#30c9dc;--cyan-500:#00bcd4;--cyan-600:#00a0b4;--cyan-700:#008494;--cyan-800:#006775;--cyan-900:#004b55;--pink-50:#fef4f7;--pink-100:#fac9da;--pink-200:#f69ebc;--pink-300:#f1749e;--pink-400:#ed4981;--pink-500:#e91e63;--pink-600:#c61a54;--pink-700:#a31545;--pink-800:#801136;--pink-900:#5d0c28;--indigo-50:#f5f6fb;--indigo-100:#d1d5ed;--indigo-200:#acb4df;--indigo-300:#8893d1;--indigo-400:#6372c3;--indigo-500:#3f51b5;--indigo-600:#36459a;--indigo-700:#2c397f;--indigo-800:#232d64;--indigo-900:#192048;--teal-50:#f2faf9;--teal-100:#c2e6e2;--teal-200:#91d2cc;--teal-300:#61beb5;--teal-400:#30aa9f;--teal-500:#009688;--teal-600:#008074;--teal-700:#00695f;--teal-800:#00534b;--teal-900:#003c36;--orange-50:#fff8f2;--orange-100:#fde0c2;--orange-200:#fbc791;--orange-300:#f9ae61;--orange-400:#f79530;--orange-500:#f57c00;--orange-600:#d06900;--orange-700:#ac5700;--orange-800:#874400;--orange-900:#623200;--bluegray-50:#f7f9f9;--bluegray-100:#d9e0e3;--bluegray-200:#bbc7cd;--bluegray-300:#9caeb7;--bluegray-400:#7e96a1;--bluegray-500:#607d8b;--bluegray-600:#526a76;--bluegray-700:#435861;--bluegray-800:#35454c;--bluegray-900:#263238;--purple-50:#faf4fb;--purple-100:#e7cbec;--purple-200:#d4a2dd;--purple-300:#c279ce;--purple-400:#af50bf;--purple-500:#9c27b0;--purple-600:#852196;--purple-700:#6d1b7b;--purple-800:#561561;--purple-900:#3e1046;--red-50:#fff5f5;--red-100:#ffd1ce;--red-200:#ffada7;--red-300:#ff8980;--red-400:#ff6459;--red-500:#ff4032;--red-600:#d9362b;--red-700:#b32d23;--red-800:#8c231c;--red-900:#661a14;--primary-50:#f7fbff;--primary-100:#daedfd;--primary-200:#bcdffb;--primary-300:#9fd1f9;--primary-400:#81c3f8;--primary-500:#64b5f6;--primary-600:#559ad1;--primary-700:#467fac;--primary-800:#376487;--primary-900:#284862}</style><link id="theme-css" rel="stylesheet" type="text/css" href="assets/layout/styles/theme/vela-blue/theme.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="assets/layout/styles/theme/vela-blue/theme.css"></noscript>
|
|
||||||
<style>*{box-sizing:border-box}html{font-size:14px}body{font-family:var(--font-family);color:var(--text-color);background-color:var(--surface-ground);margin:0;padding:0;min-height:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}</style><link rel="stylesheet" href="styles.86ee2b39234242ec.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.86ee2b39234242ec.css"></noscript></head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<app-root></app-root>
|
|
||||||
<script src="runtime.69a46451081d025b.js" type="module"></script><script src="polyfills.315fefdcc45f5d18.js" type="module"></script><script src="scripts.ee09a5738baa913c.js" defer></script><script src="main.27fb8544590bf3bd.js" type="module"></script></body>
|
|
||||||
|
|
||||||
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
Generated
+14576
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "public-pool-ui",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve --proxy-config proxy.config.local.json ",
|
||||||
|
"start:prod": "ng serve --configuration=production",
|
||||||
|
"build": "ng build --configuration=production && gzipper compress --gzip --brotli ./dist/public-pool-ui/",
|
||||||
|
"build:electron": "ng build --configuration=electron --base-href ./",
|
||||||
|
"build:github": "ng build --configuration=production && echo \"web.public-pool.io\" > dist/public-pool-ui/CNAME",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test",
|
||||||
|
"bundle-report": "ng build --configuration=production --source-map=true --named-chunks=true --stats-json && webpack-bundle-analyzer dist/public-pool-ui/stats.json"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "18.1.0",
|
||||||
|
"@angular/common": "18.1.0",
|
||||||
|
"@angular/compiler": "18.1.0",
|
||||||
|
"@angular/core": "18.1.0",
|
||||||
|
"@angular/forms": "18.1.0",
|
||||||
|
"@angular/platform-browser": "18.1.0",
|
||||||
|
"@angular/platform-browser-dynamic": "18.1.0",
|
||||||
|
"@angular/router": "18.1.0",
|
||||||
|
"bitcoin-address-validation": "^2.2.3",
|
||||||
|
"chart.js": "^4.4.3",
|
||||||
|
"chartjs-adapter-moment": "^1.0.1",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"ng-particles": "^3.12.0",
|
||||||
|
"ngx-device-detector": "8.0.0",
|
||||||
|
"primeflex": "^3.3.1",
|
||||||
|
"primeicons": "^6.0.1",
|
||||||
|
"primeng": "17.18.3",
|
||||||
|
"rxjs": "~7.8.1",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"tsparticles": "^2.10.1",
|
||||||
|
"tsparticles-engine": "^2.10.1",
|
||||||
|
"zone.js": "~0.14.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "18.1.0",
|
||||||
|
"@angular/cli": "18.1.0",
|
||||||
|
"@angular/compiler-cli": "^18.1.0",
|
||||||
|
"@types/jasmine": "~4.0.0",
|
||||||
|
"gzipper": "^7.2.0",
|
||||||
|
"jasmine-core": "~4.2.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.0.0",
|
||||||
|
"typescript": "~5.5.3",
|
||||||
|
"webpack-bundle-analyzer": "^4.9.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 285 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "https://localhost:3334",
|
||||||
|
"secure": true,
|
||||||
|
"changeOrigin": false,
|
||||||
|
"logLevel": "debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "https://public-pool.io:40557",
|
||||||
|
"secure": true,
|
||||||
|
"changeOrigin": true,
|
||||||
|
"logLevel": "debug",
|
||||||
|
"pathRewrite": {
|
||||||
|
"^/api": "/api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
(()=>{"use strict";var e,_={},p={};function a(e){var n=p[e];if(void 0!==n)return n.exports;var r=p[e]={id:e,loaded:!1,exports:{}};return _[e].call(r.exports,r,r.exports,a),r.loaded=!0,r.exports}a.m=_,e=[],a.O=(n,r,c,f)=>{if(!r){var o=1/0;for(l=0;l<e.length;l++){for(var[r,c,f]=e[l],s=!0,t=0;t<r.length;t++)(!1&f||o>=f)&&Object.keys(a.O).every(h=>a.O[h](r[t]))?r.splice(t--,1):(s=!1,f<o&&(o=f));if(s){e.splice(l--,1);var u=c();void 0!==u&&(n=u)}}return n}f=f||0;for(var l=e.length;l>0&&e[l-1][2]>f;l--)e[l]=e[l-1];e[l]=[r,c,f]},a.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return a.d(n,{a:n}),n},a.d=(e,n)=>{for(var r in n)a.o(n,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},a.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e={666:0};a.O.j=c=>0===e[c];var n=(c,f)=>{var t,u,[l,o,s]=f,i=0;if(l.some(d=>0!==e[d])){for(t in o)a.o(o,t)&&(a.m[t]=o[t]);if(s)var v=s(a)}for(c&&c(f);i<l.length;i++)a.o(e,u=l[i])&&e[u]&&e[u][0](),e[u]=0;return a.O(v)},r=self.webpackChunkpublic_pool_ui=self.webpackChunkpublic_pool_ui||[];r.forEach(n.bind(null,0)),r.push=n.bind(null,r.push.bind(r))})()})();
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,57 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { DashboardComponent } from './components/dashboard/dashboard.component';
|
||||||
|
import { SettingsComponent } from './components/settings/settings.component';
|
||||||
|
import { SplashComponent } from './components/splash/splash.component';
|
||||||
|
import { WorkerGroupComponent } from './components/worker-group/worker-group.component';
|
||||||
|
import { WorkerComponent } from './components/worker/worker.component';
|
||||||
|
import { AppLayoutComponent } from './layout/app.layout.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: SplashComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'app',
|
||||||
|
component: AppLayoutComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':address',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
component: SettingsComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: DashboardComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':workerName',
|
||||||
|
children: [
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: WorkerGroupComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':workerId',
|
||||||
|
component: WorkerComponent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<app-background-particles *ngIf="particles$ | async"></app-background-particles>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({
|
||||||
|
imports: [RouterTestingModule],
|
||||||
|
declarations: [AppComponent]
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should have as title 'public-pool-ui'`, () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app.title).toEqual('public-pool-ui');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('.content span')?.textContent).toContain('public-pool-ui app is running!');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { LocalStorageService } from './services/local-storage.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'public-pool-ui';
|
||||||
|
|
||||||
|
public particles$: Observable<boolean>;
|
||||||
|
constructor(private localService: LocalStorageService) {
|
||||||
|
this.particles$ = this.localService.particles$;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'chartjs-adapter-moment';
|
||||||
|
|
||||||
|
import { CommonModule, HashLocationStrategy, LocationStrategy } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { NgParticlesModule } from 'ng-particles';
|
||||||
|
|
||||||
|
import { PrimeNGModule } from '../prime-ng.module';
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { BackgroundParticlesComponent } from './components/background-particles/background-particles.component';
|
||||||
|
import { DashboardComponent } from './components/dashboard/dashboard.component';
|
||||||
|
import { SettingsComponent } from './components/settings/settings.component';
|
||||||
|
import { SplashComponent } from './components/splash/splash.component';
|
||||||
|
import { UserAgentLinkComponent } from './components/user-agent-link/user-agent-link.component';
|
||||||
|
import { WorkerGroupComponent } from './components/worker-group/worker-group.component';
|
||||||
|
import { WorkerComponent } from './components/worker/worker.component';
|
||||||
|
import { AppLayoutModule } from './layout/app.layout.module';
|
||||||
|
import { DateAgoPipe } from './pipes/date-ago.pipe';
|
||||||
|
import { HashSuffixPipe } from './pipes/hash-suffix.pipe';
|
||||||
|
import { NumberSuffixPipe } from './pipes/number-suffix.pipe';
|
||||||
|
import { AverageTimeToBlockPipe } from './pipes/average-time-to-block.pipe';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
SplashComponent,
|
||||||
|
DashboardComponent,
|
||||||
|
WorkerComponent,
|
||||||
|
NumberSuffixPipe,
|
||||||
|
DateAgoPipe,
|
||||||
|
WorkerGroupComponent,
|
||||||
|
BackgroundParticlesComponent,
|
||||||
|
HashSuffixPipe,
|
||||||
|
SettingsComponent,
|
||||||
|
UserAgentLinkComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
PrimeNGModule,
|
||||||
|
AppLayoutModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgParticlesModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: LocationStrategy, useClass: HashLocationStrategy },
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<ng-particles id="particles" [options]="particleOptions" [style]="particleStyles" [particlesInit]="particlesInit"
|
||||||
|
(particlesLoaded)="particlesLoaded($event)"></ng-particles>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BackgroundParticlesComponent } from './background-particles.component';
|
||||||
|
|
||||||
|
describe('BackgroundParticlesComponent', () => {
|
||||||
|
let component: BackgroundParticlesComponent;
|
||||||
|
let fixture: ComponentFixture<BackgroundParticlesComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [BackgroundParticlesComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(BackgroundParticlesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { IParticlesProps } from 'ng-particles';
|
||||||
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
|
import { loadFull } from 'tsparticles';
|
||||||
|
import { Container, Engine } from 'tsparticles-engine';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-background-particles',
|
||||||
|
templateUrl: './background-particles.component.html',
|
||||||
|
styleUrls: ['./background-particles.component.scss']
|
||||||
|
})
|
||||||
|
export class BackgroundParticlesComponent implements OnInit {
|
||||||
|
public particleStyles!: any;
|
||||||
|
public particleOptions!: IParticlesProps;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private deviceService: DeviceDetectorService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.particleStyles = {
|
||||||
|
position: 'fixed',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
'z-index': -1,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const color: string = '#96bcf9';
|
||||||
|
const lineColor: string = '#afccfa';
|
||||||
|
|
||||||
|
this.particleOptions = {
|
||||||
|
fpsLimit: 30,
|
||||||
|
detectRetina: true,
|
||||||
|
background: {
|
||||||
|
position: "50% 50%",
|
||||||
|
repeat: "no-repeat",
|
||||||
|
size: "cover"
|
||||||
|
},
|
||||||
|
fullScreen: {
|
||||||
|
zIndex: 1
|
||||||
|
},
|
||||||
|
particles: {
|
||||||
|
number: {
|
||||||
|
value: 80,
|
||||||
|
density: {
|
||||||
|
enable: true,
|
||||||
|
value_area: 800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
value: color
|
||||||
|
},
|
||||||
|
opacity: { value: 0.6 },
|
||||||
|
links: {
|
||||||
|
distance: 150,
|
||||||
|
enable: true,
|
||||||
|
color: {
|
||||||
|
value: lineColor
|
||||||
|
},
|
||||||
|
opacity: 0.6
|
||||||
|
},
|
||||||
|
move: {
|
||||||
|
enable: true,
|
||||||
|
speed: 0.4
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
random: true,
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
particlesLoaded(container: Container): void {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async particlesInit(engine: Engine): Promise<void> {
|
||||||
|
|
||||||
|
|
||||||
|
// Starting from 1.19.0 you can add custom presets or shape here, using the current tsParticles instance (main)
|
||||||
|
// this loads the tsparticles package bundle, it's the easiest method for getting everything ready
|
||||||
|
// starting from v2 you can add only the features you need reducing the bundle size
|
||||||
|
await loadFull(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<ng-container>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Your Best Difficulty <i
|
||||||
|
class="pi pi-question-circle ml-2"
|
||||||
|
pTooltip="If you find a solution with a difficulty higher than the network difficulty, you've found a block."></i></span>
|
||||||
|
<div class="text-900 font-medium text-xl">
|
||||||
|
|
||||||
|
<ng-container
|
||||||
|
*ngIf="clientInfo$ | async as clientInfo; else loading">{{clientInfo.bestDifficulty |
|
||||||
|
numberSuffix}}</ng-container>
|
||||||
|
<ng-template #loading>
|
||||||
|
<p-skeleton></p-skeleton>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center justify-content-center bg-orange-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-star text-orange-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-green-500 font-medium">
|
||||||
|
|
||||||
|
<ng-container *ngIf="clientInfo$ | async as clientInfo; else loading">{{clientInfo.bestDifficulty |
|
||||||
|
number}}</ng-container>
|
||||||
|
<ng-template #loading>
|
||||||
|
<p-skeleton></p-skeleton>
|
||||||
|
</ng-template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container>
|
||||||
|
<div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Network Difficulty</span>
|
||||||
|
<div class="text-900 font-medium text-xl">
|
||||||
|
<ng-container
|
||||||
|
*ngIf="networkInfo$ | async as networkInfo; else loading">{{networkInfo.difficulty |
|
||||||
|
numberSuffix}}</ng-container>
|
||||||
|
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- <div class="flex align-items-center justify-content-center bg-orange-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-star text-orange-500 text-xl"></i>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<span class="text-green-500 font-medium">
|
||||||
|
<ng-container *ngIf="networkInfo$ | async as networkInfo; else loading">
|
||||||
|
{{networkInfo.difficulty
|
||||||
|
| number}}</ng-container>
|
||||||
|
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Network Hash Rate </span>
|
||||||
|
<div class="text-900 font-medium text-xl">
|
||||||
|
<ng-container *ngIf="networkInfo$ | async as networkInfo; else loading">
|
||||||
|
{{networkInfo.networkhashps | hashSuffix}}</ng-container>
|
||||||
|
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="flex align-items-center justify-content-center bg-orange-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-star text-orange-500 text-xl"></i>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<span class="text-green-500 font-medium"> </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Block Height</span>
|
||||||
|
<div class="text-900 font-medium text-xl">
|
||||||
|
<ng-container *ngIf="networkInfo$ | async as networkInfo; else loading">
|
||||||
|
{{networkInfo.blocks}}</ng-container>
|
||||||
|
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="flex align-items-center justify-content-center bg-orange-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-star text-orange-500 text-xl"></i>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<span class="text-green-500 font-medium">Weight:
|
||||||
|
<ng-container *ngIf="networkInfo$ | async as networkInfo; else loading">
|
||||||
|
{{networkInfo.currentblockweight | number}}</ng-container>
|
||||||
|
<ng-template #loading><p-skeleton></p-skeleton></ng-template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<ng-container *ngIf="clientInfo$ | async as clientInfo; else loadingTable">
|
||||||
|
<p-table #dataTable [rowHover]="true" groupRowsBy="name" dataKey="name" rowGroupMode="subheader"
|
||||||
|
[value]="clientInfo.workers" [expandedRowKeys]="expandedRows$ | async"
|
||||||
|
[tableStyle]="{ 'min-width': '50rem' }">
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Session ID</th>
|
||||||
|
<th>Hash Rate</th>
|
||||||
|
<th>Session Best Difficulty</th>
|
||||||
|
<th>Uptime</th>
|
||||||
|
<th>Last Seen</th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template pTemplate="groupheader" let-worker let-rowIndex="rowIndex" let-expanded="expanded">
|
||||||
|
<tr [routerLink]="[worker.name]">
|
||||||
|
<td>
|
||||||
|
<button (click)="$event.stopImmediatePropagation()" type="button" pButton pRipple
|
||||||
|
[pRowToggler]="worker" class="p-button-text p-button-rounded p-button-plain mr-2"
|
||||||
|
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
|
||||||
|
|
||||||
|
<span class="worker-name font-bold ml-2">{{worker.name}} </span>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{getSessionCount(worker.name, clientInfo.workers)}} Sessions
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{getTotalHashRate(worker.name, clientInfo.workers) | hashSuffix}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{getBestDifficulty(worker.name, clientInfo.workers) | numberSuffix}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template pTemplate="rowexpansion" let-worker>
|
||||||
|
<tr [routerLink]="[worker.name, worker.sessionId]">
|
||||||
|
<td></td>
|
||||||
|
<td>{{worker.sessionId}}</td>
|
||||||
|
<td>{{worker.hashRate | hashSuffix}}</td>
|
||||||
|
<td>{{worker.bestDifficulty | numberSuffix}}</td>
|
||||||
|
<td>{{worker.startTime | dateAgo}}</td>
|
||||||
|
<td>{{worker.lastSeen | dateAgo}}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
</p-table>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #loadingTable>
|
||||||
|
<p-table #dataTable [rowHover]="true" groupRowsBy="loading" dataKey="loading" rowGroupMode="subheader"
|
||||||
|
[value]="[{loading: 'loading'}]" [expandedRowKeys]="{loading: true}"
|
||||||
|
[tableStyle]="{ 'min-width': '50rem' }">
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Session ID</th>
|
||||||
|
<th>Hash Rate</th>
|
||||||
|
<th>Session Best Difficulty</th>
|
||||||
|
<th>Uptime</th>
|
||||||
|
<th>Last Seen</th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template pTemplate="groupheader" let-worker let-rowIndex="rowIndex" let-expanded="expanded">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<button (click)="$event.stopImmediatePropagation()" type="button" pButton pRipple
|
||||||
|
[pRowToggler]="worker" class="p-button-text p-button-rounded p-button-plain mr-2"
|
||||||
|
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
|
||||||
|
|
||||||
|
<span class="worker-name font-bold ml-2">{{worker.name}} </span>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p-skeleton></p-skeleton>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p-skeleton></p-skeleton>
|
||||||
|
<td>
|
||||||
|
<p-skeleton></p-skeleton>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template pTemplate="rowexpansion" let-worker>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
</p-table>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card chart">
|
||||||
|
<ng-container *ngIf="chartData$ | async as chartData; else loadingChart">
|
||||||
|
<p-chart [data]="chartData" [options]="chartOptions"></p-chart>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #loadingChart>
|
||||||
|
<p-skeleton width="100%" height="40vh"></p-skeleton>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.worker-name {
|
||||||
|
line-height: 42px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DashboardComponent } from './dashboard.component';
|
||||||
|
|
||||||
|
describe('DashboardComponent', () => {
|
||||||
|
let component: DashboardComponent;
|
||||||
|
let fixture: ComponentFixture<DashboardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ DashboardComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DashboardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { AfterViewInit, Component, ViewChild } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Table } from 'primeng/table';
|
||||||
|
import { combineLatest, map, Observable, shareReplay } from 'rxjs';
|
||||||
|
|
||||||
|
import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe';
|
||||||
|
import { AppService } from '../../services/app.service';
|
||||||
|
import { ClientService } from '../../services/client.service';
|
||||||
|
import { AverageTimeToBlockPipe } from 'src/app/pipes/average-time-to-block.pipe';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
templateUrl: './dashboard.component.html',
|
||||||
|
styleUrls: ['./dashboard.component.scss']
|
||||||
|
})
|
||||||
|
export class DashboardComponent implements AfterViewInit {
|
||||||
|
|
||||||
|
public address: string;
|
||||||
|
|
||||||
|
public clientInfo$: Observable<any>;
|
||||||
|
public chartData$: Observable<any>;
|
||||||
|
|
||||||
|
public chartOptions: any;
|
||||||
|
|
||||||
|
public networkInfo$: Observable<any>;
|
||||||
|
private networkInfo:any;
|
||||||
|
|
||||||
|
@ViewChild('dataTable') dataTable!: Table;
|
||||||
|
|
||||||
|
public expandedRows$: Observable<any>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private clientService: ClientService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private appService: AppService
|
||||||
|
) {
|
||||||
|
|
||||||
|
this.networkInfo$ = this.appService.getNetworkInfo().pipe(
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.address = this.route.snapshot.params['address'];
|
||||||
|
this.clientInfo$ = this.clientService.getClientInfo(this.address).pipe(
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.expandedRows$ = this.clientInfo$.pipe(map((info: any) => {
|
||||||
|
|
||||||
|
return info.workers.reduce((pre: any, cur: any) => { pre[cur.name] = true; return pre; }, {});
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
const documentStyle = getComputedStyle(document.documentElement);
|
||||||
|
const textColor = documentStyle.getPropertyValue('--text-color');
|
||||||
|
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
|
||||||
|
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
|
||||||
|
|
||||||
|
|
||||||
|
this.chartData$ = combineLatest([this.clientService.getClientInfoChart(this.address), this.networkInfo$]).pipe(
|
||||||
|
map(([chartData, networkInfo]) => {
|
||||||
|
|
||||||
|
this.networkInfo = networkInfo;
|
||||||
|
const GROUP_SIZE = 12; //6 = 1 hour
|
||||||
|
|
||||||
|
|
||||||
|
let hourlyData = [];
|
||||||
|
|
||||||
|
for (let i = GROUP_SIZE; i < chartData.length; i += GROUP_SIZE) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = GROUP_SIZE - 1; j >= 0; j--) {
|
||||||
|
sum += parseInt(chartData[i - j].data);
|
||||||
|
}
|
||||||
|
sum = sum / GROUP_SIZE;
|
||||||
|
hourlyData.push({ y: sum, x: chartData[i].label });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const data = chartData.map((d: any) => { return { y: d.data, x: d.label } });
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: chartData.map((d: any) => d.label),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
label: '2 Hour',
|
||||||
|
data: hourlyData,
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: documentStyle.getPropertyValue('--yellow-600'),
|
||||||
|
borderColor: documentStyle.getPropertyValue('--yellow-600'),
|
||||||
|
tension: .4,
|
||||||
|
pointRadius: 1,
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
label: '10 Minute',
|
||||||
|
data: data,
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: documentStyle.getPropertyValue('--primary-color'),
|
||||||
|
borderColor: documentStyle.getPropertyValue('--primary-color'),
|
||||||
|
tension: .4,
|
||||||
|
pointRadius: 1,
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.chartOptions = {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
unit: 'hour', // Set the unit to 'minute'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: textColorSecondary
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: surfaceBorder,
|
||||||
|
drawBorder: false,
|
||||||
|
display: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: textColorSecondary,
|
||||||
|
callback: (value: number) => {
|
||||||
|
return HashSuffixPipe.transform(value) + " - " + AverageTimeToBlockPipe.transform(value, this.networkInfo.difficulty);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: surfaceBorder,
|
||||||
|
drawBorder: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSessionCount(name: string, workers: any[]) {
|
||||||
|
const workersByName = workers.filter(w => w.name == name);
|
||||||
|
return workersByName.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTotalHashRate(name: string, workers: any[]) {
|
||||||
|
const workersByName = workers.filter(w => w.name == name);
|
||||||
|
const sum = workersByName.reduce((pre, cur, idx, arr) => {
|
||||||
|
return pre += Math.floor(cur.hashRate);
|
||||||
|
}, 0);
|
||||||
|
return Math.floor(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBestDifficulty(name: string, workers: any[]) {
|
||||||
|
const workersByName = workers.filter(w => w.name == name);
|
||||||
|
const best = workersByName.reduce((pre, cur, idx, arr) => {
|
||||||
|
if (cur.bestDifficulty > pre) {
|
||||||
|
return cur.bestDifficulty;
|
||||||
|
}
|
||||||
|
return pre;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTotalUptime(name: string, workers: any[]) {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const workersByName = workers.filter(w => w.name == name);
|
||||||
|
const sum = workersByName.reduce((pre, cur, idx, arr) => {
|
||||||
|
return pre += now - new Date(cur.startTime).getTime();
|
||||||
|
}, 0);
|
||||||
|
return new Date(now - sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div class="card">
|
||||||
|
<h5>Background Particles </h5>
|
||||||
|
<p-inputSwitch [(ngModel)]="value" (ngModelChange)="particlesChanged($event)" optionLabel="label"
|
||||||
|
optionValue="value"></p-inputSwitch>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SettingsComponent } from './settings.component';
|
||||||
|
|
||||||
|
describe('SettingsComponent', () => {
|
||||||
|
let component: SettingsComponent;
|
||||||
|
let fixture: ComponentFixture<SettingsComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [SettingsComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(SettingsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
import { LocalStorageService } from '../../services/local-storage.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-settings',
|
||||||
|
templateUrl: './settings.component.html',
|
||||||
|
styleUrls: ['./settings.component.scss']
|
||||||
|
})
|
||||||
|
export class SettingsComponent {
|
||||||
|
|
||||||
|
public stateOptions: any[] = [{ label: 'On', value: true }, { label: 'Off', value: false },];
|
||||||
|
|
||||||
|
public value: boolean = true;
|
||||||
|
|
||||||
|
constructor(private localStorageService: LocalStorageService) {
|
||||||
|
this.value = this.localStorageService.getParticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public particlesChanged(newVal: boolean) {
|
||||||
|
this.localStorageService.setParticles(newVal);
|
||||||
|
this.value = newVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
<div class="py-4 px-1 md:px4 lg:px-8 mx-0 lg:mx-8 main">
|
||||||
|
<div class="grid justify-content-center">
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img id="logo" style="height: 200px;" src="assets/layout/images/logo.svg" alt="logo">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 text-center title">
|
||||||
|
<h1 class="text-900 font-normal">Public Pool</h1>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="ml-4 mr-4" href="https://twitter.com/Public_Pool_BTC" target="_blank">
|
||||||
|
<i class="pi pi-twitter" style="font-size: 30pt; color: white;"></i>
|
||||||
|
</a>
|
||||||
|
<a class="ml-4 mr-4" href="https://discord.gg/pF9smpe3yE" target="_blank">
|
||||||
|
<i class="pi pi-discord" style="font-size: 30pt; color: white;"></i>
|
||||||
|
</a>
|
||||||
|
<a class="ml-4 mr-4" href="https://github.com/benjamin-wilson/public-pool" target="_blank">
|
||||||
|
<i class="pi pi-github" style="font-size: 30pt; color: white;"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<code>Fully Open Source Solo Bitcoin Mining Pool</code>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<div class="info mb-2 p-4 border-dashed border-1 border-round">
|
||||||
|
<code>stratum+tcp://{{stratumURL}}</code>
|
||||||
|
<br>
|
||||||
|
<code>username: <your BTC address>.<worker name>, password: x</code>
|
||||||
|
</div>
|
||||||
|
<span><b>NO FEES</b></span>
|
||||||
|
<br>
|
||||||
|
<span>No second best.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<h4>Login</h4>
|
||||||
|
<div class="field text-center">
|
||||||
|
|
||||||
|
<input [formControl]="address" placeholder="Address (bc1...)" pInputText id="address" type="text"
|
||||||
|
class="p-inputtext p-component p-element">
|
||||||
|
<button [disabled]="address.invalid" class="ml-3 mt-3" pButton label="My Workers"
|
||||||
|
[routerLink]="['app',address.value]"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card chart">
|
||||||
|
|
||||||
|
<div class="text-center mb-2">Uptime: {{uptime$ | async | dateAgo}}</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="chartData$ | async as chartData; else loadingChart">
|
||||||
|
|
||||||
|
<p-chart [responsive]="true" type="line" [data]="chartData" [options]="chartOptions"></p-chart>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #loadingChart>
|
||||||
|
<p-skeleton width="100%" height="40vh"></p-skeleton>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<h4 style="text-align: center;">Online Devices</h4>
|
||||||
|
<ng-container *ngIf="userAgents$ | async as userAgents">
|
||||||
|
<p-table [value]="userAgents">
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th>Device</th>
|
||||||
|
<th>Currently Working</th>
|
||||||
|
<th>Total Hash Rate</th>
|
||||||
|
<th>Best Difficulty <i class="pi pi-question-circle ml-2"
|
||||||
|
pTooltip="If you find a solution with a difficulty higher than the network difficulty, you've found a block."></i>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template pTemplate="body" let-userAgent>
|
||||||
|
<tr></tr>
|
||||||
|
<tr>
|
||||||
|
<td><app-user-agent-link [userAgent]="userAgent.userAgent"></app-user-agent-link></td>
|
||||||
|
<td>{{ userAgent.count }}</td>
|
||||||
|
<td>{{ userAgent.totalHashRate | hashSuffix }}</td>
|
||||||
|
<td>{{ userAgent.bestDifficulty | numberSuffix}}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
||||||
|
<div class="text-right mt-2">
|
||||||
|
<i class="pi pi-star-fill"></i> = Open Source Software & Hardware
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #loadingTable>
|
||||||
|
<p-table [value]="[{},{},{}]">
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th>Device</th>
|
||||||
|
<th>Currently Working</th>
|
||||||
|
<th>Total Hash Rate</th>
|
||||||
|
<th>Best Difficulty <i class="pi pi-question-circle ml-2"
|
||||||
|
pTooltip="If you find a solution with a difficulty higher than the network difficulty, you've found a block."></i>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template pTemplate="body" let-row>
|
||||||
|
<tr></tr>
|
||||||
|
<tr>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-12" *ngIf="highScores$ | async as highScores">
|
||||||
|
<div class="card">
|
||||||
|
<h4 style="text-align: center;">High Scores</h4>
|
||||||
|
<ng-container>
|
||||||
|
<p-table [value]="highScores">
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th>Rank</th>
|
||||||
|
<th>Difficulty</th>
|
||||||
|
<th>Device</th>
|
||||||
|
<th>When</th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template pTemplate="body" let-highScore let-i="rowIndex">
|
||||||
|
<tr></tr>
|
||||||
|
<tr>
|
||||||
|
<td>#{{i+1}}</td>
|
||||||
|
<td>{{ highScore.bestDifficulty | numberSuffix }}</td>
|
||||||
|
<td><app-user-agent-link
|
||||||
|
[userAgent]="highScore.bestDifficultyUserAgent ? highScore.bestDifficultyUserAgent : 'unknown'"></app-user-agent-link>
|
||||||
|
</td>
|
||||||
|
<td>{{ highScore.updatedAt | dateAgo}} ago</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
||||||
|
<div class="text-right mt-2">
|
||||||
|
<i class="pi pi-star-fill"></i> = Open Source Software & Hardware
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #loadingTable>
|
||||||
|
<p-table [value]="[{},{},{}]">
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th>Device</th>
|
||||||
|
<th>Currently Working</th>
|
||||||
|
<th>Total Hash Rate</th>
|
||||||
|
<th>Best Difficulty <i class="pi pi-question-circle ml-2"
|
||||||
|
pTooltip="If you find a solution with a difficulty higher than the network difficulty, you've found a block."></i>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template pTemplate="body" let-row>
|
||||||
|
<tr></tr>
|
||||||
|
<tr>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
<td><p-skeleton></p-skeleton></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-12" *ngIf="blockData$ | async as blockData">
|
||||||
|
<div class="card">
|
||||||
|
<h4 style="text-align: center;"> Found Blocks</h4>
|
||||||
|
<p-table [value]="blockData">
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th>Height</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>Worker</th>
|
||||||
|
<th>Session</th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template pTemplate="body" let-block>
|
||||||
|
<tr>
|
||||||
|
<td>{{ block.height }}</td>
|
||||||
|
<td>{{ block.minerAddress }}</td>
|
||||||
|
<td>{{ block.worker }}</td>
|
||||||
|
<td>{{ block.sessionId }}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="col-12 md:col-2">
|
||||||
|
<h3>Donate</h3>
|
||||||
|
<p>Like the project? Consider a donation. </p>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-4 text-center">
|
||||||
|
<img style="height: 200px;" src="assets/layout/images/qr-code-ln.svg"
|
||||||
|
alt="bc1q99n3pu025yyu0jlywpmwzalyhm36tg5u37w20d">
|
||||||
|
<div>public_pool@strike.me</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-6 text-center">
|
||||||
|
<img style="height: 200px;" src="assets/layout/images/qr-code-onchain.svg"
|
||||||
|
alt="bc1q99n3pu025yyu0jlywpmwzalyhm36tg5u37w20d">
|
||||||
|
<div>bc1q99n3pu025yyu0jlywpmwzalyhm36tg5u37w20d</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#address {
|
||||||
|
min-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -100px;
|
||||||
|
height: 200px;
|
||||||
|
width: 200px;
|
||||||
|
position: absolute;
|
||||||
|
top: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: x-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info code {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop styles */
|
||||||
|
@media screen and (min-width: 769px) {
|
||||||
|
.info {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info code {
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet and mobile styles */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.info {
|
||||||
|
font-size: large;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Very small screens */
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.info {
|
||||||
|
font-size: medium;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 1024px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-star-fill {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SplashComponent } from './splash.component';
|
||||||
|
|
||||||
|
describe('SplashComponent', () => {
|
||||||
|
let component: SplashComponent;
|
||||||
|
let fixture: ComponentFixture<SplashComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ SplashComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SplashComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { combineLatest, map, Observable, shareReplay } from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe';
|
||||||
|
import { AppService } from '../../services/app.service';
|
||||||
|
import { bitcoinAddressValidator } from '../../validators/bitcoin-address.validator';
|
||||||
|
import { AverageTimeToBlockPipe } from 'src/app/pipes/average-time-to-block.pipe';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-splash',
|
||||||
|
templateUrl: './splash.component.html',
|
||||||
|
styleUrls: ['./splash.component.scss']
|
||||||
|
})
|
||||||
|
export class SplashComponent {
|
||||||
|
|
||||||
|
public address: FormControl;
|
||||||
|
|
||||||
|
public chartData$: Observable<any>;
|
||||||
|
public blockData$: Observable<any>;
|
||||||
|
public userAgents$: Observable<any>;
|
||||||
|
public highScores$: Observable<any>;
|
||||||
|
public uptime$: Observable<string>;
|
||||||
|
|
||||||
|
public chartOptions: any;
|
||||||
|
|
||||||
|
public stratumURL = '';
|
||||||
|
|
||||||
|
private info$: Observable<any>;
|
||||||
|
|
||||||
|
private networkInfo:any;
|
||||||
|
|
||||||
|
constructor(private appService: AppService, private cdr: ChangeDetectorRef) {
|
||||||
|
|
||||||
|
this.info$ = this.appService.getInfo().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
|
|
||||||
|
if (environment.STRATUM_URL.length > 1) {
|
||||||
|
this.stratumURL = environment.STRATUM_URL;
|
||||||
|
} else {
|
||||||
|
this.stratumURL = window.location.hostname + ':3333';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blockData$ = this.info$.pipe(map(info => info.blockData));
|
||||||
|
this.userAgents$ = this.info$.pipe(map(info => info.userAgents));
|
||||||
|
this.highScores$ = this.info$.pipe(map(info => info.highScores));
|
||||||
|
this.uptime$ = this.info$.pipe(map(info => info.uptime))
|
||||||
|
|
||||||
|
this.chartData$ = combineLatest([this.appService.getInfoChart(), this.appService.getNetworkInfo()]).pipe(
|
||||||
|
map(([chartData, networkInfo]) => {
|
||||||
|
this.networkInfo = networkInfo;
|
||||||
|
return {
|
||||||
|
|
||||||
|
labels: chartData.map((d: any) => d.label),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Public-Pool Hashrate',
|
||||||
|
data: chartData.map((d: any) => d.data),
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: documentStyle.getPropertyValue('--primary-color'),
|
||||||
|
borderColor: documentStyle.getPropertyValue('--primary-color'),
|
||||||
|
tension: .4,
|
||||||
|
pointRadius: 1,
|
||||||
|
borderWidth: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.address = new FormControl(null, bitcoinAddressValidator());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const documentStyle = getComputedStyle(document.documentElement);
|
||||||
|
const textColor = documentStyle.getPropertyValue('--text-color');
|
||||||
|
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
|
||||||
|
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
|
||||||
|
|
||||||
|
|
||||||
|
this.chartOptions = {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
unit: 'day', // Set the unit to 'minute'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: textColorSecondary
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: surfaceBorder,
|
||||||
|
drawBorder: false,
|
||||||
|
display: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: textColorSecondary,
|
||||||
|
callback: (value: number) => {
|
||||||
|
return HashSuffixPipe.transform(value) + " - " + AverageTimeToBlockPipe.transform(value, this.networkInfo.difficulty);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: surfaceBorder,
|
||||||
|
drawBorder: false
|
||||||
|
},
|
||||||
|
type: 'logarithmic',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<ng-container [ngSwitch]="userAgent">
|
||||||
|
<ng-container *ngSwitchCase="'NerdMiner'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/BitMaker-hub/NerdMiner_v2" target="_blank">Nerd Miner
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'NerdNOS'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/WantClue/NerdMiner_v2/tree/nerdnos" target="_blank">NerdNOS
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'NerdAxe'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/BitMaker-hub/NerdAxeUltra" target="_blank">NerdAxe
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'NerdAxeGamma'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/BitMaker-hub/NerdAxe" target="_blank">NerdAxeGamma
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'NerdOCTAXE'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/Patsch91/NerdOCTAXE-Gamma" target="_blank">NerdOCTAXE
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'NerdEKO'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/phil31/NerdEKO-Gamma" target="_blank">NerdEKO
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'PiAxe'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/shufps/piaxe" target="_blank">PiAxe
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'QAxe'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/shufps/qaxe" target="_blank">QAxe
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'QAxe+'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/shufps/qaxe" target="_blank">QAxe+
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'NerdQAxe+'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/shufps/qaxe" target="_blank">NerdQAxe+
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'NerdQAxe++'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/shufps/qaxe" target="_blank">NerdQAxe++
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'0xAxe'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/shufps/0xaxe" target="_blank">0xAxe
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'bitaxe'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/skot/bitaxe/" target="_blank">Bitaxe
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'bitaxeHex'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/skot/bitaxeHex/" target="_blank">Bitaxe Hex
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'cpuminer'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/pooler/cpuminer" target="_blank">cpuminer
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'cgminer'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://cgminer.info/" target="_blank">cgminer
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'Braiins OS'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://braiins.com/os/plus" target="_blank">Braiins OS
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'termux-miner'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/wong-fi-hung/termux-miner"
|
||||||
|
target="_blank">termux-miner
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'LeafMiner'">
|
||||||
|
<a (click)="cancelClick($event)" href="https://github.com/matteocrippa/leafminer" target="_blank">LeafMiner
|
||||||
|
<i class="pi pi-star-fill"></i>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchDefault>{{userAgent}}</ng-container>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.pi-external-link {
|
||||||
|
|
||||||
|
margin-left: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-star-fill {
|
||||||
|
margin-left: 25px;
|
||||||
|
color: gold;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UserAgentLinkComponent } from './user-agent-link.component';
|
||||||
|
|
||||||
|
describe('UserAgentLinkComponent', () => {
|
||||||
|
let component: UserAgentLinkComponent;
|
||||||
|
let fixture: ComponentFixture<UserAgentLinkComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [UserAgentLinkComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(UserAgentLinkComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-agent-link',
|
||||||
|
templateUrl: './user-agent-link.component.html',
|
||||||
|
styleUrls: ['./user-agent-link.component.scss']
|
||||||
|
})
|
||||||
|
export class UserAgentLinkComponent {
|
||||||
|
|
||||||
|
@Input('userAgent') userAgent!: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelClick(event: Event) {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<div class="grid">
|
||||||
|
<ng-container *ngIf="workerInfo$ | async as workerInfo">
|
||||||
|
<div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-0">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">ID</span>
|
||||||
|
<div class="text-900 font-medium text-xl">{{workerInfo.name}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center justify-content-center bg-blue-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-id-card text-blue-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <span class="text-green-500 font-medium">24 new </span> -->
|
||||||
|
<span class="text-500"> </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-0">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Best Difficulty</span>
|
||||||
|
<div class="text-900 font-medium text-xl">{{workerInfo.bestDifficulty | numberSuffix}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center justify-content-center bg-orange-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-star text-orange-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-green-500 font-medium">{{workerInfo.bestDifficulty | number}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-0">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Uptime</span>
|
||||||
|
<div class="text-900 font-medium text-xl">{{workerInfo.startTime | dateAgo}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center justify-content-center bg-cyan-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-clock text-cyan-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-500">Since {{workerInfo.startTime | date : 'short'}}</span>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
<!-- <div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-0">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Comments</span>
|
||||||
|
<div class="text-900 font-medium text-xl">152 Unread</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center justify-content-center bg-purple-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-comment text-purple-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-green-500 font-medium">85 </span>
|
||||||
|
<span class="text-500">responded</span>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-12" *ngIf="chartData$ | async as chartData">
|
||||||
|
<div class="card chart">
|
||||||
|
<p-chart type="line" [data]="chartData" [options]="chartOptions"></p-chart>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { WorkerGroupComponent } from './worker-group.component';
|
||||||
|
|
||||||
|
describe('WorkerGroupComponent', () => {
|
||||||
|
let component: WorkerGroupComponent;
|
||||||
|
let fixture: ComponentFixture<WorkerGroupComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ WorkerGroupComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(WorkerGroupComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { combineLatest, map, Observable, shareReplay } from 'rxjs';
|
||||||
|
|
||||||
|
import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe';
|
||||||
|
import { WorkerService } from '../../services/worker.service';
|
||||||
|
import { AverageTimeToBlockPipe } from 'src/app/pipes/average-time-to-block.pipe';
|
||||||
|
import { AppService } from 'src/app/services/app.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-worker-group',
|
||||||
|
templateUrl: './worker-group.component.html',
|
||||||
|
styleUrls: ['./worker-group.component.scss']
|
||||||
|
})
|
||||||
|
export class WorkerGroupComponent {
|
||||||
|
|
||||||
|
public workerInfo$: Observable<any>;
|
||||||
|
|
||||||
|
public chartData$: Observable<any>;
|
||||||
|
|
||||||
|
public chartOptions: any;
|
||||||
|
|
||||||
|
public networkInfo$: Observable<any>;
|
||||||
|
private networkInfo:any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private workerService: WorkerService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private appService: AppService
|
||||||
|
) {
|
||||||
|
const documentStyle = getComputedStyle(document.documentElement);
|
||||||
|
const textColor = documentStyle.getPropertyValue('--text-color');
|
||||||
|
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
|
||||||
|
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
|
||||||
|
|
||||||
|
this.workerInfo$ = this.workerService.getGroupWorkerInfo(this.route.snapshot.params['address'], this.route.snapshot.params['workerName']).pipe(
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.networkInfo$ = this.appService.getNetworkInfo().pipe(
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.chartData$ = combineLatest([this.workerInfo$, this.networkInfo$]).pipe(
|
||||||
|
map(([workerInfo, networkInfo]) => {
|
||||||
|
this.networkInfo = networkInfo;
|
||||||
|
return {
|
||||||
|
labels: workerInfo.chartData.map((d: any) => d.label),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: workerInfo.name,
|
||||||
|
data: workerInfo.chartData.map((d: any) => d.data),
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: documentStyle.getPropertyValue('--primary-color'),
|
||||||
|
borderColor: documentStyle.getPropertyValue('--primary-color'),
|
||||||
|
tension: .4,
|
||||||
|
pointRadius: 1,
|
||||||
|
borderWidth: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.chartOptions = {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
unit: 'day'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: textColorSecondary
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: surfaceBorder,
|
||||||
|
drawBorder: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: textColorSecondary,
|
||||||
|
callback: (value: number) => {
|
||||||
|
return HashSuffixPipe.transform(value) + " - " + AverageTimeToBlockPipe.transform(value, this.networkInfo.difficulty);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: surfaceBorder,
|
||||||
|
drawBorder: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<div class="grid">
|
||||||
|
<ng-container *ngIf="workerInfo$ | async as workerInfo">
|
||||||
|
<div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-0">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">ID</span>
|
||||||
|
<div class="text-900 font-medium text-xl">{{workerInfo.name}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center justify-content-center bg-blue-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-id-card text-blue-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <span class="text-green-500 font-medium">24 new </span> -->
|
||||||
|
<span class="text-500">{{workerInfo.sessionId}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-0">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Best Difficulty</span>
|
||||||
|
<div class="text-900 font-medium text-xl">{{workerInfo.bestDifficulty | numberSuffix}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center justify-content-center bg-orange-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-star text-orange-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-green-500 font-medium">{{workerInfo.bestDifficulty | number}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-0">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Uptime</span>
|
||||||
|
<div class="text-900 font-medium text-xl">{{workerInfo.startTime | dateAgo}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center justify-content-center bg-cyan-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-clock text-cyan-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <span class="text-green-500 font-medium">520 </span>-->
|
||||||
|
<span class="text-500">Since {{workerInfo.startTime | date : 'short'}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="col-12 lg:col-6 xl:col-3">
|
||||||
|
<div class="card mb-0">
|
||||||
|
<div class="flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<span class="block text-500 font-medium mb-3">Comments</span>
|
||||||
|
<div class="text-900 font-medium text-xl">152 Unread</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center justify-content-center bg-purple-100 border-round"
|
||||||
|
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
|
||||||
|
<i class="pi pi-comment text-purple-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-green-500 font-medium">85 </span>
|
||||||
|
<span class="text-500">responded</span>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-12" *ngIf="chartData$ | async as chartData">
|
||||||
|
<div class="card chart">
|
||||||
|
<p-chart type="line" [data]="chartData" [options]="chartOptions"></p-chart>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { WorkerComponent } from './worker.component';
|
||||||
|
|
||||||
|
describe('WorkerComponent', () => {
|
||||||
|
let component: WorkerComponent;
|
||||||
|
let fixture: ComponentFixture<WorkerComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ WorkerComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(WorkerComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { combineLatest, map, Observable, shareReplay } from 'rxjs';
|
||||||
|
|
||||||
|
import { HashSuffixPipe } from '../../pipes/hash-suffix.pipe';
|
||||||
|
import { WorkerService } from '../../services/worker.service';
|
||||||
|
import { AverageTimeToBlockPipe } from 'src/app/pipes/average-time-to-block.pipe';
|
||||||
|
import { AppService } from 'src/app/services/app.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-worker',
|
||||||
|
templateUrl: './worker.component.html',
|
||||||
|
styleUrls: ['./worker.component.scss']
|
||||||
|
})
|
||||||
|
export class WorkerComponent {
|
||||||
|
|
||||||
|
public workerInfo$: Observable<any>;
|
||||||
|
|
||||||
|
public chartData$: Observable<any>;
|
||||||
|
|
||||||
|
public chartOptions: any;
|
||||||
|
|
||||||
|
public networkInfo$: Observable<any>;
|
||||||
|
private networkInfo:any;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private workerService: WorkerService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private appService: AppService
|
||||||
|
) {
|
||||||
|
const documentStyle = getComputedStyle(document.documentElement);
|
||||||
|
const textColor = documentStyle.getPropertyValue('--text-color');
|
||||||
|
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
|
||||||
|
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
|
||||||
|
|
||||||
|
this.workerInfo$ = this.workerService.getWorkerInfo(this.route.snapshot.params['address'], this.route.snapshot.params['workerName'], this.route.snapshot.params['workerId']).pipe(
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.networkInfo$ = this.appService.getNetworkInfo().pipe(
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.chartData$ = combineLatest([ this.workerInfo$, this.networkInfo$]).pipe(
|
||||||
|
map(([workerInfo, networkInfo]) => {
|
||||||
|
|
||||||
|
this.networkInfo = networkInfo;
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: workerInfo.chartData.map((d: any) => d.label),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: workerInfo.name,
|
||||||
|
data: workerInfo.chartData.map((d: any) => d.data),
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: documentStyle.getPropertyValue('--primary-color'),
|
||||||
|
borderColor: documentStyle.getPropertyValue('--primary-color'),
|
||||||
|
tension: .4,
|
||||||
|
pointRadius: 1,
|
||||||
|
borderWidth: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.chartOptions = {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
unit: 'day'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: textColorSecondary
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: surfaceBorder,
|
||||||
|
drawBorder: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: textColorSecondary,
|
||||||
|
callback: (value: number) => {
|
||||||
|
return HashSuffixPipe.transform(value) + " - " + AverageTimeToBlockPipe.transform(value, this.networkInfo.difficulty);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: surfaceBorder,
|
||||||
|
drawBorder: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface MenuChangeEvent {
|
||||||
|
key: string;
|
||||||
|
routeEvent?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { LayoutService } from "./service/app.layout.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-footer',
|
||||||
|
templateUrl: './app.footer.component.html'
|
||||||
|
})
|
||||||
|
export class AppFooterComponent {
|
||||||
|
constructor(public layoutService: LayoutService) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<div class="layout-wrapper" [ngClass]="containerClass">
|
||||||
|
<app-topbar></app-topbar>
|
||||||
|
<div class="layout-sidebar">
|
||||||
|
<app-sidebar></app-sidebar>
|
||||||
|
</div>
|
||||||
|
<div class="layout-main-container">
|
||||||
|
<div class="layout-main">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
</div>
|
||||||
|
<!-- <app-config></app-config> -->
|
||||||
|
<div class="layout-mask"></div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { Component, OnDestroy, Renderer2, ViewChild } from '@angular/core';
|
||||||
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { filter, Subscription } from 'rxjs';
|
||||||
|
import { LayoutService } from "./service/app.layout.service";
|
||||||
|
import { AppSidebarComponent } from "./app.sidebar.component";
|
||||||
|
import { AppTopBarComponent } from './app.topbar.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-layout',
|
||||||
|
templateUrl: './app.layout.component.html'
|
||||||
|
})
|
||||||
|
export class AppLayoutComponent implements OnDestroy {
|
||||||
|
|
||||||
|
overlayMenuOpenSubscription: Subscription;
|
||||||
|
|
||||||
|
menuOutsideClickListener: any;
|
||||||
|
|
||||||
|
profileMenuOutsideClickListener: any;
|
||||||
|
|
||||||
|
@ViewChild(AppSidebarComponent) appSidebar!: AppSidebarComponent;
|
||||||
|
|
||||||
|
@ViewChild(AppTopBarComponent) appTopbar!: AppTopBarComponent;
|
||||||
|
|
||||||
|
constructor(public layoutService: LayoutService, public renderer: Renderer2, public router: Router) {
|
||||||
|
this.overlayMenuOpenSubscription = this.layoutService.overlayOpen$.subscribe(() => {
|
||||||
|
if (!this.menuOutsideClickListener) {
|
||||||
|
this.menuOutsideClickListener = this.renderer.listen('document', 'click', event => {
|
||||||
|
const isOutsideClicked = !(this.appSidebar.el.nativeElement.isSameNode(event.target) || this.appSidebar.el.nativeElement.contains(event.target)
|
||||||
|
|| this.appTopbar.menuButton.nativeElement.isSameNode(event.target) || this.appTopbar.menuButton.nativeElement.contains(event.target));
|
||||||
|
|
||||||
|
if (isOutsideClicked) {
|
||||||
|
this.hideMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.profileMenuOutsideClickListener) {
|
||||||
|
this.profileMenuOutsideClickListener = this.renderer.listen('document', 'click', event => {
|
||||||
|
const isOutsideClicked = !(this.appTopbar.menu.nativeElement.isSameNode(event.target) || this.appTopbar.menu.nativeElement.contains(event.target)
|
||||||
|
|| this.appTopbar.topbarMenuButton.nativeElement.isSameNode(event.target) || this.appTopbar.topbarMenuButton.nativeElement.contains(event.target));
|
||||||
|
|
||||||
|
if (isOutsideClicked) {
|
||||||
|
this.hideProfileMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.layoutService.state.staticMenuMobileActive) {
|
||||||
|
this.blockBodyScroll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.router.events.pipe(filter(event => event instanceof NavigationEnd))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.hideMenu();
|
||||||
|
this.hideProfileMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hideMenu() {
|
||||||
|
this.layoutService.state.overlayMenuActive = false;
|
||||||
|
this.layoutService.state.staticMenuMobileActive = false;
|
||||||
|
this.layoutService.state.menuHoverActive = false;
|
||||||
|
if (this.menuOutsideClickListener) {
|
||||||
|
this.menuOutsideClickListener();
|
||||||
|
this.menuOutsideClickListener = null;
|
||||||
|
}
|
||||||
|
this.unblockBodyScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideProfileMenu() {
|
||||||
|
this.layoutService.state.profileSidebarVisible = false;
|
||||||
|
if (this.profileMenuOutsideClickListener) {
|
||||||
|
this.profileMenuOutsideClickListener();
|
||||||
|
this.profileMenuOutsideClickListener = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blockBodyScroll(): void {
|
||||||
|
if (document.body.classList) {
|
||||||
|
document.body.classList.add('blocked-scroll');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.body.className += ' blocked-scroll';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unblockBodyScroll(): void {
|
||||||
|
if (document.body.classList) {
|
||||||
|
document.body.classList.remove('blocked-scroll');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.body.className = document.body.className.replace(new RegExp('(^|\\b)' +
|
||||||
|
'blocked-scroll'.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get containerClass() {
|
||||||
|
return {
|
||||||
|
'layout-theme-light': this.layoutService.config.colorScheme === 'light',
|
||||||
|
'layout-theme-dark': this.layoutService.config.colorScheme === 'dark',
|
||||||
|
'layout-overlay': this.layoutService.config.menuMode === 'overlay',
|
||||||
|
'layout-static': this.layoutService.config.menuMode === 'static',
|
||||||
|
'layout-static-inactive': this.layoutService.state.staticMenuDesktopInactive && this.layoutService.config.menuMode === 'static',
|
||||||
|
'layout-overlay-active': this.layoutService.state.overlayMenuActive,
|
||||||
|
'layout-mobile-active': this.layoutService.state.staticMenuMobileActive,
|
||||||
|
'p-input-filled': this.layoutService.config.inputStyle === 'filled',
|
||||||
|
'p-ripple-disabled': !this.layoutService.config.ripple
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.overlayMenuOpenSubscription) {
|
||||||
|
this.overlayMenuOpenSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.menuOutsideClickListener) {
|
||||||
|
this.menuOutsideClickListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { BadgeModule } from 'primeng/badge';
|
||||||
|
import { InputSwitchModule } from 'primeng/inputswitch';
|
||||||
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
|
import { RadioButtonModule } from 'primeng/radiobutton';
|
||||||
|
import { RippleModule } from 'primeng/ripple';
|
||||||
|
import { SidebarModule } from 'primeng/sidebar';
|
||||||
|
|
||||||
|
import { AppFooterComponent } from './app.footer.component';
|
||||||
|
import { AppLayoutComponent } from './app.layout.component';
|
||||||
|
import { AppMenuComponent } from './app.menu.component';
|
||||||
|
import { AppMenuitemComponent } from './app.menuitem.component';
|
||||||
|
import { AppSidebarComponent } from './app.sidebar.component';
|
||||||
|
import { AppTopBarComponent } from './app.topbar.component';
|
||||||
|
|
||||||
|
//import { AppConfigModule } from './config/config.module';
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppMenuitemComponent,
|
||||||
|
AppTopBarComponent,
|
||||||
|
AppFooterComponent,
|
||||||
|
AppMenuComponent,
|
||||||
|
AppSidebarComponent,
|
||||||
|
AppLayoutComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
FormsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
InputTextModule,
|
||||||
|
SidebarModule,
|
||||||
|
BadgeModule,
|
||||||
|
RadioButtonModule,
|
||||||
|
InputSwitchModule,
|
||||||
|
RippleModule,
|
||||||
|
RouterModule,
|
||||||
|
// AppConfigModule
|
||||||
|
],
|
||||||
|
exports: [AppLayoutComponent]
|
||||||
|
})
|
||||||
|
export class AppLayoutModule { }
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<ul class="layout-menu">
|
||||||
|
<ng-container *ngFor="let item of model$ | async; let i = index;">
|
||||||
|
<li app-menuitem *ngIf="!item.separator" [item]="item" [index]="i" [root]="true"></li>
|
||||||
|
<li *ngIf="item.separator" class="menu-separator"></li>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { filter, map, mergeMap, Observable, startWith } from 'rxjs';
|
||||||
|
|
||||||
|
import { LayoutService } from './service/app.layout.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-menu',
|
||||||
|
templateUrl: './app.menu.component.html'
|
||||||
|
})
|
||||||
|
export class AppMenuComponent implements OnInit {
|
||||||
|
|
||||||
|
private params$!: Observable<any>;
|
||||||
|
public model$!: Observable<any[]>;
|
||||||
|
|
||||||
|
constructor(public layoutService: LayoutService, private router: Router, private activatedRoute: ActivatedRoute) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
|
||||||
|
const rootRoute = this.activatedRoute.root;
|
||||||
|
const initialParams = this.extractRouteParams(rootRoute.snapshot);
|
||||||
|
|
||||||
|
this.params$ = this.router.events
|
||||||
|
.pipe(
|
||||||
|
filter((event) => event instanceof NavigationEnd),
|
||||||
|
map(() => this.activatedRoute),
|
||||||
|
map((route) => {
|
||||||
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild;
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
}),
|
||||||
|
filter((route) => route.outlet === 'primary'), // Adjust the outlet name if necessary
|
||||||
|
mergeMap((route) => route.params),
|
||||||
|
startWith(initialParams)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.model$ = this.params$.pipe(map(params => {
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
items: [
|
||||||
|
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: [params.address] },
|
||||||
|
{ label: 'Settings', icon: 'pi pi-fw pi-cog', routerLink: [params.address, 'settings'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractRouteParams(routeSnapshot: ActivatedRouteSnapshot): any {
|
||||||
|
let route = routeSnapshot;
|
||||||
|
let params = {};
|
||||||
|
|
||||||
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route && route.params) {
|
||||||
|
params = route.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { MenuChangeEvent } from './api/menuchangeevent';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MenuService {
|
||||||
|
|
||||||
|
private menuSource = new Subject<MenuChangeEvent>();
|
||||||
|
private resetSource = new Subject();
|
||||||
|
|
||||||
|
menuSource$ = this.menuSource.asObservable();
|
||||||
|
resetSource$ = this.resetSource.asObservable();
|
||||||
|
|
||||||
|
onMenuStateChange(event: MenuChangeEvent) {
|
||||||
|
this.menuSource.next(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.resetSource.next(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { ChangeDetectorRef, Component, Host, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { filter } from 'rxjs/operators';
|
||||||
|
import { MenuService } from './app.menu.service';
|
||||||
|
import { LayoutService } from './service/app.layout.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
// eslint-disable-next-line @angular-eslint/component-selector
|
||||||
|
selector: '[app-menuitem]',
|
||||||
|
template: `
|
||||||
|
<ng-container>
|
||||||
|
<div *ngIf="root && item.visible !== false" class="layout-menuitem-root-text">{{item.label}}</div>
|
||||||
|
<a *ngIf="(!item.routerLink || item.items) && item.visible !== false" [attr.href]="item.url" (click)="itemClick($event)"
|
||||||
|
[ngClass]="item.class" [attr.target]="item.target" tabindex="0" pRipple>
|
||||||
|
<i [ngClass]="item.icon" class="layout-menuitem-icon"></i>
|
||||||
|
<span class="layout-menuitem-text">{{item.label}}</span>
|
||||||
|
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" *ngIf="item.items"></i>
|
||||||
|
</a>
|
||||||
|
<a *ngIf="(item.routerLink && !item.items) && item.visible !== false" (click)="itemClick($event)" [ngClass]="item.class"
|
||||||
|
[routerLink]="item.routerLink" routerLinkActive="active-route" [routerLinkActiveOptions]="item.routerLinkActiveOptions||{ paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored' }"
|
||||||
|
[fragment]="item.fragment" [queryParamsHandling]="item.queryParamsHandling" [preserveFragment]="item.preserveFragment"
|
||||||
|
[skipLocationChange]="item.skipLocationChange" [replaceUrl]="item.replaceUrl" [state]="item.state" [queryParams]="item.queryParams"
|
||||||
|
[attr.target]="item.target" tabindex="0" pRipple>
|
||||||
|
<i [ngClass]="item.icon" class="layout-menuitem-icon"></i>
|
||||||
|
<span class="layout-menuitem-text">{{item.label}}</span>
|
||||||
|
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" *ngIf="item.items"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul *ngIf="item.items && item.visible !== false" [@children]="submenuAnimation">
|
||||||
|
<ng-template ngFor let-child let-i="index" [ngForOf]="item.items">
|
||||||
|
<li app-menuitem [item]="child" [index]="i" [parentKey]="key" [class]="child.badgeClass"></li>
|
||||||
|
</ng-template>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
`,
|
||||||
|
animations: [
|
||||||
|
trigger('children', [
|
||||||
|
state('collapsed', style({
|
||||||
|
height: '0'
|
||||||
|
})),
|
||||||
|
state('expanded', style({
|
||||||
|
height: '*'
|
||||||
|
})),
|
||||||
|
transition('collapsed <=> expanded', animate('400ms cubic-bezier(0.86, 0, 0.07, 1)'))
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppMenuitemComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@Input() item: any;
|
||||||
|
|
||||||
|
@Input() index!: number;
|
||||||
|
|
||||||
|
@Input() @HostBinding('class.layout-root-menuitem') root!: boolean;
|
||||||
|
|
||||||
|
@Input() parentKey!: string;
|
||||||
|
|
||||||
|
active = false;
|
||||||
|
|
||||||
|
menuSourceSubscription: Subscription;
|
||||||
|
|
||||||
|
menuResetSubscription: Subscription;
|
||||||
|
|
||||||
|
key: string = "";
|
||||||
|
|
||||||
|
constructor(public layoutService: LayoutService, private cd: ChangeDetectorRef, public router: Router, private menuService: MenuService) {
|
||||||
|
this.menuSourceSubscription = this.menuService.menuSource$.subscribe(value => {
|
||||||
|
Promise.resolve(null).then(() => {
|
||||||
|
if (value.routeEvent) {
|
||||||
|
this.active = (value.key === this.key || value.key.startsWith(this.key + '-')) ? true : false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (value.key !== this.key && !value.key.startsWith(this.key + '-')) {
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.menuResetSubscription = this.menuService.resetSource$.subscribe(() => {
|
||||||
|
this.active = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.router.events.pipe(filter(event => event instanceof NavigationEnd))
|
||||||
|
.subscribe(params => {
|
||||||
|
if (this.item.routerLink) {
|
||||||
|
this.updateActiveStateFromRoute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.key = this.parentKey ? this.parentKey + '-' + this.index : String(this.index);
|
||||||
|
|
||||||
|
if (this.item.routerLink) {
|
||||||
|
this.updateActiveStateFromRoute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActiveStateFromRoute() {
|
||||||
|
let activeRoute = this.router.isActive(this.item.routerLink[0], { paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored' });
|
||||||
|
|
||||||
|
if (activeRoute) {
|
||||||
|
this.menuService.onMenuStateChange({ key: this.key, routeEvent: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemClick(event: Event) {
|
||||||
|
// avoid processing disabled items
|
||||||
|
if (this.item.disabled) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute command
|
||||||
|
if (this.item.command) {
|
||||||
|
this.item.command({ originalEvent: event, item: this.item });
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggle active state
|
||||||
|
if (this.item.items) {
|
||||||
|
this.active = !this.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.menuService.onMenuStateChange({ key: this.key });
|
||||||
|
}
|
||||||
|
|
||||||
|
get submenuAnimation() {
|
||||||
|
return this.root ? 'expanded' : (this.active ? 'expanded' : 'collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostBinding('class.active-menuitem')
|
||||||
|
get activeClass() {
|
||||||
|
return this.active && !this.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.menuSourceSubscription) {
|
||||||
|
this.menuSourceSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.menuResetSubscription) {
|
||||||
|
this.menuResetSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<app-menu></app-menu>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component, ElementRef } from '@angular/core';
|
||||||
|
import { LayoutService } from "./service/app.layout.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-sidebar',
|
||||||
|
templateUrl: './app.sidebar.component.html'
|
||||||
|
})
|
||||||
|
export class AppSidebarComponent {
|
||||||
|
constructor(public layoutService: LayoutService, public el: ElementRef) { }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<div class="layout-topbar">
|
||||||
|
<a class="layout-topbar-logo" routerLink="">
|
||||||
|
<img src="assets/layout/images/logo.svg" alt="logo">
|
||||||
|
<span>Public Pool</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button #menubutton class="p-link layout-menu-button layout-topbar-button" (click)="layoutService.onMenuToggle()">
|
||||||
|
<i class="pi pi-bars"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button #topbarmenubutton class="p-link layout-topbar-menu-button layout-topbar-button"
|
||||||
|
(click)="layoutService.showProfileSidebar()">
|
||||||
|
<i class="pi pi-ellipsis-v"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- <div #topbarmenu class="layout-topbar-menu"
|
||||||
|
[ngClass]="{'layout-topbar-menu-mobile-active': layoutService.state.profileSidebarVisible}">
|
||||||
|
<button class="p-link layout-topbar-button">
|
||||||
|
<i class="pi pi-calendar"></i>
|
||||||
|
<span>Calendar</span>
|
||||||
|
</button>
|
||||||
|
<button class="p-link layout-topbar-button">
|
||||||
|
<i class="pi pi-user"></i>
|
||||||
|
<span>Profile</span>
|
||||||
|
</button>
|
||||||
|
<button class="p-link layout-topbar-button" [routerLink]="'/documentation'">
|
||||||
|
<i class="pi pi-cog"></i>
|
||||||
|
<span>Settings</span>
|
||||||
|
</button>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||||
|
import { MenuItem } from 'primeng/api';
|
||||||
|
import { LayoutService } from "./service/app.layout.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-topbar',
|
||||||
|
templateUrl: './app.topbar.component.html'
|
||||||
|
})
|
||||||
|
export class AppTopBarComponent {
|
||||||
|
|
||||||
|
items!: MenuItem[];
|
||||||
|
|
||||||
|
@ViewChild('menubutton') menuButton!: ElementRef;
|
||||||
|
|
||||||
|
@ViewChild('topbarmenubutton') topbarMenuButton!: ElementRef;
|
||||||
|
|
||||||
|
@ViewChild('topbarmenu') menu!: ElementRef;
|
||||||
|
|
||||||
|
constructor(public layoutService: LayoutService) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
inputStyle: string;
|
||||||
|
colorScheme: string;
|
||||||
|
theme: string;
|
||||||
|
ripple: boolean;
|
||||||
|
menuMode: string;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LayoutState {
|
||||||
|
staticMenuDesktopInactive: boolean;
|
||||||
|
overlayMenuActive: boolean;
|
||||||
|
profileSidebarVisible: boolean;
|
||||||
|
configSidebarVisible: boolean;
|
||||||
|
staticMenuMobileActive: boolean;
|
||||||
|
menuHoverActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class LayoutService {
|
||||||
|
|
||||||
|
config: AppConfig = {
|
||||||
|
ripple: false,
|
||||||
|
inputStyle: 'outlined',
|
||||||
|
menuMode: 'static',
|
||||||
|
colorScheme: 'light',
|
||||||
|
theme: 'lara-light-indigo',
|
||||||
|
scale: 14,
|
||||||
|
};
|
||||||
|
|
||||||
|
state: LayoutState = {
|
||||||
|
staticMenuDesktopInactive: false,
|
||||||
|
overlayMenuActive: false,
|
||||||
|
profileSidebarVisible: false,
|
||||||
|
configSidebarVisible: false,
|
||||||
|
staticMenuMobileActive: false,
|
||||||
|
menuHoverActive: false
|
||||||
|
};
|
||||||
|
|
||||||
|
private configUpdate = new Subject<AppConfig>();
|
||||||
|
|
||||||
|
private overlayOpen = new Subject<any>();
|
||||||
|
|
||||||
|
configUpdate$ = this.configUpdate.asObservable();
|
||||||
|
|
||||||
|
overlayOpen$ = this.overlayOpen.asObservable();
|
||||||
|
|
||||||
|
onMenuToggle() {
|
||||||
|
if (this.isOverlay()) {
|
||||||
|
this.state.overlayMenuActive = !this.state.overlayMenuActive;
|
||||||
|
if (this.state.overlayMenuActive) {
|
||||||
|
this.overlayOpen.next(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDesktop()) {
|
||||||
|
this.state.staticMenuDesktopInactive = !this.state.staticMenuDesktopInactive;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.state.staticMenuMobileActive = !this.state.staticMenuMobileActive;
|
||||||
|
|
||||||
|
if (this.state.staticMenuMobileActive) {
|
||||||
|
this.overlayOpen.next(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showProfileSidebar() {
|
||||||
|
this.state.profileSidebarVisible = !this.state.profileSidebarVisible;
|
||||||
|
if (this.state.profileSidebarVisible) {
|
||||||
|
this.overlayOpen.next(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showConfigSidebar() {
|
||||||
|
this.state.configSidebarVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOverlay() {
|
||||||
|
return this.config.menuMode === 'overlay';
|
||||||
|
}
|
||||||
|
|
||||||
|
isDesktop() {
|
||||||
|
return window.innerWidth > 991;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMobile() {
|
||||||
|
return !this.isDesktop();
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfigUpdate() {
|
||||||
|
this.configUpdate.next(this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { AverageTimeToBlockPipe } from './average-time-to-block.pipe';
|
||||||
|
|
||||||
|
describe('AverageTimeToBlockPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new AverageTimeToBlockPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { DateAgoPipe } from './date-ago.pipe';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'averageTimeToBlock',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class AverageTimeToBlockPipe implements PipeTransform {
|
||||||
|
|
||||||
|
private static _this = new AverageTimeToBlockPipe();
|
||||||
|
private static _dateAgo = new DateAgoPipe();
|
||||||
|
|
||||||
|
public static transform(value: number, difficulty: number): string {
|
||||||
|
return this._this.transform(value, difficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public transform(value: number, difficulty: number): string {
|
||||||
|
const blockTimeInSeconds = this.calculateBlockTime(value, difficulty);
|
||||||
|
if(blockTimeInSeconds > 8000000000000){
|
||||||
|
return '~∞';
|
||||||
|
}
|
||||||
|
const date = new Date(new Date().getTime() + (blockTimeInSeconds * 1000));
|
||||||
|
return AverageTimeToBlockPipe._dateAgo.transform(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateBlockTime(hashRate: number, difficulty: number): number {
|
||||||
|
const hashesPerDifficulty = Math.pow(2, 32);
|
||||||
|
|
||||||
|
// Calculate the average block time in seconds
|
||||||
|
const averageBlockTime = (difficulty * hashesPerDifficulty) / hashRate;
|
||||||
|
|
||||||
|
return averageBlockTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { DateAgoPipe } from './date-ago.pipe';
|
||||||
|
|
||||||
|
describe('DateAgoPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new DateAgoPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'dateAgo',
|
||||||
|
pure: true
|
||||||
|
})
|
||||||
|
export class DateAgoPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(value: any, args?: any): any {
|
||||||
|
if (value) {
|
||||||
|
const seconds = Math.abs(Math.floor((+new Date() - +new Date(value)) / 1000));
|
||||||
|
if (seconds < 29) // less than 30 seconds ago will show as 'Just now'
|
||||||
|
return 'Just now';
|
||||||
|
const intervals: { [key: string]: number } = {
|
||||||
|
'year': 31536000,
|
||||||
|
'month': 2592000,
|
||||||
|
'week': 604800,
|
||||||
|
'day': 86400,
|
||||||
|
'hour': 3600,
|
||||||
|
'minute': 60,
|
||||||
|
'second': 1
|
||||||
|
};
|
||||||
|
let counter;
|
||||||
|
for (const i in intervals) {
|
||||||
|
const number = (seconds / intervals[i]).toFixed(1);
|
||||||
|
counter = Math.floor(seconds / intervals[i]);
|
||||||
|
|
||||||
|
if (counter > 0)
|
||||||
|
return number + ' ' + i + 's'; // plural (2 days ago)
|
||||||
|
// if (counter === 1) {
|
||||||
|
// return number + ' ' + i + ''; // singular (1 day ago)
|
||||||
|
// } else {
|
||||||
|
// return number + ' ' + i + 's'; // plural (2 days ago)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { HashSuffixPipe } from './hash-suffix.pipe';
|
||||||
|
|
||||||
|
describe('HashSuffixPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new HashSuffixPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'hashSuffix'
|
||||||
|
})
|
||||||
|
export class HashSuffixPipe implements PipeTransform {
|
||||||
|
|
||||||
|
private static _this = new HashSuffixPipe();
|
||||||
|
|
||||||
|
public static transform(value: number): string {
|
||||||
|
return this._this.transform(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public transform(value: number): string {
|
||||||
|
|
||||||
|
if (value == null || value < 0) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffixes = [' H/s', ' KH/s', ' MH/s', ' GH/s', ' TH/s', ' PH/s', ' EH/s', ' ZH/s', ' YH/s', ' RH/s', ' QH/s'];
|
||||||
|
|
||||||
|
let power = Math.floor(Math.log10(value) / 3);
|
||||||
|
if (power < 0) {
|
||||||
|
power = 0;
|
||||||
|
}
|
||||||
|
const scaledValue = value / Math.pow(1000, power);
|
||||||
|
const suffix = suffixes[power];
|
||||||
|
|
||||||
|
return scaledValue.toFixed(1) + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { NumberSuffixPipe } from './number-suffix.pipe';
|
||||||
|
|
||||||
|
describe('NumberSuffixPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new NumberSuffixPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'numberSuffix',
|
||||||
|
pure: true
|
||||||
|
})
|
||||||
|
export class NumberSuffixPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(value: number): string {
|
||||||
|
|
||||||
|
const suffixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'];
|
||||||
|
|
||||||
|
if (value == null || value < 0) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
let power = Math.floor(Math.log10(value) / 3);
|
||||||
|
if (power < 0) {
|
||||||
|
power = 0;
|
||||||
|
}
|
||||||
|
const scaledValue = value / Math.pow(1000, power);
|
||||||
|
const suffix = suffixes[power];
|
||||||
|
|
||||||
|
return scaledValue.toFixed(2) + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AppService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private httpClient: HttpClient
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public getInfo() {
|
||||||
|
return this.httpClient.get(`${environment.API_URL}/api/info`) as Observable<any>;
|
||||||
|
}
|
||||||
|
public getNetworkInfo() {
|
||||||
|
return this.httpClient.get(`${environment.API_URL}/api/network`) as Observable<any>;
|
||||||
|
}
|
||||||
|
public getInfoChart() {
|
||||||
|
return this.httpClient.get(`${environment.API_URL}/api/info/chart`) as Observable<any>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ClientService } from './client.service';
|
||||||
|
|
||||||
|
describe('ClientService', () => {
|
||||||
|
let service: ClientService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ClientService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ClientService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private httpClient: HttpClient
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public getClientInfo(address: string) {
|
||||||
|
return this.httpClient.get(`${environment.API_URL}/api/client/${address}`);
|
||||||
|
}
|
||||||
|
public getClientInfoChart(address: string) {
|
||||||
|
return this.httpClient.get(`${environment.API_URL}/api/client/${address}/chart`) as Observable<any[]>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LocalStorageService } from './local-storage.service';
|
||||||
|
|
||||||
|
describe('LocalStorageService', () => {
|
||||||
|
let service: LocalStorageService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(LocalStorageService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject, Observable, shareReplay } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LocalStorageService {
|
||||||
|
|
||||||
|
private PARTICLES = 'PARTICLES';
|
||||||
|
|
||||||
|
private _particles$: BehaviorSubject<boolean>;
|
||||||
|
public particles$: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._particles$ = new BehaviorSubject<boolean>(this.getParticles());
|
||||||
|
this.particles$ = this._particles$.asObservable().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private get(key: string): string | null {
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private set(key: string, value: string) {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private remove(key: string): void {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public getParticles(): boolean {
|
||||||
|
const result = this.get(this.PARTICLES);
|
||||||
|
return result == null || JSON.parse(result)?.particles === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setParticles(particles: boolean) {
|
||||||
|
this.set(this.PARTICLES, JSON.stringify({ particles }));
|
||||||
|
this._particles$.next(particles);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { WorkerService } from './worker.service';
|
||||||
|
|
||||||
|
describe('WorkerService', () => {
|
||||||
|
let service: WorkerService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(WorkerService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class WorkerService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private httpClient: HttpClient
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public getGroupWorkerInfo(address: string, workerName: string): Observable<any> {
|
||||||
|
return this.httpClient.get(`${environment.API_URL}/api/client/${address}/${workerName}`);
|
||||||
|
}
|
||||||
|
public getWorkerInfo(address: string, workerName: string, workerId: string): Observable<any> {
|
||||||
|
return this.httpClient.get(`${environment.API_URL}/api/client/${address}/${workerName}/${workerId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { AbstractControl, ValidatorFn } from '@angular/forms';
|
||||||
|
import { validate } from 'bitcoin-address-validation';
|
||||||
|
|
||||||
|
export function bitcoinAddressValidator(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): { [key: string]: any } | null => {
|
||||||
|
|
||||||
|
if (validate(control.value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ['bitcoin-address']: true };
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user