Compare commits
375 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7a3fdfb467 | ||
![]() |
d91b0cea85 | ||
![]() |
6c1dd0dbf6 | ||
![]() |
281c8c1ea4 | ||
![]() |
cc7aaf17f2 | ||
![]() |
6e06daa531 | ||
![]() |
eae11eb3ce | ||
![]() |
9085b2587c | ||
![]() |
49d7dd7d4a | ||
![]() |
9d133e40e6 | ||
![]() |
23ab2f7a97 | ||
![]() |
20c1d18ac8 | ||
![]() |
9aaba072e8 | ||
![]() |
3e77d19f84 | ||
![]() |
768dc1b06c | ||
![]() |
4884c95a50 | ||
![]() |
320b29df55 | ||
![]() |
9644600f0d | ||
![]() |
3d2932f385 | ||
![]() |
7ade6fcb9c | ||
![]() |
ec66cc7692 | ||
![]() |
585780e26f | ||
![]() |
411a9cc57a | ||
![]() |
fb5cc7bc3c | ||
![]() |
9edf3e5092 | ||
![]() |
df0f19bec7 | ||
![]() |
62e0050147 | ||
![]() |
456a74100c | ||
![]() |
bb5b166d36 | ||
![]() |
161b39521b | ||
![]() |
253f95c186 | ||
![]() |
0faa1133fc | ||
![]() |
2c9c28253f | ||
![]() |
2505c930f7 | ||
![]() |
714d560774 | ||
![]() |
923fa48a97 | ||
![]() |
dccb7533a2 | ||
![]() |
9ec6784b26 | ||
![]() |
2daf467650 | ||
![]() |
902c823d7d | ||
![]() |
133589b1af | ||
![]() |
73196dadf6 | ||
![]() |
050fe7484b | ||
![]() |
34bc76dbf8 | ||
![]() |
aa963bb89f | ||
![]() |
3abd0ee6ea | ||
![]() |
57e6680668 | ||
![]() |
6d275216de | ||
![]() |
cc7cab467d | ||
![]() |
798505f7a7 | ||
![]() |
19d69d6dd2 | ||
![]() |
733745944a | ||
![]() |
5b8622ff57 | ||
![]() |
6a5f9b74fe | ||
![]() |
bdfcf0a2bf | ||
![]() |
0daf229b56 | ||
![]() |
53db1b883d | ||
![]() |
d68f9df66f | ||
![]() |
ca67ebbb45 | ||
![]() |
6f0e37c524 | ||
![]() |
ba67c90714 | ||
![]() |
f6b322a92a | ||
![]() |
10fe2d7f25 | ||
![]() |
d247708d73 | ||
![]() |
b8ad765d1b | ||
![]() |
b50d17eae3 | ||
![]() |
b5f4ce08ea | ||
![]() |
15d8fdd404 | ||
![]() |
4556bca285 | ||
![]() |
4a83715d39 | ||
![]() |
a578ec8987 | ||
![]() |
7c1238b492 | ||
![]() |
e380c80591 | ||
![]() |
d7cf669583 | ||
![]() |
af4ecbef41 | ||
![]() |
5c1b7e5d74 | ||
![]() |
1075f832c9 | ||
![]() |
0f49682376 | ||
![]() |
379c574358 | ||
![]() |
2c5ec16653 | ||
![]() |
1d650b3c4e | ||
![]() |
d3a4d615b2 | ||
![]() |
9d1cd315bc | ||
![]() |
5e26d6ad90 | ||
![]() |
33590ce369 | ||
![]() |
8a547e1b9c | ||
![]() |
6aa8a9e5fa | ||
![]() |
6ba22dc4cb | ||
![]() |
af30f256f1 | ||
![]() |
d80c37e714 | ||
![]() |
8a5e261bd1 | ||
![]() |
72436c7b78 | ||
![]() |
e80b202b6c | ||
![]() |
4d5a148278 | ||
![]() |
14bc328a66 | ||
![]() |
498112cb16 | ||
![]() |
7f5f87e2b6 | ||
![]() |
37c7a61db9 | ||
![]() |
22a25ee1e8 | ||
![]() |
2440a0a301 | ||
![]() |
a854cd9bd7 | ||
![]() |
7e40039901 | ||
![]() |
f0b810f480 | ||
![]() |
1fdeb258d0 | ||
![]() |
1ce42346bc | ||
![]() |
1a41872613 | ||
![]() |
8d127c3d3f | ||
![]() |
3f39200214 | ||
![]() |
744b98b3db | ||
![]() |
dd1305290a | ||
![]() |
93ca8bd79d | ||
![]() |
3dc4866775 | ||
![]() |
4166e2d2d1 | ||
![]() |
112d1aaa99 | ||
![]() |
a732130ca1 | ||
![]() |
2bdc2952d6 | ||
![]() |
df27afb694 | ||
![]() |
50dadfd9e8 | ||
![]() |
0442951a71 | ||
![]() |
4b3c7d8be9 | ||
![]() |
f796c521ef | ||
![]() |
4436a56297 | ||
![]() |
c779d47537 | ||
![]() |
dd61551566 | ||
![]() |
9fe6e2c95e | ||
![]() |
2220b03847 | ||
![]() |
443f584cb3 | ||
![]() |
b57969fd6d | ||
![]() |
e0bf591081 | ||
![]() |
a9f83a37b4 | ||
![]() |
a3df4704c4 | ||
![]() |
4f7421e641 | ||
![]() |
1f7c588a2b | ||
![]() |
3f7527b6ef | ||
![]() |
cf8a29c1aa | ||
![]() |
8d359ca008 | ||
![]() |
c7dda3db6c | ||
![]() |
9b033696e3 | ||
![]() |
a6b96bc42a | ||
![]() |
ef37940742 | ||
![]() |
c912f16640 | ||
![]() |
a57dbc806b | ||
![]() |
07e7f634b7 | ||
![]() |
0af998d06c | ||
![]() |
2e728b3ac3 | ||
![]() |
0479504558 | ||
![]() |
0142c6fb11 | ||
![]() |
9206dbade8 | ||
![]() |
735874006c | ||
![]() |
ff90bdf51e | ||
![]() |
da94e1dfee | ||
![]() |
84335be674 | ||
![]() |
486291871e | ||
![]() |
bb11cbbe96 | ||
![]() |
8e83fde02e | ||
![]() |
f127e5a146 | ||
![]() |
bc7aaa21d0 | ||
![]() |
0046b5b65a | ||
![]() |
525146a210 | ||
![]() |
75c0521ad9 | ||
![]() |
bbe3e7483e | ||
![]() |
7e38f5b78e | ||
![]() |
43a526fb61 | ||
![]() |
425747234b | ||
![]() |
ac560df6b3 | ||
![]() |
25707fafff | ||
![]() |
6398916769 | ||
![]() |
8265dedd95 | ||
![]() |
2fcf44fbca | ||
![]() |
245c00fecb | ||
![]() |
f7ee52dbb3 | ||
![]() |
60be289c76 | ||
![]() |
532795e938 | ||
![]() |
bc3eeb6bd6 | ||
![]() |
b63aad4960 | ||
![]() |
0801799600 | ||
![]() |
3d8d411dd4 | ||
![]() |
6d78a129cc | ||
![]() |
7c8750d240 | ||
![]() |
4975d764c6 | ||
![]() |
d86e4c04ca | ||
![]() |
f1b299e9a4 | ||
![]() |
658e99df44 | ||
![]() |
6dc5261828 | ||
![]() |
7cd0a7a67e | ||
![]() |
83da4237a4 | ||
![]() |
26d5fe5767 | ||
![]() |
8e9649ec2c | ||
![]() |
6417e850e4 | ||
![]() |
1f8fbfb0a2 | ||
![]() |
d68a27dd67 | ||
![]() |
561429483a | ||
![]() |
0b751ca163 | ||
![]() |
59803821ef | ||
![]() |
52b600deee | ||
![]() |
7798dd35fd | ||
![]() |
2a6ac2ea08 | ||
![]() |
3a0ef7d775 | ||
![]() |
b168148733 | ||
![]() |
2447b25422 | ||
![]() |
7dc5ea5214 | ||
![]() |
43a9ddef08 | ||
![]() |
6976f4d99d | ||
![]() |
85682f7f05 | ||
![]() |
6d162c6987 | ||
![]() |
7ef0685a35 | ||
![]() |
a27dfcd5cb | ||
![]() |
97c20d9b1f | ||
![]() |
cc3604b8f3 | ||
![]() |
bba2b59816 | ||
![]() |
d96d1aecd0 | ||
![]() |
cb4319bee0 | ||
![]() |
9960a37b03 | ||
![]() |
2b488ddb51 | ||
![]() |
3c9318aba0 | ||
![]() |
1b22ca65fc | ||
![]() |
a6c6e2b524 | ||
![]() |
f2c495baa5 | ||
![]() |
2185d2be7c | ||
![]() |
b58855b428 | ||
![]() |
d0eedf7cc1 | ||
![]() |
004c59b00d | ||
![]() |
b9bd5d671a | ||
![]() |
68f14b2efc | ||
![]() |
681e00db85 | ||
![]() |
dccbbc0885 | ||
![]() |
7f9b002f2b | ||
![]() |
50c911c2e3 | ||
![]() |
867e12400e | ||
![]() |
6073f74150 | ||
![]() |
352cf44eda | ||
![]() |
691e0319d1 | ||
![]() |
a4c7eded55 | ||
![]() |
e874e53cdb | ||
![]() |
b27746f0ba | ||
![]() |
c3dd4f61f2 | ||
![]() |
71f888eb36 | ||
![]() |
274cc38e7f | ||
![]() |
6a32ac8313 | ||
![]() |
4bba986b92 | ||
![]() |
4a024db90d | ||
![]() |
490468ca6f | ||
![]() |
571ffca7b5 | ||
![]() |
760787df9f | ||
![]() |
bbcaec9066 | ||
![]() |
348fd02916 | ||
![]() |
d29f691eac | ||
![]() |
1a6ea9c997 | ||
![]() |
6c8b85609c | ||
![]() |
351d0327e5 | ||
![]() |
5288f0d954 | ||
![]() |
6f1d395df3 | ||
![]() |
eec4b3cbdb | ||
![]() |
aa569f8c90 | ||
![]() |
602dce53f2 | ||
![]() |
c40ab3addb | ||
![]() |
3f8e6a273e | ||
![]() |
10abffbf26 | ||
![]() |
bfe587b1ed | ||
![]() |
ee87be7994 | ||
![]() |
245305e3a6 | ||
![]() |
891be69fb2 | ||
![]() |
97aff87071 | ||
![]() |
2028901fa6 | ||
![]() |
a8372855e0 | ||
![]() |
0be36afe71 | ||
![]() |
76b6c86b96 | ||
![]() |
5f358b3fc4 | ||
![]() |
c372a3ca94 | ||
![]() |
7d3b36213b | ||
![]() |
ca27d88ec6 | ||
![]() |
7e565a42d2 | ||
![]() |
2304d269d8 | ||
![]() |
02b1d50432 | ||
![]() |
320b0fbbb4 | ||
![]() |
9e5305eff5 | ||
![]() |
9bef41a0ca | ||
![]() |
aa9a72b787 | ||
![]() |
8d113b5337 | ||
![]() |
d4a2f24a50 | ||
![]() |
2122e8068a | ||
![]() |
6bc9e02e28 | ||
![]() |
8d6329f8d5 | ||
![]() |
e69c81fe7a | ||
![]() |
6c170e5145 | ||
![]() |
d6a257bb44 | ||
![]() |
c28a5c51e2 | ||
![]() |
2fdcd9ee70 | ||
![]() |
15397a75fe | ||
![]() |
978381c1e3 | ||
![]() |
2172aa0a7d | ||
![]() |
55dcdfd7f5 | ||
![]() |
1125569811 | ||
![]() |
1970f14eac | ||
![]() |
ebe8423a2d | ||
![]() |
40585de96b | ||
![]() |
60bf7627bb | ||
![]() |
0cdb11ba82 | ||
![]() |
fc3908ee8e | ||
![]() |
0fd787e4c0 | ||
![]() |
ad95b4acb5 | ||
![]() |
76a95daf5b | ||
![]() |
da883991d0 | ||
![]() |
68e3220e71 | ||
![]() |
ffddd9ef5f | ||
![]() |
12546db205 | ||
![]() |
c1a6626a0b | ||
![]() |
7a3032de08 | ||
![]() |
0ea9ae762e | ||
![]() |
34269131c7 | ||
![]() |
2b3b19eb15 | ||
![]() |
bc9d840e3f | ||
![]() |
3cb1f8e7a1 | ||
![]() |
40134af60a | ||
![]() |
5009ddd5d5 | ||
![]() |
2a6c091ff0 | ||
![]() |
dc4e362a55 | ||
![]() |
3a228a9831 | ||
![]() |
95fe730985 | ||
![]() |
c97ebed936 | ||
![]() |
caddef01d1 | ||
![]() |
f0a7cc9c72 | ||
![]() |
3b50610a4d | ||
![]() |
9a11b80e6b | ||
![]() |
e814aff79b | ||
![]() |
2989f97610 | ||
![]() |
7a204021f1 | ||
![]() |
3b9a7c3223 | ||
![]() |
4a8f772544 | ||
![]() |
a6098f0507 | ||
![]() |
c8731e2fa7 | ||
![]() |
50b218c0d8 | ||
![]() |
5fb4ce9d99 | ||
![]() |
84ceae0591 | ||
![]() |
4e70cdfc60 | ||
![]() |
913e88896b | ||
![]() |
d39a3efc7a | ||
![]() |
9e660973d7 | ||
![]() |
574ead679d | ||
![]() |
c48926c367 | ||
![]() |
ec5289081c | ||
![]() |
98abbfe7bc | ||
![]() |
1722f3f832 | ||
![]() |
7134301f64 | ||
![]() |
aa0a4f8865 | ||
![]() |
a208be1c8a | ||
![]() |
550917bf6e | ||
![]() |
0f46cff353 | ||
![]() |
4beede621b | ||
![]() |
7352e1f61f | ||
![]() |
cc849b756d | ||
![]() |
ed48fa6fac | ||
![]() |
a1a16f5bc9 | ||
![]() |
4de4f572ea | ||
![]() |
dd202311ea | ||
![]() |
68fd353181 | ||
![]() |
467f84187f | ||
![]() |
e3503f4345 | ||
![]() |
364b56a317 | ||
![]() |
b94651811e | ||
![]() |
e5dde6e02c | ||
![]() |
1d10c66a22 | ||
![]() |
5545163768 | ||
![]() |
1ffbb8b39b | ||
![]() |
62ec6ad308 | ||
![]() |
3cbd88f4a6 | ||
![]() |
c670456d47 | ||
![]() |
8b4d738c2e | ||
![]() |
b76f0650d8 | ||
![]() |
7234ed5411 | ||
![]() |
68ef1af276 | ||
![]() |
d17aeb5a47 | ||
![]() |
1d85218c8d | ||
![]() |
b22c83333d | ||
![]() |
c0cba101fa |
|
@ -2,11 +2,15 @@ version: 2
|
|||
jobs:
|
||||
build:
|
||||
machine: # executor type
|
||||
image: ubuntu-2004:202010-01 # # recommended linux image - includes Ubuntu 20.04, docker 19.03.13, docker-compose 1.27.4
|
||||
image: ubuntu-2204:2023.07.2
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Setup docker and buildx
|
||||
command: docker buildx create --use
|
||||
|
||||
- run:
|
||||
name: install dependencies
|
||||
command: sudo apt-get install bash curl
|
||||
|
@ -49,6 +53,24 @@ jobs:
|
|||
command: |
|
||||
curl -s -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=$MAX_TOKEN&suffix=tar.gz" -o GeoLite2-Country.tar.gz
|
||||
tar -xzf GeoLite2-Country.tar.gz --strip-components 1 --wildcards "*.mmdb"
|
||||
|
||||
- run:
|
||||
name: Download and Extract ARM Nebula Binary
|
||||
command: |
|
||||
curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-arm64.tar.gz
|
||||
tar -xzvf nebula-linux-arm64.tar.gz
|
||||
|
||||
- run:
|
||||
name: Rename ARM Nebula Binary
|
||||
command: |
|
||||
mv nebula nebula-arm
|
||||
mv nebula-cert nebula-arm-cert
|
||||
|
||||
- run:
|
||||
name: Download and Extract Nebula Binary
|
||||
command: |
|
||||
curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-amd64.tar.gz
|
||||
tar -xzvf nebula-linux-amd64.tar.gz
|
||||
|
||||
- run:
|
||||
name: Build UI
|
||||
|
@ -122,12 +144,6 @@ workflows:
|
|||
build-all:
|
||||
jobs:
|
||||
- build:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- unstable
|
||||
- buildarm:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
|
|
4
.clabot
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"contributors": ["azukaar", "jwr1", "Jogai", "InterN0te", "catmandx", "revam"],
|
||||
"message": "We require contributors to sign our [Contributor License Agreement](https://github.com/azukaar/Cosmos-Server/blob/master/cla.md). In order for us to review and merge your code, add yourself to the .clabot file as contributor, as a way of signing the CLA."
|
||||
}
|
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
161
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,161 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
enable-beta-ecosystems: true
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "npm dependencies"
|
||||
- package-ecosystem: "bundler"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "bundler dependencies"
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "composer dependencies"
|
||||
- package-ecosystem: "maven"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "maven dependencies"
|
||||
- package-ecosystem: "mix"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "mix dependencies"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "cargo dependencies"
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "gradle dependencies"
|
||||
- package-ecosystem: "nuget"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "nuget dependencies"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "gomod dependencies"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "docker dependencies"
|
||||
- package-ecosystem: "elm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "elm dependencies"
|
||||
- package-ecosystem: "gitsubmodule"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "gitsubmodule library"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "github-actions"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "pip dependencies"
|
||||
- package-ecosystem: "terraform"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "terraform dependencies"
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "pub dependencies"
|
||||
- package-ecosystem: "swift"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "library-updates"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
labels:
|
||||
- "swift dependencies"
|
8
.gitignore
vendored
|
@ -12,4 +12,10 @@ todo.txt
|
|||
LICENCE
|
||||
tokens.json
|
||||
.vscode
|
||||
GeoLite2-Country.mmdb
|
||||
GeoLite2-Country.mmdb
|
||||
dns-blacklist.txt
|
||||
zz_test_config
|
||||
nebula-arm
|
||||
nebula-arm-cert
|
||||
nebula
|
||||
nebula-cert
|
263
LICENCE
|
@ -1,11 +1,256 @@
|
|||
All Rights Reserved
|
||||
Software: Cosmos-Server
|
||||
|
||||
Copyright (c) 2023
|
||||
License: Apache 2.0 with Commons Clause and Anti Tampering Clause
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Licensor: Yann Stepienik
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
||||
“Commons Clause” License Condition v1.0
|
||||
|
||||
The Software is provided to you by the Licensor under the
|
||||
License, as defined below, subject to the following condition.
|
||||
|
||||
Without limiting other conditions in the License, the grant
|
||||
of rights under the License will not include, and the License
|
||||
does not grant to you, the right to Sell the Software.
|
||||
|
||||
For purposes of the foregoing, “Sell” means practicing any or
|
||||
all of the rights granted to you under the License to provide
|
||||
to third parties, for a fee or other consideration (including
|
||||
without limitation fees for hosting or consulting/ support
|
||||
services related to the Software), a product or service whose
|
||||
value derives, entirely or substantially, from the functionality
|
||||
of the Software. Any license notice or attribution required by
|
||||
the License must also include this Commons Clause License
|
||||
Condition notice.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
||||
"Anti Tampering Clause” License Condition v1.0
|
||||
|
||||
Notwithstanding any provision of the Apache License 2.0, if the User
|
||||
(or any party receiving or distributing derivative works, services,
|
||||
or anything of value from the User related to the Software), directly
|
||||
or indirectly, seeks to tamper with, alter, circumvent, or avoid
|
||||
compliance with any subscription, paywall, feature restriction, or any
|
||||
other licensing mechanism built into the Software or its usage, the
|
||||
License granted under the Apache License 2.0 shall automatically and
|
||||
immediately terminate, and access to the Software shall be withdrawn
|
||||
with immediate effect. Upon such termination, any and all rights
|
||||
established under the Apache License 2.0 shall be null and void.
|
||||
|
||||
Tampering includes but is not limited to: (a) removing, disabling,
|
||||
or circumventing any license key or other copy protection mechanism,
|
||||
(b) redistributing parts or all of a feature that was intended
|
||||
to be a paid feature, without keeping the restrictions, limitations,
|
||||
or other licensing mechanisms with it(c) disabling, circumventing, or
|
||||
avoiding any feature of the Software that is intended to enforce usage or
|
||||
copy restrictions, or (d) providing or distributing any information
|
||||
or code that enables disabling, circumvention, or avoidance of any
|
||||
feature of the Software that is intended to enforce usage or copy
|
||||
restrictions.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2020 n8n GmbH
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
BIN
banner.jpg
Normal file
After Width: | Height: | Size: 278 KiB |
16
build.sh
|
@ -1,12 +1,24 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo " ---- Build Cosmos ----"
|
||||
|
||||
rm -rf build
|
||||
|
||||
env GOARCH=arm64 go build -o build/cosmos-arm64 src/*.go
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go build -o build/cosmos src/*.go
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " ---- Build complete, copy assets ----"
|
||||
|
||||
cp -r static build/
|
||||
cp -r GeoLite2-Country.mmdb build/
|
||||
cp nebula-arm-cert nebula-cert nebula-arm nebula build/
|
||||
cp -r Logo.png build/
|
||||
mkdir build/images
|
||||
cp client/src/assets/images/icons/cosmos_gray.png build/cosmos_gray.png
|
||||
|
@ -15,4 +27,6 @@ echo '{' > build/meta.json
|
|||
cat package.json | grep -E '"version"' >> build/meta.json
|
||||
echo ' "buildDate": "'`date`'",' >> build/meta.json
|
||||
echo ' "built from": "'`hostname`'"' >> build/meta.json
|
||||
echo '}' >> build/meta.json
|
||||
echo '}' >> build/meta.json
|
||||
|
||||
echo " ---- copy complete ----"
|
322
changelog.md
|
@ -1,16 +1,322 @@
|
|||
## Version 0.13.1
|
||||
- Fix a security issue with token (thansk @vncloudsco)
|
||||
|
||||
## Version 0.13.0
|
||||
- Display container stacks as a group in the UI
|
||||
- New Delete modal to delete services entirely
|
||||
- Upload custom icons to containers
|
||||
- improve backup file, by splitting cosmos out to a separate docker-compose.yml file
|
||||
- Cosmos-networks now have specific names instead for generic names
|
||||
- Fix issue where search bar reset when deleting volume/network
|
||||
- Fix breadcrumbs in subpaths
|
||||
- Remove graphs from non-admin UI to prevent errors
|
||||
- Rewrite the overwriting container logic to fix race conditions
|
||||
- Edit container user and devices from UI
|
||||
- Fix bug where Cosmos Constellation's UDP ports by a TCP one
|
||||
- Fix a bug with URL screen, where you can't delete a URL when there is a search
|
||||
- Fix issue where negative network rate are reported
|
||||
- Support array command and single device in docker-compose import
|
||||
- Add default alerts... by default (was missing from the default config)
|
||||
- disable few features liks Constellation, Backup and Monitoring when in install mode to reduce logs and prevent issues with the DB
|
||||
|
||||
## Version 0.12.6
|
||||
- Fix a security issue with cross-domain APIs availability
|
||||
|
||||
## Version 0.12.5
|
||||
- Added index on event date for faster query
|
||||
|
||||
## Version 0.12.4
|
||||
- Fix crash with metrics not seeing any network interface
|
||||
|
||||
## Version 0.12.3
|
||||
- Performance update for metrics saving
|
||||
|
||||
## Version 0.12.2
|
||||
- Fix XSS vulnerability in the redirect function (thanks @catmandx)
|
||||
|
||||
## Version 0.12.1
|
||||
- Fix a crash that would occasionally happen since 0.12 the DB is down
|
||||
|
||||
## Version 0.12.0
|
||||
- New real time persisting and optimized metrics monitoring system (RAM, CPU, Network, disk, requests, errors, etc...)
|
||||
- New Dashboard with graphs for metrics, including graphs in many screens such as home, routes and servapps
|
||||
- New customizable alerts system based on metrics in real time, with included preset for anti-crypto mining and anti memory leak
|
||||
- New events manager (improved logs with requests and advanced search)
|
||||
- New notification system
|
||||
- Added Marketplace UI to edit sources, with new display of 3rd party sources
|
||||
- Added a notification when updating a container, renewing certs, etc...
|
||||
- Certificates now renew sooner to avoid Let's Encrypt sending emails about expiring certificates
|
||||
- Added option to disable routes without deleting them
|
||||
- Improved icon loading speed, and added proper placeholder
|
||||
- Marketplace now fetch faster (removed the domain indirection to directly fetch from github)
|
||||
- Integrated a new docker-less mode of functioning for networking
|
||||
- Added a dangerous IP detector that stops sending HTTP response to IPs that are abusing various shields features
|
||||
- Added CORS headers to openID endpoints
|
||||
- Added a button in the servapp page to easily download the docker backup
|
||||
- Added Button to force reset HTTPS cert in settings
|
||||
- Added lazyloading to URL and Servapp pages images
|
||||
- Fixed annoying marketplace screenshot bug (you know what I'm talking about!)
|
||||
- New color slider with reset buttons
|
||||
- Redirect static folder to host if possible
|
||||
- New Homescreen look
|
||||
- Fixed blinking modals issues
|
||||
- Add AutoFocus on Token field for 2FA Authentication (thanks @InterN0te)
|
||||
- Allow Insecure TLS like self-signed certificate for SMTP server (thanks @InterN0te)
|
||||
- Improve display of icons [fixes #121]
|
||||
- Refactored Mongo connection code [fixes #111]
|
||||
- Forward simultaneously TCP and UDP [fixes #122]
|
||||
|
||||
## Version 0.11.3
|
||||
- Fix missing event subscriber on export
|
||||
|
||||
## Version 0.11.2
|
||||
- Improve Docker exports logs
|
||||
|
||||
## Version 0.11.1
|
||||
- fix issue exporting text user node
|
||||
|
||||
## Version 0.11.0
|
||||
- Disable support for X-FORWARDED-FOR incoming header (needs further testing)
|
||||
- Docker export feature for backups on every docker event
|
||||
- Compose Import feature now supports skipping creating existing resources
|
||||
- Compose Import now overwrite containers if they are differents
|
||||
- Added support for cosmos-persistent-env, to persist password when overwriting containers (useful for encrypted or password protected volumes, like databases use)
|
||||
- Fixed bug where import compose would try to revert a previously created volume when errors occurs
|
||||
- Terminal for import now has colours
|
||||
- Fix a bug where ARM CPU would not be able to start Constellation
|
||||
|
||||
## Version 0.10.4
|
||||
- Encode OpenID .well-known to JSON
|
||||
- Fix incompatibility with other apps using .well-known
|
||||
- Secure the OpenID routes that missed the hardening
|
||||
- Added some logs
|
||||
|
||||
## Version 0.10.3
|
||||
- Add missing Constellation logs when creating certs
|
||||
- Ignore empty links in cosmos-compose
|
||||
|
||||
## Version 0.10.2
|
||||
- Fix port in host header
|
||||
|
||||
## Version 0.10.1
|
||||
- Fix an issue where Constellation is stuck if creating a new network is interrupted
|
||||
- Fix a logic issue with the whitelist inbound IPs
|
||||
|
||||
## Version 0.10.0
|
||||
- Added Constellation
|
||||
- DNS Challenge is now used for all certificates when enabled [breaking change]
|
||||
- Rework headers for better compatibility
|
||||
- Improve experience for non-admin users
|
||||
- Fix bug with redirect on logout
|
||||
- Added OverwriteHostHeader to routes to override the host header sent to the target app
|
||||
- Added WhitelistInboundIPs to routes to filter incoming requests based on IP per URL
|
||||
|
||||
> **Note: If you use the ARM (:latest-arm) you need to manually update to using the :latest tag instead**
|
||||
|
||||
## Version 0.9.20 - 0.9.21
|
||||
- Add option to disable CORS hardening (with empty value)
|
||||
|
||||
## Version 0.9.19
|
||||
- Add country whitelist option to geoblocker
|
||||
- No countries blocked by default anymore
|
||||
- Merged ARM and AMD into a single docker tag (latest)
|
||||
- Update to Debian 12
|
||||
- Fix issue with Contradictory scheme headers
|
||||
- Fix issue where non-admin users cant see Servapp on the homepage
|
||||
|
||||
## Version 0.9.18
|
||||
- Typo with x-forwarded-host
|
||||
|
||||
## Version 0.9.17
|
||||
- Upgraded to Lego 4.13.3 (support for Google Domain)
|
||||
- Add VerboseForwardHeader to URL Config to allow to transfer more sensitive header to target app
|
||||
- App DisableHeaderHardening to allow disabling header hardening for specific apps
|
||||
|
||||
## Version 0.9.16
|
||||
- Small redirection bug fix
|
||||
|
||||
## Version 0.9.15
|
||||
- Check background extension on upload is an image
|
||||
- Update Docker for security patch
|
||||
- Check redirect target is local
|
||||
- Improve OpenID client secret generation
|
||||
|
||||
## Version 0.9.14
|
||||
- Check network mode before pruning networks
|
||||
|
||||
## Version 0.9.13
|
||||
- Fix issue with duplicated ports in network tab of servapps (because it shows the IPV4 and the IPV6 ports)
|
||||
|
||||
## Version 0.9.12
|
||||
- Add integration to the `docker login` credentials store
|
||||
- Smart-shield now works with different budgets per routes, so that requests on a permissive route don't count as requests on a strict route
|
||||
- Fix an issue where users would never receive permanent bans from the shield
|
||||
|
||||
## Version 0.9.11
|
||||
- Add support for port ranges in cosmos-compose
|
||||
- Fix bug where multiple host port to the same container would override each other
|
||||
- Port display on Servapp tab was inverted
|
||||
- Fixed Network screen to support complex port mappings
|
||||
- Add support for protocol in cosmos-compose port exposing logic
|
||||
- Add support for relative bind path in docker-compose import
|
||||
- Fix environment vars and labels containing multiple equals (@jwr1)
|
||||
- Fix link to Other Setups page (@jwr1)
|
||||
|
||||
## Version 0.9.10
|
||||
- Never ban gateway ips
|
||||
- Prevent deleting networks if there's an error on disconnect
|
||||
- Disabling network pruning now also disables cleaning up Cosmos networks
|
||||
|
||||
## Version 0.9.9
|
||||
- Add new filters for routes based on method, query strings and headers (missing UI)
|
||||
|
||||
## Version 0.9.1 > 0.9.8
|
||||
- Fix subdomain logic for composed TLDs
|
||||
- Add option for custom wildcard domains
|
||||
- Fix domain depupe logic
|
||||
- Add import button in market
|
||||
- Update LEGO
|
||||
- Fix issue with hot-reloading between HTTP and HTTPS
|
||||
- Fix loading bar in container overview page
|
||||
- Flush Etag cache on restart
|
||||
- Add timeout to icon fetching
|
||||
- Bootstrap containers when adding new routes to them
|
||||
- Remove headers from origin server to prevent duplicates
|
||||
- Add licence
|
||||
|
||||
## Version 0.9.0
|
||||
- Rewrote the entire HTTPS / DNS challenge system to be more robust and easier to use
|
||||
- Let's Encrypt Certificate is now saved in the config file
|
||||
- Cosmos will re-use previous certificate if renewal fails
|
||||
- Self-Signed certificate will now renew on expiry
|
||||
- If LE fails to renew, Cosmos will fallback to self-signed certificate
|
||||
- If LE fails to renew, Cosmos will display a warning on the home page
|
||||
- If certificate have more hostnames than required, Cosmos will not request a new certificate to prevent LE rate limiting issues
|
||||
- No more restart needed when changing config, adding route, installing apps, etc...
|
||||
- Change auto mapper to keep existing user definied ports
|
||||
- When using a subdomain as the main Cosmos domain, UseWildcardCertificate will now request the root domain instead of *.sub.domain.com
|
||||
- open id now supports multiple redirect uri (comma separated)
|
||||
- add manual restart button in config
|
||||
- New simpler Homepage style, with a toggle for expanded details homepage style in the config
|
||||
- add a button on the first setup screen to perform a clean install
|
||||
|
||||
## version 0.8.1 -> 0.8.10
|
||||
- Added new automatic Docker mapping feature (for people not using (sub)domains)
|
||||
- Added guardrails to prevent Let's Encrypt from failing to initialize when adding wrong domains
|
||||
- Add search bar on the marketplace
|
||||
- App store image size issue
|
||||
- Display more tags in the market
|
||||
- Fixed wrong x-forwarded-proto header
|
||||
- Add installer option for hostname prefix/suffix
|
||||
- Fix minor issue with inconsistent password on market installer
|
||||
- Fixed issue where home page was https:// links on http only servers
|
||||
- Improved setup flow for setting up hostname and HTTPS
|
||||
- Fixed auto-update on ARM based CPU
|
||||
- Fix issue with email links
|
||||
- HideFromDashboard option on routes
|
||||
- Fix docker compose import issue with uppercase volumes
|
||||
|
||||
## Version 0.8.0
|
||||
- Custmizable homepage / theme colors
|
||||
- Auto-connect containers that have SERVAPP routes attached to them. aka. you do not need to "force secure" containers anymore
|
||||
- Manually create smaller docker subnets when using force secure / links to not hit IP range limit
|
||||
- Self-heal containers that have lost their network configurations
|
||||
- Stop showing Docker not connected when first loading status in new installs
|
||||
- Add a cosmos-icon label to containers to change the icon in the UI
|
||||
- Add privacy settings to external links
|
||||
- Force secure is now called "isolate network" to make it more clear, but does the same thing
|
||||
- allow iframes in the same subdomain as the app to fix wordpress compatibility
|
||||
|
||||
## Version 0.7.1 -> 0.7.10
|
||||
- Fix issue where multiple DBs get created at the setup
|
||||
- Add more special characters to be used for password validation
|
||||
- Add configurable default data path for binds
|
||||
- Remove Redirects from home page
|
||||
- Fix compat with non-HTTP protocol like WebDAV (for Nextcloud for example)
|
||||
- Fix regression with DNS wildcards certificates
|
||||
- Fix issue with the installer when changing both the labels and the volumes
|
||||
- Fix regression where DNS keys don't appear in the config page after being changed
|
||||
- Fix typo on "updating ServApp" message
|
||||
|
||||
## Version 0.7.0
|
||||
- Add Cosmos App Market!
|
||||
- Reforged the DNS CHallenge to be more user friendly. You can select your DNS provider in a list, and it will guide you through the process with the right fields to set (directly in the UI). No more env variables to set!
|
||||
- Fix issue with docker compose timeout healthcheck as string, inverted ports, and supports for uid:gid syntax in user
|
||||
- Fix for SELinux compatibility
|
||||
- Fix false-negative error message on login screen when SMTP is disabled
|
||||
|
||||
## Version 0.6.1 - 0.6.4
|
||||
- Workaround for Docker-compose race condition in Debian
|
||||
- Fix ARM based MongDB image for older ARM Devices
|
||||
- Fix issue with missing auth key with OpenID
|
||||
|
||||
## Version 0.6.0
|
||||
- OpenID support!
|
||||
- Add hostname check when adding new routes to Cosmos
|
||||
- Add hostname check on new Install
|
||||
- Fix missing save button for network mode
|
||||
|
||||
## Version 0.5.11
|
||||
- Improve docker-compose import support for alternative syntaxes
|
||||
- Improve docker service creation when using force secure label (fixes few containers not liking restarting too fast when created)
|
||||
- Add toggle for using insecure HTTPS targets (fixes Unifi controller)
|
||||
|
||||
## Version 0.5.1 -> 0.5.10
|
||||
- Add Wilcard certificates support
|
||||
- Auto switch to Mongo 4 if CPU has no ADX
|
||||
- Improve setup for certificates on new install
|
||||
- Fix issue docker compose import labels and networks array
|
||||
- Fix issue docker compose one-service syntax
|
||||
- Fix issue with docker network mode not supporting hostname
|
||||
- Fix an issue with the shield and the docker networking
|
||||
- Fix issue with network namespace
|
||||
- Fixed issue with a Docker bug preventing re-creating a container with a network mode as container (https://github.com/portainer/portainer/issues/2657)
|
||||
- Silent error on favicon fetching
|
||||
- Create Servapp step 1: make name / image required
|
||||
|
||||
## Version 0.5.0
|
||||
- Add Terminal to containers
|
||||
- Add "Create ServApp"
|
||||
- Add support for importing Docker Compose
|
||||
- Improved icon fetching
|
||||
- Change Home background and style (especially fixing the awckward light theme)
|
||||
- Fixed 2 bugs with the smart shield, that made it too strict
|
||||
- Fixed issues that prevented from login in with different hostnames
|
||||
- Added more info on the shield when blocking someone
|
||||
- Fixed issue where the UI would have missing icon images
|
||||
- Fixed Homepage showing stopped containers
|
||||
- Fixed bug where you can't save changes on the URLs Screen
|
||||
|
||||
## Version 0.4.3
|
||||
- Fix for exposing routes from the details page
|
||||
|
||||
## Version 0.4.2
|
||||
- Fix when using custom port and logging in (Isssue #10)
|
||||
|
||||
## Version 0.4.1
|
||||
- Fix small UI issues
|
||||
- Fix HTTP login
|
||||
|
||||
## Version 0.4.0
|
||||
- Protect server against direct IP access
|
||||
- Improvements to installer to make it more robust
|
||||
- Fix bug where you can't complete the setup if you don't have a database
|
||||
- When re-creating a container to edit it, restore the previous container if the edit is not succesful
|
||||
- Stop / Start / Restart / Remove / Kill containers
|
||||
- List / Delete / Create Volumes - done
|
||||
- List / Delete / Create Networks - done
|
||||
- Container Logs Viewer - done
|
||||
- Edit Container Details and Docker Settings - done
|
||||
- Set Labels / Env variable to containers - done
|
||||
- (De)Attach networks to containers - done
|
||||
- (De)Attach volumes to containers - done
|
||||
- List / Delete / Create Volumes
|
||||
- List / Delete / Create Networks
|
||||
- Container Logs Viewer
|
||||
- Edit Container Details and Docker Settings
|
||||
- Set Labels / Env variables on containers
|
||||
- (De)Attach networks to containers
|
||||
- (De)Attach volumes to containers
|
||||
|
||||
## Version 0.3.1 -> 0.3.5
|
||||
- Fix UI issue with long name in home
|
||||
- Fix ARM docker image
|
||||
- Add more validation for Let's Encrypt
|
||||
- Prevent browser from auto-filling password in config page
|
||||
- Revert to HTTP when Let's Encrypt fails to initialize
|
||||
|
||||
## Version 0.3.0
|
||||
- Implement 2 FA
|
||||
|
@ -65,4 +371,4 @@
|
|||
- Ports is now freetype, in case container does not expose any
|
||||
- Container picker now tries to pick the best port as default
|
||||
- Hostname now default to container name
|
||||
- Additional UI improvements
|
||||
- Additional UI improvements
|
||||
|
|
21
cla.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
Cosmos Software Grant and Contributor License Agreement (“Agreement”)
|
||||
|
||||
This agreement is based on the Apache Software Foundation Contributor License Agreement. (v r190612)
|
||||
|
||||
Thank you for your interest in dba Cosmos (“Cosmos”). In order to clarify the intellectual property license granted with Contributions from any person or entity, Cosmos must have a Contributor License Agreement (CLA) on file that has been agreed to by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Cosmos and its users; it does not change your rights to use your own Contributions for any other purpose. This Agreement allows an individual to contribute to Cosmos on that individual’s own behalf, or an entity (the “Corporation”) to submit Contributions to Cosmos, to authorize Contributions submitted by its designated employees to Cosmos, and to grant copyright and patent licenses thereto.
|
||||
|
||||
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Cosmos. Except for the license granted herein to Cosmos and recipients of software distributed by Cosmos, You reserve all right, title, and interest in and to Your Contributions.
|
||||
|
||||
Definitions. “You” (or “Your”) shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Cosmos. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. “Contribution” shall mean any work, as well as any modifications or additions to an existing work, that is intentionally submitted by You to Cosmos for inclusion in, or documentation of, any of the products owned or managed by Cosmos (the “Work”). For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to Cosmos or its representatives, including but not limited to communication on electronic mailing lists, source code control systems (such as GitHub), and issue tracking systems that are managed by, or on behalf of, Cosmos for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as “Not a Contribution.”
|
||||
|
||||
Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Yann Stepienik and to recipients of software distributed by Cosmos' owners a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
|
||||
|
||||
Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Yann Stepienik and to recipients of software distributed by Yann Stepienik a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) were submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
You represent that You are legally entitled to grant the above license. If You are an individual, and if Your employer(s) has rights to intellectual property that you create that includes Your Contributions, you represent that You have received permission to make Contributions on behalf of that employer, or that Your employer has waived such rights for your Contributions to Cosmos. If You are a Corporation, any individual who makes a contribution from an account associated with You will be considered authorized to Contribute on Your behalf.
|
||||
|
||||
You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
|
||||
|
||||
You are not expected to provide support for Your Contributions,except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
Should You wish to submit work that is not Your original creation, You may submit it to Cosmos separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as “Submitted on behalf of a third-party: [named here]”.
|
|
@ -4,23 +4,81 @@ import * as React from 'react';
|
|||
import ThemeCustomization from './themes';
|
||||
import ScrollTop from './components/ScrollTop';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import {Alert} from '@mui/material';
|
||||
import {Alert, Box} from '@mui/material';
|
||||
import logo from './assets/images/icons/cosmos.png';
|
||||
|
||||
import { setSnackit } from './api/wrap';
|
||||
import * as API from './api';
|
||||
|
||||
import { setSnackit, snackit } from './api/wrap';
|
||||
import { DisconnectOutlined } from '@ant-design/icons';
|
||||
|
||||
// ==============================|| APP - THEME, ROUTER, LOCAL ||============================== //
|
||||
|
||||
const LoadingAnimation = () => (
|
||||
<div className="loader">
|
||||
<div className="dot"></div>
|
||||
<div className="dot"></div>
|
||||
<div className="dot"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export let SetPrimaryColor = () => {};
|
||||
export let SetSecondaryColor = () => {};
|
||||
export let GlobalPrimaryColor = '';
|
||||
export let GlobalSecondaryColor = '';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [message, setMessage] = React.useState('');
|
||||
const [severity, setSeverity] = React.useState('error');
|
||||
const [statusLoaded, setStatusLoaded] = React.useState(false);
|
||||
const [PrimaryColor, setPrimaryColor] = React.useState(API.PRIMARY_COLOR);
|
||||
const [SecondaryColor, setSecondaryColor] = React.useState(API.SECONDARY_COLOR);
|
||||
const [timeoutError, setTimeoutError] = React.useState(false);
|
||||
|
||||
SetPrimaryColor = (color) => {
|
||||
setPrimaryColor(color);
|
||||
GlobalPrimaryColor = color;
|
||||
}
|
||||
|
||||
SetSecondaryColor = (color) => {
|
||||
setSecondaryColor(color);
|
||||
GlobalSecondaryColor = color;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
setTimeoutError(true);
|
||||
}, 10000
|
||||
)
|
||||
API.getStatus(true).then((r) => {
|
||||
clearTimeout(timeout);
|
||||
if(r == "NOT_AVAILABLE") {
|
||||
setTimeoutError(true);
|
||||
}
|
||||
else if(r) {
|
||||
setStatusLoaded(true);
|
||||
}
|
||||
setPrimaryColor(API.PRIMARY_COLOR);
|
||||
setSecondaryColor(API.SECONDARY_COLOR);
|
||||
|
||||
}).catch(() => {
|
||||
clearTimeout(timeout);
|
||||
setStatusLoaded(true);
|
||||
setPrimaryColor(API.PRIMARY_COLOR);
|
||||
setSecondaryColor(API.SECONDARY_COLOR);
|
||||
});
|
||||
}, []);
|
||||
|
||||
setSnackit((message, severity='error') => {
|
||||
setMessage(message);
|
||||
setOpen(true);
|
||||
setSeverity(severity);
|
||||
})
|
||||
return (
|
||||
<ThemeCustomization>
|
||||
|
||||
return statusLoaded ?
|
||||
<ThemeCustomization PrimaryColor={PrimaryColor} SecondaryColor={SecondaryColor}>
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={5000}
|
||||
|
@ -35,7 +93,18 @@ const App = () => {
|
|||
<Routes />
|
||||
</ScrollTop>
|
||||
</ThemeCustomization>
|
||||
)
|
||||
|
||||
: <div>
|
||||
<Box sx={{ position: 'fixed', top: 0, bottom: 0, left: 0, right: 0, display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
|
||||
{/* <img src={logo} style={{ display:'inline', height: '200px'}} className='pulsing' /> */}
|
||||
{!timeoutError && <LoadingAnimation />}
|
||||
{timeoutError && <DisconnectOutlined style={{
|
||||
fontSize: '200px',
|
||||
color: 'red',
|
||||
}}/>}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { resolve } from 'eslint-import-resolver-typescript';
|
||||
import configDemo from './demo.config.json';
|
||||
|
||||
interface Route {
|
||||
|
|
|
@ -34,12 +34,14 @@ function restart() {
|
|||
}
|
||||
|
||||
function canSendEmail() {
|
||||
return wrap(fetch('/cosmos/api/can-send-email', {
|
||||
return fetch('/cosmos/api/can-send-email', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}).then((response) => {
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
async function rawUpdateRoute(routeName: string, operation: Operation, newRoute?: Route): Promise<void> {
|
||||
|
@ -81,6 +83,15 @@ async function addRoute(newRoute: Route): Promise<void> {
|
|||
return rawUpdateRoute("", 'add', newRoute);
|
||||
}
|
||||
|
||||
function getBackup() {
|
||||
return wrap(fetch('/cosmos/api/get-backup', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export {
|
||||
get,
|
||||
set,
|
||||
|
@ -92,4 +103,5 @@ export {
|
|||
deleteRoute,
|
||||
addRoute,
|
||||
canSendEmail,
|
||||
getBackup,
|
||||
};
|
131
client/src/api/constellation.demo.tsx
Normal file
|
@ -0,0 +1,131 @@
|
|||
import wrap from './wrap';
|
||||
|
||||
function list() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"data": [
|
||||
{
|
||||
"nickname": "admin",
|
||||
"deviceName": "phone",
|
||||
"publicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\naACf/...=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
|
||||
"ip": "192.168.201.4/24",
|
||||
"isLighthouse": false,
|
||||
"isRelay": true,
|
||||
"publicHostname": "",
|
||||
"port": "4242",
|
||||
"blocked": false,
|
||||
"fingerprint": "..."
|
||||
},
|
||||
{
|
||||
"nickname": "admin",
|
||||
"deviceName": "laptop",
|
||||
"publicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\n78l4nDEB0+.../36YBQk7dkwg+.=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
|
||||
"ip": "192.168.201.5/24",
|
||||
"isLighthouse": false,
|
||||
"isRelay": true,
|
||||
"publicHostname": "",
|
||||
"port": "4242",
|
||||
"blocked": false,
|
||||
"fingerprint": "..."
|
||||
},
|
||||
{
|
||||
"nickname": "Martha",
|
||||
"deviceName": "pink phone",
|
||||
"publicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\naACf/..=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
|
||||
"ip": "192.168.201.6/24",
|
||||
"isLighthouse": false,
|
||||
"isRelay": true,
|
||||
"publicHostname": "",
|
||||
"port": "4242",
|
||||
"blocked": false,
|
||||
"fingerprint": "..."
|
||||
}
|
||||
],
|
||||
"status": "OK"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function addDevice(device) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"data": {
|
||||
"CA": "-----BEGIN NEBULA CERTIFICATE-----\....\n+dfE+ikL8jUh/n+C+....\....\nZon/Dw==\n-----END NEBULA CERTIFICATE-----\n",
|
||||
"Config": "constellation_api_key: ...\nconstellation_device_name: test\nconstellation_local_dns_overwrite: true\nconstellation_local_dns_overwrite_address: 192.168.201.1\nconstellation_public_hostname: \"\"\nfirewall:\n conntrack:\n default_timeout: 10m\n tcp_timeout: 12m\n udp_timeout: 3m\n inbound:\n - host: any\n port: any\n proto: any\n inbound_action: drop\n outbound:\n - host: any\n port: any\n proto: any\n outbound_action: drop\nlighthouse:\n am_lighthouse: false\n hosts:\n - 192.168.201.1\n interval: 60\nlisten:\n host: 0.0.0.0\n port: \"4242\"\nlogging:\n format: text\n level: info\npki:\n blocklist: []\n ca: |\n -----BEGIN NEBULA CERTIFICATE-----\n ...\n +dfE+ikL8jUh/n+C+...\n .\n Zon/Dw==\n -----END NEBULA CERTIFICATE-----\n cert: |\n -----BEGIN NEBULA CERTIFICATE-----\n CmIKBHRlc3QSCoeSo4UMgP7//..\n ...+QwZSiBxLdKhjkCH+.../..\n ./hfL+....\n ..==\n -----END NEBULA CERTIFICATE-----\n key: |\n -----BEGIN NEBULA X25519 PRIVATE KEY-----\n nS39dWX7uo1rhTvP2yl2XonGx3fWEkpk+43thNrMu7U=\n -----END NEBULA X25519 PRIVATE KEY-----\npunchy:\n punch: true\n respond: true\nrelay:\n am_relay: false\n relays:\n - 192.168.201.1\n use_relays: true\nstatic_host_map:\n 192.168.201.1:\n - vpn.domain.com:4242\ntun:\n dev: nebula1\n disabled: false\n drop_local_broadcast: false\n drop_multicast: false\n mtu: 1300\n routes: []\n tx_queue: 500\n unsafe_routes: []\n",
|
||||
"DeviceName": "test",
|
||||
"IP": "192.168.201.7/24",
|
||||
"IsLighthouse": false,
|
||||
"IsRelay": true,
|
||||
"LighthousesList": [],
|
||||
"Nickname": "admin",
|
||||
"Port": "4242",
|
||||
"PrivateKey": "-----BEGIN NEBULA CERTIFICATE-----\...//w8o3ZaFqQYwhdGFuAY6IGXmYRCr3z932Y....w\..==\n-----END NEBULA CERTIFICATE-----\n",
|
||||
"PublicHostname": "",
|
||||
"PublicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\nnS39dWX...hTvP......+43thNrMu7U=\n-----END NEBULA X25519 PRIVATE KEY-----\n"
|
||||
},
|
||||
"status": "OK"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function restart() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function reset() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"data": "pki:\n ca: /config/ca.crt\n cert: /config/cosmos.crt\n key: /config/cosmos.key\n blocklist: []\nstatic_host_map:\n 192.168.201.1:\n - vpn.domain.com:4242\nlighthouse:\n am_lighthouse: true\n interval: 60\n hosts: []\nlisten:\n host: 0.0.0.0\n port: 4242\npunchy:\n punch: true\n respond: true\nrelay:\n am_relay: true\n use_relays: true\n relays: []\ntun:\n disabled: false\n dev: nebula1\n drop_local_broadcast: false\n drop_multicast: false\n tx_queue: 500\n mtu: 1300\n routes: []\n unsafe_routes: []\nlogging:\n level: info\n format: text\nfirewall:\n outbound_action: drop\n inbound_action: drop\n conntrack:\n tcp_timeout: 12m\n udp_timeout: 3m\n default_timeout: 10m\n outbound:\n - port: any\n proto: any\n host: any\n inbound:\n - port: any\n proto: any\n host: any\n",
|
||||
"status": "OK"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"data": "Some logs...",
|
||||
"status": "OK"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function connect(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function block(nickname, devicename, block) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
addDevice,
|
||||
restart,
|
||||
getConfig,
|
||||
getLogs,
|
||||
reset,
|
||||
connect,
|
||||
block,
|
||||
};
|
110
client/src/api/constellation.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
import wrap from './wrap';
|
||||
|
||||
function list() {
|
||||
return wrap(fetch('/cosmos/api/constellation/devices', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function addDevice(device) {
|
||||
return wrap(fetch('/cosmos/api/constellation/devices', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(device),
|
||||
}))
|
||||
}
|
||||
|
||||
function restart() {
|
||||
return wrap(fetch('/cosmos/api/constellation/restart', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
function reset() {
|
||||
return wrap(fetch('/cosmos/api/constellation/reset', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
return wrap(fetch('/cosmos/api/constellation/config', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
function getLogs() {
|
||||
return wrap(fetch('/cosmos/api/constellation/logs', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
function connect(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
fetch('/cosmos/api/constellation/connect', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body: reader.result,
|
||||
})
|
||||
.then(response => {
|
||||
// Add additional response handling here if needed.
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
// Handle the error.
|
||||
reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error('Failed to read the file.'));
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
function block(nickname, devicename, block) {
|
||||
return wrap(fetch(`/cosmos/api/constellation/block`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
nickname, devicename, block
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
addDevice,
|
||||
restart,
|
||||
getConfig,
|
||||
getLogs,
|
||||
reset,
|
||||
connect,
|
||||
block,
|
||||
};
|
240
client/src/api/container.demo.json
Normal file
|
@ -0,0 +1,240 @@
|
|||
{
|
||||
"data": {
|
||||
"Id": "9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a",
|
||||
"Created": "2023-05-07T16:14:53.9905374Z",
|
||||
"Path": "/init",
|
||||
"Args": [],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 6587,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2023-05-08T15:17:06.69445Z",
|
||||
"FinishedAt": "2023-05-07T17:40:51.0172716Z"
|
||||
},
|
||||
"Image": "sha256:ef697cc05e50849d738bd9e83a269db982997be7b7d8f851c84640e1c9a8c1c4",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a-json.log",
|
||||
"Name": "/jellyfin",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "default",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 0,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": null,
|
||||
"BlkioDeviceWriteBps": null,
|
||||
"BlkioDeviceReadIOps": null,
|
||||
"BlkioDeviceWriteIOps": null,
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": null,
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/51d2becddce22ab73b3ce9bf4acecc98ba3e32e131d0ba7a038598988e58f5e1-init/diff:/var/lib/docker/overlay2/62b6f245e00f4a30491f842e6baf28c8a5162a47a37b1ecb52f8cf3946cd87fc/diff:/var/lib/docker/overlay2/cc6b084bfce23f3e2d19f4b0724265185127583488fbdf9d319bba41af794f46/diff:/var/lib/docker/overlay2/0ce4d4e04f10459993b1887a2f7410571e0e155736baa9f14be6d7bcacfc66cf/diff:/var/lib/docker/overlay2/5af33d768817f5c4c5f120d2b203f3e4fcb70ec9ececdccd36ab4bc678de6c00/diff:/var/lib/docker/overlay2/5e30ac5dd70a3275bd17a906bc95372995ac449346f7bc0e534307e4b0847ee6/diff:/var/lib/docker/overlay2/67570d82c710f15a56f1272c435ae7123056a372cd8ba37430ca9cd74d71ff79/diff:/var/lib/docker/overlay2/ca55695aef06ad37a18d3d773826dec5e00fac8185a69e59705e81af7eaa5b09/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/51d2becddce22ab73b3ce9bf4acecc98ba3e32e131d0ba7a038598988e58f5e1/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/51d2becddce22ab73b3ce9bf4acecc98ba3e32e131d0ba7a038598988e58f5e1/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/51d2becddce22ab73b3ce9bf4acecc98ba3e32e131d0ba7a038598988e58f5e1/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "MyVolume",
|
||||
"Source": "/var/lib/docker/volumes/MyVolume/_data",
|
||||
"Destination": "/config",
|
||||
"Driver": "local",
|
||||
"Mode": "",
|
||||
"RW": true,
|
||||
"Propagation": ""
|
||||
}
|
||||
],
|
||||
"Config": {
|
||||
"Hostname": "/jellyfin",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"ExposedPorts": {
|
||||
"8096/tcp": {},
|
||||
"8920/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"HOME=/root",
|
||||
"LANGUAGE=en_US.UTF-8",
|
||||
"LANG=en_US.UTF-8",
|
||||
"TERM=xterm",
|
||||
"S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0",
|
||||
"S6_VERBOSITY=1",
|
||||
"S6_STAGE2_HOOK=/docker-mods",
|
||||
"LSIO_FIRST_PARTY=true",
|
||||
"NVIDIA_DRIVER_CAPABILITIES=compute,video,utility"
|
||||
],
|
||||
"Cmd": null,
|
||||
"Image": "lscr.io/linuxserver/jellyfin:latest",
|
||||
"Volumes": {
|
||||
"/config": {}
|
||||
},
|
||||
"WorkingDir": "/",
|
||||
"Entrypoint": [
|
||||
"/init"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"build_version": "Linuxserver.io version:- 10.8.9-1-ls203 Build-date:- 2023-03-09T03:55:22+01:00",
|
||||
"hello": "world",
|
||||
"maintainer": "thelamer 2",
|
||||
"org.opencontainers.image.authors": "linuxserver.io",
|
||||
"org.opencontainers.image.created": "2023-03-09T03:55:22+01:00",
|
||||
"org.opencontainers.image.description": "[Jellyfin](https://jellyfin.github.io/) is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it.",
|
||||
"org.opencontainers.image.documentation": "https://docs.linuxserver.io/images/docker-jellyfin",
|
||||
"org.opencontainers.image.licenses": "GPL-3.0-only",
|
||||
"org.opencontainers.image.ref.name": "ba55178b00972a2fbc8e7eec83e47039bfc07cae",
|
||||
"org.opencontainers.image.revision": "ba55178b00972a2fbc8e7eec83e47039bfc07cae",
|
||||
"org.opencontainers.image.source": "https://github.com/linuxserver/docker-jellyfin",
|
||||
"org.opencontainers.image.title": "Jellyfin",
|
||||
"org.opencontainers.image.url": "https://github.com/linuxserver/docker-jellyfin/packages",
|
||||
"org.opencontainers.image.vendor": "linuxserver.io",
|
||||
"org.opencontainers.image.version": "10.8.9-1-ls203"
|
||||
}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "89ef2aa8a50584797af8ef19750429337f78b8a072eb71ea3cc8a332011199d9",
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"Ports": {
|
||||
"8096/tcp": null,
|
||||
"8920/tcp": null
|
||||
},
|
||||
"SandboxKey": "/var/run/docker/netns/89ef2aa8a505",
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "7a376127fcbc669a14fef2e56fbfd761130456a144f548030f2293dcb5d150c8",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.4",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "02:42:ac:11:00:04",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"NetworkID": "d1d39f3e484527f0a22f72275309a6853ea5e84b64c726d75ab3735e5b316eeb",
|
||||
"EndpointID": "7a376127fcbc669a14fef2e56fbfd761130456a144f548030f2293dcb5d150c8",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.4",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "02:42:ac:11:00:04",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": "OK"
|
||||
}
|
|
@ -21,7 +21,7 @@
|
|||
"Name": "Jellyfin",
|
||||
"Description": "Expose Jellyfin to the internet",
|
||||
"UseHost": true,
|
||||
"Host": "play.my-server.com",
|
||||
"Host": "play.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 14400000,
|
||||
|
@ -50,7 +50,7 @@
|
|||
"Name": "Documents Folder",
|
||||
"Description": "Share my Documents",
|
||||
"UseHost": true,
|
||||
"Host": "doc.my-server.com",
|
||||
"Host": "documents.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 14400000,
|
||||
|
@ -76,10 +76,10 @@
|
|||
"BlockAPIAbuse": false
|
||||
},
|
||||
{
|
||||
"Name": "navidrome",
|
||||
"Name": "Navidrome",
|
||||
"Description": "Expose navidrome to the internet",
|
||||
"UseHost": true,
|
||||
"Host": "music.my-server.com",
|
||||
"Host": "navidrome.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 0,
|
||||
|
@ -108,7 +108,7 @@
|
|||
"Name": "Plex",
|
||||
"Description": "Expose Plex to the internet",
|
||||
"UseHost": true,
|
||||
"Host": "plex.my-server.com",
|
||||
"Host": "plex.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 0,
|
||||
|
@ -137,7 +137,7 @@
|
|||
"Name": "Radarr",
|
||||
"Description": "Expose Radarr to the internet",
|
||||
"UseHost": true,
|
||||
"Host": "dl-movies.my-server.com",
|
||||
"Host": "radarr.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 30000,
|
||||
|
@ -166,7 +166,7 @@
|
|||
"Name": "Sonarr",
|
||||
"Description": "Expose Sonarr to the internet",
|
||||
"UseHost": true,
|
||||
"Host": "dl-series.my-server.com",
|
||||
"Host": "sonarr.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 30000,
|
||||
|
@ -195,7 +195,7 @@
|
|||
"Name": "nzbget",
|
||||
"Description": "Expose nzbget to the internet",
|
||||
"UseHost": true,
|
||||
"Host": "dl.my-server.com",
|
||||
"Host": "nzbget.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 999999999,
|
||||
|
@ -224,7 +224,7 @@
|
|||
"Name": "photoprism",
|
||||
"Description": "Expose photoprism to the internet",
|
||||
"UseHost": true,
|
||||
"Host": "photos.my-server.com",
|
||||
"Host": "photoprism.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 45000,
|
||||
|
@ -253,7 +253,7 @@
|
|||
"Name": "Owncloud",
|
||||
"Description": "Expose Owncloud to the internet",
|
||||
"UseHost": true,
|
||||
"Host": "cloud.my-server.com",
|
||||
"Host": "owncloud.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 0,
|
||||
|
@ -282,7 +282,7 @@
|
|||
"Name": "Lidarr",
|
||||
"Description": "Expose Lidarr to the internet",
|
||||
"UseHost": true,
|
||||
"Host": "dl-music.my-server.com",
|
||||
"Host": "lidarr.xxxxxxxxxxxxx",
|
||||
"UsePathPrefix": false,
|
||||
"PathPrefix": "",
|
||||
"Timeout": 30000,
|
||||
|
@ -309,9 +309,86 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"Hostname": "my-server.com",
|
||||
"Hostname": "xxxxxxxxxxxxx",
|
||||
"SSLEmail": "myemail@gmail.com"
|
||||
},
|
||||
"MonitoringAlerts": {
|
||||
"Anti Crypto-Miner": {
|
||||
"Name": "Anti Crypto-Miner",
|
||||
"Enabled": true,
|
||||
"Period": "daily",
|
||||
"TrackingMetric": "cosmos.system.docker.cpu.*",
|
||||
"Condition": {
|
||||
"Operator": "gt",
|
||||
"Value": 80
|
||||
},
|
||||
"Actions": [
|
||||
{
|
||||
"Type": "notification",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "email",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "stop",
|
||||
"Target": ""
|
||||
}
|
||||
],
|
||||
"LastTriggered": "0001-01-01T00:00:00Z",
|
||||
"Throttled": false,
|
||||
"Severity": "warn"
|
||||
},
|
||||
"Anti Memory Leak": {
|
||||
"Name": "Anti Memory Leak",
|
||||
"Enabled": true,
|
||||
"Period": "daily",
|
||||
"TrackingMetric": "cosmos.system.docker.ram.*",
|
||||
"Condition": {
|
||||
"Percent": true,
|
||||
"Operator": "gt",
|
||||
"Value": 80
|
||||
},
|
||||
"Actions": [
|
||||
{
|
||||
"Type": "notification",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "email",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "stop",
|
||||
"Target": ""
|
||||
}
|
||||
],
|
||||
"LastTriggered": "0001-01-01T00:00:00Z",
|
||||
"Throttled": false,
|
||||
"Severity": "warn"
|
||||
},
|
||||
"Disk Full Notification": {
|
||||
"Name": "Disk Full Notification",
|
||||
"Enabled": true,
|
||||
"Period": "latest",
|
||||
"TrackingMetric": "cosmos.system.disk./",
|
||||
"Condition": {
|
||||
"Percent": true,
|
||||
"Operator": "gt",
|
||||
"Value": 95
|
||||
},
|
||||
"Actions": [
|
||||
{
|
||||
"Type": "notification",
|
||||
"Target": ""
|
||||
}
|
||||
],
|
||||
"LastTriggered": "0001-01-01T00:00:00Z",
|
||||
"Throttled": true,
|
||||
"Severity": "warn"
|
||||
}
|
||||
},
|
||||
"EmailConfig": {
|
||||
"Enabled": true,
|
||||
"Host": "smtp.gmail.com",
|
||||
|
@ -324,6 +401,23 @@
|
|||
"DockerConfig": {
|
||||
"SkipPruneNetwork": false
|
||||
},
|
||||
"OpenIDClients": [
|
||||
{
|
||||
"id": "gitea",
|
||||
"secret": "$2a$10$406wbQbinog/zqpnc6amSu1UArA.zVrb/KuRkaBGJYA4oruGnxUga",
|
||||
"redirect": "http://localhost:3000/user/oauth2/Cosmos/callback"
|
||||
},
|
||||
{
|
||||
"id": "minio",
|
||||
"secret": "$2a$10$cE30L/Ik3ThX0G8KX6663ujmDC5UsqAsbMGqE6zRKjI0WFD6zV.N6",
|
||||
"redirect": "http://localhost:9090/oauth_callback"
|
||||
},
|
||||
{
|
||||
"id": "nextcloud",
|
||||
"secret": "$2a$10$IcpiICqki2cBnZc1.VOaYu0SPxKx6sXWyly44s0hsSNYMyfibsVAy",
|
||||
"redirect": "https://localhost:12443/apps/oidc_login/oidc"
|
||||
}
|
||||
],
|
||||
"BlockedCountries": [
|
||||
"CN",
|
||||
"RU",
|
||||
|
@ -344,7 +438,78 @@
|
|||
],
|
||||
"ServerCountry": "",
|
||||
"RequireMFA": false,
|
||||
"AutoUpdate": false
|
||||
"AutoUpdate": false,
|
||||
"ConstellationConfig": {
|
||||
"Enabled": true,
|
||||
"SlaveMode": false,
|
||||
"PrivateNode": false,
|
||||
"DNSDisabled": false,
|
||||
"DNSPort": "",
|
||||
"DNSFallback": "8.8.8.8:53",
|
||||
"DNSBlockBlacklist": true,
|
||||
"DNSAdditionalBlocklists": [
|
||||
"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
|
||||
"https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt"
|
||||
],
|
||||
"CustomDNSEntries": [],
|
||||
"NebulaConfig": {
|
||||
"PKI": {
|
||||
"CA": "",
|
||||
"Cert": "",
|
||||
"Key": "",
|
||||
"Blocklist": null
|
||||
},
|
||||
"StaticHostMap": null,
|
||||
"Lighthouse": {
|
||||
"AMLighthouse": false,
|
||||
"Interval": 0,
|
||||
"Hosts": null
|
||||
},
|
||||
"Listen": {
|
||||
"Host": "",
|
||||
"Port": 0
|
||||
},
|
||||
"Punchy": {
|
||||
"Punch": false,
|
||||
"Respond": false
|
||||
},
|
||||
"Relay": {
|
||||
"AMRelay": true,
|
||||
"UseRelays": false,
|
||||
"Relays": null
|
||||
},
|
||||
"TUN": {
|
||||
"Disabled": false,
|
||||
"Dev": "",
|
||||
"DropLocalBroadcast": false,
|
||||
"DropMulticast": false,
|
||||
"TxQueue": 0,
|
||||
"MTU": 0,
|
||||
"Routes": null,
|
||||
"UnsafeRoutes": null
|
||||
},
|
||||
"Logging": {
|
||||
"Level": "",
|
||||
"Format": ""
|
||||
},
|
||||
"Firewall": {
|
||||
"OutboundAction": "",
|
||||
"InboundAction": "",
|
||||
"Conntrack": {
|
||||
"TCPTimeout": "",
|
||||
"UDPTimeout": "",
|
||||
"DefaultTimeout": ""
|
||||
},
|
||||
"Outbound": null,
|
||||
"Inbound": null
|
||||
}
|
||||
},
|
||||
"ConstellationHostname": "vpn.domain.com"
|
||||
}
|
||||
},
|
||||
"updates": {
|
||||
"/Sonarr": true,
|
||||
"/Jellyfin": true
|
||||
},
|
||||
"status": "OK"
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
import configDemo from './docker.demo.json';
|
||||
import configDemoCont from './container.demo.json';
|
||||
import volumesDemo from './volumes.demo.json';
|
||||
import networkDemo from './networks.demo.json';
|
||||
import logDemo from './logs.demo.json';
|
||||
|
||||
function list() {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -6,6 +10,12 @@ function list() {
|
|||
});
|
||||
}
|
||||
|
||||
function get(containerName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(configDemoCont)
|
||||
});
|
||||
}
|
||||
|
||||
function secure(id, res) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
|
@ -24,15 +34,197 @@ const newDB = () => {
|
|||
|
||||
const manageContainer = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateContainer(containerId, values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function listContainerNetworks(containerId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function createNetwork(values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function attachNetwork(containerId, networkId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function detachNetwork(containerId, networkId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function createVolume(values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function volumeList() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(volumesDemo)
|
||||
});
|
||||
}
|
||||
|
||||
function volumeDelete(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function networkList() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(networkDemo)
|
||||
});
|
||||
}
|
||||
|
||||
function networkDelete(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getContainerLogs(containerId, searchQuery, limit, lastReceivedLogs, errorOnly) {
|
||||
if(limit < 50) limit = 50;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(logDemo)
|
||||
});
|
||||
}
|
||||
|
||||
function attachTerminal(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
100
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function createTerminal(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
100
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function autoUpdate(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
1000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function pullImage(imageName, onProgress, ifMissing) {
|
||||
onProgress('Updating...')
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
onProgress('Download in progress...')
|
||||
},
|
||||
1000
|
||||
);
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
onProgress('[OPERATION SUCCEEDED]')
|
||||
},
|
||||
2500
|
||||
);
|
||||
});
|
||||
}
|
||||
const updateContainerImage = pullImage;
|
||||
const createService = pullImage;
|
||||
|
||||
export {
|
||||
list,
|
||||
get,
|
||||
newDB,
|
||||
secure,
|
||||
manageContainer
|
||||
manageContainer,
|
||||
volumeList,
|
||||
volumeDelete,
|
||||
networkList,
|
||||
networkDelete,
|
||||
getContainerLogs,
|
||||
updateContainer,
|
||||
listContainerNetworks,
|
||||
createNetwork,
|
||||
attachNetwork,
|
||||
detachNetwork,
|
||||
createVolume,
|
||||
attachTerminal,
|
||||
createTerminal,
|
||||
createService,
|
||||
pullImage,
|
||||
autoUpdate,
|
||||
updateContainerImage,
|
||||
};
|
|
@ -157,6 +157,174 @@ function createVolume(values) {
|
|||
}))
|
||||
}
|
||||
|
||||
function attachTerminal(containerId) {
|
||||
let protocol = 'ws://';
|
||||
if (window.location.protocol === 'https:') {
|
||||
protocol = 'wss://';
|
||||
}
|
||||
return new WebSocket(protocol + window.location.host + '/cosmos/api/servapps/' + containerId + '/terminal/attach');
|
||||
}
|
||||
|
||||
function createTerminal(containerId) {
|
||||
let protocol = 'ws://';
|
||||
if (window.location.protocol === 'https:') {
|
||||
protocol = 'wss://';
|
||||
}
|
||||
return new WebSocket(protocol + window.location.host + '/cosmos/api/servapps/' + containerId + '/terminal/new');
|
||||
}
|
||||
|
||||
function createService(serviceData, onProgress) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serviceData)
|
||||
};
|
||||
|
||||
return fetch('/cosmos/api/docker-service', requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
|
||||
const reader = response.body.getReader();
|
||||
|
||||
// Read the stream and pass chunks to the callback as they arrive
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
function read() {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Decode the UTF-8 text
|
||||
let text = new TextDecoder().decode(value);
|
||||
// Split by lines in case there are multiple lines in one chunk
|
||||
let lines = text.split('\n');
|
||||
for (let line of lines) {
|
||||
if (line) {
|
||||
// Call the progress callback
|
||||
onProgress(line);
|
||||
}
|
||||
}
|
||||
controller.enqueue(value);
|
||||
return read();
|
||||
});
|
||||
}
|
||||
return read();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pullImage(imageName, onProgress, ifMissing) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
};
|
||||
|
||||
const imageNameEncoded = encodeURIComponent(imageName);
|
||||
|
||||
return fetch(`/cosmos/api/images/${ifMissing ? 'pull-if-missing' : 'pull'}?imageName=${imageNameEncoded}`, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
|
||||
const reader = response.body.getReader();
|
||||
|
||||
// Read the stream and pass chunks to the callback as they arrive
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
function read() {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Decode the UTF-8 text
|
||||
let text = new TextDecoder().decode(value);
|
||||
// Split by lines in case there are multiple lines in one chunk
|
||||
let lines = text.split('\n');
|
||||
for (let line of lines) {
|
||||
if (line) {
|
||||
// Call the progress callback
|
||||
onProgress(line);
|
||||
}
|
||||
}
|
||||
controller.enqueue(value);
|
||||
return read();
|
||||
});
|
||||
}
|
||||
return read();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateContainerImage(containerName, onProgress) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
};
|
||||
|
||||
const containerNameEncoded = encodeURIComponent(containerName);
|
||||
|
||||
return fetch(`/cosmos/api/servapps/${containerNameEncoded}/manage/update`, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
|
||||
const reader = response.body.getReader();
|
||||
|
||||
// Read the stream and pass chunks to the callback as they arrive
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
function read() {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Decode the UTF-8 text
|
||||
let text = new TextDecoder().decode(value);
|
||||
// Split by lines in case there are multiple lines in one chunk
|
||||
let lines = text.split('\n');
|
||||
for (let line of lines) {
|
||||
if (line) {
|
||||
// Call the progress callback
|
||||
onProgress(line);
|
||||
}
|
||||
}
|
||||
controller.enqueue(value);
|
||||
return read();
|
||||
});
|
||||
}
|
||||
return read();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function autoUpdate(id, toggle) {
|
||||
return wrap(fetch('/cosmos/api/servapps/' + id + '/auto-update/'+toggle, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
get,
|
||||
|
@ -174,4 +342,10 @@ export {
|
|||
attachNetwork,
|
||||
detachNetwork,
|
||||
createVolume,
|
||||
attachTerminal,
|
||||
createTerminal,
|
||||
createService,
|
||||
pullImage,
|
||||
autoUpdate,
|
||||
updateContainerImage,
|
||||
};
|
49
client/src/api/downloadButton.jsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { ArrowDownOutlined } from "@ant-design/icons";
|
||||
import { Button } from "@mui/material";
|
||||
import ResponsiveButton from "../components/responseiveButton";
|
||||
|
||||
export const DownloadFile = ({ filename, content, contentGetter, label, style }) => {
|
||||
const downloadFile = async () => {
|
||||
// Get the content
|
||||
if (contentGetter) {
|
||||
try {
|
||||
content = await contentGetter();
|
||||
if(typeof content !== "string") {
|
||||
content = JSON.stringify(content, null, 2);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a blob with the content
|
||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||
|
||||
// Create a link element
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
|
||||
// Append the link to the document (needed for Firefox)
|
||||
document.body.appendChild(link);
|
||||
|
||||
// Simulate a click to start the download
|
||||
link.click();
|
||||
|
||||
// Cleanup the DOM by removing the link element
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveButton
|
||||
color="primary"
|
||||
onClick={downloadFile}
|
||||
style={style}
|
||||
variant={"outlined"}
|
||||
startIcon={<ArrowDownOutlined />}
|
||||
>
|
||||
{label}
|
||||
</ResponsiveButton>
|
||||
);
|
||||
}
|
1
client/src/api/events.demo.json
Normal file
|
@ -3,13 +3,29 @@ export const getStatus = () => {
|
|||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"data": {
|
||||
"AVX": true,
|
||||
"CPU": "amd64",
|
||||
"HTTPSCertificateMode": "LETSENCRYPT",
|
||||
"LetsEncryptErrors": [],
|
||||
"MonitoringDisabled": false,
|
||||
"backup_status": "",
|
||||
"database": true,
|
||||
"docker": true,
|
||||
"domain": false,
|
||||
"homepage": {
|
||||
"Background": "/cosmos/api/background/avif",
|
||||
"Widgets": null,
|
||||
"Expanded": false
|
||||
},
|
||||
"hostname": "yann-server.com",
|
||||
"letsencrypt": false,
|
||||
"needsRestart": false,
|
||||
"newVersionAvailable": false
|
||||
"newVersionAvailable": false,
|
||||
"resources": {},
|
||||
"theme": {
|
||||
"PrimaryColor": "rgba(191, 100, 64, 1)",
|
||||
"SecondaryColor": ""
|
||||
}
|
||||
},
|
||||
"status": "OK"
|
||||
});
|
||||
|
@ -36,4 +52,38 @@ export const newInstall = (req) => {
|
|||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const getDNS = (host) => (req) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
"data": "199.199.199.199"
|
||||
})},
|
||||
100
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const checkHost = (host) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
"data": "199.199.199.199"
|
||||
})},
|
||||
100
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const uploadImage = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
"data": ""
|
||||
})}, 100 );
|
||||
});
|
||||
}
|
|
@ -2,22 +2,74 @@ import * as _auth from './authentication';
|
|||
import * as _users from './users';
|
||||
import * as _config from './config';
|
||||
import * as _docker from './docker';
|
||||
import * as _market from './market';
|
||||
import * as _constellation from './constellation';
|
||||
import * as _metrics from './metrics';
|
||||
|
||||
import * as authDemo from './authentication.demo';
|
||||
import * as usersDemo from './users.demo';
|
||||
import * as configDemo from './config.demo';
|
||||
import * as dockerDemo from './docker.demo';
|
||||
import * as indexDemo from './index.demo';
|
||||
import * as marketDemo from './market.demo';
|
||||
import * as constellationDemo from './constellation.demo';
|
||||
import * as metricsDemo from './metrics.demo';
|
||||
|
||||
import wrap from './wrap';
|
||||
import { redirectToLocal } from '../utils/indexs';
|
||||
|
||||
let getStatus = () => {
|
||||
export let CPU_ARCH = 'amd64';
|
||||
export let CPU_AVX = true;
|
||||
|
||||
export let HOME_BACKGROUND;
|
||||
export let PRIMARY_COLOR;
|
||||
export let SECONDARY_COLOR;
|
||||
|
||||
export let FIRST_LOAD = false;
|
||||
|
||||
let getStatus = (initial) => {
|
||||
return wrap(fetch('/cosmos/api/status', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}))
|
||||
}), initial)
|
||||
.then(async (response) => {
|
||||
CPU_ARCH = response.data.CPU;
|
||||
CPU_AVX = response.data.AVX;
|
||||
HOME_BACKGROUND = response.data.homepage.Background;
|
||||
PRIMARY_COLOR = response.data.theme.PrimaryColor;
|
||||
SECONDARY_COLOR = response.data.theme.SecondaryColor;
|
||||
FIRST_LOAD = true;
|
||||
return response
|
||||
}).catch((response) => {
|
||||
const urlSearch = encodeURIComponent(window.location.search);
|
||||
const redirectToURL = (window.location.pathname + urlSearch);
|
||||
|
||||
if(response.status != 'OK') {
|
||||
if(
|
||||
window.location.href.indexOf('/cosmos-ui/newInstall') == -1 &&
|
||||
window.location.href.indexOf('/cosmos-ui/login') == -1 &&
|
||||
window.location.href.indexOf('/cosmos-ui/loginmfa') == -1 &&
|
||||
window.location.href.indexOf('/cosmos-ui/newmfa') == -1 &&
|
||||
window.location.href.indexOf('/cosmos-ui/register') == -1 &&
|
||||
window.location.href.indexOf('/cosmos-ui/forgot-password') == -1) {
|
||||
if(response.status == 'NEW_INSTALL') {
|
||||
redirectToLocal('/cosmos-ui/newInstall');
|
||||
} else if (response.status == 'error' && response.code == "HTTP004") {
|
||||
redirectToLocal('/cosmos-ui/login?redirect=' + redirectToURL);
|
||||
} else if (response.status == 'error' && response.code == "HTTP006") {
|
||||
redirectToLocal('/cosmos-ui/loginmfa?redirect=' + redirectToURL);
|
||||
} else if (response.status == 'error' && response.code == "HTTP007") {
|
||||
redirectToLocal('/cosmos-ui/newmfa?redirect=' + redirectToURL);
|
||||
}
|
||||
} else {
|
||||
return "nothing";
|
||||
}
|
||||
}
|
||||
|
||||
return "NOT_AVAILABLE";
|
||||
});
|
||||
}
|
||||
|
||||
let isOnline = () => {
|
||||
|
@ -42,31 +94,144 @@ let isOnline = () => {
|
|||
});
|
||||
}
|
||||
|
||||
let newInstall = (req) => {
|
||||
return wrap(fetch('/cosmos/api/newInstall', {
|
||||
method: 'POST',
|
||||
let newInstall = (req, onProgress) => {
|
||||
if(req.step == '2') {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(req)
|
||||
};
|
||||
|
||||
return fetch('/cosmos/api/newInstall', requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
|
||||
const reader = response.body.getReader();
|
||||
|
||||
// Read the stream and pass chunks to the callback as they arrive
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
function read() {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Decode the UTF-8 text
|
||||
let text = new TextDecoder().decode(value);
|
||||
// Split by lines in case there are multiple lines in one chunk
|
||||
let lines = text.split('\n');
|
||||
for (let line of lines) {
|
||||
if (line) {
|
||||
// Call the progress callback
|
||||
onProgress(line);
|
||||
}
|
||||
}
|
||||
controller.enqueue(value);
|
||||
return read();
|
||||
});
|
||||
}
|
||||
return read();
|
||||
}
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
} else {
|
||||
return wrap(fetch('/cosmos/api/newInstall', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(req)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
let checkHost = (host) => {
|
||||
return fetch('/cosmos/api/dns-check?url=' + host, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(req)
|
||||
}))
|
||||
}
|
||||
}).then(async (response) => {
|
||||
let rep;
|
||||
try {
|
||||
rep = await response.json();
|
||||
} catch {
|
||||
throw new Error('Server error');
|
||||
}
|
||||
if (response.status == 200) {
|
||||
return rep;
|
||||
}
|
||||
const e = new Error(rep.message);
|
||||
e.status = response.status;
|
||||
e.message = rep.message;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
let getDNS = (host) => {
|
||||
return fetch('/cosmos/api/dns?url=' + host, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(async (response) => {
|
||||
let rep;
|
||||
try {
|
||||
rep = await response.json();
|
||||
} catch {
|
||||
throw new Error('Server error');
|
||||
}
|
||||
if (response.status == 200) {
|
||||
return rep;
|
||||
}
|
||||
const e = new Error(rep.message);
|
||||
e.status = response.status;
|
||||
e.message = rep.message;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
let uploadImage = (file, name) => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
return wrap(fetch('/cosmos/api/upload/' + name, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}));
|
||||
};
|
||||
|
||||
const isDemo = import.meta.env.MODE === 'demo';
|
||||
|
||||
let auth = _auth;
|
||||
let users = _users;
|
||||
let config = _config;
|
||||
let docker = _docker;
|
||||
let market = _market;
|
||||
let constellation = _constellation;
|
||||
let metrics = _metrics;
|
||||
|
||||
if(isDemo) {
|
||||
auth = authDemo;
|
||||
users = usersDemo;
|
||||
config = configDemo;
|
||||
docker = dockerDemo;
|
||||
market = marketDemo;
|
||||
getStatus = indexDemo.getStatus;
|
||||
newInstall = indexDemo.newInstall;
|
||||
isOnline = indexDemo.isOnline;
|
||||
checkHost = indexDemo.checkHost;
|
||||
getDNS = indexDemo.getDNS;
|
||||
uploadImage = indexDemo.uploadImage;
|
||||
constellation = constellationDemo;
|
||||
metrics = metricsDemo;
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -74,7 +239,13 @@ export {
|
|||
users,
|
||||
config,
|
||||
docker,
|
||||
market,
|
||||
constellation,
|
||||
getStatus,
|
||||
newInstall,
|
||||
isOnline
|
||||
isOnline,
|
||||
checkHost,
|
||||
getDNS,
|
||||
metrics,
|
||||
uploadImage
|
||||
};
|
2505
client/src/api/logs.demo.json
Normal file
3256
client/src/api/market.demo.json
Normal file
11
client/src/api/market.demo.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import marketDemo from './market.demo.json';
|
||||
|
||||
function list() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(marketDemo);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
};
|
14
client/src/api/market.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import wrap from './wrap';
|
||||
|
||||
function list() {
|
||||
return wrap(fetch('/cosmos/api/markets', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
};
|
1
client/src/api/metrics.demo.json
Normal file
49
client/src/api/metrics.demo.jsx
Normal file
44
client/src/api/metrics.jsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import wrap from './wrap';
|
||||
|
||||
function get(metarr) {
|
||||
return wrap(fetch('/cosmos/api/metrics?metrics=' + metarr.join(','), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function reset() {
|
||||
return wrap(fetch('/cosmos/api/reset-metrics', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function list() {
|
||||
return wrap(fetch('/cosmos/api/list-metrics', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function events(from, to, search = '', query = '', page = '', logLevel) {
|
||||
return wrap(fetch('/cosmos/api/events?from=' + from + '&to=' + to + '&search=' + search + '&query=' + query + '&page=' + page + '&logLevel=' + logLevel, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export {
|
||||
get,
|
||||
reset,
|
||||
list,
|
||||
events
|
||||
};
|
170
client/src/api/networks.demo.json
Normal file
|
@ -0,0 +1,170 @@
|
|||
{
|
||||
"data": [
|
||||
{
|
||||
"Name": "test2",
|
||||
"Id": "6675d361c126d013dbc4cba07c140821879718af3d52f15a07aa94dabbffe124",
|
||||
"Created": "2023-05-08T11:18:08.4273302Z",
|
||||
"Scope": "local",
|
||||
"Driver": "bridge",
|
||||
"EnableIPv6": false,
|
||||
"IPAM": {
|
||||
"Driver": "default",
|
||||
"Options": null,
|
||||
"Config": [
|
||||
{
|
||||
"Subnet": "172.18.0.0/16",
|
||||
"Gateway": "172.18.0.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Internal": false,
|
||||
"Attachable": false,
|
||||
"Ingress": false,
|
||||
"ConfigFrom": {
|
||||
"Network": ""
|
||||
},
|
||||
"ConfigOnly": false,
|
||||
"Containers": {},
|
||||
"Options": {},
|
||||
"Labels": {}
|
||||
},
|
||||
{
|
||||
"Name": "bridge",
|
||||
"Id": "d1d39f3e484527f0a22f72275309a6853ea5e84b64c726d75ab3735e5b316eeb",
|
||||
"Created": "2023-05-08T11:06:04.195972Z",
|
||||
"Scope": "local",
|
||||
"Driver": "bridge",
|
||||
"EnableIPv6": false,
|
||||
"IPAM": {
|
||||
"Driver": "default",
|
||||
"Options": null,
|
||||
"Config": [
|
||||
{
|
||||
"Subnet": "172.17.0.0/16",
|
||||
"Gateway": "172.17.0.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Internal": false,
|
||||
"Attachable": false,
|
||||
"Ingress": false,
|
||||
"ConfigFrom": {
|
||||
"Network": ""
|
||||
},
|
||||
"ConfigOnly": false,
|
||||
"Containers": {},
|
||||
"Options": {
|
||||
"com.docker.network.bridge.default_bridge": "true",
|
||||
"com.docker.network.bridge.enable_icc": "true",
|
||||
"com.docker.network.bridge.enable_ip_masquerade": "true",
|
||||
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
|
||||
"com.docker.network.bridge.name": "docker0",
|
||||
"com.docker.network.driver.mtu": "1500"
|
||||
},
|
||||
"Labels": {}
|
||||
},
|
||||
{
|
||||
"Name": "Also Test",
|
||||
"Id": "3128c9c7e83a03b297249a6c8df437d6a9656a405efd0c3f937aee7e7b1d993d",
|
||||
"Created": "2023-05-07T17:28:12.2844952Z",
|
||||
"Scope": "local",
|
||||
"Driver": "bridge",
|
||||
"EnableIPv6": false,
|
||||
"IPAM": {
|
||||
"Driver": "default",
|
||||
"Options": null,
|
||||
"Config": [
|
||||
{
|
||||
"Subnet": "172.26.0.0/16",
|
||||
"Gateway": "172.26.0.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Internal": false,
|
||||
"Attachable": false,
|
||||
"Ingress": false,
|
||||
"ConfigFrom": {
|
||||
"Network": ""
|
||||
},
|
||||
"ConfigOnly": false,
|
||||
"Containers": {},
|
||||
"Options": {},
|
||||
"Labels": {}
|
||||
},
|
||||
{
|
||||
"Name": "host",
|
||||
"Id": "40f43ad5c13390b477d496a57cd3f2eacef3b92262de2429e369e5655d1fe13a",
|
||||
"Created": "2020-03-15T16:34:57.75505446Z",
|
||||
"Scope": "local",
|
||||
"Driver": "host",
|
||||
"EnableIPv6": false,
|
||||
"IPAM": {
|
||||
"Driver": "default",
|
||||
"Options": null,
|
||||
"Config": []
|
||||
},
|
||||
"Internal": false,
|
||||
"Attachable": false,
|
||||
"Ingress": false,
|
||||
"ConfigFrom": {
|
||||
"Network": ""
|
||||
},
|
||||
"ConfigOnly": false,
|
||||
"Containers": {},
|
||||
"Options": {},
|
||||
"Labels": {}
|
||||
},
|
||||
{
|
||||
"Name": "none",
|
||||
"Id": "d2fd80f8cd33c60a81487eec8b7e663acf539479411c8d38d234b7943403270f",
|
||||
"Created": "2020-03-15T16:34:57.74630246Z",
|
||||
"Scope": "local",
|
||||
"Driver": "null",
|
||||
"EnableIPv6": false,
|
||||
"IPAM": {
|
||||
"Driver": "default",
|
||||
"Options": null,
|
||||
"Config": []
|
||||
},
|
||||
"Internal": false,
|
||||
"Attachable": false,
|
||||
"Ingress": false,
|
||||
"ConfigFrom": {
|
||||
"Network": ""
|
||||
},
|
||||
"ConfigOnly": false,
|
||||
"Containers": {},
|
||||
"Options": {},
|
||||
"Labels": {}
|
||||
},
|
||||
{
|
||||
"Name": "cosmos-network-Y5UKzxkrk",
|
||||
"Id": "37357fc736ac7510162d036033c4bc3b50955accc484a473bc78b21c9fe860e5",
|
||||
"Created": "2023-05-08T13:28:43.0766901Z",
|
||||
"Scope": "local",
|
||||
"Driver": "bridge",
|
||||
"EnableIPv6": false,
|
||||
"IPAM": {
|
||||
"Driver": "default",
|
||||
"Options": null,
|
||||
"Config": [
|
||||
{
|
||||
"Subnet": "172.20.0.0/16",
|
||||
"Gateway": "172.20.0.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Internal": false,
|
||||
"Attachable": true,
|
||||
"Ingress": false,
|
||||
"ConfigFrom": {
|
||||
"Network": ""
|
||||
},
|
||||
"ConfigOnly": false,
|
||||
"Containers": {},
|
||||
"Options": {},
|
||||
"Labels": {}
|
||||
}
|
||||
],
|
||||
"status": "OK"
|
||||
}
|
|
@ -116,6 +116,23 @@ function resetPassword(values) {
|
|||
});
|
||||
}
|
||||
|
||||
function getNotifs() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({"data":[{"ID":"654b0fccce74bf6f8c8ccc61","Title":"Container Update","Message":"Container Prowlarr updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/Prowlarr","Date":"2023-11-08T04:33:39.041Z","Level":"info","Read":false,"Recipient":"admin","Actions":null},{"ID":"654b0fccce74bf6f8c8ccc60","Title":"Container Update","Message":"Container Lidarr updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/Lidarr","Date":"2023-11-08T04:33:29.779Z","Level":"info","Read":false,"Recipient":"admin","Actions":null},{"ID":"654a589ff54b04d499103b19","Title":"Container Update","Message":"Container transmission updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/transmission","Date":"2023-11-07T15:31:56.385Z","Level":"info","Read":true,"Recipient":"admin","Actions":null},{"ID":"6547ff777b87120576934c61","Title":"Container Update","Message":"Container Jellyfin updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/Jellyfin","Date":"2023-11-05T20:47:04.658Z","Level":"info","Read":true,"Recipient":"admin","Actions":null},{"ID":"6547ff777b87120576934c60","Title":"Container Update","Message":"Container logs updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/logs","Date":"2023-11-05T20:46:56.503Z","Level":"info","Read":true,"Recipient":"admin","Actions":null}],"status":"OK"})
|
||||
});
|
||||
}
|
||||
|
||||
function readNotifs(notifs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
})},
|
||||
2000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
create,
|
||||
|
@ -128,4 +145,6 @@ export {
|
|||
check2FA,
|
||||
reset2FA,
|
||||
resetPassword,
|
||||
getNotifs,
|
||||
readNotifs
|
||||
};
|
|
@ -110,6 +110,24 @@ function resetPassword(values) {
|
|||
}))
|
||||
}
|
||||
|
||||
function getNotifs() {
|
||||
return wrap(fetch('/cosmos/api/notifications', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function readNotifs(notifs) {
|
||||
return wrap(fetch('/cosmos/api/notifications/read?ids=' + notifs.join(','), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
create,
|
||||
|
@ -122,4 +140,6 @@ export {
|
|||
check2FA,
|
||||
reset2FA,
|
||||
resetPassword,
|
||||
getNotifs,
|
||||
readNotifs,
|
||||
};
|
161
client/src/api/volumes.demo.json
Normal file
|
@ -0,0 +1,161 @@
|
|||
{
|
||||
"data": {
|
||||
"Volumes": [
|
||||
{
|
||||
"CreatedAt": "2023-05-02T13:26:13Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/c4106d6074bc26e4f32858d750b7bac534d4f801396366afdd2df92f32229c21/_data",
|
||||
"Name": "c4106d6074bc26e4f32858d750b7bac534d4f801396366afdd2df92f32229c21",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-04-10T19:59:09Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/fb7fd95481d9975953dcd967ea93addc95e6e93d16eb305fe9d4e43882bc4ad2/_data",
|
||||
"Name": "fb7fd95481d9975953dcd967ea93addc95e6e93d16eb305fe9d4e43882bc4ad2",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-05-07T16:36:48Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/test3/_data",
|
||||
"Name": "test3",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-03-25T17:00:47Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/1bc3e0f8ea49eb7482f8255a2525b0419d919611e62f0536517f79f3a258d278/_data",
|
||||
"Name": "1bc3e0f8ea49eb7482f8255a2525b0419d919611e62f0536517f79f3a258d278",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-05-07T13:09:19Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/52573b0b23018e5412182e7ebdf60d684790616ffdfe7424b063acbfdd078bd8/_data",
|
||||
"Name": "52573b0b23018e5412182e7ebdf60d684790616ffdfe7424b063acbfdd078bd8",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-04-10T19:33:39Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/740c05e5c53a7e9df5954ed82ab3a9987e666c2b6030cd66e3c5f5320a9d0053/_data",
|
||||
"Name": "740c05e5c53a7e9df5954ed82ab3a9987e666c2b6030cd66e3c5f5320a9d0053",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-03-29T15:10:38Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/bb92c6cda0dcebe101489e0bd44e8cc2ddb3f2f6706b78ab08b496067e09a7ce/_data",
|
||||
"Name": "bb92c6cda0dcebe101489e0bd44e8cc2ddb3f2f6706b78ab08b496067e09a7ce",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-05-01T13:28:52Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/cosmos-mongo-data-fxL/_data",
|
||||
"Name": "cosmos-mongo-data-fxL",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-04-01T11:42:49Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/cosmos-mongo-config-NkM/_data",
|
||||
"Name": "cosmos-mongo-config-NkM",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-04-01T12:26:59Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/cosmos-mongo-config-mPv/_data",
|
||||
"Name": "cosmos-mongo-config-mPv",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-04-11T12:56:07Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/cosmos-mongo-data-XNq/_data",
|
||||
"Name": "cosmos-mongo-data-XNq",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-03-31T14:34:56Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/469dd6e435f069173a7e88565812fbf8f354b966356361594b36fef0b0ae58fb/_data",
|
||||
"Name": "469dd6e435f069173a7e88565812fbf8f354b966356361594b36fef0b0ae58fb",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-04-11T13:06:57Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/66689e6835b842df45a0062d7d69986e2876936a4e0c5b984b4b49a0a22247c6/_data",
|
||||
"Name": "66689e6835b842df45a0062d7d69986e2876936a4e0c5b984b4b49a0a22247c6",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-05-07T16:46:53Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/8399775751b3ded6ba5a6532480b1ba43e85697b8261cf5d1d5f81d4a64fcbec/_data",
|
||||
"Name": "8399775751b3ded6ba5a6532480b1ba43e85697b8261cf5d1d5f81d4a64fcbec",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-04-01T16:30:01Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/8715d74238cf3162649cf0bfa582efa6266e025cd45b8b197c897f445ed0c844/_data",
|
||||
"Name": "8715d74238cf3162649cf0bfa582efa6266e025cd45b8b197c897f445ed0c844",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-04-11T12:36:37Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/a1fb959d8a54919926f703fcd950d4581bc51572a0ec2275ef08a11fde258956/_data",
|
||||
"Name": "a1fb959d8a54919926f703fcd950d4581bc51572a0ec2275ef08a11fde258956",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
},
|
||||
{
|
||||
"CreatedAt": "2023-05-07T16:08:35Z",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/MyVolume/_data",
|
||||
"Name": "MyVolume",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
}
|
||||
],
|
||||
"Warnings": null
|
||||
},
|
||||
"status": "OK"
|
||||
}
|
|
@ -1,20 +1,32 @@
|
|||
let snackit;
|
||||
|
||||
export default function wrap(apicall) {
|
||||
export default function wrap(apicall, noError = false) {
|
||||
return apicall.then(async (response) => {
|
||||
let rep;
|
||||
try {
|
||||
rep = await response.json();
|
||||
} catch {
|
||||
snackit('Server error');
|
||||
throw new Error('Server error');
|
||||
} catch (err) {
|
||||
if (!noError) {
|
||||
snackit('Server error');
|
||||
throw new Error('Server error');
|
||||
} else {
|
||||
const e = new Error(rep.message);
|
||||
e.status = rep.status;
|
||||
e.code = rep.code;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status == 200) {
|
||||
return rep;
|
||||
}
|
||||
snackit(rep.message);
|
||||
|
||||
if (!noError) {
|
||||
snackit(rep.message);
|
||||
}
|
||||
|
||||
const e = new Error(rep.message);
|
||||
e.status = response.status;
|
||||
e.status = rep.status;
|
||||
e.code = rep.code;
|
||||
throw e;
|
||||
});
|
||||
|
|
BIN
client/src/assets/images/icons/constellation.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
client/src/assets/images/icons/cosmos_simple_black.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
client/src/assets/images/icons/cosmos_simple_white.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 684 KiB |
BIN
client/src/assets/images/wallpaper2.jpg
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
client/src/assets/images/wallpaper2_light.jpg
Normal file
After Width: | Height: | Size: 245 KiB |
|
@ -15,6 +15,15 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
|
|||
const location = useLocation();
|
||||
const [main, setMain] = useState();
|
||||
const [item, setItem] = useState();
|
||||
let subItem = '';
|
||||
|
||||
// extract /servapps/stack/:stack
|
||||
const subPath = location.pathname.split('/')[3];
|
||||
if(subPath && location.pathname.split('/')[4]) {
|
||||
subItem = <Typography variant="subtitle1" color="textPrimary">
|
||||
{location.pathname.split('/')[4]}
|
||||
</Typography>;
|
||||
}
|
||||
|
||||
// set active item state
|
||||
const getCollapse = (menu) => {
|
||||
|
@ -23,7 +32,7 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
|
|||
if (collapse.type && collapse.type === 'collapse') {
|
||||
getCollapse(collapse);
|
||||
} else if (collapse.type && collapse.type === 'item') {
|
||||
if (location.pathname === collapse.url) {
|
||||
if (location.pathname.startsWith(collapse.url)) {
|
||||
setMain(menu);
|
||||
setItem(collapse);
|
||||
}
|
||||
|
@ -82,6 +91,8 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
|
|||
</Typography>
|
||||
{mainContent}
|
||||
{itemContent}
|
||||
{subPath && <Typography variant="subtitle1" color="textPrimary">{subPath}</Typography>}
|
||||
{subItem}
|
||||
</MuiBreadcrumbs>
|
||||
</Grid>
|
||||
{title && (
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
import { useTheme } from '@mui/material/styles';
|
||||
import { fontWeight } from '@mui/system';
|
||||
|
||||
import logo from '../../assets/images/icons/cosmos.png';
|
||||
import logo from '../../assets/images/icons/cosmos_simple_black.png';
|
||||
import logoDark from '../../assets/images/icons/cosmos_simple_white.png';
|
||||
|
||||
// ==============================|| LOGO SVG ||============================== //
|
||||
|
||||
const Logo = () => {
|
||||
const theme = useTheme();
|
||||
const isLight = theme.palette.mode === 'light';
|
||||
|
||||
return (
|
||||
/**
|
||||
|
@ -17,7 +19,7 @@ const Logo = () => {
|
|||
*
|
||||
*/
|
||||
<>
|
||||
<img src={logo} alt="Cosmos" width="50" />
|
||||
<img src={isLight ? logo : logoDark} alt="Cosmos" width="40" />
|
||||
<span style={{fontWeight: 'bold', fontSize: '170%', paddingLeft:'10px'}}> Cosmos</span>
|
||||
</>
|
||||
);
|
||||
|
|
94
client/src/components/apiModal.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
// material-ui
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { Button } from '@mui/material';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const preStyle = {
|
||||
backgroundColor: '#000',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '500px',
|
||||
maxWidth: '100%',
|
||||
width: '100%',
|
||||
margin: '0',
|
||||
position: 'relative',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.5',
|
||||
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
boxSizing: 'border-box',
|
||||
marginBottom: '10px',
|
||||
marginTop: '10px',
|
||||
marginLeft: '0',
|
||||
marginRight: '0',
|
||||
display: 'block',
|
||||
textAlign: 'left',
|
||||
verticalAlign: 'baseline',
|
||||
opacity: '1',
|
||||
}
|
||||
|
||||
const ApiModal = ({ callback, label }) => {
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [content, setContent] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const getContent = async () => {
|
||||
setLoading(true);
|
||||
let content = await callback();
|
||||
setContent(content.data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (openModal)
|
||||
getContent();
|
||||
}, [openModal]);
|
||||
|
||||
return <>
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)} fullWidth maxWidth={'sm'}>
|
||||
<DialogTitle>Refresh Page</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<pre style={preStyle}>
|
||||
{content}
|
||||
</pre>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
getContent();
|
||||
}}>Refresh</LoadingButton>
|
||||
<Button onClick={() => {
|
||||
setOpenModal(false);
|
||||
}}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Button
|
||||
disableElevation
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setOpenModal(true);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</>
|
||||
};
|
||||
|
||||
export default ApiModal;
|
49
client/src/components/confirmModal.jsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
// material-ui
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { Button } from '@mui/material';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const ConfirmModal = ({ callback, label, content, startIcon }) => {
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
|
||||
return <>
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{content}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => {
|
||||
setOpenModal(false);
|
||||
}}>Cancel</Button>
|
||||
<LoadingButton
|
||||
onClick={() => {
|
||||
callback();
|
||||
setOpenModal(false);
|
||||
}}>Confirm</LoadingButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Button
|
||||
disableElevation
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
startIcon={startIcon}
|
||||
onClick={() => {
|
||||
setOpenModal(true);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</>
|
||||
};
|
||||
|
||||
export default ConfirmModal;
|
9
client/src/components/containers.jsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { WarningFilled } from "@ant-design/icons";
|
||||
import { Tooltip } from "@mui/material";
|
||||
|
||||
export const ContainerNetworkWarning = ({container}) => (
|
||||
container.HostConfig.NetworkMode != "bridge" && container.HostConfig.NetworkMode != "default" &&
|
||||
<Tooltip title={`This container is using an incompatible network mode (${container.HostConfig.NetworkMode.slice(0, 16)}). If you want Cosmos to proxy to this container, enabling this option will change the network mode to bridge for you. Otherwise, you dont need to do anything, as the container is already isolated. Note that changing to bridge might break connectivity to other containers. To fix it, please use a private network and static ips instead.`}>
|
||||
<WarningFilled style={{color: 'red', fontSize: '18px', paddingLeft: '10px', paddingRight: '10px'}} />
|
||||
</Tooltip>
|
||||
);
|
15
client/src/components/delete.jsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, SafetyOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import { Card, Chip, Stack, Tooltip } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const DeleteButton = ({onDelete, disabled, size}) => {
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
return (<>
|
||||
{!confirmDelete && (<Chip label={<DeleteOutlined size={size}/>}
|
||||
onClick={() => !disabled && setConfirmDelete(true)}/>)}
|
||||
{confirmDelete && (<Chip label={<CheckOutlined size={size}/>} color="error"
|
||||
onClick={(event) => !disabled && onDelete(event)}/>)}
|
||||
</>);
|
||||
}
|
24
client/src/components/fileUpload.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
|
||||
export default function UploadButtons({OnChange, accept, label, variant, fullWidth, size}) {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
accept={accept}
|
||||
style={{ display: 'none' }}
|
||||
id="contained-button-file"
|
||||
multiple
|
||||
type="file"
|
||||
onChange={OnChange}
|
||||
/>
|
||||
<label htmlFor="contained-button-file">
|
||||
<Button variant={variant || "contained"} component="span"
|
||||
fullWidth={fullWidth} startIcon={<UploadOutlined />}>
|
||||
{label || 'Upload'}
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -36,7 +36,7 @@ const HostChip = ({route, settings, style}) => {
|
|||
window.open(window.location.origin + route.PathPrefix, '_blank');
|
||||
}}
|
||||
onDelete={settings ? () => {
|
||||
window.open('/ui/config-url/'+route.Name, '_blank');
|
||||
window.open('/cosmos-ui/config-url/'+route.Name, '_blank');
|
||||
} : null}
|
||||
deleteIcon={settings ? <SettingOutlined /> : null}
|
||||
/>
|
||||
|
|
48
client/src/components/imageWithPlaceholder.jsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import React, { useState } from 'react';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import cosmosGray from '../assets/images/icons/cosmos_gray.png';
|
||||
|
||||
function ImageWithPlaceholder({ src, alt, placeholder, ...props }) {
|
||||
const [imageSrc, setImageSrc] = useState(placeholder || cosmosGray);
|
||||
const [imageRef, setImageRef] = useState();
|
||||
|
||||
const onLoad = event => {
|
||||
event.target.classList.add('loaded');
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
setImageSrc(cosmosGray);
|
||||
};
|
||||
|
||||
// This function will be called when the actual image is loaded
|
||||
const handleImageLoad = () => {
|
||||
if (imageRef) {
|
||||
imageRef.src = src;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
ref={setImageRef}
|
||||
{...props}
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
// style={{ opacity: imageSrc === src ? 1 : 0, transition: 'opacity 0.5s ease-in-out' }}
|
||||
/>
|
||||
{/* This image will load the actual image and then handleImageLoad will be triggered */}
|
||||
<img
|
||||
{...props}
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{ display: 'none' }} // Hide this image
|
||||
onLoad={handleImageLoad}
|
||||
onError={onError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageWithPlaceholder;
|
|
@ -56,7 +56,6 @@ const LogLine = ({ message, docker, isMobile }) => {
|
|||
if(docker) {
|
||||
let parts = html.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/)
|
||||
if(!parts) {
|
||||
console.error('Could not parse log line', html)
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
let restString = html.replace(parts[0], '')
|
||||
|
|
88
client/src/components/logsInModal.jsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
// material-ui
|
||||
import * as React from 'react';
|
||||
import { Alert, Button, Stack, Typography } from '@mui/material';
|
||||
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined } from '@ant-design/icons';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { smartDockerLogConcat, tryParseProgressLog } from '../utils/docker';
|
||||
|
||||
const preStyle = {
|
||||
backgroundColor: '#000',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '500px',
|
||||
maxWidth: '100%',
|
||||
width: '100%',
|
||||
margin: '0',
|
||||
position: 'relative',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.5',
|
||||
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
boxSizing: 'border-box',
|
||||
marginBottom: '10px',
|
||||
marginTop: '10px',
|
||||
marginLeft: '0',
|
||||
marginRight: '0',
|
||||
}
|
||||
|
||||
const LogsInModal = ({title, request, OnSuccess, OnError, closeAnytime, initialLogs = [], }) => {
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [logs, setLogs] = useState(initialLogs);
|
||||
const [done, setDone] = useState(closeAnytime);
|
||||
const preRef = React.useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLogs(initialLogs);
|
||||
setDone(closeAnytime);
|
||||
|
||||
if(request === null) return;
|
||||
|
||||
request((newlog) => {
|
||||
setLogs((old) => smartDockerLogConcat(old, newlog));
|
||||
|
||||
if(preRef.current)
|
||||
preRef.current.scrollTop = preRef.current.scrollHeight;
|
||||
|
||||
if (newlog.includes('[OPERATION SUCCEEDED]')) {
|
||||
setDone(true);
|
||||
setOpenModal(false);
|
||||
OnSuccess && OnSuccess();
|
||||
} else if (newlog.includes('[OPERATION FAILED]')) {
|
||||
setDone(true);
|
||||
OnError && OnError(newlog);
|
||||
} else {
|
||||
setOpenModal(true);
|
||||
}
|
||||
});
|
||||
}, [request]);
|
||||
|
||||
return <>
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<pre style={preStyle} ref={preRef}>
|
||||
{logs.map((l) => {
|
||||
return <div>{tryParseProgressLog(l)}</div>
|
||||
})}
|
||||
</pre>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default LogsInModal;
|
|
@ -1,7 +1,7 @@
|
|||
import { Button, useMediaQuery, IconButton } from "@mui/material";
|
||||
|
||||
|
||||
const ResponsiveButton = ({ children, startIcon, size, style, ...props }) => {
|
||||
const ResponsiveButton = ({ children, startIcon, endIcon, size, style, ...props }) => {
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||
let newStyle = style || {};
|
||||
if (isMobile) {
|
||||
|
@ -10,8 +10,18 @@ const ResponsiveButton = ({ children, startIcon, size, style, ...props }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Button className="responsive-button" size={isMobile ? 'large' : size} startIcon={isMobile ? null : startIcon} {...props} style={newStyle}>
|
||||
{isMobile ? startIcon : children}
|
||||
<Button
|
||||
className="responsive-button"
|
||||
size={isMobile ? 'large' : size}
|
||||
startIcon={isMobile ? null : startIcon}
|
||||
endIcon={isMobile ? null : endIcon}
|
||||
{...props} style={newStyle}>
|
||||
{(isMobile) ? startIcon : (
|
||||
startIcon ? children : null
|
||||
)}
|
||||
{(isMobile) ? endIcon : (
|
||||
endIcon ? children : null
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ export const RouteMode = ({route}) => {
|
|||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
let c = routeImages[route.Mode.toUpperCase()];
|
||||
return <>
|
||||
return c ? <>
|
||||
<Chip
|
||||
icon={<span>{c.icon}</span>}
|
||||
label={c.label}
|
||||
|
@ -56,7 +56,7 @@ export const RouteMode = ({route}) => {
|
|||
alignItems: "right",
|
||||
}}
|
||||
></Chip>
|
||||
</>
|
||||
</> : <></>;
|
||||
}
|
||||
|
||||
export const RouteSecurity = ({route}) => {
|
||||
|
|
|
@ -37,21 +37,26 @@ const a11yProps = (index) => {
|
|||
};
|
||||
};
|
||||
|
||||
const PrettyTabbedView = ({ tabs, isLoading }) => {
|
||||
const PrettyTabbedView = ({ tabs, isLoading, currentTab, setCurrentTab }) => {
|
||||
const [value, setValue] = useState(0);
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'));
|
||||
|
||||
if((currentTab != null && typeof currentTab === 'number') && value !== currentTab)
|
||||
setValue(currentTab);
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
setCurrentTab && setCurrentTab(newValue);
|
||||
};
|
||||
|
||||
const handleSelectChange = (event) => {
|
||||
setValue(event.target.value);
|
||||
setCurrentTab && setCurrentTab(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" height="100%" flexDirection={isMobile ? 'column' : 'row'}>
|
||||
{isMobile ? (
|
||||
{(isMobile) ? (
|
||||
<Select value={value} onChange={handleSelectChange} sx={{ minWidth: 120, marginBottom: '15px' }}>
|
||||
{tabs.map((tab, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
|
@ -70,7 +75,7 @@ const PrettyTabbedView = ({ tabs, isLoading }) => {
|
|||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
style={{fontWeight: !tab.children ? '1000' : '', }}
|
||||
disabled={!tab.children} key={index}
|
||||
disabled={tab.disabled || !tab.children} key={index}
|
||||
label={tab.title} {...a11yProps(index)}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -6,12 +6,12 @@ import TableContainer from '@mui/material/TableContainer';
|
|||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
|
||||
import { CircularProgress, Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo, buttons, fullWidth }) => {
|
||||
const PrettyTableView = ({ isLoading, getKey, data, columns, sort, onRowClick, linkTo, buttons, fullWidth }) => {
|
||||
const [search, setSearch] = React.useState('');
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
|
@ -43,7 +43,17 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo, buttons, f
|
|||
/>
|
||||
{buttons}
|
||||
</Stack>
|
||||
<TableContainer style={{width: fullWidth ? '100%': '', background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
|
||||
|
||||
{isLoading && (<div style={{height: '550px'}}>
|
||||
<center>
|
||||
<br />
|
||||
<CircularProgress />
|
||||
</center>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{!isLoading && <TableContainer style={{width: fullWidth ? '100%': '', background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
|
||||
<Table aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
@ -64,6 +74,10 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo, buttons, f
|
|||
})
|
||||
return found;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (!sort) return 0;
|
||||
return sort(a, b);
|
||||
})
|
||||
.map((row, key) => (
|
||||
<TableRow
|
||||
key={getKey(row)}
|
||||
|
@ -98,7 +112,7 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo, buttons, f
|
|||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableContainer>}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
|
62
client/src/components/terminal.jsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Box, Button, Checkbox, CircularProgress, Input, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
|
||||
import * as API from '../api';
|
||||
import LogLine from '../components/logLine';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
const Terminal = ({ logs, setLogs, fetchLogs, docker }) => {
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [hasScrolled, setHasScrolled] = useState(false);
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
const screenMin = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||
|
||||
const bottomRef = useRef(null);
|
||||
const terminalRef = useRef(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
bottomRef.current.scrollIntoView({});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasScrolled) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [logs]);
|
||||
|
||||
const handleScroll = (event) => {
|
||||
if (event.target.scrollHeight - event.target.scrollTop === event.target.clientHeight) {
|
||||
setHasScrolled(false);
|
||||
}else {
|
||||
setHasScrolled(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={terminalRef}
|
||||
sx={{
|
||||
minHeight: '50px',
|
||||
maxHeight: 'calc(1vh * 80 - 200px)',
|
||||
overflow: 'auto',
|
||||
padding: '10px',
|
||||
wordBreak: 'break-all',
|
||||
background: '#272d36',
|
||||
color: '#fff',
|
||||
borderTop: '3px solid ' + theme.palette.primary.main
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{logs && logs.map((log, index) => (
|
||||
<div key={index} style={{paddingTop: (!screenMin) ? '10px' : '2px'}}>
|
||||
<LogLine message={log.output} docker isMobile={!screenMin} />
|
||||
</div>
|
||||
))}
|
||||
{fetching && <CircularProgress sx={{ mt: 1, mb: 2 }} />}
|
||||
<div ref={bottomRef} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Terminal;
|
|
@ -1,7 +1,7 @@
|
|||
// ==============================|| THEME CONFIG ||============================== //
|
||||
|
||||
const config = {
|
||||
defaultPath: '/ui',
|
||||
defaultPath: '/cosmos-ui',
|
||||
fontFamily: `'Public Sans', sans-serif`,
|
||||
i18n: 'en',
|
||||
miniDrawer: false,
|
||||
|
|
|
@ -21,6 +21,58 @@
|
|||
}
|
||||
|
||||
|
||||
@keyframes pulsing {
|
||||
0% { -webkit-transform: scale(1); }
|
||||
50% { -webkit-transform: scale(1.1); }
|
||||
100% { -webkit-transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
.dot {
|
||||
background-color: #222;
|
||||
border-radius: 100%;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin: 0 0.25rem;
|
||||
|
||||
/* Animation */
|
||||
animation: bounce 1.4s infinite;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dot {
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.dot:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.shinyButton {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -69,4 +121,31 @@
|
|||
|
||||
.MuiAlert-icon {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-image:empty {
|
||||
/* background: url('assets/images/icons/cosmos_gray.png') no-repeat center center;
|
||||
background-size: contain; */
|
||||
}
|
||||
|
||||
.raw-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.raw-table table th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.raw-table table th, .raw-table table td {
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.pulsing {
|
||||
animation: pulsing 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.force-light > * {
|
||||
color: black !important;
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'; // import this if you need to parse custom formats
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat'; // import this for localized formatting
|
||||
import 'dayjs/locale/en-gb';
|
||||
|
||||
// scroll bar
|
||||
import 'simplebar/src/simplebar.css';
|
||||
|
@ -17,6 +21,13 @@ import './index.css';
|
|||
import App from './App';
|
||||
import { store } from './store';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/en-gb';
|
||||
dayjs.extend(customParseFormat); // if needed
|
||||
dayjs.extend(localizedFormat); // if needed
|
||||
dayjs.locale('en-gb');
|
||||
|
||||
// ==============================|| MAIN - REACT DOM RENDER ||============================== //
|
||||
|
||||
|
@ -25,8 +36,10 @@ const root = createRoot(container); // createRoot(container!) if you use TypeScr
|
|||
root.render(
|
||||
<StrictMode>
|
||||
<ReduxProvider store={store}>
|
||||
<BrowserRouter basename="/">
|
||||
<App />
|
||||
<BrowserRouter basename="/">
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<App />
|
||||
</LocalizationProvider>
|
||||
</BrowserRouter>
|
||||
</ReduxProvider>
|
||||
</StrictMode>
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
|
||||
import * as API from './api';
|
||||
import { useEffect } from 'react';
|
||||
import { redirectToLocal } from './utils/indexs';
|
||||
|
||||
const IsLoggedIn = () => useEffect(() => {
|
||||
console.log("CHECK LOGIN")
|
||||
const urlSearch = encodeURIComponent(window.location.search);
|
||||
const redirectToURL = (window.location.pathname + urlSearch);
|
||||
|
||||
API.auth.me().then((data) => {
|
||||
if(data.status != 'OK') {
|
||||
if(data.status == 'NEW_INSTALL') {
|
||||
window.location.href = '/ui/newInstall';
|
||||
redirectToLocal('/cosmos-ui/newInstall');
|
||||
} else if (data.status == 'error' && data.code == "HTTP004") {
|
||||
window.location.href = '/ui/login';
|
||||
redirectToLocal('/cosmos-ui/login?redirect=' + redirectToURL);
|
||||
} else if (data.status == 'error' && data.code == "HTTP006") {
|
||||
window.location.href = '/ui/loginmfa';
|
||||
redirectToLocal('/cosmos-ui/loginmfa?redirect=' + redirectToURL);
|
||||
} else if (data.status == 'error' && data.code == "HTTP007") {
|
||||
window.location.href = '/ui/newmfa';
|
||||
redirectToLocal('/cosmos-ui/newmfa?redirect=' + redirectToURL);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -47,7 +47,7 @@ const NavGroup = ({ item }) => {
|
|||
}
|
||||
sx={{ mb: drawerOpen ? 1.5 : 0, py: 0, zIndex: 0 }}
|
||||
>
|
||||
{navCollapse}
|
||||
{navCollapse}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography }
|
|||
|
||||
// project import
|
||||
import { activeItem } from '../../../../../store/reducers/menu';
|
||||
import { useClientInfos } from '../../../../../utils/hooks';
|
||||
|
||||
// ==============================|| NAVIGATION - LIST ITEM ||============================== //
|
||||
|
||||
|
@ -17,15 +18,23 @@ const NavItem = ({ item, level }) => {
|
|||
const dispatch = useDispatch();
|
||||
const menu = useSelector((state) => state.menu);
|
||||
const { drawerOpen, openItem } = menu;
|
||||
const {role} = useClientInfos();
|
||||
const isAdmin = role === "2";
|
||||
|
||||
if (item.adminOnly && !isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let itemTarget = '_self';
|
||||
if (item.target) {
|
||||
itemTarget = '_blank';
|
||||
}
|
||||
|
||||
let listItemProps = { component: forwardRef((props, ref) => <Link ref={ref} {...props} to={item.url} target={itemTarget} />) };
|
||||
let listItemProps = { component: forwardRef((props, ref) => <Link ref={ref} {...props} to={item.url} target={itemTarget}
|
||||
rel={itemTarget === '_blank' ? 'noopener noreferrer' : ''}
|
||||
/>) };
|
||||
if (item?.external) {
|
||||
listItemProps = { component: 'a', href: item.url, target: itemTarget };
|
||||
listItemProps = { component: 'a', href: item.url, target: itemTarget, rel: itemTarget === '_blank' ? 'noopener noreferrer' : '' };
|
||||
}
|
||||
|
||||
const itemHandler = (id) => {
|
||||
|
@ -52,6 +61,25 @@ const NavItem = ({ item, level }) => {
|
|||
const textColor = 'text.primary';
|
||||
const iconSelectedColor = 'primary.main';
|
||||
|
||||
// SET BETA (TODO REMOVE)
|
||||
if(item.title === "Constellation")
|
||||
item.title = <>{item.title} <span style={{
|
||||
color: 'gray',
|
||||
fontSize: '11px',
|
||||
textDecoration: 'italic',
|
||||
transform: 'translateY(-5px)',
|
||||
display: 'inline-block',
|
||||
}}>Beta</span></>;
|
||||
|
||||
if(item.title === "Monitoring")
|
||||
item.title = <>{item.title} <span style={{
|
||||
color: 'gray',
|
||||
fontSize: '11px',
|
||||
textDecoration: 'italic',
|
||||
transform: 'translateY(-5px)',
|
||||
display: 'inline-block',
|
||||
}}>New!</span></>;
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
{...listItemProps}
|
||||
|
|
|
@ -19,9 +19,9 @@ const DrawerHeader = ({ open }) => {
|
|||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Logo />
|
||||
<Chip
|
||||
label={version}
|
||||
label={version.replace('unstable', '')}
|
||||
size="small"
|
||||
sx={{ height: 16, '& .MuiChip-label': { fontSize: '0.625rem', py: 0.25 } }}
|
||||
sx={{ height: 16, '& .MuiChip-label': { fontSize: '0.55rem', py: 0.25 } }}
|
||||
component="a"
|
||||
href="/"
|
||||
clickable
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
@ -19,12 +19,16 @@ import {
|
|||
Typography,
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
// project import
|
||||
import MainCard from '../../../../components/MainCard';
|
||||
import Transitions from '../../../../components/@extended/Transitions';
|
||||
// assets
|
||||
import { BellOutlined, CloseOutlined, GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { BellOutlined, CloseOutlined, ExclamationCircleOutlined, GiftOutlined, InfoCircleOutlined, MessageOutlined, SettingOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
|
||||
import * as API from '../../../../api';
|
||||
import { redirectToLocal } from '../../../../utils/indexs';
|
||||
|
||||
// sx styles
|
||||
const avatarSX = {
|
||||
|
@ -48,10 +52,55 @@ const actionSX = {
|
|||
const Notification = () => {
|
||||
const theme = useTheme();
|
||||
const matchesXs = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [from, setFrom] = useState('');
|
||||
|
||||
const refreshNotifications = () => {
|
||||
API.users.getNotifs(from).then((res) => {
|
||||
setNotifications(() => res.data);
|
||||
});
|
||||
};
|
||||
|
||||
const setAsRead = () => {
|
||||
let unread = [];
|
||||
|
||||
notifications.forEach((notif) => {
|
||||
if (!notif.Read) {
|
||||
unread.push(notif.ID);
|
||||
}
|
||||
})
|
||||
|
||||
if (unread.length > 0) {
|
||||
API.users.readNotifs(unread);
|
||||
}
|
||||
}
|
||||
|
||||
const setLocalAsRead = (id) => {
|
||||
let newN = notifications.map((notif) => {
|
||||
notif.Read = true;
|
||||
return notif;
|
||||
})
|
||||
setNotifications(newN);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshNotifications();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
refreshNotifications();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const anchorRef = useRef(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!open) {
|
||||
setAsRead();
|
||||
}
|
||||
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
|
@ -59,12 +108,48 @@ const Notification = () => {
|
|||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setLocalAsRead();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const getNotifIcon = (notification) => {
|
||||
switch (notification.Level) {
|
||||
case 'warn':
|
||||
return <Avatar
|
||||
sx={{
|
||||
color: 'warning.main',
|
||||
bgcolor: 'warning.lighter'
|
||||
}}
|
||||
>
|
||||
<WarningOutlined />
|
||||
</Avatar>
|
||||
case 'error':
|
||||
return <Avatar
|
||||
sx={{
|
||||
color: 'error.main',
|
||||
bgcolor: 'error.lighter'
|
||||
}}
|
||||
>
|
||||
<ExclamationCircleOutlined />
|
||||
</Avatar>
|
||||
default:
|
||||
|
||||
return <Avatar
|
||||
sx={{
|
||||
color: 'info.main',
|
||||
bgcolor: 'info.lighter'
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined />
|
||||
</Avatar>
|
||||
}
|
||||
};
|
||||
|
||||
const iconBackColor = theme.palette.mode === 'dark' ? 'grey.700' : 'grey.100';
|
||||
const iconBackColorOpen = theme.palette.mode === 'dark' ? 'grey.800' : 'grey.200';
|
||||
|
||||
const nbUnread = notifications.filter((notif) => !notif.Read).length;
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
|
||||
<IconButton
|
||||
|
@ -77,7 +162,7 @@ const Notification = () => {
|
|||
aria-haspopup="true"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<Badge badgeContent={4} color="primary">
|
||||
<Badge badgeContent={nbUnread} color="error">
|
||||
<BellOutlined />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
|
@ -127,6 +212,8 @@ const Notification = () => {
|
|||
<List
|
||||
component="nav"
|
||||
sx={{
|
||||
maxHeight: 350,
|
||||
overflow: 'auto',
|
||||
p: 0,
|
||||
'& .MuiListItemButton-root': {
|
||||
py: 0.5,
|
||||
|
@ -135,127 +222,43 @@ const Notification = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
color: 'success.main',
|
||||
bgcolor: 'success.lighter'
|
||||
}}
|
||||
>
|
||||
<GiftOutlined />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6">
|
||||
It's{' '}
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Cristina danny's
|
||||
</Typography>{' '}
|
||||
birthday today.
|
||||
</Typography>
|
||||
}
|
||||
secondary="2 min ago"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
3:00 AM
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
bgcolor: 'primary.lighter'
|
||||
}}
|
||||
>
|
||||
<MessageOutlined />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6">
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Aida Burg
|
||||
</Typography>{' '}
|
||||
commented your post.
|
||||
</Typography>
|
||||
}
|
||||
secondary="5 August"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
6:00 PM
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
color: 'error.main',
|
||||
bgcolor: 'error.lighter'
|
||||
}}
|
||||
>
|
||||
<SettingOutlined />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6">
|
||||
Your Profile is Complete
|
||||
<Typography component="span" variant="subtitle1">
|
||||
60%
|
||||
</Typography>{' '}
|
||||
</Typography>
|
||||
}
|
||||
secondary="7 hours ago"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
2:45 PM
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
bgcolor: 'primary.lighter'
|
||||
}}
|
||||
>
|
||||
C
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6">
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Cristina Danny
|
||||
</Typography>{' '}
|
||||
invited to join{' '}
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Meeting.
|
||||
{notifications && notifications.map(notification => (<>
|
||||
<ListItemButton onClick={() => {
|
||||
notification.Link && redirectToLocal(notification.Link);
|
||||
}}
|
||||
style={{
|
||||
borderLeft: notification.Read ? 'none' : `4px solid ${notification.Level === 'warn' ? theme.palette.warning.main : notification.Level === 'error' ? theme.palette.error.main : theme.palette.info.main}`,
|
||||
paddingLeft: notification.Read ? '14px' : '10px',
|
||||
}}>
|
||||
|
||||
<ListItemAvatar>
|
||||
{getNotifIcon(notification)}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={<>
|
||||
<Typography variant={notification.Read ? 'body' : 'h6'} noWrap>
|
||||
{notification.Title}
|
||||
</Typography>
|
||||
<div style={{
|
||||
overflow: 'hidden',
|
||||
maxHeight: '48px',
|
||||
borderLeft: '1px solid grey',
|
||||
paddingLeft: '8px',
|
||||
margin: '2px'
|
||||
}}>
|
||||
{notification.Message}
|
||||
</div></>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
{timeago.format(notification.Date)}
|
||||
</Typography>
|
||||
}
|
||||
secondary="Daily scrum meeting time"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
9:10 PM
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
<ListItemButton sx={{ textAlign: 'center', py: `${12}px !important` }}>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider /></>))}
|
||||
|
||||
{/* <ListItemButton sx={{ textAlign: 'center', py: `${12}px !important` }}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6" color="primary">
|
||||
|
@ -263,7 +266,7 @@ const Notification = () => {
|
|||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItemButton> */}
|
||||
</List>
|
||||
</MainCard>
|
||||
</ClickAwayListener>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// material-ui
|
||||
import { Box, Chip, IconButton, Link, useMediaQuery } from '@mui/material';
|
||||
import { Box, Chip, IconButton, Link, Stack, useMediaQuery } from '@mui/material';
|
||||
import { GithubOutlined } from '@ant-design/icons';
|
||||
|
||||
// project import
|
||||
|
@ -18,10 +18,13 @@ const HeaderContent = () => {
|
|||
{!matchesXs && <Search />}
|
||||
{matchesXs && <Box sx={{ width: '100%', ml: 1 }} />}
|
||||
|
||||
<Link href="/ui/logout" underline="none">
|
||||
<Chip label="Logout" />
|
||||
</Link>
|
||||
{/* <Notification /> */}
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Notification />
|
||||
|
||||
<Link href="/cosmos-ui/logout" underline="none">
|
||||
<Chip label="Logout" />
|
||||
</Link>
|
||||
</Stack>
|
||||
{/* {!matchesXs && <Profile />}
|
||||
{matchesXs && <MobileSection />} */}
|
||||
</>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// assets
|
||||
import { HomeOutlined, AppstoreOutlined, DashboardOutlined } from '@ant-design/icons';
|
||||
import { HomeOutlined, AppstoreOutlined, DashboardOutlined, AppstoreAddOutlined } from '@ant-design/icons';
|
||||
|
||||
// icons
|
||||
const icons = {
|
||||
|
@ -17,16 +17,25 @@ const dashboard = {
|
|||
id: 'home',
|
||||
title: 'Home',
|
||||
type: 'item',
|
||||
url: '/ui/',
|
||||
url: '/cosmos-ui/',
|
||||
icon: icons.HomeOutlined,
|
||||
breadcrumbs: false
|
||||
},
|
||||
{
|
||||
id: 'dashboard',
|
||||
title: 'Dashboard',
|
||||
title: 'Monitoring',
|
||||
type: 'item',
|
||||
url: '/ui/dashboard',
|
||||
url: '/cosmos-ui/monitoring',
|
||||
icon: DashboardOutlined,
|
||||
breadcrumbs: false,
|
||||
adminOnly: true
|
||||
},
|
||||
{
|
||||
id: 'market',
|
||||
title: 'Market',
|
||||
type: 'item',
|
||||
url: '/cosmos-ui/market-listing',
|
||||
icon: AppstoreAddOutlined,
|
||||
breadcrumbs: false
|
||||
},
|
||||
]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// assets
|
||||
import { ProfileOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons';
|
||||
import { ProfileOutlined, PicLeftOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons';
|
||||
import ConstellationIcon from '../assets/images/icons/constellation.png'
|
||||
|
||||
// icons
|
||||
const icons = {
|
||||
|
@ -7,7 +8,6 @@ const icons = {
|
|||
ProfileOutlined,
|
||||
SettingOutlined
|
||||
};
|
||||
|
||||
// ==============================|| MENU ITEMS - EXTRA PAGES ||============================== //
|
||||
|
||||
const pages = {
|
||||
|
@ -19,28 +19,46 @@ const pages = {
|
|||
id: 'servapps',
|
||||
title: 'ServApps',
|
||||
type: 'item',
|
||||
url: '/ui/servapps',
|
||||
icon: AppstoreOutlined
|
||||
url: '/cosmos-ui/servapps',
|
||||
icon: AppstoreOutlined,
|
||||
adminOnly: true
|
||||
},
|
||||
{
|
||||
id: 'url',
|
||||
title: 'URLs',
|
||||
type: 'item',
|
||||
url: '/ui/config-url',
|
||||
url: '/cosmos-ui/config-url',
|
||||
icon: icons.NodeExpandOutlined,
|
||||
},
|
||||
{
|
||||
id: 'constellation',
|
||||
title: 'Constellation',
|
||||
type: 'item',
|
||||
url: '/cosmos-ui/constellation',
|
||||
icon: () => <img height="28px" width="28px" style={{marginLeft: "-6px"}} src={ConstellationIcon} />,
|
||||
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
title: 'Users',
|
||||
type: 'item',
|
||||
url: '/ui/config-users',
|
||||
url: '/cosmos-ui/config-users',
|
||||
icon: icons.ProfileOutlined,
|
||||
adminOnly: true
|
||||
},
|
||||
{
|
||||
id: 'openid',
|
||||
title: 'OpenID',
|
||||
type: 'item',
|
||||
url: '/cosmos-ui/openid-manage',
|
||||
icon: PicLeftOutlined,
|
||||
adminOnly: true
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
title: 'Configuration',
|
||||
type: 'item',
|
||||
url: '/ui/config-general',
|
||||
url: '/cosmos-ui/config-general',
|
||||
icon: icons.SettingOutlined,
|
||||
}
|
||||
]
|
||||
|
|
|
@ -41,7 +41,7 @@ const support = {
|
|||
id: 'documentation',
|
||||
title: 'Documentation',
|
||||
type: 'item',
|
||||
url: 'https://github.com/azukaar/Cosmos-Server/wiki',
|
||||
url: 'https://cosmos-cloud.io/doc',
|
||||
icon: QuestionOutlined,
|
||||
external: true,
|
||||
target: true
|
||||
|
|
|
@ -7,10 +7,15 @@ import { Grid, Stack, Typography } from '@mui/material';
|
|||
import AuthLogin from './auth-forms/AuthLogin';
|
||||
import AuthWrapper from './AuthWrapper';
|
||||
|
||||
const selfport = new URL(window.location.href).port
|
||||
const selfprotocol = new URL(window.location.href).protocol + "//"
|
||||
const selfHostname = selfprotocol + (new URL(window.location.href).hostname) + (selfport ? ":" + selfport : "")
|
||||
|
||||
// ================================|| LOGIN ||================================ //
|
||||
|
||||
const Login = () => (
|
||||
<AuthWrapper>
|
||||
<link rel="openid2.provider openid.server" href={selfHostname + "/oauth2/auth"} />
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
|
||||
|
|
|
@ -9,6 +9,7 @@ import AuthWrapper from './AuthWrapper';
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import * as API from '../../api';
|
||||
import { redirectTo, redirectToLocal } from '../../utils/indexs';
|
||||
|
||||
// ================================|| REGISTER ||================================ //
|
||||
|
||||
|
@ -17,7 +18,7 @@ const Logout = () => {
|
|||
API.auth.logout()
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
window.location.href = '/ui/login';
|
||||
redirectToLocal('/cosmos-ui/login');
|
||||
}, 2000);
|
||||
});
|
||||
},[]);
|
||||
|
|
|
@ -15,7 +15,7 @@ const Signup = () => (
|
|||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
|
||||
<Typography variant="h3">Sign up</Typography>
|
||||
<Typography component={Link} to="/ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
|
||||
<Typography component={Link} to="/cosmos-ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
|
||||
Already have an account?
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
|
|
@ -31,6 +31,7 @@ import AnimateButton from '../../../components/@extended/AnimateButton';
|
|||
// assets
|
||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { redirectToLocal } from '../../../utils/indexs';
|
||||
|
||||
// ============================|| FIREBASE - LOGIN ||============================ //
|
||||
|
||||
|
@ -53,14 +54,14 @@ const AuthLogin = () => {
|
|||
const notLogged = urlSearchParams.get('notlogged') == 1;
|
||||
const notLoggedAdmin = urlSearchParams.get('notlogged') == 2;
|
||||
const invalid = urlSearchParams.get('invalid') == 1;
|
||||
const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/ui';
|
||||
const redirectToURL = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/cosmos-ui';
|
||||
|
||||
useEffect(() => {
|
||||
API.auth.me().then((data) => {
|
||||
if(data.status == 'OK') {
|
||||
window.location.href = redirectTo;
|
||||
redirectToLocal(redirectToURL);
|
||||
} else if(data.status == 'NEW_INSTALL') {
|
||||
window.location.href = '/ui/newInstall';
|
||||
redirectToLocal('/cosmos-ui/newInstall');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -103,7 +104,7 @@ const AuthLogin = () => {
|
|||
return API.auth.login(values).then((data) => {
|
||||
setStatus({ success: true });
|
||||
setSubmitting(false);
|
||||
window.location.href = redirectTo;
|
||||
redirectToLocal(redirectToURL);
|
||||
}).catch((err) => {
|
||||
setStatus({ success: false });
|
||||
if(err.code == 'UL001') {
|
||||
|
@ -190,7 +191,7 @@ const AuthLogin = () => {
|
|||
}
|
||||
label={<Typography variant="h6">Keep me sign in</Typography>}
|
||||
/>*/}
|
||||
{showResetPassword && <Link variant="h6" component={RouterLink} to="/ui/forgot-password" color="primary">
|
||||
{showResetPassword && <Link variant="h6" component={RouterLink} to="/cosmos-ui/forgot-password" color="primary">
|
||||
Forgot Your Password?
|
||||
</Link>}
|
||||
{!showResetPassword && <Typography variant="h6">
|
||||
|
|
|
@ -33,6 +33,7 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
|
|||
// assets
|
||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { redirectToLocal } from '../../../utils/indexs';
|
||||
|
||||
// ============================|| FIREBASE - REGISTER ||============================ //
|
||||
|
||||
|
@ -73,8 +74,8 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
|
|||
.max(255)
|
||||
.required('Password is required')
|
||||
.matches(
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{9,})/,
|
||||
'Must Contain 9 Characters, One Uppercase, One Lowercase, One Number and one special case Character'
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[~!@#$%\^&\*\(\)_\+=\-\{\[\}\]:;"'<,>\/])(?=.{9,})/,
|
||||
'Must Contain 9 Characters, One Uppercase, One Lowercase, One Number and one special case Character (~!@#$%^&*()_+=-{[}]:;"\'<>.?/)'
|
||||
),
|
||||
})}
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
|
@ -85,7 +86,7 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
|
|||
}).then((res) => {
|
||||
setStatus({ success: true });
|
||||
setSubmitting(false);
|
||||
window.location.href = '/ui/login';
|
||||
redirectToLocal('/cosmos-ui/login');
|
||||
}).catch((err) => {
|
||||
setStatus({ success: false });
|
||||
setErrors({ submit: err.message });
|
||||
|
|
|
@ -109,7 +109,7 @@ const ForgotPassword = () => {
|
|||
variant="contained"
|
||||
color="primary"
|
||||
component={Link}
|
||||
to="/ui/login"
|
||||
to="/cosmos-ui/login"
|
||||
>
|
||||
Back to login
|
||||
</Button>
|
||||
|
|
|
@ -32,17 +32,18 @@ import { useTheme } from '@mui/material/styles';
|
|||
import { Formik } from 'formik';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { CosmosCollapse } from '../config/users/formShortcuts';
|
||||
import { redirectToLocal } from '../../utils/indexs';
|
||||
|
||||
const MFALoginForm = () => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/ui';
|
||||
const redirectToURL = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/cosmos-ui';
|
||||
|
||||
useEffect(() => {
|
||||
API.auth.me().then((data) => {
|
||||
if(data.status == 'OK') {
|
||||
window.location.href = redirectTo;
|
||||
redirectToLocal(redirectToURL);
|
||||
} else if(data.status == 'NEW_INSTALL') {
|
||||
window.location.href = '/ui/newInstall';
|
||||
redirectToLocal('/cosmos-ui/newInstall');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -56,7 +57,7 @@ const MFALoginForm = () => {
|
|||
})}
|
||||
onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
|
||||
API.users.check2FA(values.token).then((data) => {
|
||||
window.location.href = redirectTo;
|
||||
redirectToLocal(redirectToURL);
|
||||
}).catch((error) => {
|
||||
console.log(error)
|
||||
setStatus({ success: false });
|
||||
|
@ -76,6 +77,7 @@ const MFALoginForm = () => {
|
|||
{...formik.getFieldProps('token')}
|
||||
error={formik.touched.token && formik.errors.token && true}
|
||||
helperText={formik.touched.token && formik.errors.token && formik.errors.token}
|
||||
autoFocus
|
||||
/>
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
|
@ -149,7 +151,7 @@ const MFASetup = () => {
|
|||
<MFALoginForm />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Link to="/ui/logout">
|
||||
<Link to="/cosmos-ui/logout">
|
||||
<Typography variant="h5">Logout</Typography>
|
||||
</Link>
|
||||
</Grid>
|
||||
|
|
113
client/src/pages/authentication/openid.jsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
|
||||
// material-ui
|
||||
import { Checkbox, Grid, Stack, Typography } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import AuthLogin from './auth-forms/AuthLogin';
|
||||
import AuthWrapper from './AuthWrapper';
|
||||
import { getFaviconURL } from '../../utils/routes';
|
||||
import IsLoggedIn from '../../isLoggedIn';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { Field, useFormik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
|
||||
// ================================|| LOGIN ||================================ //
|
||||
|
||||
const OpenID = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const client_id = searchParams.get("client_id")
|
||||
const redirect_uri = searchParams.get("redirect_uri")
|
||||
const scope = searchParams.get("scope")
|
||||
const entireSearch = searchParams.toString()
|
||||
const [checkedScopes, setCheckedScopes] = useState(["openid"])
|
||||
|
||||
let icon;
|
||||
|
||||
// get hostname from redirect_uri with port
|
||||
let port, protocol, appHostname;
|
||||
|
||||
try {
|
||||
port = new URL(redirect_uri).port
|
||||
protocol = new URL(redirect_uri).protocol + "//"
|
||||
appHostname = protocol + (new URL(redirect_uri).hostname) + (port ? ":" + port : "")
|
||||
icon = getFaviconURL({
|
||||
Mode: 'PROXY',
|
||||
Target: appHostname
|
||||
});
|
||||
} catch (e) {
|
||||
icon = getFaviconURL();
|
||||
}
|
||||
|
||||
const selfport = new URL(window.location.href).port
|
||||
const selfprotocol = new URL(window.location.href).protocol + "//"
|
||||
const selfHostname = selfprotocol + (new URL(window.location.href).hostname) + (selfport ? ":" + selfport : "")
|
||||
|
||||
const onchange = (e, scope) => {
|
||||
if (e.target.checked) {
|
||||
setCheckedScopes([...checkedScopes, scope])
|
||||
} else {
|
||||
setCheckedScopes(checkedScopes.filter((scope) => scope != scope))
|
||||
}
|
||||
}
|
||||
|
||||
return (<AuthWrapper>
|
||||
<IsLoggedIn />
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="h3">Login with OpenID - {client_id}</Typography>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="baseline" spacing={2} style={{
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<img src={icon} alt={'icon'} width="64px" />
|
||||
<div>
|
||||
You are about to login into <b>{client_id}</b>. <br />
|
||||
Check which permissions you are giving to this application. <br />
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<link rel="openid2.provider openid.server" href={selfHostname + "/oauth2/auth"} />
|
||||
<form action={"/oauth2/auth?" + entireSearch} method="post">
|
||||
<input type="hidden" name="client_id" value={client_id} />
|
||||
{scope.split(' ').map((scope) => {
|
||||
return scope == "openid" ? <div>
|
||||
<input type="checkbox" name="scopes" value={scope} checked hidden />
|
||||
<Checkbox checked disabled />
|
||||
account
|
||||
</div>
|
||||
: <div>
|
||||
<input type="checkbox" name="scopes" hidden value={scope} checked={checkedScopes.includes(scope)} />
|
||||
<Checkbox onChange={(e) => onchange(e, scope)} />
|
||||
{scope}
|
||||
</div>
|
||||
})}
|
||||
<div style={{
|
||||
fontSize: '0.8rem',
|
||||
marginTop: '15px',
|
||||
marginBottom: '20px',
|
||||
opacity: '0.8',
|
||||
fontStyle: 'italic',
|
||||
}}>
|
||||
You will be redirected to <b>{redirect_uri}</b> after login. <br />
|
||||
</div>
|
||||
|
||||
<LoadingButton
|
||||
disableElevation
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
OpenID Login
|
||||
</LoadingButton>
|
||||
</form>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AuthWrapper>)
|
||||
};
|
||||
|
||||
export default OpenID;
|
|
@ -8,6 +8,8 @@ import * as API from "../../api";
|
|||
import RouteSecurity from "./routes/routeSecurity";
|
||||
import RouteOverview from "./routes/routeoverview";
|
||||
import IsLoggedIn from "../../isLoggedIn";
|
||||
import RouteMetrics from "../dashboard/routeMonitoring";
|
||||
import EventExplorerStandalone from "../dashboard/eventsExplorerStandalone";
|
||||
|
||||
const RouteConfigPage = () => {
|
||||
const { routeName } = useParams();
|
||||
|
@ -53,17 +55,23 @@ const RouteConfigPage = () => {
|
|||
submitButton
|
||||
routeConfig={currentRoute}
|
||||
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||
config={config}
|
||||
/>
|
||||
},
|
||||
{
|
||||
title: 'Security',
|
||||
children: <RouteSecurity
|
||||
routeConfig={currentRoute}
|
||||
config={config}
|
||||
/>
|
||||
},
|
||||
{
|
||||
title: 'Permissions',
|
||||
children: <div>WIP</div>
|
||||
title: 'Monitoring',
|
||||
children: <RouteMetrics routeName={routeName} />
|
||||
},
|
||||
{
|
||||
title: 'Events',
|
||||
children: <EventExplorerStandalone initLevel='info' initSearch={`{"object":"route@${routeName}"}`}/>
|
||||
},
|
||||
]}/>}
|
||||
|
||||
|
|
|
@ -27,8 +27,10 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
|
|||
});
|
||||
}
|
||||
|
||||
const routes = config.HTTPConfig.ProxyConfig.Routes || [];
|
||||
|
||||
return <>
|
||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} config={config} newRoute />
|
||||
<Dialog open={openNewModal} onClose={() => setOpenNewModal(false)}>
|
||||
<DialogTitle>New URL</DialogTitle>
|
||||
{openNewModal && <>
|
||||
|
@ -56,7 +58,7 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
|
|||
Enabled: true,
|
||||
}
|
||||
}}
|
||||
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||
routeNames={routes.map((r) => r.Name)}
|
||||
setRouteConfig={(_newRoute) => {
|
||||
setNewRoute(sanitizeRoute(_newRoute));
|
||||
}}
|
||||
|
@ -64,6 +66,7 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
|
|||
down={() => {}}
|
||||
deleteRoute={() => {}}
|
||||
noControls
|
||||
config={config}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
|
|
|
@ -14,11 +14,11 @@ import RestartModal from '../users/restart';
|
|||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
|
||||
import { snackit } from '../../../api/wrap';
|
||||
|
||||
const RouteSecurity = ({ routeConfig }) => {
|
||||
const RouteSecurity = ({ routeConfig, config }) => {
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
|
||||
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
|
||||
|
||||
{routeConfig && <>
|
||||
<Formik
|
||||
|
@ -31,6 +31,8 @@ const RouteSecurity = ({ routeConfig }) => {
|
|||
BlockAPIAbuse: routeConfig.BlockAPIAbuse,
|
||||
BlockCommonBots: routeConfig.BlockCommonBots,
|
||||
AdminOnly: routeConfig.AdminOnly,
|
||||
VerboseForwardHeader: routeConfig.VerboseForwardHeader,
|
||||
DisableHeaderHardening: routeConfig.DisableHeaderHardening,
|
||||
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
|
||||
_SmartShield_PolicyStrictness: (routeConfig.SmartShield ? routeConfig.SmartShield.PolicyStrictness : 0),
|
||||
_SmartShield_PerUserTimeBudget: (routeConfig.SmartShield ? routeConfig.SmartShield.PerUserTimeBudget : 0),
|
||||
|
@ -86,10 +88,6 @@ const RouteSecurity = ({ routeConfig }) => {
|
|||
<Stack spacing={2}>
|
||||
<MainCard name={routeConfig.Name} title={'Security'}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
|
||||
</Grid>
|
||||
|
||||
<CosmosFormDivider title={'Authentication'} />
|
||||
|
||||
<CosmosCheckbox
|
||||
|
@ -104,6 +102,20 @@ const RouteSecurity = ({ routeConfig }) => {
|
|||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosFormDivider title={'Headers'} />
|
||||
|
||||
<CosmosCheckbox
|
||||
name="VerboseForwardHeader"
|
||||
label="Forward IP and Host Headers to target"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosCheckbox
|
||||
name="DisableHeaderHardening"
|
||||
label="Disable Header Hardening"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosFormDivider title={'Smart Shield'} />
|
||||
|
||||
<CosmosCheckbox
|
||||
|
@ -167,12 +179,12 @@ const RouteSecurity = ({ routeConfig }) => {
|
|||
|
||||
<CosmosSelect
|
||||
name="_SmartShield_PrivilegedGroups"
|
||||
label="Privileged Groups (comma separated)"
|
||||
placeholder="Privileged Groups"
|
||||
label="Privileged Groups "
|
||||
placeholder="Privileged Group"
|
||||
options={[
|
||||
[0, 'Default'],
|
||||
[1, 'Users'],
|
||||
[2, 'Admin'],
|
||||
[1, 'Users & Admins'],
|
||||
[2, 'Admin Only'],
|
||||
]}
|
||||
formik={formik}
|
||||
/>
|
||||
|
|
|
@ -11,10 +11,11 @@ import {
|
|||
FormHelperText,
|
||||
} from '@mui/material';
|
||||
import RestartModal from '../users/restart';
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
|
||||
import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
|
||||
import { CosmosContainerPicker } from '../users/containerPicker';
|
||||
import { snackit } from '../../../api/wrap';
|
||||
import { ValidateRouteSchema, sanitizeRoute } from '../../../utils/routes';
|
||||
import { isDomain } from '../../../utils/indexs';
|
||||
|
||||
const Hide = ({ children, h }) => {
|
||||
return h ? <div style={{ display: 'none' }}>
|
||||
|
@ -31,11 +32,30 @@ const debounce = (func, wait) => {
|
|||
};
|
||||
};
|
||||
|
||||
const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false, newRoute }) => {
|
||||
const checkHost = debounce((host, setHostError) => {
|
||||
if(isDomain(host)) {
|
||||
API.checkHost(host).then((data) => {
|
||||
setHostError(null)
|
||||
}).catch((err) => {
|
||||
setHostError(err.message)
|
||||
});
|
||||
} else {
|
||||
setHostError(null);
|
||||
}
|
||||
}, 500)
|
||||
|
||||
const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false, newRoute }) => {
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const [hostError, setHostError] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if(routeConfig && routeConfig.Host) {
|
||||
checkHost(routeConfig.Host, setHostError);
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
|
||||
|
||||
{routeConfig && <>
|
||||
<Formik
|
||||
|
@ -46,17 +66,27 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
|
|||
Target: routeConfig.Target,
|
||||
UseHost: routeConfig.UseHost,
|
||||
Host: routeConfig.Host,
|
||||
AcceptInsecureHTTPSTarget: routeConfig.AcceptInsecureHTTPSTarget === true,
|
||||
UsePathPrefix: routeConfig.UsePathPrefix,
|
||||
PathPrefix: routeConfig.PathPrefix,
|
||||
StripPathPrefix: routeConfig.StripPathPrefix,
|
||||
AuthEnabled: routeConfig.AuthEnabled,
|
||||
HideFromDashboard: routeConfig.HideFromDashboard,
|
||||
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
|
||||
RestrictToConstellation: routeConfig.RestrictToConstellation,
|
||||
OverwriteHostHeader: routeConfig.OverwriteHostHeader,
|
||||
WhitelistInboundIPs: routeConfig.WhitelistInboundIPs && routeConfig.WhitelistInboundIPs.join(', '),
|
||||
}}
|
||||
validationSchema={ValidateRouteSchema}
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
if(!submitButton) {
|
||||
return false;
|
||||
} else {
|
||||
let commaSepIps = values.WhitelistInboundIPs;
|
||||
if(commaSepIps) {
|
||||
values.WhitelistInboundIPs = commaSepIps.split(',').map((ip) => ip.trim());
|
||||
}
|
||||
|
||||
let fullValues = {
|
||||
...routeConfig,
|
||||
...values,
|
||||
|
@ -162,11 +192,17 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
|
|||
: <CosmosInputText
|
||||
name="Target"
|
||||
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
|
||||
placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
|
||||
placeholder={formik.values.Mode == "PROXY" ? "http://localhost:8080" : "/path/to/my/app"}
|
||||
formik={formik}
|
||||
/>
|
||||
}
|
||||
|
||||
{formik.values.Target.startsWith('https://') && <CosmosCheckbox
|
||||
name="AcceptInsecureHTTPSTarget"
|
||||
label="Accept Insecure HTTPS Target (not recommended)"
|
||||
formik={formik}
|
||||
/>}
|
||||
|
||||
<CosmosFormDivider title={'Source'} />
|
||||
|
||||
<Grid item xs={12}>
|
||||
|
@ -179,13 +215,21 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
|
|||
formik={formik}
|
||||
/>
|
||||
|
||||
{formik.values.UseHost && <CosmosInputText
|
||||
{formik.values.UseHost && (<><CosmosInputText
|
||||
name="Host"
|
||||
label="Host"
|
||||
placeholder="Host"
|
||||
formik={formik}
|
||||
style={{ paddingLeft: '20px' }}
|
||||
/>}
|
||||
onChange={(e) => {
|
||||
checkHost(e.target.value, setHostError)
|
||||
}}
|
||||
/>
|
||||
{hostError && <Grid item xs={12}>
|
||||
<Alert color='error'>{hostError}</Alert>
|
||||
</Grid>}
|
||||
</>
|
||||
)}
|
||||
|
||||
<CosmosCheckbox
|
||||
name="UsePathPrefix"
|
||||
|
@ -221,6 +265,44 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
|
|||
label="Smart Shield Protection"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosCheckbox
|
||||
name="RestrictToConstellation"
|
||||
label="Restrict access to Constellation VPN"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosCollapse title={'Advanced Settings'}>
|
||||
<Stack spacing={2}>
|
||||
<CosmosCheckbox
|
||||
name="HideFromDashboard"
|
||||
label="Hide from Dashboard"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosFormDivider />
|
||||
<Alert severity='info'>These settings are for advanced users only. Please do not change these unless you know what you are doing.</Alert>
|
||||
<CosmosInputText
|
||||
name="OverwriteHostHeader"
|
||||
label="Overwrite Host Header (use this to chain resolve request from another server/ip)"
|
||||
placeholder="Overwrite Host Header"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<Alert severity='warning'>
|
||||
This setting will filter out all requests that do not come from the specified IPs.
|
||||
This requires your setup to report the true IP of the client. By default it will, but some exotic setup (like installing docker/cosmos on Windows, or behind Cloudlfare)
|
||||
will prevent Cosmos from knowing what is the client's real IP. If you used "Restrict to Constellation" above, Constellation IPs will always be allowed regardless of this setting.
|
||||
</Alert>
|
||||
|
||||
<CosmosInputText
|
||||
name="WhitelistInboundIPs"
|
||||
label="Whitelist Inbound IPs and/or IP ranges (comma separated)"
|
||||
placeholder="Whitelist Inbound IPs"
|
||||
formik={formik}
|
||||
/>
|
||||
</Stack>
|
||||
</CosmosCollapse>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
{submitButton && <MainCard ><Button
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import * as React from 'react';
|
||||
import MainCard from '../../../components/MainCard';
|
||||
import RestartModal from '../users/restart';
|
||||
import { Chip, Divider, Stack, useMediaQuery } from '@mui/material';
|
||||
import { Checkbox, Chip, Divider, FormControlLabel, Stack, useMediaQuery } from '@mui/material';
|
||||
import HostChip from '../../../components/hostChip';
|
||||
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
||||
import { getFaviconURL } from '../../../utils/routes';
|
||||
import * as API from '../../../api';
|
||||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import { CheckOutlined, ClockCircleOutlined, ContainerOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, InfoCircleFilled, InfoCircleOutlined, LockOutlined, NodeExpandOutlined, SafetyCertificateOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import IsLoggedIn from '../../../isLoggedIn';
|
||||
import { redirectToLocal } from '../../../utils/indexs';
|
||||
import { CosmosCheckbox } from '../users/formShortcuts';
|
||||
import { Field } from 'formik';
|
||||
import MiniPlotComponent from '../../dashboard/components/mini-plot';
|
||||
import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
|
||||
import UploadButtons from '../../../components/fileUpload';
|
||||
|
||||
const info = {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
|
@ -16,20 +22,17 @@ const info = {
|
|||
}
|
||||
|
||||
const RouteOverview = ({ routeConfig }) => {
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||
const [confirmDelete, setConfirmDelete] = React.useState(false);
|
||||
|
||||
function deleteRoute(event) {
|
||||
event.stopPropagation();
|
||||
API.config.deleteRoute(routeConfig.Name).then(() => {
|
||||
setOpenModal(true);
|
||||
redirectToLocal('/cosmos-ui/config-url');
|
||||
});
|
||||
}
|
||||
|
||||
return <div style={{ maxWidth: '1000px', width: '100%'}}>
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
|
||||
{routeConfig && <>
|
||||
<MainCard name={routeConfig.Name} title={<div>
|
||||
{routeConfig.Name}
|
||||
|
@ -38,17 +41,34 @@ const RouteOverview = ({ routeConfig }) => {
|
|||
</div>}>
|
||||
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
|
||||
<div>
|
||||
<img src={getFaviconURL(routeConfig)} width="128px" />
|
||||
<ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
|
||||
</div>
|
||||
<Stack spacing={2} >
|
||||
<strong>Description</strong>
|
||||
<Stack spacing={2} style={{ width: '100%' }}>
|
||||
<strong><ContainerOutlined />Description</strong>
|
||||
<div style={info}>{routeConfig.Description}</div>
|
||||
<strong>URL</strong>
|
||||
<strong><NodeExpandOutlined /> URL</strong>
|
||||
<div><HostChip route={routeConfig} /></div>
|
||||
<strong>Target</strong>
|
||||
<strong><InfoCircleOutlined /> Target</strong>
|
||||
<div><RouteMode route={routeConfig} /> <Chip label={routeConfig.Target} /></div>
|
||||
<strong>Security</strong>
|
||||
<strong><SafetyCertificateOutlined/> Security</strong>
|
||||
<div><RouteSecurity route={routeConfig} /></div>
|
||||
<strong><DashboardOutlined/> Monitoring</strong>
|
||||
<div>
|
||||
<MiniPlotComponent agglo metrics={[
|
||||
"cosmos.proxy.route.success." + routeConfig.Name,
|
||||
"cosmos.proxy.route.error." + routeConfig.Name,
|
||||
]} labels={{
|
||||
["cosmos.proxy.route.error." + routeConfig.Name]: "Error",
|
||||
["cosmos.proxy.route.success." + routeConfig.Name]: "Succ."
|
||||
}}/>
|
||||
<MiniPlotComponent agglo metrics={[
|
||||
"cosmos.proxy.route.bytes." + routeConfig.Name,
|
||||
"cosmos.proxy.route.time." + routeConfig.Name,
|
||||
]} labels={{
|
||||
["cosmos.proxy.route.bytes." + routeConfig.Name]: "Bytes",
|
||||
["cosmos.proxy.route.time." + routeConfig.Name]: "Time"
|
||||
}}/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
|
|
|
@ -8,33 +8,41 @@ import {
|
|||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
Link,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
Typography,
|
||||
FormHelperText,
|
||||
Collapse,
|
||||
TextField,
|
||||
MenuItem,
|
||||
|
||||
Skeleton,
|
||||
} from '@mui/material';
|
||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
import AnimateButton from '../../../components/@extended/AnimateButton';
|
||||
import RestartModal from './restart';
|
||||
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
|
||||
import { DeleteOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputPassword, CosmosInputText, CosmosSelect } from './formShortcuts';
|
||||
import CountrySelect, { countries } from '../../../components/countrySelect';
|
||||
import CountrySelect from '../../../components/countrySelect';
|
||||
import { DnsChallengeComp } from '../../../utils/dns-challenge-comp';
|
||||
|
||||
import UploadButtons from '../../../components/fileUpload';
|
||||
import { SliderPicker
|
||||
} from 'react-color';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
// TODO: Remove circular deps
|
||||
import {SetPrimaryColor, SetSecondaryColor} from '../../../App';
|
||||
import { useClientInfos } from '../../../utils/hooks';
|
||||
import ConfirmModal from '../../../components/confirmModal';
|
||||
import { DownloadFile } from '../../../api/downloadButton';
|
||||
|
||||
const ConfigManagement = () => {
|
||||
const [config, setConfig] = React.useState(null);
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const [openResartModal, setOpenRestartModal] = React.useState(false);
|
||||
const [uploadingBackground, setUploadingBackground] = React.useState(false);
|
||||
const [saveLabel, setSaveLabel] = React.useState("Save");
|
||||
const {role} = useClientInfos();
|
||||
const isAdmin = role === "2";
|
||||
|
||||
function refresh() {
|
||||
API.config.get().then((res) => {
|
||||
|
@ -42,23 +50,48 @@ const ConfigManagement = () => {
|
|||
});
|
||||
}
|
||||
|
||||
function getRouteDomain(domain) {
|
||||
let parts = domain.split('.');
|
||||
return parts[parts.length - 2] + '.' + parts[parts.length - 1];
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return <div style={{maxWidth: '1000px', margin: ''}}>
|
||||
<IsLoggedIn />
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</Button><br /><br />
|
||||
|
||||
<Stack direction="row" spacing={2} style={{marginBottom: '15px'}}>
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</Button>
|
||||
|
||||
{isAdmin && <Button variant="outlined" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||
setOpenRestartModal(true);
|
||||
}}>Restart Server</Button>}
|
||||
|
||||
<ConfirmModal variant="outlined" color="warning" startIcon={<DeleteOutlined />} callback={() => {
|
||||
API.metrics.reset().then((res) => {
|
||||
refresh();
|
||||
});
|
||||
}}
|
||||
label={'Purge Metrics Dashboard'}
|
||||
content={'Are you sure you want to purge all the metrics data from the dashboards?'} />
|
||||
</Stack>
|
||||
|
||||
{config && <>
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
|
||||
<RestartModal openModal={openResartModal} setOpenModal={setOpenRestartModal} />
|
||||
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
MongoDB: config.MongoDB,
|
||||
LoggingLevel: config.LoggingLevel,
|
||||
RequireMFA: config.RequireMFA,
|
||||
GeoBlocking: config.BlockedCountries,
|
||||
CountryBlacklistIsWhitelist: config.CountryBlacklistIsWhitelist,
|
||||
AutoUpdate: config.AutoUpdate,
|
||||
|
||||
Hostname: config.HTTPConfig.Hostname,
|
||||
|
@ -67,8 +100,13 @@ const ConfigManagement = () => {
|
|||
HTTPPort: config.HTTPConfig.HTTPPort,
|
||||
HTTPSPort: config.HTTPConfig.HTTPSPort,
|
||||
SSLEmail: config.HTTPConfig.SSLEmail,
|
||||
UseWildcardCertificate: config.HTTPConfig.UseWildcardCertificate,
|
||||
HTTPSCertificateMode: config.HTTPConfig.HTTPSCertificateMode,
|
||||
DNSChallengeProvider: config.HTTPConfig.DNSChallengeProvider,
|
||||
DNSChallengeConfig: config.HTTPConfig.DNSChallengeConfig,
|
||||
ForceHTTPSCertificateRenewal: config.HTTPConfig.ForceHTTPSCertificateRenewal,
|
||||
OverrideWildcardDomains: config.HTTPConfig.OverrideWildcardDomains,
|
||||
UseForwardedFor: config.HTTPConfig.UseForwardedFor,
|
||||
|
||||
Email_Enabled: config.EmailConfig.Enabled,
|
||||
Email_Host: config.EmailConfig.Host,
|
||||
|
@ -77,69 +115,125 @@ const ConfigManagement = () => {
|
|||
Email_Password: config.EmailConfig.Password,
|
||||
Email_From: config.EmailConfig.From,
|
||||
Email_UseTLS : config.EmailConfig.UseTLS,
|
||||
Email_AllowInsecureTLS : config.EmailConfig.AllowInsecureTLS,
|
||||
|
||||
SkipPruneNetwork: config.DockerConfig.SkipPruneNetwork,
|
||||
DefaultDataPath: config.DockerConfig.DefaultDataPath || "/usr",
|
||||
|
||||
Background: config && config.HomepageConfig && config.HomepageConfig.Background,
|
||||
Expanded: config && config.HomepageConfig && config.HomepageConfig.Expanded,
|
||||
PrimaryColor: config && config.ThemeConfig && config.ThemeConfig.PrimaryColor,
|
||||
SecondaryColor: config && config.ThemeConfig && config.ThemeConfig.SecondaryColor,
|
||||
|
||||
MonitoringEnabled: !config.MonitoringDisabled,
|
||||
}}
|
||||
|
||||
validationSchema={Yup.object().shape({
|
||||
Hostname: Yup.string().max(255).required('Hostname is required'),
|
||||
MongoDB: Yup.string().max(512),
|
||||
LoggingLevel: Yup.string().max(255).required('Logging Level is required'),
|
||||
})}
|
||||
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
try {
|
||||
let toSave = {
|
||||
...config,
|
||||
MongoDB: values.MongoDB,
|
||||
LoggingLevel: values.LoggingLevel,
|
||||
RequireMFA: values.RequireMFA,
|
||||
// AutoUpdate: values.AutoUpdate,
|
||||
BlockedCountries: values.GeoBlocking,
|
||||
HTTPConfig: {
|
||||
...config.HTTPConfig,
|
||||
Hostname: values.Hostname,
|
||||
GenerateMissingAuthCert: values.GenerateMissingAuthCert,
|
||||
HTTPPort: values.HTTPPort,
|
||||
HTTPSPort: values.HTTPSPort,
|
||||
SSLEmail: values.SSLEmail,
|
||||
HTTPSCertificateMode: values.HTTPSCertificateMode,
|
||||
DNSChallengeProvider: values.DNSChallengeProvider,
|
||||
},
|
||||
EmailConfig: {
|
||||
...config.EmailConfig,
|
||||
Enabled: values.Email_Enabled,
|
||||
Host: values.Email_Host,
|
||||
Port: values.Email_Port,
|
||||
Username: values.Email_Username,
|
||||
Password: values.Email_Password,
|
||||
From: values.Email_From,
|
||||
UseTLS: values.Email_UseTLS,
|
||||
}
|
||||
}
|
||||
|
||||
API.config.set(toSave).then((data) => {
|
||||
if (data.status == 'error') {
|
||||
setStatus({ success: false });
|
||||
if (data.code == 'UL001') {
|
||||
setErrors({ submit: 'Wrong nickname or password. Try again or try resetting your password' });
|
||||
} else if (data.status == 'error') {
|
||||
setErrors({ submit: 'Unexpected error. Try again later.' });
|
||||
}
|
||||
setSubmitting(false);
|
||||
return;
|
||||
} else {
|
||||
setStatus({ success: true });
|
||||
setSubmitting(false);
|
||||
setOpenModal(true);
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
setStatus({ success: false });
|
||||
setErrors({ submit: err.message });
|
||||
setSubmitting(false);
|
||||
setSubmitting(true);
|
||||
|
||||
let toSave = {
|
||||
...config,
|
||||
MongoDB: values.MongoDB,
|
||||
LoggingLevel: values.LoggingLevel,
|
||||
RequireMFA: values.RequireMFA,
|
||||
// AutoUpdate: values.AutoUpdate,
|
||||
BlockedCountries: values.GeoBlocking,
|
||||
CountryBlacklistIsWhitelist: values.CountryBlacklistIsWhitelist,
|
||||
MonitoringDisabled: !values.MonitoringEnabled,
|
||||
HTTPConfig: {
|
||||
...config.HTTPConfig,
|
||||
Hostname: values.Hostname,
|
||||
GenerateMissingAuthCert: values.GenerateMissingAuthCert,
|
||||
HTTPPort: values.HTTPPort,
|
||||
HTTPSPort: values.HTTPSPort,
|
||||
SSLEmail: values.SSLEmail,
|
||||
UseWildcardCertificate: values.UseWildcardCertificate,
|
||||
HTTPSCertificateMode: values.HTTPSCertificateMode,
|
||||
DNSChallengeProvider: values.DNSChallengeProvider,
|
||||
DNSChallengeConfig: values.DNSChallengeConfig,
|
||||
ForceHTTPSCertificateRenewal: values.ForceHTTPSCertificateRenewal,
|
||||
OverrideWildcardDomains: values.OverrideWildcardDomains.replace(/\s/g, ''),
|
||||
UseForwardedFor: values.UseForwardedFor,
|
||||
},
|
||||
EmailConfig: {
|
||||
...config.EmailConfig,
|
||||
Enabled: values.Email_Enabled,
|
||||
Host: values.Email_Host,
|
||||
Port: values.Email_Port,
|
||||
Username: values.Email_Username,
|
||||
Password: values.Email_Password,
|
||||
From: values.Email_From,
|
||||
UseTLS: values.Email_UseTLS,
|
||||
AllowInsecureTLS: values.Email_AllowInsecureTLS,
|
||||
},
|
||||
DockerConfig: {
|
||||
...config.DockerConfig,
|
||||
SkipPruneNetwork: values.SkipPruneNetwork,
|
||||
DefaultDataPath: values.DefaultDataPath
|
||||
},
|
||||
HomepageConfig: {
|
||||
...config.HomepageConfig,
|
||||
Background: values.Background,
|
||||
Expanded: values.Expanded
|
||||
},
|
||||
ThemeConfig: {
|
||||
...config.ThemeConfig,
|
||||
PrimaryColor: values.PrimaryColor,
|
||||
SecondaryColor: values.SecondaryColor
|
||||
},
|
||||
}
|
||||
|
||||
return API.config.set(toSave).then((data) => {
|
||||
setOpenModal(true);
|
||||
setSaveLabel("Saved!");
|
||||
setTimeout(() => {
|
||||
setSaveLabel("Save");
|
||||
}, 3000);
|
||||
}).catch((err) => {
|
||||
setOpenModal(true);
|
||||
setSaveLabel("Error while saving, try again.");
|
||||
setTimeout(() => {
|
||||
setSaveLabel("Save");
|
||||
}, 3000);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<form noValidate onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={3}>
|
||||
{isAdmin && <MainCard>
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<LoadingButton
|
||||
disableElevation
|
||||
loading={formik.isSubmitting}
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
{saveLabel}
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</MainCard>}
|
||||
|
||||
{!isAdmin && <div>
|
||||
<Alert severity="warning">As you are not an admin, you can't edit the configuration.
|
||||
This page is only here for visibility.
|
||||
</Alert>
|
||||
</div>}
|
||||
|
||||
<MainCard title="General">
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
|
@ -153,12 +247,6 @@ const ConfigManagement = () => {
|
|||
helperText="Require MFA for all users"
|
||||
/>
|
||||
|
||||
{/* <CosmosCheckbox
|
||||
label="Auto Update Cosmos"
|
||||
name="AutoUpdate"
|
||||
formik={formik}
|
||||
/> */}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel htmlFor="MongoDB-login">MongoDB connection string. It is advised to use Environment variable to store this securely instead. (Optional)</InputLabel>
|
||||
|
@ -216,6 +304,95 @@ const ConfigManagement = () => {
|
|||
</TextField>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<CosmosCheckbox
|
||||
label="Monitoring Enabled"
|
||||
name="MonitoringEnabled"
|
||||
formik={formik}
|
||||
/>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
|
||||
<MainCard title="Appearance">
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
{!uploadingBackground && formik.values.Background && <img src=
|
||||
{formik.values.Background} alt="preview seems broken. Please re-upload."
|
||||
width={285} />}
|
||||
{uploadingBackground && <Skeleton variant="rectangular" width={285} height={140} />}
|
||||
<Stack spacing={1} direction="row">
|
||||
<UploadButtons
|
||||
accept='.jpg, .png, .gif, .jpeg, .webp, .bmp, .avif, .tiff, .svg'
|
||||
label="Upload Wallpaper"
|
||||
OnChange={(e) => {
|
||||
setUploadingBackground(true);
|
||||
const file = e.target.files[0];
|
||||
API.uploadImage(file, "background").then((data) => {
|
||||
formik.setFieldValue('Background', data.data.path);
|
||||
setUploadingBackground(false);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
formik.setFieldValue('Background', "");
|
||||
}}
|
||||
>
|
||||
Reset Wallpaper
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
formik.setFieldValue('PrimaryColor', "");
|
||||
SetPrimaryColor("");
|
||||
formik.setFieldValue('SecondaryColor', "");
|
||||
SetSecondaryColor("");
|
||||
}}
|
||||
>
|
||||
Reset Colors
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<CosmosCheckbox
|
||||
label="Show Application Details on Homepage"
|
||||
name="Expanded"
|
||||
formik={formik}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel style={{marginBottom: '10px'}} htmlFor="PrimaryColor">Primary Color</InputLabel>
|
||||
<SliderPicker
|
||||
id="PrimaryColor"
|
||||
color={formik.values.PrimaryColor}
|
||||
onChange={color => {
|
||||
let colorRGB = `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`
|
||||
formik.setFieldValue('PrimaryColor', colorRGB);
|
||||
SetPrimaryColor(colorRGB);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel style={{marginBottom: '10px'}} htmlFor="SecondaryColor">Secondary Color</InputLabel>
|
||||
<SliderPicker
|
||||
id="SecondaryColor"
|
||||
color={formik.values.SecondaryColor}
|
||||
onChange={color => {
|
||||
let colorRGB = `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`
|
||||
formik.setFieldValue('SecondaryColor', colorRGB);
|
||||
SetSecondaryColor(colorRGB);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
|
||||
|
@ -223,7 +400,7 @@ const ConfigManagement = () => {
|
|||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel htmlFor="Hostname-login">Hostname: This will be used to restrict access to your Cosmos Server (Default: 0.0.0.0)</InputLabel>
|
||||
<InputLabel htmlFor="Hostname-login">Hostname: This will be used to restrict access to your Cosmos Server (Your IP, or your domain name)</InputLabel>
|
||||
<OutlinedInput
|
||||
id="Hostname-login"
|
||||
type="text"
|
||||
|
@ -344,22 +521,66 @@ const ConfigManagement = () => {
|
|||
formik={formik}
|
||||
helperText="SMTP Uses TLS"
|
||||
/>
|
||||
|
||||
{formik.values.Email_UseTLS && (
|
||||
<CosmosCheckbox
|
||||
label="Allow Insecure TLS"
|
||||
name="Email_AllowInsecureTLS"
|
||||
formik={formik}
|
||||
helperText="Allow self-signed certificate"
|
||||
/>
|
||||
)}
|
||||
</>)}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
|
||||
<MainCard title="Docker">
|
||||
<Stack spacing={2}>
|
||||
<CosmosCheckbox
|
||||
label="Skip Prune Network"
|
||||
name="SkipPruneNetwork"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosInputText
|
||||
label="Default data path for installs"
|
||||
name="DefaultDataPath"
|
||||
formik={formik}
|
||||
placeholder={'/usr'}
|
||||
/>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
|
||||
|
||||
<MainCard title="Security">
|
||||
<Grid container spacing={3}>
|
||||
|
||||
|
||||
{/* <CosmosCheckbox
|
||||
label={"Read Client IP from X-Forwarded-For header (not recommended)"}
|
||||
name="UseForwardedFor"
|
||||
formik={formik}
|
||||
/> */}
|
||||
|
||||
<CosmosFormDivider title='Geo-Blocking' />
|
||||
|
||||
<CosmosCheckbox
|
||||
label={"Use list as whitelist instead of blacklist"}
|
||||
name="CountryBlacklistIsWhitelist"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<InputLabel htmlFor="GeoBlocking">Geo-Blocking: (Those countries will be blocked from accessing your server)</InputLabel>
|
||||
<InputLabel htmlFor="GeoBlocking">Geo-Blocking: (Those countries will be
|
||||
{formik.values.CountryBlacklistIsWhitelist ? " allowed to access " : " blocked from accessing "}
|
||||
your server)</InputLabel>
|
||||
</Grid>
|
||||
<CountrySelect name="GeoBlocking" label="Choose which countries you want to block" formik={formik} />
|
||||
|
||||
<CountrySelect name="GeoBlocking" label="Choose which countries you want to block or allow" formik={formik} />
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Button onClick={() => {
|
||||
formik.setFieldValue("GeoBlocking", ["CN","RU","TR","BR","BD","IN","NP","PK","LK","VN","ID","IR","IQ","EG","AF","RO",])
|
||||
formik.setFieldValue("CountryBlacklistIsWhitelist", false)
|
||||
}} variant="outlined">Reset to default (most dangerous countries)</Button>
|
||||
</Grid>
|
||||
|
||||
|
@ -369,38 +590,6 @@ const ConfigManagement = () => {
|
|||
<Alert severity="info">For security reasons, It is not possible to remotely change the Private keys of any certificates on your instance. It is advised to manually edit the config file, or better, use Environment Variables to store them.</Alert>
|
||||
</Grid>
|
||||
|
||||
<CosmosSelect
|
||||
name="HTTPSCertificateMode"
|
||||
label="HTTPS Certificates"
|
||||
formik={formik}
|
||||
options={[
|
||||
["LETSENCRYPT", "Automatically generate certificates using Let's Encrypt (Recommended)"],
|
||||
["SELFSIGNED", "Locally self-sign certificates (unsecure)"],
|
||||
["PROVIDED", "I have my own certificates"],
|
||||
["DISABLED", "Do not use HTTPS (very unsecure)"],
|
||||
]}
|
||||
/>
|
||||
|
||||
{
|
||||
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
|
||||
<CosmosInputText
|
||||
name="SSLEmail"
|
||||
label="Email address for Let's Encrypt"
|
||||
formik={formik}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
|
||||
<CosmosInputText
|
||||
name="DNSChallengeProvider"
|
||||
label="DNS provider (if you are using a DNS Challenge)"
|
||||
formik={formik}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||
<Field
|
||||
|
@ -413,6 +602,69 @@ const ConfigManagement = () => {
|
|||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<CosmosSelect
|
||||
name="HTTPSCertificateMode"
|
||||
label="HTTPS Certificates"
|
||||
formik={formik}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
|
||||
}}
|
||||
options={[
|
||||
["LETSENCRYPT", "Automatically generate certificates using Let's Encrypt (Recommended)"],
|
||||
["SELFSIGNED", "Locally self-sign certificates (unsecure)"],
|
||||
["PROVIDED", "I have my own certificates"],
|
||||
["DISABLED", "Do not use HTTPS (very unsecure)"],
|
||||
]}
|
||||
/>
|
||||
|
||||
<CosmosCheckbox
|
||||
label={"Use Wildcard Certificate for the root domain of " + formik.values.Hostname}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
|
||||
}}
|
||||
name="UseWildcardCertificate"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
{formik.values.UseWildcardCertificate && (
|
||||
<CosmosInputText
|
||||
name="OverrideWildcardDomains"
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
|
||||
}}
|
||||
label="(optional, only if you know what you are doing) Override Wildcard Domains (comma separated, need to add both wildcard AND root domain like in the placeholder)"
|
||||
formik={formik}
|
||||
placeholder={"example.com,*.example.com"}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
|
||||
<CosmosInputText
|
||||
name="SSLEmail"
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
|
||||
}}
|
||||
label="Email address for Let's Encrypt"
|
||||
formik={formik}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
|
||||
<DnsChallengeComp
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
|
||||
}}
|
||||
label="Pick a DNS provider (if you are using a DNS Challenge, otherwise leave empty)"
|
||||
name="DNSChallengeProvider"
|
||||
configName="DNSChallengeConfig"
|
||||
formik={formik}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<h4>Authentication Public Key</h4>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||
|
@ -431,31 +683,38 @@ const ConfigManagement = () => {
|
|||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<CosmosCheckbox
|
||||
label={"Force HTTPS Certificate Renewal On Next Save"}
|
||||
name="ForceHTTPSCertificateRenewal"
|
||||
formik={formik}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
|
||||
</Grid>
|
||||
</MainCard>
|
||||
|
||||
<MainCard>
|
||||
{isAdmin && <MainCard>
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<AnimateButton>
|
||||
<Button
|
||||
<LoadingButton
|
||||
disableElevation
|
||||
disabled={formik.isSubmitting}
|
||||
loading={formik.isSubmitting}
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</AnimateButton>
|
||||
{saveLabel}
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</MainCard>}
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
|
|
|
@ -33,7 +33,7 @@ import defaultport from '../../servapps/defaultport.json';
|
|||
|
||||
import * as API from '../../../api';
|
||||
|
||||
export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTargetChange}) {
|
||||
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange, label = "Container Name", name = "Target"}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [containers, setContainers] = React.useState([]);
|
||||
const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
|
||||
|
@ -42,8 +42,6 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
|
|||
const [portsOptions, setPortsOptions] = React.useState(null);
|
||||
const loading = options === null;
|
||||
|
||||
const name = "Target"
|
||||
const label = "Container Name"
|
||||
let targetResult = {
|
||||
container: 'null',
|
||||
port: "",
|
||||
|
@ -155,7 +153,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
|
|||
}
|
||||
|
||||
if (targetResult.container !== 'null') {
|
||||
postContainerChange(res.data.find((container) => container.Names[0] === targetResult.container))
|
||||
postContainerChange(res.data.find((container) => container.Names[0] === targetResult.container) || targetResult.containerObject)
|
||||
}
|
||||
})();
|
||||
|
||||
|
@ -173,7 +171,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
|
|||
const newTarget = formik.values[name];
|
||||
React.useEffect(() => {
|
||||
if(onTargetChange) {
|
||||
onTargetChange(newTarget)
|
||||
onTargetChange(newTarget, targetResult.container.replace("/", ""), targetResult)
|
||||
}
|
||||
}, [newTarget])
|
||||
|
||||
|
@ -219,62 +217,63 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
|
|||
/>
|
||||
)}
|
||||
/>}
|
||||
{!nameOnly && <>
|
||||
<InputLabel htmlFor={name + "-port"}>Container Port</InputLabel>
|
||||
<Autocomplete
|
||||
className="px-2 my-2"
|
||||
variant="outlined"
|
||||
name={name + "-port"}
|
||||
id={name + "-port"}
|
||||
value={targetResult.port}
|
||||
options={((portsOptions && portsOptions.length) ? portsOptions : [])}
|
||||
placeholder='Select a port'
|
||||
onBlur={(event) => {
|
||||
targetResult.port = event.target.value || '';
|
||||
formik.setFieldValue(name, getTarget())
|
||||
}}
|
||||
freeSolo
|
||||
filterOptions={(x) => x} // disable filtering
|
||||
getOptionLabel={(option) => '' + option}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
return ('' + option) === value
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
targetResult.port = newValue || '';
|
||||
formik.setFieldValue(name, getTarget())
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
/>
|
||||
{targetResult.port == '' && targetResult.port == 0 && <FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
Please select a port
|
||||
</FormHelperText>}
|
||||
|
||||
|
||||
{(portsOptions) ? (<>
|
||||
<InputLabel htmlFor={name + "-port"}>Container Port</InputLabel>
|
||||
<Autocomplete
|
||||
className="px-2 my-2"
|
||||
variant="outlined"
|
||||
name={name + "-port"}
|
||||
id={name + "-port"}
|
||||
value={targetResult.port}
|
||||
options={portsOptions.map((option) => (option))}
|
||||
placeholder='Select a port'
|
||||
freeSolo
|
||||
filterOptions={(x) => x} // disable filtering
|
||||
getOptionLabel={(option) => '' + option}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
return ('' + option) === value
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
targetResult.port = newValue
|
||||
formik.setFieldValue(name, getTarget())
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
/>
|
||||
{targetResult.port == '' && targetResult.port == 0 && <FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
Please select a port
|
||||
</FormHelperText>}
|
||||
</>) : ''}
|
||||
|
||||
|
||||
{(portsOptions) ? (<>
|
||||
<InputLabel htmlFor={name + "-protocol"}>Container Protocol (use HTTP if unsure)</InputLabel>
|
||||
<TextField
|
||||
type="text"
|
||||
name={name + "-protocol"}
|
||||
defaultValue={targetResult.protocol}
|
||||
onChange={(event) => {
|
||||
targetResult.protocol = event.target.value && event.target.value.toLowerCase()
|
||||
formik.setFieldValue(name, getTarget())
|
||||
}}
|
||||
/>
|
||||
</>) : ''}
|
||||
<InputLabel htmlFor={name + "-protocol"}>Container Protocol (use HTTP if unsure)</InputLabel>
|
||||
<TextField
|
||||
type="text"
|
||||
name={name + "-protocol"}
|
||||
defaultValue={targetResult.protocol}
|
||||
onChange={(event) => {
|
||||
targetResult.protocol = event.target.value && event.target.value.toLowerCase()
|
||||
formik.setFieldValue(name, getTarget())
|
||||
}}
|
||||
/>
|
||||
|
||||
<InputLabel htmlFor={name}>Result Target Preview</InputLabel>
|
||||
<TextField
|
||||
name={name}
|
||||
placeholder={"This will be generated automatically"}
|
||||
id={name}
|
||||
value={formik.values[name]}
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
{formik.errors[name] && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
<InputLabel htmlFor={name}>Result Target Preview</InputLabel>
|
||||
<TextField
|
||||
name={name}
|
||||
placeholder={"This will be generated automatically"}
|
||||
id={name}
|
||||
value={formik.values[name]}
|
||||
disabled={true}
|
||||
/>
|
||||
</>}
|
||||
|
||||
{formik.errors[name] && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
@ -27,23 +27,46 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
|
|||
|
||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
|
||||
export const CosmosInputText = ({ name, style, multiline, type, placeholder, onChange, label, formik }) => {
|
||||
export const getNestedValue = (values, path) => {
|
||||
return path.split('.').reduce((current, key) => {
|
||||
if (current && current[key] !== undefined) {
|
||||
return current[key];
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
const index = parseInt(key, 10);
|
||||
return current[index];
|
||||
}
|
||||
return undefined;
|
||||
}, values);
|
||||
};
|
||||
|
||||
export const CosmosInputText = ({ name, style, value, errors, multiline, type, placeholder, onChange, label, formik }) => {
|
||||
return <Grid item xs={12}>
|
||||
<Stack spacing={1} style={style}>
|
||||
<InputLabel htmlFor={name}>{label}</InputLabel>
|
||||
{label && <InputLabel htmlFor={name}>{label}</InputLabel>}
|
||||
<OutlinedInput
|
||||
id={name}
|
||||
type={type ? type : 'text'}
|
||||
value={formik.values[name]}
|
||||
value={value || (formik && getNestedValue(formik.values, name))}
|
||||
name={name}
|
||||
multiline={multiline}
|
||||
onBlur={formik.handleBlur}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={(...ar) => {
|
||||
return formik && formik.handleBlur(...ar);
|
||||
}}
|
||||
onChange={(...ar) => {
|
||||
onChange && onChange(...ar);
|
||||
return formik && formik.handleChange(...ar);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
fullWidth
|
||||
error={Boolean(formik.touched[name] && formik.errors[name])}
|
||||
error={Boolean(formik && formik.touched[name] && formik.errors[name])}
|
||||
/>
|
||||
{formik.touched[name] && formik.errors[name] && (
|
||||
{formik && formik.touched[name] && formik.errors[name] && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
{errors && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
|
@ -78,7 +101,7 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
|
|||
<OutlinedInput
|
||||
id={name}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formik.values[name]}
|
||||
value={getNestedValue(formik.values, name)}
|
||||
name={name}
|
||||
autoComplete={autoComplete}
|
||||
onBlur={formik.handleBlur}
|
||||
|
@ -125,7 +148,7 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
|
|||
</Grid>
|
||||
}
|
||||
|
||||
export const CosmosSelect = ({ name, label, formik, disabled, options }) => {
|
||||
export const CosmosSelect = ({ name, onChange, label, formik, disabled, options, style }) => {
|
||||
return <Grid item xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel htmlFor={name}>{label}</InputLabel>
|
||||
|
@ -136,8 +159,11 @@ export const CosmosSelect = ({ name, label, formik, disabled, options }) => {
|
|||
id={name}
|
||||
disabled={disabled}
|
||||
select
|
||||
value={formik.values[name]}
|
||||
onChange={formik.handleChange}
|
||||
value={getNestedValue(formik.values, name)}
|
||||
onChange={(...ar) => {
|
||||
onChange && onChange(...ar);
|
||||
formik.handleChange(...ar);
|
||||
}}
|
||||
error={
|
||||
formik.touched[name] &&
|
||||
Boolean(formik.errors[name])
|
||||
|
@ -145,6 +171,7 @@ export const CosmosSelect = ({ name, label, formik, disabled, options }) => {
|
|||
helperText={
|
||||
formik.touched[name] && formik.errors[name]
|
||||
}
|
||||
style={style}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<MenuItem key={option[0]} value={option[0]}>
|
||||
|
@ -186,8 +213,9 @@ export const CosmosCollapse = ({ children, title }) => {
|
|||
aria-controls="panel1a-content"
|
||||
id="panel1a-header"
|
||||
>
|
||||
<Typography variant="h6">
|
||||
{title}</Typography>
|
||||
<Typography variant="h6" style={{width: '100%', marginRight: '20px'}}>
|
||||
{title}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{children}
|
||||
|
@ -200,7 +228,7 @@ export const CosmosCollapse = ({ children, title }) => {
|
|||
export function CosmosFormDivider({title}) {
|
||||
return <Grid item xs={12}>
|
||||
<Divider>
|
||||
<Chip label={title} />
|
||||
{title && <Chip label={title} />}
|
||||
</Divider>
|
||||
</Grid>
|
||||
}
|
||||
|
|
|
@ -39,6 +39,9 @@ import HostChip from '../../../components/hostChip';
|
|||
import {RouteActions, RouteMode, RouteSecurity} from '../../../components/routeComponents';
|
||||
import { useNavigate } from 'react-router';
|
||||
import NewRouteCreate from '../routes/newRoute';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import MiniPlotComponent from '../../dashboard/components/mini-plot';
|
||||
import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
|
||||
|
||||
const stickyButton = {
|
||||
position: 'fixed',
|
||||
|
@ -64,6 +67,15 @@ const ProxyManagement = () => {
|
|||
const [submitErrors, setSubmitErrors] = React.useState([]);
|
||||
const [needSave, setNeedSave] = React.useState(false);
|
||||
const [openNewModal, setOpenNewModal] = React.useState(false);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
function setRouteEnabled(key) {
|
||||
return (event) => {
|
||||
routes[key].Disabled = !event.target.checked;
|
||||
updateRoutes(routes);
|
||||
setNeedSave(true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateRoutes(routes) {
|
||||
let con = {
|
||||
|
@ -144,21 +156,31 @@ const ProxyManagement = () => {
|
|||
</Stack>
|
||||
|
||||
{config && <>
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
|
||||
<NewRouteCreate openNewModal={openNewModal} setOpenNewModal={setOpenNewModal} config={config}/>
|
||||
|
||||
{routes && <PrettyTableView
|
||||
data={routes}
|
||||
getKey={(r) => r.Name + r.Target + r.Mode}
|
||||
linkTo={(r) => '/ui/config-url/' + r.Name}
|
||||
linkTo={(r) => '/cosmos-ui/config-url/' + r.Name}
|
||||
columns={[
|
||||
{
|
||||
title: '',
|
||||
field: (r) => <img src={getFaviconURL(r)} width="64px" />,
|
||||
field: (r) => <LazyLoad width={"64px"} height={"64px"}>
|
||||
<ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(r)} width="64px" height="64px"/>
|
||||
</LazyLoad>,
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Enabled',
|
||||
clickable:true,
|
||||
field: (r, k) => <Checkbox disabled={isLoading} size='large' color={!r.Disabled ? 'success' : 'default'}
|
||||
onChange={setRouteEnabled(k)}
|
||||
checked={!r.Disabled}
|
||||
/>,
|
||||
},
|
||||
{ title: 'URL',
|
||||
search: (r) => r.Name + ' ' + r.Description,
|
||||
style: {
|
||||
|
@ -170,16 +192,24 @@ const ProxyManagement = () => {
|
|||
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
|
||||
</>
|
||||
},
|
||||
{ title: 'Network', screenMin: 'lg', clickable:false, field: (r) =>
|
||||
<div style={{width: '400px', marginLeft: '-200px', marginBottom: '10px'}}>
|
||||
<MiniPlotComponent metrics={[
|
||||
"cosmos.proxy.route.bytes." + r.Name,
|
||||
"cosmos.proxy.route.time." + r.Name,
|
||||
]} noLabels noBackground/>
|
||||
</div>
|
||||
},
|
||||
{ title: 'Origin', screenMin: 'md', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => <HostChip route={r} /> },
|
||||
{ title: 'Target', screenMin: 'md', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
|
||||
{ title: 'Security', screenMin: 'lg', field: (r) => <RouteSecurity route={r} />,
|
||||
style: {minWidth: '70px'} },
|
||||
{ title: '', clickable:true, field: (r, k) => <RouteActions
|
||||
route={r}
|
||||
routeKey={k}
|
||||
up={(event) => up(event, k)}
|
||||
down={(event) => down(event, k)}
|
||||
deleteRoute={(event) => deleteRoute(event, k)}
|
||||
routeKey={routes.indexOf(r)}
|
||||
up={(event) => up(event, routes.indexOf(r))}
|
||||
down={(event) => down(event, routes.indexOf(r))}
|
||||
deleteRoute={(event) => deleteRoute(event, routes.indexOf(r))}
|
||||
/>,
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
|
@ -215,20 +245,6 @@ const ProxyManagement = () => {
|
|||
disableElevation
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
if(routes.some((route, key) => {
|
||||
let errors = ValidateRoute(route, config);
|
||||
if (errors && errors.length > 0) {
|
||||
errors = errors.map((err) => {
|
||||
return `${route.Name}: ${err}`;
|
||||
});
|
||||
setSubmitErrors(errors);
|
||||
return true;
|
||||
}
|
||||
})) {
|
||||
return;
|
||||
} else {
|
||||
setSubmitErrors([]);
|
||||
}
|
||||
API.config.set(cleanRoutes(updateRoutes(routes))).then(() => {
|
||||
setNeedSave(false);
|
||||
setOpenModal(true);
|
||||
|
|
|
@ -22,6 +22,7 @@ import * as API from '../../../api';
|
|||
import MainCard from '../../../components/MainCard';
|
||||
import IsLoggedIn from '../../../isLoggedIn';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDomain } from '../../../utils/indexs';
|
||||
|
||||
function checkIsOnline() {
|
||||
API.isOnline().then((res) => {
|
||||
|
@ -33,25 +34,62 @@ function checkIsOnline() {
|
|||
});
|
||||
}
|
||||
|
||||
const RestartModal = ({openModal, setOpenModal}) => {
|
||||
const RestartModal = ({openModal, setOpenModal, config, newRoute }) => {
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
const [warn, setWarn] = useState(false);
|
||||
const needsRefresh = config && (config.HTTPConfig.HTTPSCertificateMode == "SELFSIGNED" ||
|
||||
!isDomain(config.HTTPConfig.Hostname))
|
||||
const isNotDomain = config && !isDomain(config.HTTPConfig.Hostname);
|
||||
let newRouteWarning = config && (config.HTTPConfig.HTTPSCertificateMode == "LETSENCRYPT" && newRoute &&
|
||||
(!config.HTTPConfig.DNSChallengeProvider || !config.HTTPConfig.UseWildcardCertificate))
|
||||
|
||||
return <>
|
||||
return config ? (<>
|
||||
{needsRefresh && <>
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>Refresh Page</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
You need to refresh the page because you are using a self-signed certificate, in case you have to accept any new certificates. To avoid it in the future, please use Let's Encrypt. {isNotDomain && 'You are also not using a domain name, the server might go offline for a few seconds to remap your docker ports.'}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => {
|
||||
window.location.reload(true);
|
||||
}}>Refresh</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>}
|
||||
{newRouteWarning && <>
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>Certificate Renewal</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
You are using Let's Encrypt but you are not using the DNS Challenge with a wildcard certificate. This means the server has to renew the certificate everytime you add a new hostname, causing a few seconds of downtime. To avoid it in the future, please refer to <a target="_blank" rel="noopener noreferrer" href="https://cosmos-cloud.io/doc/9%20Other%20Setups/#dns-challenge-and-wildcard-certificates">this link to the documentation</a>.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => {
|
||||
setOpenModal(false);
|
||||
}}>OK</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>}
|
||||
</>)
|
||||
:(<>
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>{!isRestarting ? 'Restart Server?' : 'Restarting Server...'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{warn && <div>
|
||||
<Alert severity="warning" icon={<WarningOutlined />}>
|
||||
The server is taking longer than expected to restart.<br />Consider troubleshouting the logs.
|
||||
The server is taking longer than expected to restart.<br />Consider troubleshouting the logs. If you use a self-signed certificate, you might have to refresh and re-accept it.
|
||||
</Alert>
|
||||
</div>}
|
||||
{isRestarting ?
|
||||
<div style={{textAlign: 'center', padding: '20px'}}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
: 'A restart is required to apply changes. Do you want to restart?'}
|
||||
: 'Do you want to restart your server?'}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
{!isRestarting && <DialogActions>
|
||||
|
@ -68,7 +106,7 @@ const RestartModal = ({openModal, setOpenModal}) => {
|
|||
}}>Restart</Button>
|
||||
</DialogActions>}
|
||||
</Dialog>
|
||||
</>;
|
||||
</>);
|
||||
};
|
||||
|
||||
export default RestartModal;
|
||||
|
|
|
@ -57,7 +57,7 @@ const UserManagement = () => {
|
|||
formType: ""+formType,
|
||||
})
|
||||
.then((values) => {
|
||||
let sendLink = window.location.origin + '/ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
|
||||
let sendLink = window.location.origin + '/cosmos-ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
|
||||
setToAction({...values.data, nickname, sendLink, formType, formAction: formType === 2 ? 'invite them to the server' : 'let them reset their password'});
|
||||
setOpenInviteForm(true);
|
||||
});
|
||||
|
|
302
client/src/pages/constellation/addDevice.jsx
Normal file
|
@ -0,0 +1,302 @@
|
|||
// material-ui
|
||||
import { Alert, Button, InputLabel, OutlinedInput, Stack, TextField } from '@mui/material';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import ResponsiveButton from '../../components/responseiveButton';
|
||||
import { PlusCircleFilled } from '@ant-design/icons';
|
||||
import { Formik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
import * as API from '../../api';
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
|
||||
import { DownloadFile } from '../../api/downloadButton';
|
||||
import QRCode from 'qrcode';
|
||||
import { useClientInfos } from '../../utils/hooks';
|
||||
|
||||
const getDocker = (data, isCompose) => {
|
||||
let lighthouses = '';
|
||||
|
||||
for (let i = 0; i < data.LighthousesList.length; i++) {
|
||||
const l = data.LighthousesList[i];
|
||||
lighthouses += l.publicHostname + ";" + l.ip + ":" + l.port + ";" + l.isRelay + ",";
|
||||
}
|
||||
|
||||
let containerName = "cosmos-constellation-lighthouse";
|
||||
let imageName = "cosmos-constellation-lighthouse:latest";
|
||||
|
||||
let volPath = "/var/lib/cosmos-constellation";
|
||||
|
||||
if (isCompose) {
|
||||
return `
|
||||
version: "3.8"
|
||||
services:
|
||||
${containerName}:
|
||||
image: ${imageName}
|
||||
container_name: ${containerName}
|
||||
restart: unless-stopped
|
||||
network_mode: bridge
|
||||
ports:
|
||||
- "${data.Port}:4242"
|
||||
volumes:
|
||||
- ${volPath}:/config
|
||||
environment:
|
||||
- CA=${JSON.stringify(data.CA)}
|
||||
- CERT=${JSON.stringify(data.PrivateKey)}
|
||||
- KEY=${JSON.stringify(data.PublicKey)}
|
||||
- LIGHTHOUSES=${lighthouses}
|
||||
- PUBLIC_HOSTNAME=${data.PublicHostname}
|
||||
- IS_RELAY=${data.IsRelay}
|
||||
- IP=${data.IP}
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
docker run -d \\
|
||||
--name ${containerName} \\
|
||||
--restart unless-stopped \\
|
||||
--network bridge \\
|
||||
-v ${volPath}:/config \\
|
||||
-e CA=${JSON.stringify(data.CA)} \\
|
||||
-e CERT=${JSON.stringify(data.PrivateKey)} \\
|
||||
-e KEY=${JSON.stringify(data.PublicKey)} \\
|
||||
-e LIGHTHOUSES=${lighthouses} \\
|
||||
-e PUBLIC_HOSTNAME=${data.PublicHostname} \\
|
||||
-e IS_RELAY=${data.IsRelay} \\
|
||||
-e IP=${data.IP} \\
|
||||
-p ${data.Port}:4242 \\
|
||||
${imageName}
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [isDone, setIsDone] = useState(null);
|
||||
const canvasRef = React.useRef(null);
|
||||
const {role, nickname} = useClientInfos();
|
||||
const isAdmin = role === "2";
|
||||
|
||||
let firstIP = "192.168.201.2/24";
|
||||
if (devices && devices.length > 0) {
|
||||
const isIpFree = (ip) => {
|
||||
return devices.filter((d) => d.ip === ip).length === 0;
|
||||
}
|
||||
let i = 1;
|
||||
let j = 201;
|
||||
while (!isIpFree(firstIP)) {
|
||||
i++;
|
||||
if (i > 254) {
|
||||
i = 0;
|
||||
j++;
|
||||
}
|
||||
firstIP = "192.168." + j + "." + i + "/24";
|
||||
}
|
||||
}
|
||||
|
||||
const renderCanvas = (data) => {
|
||||
if (!canvasRef.current) return setTimeout(() => {
|
||||
renderCanvas(data);
|
||||
}, 500);
|
||||
|
||||
QRCode.toCanvas(canvasRef.current, JSON.stringify(data),
|
||||
{
|
||||
width: 600,
|
||||
color: {
|
||||
dark: "#000",
|
||||
light: '#fff'
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) console.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<Formik
|
||||
initialValues={{
|
||||
nickname: nickname,
|
||||
deviceName: '',
|
||||
ip: firstIP,
|
||||
publicKey: '',
|
||||
Port: "4242",
|
||||
PublicHostname: '',
|
||||
IsRelay: true,
|
||||
isLighthouse: false,
|
||||
}}
|
||||
|
||||
validationSchema={yup.object({
|
||||
})}
|
||||
|
||||
onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
|
||||
if(values.isLighthouse) values.nickname = null;
|
||||
|
||||
return API.constellation.addDevice(values).then(({data}) => {
|
||||
setIsDone(data);
|
||||
refreshConfig();
|
||||
renderCanvas(data.Config);
|
||||
}).catch((err) => {
|
||||
setErrors(err.response.data);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogTitle>Add Device</DialogTitle>
|
||||
|
||||
{isDone ? <DialogContent>
|
||||
<DialogContentText>
|
||||
<p>
|
||||
Device added successfully!
|
||||
Download scan the QR Code from the Cosmos app or download the relevant
|
||||
files to your device along side the config and network certificate to
|
||||
connect:
|
||||
</p>
|
||||
|
||||
<Stack spacing={2} direction={"column"}>
|
||||
{/* {isDone.isLighthouse ? <>
|
||||
<CosmosFormDivider title={"Docker"} />
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
value={getDocker(isDone, false)}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
disabled
|
||||
/>
|
||||
<CosmosFormDivider title={"File (Docker-Compose)"} />
|
||||
<DownloadFile
|
||||
filename={`docker-compose.yml`}
|
||||
content={getDocker(isDone, true)}
|
||||
label={"Download docker-compose.yml"}
|
||||
/>
|
||||
</> : <> */}
|
||||
<CosmosFormDivider title={"QR Code"} />
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<canvas style={{borderRadius: '15px'}} ref={canvasRef} />
|
||||
</div>
|
||||
{/* </>} */}
|
||||
|
||||
<CosmosFormDivider title={"File"} />
|
||||
<DownloadFile
|
||||
filename={`constellation.yml`}
|
||||
content={isDone.Config}
|
||||
label={"Download constellation.yml"}
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContentText>
|
||||
</DialogContent> : <DialogContent>
|
||||
<DialogContentText>
|
||||
<p>Add a Device to the constellation using either the Cosmos or Nebula client</p>
|
||||
<div>
|
||||
<Stack spacing={2} style={{}}>
|
||||
<CosmosCheckbox
|
||||
name="isLighthouse"
|
||||
label="Lighthouse"
|
||||
formik={formik}
|
||||
/>
|
||||
{!formik.values.isLighthouse &&
|
||||
(isAdmin ? <CosmosSelect
|
||||
name="nickname"
|
||||
label="Owner"
|
||||
formik={formik}
|
||||
// disabled={!isAdmin}
|
||||
options={
|
||||
users.map((u) => {
|
||||
return [u.nickname, u.nickname]
|
||||
})
|
||||
}
|
||||
/> : <>
|
||||
<InputLabel>Owner</InputLabel>
|
||||
<OutlinedInput
|
||||
fullWidth
|
||||
multiline
|
||||
value={nickname}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
disabled
|
||||
/>
|
||||
</>)}
|
||||
|
||||
<CosmosInputText
|
||||
name="deviceName"
|
||||
label="Device Name"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosInputText
|
||||
name="ip"
|
||||
label="Constellation IP Address"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
{/* <CosmosInputText
|
||||
name="Port"
|
||||
label="VPN Port (default: 4242)"
|
||||
formik={formik}
|
||||
/> */}
|
||||
|
||||
<CosmosInputText
|
||||
multiline
|
||||
name="publicKey"
|
||||
label="Public Key (Optional)"
|
||||
formik={formik}
|
||||
/>
|
||||
{formik.values.isLighthouse && <>
|
||||
<CosmosFormDivider title={"Lighthouse Setup"} />
|
||||
|
||||
<CosmosInputText
|
||||
name="PublicHostname"
|
||||
label="Public Hostname"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosCheckbox
|
||||
name="IsRelay"
|
||||
label="Can Relay Traffic"
|
||||
formik={formik}
|
||||
/>
|
||||
</>}
|
||||
<div>
|
||||
{formik.errors && formik.errors.length > 0 && <Stack spacing={2} direction={"column"}>
|
||||
<Alert severity="error">{formik.errors.map((err) => {
|
||||
return <div>{err}</div>
|
||||
})}</Alert>
|
||||
</Stack>}
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
</DialogContentText>
|
||||
</DialogContent>}
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenModal(false)}>Close</Button>
|
||||
{!isDone && <Button color="primary" variant="contained" type="submit">Add</Button>}
|
||||
</DialogActions>
|
||||
</form>
|
||||
|
||||
)}
|
||||
</Formik>
|
||||
</Dialog>
|
||||
|
||||
<ResponsiveButton
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setIsDone(null);
|
||||
setOpenModal(true);
|
||||
}}
|
||||
variant={
|
||||
"contained"
|
||||
}
|
||||
startIcon={<PlusCircleFilled />}
|
||||
>
|
||||
Add Device
|
||||
</ResponsiveButton>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default AddDeviceModal;
|
174
client/src/pages/constellation/dns.jsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as API from "../../api";
|
||||
import AddDeviceModal from "./addDevice";
|
||||
import PrettyTableView from "../../components/tableView/prettyTableView";
|
||||
import { DeleteButton } from "../../components/delete";
|
||||
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
|
||||
import IsLoggedIn from "../../isLoggedIn";
|
||||
import { Alert, Button, CircularProgress, InputLabel, Stack } from "@mui/material";
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
|
||||
import MainCard from "../../components/MainCard";
|
||||
import { Formik } from "formik";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import ApiModal from "../../components/apiModal";
|
||||
import { isDomain } from "../../utils/indexs";
|
||||
import ConfirmModal from "../../components/confirmModal";
|
||||
import UploadButtons from "../../components/fileUpload";
|
||||
|
||||
export const ConstellationDNS = () => {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
const refreshConfig = async () => {
|
||||
let configAsync = await API.config.get();
|
||||
setConfig(configAsync.data);
|
||||
setIsAdmin(configAsync.isAdmin);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshConfig();
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
{(config) ? <>
|
||||
<Stack spacing={2} style={{maxWidth: "1000px"}}>
|
||||
<div>
|
||||
<MainCard title={"Constellation Internal DNS"} content={config.constellationIP}>
|
||||
<Stack spacing={2}>
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
Fallback: config.ConstellationConfig.DNSFallback,
|
||||
DNSBlockBlacklist: config.ConstellationConfig.DNSBlockBlacklist,
|
||||
DNSAdditionalBlocklists: config.ConstellationConfig.DNSAdditionalBlocklists || [],
|
||||
CustomDNSEntries: config.ConstellationConfig.CustomDNSEntries || []
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
let newConfig = { ...config };
|
||||
newConfig.ConstellationConfig.DNSFallback = values.Fallback;
|
||||
newConfig.ConstellationConfig.DNSBlockBlacklist = values.DNSBlockBlacklist;
|
||||
newConfig.ConstellationConfig.DNSAdditionalBlocklists = values.DNSAdditionalBlocklists;
|
||||
newConfig.ConstellationConfig.CustomDNSEntries = values.CustomDNSEntries;
|
||||
|
||||
return API.config.set(newConfig);
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={2}>
|
||||
<Alert severity="info">This is a DNS that runs inside your Constellation network. It automatically
|
||||
rewrites your domains DNS entries to be local to your network, and also allows you to do things like block ads
|
||||
and trackers on all devices connected to your network. You can also add custom DNS entries to resolve to specific
|
||||
IP addresses. This DNS server is only accessible from inside your network.</Alert>
|
||||
|
||||
<CosmosInputText formik={formik} name="Fallback" label="DNS Fallback" placeholder={'8.8.8.8:53'} />
|
||||
|
||||
<CosmosFormDivider title={"DNS Blocklists"} />
|
||||
|
||||
<CosmosCheckbox formik={formik} name="DNSBlockBlacklist" label="Use Blacklists to block domains" />
|
||||
|
||||
<Alert severity="warning">When changing your DNS records, always use private mode on your browser and allow some times for various caches to expire.</Alert>
|
||||
|
||||
<InputLabel>DNS Blocklist URLs</InputLabel>
|
||||
{formik.values.DNSAdditionalBlocklists && formik.values.DNSAdditionalBlocklists.map((item, index) => (
|
||||
<Stack direction={"row"} spacing={2} key={`DNSAdditionalBlocklists${item}`} width={"100%"}>
|
||||
<DeleteButton onDelete={() => {
|
||||
formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists.slice(0, index), ...formik.values.DNSAdditionalBlocklists.slice(index + 1)]);
|
||||
}} />
|
||||
<div style={{flexGrow: 1}}>
|
||||
<CosmosInputText
|
||||
value={item}
|
||||
name={`DNSAdditionalBlocklists${index}`}
|
||||
placeholder={'https://example.com/blocklist.txt'}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists.slice(0, index), e.target.value, ...formik.values.DNSAdditionalBlocklists.slice(index + 1)]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
))}
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button variant="outlined" onClick={() => {
|
||||
formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists, ""]);
|
||||
}}>Add</Button>
|
||||
<Button variant="outlined" onClick={() => {
|
||||
formik.setFieldValue("DNSAdditionalBlocklists", [
|
||||
"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
|
||||
"https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
|
||||
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
|
||||
"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-only/hosts"
|
||||
]);
|
||||
}}>Reset Default</Button>
|
||||
</Stack>
|
||||
|
||||
<CosmosFormDivider title={"DNS Custom Entries"} />
|
||||
|
||||
<InputLabel>DNS Custom Entries</InputLabel>
|
||||
{formik.values.CustomDNSEntries && formik.values.CustomDNSEntries.map((item, index) => (
|
||||
<Stack direction={"row"} spacing={2} key={`CustomDNSEntries${item}`} width={"100%"}>
|
||||
<DeleteButton onDelete={() => {
|
||||
formik.setFieldValue("CustomDNSEntries", [...formik.values.CustomDNSEntries.slice(0, index), ...formik.values.CustomDNSEntries.slice(index + 1)]);
|
||||
}} />
|
||||
<div style={{flexGrow: 1}}>
|
||||
<CosmosInputText
|
||||
value={item.Key}
|
||||
name={`CustomDNSEntries${index}-key`}
|
||||
placeholder={'domain.com'}
|
||||
onChange={(e) => {
|
||||
const updatedCustomDNSEntries = [...formik.values.CustomDNSEntries];
|
||||
updatedCustomDNSEntries[index].Key = e.target.value;
|
||||
formik.setFieldValue("CustomDNSEntries", updatedCustomDNSEntries);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{flexGrow: 1}}>
|
||||
<CosmosInputText
|
||||
value={item.Value}
|
||||
name={`CustomDNSEntries${index}-value`}
|
||||
placeholder={'1213.123.123.123'}
|
||||
onChange={(e) => {
|
||||
const updatedCustomDNSEntries = [...formik.values.CustomDNSEntries];
|
||||
updatedCustomDNSEntries[index].Value = e.target.value;
|
||||
formik.setFieldValue("CustomDNSEntries", updatedCustomDNSEntries);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
))}
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button variant="outlined" onClick={() => {
|
||||
formik.setFieldValue("CustomDNSEntries", [...formik.values.CustomDNSEntries, {
|
||||
Key: "",
|
||||
Value: "",
|
||||
Type: "A"
|
||||
}]);
|
||||
}}>Add</Button>
|
||||
<Button variant="outlined" onClick={() => {
|
||||
formik.setFieldValue("CustomDNSEntries", [
|
||||
]);
|
||||
}}>Reset</Button>
|
||||
</Stack>
|
||||
|
||||
<LoadingButton
|
||||
disableElevation
|
||||
loading={formik.isSubmitting}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</div>
|
||||
</Stack>
|
||||
</> : <center>
|
||||
<CircularProgress color="inherit" size={20} />
|
||||
</center>}
|
||||
</>
|
||||
};
|
57
client/src/pages/constellation/index.jsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import * as React from 'react';
|
||||
import MainCard from '../../components/MainCard';
|
||||
import { Alert, Chip, Divider, Stack, useMediaQuery } from '@mui/material';
|
||||
import HostChip from '../../components/hostChip';
|
||||
import { RouteMode, RouteSecurity } from '../../components/routeComponents';
|
||||
import { getFaviconURL } from '../../utils/routes';
|
||||
import * as API from '../../api';
|
||||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import IsLoggedIn from '../../isLoggedIn';
|
||||
import PrettyTabbedView from '../../components/tabbedView/tabbedView';
|
||||
import { useClientInfos } from '../../utils/hooks';
|
||||
|
||||
import { ConstellationVPN } from './vpn';
|
||||
import { ConstellationDNS } from './dns';
|
||||
|
||||
const ConstellationIndex = () => {
|
||||
const {role} = useClientInfos();
|
||||
const isAdmin = role === "2";
|
||||
|
||||
return isAdmin ? <div>
|
||||
<IsLoggedIn />
|
||||
|
||||
<PrettyTabbedView path="/cosmos-ui/constellation/:tab" tabs={[
|
||||
{
|
||||
title: 'VPN',
|
||||
children: <ConstellationVPN />,
|
||||
path: 'vpn'
|
||||
},
|
||||
{
|
||||
title: 'DNS',
|
||||
children: <ConstellationDNS />,
|
||||
path: 'dns'
|
||||
},
|
||||
{
|
||||
title: 'Firewall',
|
||||
children: <div>
|
||||
<Alert severity="info">
|
||||
Coming soon. This feature will allow you to open and close ports individually
|
||||
on each device and decide who can access them.
|
||||
</Alert>
|
||||
</div>,
|
||||
},
|
||||
{
|
||||
title: 'Unsafe Routes',
|
||||
children: <div>
|
||||
<Alert severity="info">
|
||||
Coming soon. This feature will allow you to tunnel your traffic through
|
||||
your devices to things outside of your constellation.
|
||||
</Alert>
|
||||
</div>,
|
||||
}
|
||||
]}/>
|
||||
|
||||
</div> : <ConstellationVPN />;
|
||||
}
|
||||
|
||||
export default ConstellationIndex;
|
220
client/src/pages/constellation/vpn.jsx
Normal file
|
@ -0,0 +1,220 @@
|
|||
import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as API from "../../api";
|
||||
import AddDeviceModal from "./addDevice";
|
||||
import PrettyTableView from "../../components/tableView/prettyTableView";
|
||||
import { DeleteButton } from "../../components/delete";
|
||||
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
|
||||
import IsLoggedIn from "../../isLoggedIn";
|
||||
import { Alert, Button, CircularProgress, Stack } from "@mui/material";
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
|
||||
import MainCard from "../../components/MainCard";
|
||||
import { Formik } from "formik";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import ApiModal from "../../components/apiModal";
|
||||
import { isDomain } from "../../utils/indexs";
|
||||
import ConfirmModal from "../../components/confirmModal";
|
||||
import UploadButtons from "../../components/fileUpload";
|
||||
import { useClientInfos } from "../../utils/hooks";
|
||||
|
||||
const getDefaultConstellationHostname = (config) => {
|
||||
// if domain is set, use it
|
||||
if(isDomain(config.HTTPConfig.Hostname)) {
|
||||
return "vpn." + config.HTTPConfig.Hostname;
|
||||
} else {
|
||||
return config.HTTPConfig.Hostname;
|
||||
}
|
||||
}
|
||||
|
||||
export const ConstellationVPN = () => {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [users, setUsers] = useState(null);
|
||||
const [devices, setDevices] = useState(null);
|
||||
const {role} = useClientInfos();
|
||||
const isAdmin = role === "2";
|
||||
|
||||
const refreshConfig = async () => {
|
||||
let configAsync = await API.config.get();
|
||||
setConfig(configAsync.data);
|
||||
setDevices((await API.constellation.list()).data || []);
|
||||
if(isAdmin)
|
||||
setUsers((await API.users.list()).data || []);
|
||||
else
|
||||
setUsers([]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshConfig();
|
||||
}, []);
|
||||
|
||||
const getIcon = (r) => {
|
||||
if (r.deviceName.toLowerCase().includes("mobile") || r.deviceName.toLowerCase().includes("phone")) {
|
||||
return <MobileOutlined />
|
||||
}
|
||||
else if (r.deviceName.toLowerCase().includes("laptop") || r.deviceName.toLowerCase().includes("computer")) {
|
||||
return <LaptopOutlined />
|
||||
} else if (r.deviceName.toLowerCase().includes("desktop")) {
|
||||
return <DesktopOutlined />
|
||||
} else if (r.deviceName.toLowerCase().includes("tablet")) {
|
||||
return <TabletOutlined />
|
||||
} else if (r.deviceName.toLowerCase().includes("lighthouse") || r.deviceName.toLowerCase().includes("server")) {
|
||||
return <CompassOutlined />
|
||||
} else {
|
||||
return <CloudOutlined />
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
{(devices && config && users) ? <>
|
||||
<Stack spacing={2} style={{maxWidth: "1000px"}}>
|
||||
<div>
|
||||
<Alert severity="info">
|
||||
Constellation is a VPN that runs inside your Cosmos network. It automatically
|
||||
connects all your devices together, and allows you to access them from anywhere.
|
||||
Please refer to the <a href="https://cosmos-cloud.io/doc/61 Constellation VPN/" target="_blank">documentation</a> for more information.
|
||||
In order to connect, please use the <a href="https://cosmos-cloud.io/clients" target="_blank">Constellation App</a>.
|
||||
Constellation is currently free to use until the end of the beta, planned January 2024.
|
||||
</Alert>
|
||||
<MainCard title={"Constellation Setup"} content={config.constellationIP}>
|
||||
<Stack spacing={2}>
|
||||
{config.ConstellationConfig.Enabled && config.ConstellationConfig.SlaveMode && <>
|
||||
<Alert severity="info">
|
||||
You are currently connected to an external constellation network. Use your main Cosmos server to manage your constellation network and devices.
|
||||
</Alert>
|
||||
</>}
|
||||
<Formik
|
||||
initialValues={{
|
||||
Enabled: config.ConstellationConfig.Enabled,
|
||||
PrivateNode: config.ConstellationConfig.PrivateNode,
|
||||
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
|
||||
ConstellationHostname: (config.ConstellationConfig.ConstellationHostname && config.ConstellationConfig.ConstellationHostname != "") ? config.ConstellationConfig.ConstellationHostname :
|
||||
getDefaultConstellationHostname(config)
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
let newConfig = { ...config };
|
||||
newConfig.ConstellationConfig.Enabled = values.Enabled;
|
||||
newConfig.ConstellationConfig.PrivateNode = values.PrivateNode;
|
||||
newConfig.ConstellationConfig.NebulaConfig.Relay.AMRelay = values.IsRelay;
|
||||
newConfig.ConstellationConfig.ConstellationHostname = values.ConstellationHostname;
|
||||
setTimeout(() => {
|
||||
refreshConfig();
|
||||
}, 1500);
|
||||
return API.config.set(newConfig);
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={2}>
|
||||
{formik.values.Enabled && <Stack spacing={2} direction="row">
|
||||
<Button
|
||||
disableElevation
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
await API.constellation.restart();
|
||||
}}
|
||||
>
|
||||
Restart VPN Service
|
||||
</Button>
|
||||
<ApiModal callback={API.constellation.getLogs} label={"Show VPN logs"} />
|
||||
<ApiModal callback={API.constellation.getConfig} label={"Show VPN Config"} />
|
||||
<ConfirmModal
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
label={"Reset Network"}
|
||||
content={"This will completely reset the network, and disconnect all the clients. You will need to reconnect them. This cannot be undone."}
|
||||
callback={async () => {
|
||||
await API.constellation.reset();
|
||||
refreshConfig();
|
||||
}}
|
||||
/>
|
||||
</Stack>}
|
||||
<CosmosCheckbox formik={formik} name="Enabled" label="Constellation Enabled" />
|
||||
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
|
||||
{formik.values.Enabled && <>
|
||||
<CosmosCheckbox formik={formik} name="IsRelay" label="Relay requests via this Node" />
|
||||
<CosmosCheckbox formik={formik} name="PrivateNode" label="This node is Private (no public IP)" />
|
||||
{!formik.values.PrivateNode && <>
|
||||
<Alert severity="info">This is your Constellation hostname, that you will use to connect. If you are using a domain name, this needs to be different from your server's hostname. Whatever the domain you choose, it is very important that you make sure there is a A entry in your domain DNS pointing to this server. <strong>If you change this value, you will need to reset your network and reconnect all the clients!</strong></Alert>
|
||||
<CosmosInputText formik={formik} name="ConstellationHostname" label="Constellation Hostname" />
|
||||
</>}
|
||||
</>}
|
||||
</>}
|
||||
<LoadingButton
|
||||
disableElevation
|
||||
loading={formik.isSubmitting}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
<UploadButtons
|
||||
accept=".yml,.yaml"
|
||||
label={"Upload External Constellation Network File"}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
OnChange={async (e) => {
|
||||
let file = e.target.files[0];
|
||||
await API.constellation.connect(file);
|
||||
setTimeout(() => {
|
||||
refreshConfig();
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</div>
|
||||
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
|
||||
<CosmosFormDivider title={"Devices"} />
|
||||
<PrettyTableView
|
||||
data={devices.filter((d) => !d.blocked)}
|
||||
getKey={(r) => r.deviceName}
|
||||
buttons={[
|
||||
<AddDeviceModal users={users} config={config} refreshConfig={refreshConfig} devices={devices}/>,
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
title: '',
|
||||
field: getIcon,
|
||||
},
|
||||
{
|
||||
title: 'Device Name',
|
||||
field: (r) => <strong>{r.deviceName}</strong>,
|
||||
},
|
||||
{
|
||||
title: 'Owner',
|
||||
field: (r) => <strong>{r.nickname}</strong>,
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
|
||||
},
|
||||
{
|
||||
title: 'Constellation IP',
|
||||
screenMin: 'md',
|
||||
field: (r) => r.ip,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
clickable: true,
|
||||
field: (r) => {
|
||||
return <DeleteButton onDelete={async () => {
|
||||
await API.constellation.block(r.nickname, r.deviceName, true);
|
||||
refreshConfig();
|
||||
}}></DeleteButton>
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>}
|
||||
</Stack>
|
||||
</> : <center>
|
||||
<CircularProgress color="inherit" size={20} />
|
||||
</center>}
|
||||
</>
|
||||
};
|
507
client/src/pages/dashboard/AlertPage.jsx
Normal file
|
@ -0,0 +1,507 @@
|
|||
import * as React from 'react';
|
||||
import IsLoggedIn from '../../isLoggedIn';
|
||||
import * as API from '../../api';
|
||||
import MainCard from '../../components/MainCard';
|
||||
import { Formik, Field, useFormik, FormikProvider } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
FormHelperText,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Skeleton,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { ExclamationCircleOutlined, InfoCircleOutlined, PlusCircleOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import PrettyTableView from '../../components/tableView/prettyTableView';
|
||||
import { DeleteButton } from '../../components/delete';
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
|
||||
import { MetricPicker } from './MetricsPicker';
|
||||
|
||||
const DisplayOperator = (operator) => {
|
||||
switch (operator) {
|
||||
case 'gt':
|
||||
return '>';
|
||||
case 'lt':
|
||||
return '<';
|
||||
case 'eq':
|
||||
return '=';
|
||||
default:
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
const AlertValidationSchema = Yup.object().shape({
|
||||
name: Yup.string().required('Name is required'),
|
||||
trackingMetric: Yup.string().required('Tracking metric is required'),
|
||||
conditionOperator: Yup.string().required('Condition operator is required'),
|
||||
conditionValue: Yup.number().required('Condition value is required'),
|
||||
period: Yup.string().required('Period is required'),
|
||||
});
|
||||
|
||||
const EditAlertModal = ({ open, onClose, onSave }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: open.Name || 'New Alert',
|
||||
trackingMetric: open.TrackingMetric || '',
|
||||
conditionOperator: (open.Condition && open.Condition.Operator) || 'gt',
|
||||
conditionValue: (open.Condition && open.Condition.Value) || 0,
|
||||
conditionPercent: (open.Condition && open.Condition.Percent) || false,
|
||||
period: open.Period || 'latest',
|
||||
actions: open.Actions || [],
|
||||
throttled: typeof open.Throttled === 'boolean' ? open.Throttled : true,
|
||||
severity: open.Severity || 'error',
|
||||
},
|
||||
validationSchema: AlertValidationSchema,
|
||||
onSubmit: (values) => {
|
||||
values.actions = values.actions.filter((a) => !a.removed);
|
||||
onSave(values);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Edit Alert</DialogTitle>
|
||||
<FormikProvider value={formik}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<CosmosInputText
|
||||
name="name"
|
||||
label="Name of the alert"
|
||||
formik={formik}
|
||||
required
|
||||
/>
|
||||
<MetricPicker
|
||||
name="trackingMetric"
|
||||
label="Metric to track"
|
||||
formik={formik}
|
||||
required
|
||||
/>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<CosmosSelect
|
||||
name="conditionOperator"
|
||||
label="Trigger Condition Operator"
|
||||
formik={formik}
|
||||
options={[
|
||||
['gt', '>'],
|
||||
['lt', '<'],
|
||||
['eq', '='],
|
||||
]}
|
||||
>
|
||||
</CosmosSelect>
|
||||
<CosmosInputText
|
||||
name="conditionValue"
|
||||
label="Trigger Condition Value"
|
||||
formik={formik}
|
||||
required
|
||||
/>
|
||||
<CosmosCheckbox
|
||||
style={{paddingTop: '20px'}}
|
||||
name="conditionPercent"
|
||||
label="Condition is a percent of max value"
|
||||
formik={formik}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<CosmosSelect
|
||||
name="period"
|
||||
label="Period (how often to check the metric)"
|
||||
formik={formik}
|
||||
options={[
|
||||
['latest', 'Latest'],
|
||||
['hourly', 'Hourly'],
|
||||
['daily', 'Daily'],
|
||||
]}></CosmosSelect>
|
||||
|
||||
<CosmosSelect
|
||||
name="severity"
|
||||
label="Severity"
|
||||
formik={formik}
|
||||
options={[
|
||||
['info', 'Info'],
|
||||
['warn', 'Warning'],
|
||||
['error', 'Error'],
|
||||
]}></CosmosSelect>
|
||||
|
||||
<CosmosCheckbox
|
||||
name="throttled"
|
||||
label="Throttle (only triggers a maximum of once a day)"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosFormDivider title={'Action Triggers'} />
|
||||
|
||||
<Stack direction="column" spacing={2}>
|
||||
{formik.values.actions
|
||||
.map((action, index) => {
|
||||
return !action.removed && <>
|
||||
{action.Type === 'stop' &&
|
||||
<Alert severity="info">Stop action will attempt to stop/disable any resources (ex. Containers, routes, etc... ) attachted to the metric.
|
||||
This will only have an effect on metrics specific to a resources (ex. CPU of a specific container). It will not do anything on global metric such as global used CPU</Alert>
|
||||
}
|
||||
<Stack direction="row" spacing={2} key={index}>
|
||||
<Box style={{
|
||||
width: '100%',
|
||||
}}>
|
||||
<CosmosSelect
|
||||
name={`actions.${index}.Type`}
|
||||
label="Action Type"
|
||||
formik={formik}
|
||||
options={[
|
||||
['notification', 'Send a notification'],
|
||||
['email', 'Send an email'],
|
||||
['stop', 'Stop resources causing the alert'],
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box style={{
|
||||
height: '95px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<DeleteButton
|
||||
onDelete={() => {
|
||||
formik.setFieldValue(`actions.${index}.removed`, true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
})}
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<PlusCircleOutlined />}
|
||||
onClick={() => {
|
||||
formik.setFieldValue('actions', [
|
||||
...formik.values.actions,
|
||||
{
|
||||
Type: 'notification',
|
||||
},
|
||||
]);
|
||||
}}>
|
||||
Add Action
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button variant='contained' type="submit">Save</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</FormikProvider>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const AlertPage = () => {
|
||||
const [config, setConfig] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const [metrics, setMetrics] = React.useState({});
|
||||
|
||||
function refresh() {
|
||||
API.config.get().then((res) => {
|
||||
setConfig(res.data);
|
||||
setIsLoading(false);
|
||||
});
|
||||
API.metrics.list().then((res) => {
|
||||
setMetrics(res.data);
|
||||
});
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
const setEnabled = (name) => (event) => {
|
||||
setIsLoading(true);
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: {
|
||||
...config.MonitoringAlerts,
|
||||
[name]: {
|
||||
...config.MonitoringAlerts[name],
|
||||
Enabled: event.target.checked,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
API.config.set(toSave).then(() => {
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const deleteAlert = (name) => {
|
||||
setIsLoading(true);
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: {
|
||||
...config.MonitoringAlerts,
|
||||
}
|
||||
};
|
||||
delete toSave.MonitoringAlerts[name];
|
||||
|
||||
API.config.set(toSave).then(() => {
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const saveAlert = (data) => {
|
||||
setIsLoading(true);
|
||||
|
||||
data.conditionValue = parseInt(data.conditionValue);
|
||||
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: {
|
||||
...config.MonitoringAlerts,
|
||||
[data.name]: {
|
||||
Name: data.name,
|
||||
Enabled: true,
|
||||
TrackingMetric: data.trackingMetric,
|
||||
Condition: {
|
||||
Operator: data.conditionOperator,
|
||||
Value: data.conditionValue,
|
||||
Percent: data.conditionPercent,
|
||||
},
|
||||
Period: data.period,
|
||||
Actions: data.actions,
|
||||
LastTriggered: null,
|
||||
Throttled: data.throttled,
|
||||
Severity: data.severity,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
API.config.set(toSave).then(() => {
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const resetTodefault = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: {
|
||||
"Anti Crypto-Miner": {
|
||||
"Name": "Anti Crypto-Miner",
|
||||
"Enabled": false,
|
||||
"Period": "daily",
|
||||
"TrackingMetric": "cosmos.system.docker.cpu.*",
|
||||
"Condition": {
|
||||
"Operator": "gt",
|
||||
"Value": 80
|
||||
},
|
||||
"Actions": [
|
||||
{
|
||||
"Type": "notification",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "email",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "stop",
|
||||
"Target": ""
|
||||
}
|
||||
],
|
||||
"LastTriggered": "0001-01-01T00:00:00Z",
|
||||
"Throttled": false,
|
||||
"Severity": "warn"
|
||||
},
|
||||
"Anti Memory Leak": {
|
||||
"Name": "Anti Memory Leak",
|
||||
"Enabled": false,
|
||||
"Period": "daily",
|
||||
"TrackingMetric": "cosmos.system.docker.ram.*",
|
||||
"Condition": {
|
||||
"Percent": true,
|
||||
"Operator": "gt",
|
||||
"Value": 80
|
||||
},
|
||||
"Actions": [
|
||||
{
|
||||
"Type": "notification",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "email",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "stop",
|
||||
"Target": ""
|
||||
}
|
||||
],
|
||||
"LastTriggered": "0001-01-01T00:00:00Z",
|
||||
"Throttled": false,
|
||||
"Severity": "warn"
|
||||
},
|
||||
"Disk Full Notification": {
|
||||
"Name": "Disk Full Notification",
|
||||
"Enabled": true,
|
||||
"Period": "latest",
|
||||
"TrackingMetric": "cosmos.system.disk./",
|
||||
"Condition": {
|
||||
"Percent": true,
|
||||
"Operator": "gt",
|
||||
"Value": 95
|
||||
},
|
||||
"Actions": [
|
||||
{
|
||||
"Type": "notification",
|
||||
"Target": ""
|
||||
}
|
||||
],
|
||||
"LastTriggered": "0001-01-01T00:00:00Z",
|
||||
"Throttled": true,
|
||||
"Severity": "warn"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
API.config.set(toSave).then(() => {
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const GetSevIcon = ({level}) => {
|
||||
switch (level) {
|
||||
case 'info':
|
||||
return <span style={{color: '#2196f3'}}><InfoCircleOutlined /></span>;
|
||||
case 'warn':
|
||||
return <span style={{color: '#ff9800'}}><WarningOutlined /></span>;
|
||||
case 'error':
|
||||
return <span style={{color: '#f44336'}}><ExclamationCircleOutlined /></span>;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return <div style={{maxWidth: '1200px', margin: ''}}>
|
||||
<IsLoggedIn />
|
||||
|
||||
{openModal && <EditAlertModal open={openModal} onClose={() => setOpenModal(false)} onSave={saveAlert} />}
|
||||
|
||||
<Stack direction="row" spacing={2} style={{marginBottom: '15px'}}>
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</Button>
|
||||
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
||||
setOpenModal(true);
|
||||
}}>Create</Button>
|
||||
<Button variant="outlined" color="warning" startIcon={<WarningOutlined />} onClick={() => {
|
||||
resetTodefault();
|
||||
}}>Reset to default</Button>
|
||||
</Stack>
|
||||
|
||||
{config && <>
|
||||
<Formik
|
||||
initialValues={{
|
||||
Actions: config.MonitoringAlerts
|
||||
}}
|
||||
|
||||
// validationSchema={Yup.object().shape({
|
||||
// })}
|
||||
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: values.Actions
|
||||
};
|
||||
|
||||
return API.config.set(toSave);
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<form noValidate onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={3}>
|
||||
{!config && <Skeleton variant="rectangular" height={300} />}
|
||||
{config && (!config.MonitoringAlerts || !Object.values(config.MonitoringAlerts).length) ? <Alert severity="info">No alerts configured.</Alert> : ''}
|
||||
{config && config.MonitoringAlerts && Object.values(config.MonitoringAlerts).length ? <PrettyTableView
|
||||
data={Object.values(config.MonitoringAlerts)}
|
||||
getKey={(r) => r.Name + r.Target + r.Mode}
|
||||
onRowClick={(r, k) => {
|
||||
setOpenModal(r);
|
||||
}}
|
||||
|
||||
columns={[
|
||||
{
|
||||
title: 'Enabled',
|
||||
clickable:true,
|
||||
field: (r, k) => <Checkbox disabled={isLoading} size='large' color={r.Enabled ? 'success' : 'default'}
|
||||
onChange={setEnabled(Object.keys(config.MonitoringAlerts)[k])}
|
||||
checked={r.Enabled}
|
||||
/>,
|
||||
style: {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
field: (r) => <><GetSevIcon level={r.Severity} /> {r.Name}</>,
|
||||
},
|
||||
{
|
||||
title: 'Tracking Metric',
|
||||
field: (r) => metrics[r.TrackingMetric] ? metrics[r.TrackingMetric] : r.TrackingMetric,
|
||||
},
|
||||
{
|
||||
title: 'Condition',
|
||||
screenMin: 'md',
|
||||
field: (r) => DisplayOperator(r.Condition.Operator) + ' ' + r.Condition.Value + (r.Condition.Percent ? '%' : ''),
|
||||
},
|
||||
{
|
||||
title: 'Period',
|
||||
field: (r) => r.Period,
|
||||
},
|
||||
{
|
||||
title: 'Last Triggered',
|
||||
screenMin: 'md',
|
||||
field: (r) => (r.LastTriggered != "0001-01-01T00:00:00Z") ? new Date(r.LastTriggered).toLocaleString() : 'Never',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
field: (r) => r.Actions.map((a) => a.Type).join(', '),
|
||||
screenMin: 'md',
|
||||
},
|
||||
{ title: '', clickable:true, field: (r, k) => <DeleteButton disabled={isLoading} onDelete={() => {
|
||||
deleteAlert(Object.keys(config.MonitoringAlerts)[k])
|
||||
}}/>,
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
}
|
||||
},
|
||||
]}
|
||||
/> : ''}
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</>}
|
||||
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default AlertPage;
|
|
@ -1,121 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
// third-party
|
||||
import ReactApexChart from 'react-apexcharts';
|
||||
|
||||
// chart options
|
||||
const areaChartOptions = {
|
||||
chart: {
|
||||
height: 450,
|
||||
type: 'area',
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 2
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 0
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================|| INCOME AREA CHART ||============================== //
|
||||
|
||||
const IncomeAreaChart = ({ slot }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { primary, secondary } = theme.palette.text;
|
||||
const line = theme.palette.divider;
|
||||
|
||||
const [options, setOptions] = useState(areaChartOptions);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions((prevState) => ({
|
||||
...prevState,
|
||||
colors: [theme.palette.primary.main, theme.palette.primary[700]],
|
||||
xaxis: {
|
||||
categories:
|
||||
slot === 'month'
|
||||
? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||
labels: {
|
||||
style: {
|
||||
colors: [
|
||||
secondary,
|
||||
secondary,
|
||||
secondary,
|
||||
secondary,
|
||||
secondary,
|
||||
secondary,
|
||||
secondary,
|
||||
secondary,
|
||||
secondary,
|
||||
secondary,
|
||||
secondary,
|
||||
secondary
|
||||
]
|
||||
}
|
||||
},
|
||||
axisBorder: {
|
||||
show: true,
|
||||
color: line
|
||||
},
|
||||
tickAmount: slot === 'month' ? 11 : 7
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: [secondary]
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderColor: line
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'light'
|
||||
}
|
||||
}));
|
||||
}, [primary, secondary, line, theme, slot]);
|
||||
|
||||
const [series, setSeries] = useState([
|
||||
{
|
||||
name: 'Page Views',
|
||||
data: [0, 86, 28, 115, 48, 210, 136]
|
||||
},
|
||||
{
|
||||
name: 'Sessions',
|
||||
data: [0, 43, 14, 56, 24, 105, 68]
|
||||
}
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setSeries([
|
||||
{
|
||||
name: 'Page Views',
|
||||
data: slot === 'month' ? [76, 85, 101, 98, 87, 105, 91, 114, 94, 86, 115, 35] : [31, 40, 28, 51, 42, 109, 100]
|
||||
},
|
||||
{
|
||||
name: 'Sessions',
|
||||
data: slot === 'month' ? [110, 60, 150, 35, 60, 36, 26, 45, 65, 52, 53, 41] : [11, 32, 45, 32, 34, 52, 41]
|
||||
}
|
||||
]);
|
||||
}, [slot]);
|
||||
|
||||
return <ReactApexChart options={options} series={series} type="area" height={450} />;
|
||||
};
|
||||
|
||||
IncomeAreaChart.propTypes = {
|
||||
slot: PropTypes.string
|
||||
};
|
||||
|
||||
export default IncomeAreaChart;
|
88
client/src/pages/dashboard/MetricHeaders.jsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
Button,
|
||||
Stack,
|
||||
} from '@mui/material';
|
||||
|
||||
import { formatDate } from './components/utils';
|
||||
|
||||
const MetricHeaders = ({loaded, slot, setSlot, zoom, setZoom}) => {
|
||||
const resetZoom = () => {
|
||||
setZoom({
|
||||
xaxis: {}
|
||||
});
|
||||
}
|
||||
|
||||
let xAxis = [];
|
||||
|
||||
if(slot === 'latest') {
|
||||
for(let i = 0; i < 100; i++) {
|
||||
xAxis.unshift(i);
|
||||
}
|
||||
}
|
||||
else if(slot === 'hourly') {
|
||||
for(let i = 0; i < 48; i++) {
|
||||
let now = new Date();
|
||||
now.setHours(now.getHours() - i);
|
||||
now.setMinutes(0);
|
||||
now.setSeconds(0);
|
||||
xAxis.unshift(formatDate(now, true));
|
||||
}
|
||||
} else if(slot === 'daily') {
|
||||
for(let i = 0; i < 30; i++) {
|
||||
let now = new Date();
|
||||
now.setDate(now.getDate() - i);
|
||||
xAxis.unshift(formatDate(now));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{loaded && <div style={{zIndex:2, position: 'relative'}}>
|
||||
<Stack direction="row" alignItems="center" spacing={0} style={{marginTop: 10}}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {setSlot('latest'); resetZoom()}}
|
||||
color={slot === 'latest' ? 'primary' : 'secondary'}
|
||||
variant={slot === 'latest' ? 'outlined' : 'text'}
|
||||
>
|
||||
Latest
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {setSlot('hourly'); resetZoom()}}
|
||||
color={slot === 'hourly' ? 'primary' : 'secondary'}
|
||||
variant={slot === 'hourly' ? 'outlined' : 'text'}
|
||||
>
|
||||
Hourly
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {setSlot('daily'); resetZoom()}}
|
||||
color={slot === 'daily' ? 'primary' : 'secondary'}
|
||||
variant={slot === 'daily' ? 'outlined' : 'text'}
|
||||
>
|
||||
Daily
|
||||
</Button>
|
||||
|
||||
{zoom.xaxis.min && <Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setZoom({
|
||||
xaxis: {}
|
||||
});
|
||||
}}
|
||||
color={'primary'}
|
||||
variant={'outlined'}
|
||||
>
|
||||
Reset Zoom
|
||||
</Button>}
|
||||
</Stack>
|
||||
</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricHeaders;
|
121
client/src/pages/dashboard/MetricsPicker.jsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Checkbox,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
Typography,
|
||||
FormHelperText,
|
||||
TextField,
|
||||
MenuItem,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Accordion,
|
||||
Chip,
|
||||
Box,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Autocomplete,
|
||||
|
||||
} from '@mui/material';
|
||||
import { Field } from 'formik';
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import * as API from '../../api';
|
||||
|
||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
|
||||
export const MetricPicker = ({ metricsInit, name, style, value, errors, placeholder, onChange, label, formik }) => {
|
||||
const [metrics, setMetrics] = React.useState(metricsInit || {});
|
||||
|
||||
function refresh() {
|
||||
API.metrics.list().then((res) => {
|
||||
let m = [];
|
||||
let wildcards = {};
|
||||
|
||||
Object.keys(res.data).forEach((key) => {
|
||||
m.push({
|
||||
label: res.data[key],
|
||||
value: key,
|
||||
});
|
||||
|
||||
let keysplit = key.split('.');
|
||||
if (keysplit.length > 1) {
|
||||
for (let i = 0; i < keysplit.length - 1; i++) {
|
||||
let wildcard = keysplit.slice(0, i + 1).join('.') + '.*';
|
||||
wildcards[wildcard] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(wildcards).forEach((key) => {
|
||||
m.push({
|
||||
label: "Wildcard for " + key.split('.*')[0],
|
||||
value: key,
|
||||
});
|
||||
});
|
||||
|
||||
setMetrics(m);
|
||||
});
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!metricsInit)
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return <Grid item xs={12}>
|
||||
<Stack spacing={1} style={style}>
|
||||
{label && <InputLabel htmlFor={name}>{label}</InputLabel>}
|
||||
{/* <OutlinedInput
|
||||
id={name}
|
||||
type={'text'}
|
||||
value={value || (formik && formik.values[name])}
|
||||
name={name}
|
||||
onBlur={(...ar) => {
|
||||
return formik && formik.handleBlur(...ar);
|
||||
}}
|
||||
onChange={(...ar) => {
|
||||
onChange && onChange(...ar);
|
||||
return formik && formik.handleChange(...ar);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
fullWidth
|
||||
error={Boolean(formik && formik.touched[name] && formik.errors[name])}
|
||||
/> */}
|
||||
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
name={name}
|
||||
value={value || (formik && formik.values[name])}
|
||||
id="combo-box-demo"
|
||||
isOptionEqualToValue={(option, value) => option.value === value}
|
||||
options={metrics}
|
||||
freeSolo
|
||||
getOptionLabel={(option) => {
|
||||
return option.label ?
|
||||
`${option.value} - ${option.label}` : (formik && formik.values[name]);
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
onChange && onChange(newValue.value);
|
||||
return formik && formik.setFieldValue(name, newValue.value);
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
/>
|
||||
|
||||
{formik && formik.touched[name] && formik.errors[name] && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
{errors && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
// third-party
|
||||
import ReactApexChart from 'react-apexcharts';
|
||||
|
||||
// chart options
|
||||
const barChartOptions = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
height: 365,
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '45%',
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
xaxis: {
|
||||
categories: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: false
|
||||
},
|
||||
grid: {
|
||||
show: false
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================|| MONTHLY BAR CHART ||============================== //
|
||||
|
||||
const MonthlyBarChart = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { primary, secondary } = theme.palette.text;
|
||||
const info = theme.palette.info.light;
|
||||
|
||||
const [series] = useState([
|
||||
{
|
||||
data: [80, 95, 70, 42, 65, 55, 78]
|
||||
}
|
||||
]);
|
||||
|
||||
const [options, setOptions] = useState(barChartOptions);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions((prevState) => ({
|
||||
...prevState,
|
||||
colors: [info],
|
||||
xaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary]
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'light'
|
||||
}
|
||||
}));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [primary, info, secondary]);
|
||||
|
||||
return (
|
||||
<div id="chart">
|
||||
<ReactApexChart options={options} series={series} type="bar" height={365} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthlyBarChart;
|
|
@ -1,224 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
// material-ui
|
||||
import { Box, Link, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material';
|
||||
|
||||
// third-party
|
||||
import NumberFormat from 'react-number-format';
|
||||
|
||||
// project import
|
||||
import Dot from '../../components/@extended/Dot';
|
||||
|
||||
function createData(trackingNo, name, fat, carbs, protein) {
|
||||
return { trackingNo, name, fat, carbs, protein };
|
||||
}
|
||||
|
||||
const rows = [
|
||||
createData(84564564, 'Camera Lens', 40, 2, 40570),
|
||||
createData(98764564, 'Laptop', 300, 0, 180139),
|
||||
createData(98756325, 'Mobile', 355, 1, 90989),
|
||||
createData(98652366, 'Handset', 50, 1, 10239),
|
||||
createData(13286564, 'Computer Accessories', 100, 1, 83348),
|
||||
createData(86739658, 'TV', 99, 0, 410780),
|
||||
createData(13256498, 'Keyboard', 125, 2, 70999),
|
||||
createData(98753263, 'Mouse', 89, 2, 10570),
|
||||
createData(98753275, 'Desktop', 185, 1, 98063),
|
||||
createData(98753291, 'Chair', 100, 0, 14001)
|
||||
];
|
||||
|
||||
function descendingComparator(a, b, orderBy) {
|
||||
if (b[orderBy] < a[orderBy]) {
|
||||
return -1;
|
||||
}
|
||||
if (b[orderBy] > a[orderBy]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getComparator(order, orderBy) {
|
||||
return order === 'desc' ? (a, b) => descendingComparator(a, b, orderBy) : (a, b) => -descendingComparator(a, b, orderBy);
|
||||
}
|
||||
|
||||
function stableSort(array, comparator) {
|
||||
const stabilizedThis = array.map((el, index) => [el, index]);
|
||||
stabilizedThis.sort((a, b) => {
|
||||
const order = comparator(a[0], b[0]);
|
||||
if (order !== 0) {
|
||||
return order;
|
||||
}
|
||||
return a[1] - b[1];
|
||||
});
|
||||
return stabilizedThis.map((el) => el[0]);
|
||||
}
|
||||
|
||||
// ==============================|| ORDER TABLE - HEADER CELL ||============================== //
|
||||
|
||||
const headCells = [
|
||||
{
|
||||
id: 'trackingNo',
|
||||
align: 'left',
|
||||
disablePadding: false,
|
||||
label: 'Tracking No.'
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
align: 'left',
|
||||
disablePadding: true,
|
||||
label: 'Product Name'
|
||||
},
|
||||
{
|
||||
id: 'fat',
|
||||
align: 'right',
|
||||
disablePadding: false,
|
||||
label: 'Total Order'
|
||||
},
|
||||
{
|
||||
id: 'carbs',
|
||||
align: 'left',
|
||||
disablePadding: false,
|
||||
|
||||
label: 'Status'
|
||||
},
|
||||
{
|
||||
id: 'protein',
|
||||
align: 'right',
|
||||
disablePadding: false,
|
||||
label: 'Total Amount'
|
||||
}
|
||||
];
|
||||
|
||||
// ==============================|| ORDER TABLE - HEADER ||============================== //
|
||||
|
||||
function OrderTableHead({ order, orderBy }) {
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{headCells.map((headCell) => (
|
||||
<TableCell
|
||||
key={headCell.id}
|
||||
align={headCell.align}
|
||||
padding={headCell.disablePadding ? 'none' : 'normal'}
|
||||
sortDirection={orderBy === headCell.id ? order : false}
|
||||
>
|
||||
{headCell.label}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
|
||||
OrderTableHead.propTypes = {
|
||||
order: PropTypes.string,
|
||||
orderBy: PropTypes.string
|
||||
};
|
||||
|
||||
// ==============================|| ORDER TABLE - STATUS ||============================== //
|
||||
|
||||
const OrderStatus = ({ status }) => {
|
||||
let color;
|
||||
let title;
|
||||
|
||||
switch (status) {
|
||||
case 0:
|
||||
color = 'warning';
|
||||
title = 'Pending';
|
||||
break;
|
||||
case 1:
|
||||
color = 'success';
|
||||
title = 'Approved';
|
||||
break;
|
||||
case 2:
|
||||
color = 'error';
|
||||
title = 'Rejected';
|
||||
break;
|
||||
default:
|
||||
color = 'primary';
|
||||
title = 'None';
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Dot color={color} />
|
||||
<Typography>{title}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
OrderStatus.propTypes = {
|
||||
status: PropTypes.number
|
||||
};
|
||||
|
||||
// ==============================|| ORDER TABLE ||============================== //
|
||||
|
||||
export default function OrderTable() {
|
||||
const [order] = useState('asc');
|
||||
const [orderBy] = useState('trackingNo');
|
||||
const [selected] = useState([]);
|
||||
|
||||
const isSelected = (trackingNo) => selected.indexOf(trackingNo) !== -1;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<TableContainer
|
||||
sx={{
|
||||
width: '100%',
|
||||
overflowX: 'auto',
|
||||
position: 'relative',
|
||||
display: 'block',
|
||||
maxWidth: '100%',
|
||||
'& td, & th': { whiteSpace: 'nowrap' }
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
aria-labelledby="tableTitle"
|
||||
sx={{
|
||||
'& .MuiTableCell-root:first-child': {
|
||||
pl: 2
|
||||
},
|
||||
'& .MuiTableCell-root:last-child': {
|
||||
pr: 3
|
||||
}
|
||||
}}
|
||||
>
|
||||
<OrderTableHead order={order} orderBy={orderBy} />
|
||||
<TableBody>
|
||||
{stableSort(rows, getComparator(order, orderBy)).map((row, index) => {
|
||||
const isItemSelected = isSelected(row.trackingNo);
|
||||
const labelId = `enhanced-table-checkbox-${index}`;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
role="checkbox"
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
aria-checked={isItemSelected}
|
||||
tabIndex={-1}
|
||||
key={row.trackingNo}
|
||||
selected={isItemSelected}
|
||||
>
|
||||
<TableCell component="th" id={labelId} scope="row" align="left">
|
||||
<Link color="secondary" component={RouterLink} to="">
|
||||
{row.trackingNo}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell align="left">{row.name}</TableCell>
|
||||
<TableCell align="right">{row.fat}</TableCell>
|
||||
<TableCell align="left">
|
||||
<OrderStatus status={row.carbs} />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<NumberFormat value={row.protein} displayType="text" thousandSeparator prefix="$" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
// third-party
|
||||
import ReactApexChart from 'react-apexcharts';
|
||||
|
||||
// chart options
|
||||
const areaChartOptions = {
|
||||
chart: {
|
||||
height: 340,
|
||||
type: 'line',
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 1.5
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 4
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
categories: [
|
||||
'2018-05-19T00:00:00.000Z',
|
||||
'2018-06-19T00:00:00.000Z',
|
||||
'2018-07-19T01:30:00.000Z',
|
||||
'2018-08-19T02:30:00.000Z',
|
||||
'2018-09-19T03:30:00.000Z',
|
||||
'2018-10-19T04:30:00.000Z',
|
||||
'2018-11-19T05:30:00.000Z',
|
||||
'2018-12-19T06:30:00.000Z'
|
||||
],
|
||||
labels: {
|
||||
format: 'MMM'
|
||||
},
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: false
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
format: 'MM'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================|| REPORT AREA CHART ||============================== //
|
||||
|
||||
const ReportAreaChart = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { primary, secondary } = theme.palette.text;
|
||||
const line = theme.palette.divider;
|
||||
|
||||
const [options, setOptions] = useState(areaChartOptions);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions((prevState) => ({
|
||||
...prevState,
|
||||
colors: [theme.palette.warning.main],
|
||||
xaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary, secondary]
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderColor: line
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'light'
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: 'grey.500'
|
||||
}
|
||||
}
|
||||
}));
|
||||
}, [primary, secondary, line, theme]);
|
||||
|
||||
const [series] = useState([
|
||||
{
|
||||
name: 'Series 1',
|
||||
data: [58, 115, 28, 83, 63, 75, 35, 55]
|
||||
}
|
||||
]);
|
||||
|
||||
return <ReactApexChart options={options} series={series} type="line" height={345} />;
|
||||
};
|
||||
|
||||
export default ReportAreaChart;
|
|
@ -1,148 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
// third-party
|
||||
import ReactApexChart from 'react-apexcharts';
|
||||
|
||||
// chart options
|
||||
const columnChartOptions = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
height: 430,
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '30%',
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 8,
|
||||
colors: ['transparent']
|
||||
},
|
||||
xaxis: {
|
||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: '$ (thousands)'
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter(val) {
|
||||
return `$ ${val} thousands`;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
fontFamily: `'Public Sans', sans-serif`,
|
||||
offsetX: 10,
|
||||
offsetY: 10,
|
||||
labels: {
|
||||
useSeriesColors: false
|
||||
},
|
||||
markers: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
radius: '50%',
|
||||
offsexX: 2,
|
||||
offsexY: 2
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 15,
|
||||
vertical: 50
|
||||
}
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 600,
|
||||
options: {
|
||||
yaxis: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// ==============================|| SALES COLUMN CHART ||============================== //
|
||||
|
||||
const SalesColumnChart = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { primary, secondary } = theme.palette.text;
|
||||
const line = theme.palette.divider;
|
||||
|
||||
const warning = theme.palette.warning.main;
|
||||
const primaryMain = theme.palette.primary.main;
|
||||
const successDark = theme.palette.success.dark;
|
||||
|
||||
const [series] = useState([
|
||||
{
|
||||
name: 'Net Profit',
|
||||
data: [180, 90, 135, 114, 120, 145]
|
||||
},
|
||||
{
|
||||
name: 'Revenue',
|
||||
data: [120, 45, 78, 150, 168, 99]
|
||||
}
|
||||
]);
|
||||
|
||||
const [options, setOptions] = useState(columnChartOptions);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions((prevState) => ({
|
||||
...prevState,
|
||||
colors: [warning, primaryMain],
|
||||
xaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: [secondary, secondary, secondary, secondary, secondary, secondary]
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: [secondary]
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderColor: line
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'light'
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
horizontalAlign: 'right',
|
||||
labels: {
|
||||
colors: 'grey.500'
|
||||
}
|
||||
}
|
||||
}));
|
||||
}, [primary, secondary, line, warning, primaryMain, successDark]);
|
||||
|
||||
return (
|
||||
<div id="chart">
|
||||
<ReactApexChart options={options} series={series} type="bar" height={430} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesColumnChart;
|