Compare commits
594 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68a3518d13 | ||
|
|
a37094b0fa | ||
|
|
b4304e6691 | ||
|
|
ee9d92e37e | ||
|
|
2df5c28d3f | ||
|
|
f6440d3ee1 | ||
|
|
15a62419a1 | ||
|
|
23289264bd | ||
|
|
41c844b511 | ||
|
|
82260afcdd | ||
|
|
80b01f27bc | ||
|
|
88d62e5386 | ||
|
|
f26346a597 | ||
|
|
9d5d6a7815 | ||
|
|
7bb124c036 | ||
|
|
867bf7e6f5 | ||
|
|
d710ceeeb6 | ||
|
|
d6d173f05d | ||
|
|
a5fb3afcf9 | ||
|
|
ac10db95ef | ||
|
|
c806b73ff7 | ||
|
|
1d07d61a18 | ||
|
|
70382e111a | ||
|
|
e79a20a8cc | ||
|
|
461298d80a | ||
|
|
2686922392 | ||
|
|
3375d11ea4 | ||
|
|
0988db8ffc | ||
|
|
358b98bf4f | ||
|
|
2aa5d7e7ba | ||
|
|
ffe41ad5e3 | ||
|
|
b1a3eb7195 | ||
|
|
9d2fc36f61 | ||
|
|
afb219dce0 | ||
|
|
e84f80271d | ||
|
|
e872ed40f7 | ||
|
|
b0eafb68a9 | ||
|
|
3052c4dfc8 | ||
|
|
20002254e6 | ||
|
|
0f8d38b5de | ||
|
|
6fe81f0cef | ||
|
|
4b3850bf45 | ||
|
|
a285fec7e4 | ||
|
|
b4b36ef29a | ||
|
|
be8cb1efb4 | ||
|
|
3822a031dd | ||
|
|
5156cd1164 | ||
|
|
8f2c09235c | ||
|
|
96fb6cfb72 | ||
|
|
4ca542b811 | ||
|
|
a351d3d36d | ||
|
|
0ad5712ac7 | ||
|
|
ffeb8004fb | ||
|
|
f18c0d3636 | ||
|
|
6af66f9626 | ||
|
|
9e69d726e0 | ||
|
|
51e64f26ec | ||
|
|
f420a35cb7 | ||
|
|
fa315d842b | ||
|
|
e91269d513 | ||
|
|
043ce82954 | ||
|
|
1ed4b715bc | ||
|
|
68cea10fa7 | ||
|
|
a5fa465796 | ||
|
|
10f804941c | ||
|
|
3251f4042b | ||
|
|
2d98e03d7c | ||
|
|
f95fcc26b6 | ||
|
|
3a4432a6e4 | ||
|
|
af27e81747 | ||
|
|
9ffe9009fb | ||
|
|
2e454014e0 | ||
|
|
82bb667f05 | ||
|
|
1689e53f6f | ||
|
|
f646c22a67 | ||
|
|
bb5cc876ef | ||
|
|
d958d57e0d | ||
|
|
046ec35b11 | ||
|
|
b1df1f0efb | ||
|
|
48830b9713 | ||
|
|
43b66893a8 | ||
|
|
9afdea76cf | ||
|
|
537702e12f | ||
|
|
0493654c85 | ||
|
|
120b530a31 | ||
|
|
83bae06cae | ||
|
|
26e2aa7ce4 | ||
|
|
2834a05d2b | ||
|
|
502c84b1ee | ||
|
|
2b6e8c67a6 | ||
|
|
4a5dd69899 | ||
|
|
da210b58a7 | ||
|
|
4aff870f6c | ||
|
|
f60fe4ad99 | ||
|
|
a5d6c92f60 | ||
|
|
be5b9a4393 | ||
|
|
d35cb8aa7c | ||
|
|
461bb2e56c | ||
|
|
8d95122d85 | ||
|
|
a5b07f65ca | ||
|
|
03e44f5b60 | ||
|
|
8d161ac19c | ||
|
|
e3cf77e177 | ||
|
|
9c69fc1b8f | ||
|
|
d2c8fbfe0c | ||
|
|
36d167d629 | ||
|
|
4bee6fb074 | ||
|
|
43b978658e | ||
|
|
bbb7ecbe36 | ||
|
|
fc792d6411 | ||
|
|
905528a28d | ||
|
|
115165bec2 | ||
|
|
1fc3d8cd1c | ||
|
|
64821a108c | ||
|
|
a5630fd2dd | ||
|
|
55f8555f18 | ||
|
|
6ed92b735f | ||
|
|
c972de233a | ||
|
|
edd98f946b | ||
|
|
49f7549b75 | ||
|
|
1f8bfa3b30 | ||
|
|
be5329512c | ||
|
|
6a0b0b376c | ||
|
|
5f7b28cb19 | ||
|
|
3003f37779 | ||
|
|
de0cec409a | ||
|
|
5bd3ad4e90 | ||
|
|
03a54ebd29 | ||
|
|
e8f1954c55 | ||
|
|
feacab72ca | ||
|
|
159fec230d | ||
|
|
78d21b71d2 | ||
|
|
15f0d99534 | ||
|
|
e34b0f4a12 | ||
|
|
60e1c56233 | ||
|
|
4ecad70c66 | ||
|
|
7605e43eaa | ||
|
|
64ffc0378e | ||
|
|
d981e33b1f | ||
|
|
40e341792e | ||
|
|
0e3cb037d6 | ||
|
|
8fc1d232ea | ||
|
|
8a8b0428fd | ||
|
|
3749f20c5e | ||
|
|
5d7d30de0f | ||
|
|
5a3ac9b110 | ||
|
|
1326f771b9 | ||
|
|
89699c6466 | ||
|
|
9ef8030535 | ||
|
|
e21d3a40ad | ||
|
|
ade0e572d2 | ||
|
|
b4b3bcad18 | ||
|
|
247304085f | ||
|
|
7e4b51778a | ||
|
|
72a355b8d7 | ||
|
|
4187bffeeb | ||
|
|
6e70fce94f | ||
|
|
0e1fa66f75 | ||
|
|
9133d5b5f9 | ||
|
|
a5736f02e5 | ||
|
|
575b4f3790 | ||
|
|
49bb8516ed | ||
|
|
5d1b587fdd | ||
|
|
90dbb2d359 | ||
|
|
ea5aa12261 | ||
|
|
6669a59fd1 | ||
|
|
84f70dfaad | ||
|
|
a60c8dc0c0 | ||
|
|
6e5405767c | ||
|
|
2d613cac1f | ||
|
|
e61ffdd6ee | ||
|
|
81562cea8d | ||
|
|
085eed30c2 | ||
|
|
efcb34fa21 | ||
|
|
589317b094 | ||
|
|
c655b2f577 | ||
|
|
7d5201a32f | ||
|
|
01034bdb5f | ||
|
|
f224688d0c | ||
|
|
a14bafeca6 | ||
|
|
78be60a079 | ||
|
|
7a0bbba33a | ||
|
|
33cfd1f6b4 | ||
|
|
50af081a20 | ||
|
|
e6ed4498cd | ||
|
|
b9b1a51a2e | ||
|
|
3d6b76ee2b | ||
|
|
f6f29f8c1c | ||
|
|
fd468868dd | ||
|
|
ecdde39c1c | ||
|
|
aec7e8d23c | ||
|
|
95fa855701 | ||
|
|
6883083e8a | ||
|
|
9531241400 | ||
|
|
db5d0cc1f4 | ||
|
|
2d04f0165d | ||
|
|
5cbbb9c44a | ||
|
|
b0c7069a10 | ||
|
|
28f879c09e | ||
|
|
e09af56554 | ||
|
|
0676eb3738 | ||
|
|
1683d09017 | ||
|
|
01e64778ad | ||
|
|
9dba61e674 | ||
|
|
b515e164b7 | ||
|
|
9521d1768e | ||
|
|
b1f95caa4e | ||
|
|
87ea4468db | ||
|
|
0f19a29995 | ||
|
|
735be5366b | ||
|
|
d7fa9ea42b | ||
|
|
89f321671f | ||
|
|
d4c1b3f515 | ||
|
|
6d20cff0b9 | ||
|
|
5b0f89fccf | ||
|
|
3e13855d21 | ||
|
|
382756c3ec | ||
|
|
c7dca629bb | ||
|
|
da639fb2ae | ||
|
|
4c435ee1c3 | ||
|
|
736c3457a2 | ||
|
|
c1a3b7f725 | ||
|
|
3ffcab4167 | ||
|
|
064318188f | ||
|
|
2afa090a1a | ||
|
|
38db75a471 | ||
|
|
4d3a18dfc3 | ||
|
|
58bff886fb | ||
|
|
e1608311da | ||
|
|
10df1b45b4 | ||
|
|
4fedb4e28d | ||
|
|
311fb72a27 | ||
|
|
c43929212d | ||
|
|
454ff6ea91 | ||
|
|
58518b154e | ||
|
|
5835bc4c10 | ||
|
|
745aeaacdd | ||
|
|
0bf2b7fb0b | ||
|
|
347dd20db8 | ||
|
|
cf2a6407a7 | ||
|
|
8020636b24 | ||
|
|
676bb0e001 | ||
|
|
7a39409764 | ||
|
|
40fdba341f | ||
|
|
e3cc7af048 | ||
|
|
162ff9d978 | ||
|
|
79725294d9 | ||
|
|
df57fa6c6b | ||
|
|
5afec12b9e | ||
|
|
999a6e5c4c | ||
|
|
062520ff6f | ||
|
|
883acf6dc9 | ||
|
|
a7acac0d9d | ||
|
|
dc90f49b65 | ||
|
|
d62ef2a856 | ||
|
|
7996ce875c | ||
|
|
2c837f5766 | ||
|
|
9c2ccd1aa4 | ||
|
|
b20a650bcc | ||
|
|
437af70685 | ||
|
|
bfb750a564 | ||
|
|
78970361b3 | ||
|
|
2ee8cbdac1 | ||
|
|
d6b65a89ce | ||
|
|
aa5cbdbb32 | ||
|
|
64f65c027d | ||
|
|
dde1f3b094 | ||
|
|
a1177796f9 | ||
|
|
3501ca3fc9 | ||
|
|
a017567bd8 | ||
|
|
e8b405f770 | ||
|
|
0cdf266659 | ||
|
|
2a5b7cd2ff | ||
|
|
e48bb85042 | ||
|
|
928df9ff32 | ||
|
|
b2471e89d0 | ||
|
|
0c50257362 | ||
|
|
8d32419234 | ||
|
|
15fddc20d1 | ||
|
|
c5559f82ea | ||
|
|
f111efa4fd | ||
|
|
fbe9ee7210 | ||
|
|
6bf5e1256e | ||
|
|
7e66b73ce4 | ||
|
|
51083ee54a | ||
|
|
c16785f66a | ||
|
|
aacb9ea12a | ||
|
|
f372d3c087 | ||
|
|
261f558105 | ||
|
|
7f14a081af | ||
|
|
6b70ca845d | ||
|
|
319d450bec | ||
|
|
cf93d1da50 | ||
|
|
d53046d66b | ||
|
|
4a819c891e | ||
|
|
c071b0c302 | ||
|
|
aa157deb47 | ||
|
|
49dd139a34 | ||
|
|
f6efb0d487 | ||
|
|
bcd21f899e | ||
|
|
13220ad1f5 | ||
|
|
8407542364 | ||
|
|
7b8eff314a | ||
|
|
ba4ab7663d | ||
|
|
9e12fc0203 | ||
|
|
c8bae5b130 | ||
|
|
45110c1630 | ||
|
|
8189ce639c | ||
|
|
a95dbf6dea | ||
|
|
ddf7a65deb | ||
|
|
0bba4fc63f | ||
|
|
ed6721d0ff | ||
|
|
d13f97d6e3 | ||
|
|
a35c612297 | ||
|
|
28a000898f | ||
|
|
7148e22691 | ||
|
|
18afb0491c | ||
|
|
64e7c5371b | ||
|
|
2396874180 | ||
|
|
121bbf57bf | ||
|
|
54fc2533c8 | ||
|
|
5ef8d10da6 | ||
|
|
9109c39202 | ||
|
|
d17aa32eb4 | ||
|
|
8df4bd63d7 | ||
|
|
60ed17bf96 | ||
|
|
34edc91701 | ||
|
|
eaa8d084e8 | ||
|
|
94d84581c2 | ||
|
|
59551419b5 | ||
|
|
b7e91380ca | ||
|
|
3e83971bca | ||
|
|
3271d12f24 | ||
|
|
267c536516 | ||
|
|
5dc3b0fa00 | ||
|
|
53dc88d409 | ||
|
|
141d2ec303 | ||
|
|
1fd75548ab | ||
|
|
5235dbcc4d | ||
|
|
8812650c8b | ||
|
|
9aa3d15708 | ||
|
|
b33c1d6073 | ||
|
|
427685c6b6 | ||
|
|
614b3cd488 | ||
|
|
f5be46bc99 | ||
|
|
a3faf027a1 | ||
|
|
49275c6726 | ||
|
|
66cc6d92b8 | ||
|
|
a69f23580e | ||
|
|
081b26ed59 | ||
|
|
8832b5909f | ||
|
|
463177281c | ||
|
|
8315a64201 | ||
|
|
5c7c81d39b | ||
|
|
ad772f5bc6 | ||
|
|
3637defc27 | ||
|
|
11d4f4de44 | ||
|
|
e914453239 | ||
|
|
7d602ae678 | ||
|
|
3b974606a6 | ||
|
|
ab6ec02d93 | ||
|
|
f77ff713db | ||
|
|
6dce03cf5a | ||
|
|
38e653f14b | ||
|
|
563cf87d59 | ||
|
|
01a79a9781 | ||
|
|
04070ce5b1 | ||
|
|
64c1d6191c | ||
|
|
96fb7eaacd | ||
|
|
c1a3584103 | ||
|
|
9716d0def1 | ||
|
|
b33a25589c | ||
|
|
7e47328692 | ||
|
|
6971ce9d6b | ||
|
|
80ea69b416 | ||
|
|
4fecfeef70 | ||
|
|
c39864ee29 | ||
|
|
da24e83f64 | ||
|
|
4f5e2bd12b | ||
|
|
bfc6233d92 | ||
|
|
67b0280dba | ||
|
|
017a637682 | ||
|
|
45a670a4ae | ||
|
|
aca2ab3cb2 | ||
|
|
1025802ef1 | ||
|
|
e06715d2e5 | ||
|
|
0b73467346 | ||
|
|
cd7e37fa95 | ||
|
|
79580cd0b6 | ||
|
|
a18a674d51 | ||
|
|
c4ac6d1c14 | ||
|
|
834cc8476d | ||
|
|
d739e44117 | ||
|
|
ca867fb10d | ||
|
|
1eb5c21d91 | ||
|
|
4e95d1e906 | ||
|
|
a16b036be0 | ||
|
|
2f47411867 | ||
|
|
b5ff20a615 | ||
|
|
aeecc06ce1 | ||
|
|
3ccdce4055 | ||
|
|
6c8df606b1 | ||
|
|
6654703de6 | ||
|
|
851ebc36cd | ||
|
|
1fad0e8822 | ||
|
|
52a0eae95a | ||
|
|
ec12ffb90e | ||
|
|
5c5cee2dea | ||
|
|
0dce277c5a | ||
|
|
c860b3490a | ||
|
|
c75098386d | ||
|
|
ef91752982 | ||
|
|
4ec859d9bc | ||
|
|
55e47317c0 | ||
|
|
ceff5d3fc0 | ||
|
|
d6f0eee91c | ||
|
|
05da27edef | ||
|
|
82a386530e | ||
|
|
65c39bc04a | ||
|
|
7edda053ea | ||
|
|
20b39dd8f6 | ||
|
|
ab2538f5e5 | ||
|
|
641764474d | ||
|
|
9e1f1ef616 | ||
|
|
c7f2b15ac9 | ||
|
|
d3812684c2 | ||
|
|
71295836e5 | ||
|
|
511c7843d0 | ||
|
|
53ecc2ad95 | ||
|
|
17c580c2a3 | ||
|
|
f340cf64fc | ||
|
|
c91a73150f | ||
|
|
088a3ed9e9 | ||
|
|
8daf5b0955 | ||
|
|
d1f37e222d | ||
|
|
14247821fb | ||
|
|
0dbcfb8c64 | ||
|
|
dacb645550 | ||
|
|
5e76f7302a | ||
|
|
f0de3268af | ||
|
|
6cc360f2b4 | ||
|
|
8b06793b3d | ||
|
|
1820e77d5c | ||
|
|
21823de2c1 | ||
|
|
d9c6e21492 | ||
|
|
0c15c08eee | ||
|
|
ae6a1cc908 | ||
|
|
7a17156d65 | ||
|
|
66c5ceb848 | ||
|
|
a4fc99f7ab | ||
|
|
8d4faa8821 | ||
|
|
335633d208 | ||
|
|
ff8ced046a | ||
|
|
bc2b6fdd21 | ||
|
|
75a2c32d77 | ||
|
|
7c65b8bfe0 | ||
|
|
770b2612d5 | ||
|
|
2908d35808 | ||
|
|
5c085daf0b | ||
|
|
8f30708d44 | ||
|
|
c159449543 | ||
|
|
402964713a | ||
|
|
6a504e1995 | ||
|
|
e455399aef | ||
|
|
89b0dd2e4f | ||
|
|
68152254af | ||
|
|
bfab8fc134 | ||
|
|
bb5511d569 | ||
|
|
99f3e147e4 | ||
|
|
b51cbf5a00 | ||
|
|
4e77e86d47 | ||
|
|
abde5f2bad | ||
|
|
a86b1f26da | ||
|
|
12a6f6824f | ||
|
|
f00e321173 | ||
|
|
6bac17e394 | ||
|
|
4227a5578a | ||
|
|
6c9af17882 | ||
|
|
25d17082b0 | ||
|
|
e1fc474412 | ||
|
|
fb4bb95e9d | ||
|
|
5b2485ace3 | ||
|
|
8b7e349a25 | ||
|
|
76899da698 | ||
|
|
1d3d61ead9 | ||
|
|
f2c4f9bdc2 | ||
|
|
71bc0913bd | ||
|
|
65f4704b62 | ||
|
|
38c1f53f83 | ||
|
|
eb1fff8294 | ||
|
|
7705801f48 | ||
|
|
faacb77fb4 | ||
|
|
99829a9e78 | ||
|
|
c7ca21b4b8 | ||
|
|
96ecd62ac1 | ||
|
|
46231d0da2 | ||
|
|
fe787c8c12 | ||
|
|
f411afb40c | ||
|
|
5267494a87 | ||
|
|
df7def01a1 | ||
|
|
de73e0b3d8 | ||
|
|
5e0cecb7d5 | ||
|
|
f59d1076d7 | ||
|
|
fbbe68fd69 | ||
|
|
cd20320b7f | ||
|
|
e722f108aa | ||
|
|
e8c60b3215 | ||
|
|
665b206b0f | ||
|
|
4fc57df439 | ||
|
|
0668981954 | ||
|
|
58b7186065 | ||
|
|
d7aeb02f3e | ||
|
|
824f997749 | ||
|
|
f0966f2930 | ||
|
|
5a1fddb622 | ||
|
|
205d2751f4 | ||
|
|
5e9eb2456c | ||
|
|
7f671fee58 | ||
|
|
e71fc6e438 | ||
|
|
003c49829d | ||
|
|
82071150f7 | ||
|
|
d8b5cd80c8 | ||
|
|
036f48c14d | ||
|
|
21860ea45c | ||
|
|
3dfb0b9a2b | ||
|
|
77d6d9929e | ||
|
|
259c29df9f | ||
|
|
a8ab1d9820 | ||
|
|
03325cedcf | ||
|
|
87c64c495e | ||
|
|
d91987a558 | ||
|
|
e972890e69 | ||
|
|
0970ef7fb0 | ||
|
|
6fc91fc698 | ||
|
|
9e059d560c | ||
|
|
5995eddcc4 | ||
|
|
f5853d4605 | ||
|
|
03c4c65bab | ||
|
|
3fc020f417 | ||
|
|
0e79ce21c1 | ||
|
|
994fe897b7 | ||
|
|
bb64cd0341 | ||
|
|
af48de6bf8 | ||
|
|
d76eedb46f | ||
|
|
24313a9d80 | ||
|
|
89de7ec504 | ||
|
|
b6eedbba04 | ||
|
|
6607de47e8 | ||
|
|
ab34f5b215 | ||
|
|
17c320c831 | ||
|
|
f869d73e50 | ||
|
|
a610dd9e32 | ||
|
|
d1c4a7389f | ||
|
|
59754b7bf8 | ||
|
|
51a2f11c6b | ||
|
|
00cbb47e09 | ||
|
|
3d886f9422 | ||
|
|
580ccacf1a | ||
|
|
142d5d2cdf | ||
|
|
674895d354 | ||
|
|
e8c1bffbcb | ||
|
|
4957686b0d | ||
|
|
94791efabb | ||
|
|
b0e33a3e2a | ||
|
|
ac4fa7fee8 | ||
|
|
5eb59676c3 | ||
|
|
8556628c93 | ||
|
|
52428d6055 | ||
|
|
2f80505306 | ||
|
|
ed9ae47edd | ||
|
|
4ae75df35d | ||
|
|
53758e164a | ||
|
|
d453438aa0 | ||
|
|
e3a5040c55 | ||
|
|
557c45056c | ||
|
|
fe3a919417 | ||
|
|
a78ec00494 | ||
|
|
65ed0bf8b8 | ||
|
|
97b71d24a5 | ||
|
|
10a6a6d5ef | ||
|
|
9c1296e503 | ||
|
|
d78eb5ec99 | ||
|
|
fef7d02477 | ||
|
|
ee4801472d | ||
|
|
322de5629f | ||
|
|
518dd0b6d9 | ||
|
|
fba354621f | ||
|
|
d00e8b940a | ||
|
|
52d3f3b019 | ||
|
|
bc9ae6029f | ||
|
|
f0f898d2dd | ||
|
|
a2f0234d48 | ||
|
|
32bb96f4c7 | ||
|
|
c60c729250 |
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
*.buildinfo
|
||||
*.changes
|
||||
*.deb
|
||||
*.dsc
|
||||
*.tar.xz
|
||||
/proxmox-widget-toolkit-[0-9]*/
|
||||
src/.lint-incremental
|
||||
src/proxmox-dark/theme-proxmox-dark.css
|
||||
src/proxmoxlib.js
|
||||
src/proxmoxlib.min.js
|
||||
66
Makefile
@ -1,46 +1,56 @@
|
||||
include /usr/share/dpkg/pkg-info.mk
|
||||
export DEB_VERSION_UPSTREAM_REVISION
|
||||
|
||||
export PACKAGE=proxmox-widget-toolkit
|
||||
BUILDDIR ?= ${PACKAGE}-${DEB_VERSION_UPSTREAM}
|
||||
DEB=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}_all.deb
|
||||
DSC=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}.dsc
|
||||
PACKAGE=proxmox-widget-toolkit
|
||||
|
||||
GITVERSION:=$(shell git rev-parse HEAD)
|
||||
DEB=$(PACKAGE)_$(DEB_VERSION)_all.deb
|
||||
DEV_DEB=$(PACKAGE)-dev_$(DEB_VERSION)_all.deb
|
||||
|
||||
${BUILDDIR}:
|
||||
rm -rf ${BUILDDIR} ${BUILDDIR}.tmp
|
||||
cp -a src/ ${BUILDDIR}.tmp
|
||||
cp -a debian ${BUILDDIR}.tmp/
|
||||
echo "git clone git://git.proxmox.com/git/proxmox-widget-toolkit.git\\ngit checkout ${GITVERSION}" > ${BUILDDIR}.tmp/debian/SOURCE
|
||||
mv ${BUILDDIR}.tmp/ ${BUILDDIR}
|
||||
DEBS=$(DEB) $(DEV_DEB)
|
||||
DSC=$(PACKAGE)_$(DEB_VERSION).dsc
|
||||
|
||||
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
|
||||
|
||||
$(BUILDDIR): GITVERSION:=$(shell git rev-parse HEAD)
|
||||
$(BUILDDIR):
|
||||
rm -rf $(BUILDDIR) $(BUILDDIR).tmp
|
||||
cp -a src/ $(BUILDDIR).tmp
|
||||
cp -a debian $(BUILDDIR).tmp/
|
||||
echo "git clone git://git.proxmox.com/git/proxmox-widget-toolkit.git\\ngit checkout $(GITVERSION)" > $(BUILDDIR).tmp/debian/SOURCE
|
||||
mv $(BUILDDIR).tmp/ $(BUILDDIR)
|
||||
|
||||
.PHONY: deb
|
||||
deb: ${DEB}
|
||||
${DEB}: ${BUILDDIR}
|
||||
cd ${BUILDDIR}; dpkg-buildpackage -b -us -uc
|
||||
lintian ${DEB}
|
||||
deb: $(DEBS)
|
||||
$(DEBS): $(BUILDDIR)
|
||||
cd $(BUILDDIR); dpkg-buildpackage -b -us -uc
|
||||
lintian $(DEBS)
|
||||
|
||||
.PHONY: dsc
|
||||
dsc: ${DSC}
|
||||
${DSC}: ${BUILDDIR}
|
||||
cd ${BUILDDIR}; dpkg-buildpackage -S -us -uc -d
|
||||
lintian ${DSC}
|
||||
dsc: $(DSC)
|
||||
$(MAKE) clean
|
||||
$(MAKE) $(DSC)
|
||||
lintian $(DSC)
|
||||
|
||||
$(DSC): $(BUILDDIR)
|
||||
cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
|
||||
|
||||
sbuild: $(DSC)
|
||||
sbuild $(DSC)
|
||||
|
||||
.PHONY: lint
|
||||
lint: ${JSSRC}
|
||||
${MAKE} -C src lint
|
||||
lint: $(JSSRC)
|
||||
$(MAKE) -C src lint
|
||||
|
||||
.PHONY: upload
|
||||
upload: ${DEB}
|
||||
tar cf - ${DEB} | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs --dist buster
|
||||
upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
|
||||
upload: $(DEBS)
|
||||
tar cf - $(DEB) | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs --dist $(UPLOAD_DIST)
|
||||
tar cf - $(DEV_DEB) | ssh -X repoman@repo.proxmox.com -- upload --product devel --dist $(UPLOAD_DIST)
|
||||
|
||||
distclean: clean
|
||||
clean:
|
||||
$(MAKE) -C src clean
|
||||
rm -rf ${BUILDDIR} ${BUILDDIR}.tmp *.tar.gz *.dsc *.deb *.changes *.buildinfo
|
||||
find . -name '*~' -exec rm {} ';'
|
||||
rm -rf $(PACKAGE)-[0-9]*/ *.tar.* *.dsc *.deb *.changes *.buildinfo *.build
|
||||
|
||||
.PHONY: dinstall
|
||||
dinstall: ${DEB}
|
||||
dpkg -i ${DEB}
|
||||
dinstall: $(DEBS)
|
||||
dpkg -i $(DEBS)
|
||||
|
||||
950
debian/changelog
vendored
@ -1,3 +1,947 @@
|
||||
proxmox-widget-toolkit (4.3.7) bookworm; urgency=medium
|
||||
|
||||
* authentication realm edit: use correct property to derive the realm type.
|
||||
|
||||
* authentication view: allow downstream users to override the API path to
|
||||
query available authentication realms from.
|
||||
|
||||
* from: realm combobox: allow downstream users to override the API path to
|
||||
query available authentication realms from.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 26 Feb 2025 19:12:24 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.3.6) bookworm; urgency=medium
|
||||
|
||||
* object grid: fix onlineHelp setting from editorConfig for row editors
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 25 Feb 2025 18:08:50 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.3.5) bookworm; urgency=medium
|
||||
|
||||
* add form-field component for entering certificate fingerprints
|
||||
|
||||
* fix #6088: notification: matcher: use more descriptive strings
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 25 Feb 2025 17:08:11 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.3.4) bookworm; urgency=medium
|
||||
|
||||
* textarea field: add emptyText message to show markdown is supported
|
||||
|
||||
* add missing htmlEncode for some UI elements
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 20 Jan 2025 11:38:34 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.3.3) bookworm; urgency=medium
|
||||
|
||||
* display-edit field: add emptyText getter and setter to support binding to
|
||||
the property of the underlying edit-field directly.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 27 Nov 2024 12:25:44 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.3.2) bookworm; urgency=medium
|
||||
|
||||
* node: service state: restore original behavior
|
||||
|
||||
* window: add consent modal widget
|
||||
|
||||
* object grid: add support for multiline textarea widget
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 25 Nov 2024 18:32:21 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.3.1) bookworm; urgency=medium
|
||||
|
||||
* peraration to fix #5379: panel: authentication realm view: add opt-in
|
||||
column displaying whether the realm is default and allow enabling it for a
|
||||
realm
|
||||
|
||||
* various UX improvements for the webhook edit window:
|
||||
- improve layout and component hierarchy
|
||||
- use type in add button text
|
||||
- show empty-text to key-value fields
|
||||
- display validity for added key/value fields immediately
|
||||
|
||||
* add Bulgarian as available language
|
||||
|
||||
* dark theme: make icons in the permissions tree in Proxmox VE UI dark
|
||||
|
||||
* fix #3892: network: add bridge VIDs field for Linux bridge and enable them
|
||||
if VLAN-aware is enabled.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 19 Nov 2024 12:40:04 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.3.0) bookworm; urgency=medium
|
||||
|
||||
* css: add some conditions to the tag classes for the tag view
|
||||
|
||||
* utils: add base64 conversion helper
|
||||
|
||||
* notification: add UI for adding/updating webhook targets
|
||||
|
||||
* fix #5836: ui: translate systemd states in system service view
|
||||
|
||||
* fix #5611: node service view: hide non-installed system services by
|
||||
default
|
||||
|
||||
* password edit: allow one to override the minimum length parameter
|
||||
|
||||
* fix #5831: ui: right-align s.m.a.r.t numerical table data
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 11 Nov 2024 21:57:31 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.2.4) bookworm; urgency=medium
|
||||
|
||||
* notification: matcher: match-field: show known fields/values
|
||||
|
||||
* notification: matcher: move match-severity and match-calendar fields to
|
||||
panel
|
||||
|
||||
* css: dark theme: fix panel borders for the Proxmox Mail Gateway's EOL
|
||||
notice widget
|
||||
|
||||
* fix opening a link from another site to a web UI of our products by
|
||||
setting the auth-cookie's 'SameSite' attribute to lax, which is the safe
|
||||
default in modern browsers.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 16 Oct 2024 18:54:37 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.2.3) bookworm; urgency=medium
|
||||
|
||||
* realm edit: don't send type as extra parameter when 'useTypeInUrl' is set
|
||||
|
||||
* realm edit: don't send 'delete' parameter when creating new entry
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 25 Apr 2024 11:45:12 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.2.2) bookworm; urgency=medium
|
||||
|
||||
* form: move network VLAN field widget over from PVE
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 24 Apr 2024 21:44:12 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.2.1) bookworm; urgency=medium
|
||||
|
||||
* fix #5251: tfa: set one-time-code auto-complete hint on TOTP input field
|
||||
|
||||
* sendmail: smtp: allow one to override the default mail author
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 23 Apr 2024 19:25:06 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.2.0) bookworm; urgency=medium
|
||||
|
||||
* window: add widget for Active Directory specific LDAP-like authentication
|
||||
|
||||
* window: ldap: add tooltips for firstname, lastname and email attributes
|
||||
|
||||
* dark-mode: set intentionally black icons to `$icon-color`
|
||||
|
||||
* window: edit: avoid sharing custom config objects between subclasses
|
||||
|
||||
* i18n: make various user-facing strings translatable
|
||||
|
||||
* remove button: allow one to set custom confirmation message
|
||||
|
||||
* notify: change 'Remove' button to 'Reset' for built-in targets
|
||||
|
||||
* css: correctly mask disabled elements inside headers
|
||||
|
||||
* fix #5277: move reset button into window header toolbar. The form reset
|
||||
and the form submit button where located besides each other with the exact
|
||||
same styling. This made it easy to click the wrong one by accident, while
|
||||
most of the time not a huge issue, it's quite annoying and just
|
||||
unnecessary to do it this way. Moving the reset functionality into the
|
||||
header, besides the close tool, avoid this potential mis-click and makes
|
||||
the form simpler in general. Mis-clicks between the close and the reset
|
||||
tool in the header are not an issue because they both have the same level
|
||||
of destructiveness w.r.t. pending data. Using the font-awesome undo icon
|
||||
makes it quite clear and a tooltip, which is also exposed as ARIA label,
|
||||
helps to further improve accessibility.
|
||||
|
||||
* notes view:
|
||||
- place collapse tool on the right
|
||||
- use pencil-square-o icon for opening the editor
|
||||
- make opening the editor on double-click opt-in to avoid interfering with
|
||||
word-boundary selection of text by default.
|
||||
|
||||
* network edit: allow bridges to have any valid interface name
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Sun, 21 Apr 2024 12:31:26 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.1.5) bookworm; urgency=medium
|
||||
|
||||
* dns: provide option to change behavior from sending the new, full actual
|
||||
state to what actually changed. This fixes deleting entries in APIs like
|
||||
the one from PBS.
|
||||
|
||||
* edit window: add optional custom submit options
|
||||
|
||||
* certificates: removal prompt: don't display name if there is no name
|
||||
|
||||
* utils: api request: defer masking after layout
|
||||
|
||||
* window: password edit: add opt-in config to show a confirmation-password
|
||||
field for the current user
|
||||
|
||||
* window: password edit: clarify labels
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 21 Mar 2024 17:40:54 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.1.4) bookworm; urgency=medium
|
||||
|
||||
* fix #5074: notify: sendmail smtp: fix mailto/mailto-user parameter
|
||||
deletion
|
||||
|
||||
* i18n: use correct ISO 639-1 code for Korean with backward compat
|
||||
|
||||
* form: date time: fix changing date to end of month from a longer to a
|
||||
shorter month
|
||||
|
||||
* form: combo grid: allow one to force showing a clear trigger through a new
|
||||
showClearTrigger config value
|
||||
|
||||
* utils: add mechanism to add and override translatable notification event
|
||||
descriptions in the product specific UIs
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 28 Feb 2024 11:46:31 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.1.3) bookworm; urgency=medium
|
||||
|
||||
* notification ui: change icon for match-field tree nodes to avoid clash
|
||||
with use for LXC containers
|
||||
|
||||
* notification ui: display yellow warning triangle instead of red icon if
|
||||
the repesctive entry is not valid
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 23 Nov 2023 10:12:50 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.1.2) bookworm; urgency=medium
|
||||
|
||||
* notification matcher: fix inverted match modes
|
||||
|
||||
* notification ui: add appropriate onlineHelp anchors
|
||||
|
||||
* notification ui: add 'unknown' to match-severity dropdown
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 21 Nov 2023 21:35:56 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.1.1) bookworm; urgency=medium
|
||||
|
||||
* api-viewer: implement basic oneOf support
|
||||
|
||||
* form: displaye-edit: add one of the two missing returns
|
||||
|
||||
* notification ui: rework for new matcher based approach, drop old filter
|
||||
and grouping widgets
|
||||
|
||||
* notification: matcher: add UI for matcher editing
|
||||
|
||||
* panel: notification: add gui for SMTP endpoints
|
||||
|
||||
* notification ui: add enable checkbox for targets/matchers
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 17 Nov 2023 16:56:06 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.1.0) bookworm; urgency=medium
|
||||
|
||||
* text field: add trimValue option to auto-trim leading and trailing
|
||||
whitespace from the to be submitted value
|
||||
|
||||
* schema: endpoint types: don't translate endpoint type names
|
||||
|
||||
* adapt the date time field to be more declarative
|
||||
|
||||
* fix #4442: extend the log view for firewall to allow filtering by a
|
||||
date-time range
|
||||
|
||||
* disk list: render osdid-list if present
|
||||
|
||||
* file-level restore: enable the download-as-tar button by default, all use
|
||||
sites support that feature now.
|
||||
|
||||
* apt updates: drop handling the ChangeLogUrl, it's not returned anymore
|
||||
|
||||
* combo grid: initialize value with [] by default to avoid glitches where
|
||||
ExtJS sometimes does not finishes with initializing everything
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 14 Nov 2023 09:11:23 +0100
|
||||
|
||||
proxmox-widget-toolkit (4.0.9) bookworm; urgency=medium
|
||||
|
||||
* fix using gettext with parameter in various newly added notification and
|
||||
sendmail widgets
|
||||
|
||||
* parser: split checking IMG and A tags, make the latter more strict
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 03 Oct 2023 10:39:31 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.0.8) bookworm; urgency=medium
|
||||
|
||||
* fix #4531: acme plugins: correct change detection of dirty form fields
|
||||
|
||||
* fix #4951: accept undefined as value for the multi-disk selector
|
||||
|
||||
* auth: ldap: openid: use our proxmox textfield variant for the comment
|
||||
field to avoid that an empty comments are saved in the config
|
||||
|
||||
* utils: language map: add entry for new Croatian translation
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 13 Sep 2023 17:16:01 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.0.7) bookworm; urgency=medium
|
||||
|
||||
* fix #4874: improve error message for invalid hostname
|
||||
|
||||
* add entry for Georgian translation
|
||||
|
||||
* fix the UI not refreshing after successful certificate deletion
|
||||
|
||||
* add missing htmlEncode calls to network selector and apt repository panel
|
||||
|
||||
* add panels for notification system:
|
||||
- sendmail endpoint panel
|
||||
- gotify endpoint panel
|
||||
- notification group management panel
|
||||
- notification filter management panel
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 16 Aug 2023 10:43:02 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.0.6) bookworm; urgency=medium
|
||||
|
||||
* LDAP realm edit: forbid specifying a bind_dn without a password
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 26 Jun 2023 20:24:57 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.0.5) bookworm; urgency=medium
|
||||
|
||||
* add Українська - Ukrainian to language map
|
||||
|
||||
* apt repositories: add production ready warnings for Ceph repositories
|
||||
|
||||
* add TOTP second factor: increase the size of the quiet zone for the QR
|
||||
code, following general recommendation to make scanning it more robust
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 16 Jun 2023 15:58:27 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.0.4) bookworm; urgency=medium
|
||||
|
||||
* apt repositories: fix typo for getting the default unknown text
|
||||
|
||||
* apt repositories: avoid potential type error in classifyOrigin helper
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 09 Jun 2023 17:29:44 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.0.3) bookworm; urgency=medium
|
||||
|
||||
* date time field: fix typo in xtype name but add alias for backward compat
|
||||
|
||||
* set 'SameSite' attr of auth cookie to 'strict', which modern browsers
|
||||
already ensured in practice
|
||||
|
||||
* apt repositories: actually ignore ignore-pre-upgrade-warning
|
||||
|
||||
* apt repositories: just ignore unknown info rather than throwing an error
|
||||
|
||||
* apt repositories: detect mixed suites before major upgrade
|
||||
|
||||
* tfa: improve UX for recovery keys and when none are left
|
||||
|
||||
* tfa: show 'Locked' in 'Enabled' column if tfa is locked
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 09 Jun 2023 08:07:01 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.0.2) bookworm; urgency=medium
|
||||
|
||||
* markdown parser: allow setting "target" attribute for links
|
||||
|
||||
* fix #4756: markdown parser: allow any valid URL for link (a) tags,
|
||||
allowing one to add short-cuts like a RDP url to quickly open the remote
|
||||
viewer.
|
||||
|
||||
* ship a minified version of the widget-toolkit JS library
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Sat, 03 Jun 2023 13:45:27 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.0.1) bookworm; urgency=medium
|
||||
|
||||
* tfa: paperkey: cleanup iframes for printing after window close
|
||||
|
||||
* parser: adapt to calling convention of updated Markdown renderer library
|
||||
"marked" has since its v4.0.0
|
||||
|
||||
* fix #4551: ui: translate byte unit in `format_size`
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 01 Jun 2023 16:35:32 +0200
|
||||
|
||||
proxmox-widget-toolkit (4.0.0) bookworm; urgency=medium
|
||||
|
||||
* re-build for Debian 12 Bookworm based releases
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 25 May 2023 09:13:29 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.7.0) bullseye; urgency=medium
|
||||
|
||||
* dark-mode:
|
||||
- fix focus and focus-over states for tabs
|
||||
- fix the focused state for background image grid icons
|
||||
- style the icon for the datastore maintenance mode
|
||||
- improve apt repo group header contrast ratios
|
||||
- adjust panel header tool icons
|
||||
- fix #4618: lighten critical/warning charts/gauges colors
|
||||
|
||||
* form: combo grid: use correct method to initialize the picker to ensure
|
||||
it's cleaned up again after closed.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 17 May 2023 14:02:50 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.6.5) bullseye; urgency=medium
|
||||
|
||||
* window: ldap auth edit: avoid relying on the default bind property
|
||||
|
||||
* window: ldap auth edit: set view-model form data explicitly on edit to
|
||||
avoid a data race in chromium based browser that could result in a
|
||||
mismatch of the configured value and the initially shown one.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 28 Mar 2023 17:56:10 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.6.4) bullseye; urgency=medium
|
||||
|
||||
* dark-mode:
|
||||
- improve contrast on split buttons
|
||||
- color the custom grid and tree icons
|
||||
- set boundlist (combo box picker) background so that loading progress or
|
||||
errors are styled correctly too
|
||||
- add a small white padding to the TOTP QR-code, as some apps are confused
|
||||
otherwise
|
||||
- fix #4617: increase brightness of tree expand/collapse arrows to avoid
|
||||
overly low contrast
|
||||
|
||||
* fix #4612: mobile: avoid crash due to missing getProxy method
|
||||
|
||||
* theme edit window: ensure that the saved theme is actually valid
|
||||
|
||||
* language selector: translate entries to both native and localized variants
|
||||
|
||||
* language selector: increase picker list view width
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Sun, 26 Mar 2023 17:52:48 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.6.3) bullseye; urgency=medium
|
||||
|
||||
* dark-mode fine-tuning:
|
||||
- fix highlighting of active elements in drop down menus
|
||||
- set the icon color of filtered column headers properly
|
||||
- style checkboxes that don't use blueish active states
|
||||
- style locked guest icons properly
|
||||
- tone down border on ceph install card-like window
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 22 Mar 2023 13:25:05 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.6.2) bullseye; urgency=medium
|
||||
|
||||
* network edit: add tooltip to bridge ports inputs
|
||||
|
||||
* dark-mode: reduce background mask opacity to 0.5
|
||||
|
||||
* dark-mode: make window shadow black again to avoid a backlight that some
|
||||
have very strong opionions against
|
||||
|
||||
* rename "Theme" selector to "Color Theme" to add some context for
|
||||
translation
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 21 Mar 2023 16:46:27 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.6.1) bullseye; urgency=medium
|
||||
|
||||
* repo view: replace non-clickable checkbox with icons
|
||||
|
||||
* auth ui: add LDAP realm-edit panel and sync UI, refactored & adapted from
|
||||
the pve-manager implementation for future reuse
|
||||
|
||||
* dark-theme: improve help button contrast ratios in focused state
|
||||
|
||||
* dark-theme: make "sorted-by" header highlight more subtle
|
||||
|
||||
* dark-theme: dim warning and invalid colors more
|
||||
|
||||
* dark-theme: let the background "shine through" mask more to avoid that
|
||||
information on it becomes unreadable
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 20 Mar 2023 14:13:42 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.6.0) bullseye; urgency=medium
|
||||
|
||||
* node apt: make changelog window taller for 4:3 ratio and cleanup/modernize
|
||||
code
|
||||
|
||||
* ui: SMART: show SMART data in correct columns with correct values
|
||||
|
||||
* fix #4421: ui: guard setProxy against races of slow vs fast requests
|
||||
|
||||
* dark-theme: add initial version of the proxmox-dark theme
|
||||
|
||||
* subscription/summary/backup: stop setting the background color
|
||||
|
||||
* gauge widget: add support for a dark theme and dynamic theme switching
|
||||
|
||||
* rrd chart: add support for the dark theme and dynamic theme switching
|
||||
|
||||
* form: add a theme selector window
|
||||
|
||||
* dark-theme:
|
||||
- fix summary row background
|
||||
- increase contrast on check-boxes
|
||||
- visually remove the border around the pve resource tree
|
||||
- remove thicker borders around content
|
||||
- re-work buttons colors to appear dimmer
|
||||
- make windows stand out more against the background mask
|
||||
|
||||
* fix #4585 : toolkit: configid type: add missing "-" character support
|
||||
|
||||
* input panel: improve validity change check for advanced fields
|
||||
|
||||
* auth-realm selector: add custom store filters for callers
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 14 Mar 2023 16:25:37 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.5.5) bullseye; urgency=medium
|
||||
|
||||
* combobox grid: use the grids view for the error message
|
||||
|
||||
* combobox grid: make height for the error configurable
|
||||
|
||||
* combobox grid: instead of hiding the picker collapse it, keeping the
|
||||
internal state consitent which avoids, among other things, the need
|
||||
fo two clicks after re-selecting an item
|
||||
|
||||
* utils: always html-encode response message from errors to avoid rendering
|
||||
glitches, that while not known to be problematic from a safety POV, are
|
||||
possibly odd for people to find.
|
||||
|
||||
* form: display-edit: add safe default renderer for display field to avoid
|
||||
unproblematic, but possible odd and glitchy side-effects from the
|
||||
value-bind if the display-edit field is in iput mode.
|
||||
|
||||
* api request: add wide spread alert-error logic as smart-on option
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 31 Jan 2023 17:27:41 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.5.4) bullseye; urgency=medium
|
||||
|
||||
* api-viewer: allow text selection in the parameter and the return grids
|
||||
|
||||
* task viewer: add optional button to download full task-log
|
||||
|
||||
* permission role selector: fix renderer for column of included privileges
|
||||
for Proxmox VE
|
||||
|
||||
* permission role selector: make slightly more wide and resizeable
|
||||
|
||||
* node network view: rework finding free interface ID and move add-menu
|
||||
generation to common helper (no semantic change intended)
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 11 Jan 2023 16:09:53 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.5.3) bullseye; urgency=medium
|
||||
|
||||
* css: do not make full-style tags display as inline-block in the tree
|
||||
to avoid height jumps
|
||||
|
||||
* log, journal view: fix access to `me` after destroying
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 21 Nov 2022 11:14:27 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.5.2) bullseye; urgency=medium
|
||||
|
||||
* host disks: add 'mounted' column
|
||||
|
||||
* host disk: handle partition data from Proxmox Backup Server backend
|
||||
|
||||
* number field: avoid that a single up/down arrow key press
|
||||
increment or decrements twice
|
||||
|
||||
* toolkit: make email regex pattern match pve-common
|
||||
|
||||
* css: import action column fix from pbs, pmg
|
||||
|
||||
* fix #2703: networkedit: limit custom interface name field to 15
|
||||
characters.
|
||||
|
||||
* task progress: show text instead of bogus percentage
|
||||
|
||||
* fix #3593: add CPU affinity task set type
|
||||
|
||||
* input panel: add onSetValues hook
|
||||
|
||||
* add tag related helpers
|
||||
|
||||
* toolkit: add override for ExtJS DragDropManager to fix selection behavior
|
||||
in the drag zone
|
||||
|
||||
* rdd charts: don't display power-of-two suffix 'i' for values without unit
|
||||
suffix
|
||||
|
||||
* fix #4271: api-viewer: display nested formats instead of `[object Object]`
|
||||
|
||||
* api-viewer: show min/max for values without any other format
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 17 Nov 2022 08:37:26 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.5.1) bullseye; urgency=medium
|
||||
|
||||
* pxar file types: fix over-eager s/text/label/ so that text file icon is
|
||||
shown again
|
||||
|
||||
* file browser: disable item # size rendering
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 16 May 2022 18:03:35 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.5.0) bullseye; urgency=medium
|
||||
|
||||
* file browser: try reload again when getting a 503 error
|
||||
|
||||
* ui: acl role selector: make the picker grid wider and ensure that the
|
||||
text wraps in the privilege column
|
||||
|
||||
* fix #4001: file browser: add a configurable prefix to downloaded files
|
||||
|
||||
* fix #4001: file browser: show number of items in a directory as size, if
|
||||
available
|
||||
|
||||
* file browser: align size column to end/right
|
||||
|
||||
* file browser: only disable, not hide button if not downloadable and add
|
||||
hint for why and what to do in tooltip
|
||||
|
||||
* switch to native version format for native package
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Sun, 15 May 2022 11:46:54 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.4-10) bullseye; urgency=medium
|
||||
|
||||
* css: add proxmox-good-row class
|
||||
|
||||
* status view: fix usage calculation for fields without valid values, like
|
||||
for example SWAP can often be.
|
||||
|
||||
* buttons: add AltText to unify the hack to detect the max size of a button
|
||||
that switches its text dynamically
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 27 Apr 2022 18:58:21 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.4-9) bullseye; urgency=medium
|
||||
|
||||
* file browser: optionally allow showing a "download as tar.zst"
|
||||
button, if supported
|
||||
|
||||
* tab buttons: fix vertial centering of text and reduce padding
|
||||
slightly
|
||||
|
||||
* move over the markdown based notes view panel and edit window from
|
||||
pve-manager for reuse
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 14 Apr 2022 07:56:54 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.4-8) bullseye; urgency=medium
|
||||
|
||||
* fix #3919: log view: show first task output line correctly
|
||||
|
||||
* combo grid: clear filter on blur
|
||||
|
||||
* utils: clear cookies with secure flag set to avoid bogus browser
|
||||
warning
|
||||
|
||||
* node tasks: do not count preset filters as normal filters to avoid
|
||||
"clear filter" button glitch
|
||||
|
||||
* icons: switch cpu and ram bitmaps to svg
|
||||
|
||||
* object grid: call rendere with our scope
|
||||
|
||||
* dns view: increase api polling intervall from 1s to 10s
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 12 Apr 2022 16:45:50 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.4-7) bullseye; urgency=medium
|
||||
|
||||
* extjs: fix check for 'touch' input event in drag and drop handler, making
|
||||
the 'pen' pointer event source type, that chrome/chromium emits in some
|
||||
setups, work again.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 23 Feb 2022 12:12:13 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.4-6) bullseye; urgency=medium
|
||||
|
||||
* utils: render language: fix rendering special default value
|
||||
|
||||
* sorters: use correct property 'direction' and keep default 'ASC'
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 14 Feb 2022 11:34:42 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.4-5) bullseye; urgency=medium
|
||||
|
||||
* login: tfa: hide u2f and yubico-otp if not available
|
||||
|
||||
* improve error handling when adding webauthn entries
|
||||
|
||||
* toolkit: fix noisy ext warning of feature we do not want/use
|
||||
|
||||
* zfs detail: increase default window height
|
||||
|
||||
* zfs detail: hide the pool itself in tree view
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 13 Jan 2022 12:52:18 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.4-4) bullseye; urgency=medium
|
||||
|
||||
* utils: format duration: render years when we can avoid huge day numbers
|
||||
|
||||
* journalview: fix wrong initial load with default timespan on widget
|
||||
creation
|
||||
|
||||
* logpanel: fix glitching on fast task logs
|
||||
|
||||
* logpanel: actually catch up when following the log for tasks with an
|
||||
almost artificially high log output traffic
|
||||
|
||||
* log viewer: add heuristic for scroll-direction dependent ratio
|
||||
distribution
|
||||
|
||||
* log viewer: add heuristic for triggering a new limit load earlier to
|
||||
reduce latency on casual scrolling
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 24 Nov 2021 18:29:56 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.4-3) bullseye; urgency=medium
|
||||
|
||||
* data: diffstore: fix autoDestroyRstore option (regression from ExtJS 7)
|
||||
|
||||
* ui: OpenID edit: make username-claim field editable for arbitrary values
|
||||
|
||||
* ui: OpenID realm: allow to edit scopes
|
||||
|
||||
* ui: OpenID realm: allow to edit prompt
|
||||
|
||||
* ui: OpenID realm: allow to edit acr values
|
||||
|
||||
* form: copy BandwidthSelector/SizeField from Proxmox VE's manager
|
||||
|
||||
* bandwidth/utils: move out SizeUnits definition to more common module
|
||||
|
||||
* utils: add size unit related helpers to parse/auto-scale/format
|
||||
|
||||
* bandwidth field: allow to submit auto-scaled size-units as string
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Sat, 20 Nov 2021 21:41:37 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.4-2) bullseye; urgency=medium
|
||||
|
||||
* TFA login window: fix a formatted label showed when being low on unused
|
||||
recovery-keys
|
||||
|
||||
* proxmox checkbox: add clearOnDisable config
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 15 Nov 2021 10:23:34 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.4-1) bullseye; urgency=medium
|
||||
|
||||
* panel/RRDCharts: enable scrolling for RRDCharts on touchscreens
|
||||
|
||||
* disk selector: allow requesting partitions too
|
||||
|
||||
* fix #3589: show device name in title for SMART values window
|
||||
|
||||
* cbind: document cbind by adding a small summary and example
|
||||
|
||||
* add common utils used for u2f and webauthn, adapted from PVE and PBS,
|
||||
respectively
|
||||
|
||||
* add TFA-login, TOTP, WebAuthn and recover-key edit windows for better reuse
|
||||
|
||||
* disk list: allow wiping individual partitions
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 11 Nov 2021 21:11:16 +0100
|
||||
|
||||
proxmox-widget-toolkit (3.3-6) bullseye; urgency=medium
|
||||
|
||||
* fix #3542: node: task logs: query correct node for tasks in clusters
|
||||
|
||||
* node: add a, by default hidden, MTU column in network view
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 27 Jul 2021 16:41:01 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.3-5) bullseye; urgency=medium
|
||||
|
||||
* node: repos: add possibility to link online help
|
||||
|
||||
* api-viewer: drop extra slash in api path
|
||||
|
||||
* apt: match "Debian Backports" origin as Debian one
|
||||
|
||||
* add new shared component for the package version window
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 19 Jul 2021 17:52:08 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.3-4) bullseye; urgency=medium
|
||||
|
||||
* acme: allow wildcards as domain
|
||||
|
||||
* service view: avoid showing not installed services as error
|
||||
|
||||
* service view: fix stale stop/restart button enabled behavior
|
||||
|
||||
* service view: disable all buttons for masked/not-found/unknown services
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 13 Jul 2021 18:42:51 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.3-3) bullseye; urgency=medium
|
||||
|
||||
* realm view/edit: make more generic for better reuse
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 12 Jul 2021 09:52:27 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.3-2) bullseye; urgency=medium
|
||||
|
||||
* node: repos: only show suites warning at the top if enabled repository is
|
||||
affected
|
||||
|
||||
* move over authentication-real edit window widget from Proxmox VE
|
||||
|
||||
* utils: add helper to format node's repository status
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 09 Jul 2021 17:30:43 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.2-5) bullseye; urgency=medium
|
||||
|
||||
* network: always ask for confirmation before removing a network
|
||||
interface from the configuration. While it is not that dangerous as we
|
||||
have a pending config that needs to get applied, it still is nicer to do
|
||||
for remove actions.
|
||||
|
||||
* node: tasks: use helper to format status again for a localized warnings
|
||||
text
|
||||
|
||||
* window: safe-destroy: add taskDone and apiCallDone callbacks
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 08 Jul 2021 14:30:44 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.2-4) bullseye; urgency=medium
|
||||
|
||||
* node: APT repositories: upgrade "no Proxmox product repo configured" from
|
||||
warning to error
|
||||
|
||||
* node: task history: deselect entries when filter changes
|
||||
|
||||
* node: task history: show errors on store load
|
||||
|
||||
* node: task history: add 'clear filter' button
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 05 Jul 2021 16:50:23 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.2-3) bullseye; urgency=medium
|
||||
|
||||
* node: repos: handle that components can be undefined
|
||||
|
||||
* markdown: encode bad nodes HTML instead of pruning it
|
||||
|
||||
* markdown: make sanitizer more strict in filtering tags and ensure that the
|
||||
src and the href attributes point to a HTTP url, or is a data-url on a
|
||||
image.
|
||||
|
||||
* info widget: early return from update if text & value stayed the same
|
||||
|
||||
* utils: updateColumnWidth: drop duplicate implementation and allow
|
||||
overriding tresholdWidth
|
||||
|
||||
* utils: updateColumnWidth: directly calculate column count by threshold,
|
||||
automatically using more columns on wide containers.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 05 Jul 2021 10:10:47 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.2-2) bullseye; urgency=medium
|
||||
|
||||
* avoid using unique ids for components that may have more than one instance
|
||||
at the same time. Fixes and issue with switching between nodes on the new
|
||||
APT Repository panel.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 05 Jul 2021 09:47:46 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.2-1) bullseye; urgency=medium
|
||||
|
||||
* css: some markdown heading and paragraph font-size & padding tuning
|
||||
|
||||
* node: services: fix logic for displaying unit state
|
||||
|
||||
* add Debian and Proxmox symbol logos and css
|
||||
|
||||
* node: apt: spawn a window for adding repository
|
||||
|
||||
* utils: add getOpenIDRedirectionAuthorization helper
|
||||
|
||||
* factor out userid parsing and username, realm renderer to Utils
|
||||
|
||||
* add OpenID icon + css class
|
||||
|
||||
* node: APT Repositories: rework top status and error grid
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Sat, 03 Jul 2021 00:12:34 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.1-4) bullseye; urgency=medium
|
||||
|
||||
* cbind mixin: also descend in elements with an cbind property
|
||||
|
||||
* node: tasks: merge improved Filters over from Proxmox Backup Server
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 28 Jun 2021 19:14:10 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.1-3) bullseye; urgency=medium
|
||||
|
||||
* css: markdown: add some nicer table, blockquote and task-list checkbox
|
||||
styling
|
||||
|
||||
* Journal View: fix flickering in journal livemode
|
||||
|
||||
* node/services: optionally show unit-, and active-states
|
||||
|
||||
* add initial building blocks for an APT repositories UI
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 23 Jun 2021 23:11:37 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.1-2) bullseye; urgency=medium
|
||||
|
||||
* ui: network: add, by default hidden, columns for the `vlan-id` and the
|
||||
`vlan-raw-device`
|
||||
|
||||
* add interface for markdown parser and wire-up marked to it
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 18 Jun 2021 15:32:27 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.1-1) bullseye; urgency=medium
|
||||
|
||||
* support ExtJS 7
|
||||
|
||||
* object grid: allow one to declaratively specify rows
|
||||
|
||||
* disk list: add wipe disk button which users of this widget can opt-in
|
||||
|
||||
* data/ProxmoxProxy: set responseType to undefined for XMLHTTPRequest
|
||||
|
||||
* ship api-viewer in new proxmox-widget-toolkit-dev package to improve
|
||||
possibility for code reuse
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 02 Jun 2021 16:16:02 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.0-2) bullseye; urgency=medium
|
||||
|
||||
* disks: fix regression in S.M.A.R.T. window
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 14 May 2021 10:34:57 +0200
|
||||
|
||||
proxmox-widget-toolkit (3.0-1) bullseye; urgency=medium
|
||||
|
||||
* re-build for Debian 11 Bullseye based releases
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 13 May 2021 19:46:29 +0200
|
||||
|
||||
proxmox-widget-toolkit (2.5-4) pve pmg; urgency=medium
|
||||
|
||||
* info widget: set default usage warning threshold from 60% to 75%
|
||||
@ -5,7 +949,7 @@ proxmox-widget-toolkit (2.5-4) pve pmg; urgency=medium
|
||||
* node disk: S.M.A.R.T.: improve the simple layout and enable autoscroll for
|
||||
long output
|
||||
|
||||
* format/render size: allow one to specifiy if base 2 or 10 (SI unit) is
|
||||
* format/render size: allow one to specify if base 2 or 10 (SI unit) is
|
||||
desired
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 07 May 2021 18:00:29 +0200
|
||||
@ -21,7 +965,7 @@ proxmox-widget-toolkit (2.5-2) pve pmg; urgency=medium
|
||||
|
||||
* rrd chart: add option to render values and Y-axis with a power-of-two base
|
||||
|
||||
* safe destroy: allow specifing additional items
|
||||
* safe destroy: allow specifying additional items
|
||||
|
||||
* utils: add several render and helper functions from Proxmox VE's manager
|
||||
|
||||
@ -702,7 +1646,7 @@ proxmox-widget-toolkit (1.0-6) unstable; urgency=medium
|
||||
|
||||
* change 'create' parameter to 'isCreate'
|
||||
|
||||
* make network devices types configureable
|
||||
* make network devices types configurable
|
||||
|
||||
* use Proxmox.window.TaskProgress instead of PVE.window.TaskProgress
|
||||
|
||||
|
||||
1
debian/compat
vendored
@ -1 +0,0 @@
|
||||
12
|
||||
21
debian/control
vendored
@ -2,13 +2,24 @@ Source: proxmox-widget-toolkit
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Proxmox Support Team <support@proxmox.com>
|
||||
Build-Depends: debhelper (>= 12~),
|
||||
pve-eslint (>= 7.12.1-1),
|
||||
Standards-Version: 4.5.1
|
||||
Build-Depends: debhelper-compat (= 13),
|
||||
libjs-marked,
|
||||
pve-eslint (>= 7.28.0),
|
||||
sassc,
|
||||
uglifyjs,
|
||||
Standards-Version: 4.6.2
|
||||
Homepage: https://www.proxmox.com
|
||||
|
||||
Package: proxmox-widget-toolkit
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}
|
||||
Description: ExtJS Helper Classes for Proxmox
|
||||
ExtJS Helper Classes to easy access to Proxmox APIs.
|
||||
Description: Core Widgets and ExtJS Helper Classes for Proxmox Web UIs
|
||||
The base framework providing widgets, models, and general utilities for the
|
||||
ExtJS based Web UIs of various Proxmox projects
|
||||
|
||||
Package: proxmox-widget-toolkit-dev
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}
|
||||
Description: ExtJS based widgets and utilities for development
|
||||
Contains some common JavaScript code that some Proxmox projects might used to
|
||||
build common interfaces, like the API viewer in each documnetation repo.
|
||||
|
||||
30
debian/copyright
vendored
@ -2,15 +2,25 @@ Copyright (C) 2010-2021 Proxmox Server Solutions GmbH
|
||||
|
||||
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
License: AGPLv3
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
Marked:
|
||||
The Marked JavaScript library is shipped through linkage from the Debian
|
||||
package unmodified alongside proxmox-widget-toolkit.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) Copyright (c)
|
||||
2011-2018, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
For the license and copyright details see `/usr/share/doc/libjs-marked/copyright`
|
||||
|
||||
2
debian/proxmox-widget-toolkit-dev.install
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
Toolkit.js /usr/share/javascript/proxmox-widget-toolkit-dev/
|
||||
api-viewer/APIViewer.js /usr/share/javascript/proxmox-widget-toolkit-dev/
|
||||
1
debian/proxmox-widget-toolkit.docs
vendored
Normal file
@ -0,0 +1 @@
|
||||
debian/SOURCE
|
||||
1
debian/proxmox-widget-toolkit.install
vendored
Normal file
@ -0,0 +1 @@
|
||||
/usr/share/javascript/proxmox-widget-toolkit
|
||||
4
debian/rules
vendored
@ -3,6 +3,10 @@
|
||||
# output every command that modifies files on the build system.
|
||||
#DH_VERBOSE = 1
|
||||
|
||||
include /usr/share/dpkg/pkg-info.mk
|
||||
|
||||
export DEB_SOURCE
|
||||
export DEB_VERSION
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
2
debian/source/format
vendored
@ -1 +1 @@
|
||||
1.0
|
||||
3.0 (native)
|
||||
|
||||
85
src/Makefile
@ -1,11 +1,19 @@
|
||||
include defines.mk
|
||||
|
||||
SUBDIRS= css images
|
||||
ESLINT ?= $(if $(shell command -v pve-eslint), pve-eslint, eslint)
|
||||
|
||||
SUBDIRS= css images proxmox-dark
|
||||
|
||||
# bundle it for now from the libjs-marked debian package to avoid touching our proxies file mapper,
|
||||
# we could also just ship a link to the packages file and load from same path as the widget-toolkit
|
||||
MARKEDJS=/usr/share/javascript/marked/marked.js
|
||||
|
||||
JSSRC= \
|
||||
Utils.js \
|
||||
Schema.js \
|
||||
Toolkit.js \
|
||||
Logo.js \
|
||||
Parser.js \
|
||||
mixin/CBind.js \
|
||||
data/reader/JsonObject.js \
|
||||
data/ProxmoxProxy.js \
|
||||
@ -14,13 +22,17 @@ JSSRC= \
|
||||
data/ObjectStore.js \
|
||||
data/RRDStore.js \
|
||||
data/TimezoneStore.js \
|
||||
data/model/NotificationConfig.js \
|
||||
data/model/Realm.js \
|
||||
data/model/Certificates.js \
|
||||
data/model/ACME.js \
|
||||
form/BandwidthSelector.js \
|
||||
form/DisplayEdit.js \
|
||||
form/ExpireDate.js \
|
||||
form/IntegerField.js \
|
||||
form/TextField.js \
|
||||
form/TextAreaField.js \
|
||||
form/VlanField.js \
|
||||
form/DateTimeField.js \
|
||||
form/Checkbox.js \
|
||||
form/KVComboBox.js \
|
||||
@ -36,37 +48,71 @@ JSSRC= \
|
||||
form/MultiDiskSelector.js \
|
||||
form/TaskTypeSelector.js \
|
||||
form/ACME.js \
|
||||
form/UserSelector.js \
|
||||
form/ThemeSelector.js \
|
||||
form/FingerprintField.js \
|
||||
button/Button.js \
|
||||
button/AltText.js \
|
||||
button/HelpButton.js \
|
||||
grid/ObjectGrid.js \
|
||||
grid/PendingObjectGrid.js \
|
||||
panel/AuthView.js \
|
||||
panel/DiskList.js \
|
||||
panel/EOLNotice.js \
|
||||
panel/InputPanel.js \
|
||||
panel/InfoWidget.js \
|
||||
panel/LogView.js \
|
||||
panel/NodeInfoRepoStatus.js \
|
||||
panel/NotificationConfigView.js \
|
||||
panel/JournalView.js \
|
||||
panel/PermissionView.js \
|
||||
panel/PruneKeepPanel.js \
|
||||
panel/RRDChart.js \
|
||||
panel/GaugeWidget.js \
|
||||
panel/GotifyEditPanel.js \
|
||||
panel/Certificates.js \
|
||||
panel/ACMEAccount.js \
|
||||
panel/ACMEPlugin.js \
|
||||
panel/ACMEDomains.js \
|
||||
panel/EmailRecipientPanel.js \
|
||||
panel/SendmailEditPanel.js \
|
||||
panel/SmtpEditPanel.js \
|
||||
panel/StatusView.js \
|
||||
panel/TfaView.js \
|
||||
panel/NotesView.js \
|
||||
panel/WebhookEditPanel.js \
|
||||
window/Edit.js \
|
||||
window/PasswordEdit.js \
|
||||
window/SafeDestroy.js \
|
||||
window/PackageVersions.js \
|
||||
window/TaskViewer.js \
|
||||
window/LanguageEdit.js \
|
||||
window/DiskSmart.js \
|
||||
window/ZFSDetail.js \
|
||||
window/Certificates.js \
|
||||
window/ConsentModal.js \
|
||||
window/ACMEAccount.js \
|
||||
window/ACMEPluginEdit.js \
|
||||
window/ACMEDomains.js \
|
||||
window/EndpointEditBase.js \
|
||||
window/NotificationMatcherEdit.js \
|
||||
window/FileBrowser.js \
|
||||
window/AuthEditBase.js \
|
||||
window/AuthEditOpenId.js \
|
||||
window/AuthEditLDAP.js \
|
||||
window/AuthEditAD.js \
|
||||
window/AuthEditSimple.js \
|
||||
window/TfaWindow.js \
|
||||
window/AddTfaRecovery.js \
|
||||
window/AddTotp.js \
|
||||
window/AddWebauthn.js \
|
||||
window/AddYubico.js \
|
||||
window/TfaEdit.js \
|
||||
window/NotesEdit.js \
|
||||
window/ThemeEdit.js \
|
||||
window/SyncWindow.js \
|
||||
node/APT.js \
|
||||
node/APTRepositories.js \
|
||||
node/NetworkEdit.js \
|
||||
node/NetworkView.js \
|
||||
node/DNSEdit.js \
|
||||
@ -77,29 +123,38 @@ JSSRC= \
|
||||
node/TimeEdit.js \
|
||||
node/TimeView.js
|
||||
|
||||
all: ${SUBDIRS}
|
||||
set -e && for i in ${SUBDIRS}; do ${MAKE} -C $$i; done
|
||||
all: $(SUBDIRS)
|
||||
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i; done
|
||||
|
||||
.lint-incremental: ${JSSRC}
|
||||
eslint $?
|
||||
.lint-incremental: $(JSSRC)
|
||||
$(ESLINT) $?
|
||||
touch "$@"
|
||||
|
||||
.PHONY: lint
|
||||
check: lint
|
||||
lint: ${JSSRC}
|
||||
eslint --strict ${JSSRC}
|
||||
$(ESLINT) --strict api-viewer/APIViewer.js
|
||||
lint: $(JSSRC)
|
||||
$(ESLINT) --strict $(JSSRC)
|
||||
touch ".lint-incremental"
|
||||
|
||||
proxmoxlib.js: .lint-incremental ${JSSRC}
|
||||
BUILD_TIME=$(or $(SOURCE_DATE_EPOCH),$(shell date '+%s.%N'))
|
||||
BUILD_VERSION=$(or $(DEB_VERSION),$(shell git rev-parse HEAD),unknown version)
|
||||
proxmoxlib.js: .lint-incremental $(JSSRC)
|
||||
# add the version as comment in the file
|
||||
echo "// ${DEB_VERSION_UPSTREAM_REVISION}" > $@.tmp
|
||||
cat ${JSSRC} >> $@.tmp
|
||||
echo "// v$(BUILD_VERSION)-t$(BUILD_TIME)" > $@.tmp
|
||||
cat $(JSSRC) $(MARKEDJS) >> $@.tmp
|
||||
mv $@.tmp $@
|
||||
|
||||
install: proxmoxlib.js
|
||||
install -d -m 755 ${WWWBASEDIR}
|
||||
install -m 0644 proxmoxlib.js ${WWWBASEDIR}
|
||||
set -e && for i in ${SUBDIRS}; do ${MAKE} -C $$i $@; done
|
||||
proxmoxlib.min.js: proxmoxlib.js
|
||||
uglifyjs $< -c -m -o $@.tmp
|
||||
mv $@.tmp $@
|
||||
|
||||
install: proxmoxlib.js proxmoxlib.min.js
|
||||
install -d -m 755 $(WWWBASEDIR)
|
||||
install -m 0644 proxmoxlib.js proxmoxlib.min.js $(WWWBASEDIR)
|
||||
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i $@; done
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f proxmoxlib.js
|
||||
$(MAKE) -C proxmox-dark $@
|
||||
rm -f proxmoxlib.js* proxmoxlib.min.js* .lint-incremental
|
||||
|
||||
74
src/Parser.js
Normal file
@ -0,0 +1,74 @@
|
||||
// NOTE: just relays parsing to markedjs parser
|
||||
Ext.define('Proxmox.Markdown', {
|
||||
alternateClassName: 'Px.Markdown', // just trying out something, do NOT copy this line
|
||||
singleton: true,
|
||||
|
||||
// transforms HTML to a DOM tree and recursively descends and HTML-encodes every branch with a
|
||||
// "bad" node.type and drops "bad" attributes from the remaining nodes.
|
||||
// "bad" means anything which can do XSS or break the layout of the outer page
|
||||
sanitizeHTML: function(input) {
|
||||
if (!input) {
|
||||
return input;
|
||||
}
|
||||
let _isHTTPLike = value => value.match(/^\s*https?:/i); // URL's protocol ends with :
|
||||
let _sanitize;
|
||||
_sanitize = (node) => {
|
||||
if (node.nodeType === 3) return;
|
||||
if (node.nodeType !== 1 ||
|
||||
/^(script|style|form|select|option|optgroup|map|area|canvas|textarea|applet|font|iframe|audio|video|object|embed|svg)$/i.test(node.tagName)
|
||||
) {
|
||||
// could do node.remove() instead, but it's nicer UX if we keep the (encoded!) html
|
||||
node.outerHTML = Ext.String.htmlEncode(node.outerHTML);
|
||||
return;
|
||||
}
|
||||
for (let i=node.attributes.length; i--;) {
|
||||
const name = node.attributes[i].name;
|
||||
const value = node.attributes[i].value;
|
||||
const canonicalTagName = node.tagName.toLowerCase();
|
||||
// TODO: we may want to also disallow class and id attrs
|
||||
if (
|
||||
!/^(class|id|name|href|src|alt|align|valign|disabled|checked|start|type|target)$/i.test(name)
|
||||
) {
|
||||
node.attributes.removeNamedItem(name);
|
||||
} else if ((name === 'href' || name === 'src') && !_isHTTPLike(value)) {
|
||||
let safeURL = false;
|
||||
try {
|
||||
let url = new URL(value, window.location.origin);
|
||||
safeURL = _isHTTPLike(url.protocol);
|
||||
if (canonicalTagName === 'img' && url.protocol.toLowerCase() === 'data:') {
|
||||
safeURL = true;
|
||||
} else if (canonicalTagName === 'a') {
|
||||
// allow most link protocols so admins can use short-cuts to, e.g., RDP
|
||||
safeURL = url.protocol.toLowerCase() !== 'javascript:'; // eslint-disable-line no-script-url
|
||||
}
|
||||
if (safeURL) {
|
||||
node.attributes[i].value = url.href;
|
||||
} else {
|
||||
node.attributes.removeNamedItem(name);
|
||||
}
|
||||
} catch (e) {
|
||||
node.attributes.removeNamedItem(name);
|
||||
}
|
||||
} else if (name === 'target' && canonicalTagName !== 'a') {
|
||||
node.attributes.removeNamedItem(name);
|
||||
}
|
||||
}
|
||||
for (let i=node.childNodes.length; i--;) _sanitize(node.childNodes[i]);
|
||||
};
|
||||
|
||||
const doc = new DOMParser().parseFromString(`<!DOCTYPE html><html><body>${input}`, 'text/html');
|
||||
doc.normalize();
|
||||
|
||||
_sanitize(doc.body);
|
||||
|
||||
return doc.body.innerHTML;
|
||||
},
|
||||
|
||||
parse: function(markdown) {
|
||||
/*global marked*/
|
||||
let unsafeHTML = marked.parse(markdown);
|
||||
|
||||
return `<div class="pmx-md">${this.sanitizeHTML(unsafeHTML)}</div>`;
|
||||
},
|
||||
|
||||
});
|
||||
99
src/Schema.js
Normal file
@ -0,0 +1,99 @@
|
||||
Ext.define('Proxmox.Schema', { // a singleton
|
||||
singleton: true,
|
||||
|
||||
authDomains: {
|
||||
pam: {
|
||||
name: 'Linux PAM',
|
||||
ipanel: 'pmxAuthSimplePanel',
|
||||
onlineHelp: 'user-realms-pam',
|
||||
add: false,
|
||||
edit: true,
|
||||
pwchange: true,
|
||||
sync: false,
|
||||
useTypeInUrl: false,
|
||||
},
|
||||
openid: {
|
||||
name: gettext('OpenID Connect Server'),
|
||||
ipanel: 'pmxAuthOpenIDPanel',
|
||||
add: true,
|
||||
edit: true,
|
||||
tfa: false,
|
||||
pwchange: false,
|
||||
sync: false,
|
||||
iconCls: 'pmx-itype-icon-openid-logo',
|
||||
useTypeInUrl: true,
|
||||
},
|
||||
ldap: {
|
||||
name: gettext('LDAP Server'),
|
||||
ipanel: 'pmxAuthLDAPPanel',
|
||||
syncipanel: 'pmxAuthLDAPSyncPanel',
|
||||
add: true,
|
||||
edit: true,
|
||||
tfa: true,
|
||||
pwchange: false,
|
||||
sync: true,
|
||||
useTypeInUrl: true,
|
||||
},
|
||||
ad: {
|
||||
name: gettext('Active Directory Server'),
|
||||
ipanel: 'pmxAuthADPanel',
|
||||
syncipanel: 'pmxAuthADSyncPanel',
|
||||
add: true,
|
||||
edit: true,
|
||||
tfa: true,
|
||||
pwchange: false,
|
||||
sync: true,
|
||||
useTypeInUrl: true,
|
||||
},
|
||||
},
|
||||
// to add or change existing for product specific ones
|
||||
overrideAuthDomains: function(extra) {
|
||||
for (const [key, value] of Object.entries(extra)) {
|
||||
Proxmox.Schema.authDomains[key] = value;
|
||||
}
|
||||
},
|
||||
|
||||
notificationEndpointTypes: {
|
||||
sendmail: {
|
||||
name: 'Sendmail',
|
||||
ipanel: 'pmxSendmailEditPanel',
|
||||
iconCls: 'fa-envelope-o',
|
||||
defaultMailAuthor: 'Proxmox VE',
|
||||
},
|
||||
smtp: {
|
||||
name: 'SMTP',
|
||||
ipanel: 'pmxSmtpEditPanel',
|
||||
iconCls: 'fa-envelope-o',
|
||||
defaultMailAuthor: 'Proxmox VE',
|
||||
},
|
||||
gotify: {
|
||||
name: 'Gotify',
|
||||
ipanel: 'pmxGotifyEditPanel',
|
||||
iconCls: 'fa-bell-o',
|
||||
},
|
||||
webhook: {
|
||||
name: 'Webhook',
|
||||
ipanel: 'pmxWebhookEditPanel',
|
||||
iconCls: 'fa-bell-o',
|
||||
},
|
||||
},
|
||||
|
||||
// to add or change existing for product specific ones
|
||||
overrideEndpointTypes: function(extra) {
|
||||
for (const [key, value] of Object.entries(extra)) {
|
||||
Proxmox.Schema.notificationEndpointTypes[key] = value;
|
||||
}
|
||||
},
|
||||
|
||||
pxarFileTypes: {
|
||||
b: { icon: 'cube', label: gettext('Block Device') },
|
||||
c: { icon: 'tty', label: gettext('Character Device') },
|
||||
d: { icon: 'folder-o', label: gettext('Directory') },
|
||||
f: { icon: 'file-text-o', label: gettext('File') },
|
||||
h: { icon: 'file-o', label: gettext('Hardlink') },
|
||||
l: { icon: 'link', label: gettext('Softlink') },
|
||||
p: { icon: 'exchange', label: gettext('Pipe/Fifo') },
|
||||
s: { icon: 'plug', label: gettext('Socket') },
|
||||
v: { icon: 'cube', label: gettext('Virtual') },
|
||||
},
|
||||
});
|
||||
821
src/Toolkit.js
578
src/Utils.js
@ -62,37 +62,44 @@ utilities: {
|
||||
stateText: gettext('State'),
|
||||
groupText: gettext('Group'),
|
||||
|
||||
language_map: {
|
||||
ar: 'Arabic',
|
||||
ca: 'Catalan',
|
||||
zh_CN: 'Chinese (Simplified)',
|
||||
zh_TW: 'Chinese (Traditional)',
|
||||
da: 'Danish',
|
||||
nl: 'Dutch',
|
||||
en: 'English',
|
||||
eu: 'Euskera (Basque)',
|
||||
fr: 'French',
|
||||
de: 'German',
|
||||
he: 'Hebrew',
|
||||
it: 'Italian',
|
||||
ja: 'Japanese',
|
||||
kr: 'Korean',
|
||||
nb: 'Norwegian (Bokmal)',
|
||||
nn: 'Norwegian (Nynorsk)',
|
||||
fa: 'Persian (Farsi)',
|
||||
pl: 'Polish',
|
||||
pt_BR: 'Portuguese (Brazil)',
|
||||
ru: 'Russian',
|
||||
sl: 'Slovenian',
|
||||
es: 'Spanish',
|
||||
sv: 'Swedish',
|
||||
tr: 'Turkish',
|
||||
language_map: { //language map is sorted alphabetically by iso 639-1
|
||||
ar: `العربية - ${gettext("Arabic")}`,
|
||||
bg: `Български - ${gettext("Bulgarian")}`,
|
||||
ca: `Català - ${gettext("Catalan")}`,
|
||||
da: `Dansk - ${gettext("Danish")}`,
|
||||
de: `Deutsch - ${gettext("German")}`,
|
||||
en: `English - ${gettext("English")}`,
|
||||
es: `Español - ${gettext("Spanish")}`,
|
||||
eu: `Euskera (Basque) - ${gettext("Euskera (Basque)")}`,
|
||||
fa: `فارسی - ${gettext("Persian (Farsi)")}`,
|
||||
fr: `Français - ${gettext("French")}`,
|
||||
hr: `Hrvatski - ${gettext("Croatian")}`,
|
||||
he: `עברית - ${gettext("Hebrew")}`,
|
||||
it: `Italiano - ${gettext("Italian")}`,
|
||||
ja: `日本語 - ${gettext("Japanese")}`,
|
||||
ka: `ქართული - ${gettext("Georgian")}`,
|
||||
ko: `한국어 - ${gettext("Korean")}`,
|
||||
nb: `Bokmål - ${gettext("Norwegian (Bokmal)")}`,
|
||||
nl: `Nederlands - ${gettext("Dutch")}`,
|
||||
nn: `Nynorsk - ${gettext("Norwegian (Nynorsk)")}`,
|
||||
pl: `Polski - ${gettext("Polish")}`,
|
||||
pt_BR: `Português Brasileiro - ${gettext("Portuguese (Brazil)")}`,
|
||||
ru: `Русский - ${gettext("Russian")}`,
|
||||
sl: `Slovenščina - ${gettext("Slovenian")}`,
|
||||
sv: `Svenska - ${gettext("Swedish")}`,
|
||||
tr: `Türkçe - ${gettext("Turkish")}`,
|
||||
ukr: `Українська - ${gettext("Ukrainian")}`,
|
||||
zh_CN: `中文(简体)- ${gettext("Chinese (Simplified)")}`,
|
||||
zh_TW: `中文(繁體)- ${gettext("Chinese (Traditional)")}`,
|
||||
},
|
||||
|
||||
render_language: function(value) {
|
||||
if (!value) {
|
||||
if (!value || value === '__default__') {
|
||||
return Proxmox.Utils.defaultText + ' (English)';
|
||||
}
|
||||
if (value === 'kr') {
|
||||
value = 'ko'; // fix-up wrongly used Korean code. FIXME: remove with trixie releases
|
||||
}
|
||||
let text = Proxmox.Utils.language_map[value];
|
||||
if (text) {
|
||||
return text + ' (' + value + ')';
|
||||
@ -100,6 +107,8 @@ utilities: {
|
||||
return value;
|
||||
},
|
||||
|
||||
renderEnabledIcon: enabled => `<i class="fa fa-${enabled ? 'check' : 'minus'}"></i>`,
|
||||
|
||||
language_array: function() {
|
||||
let data = [['__default__', Proxmox.Utils.render_language('')]];
|
||||
Ext.Object.each(Proxmox.Utils.language_map, function(key, value) {
|
||||
@ -109,6 +118,31 @@ utilities: {
|
||||
return data;
|
||||
},
|
||||
|
||||
theme_map: {
|
||||
crisp: 'Light theme',
|
||||
"proxmox-dark": 'Proxmox Dark',
|
||||
},
|
||||
|
||||
render_theme: function(value) {
|
||||
if (!value || value === '__default__') {
|
||||
return Proxmox.Utils.defaultText + ' (auto)';
|
||||
}
|
||||
let text = Proxmox.Utils.theme_map[value];
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
theme_array: function() {
|
||||
let data = [['__default__', Proxmox.Utils.render_theme('')]];
|
||||
Ext.Object.each(Proxmox.Utils.theme_map, function(key, value) {
|
||||
data.push([key, Proxmox.Utils.render_theme(value)]);
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
bond_mode_gettext_map: {
|
||||
'802.3ad': 'LACP (802.3ad)',
|
||||
'lacp-balance-slb': 'LACP (balance-slb)',
|
||||
@ -122,8 +156,11 @@ utilities: {
|
||||
},
|
||||
|
||||
getNoSubKeyHtml: function(url) {
|
||||
// url http://www.proxmox.com/products/proxmox-ve/subscription-service-plans
|
||||
return Ext.String.format('You do not have a valid subscription for this server. Please visit <a target="_blank" href="{0}">www.proxmox.com</a> to get a list of available options.', url || 'https://www.proxmox.com');
|
||||
let html_url = Ext.String.format('<a target="_blank" href="{0}">www.proxmox.com</a>', url || 'https://www.proxmox.com');
|
||||
return Ext.String.format(
|
||||
gettext('You do not have a valid subscription for this server. Please visit {0} to get a list of available options.'),
|
||||
html_url,
|
||||
);
|
||||
},
|
||||
|
||||
format_boolean_with_default: function(value) {
|
||||
@ -155,7 +192,7 @@ utilities: {
|
||||
// somewhat like a human would tell durations, omit zero values and do not
|
||||
// give seconds precision if we talk days already
|
||||
format_duration_human: function(ut) {
|
||||
let seconds = 0, minutes = 0, hours = 0, days = 0;
|
||||
let seconds = 0, minutes = 0, hours = 0, days = 0, years = 0;
|
||||
|
||||
if (ut <= 0.1) {
|
||||
return '<0.1s';
|
||||
@ -171,7 +208,11 @@ utilities: {
|
||||
hours = remaining % 24;
|
||||
remaining = Math.trunc(remaining / 24);
|
||||
if (remaining > 0) {
|
||||
days = remaining;
|
||||
days = remaining % 365;
|
||||
remaining = Math.trunc(remaining / 365); // yea, just lets ignore leap years...
|
||||
if (remaining > 0) {
|
||||
years = remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -182,11 +223,14 @@ utilities: {
|
||||
return t > 0;
|
||||
};
|
||||
|
||||
let addMinutes = !add(years, 'y');
|
||||
let addSeconds = !add(days, 'd');
|
||||
add(hours, 'h');
|
||||
add(minutes, 'm');
|
||||
if (addSeconds) {
|
||||
add(seconds, 's');
|
||||
if (addMinutes) {
|
||||
add(minutes, 'm');
|
||||
if (addSeconds) {
|
||||
add(seconds, 's');
|
||||
}
|
||||
}
|
||||
return res.join(' ');
|
||||
},
|
||||
@ -238,6 +282,30 @@ utilities: {
|
||||
return min < width ? width : min;
|
||||
},
|
||||
|
||||
// returns username + realm
|
||||
parse_userid: function(userid) {
|
||||
if (!Ext.isString(userid)) {
|
||||
return [undefined, undefined];
|
||||
}
|
||||
|
||||
let match = userid.match(/^(.+)@([^@]+)$/);
|
||||
if (match !== null) {
|
||||
return [match[1], match[2]];
|
||||
}
|
||||
|
||||
return [undefined, undefined];
|
||||
},
|
||||
|
||||
render_username: function(userid) {
|
||||
let username = Proxmox.Utils.parse_userid(userid)[0] || "";
|
||||
return Ext.htmlEncode(username);
|
||||
},
|
||||
|
||||
render_realm: function(userid) {
|
||||
let username = Proxmox.Utils.parse_userid(userid)[1] || "";
|
||||
return Ext.htmlEncode(username);
|
||||
},
|
||||
|
||||
getStoredAuth: function() {
|
||||
let storedAuth = JSON.parse(window.localStorage.getItem('ProxmoxUser'));
|
||||
return storedAuth || {};
|
||||
@ -250,7 +318,7 @@ utilities: {
|
||||
// that way the cookie gets deleted after the browser window is closed
|
||||
if (data.ticket) {
|
||||
Proxmox.CSRFPreventionToken = data.CSRFPreventionToken;
|
||||
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true);
|
||||
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true, "lax");
|
||||
}
|
||||
|
||||
if (data.token) {
|
||||
@ -275,10 +343,21 @@ utilities: {
|
||||
if (Proxmox.LoggedOut) {
|
||||
return;
|
||||
}
|
||||
Ext.util.Cookies.clear(Proxmox.Setup.auth_cookie_name);
|
||||
// ExtJS clear is basically the same, but browser may complain if any cookie isn't "secure"
|
||||
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, "", new Date(0), null, null, true, "lax");
|
||||
window.localStorage.removeItem("ProxmoxUser");
|
||||
},
|
||||
|
||||
// The End-User gets redirected back here after login on the OpenID auth. portal, and in the
|
||||
// redirection URL the state and auth.code are passed as URL GET params, this helper parses those
|
||||
getOpenIDRedirectionAuthorization: function() {
|
||||
const auth = Ext.Object.fromQueryString(window.location.search);
|
||||
if (auth.state !== undefined && auth.code !== undefined) {
|
||||
return auth;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
// comp.setLoading() is buggy in ExtJS 4.0.7, so we
|
||||
// use el.mask() instead
|
||||
setErrorMask: function(comp, msg) {
|
||||
@ -354,16 +433,15 @@ utilities: {
|
||||
if (!result.success) {
|
||||
msg = gettext("Unknown error");
|
||||
if (result.message) {
|
||||
msg = result.message;
|
||||
msg = Ext.htmlEncode(result.message);
|
||||
if (result.status) {
|
||||
msg += ' (' + result.status + ')';
|
||||
msg += ` (${result.status})`;
|
||||
}
|
||||
}
|
||||
if (verbose && Ext.isObject(result.errors)) {
|
||||
msg += "<br>";
|
||||
Ext.Object.each(result.errors, function(prop, desc) {
|
||||
msg += "<br><b>" + Ext.htmlEncode(prop) + "</b>: " +
|
||||
Ext.htmlEncode(desc);
|
||||
Ext.Object.each(result.errors, (prop, desc) => {
|
||||
msg += `<br><b>${Ext.htmlEncode(prop)}</b>: ${Ext.htmlEncode(desc)}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -377,10 +455,20 @@ utilities: {
|
||||
waitMsg: gettext('Please wait...'),
|
||||
}, reqOpts);
|
||||
|
||||
// default to enable if user isn't handling the failure already explicitly
|
||||
let autoErrorAlert = reqOpts.autoErrorAlert ??
|
||||
(typeof reqOpts.failure !== 'function' && typeof reqOpts.callback !== 'function');
|
||||
|
||||
if (!newopts.url.match(/^\/api2/)) {
|
||||
newopts.url = '/api2/extjs' + newopts.url;
|
||||
}
|
||||
delete newopts.callback;
|
||||
let unmask = (target) => {
|
||||
if (target.waitMsgTargetCount === undefined || --target.waitMsgTargetCount <= 0) {
|
||||
target.setLoading(false);
|
||||
delete target.waitMsgTargetCount;
|
||||
}
|
||||
};
|
||||
|
||||
let createWrapper = function(successFn, callbackFn, failureFn) {
|
||||
Ext.apply(newopts, {
|
||||
@ -389,7 +477,7 @@ utilities: {
|
||||
if (Proxmox.Utils.toolkit === 'touch') {
|
||||
options.waitMsgTarget.setMasked(false);
|
||||
} else {
|
||||
options.waitMsgTarget.setLoading(false);
|
||||
unmask(options.waitMsgTarget);
|
||||
}
|
||||
}
|
||||
let result = Ext.decode(response.responseText);
|
||||
@ -398,6 +486,9 @@ utilities: {
|
||||
response.htmlStatus = Proxmox.Utils.extractRequestError(result, true);
|
||||
Ext.callback(callbackFn, options.scope, [options, false, response]);
|
||||
Ext.callback(failureFn, options.scope, [response, options]);
|
||||
if (autoErrorAlert) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Ext.callback(callbackFn, options.scope, [options, true, response]);
|
||||
@ -408,7 +499,7 @@ utilities: {
|
||||
if (Proxmox.Utils.toolkit === 'touch') {
|
||||
options.waitMsgTarget.setMasked(false);
|
||||
} else {
|
||||
options.waitMsgTarget.setLoading(false);
|
||||
unmask(options.waitMsgTarget);
|
||||
}
|
||||
}
|
||||
response.result = {};
|
||||
@ -438,9 +529,16 @@ utilities: {
|
||||
if (target) {
|
||||
if (Proxmox.Utils.toolkit === 'touch') {
|
||||
target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg });
|
||||
} else {
|
||||
// Note: ExtJS bug - this does not work when component is not rendered
|
||||
} else if (target.rendered) {
|
||||
target.waitMsgTargetCount = (target.waitMsgTargetCount ?? 0) + 1;
|
||||
target.setLoading(newopts.waitMsg);
|
||||
} else {
|
||||
target.waitMsgTargetCount = (target.waitMsgTargetCount ?? 0) + 1;
|
||||
target.on('afterlayout', function() {
|
||||
if ((target.waitMsgTargetCount ?? 0) > 0) {
|
||||
target.setLoading(newopts.waitMsg);
|
||||
}
|
||||
}, target, { single: true });
|
||||
}
|
||||
}
|
||||
Ext.Ajax.request(newopts);
|
||||
@ -450,12 +548,7 @@ utilities: {
|
||||
// Proxmox.Async.api2({
|
||||
// ...
|
||||
// }).catch(Proxmox.Utils.alertResponseFailure);
|
||||
alertResponseFailure: (response) => {
|
||||
Ext.Msg.alert(
|
||||
gettext('Error'),
|
||||
response.htmlStatus || response.result.message,
|
||||
);
|
||||
},
|
||||
alertResponseFailure: res => Ext.Msg.alert(gettext('Error'), res.htmlStatus || res.result.message),
|
||||
|
||||
checked_command: function(orig_cmd) {
|
||||
Proxmox.Utils.API2Request(
|
||||
@ -490,7 +583,7 @@ utilities: {
|
||||
},
|
||||
|
||||
assemble_field_data: function(values, data) {
|
||||
if (!Ext.isObject(data)) {
|
||||
if (!Ext.isObject(data)) {
|
||||
return;
|
||||
}
|
||||
Ext.Object.each(data, function(name, val) {
|
||||
@ -510,7 +603,7 @@ utilities: {
|
||||
});
|
||||
},
|
||||
|
||||
updateColumnWidth: function(container) {
|
||||
updateColumnWidth: function(container, thresholdWidth) {
|
||||
let mode = Ext.state.Manager.get('summarycolumns') || 'auto';
|
||||
let factor;
|
||||
if (mode !== 'auto') {
|
||||
@ -519,14 +612,15 @@ utilities: {
|
||||
factor = 1;
|
||||
}
|
||||
} else {
|
||||
factor = container.getSize().width < 1600 ? 1 : 2;
|
||||
thresholdWidth = (thresholdWidth || 1400) + 1;
|
||||
factor = Math.ceil(container.getSize().width / thresholdWidth);
|
||||
}
|
||||
|
||||
if (container.oldFactor === factor) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items = container.query('>'); // direct childs
|
||||
let items = container.query('>'); // direct children
|
||||
factor = Math.min(factor, items.length);
|
||||
container.oldFactor = factor;
|
||||
|
||||
@ -540,6 +634,9 @@ utilities: {
|
||||
container.updateLayout();
|
||||
},
|
||||
|
||||
// NOTE: depreacated, use updateColumnWidth
|
||||
updateColumns: container => Proxmox.Utils.updateColumnWidth(container),
|
||||
|
||||
dialog_title: function(subject, create, isAdd) {
|
||||
if (create) {
|
||||
if (isAdd) {
|
||||
@ -568,6 +665,37 @@ utilities: {
|
||||
Proxmox.Utils.unknownText;
|
||||
},
|
||||
|
||||
// Only add product-agnostic fields here!
|
||||
notificationFieldName: {
|
||||
'type': gettext('Notification type'),
|
||||
'hostname': gettext('Hostname'),
|
||||
},
|
||||
|
||||
formatNotificationFieldName: (value) =>
|
||||
Proxmox.Utils.notificationFieldName[value] || value,
|
||||
|
||||
// to add or change existing for product specific ones
|
||||
overrideNotificationFieldName: function(extra) {
|
||||
for (const [key, value] of Object.entries(extra)) {
|
||||
Proxmox.Utils.notificationFieldName[key] = value;
|
||||
}
|
||||
},
|
||||
|
||||
// Only add product-agnostic fields here!
|
||||
notificationFieldValue: {
|
||||
'system-mail': gettext('Forwarded mails to the local root user'),
|
||||
},
|
||||
|
||||
formatNotificationFieldValue: (value) =>
|
||||
Proxmox.Utils.notificationFieldValue[value] || value,
|
||||
|
||||
// to add or change existing for product specific ones
|
||||
overrideNotificationFieldValue: function(extra) {
|
||||
for (const [key, value] of Object.entries(extra)) {
|
||||
Proxmox.Utils.notificationFieldValue[key] = value;
|
||||
}
|
||||
},
|
||||
|
||||
// NOTE: only add general, product agnostic, ones here! Else use override helper in product repos
|
||||
task_desc_table: {
|
||||
aptupdate: ['', gettext('Update package database')],
|
||||
@ -609,21 +737,70 @@ utilities: {
|
||||
},
|
||||
|
||||
format_size: function(size, useSI) {
|
||||
let units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||
let unitsSI = [gettext('B'), gettext('KB'), gettext('MB'), gettext('GB'),
|
||||
gettext('TB'), gettext('PB'), gettext('EB'), gettext('ZB'), gettext('YB')];
|
||||
let unitsIEC = [gettext('B'), gettext('KiB'), gettext('MiB'), gettext('GiB'),
|
||||
gettext('TiB'), gettext('PiB'), gettext('EiB'), gettext('ZiB'), gettext('YiB')];
|
||||
let order = 0;
|
||||
let commaDigits = 2;
|
||||
const baseValue = useSI ? 1000 : 1024;
|
||||
while (size >= baseValue && order < units.length) {
|
||||
while (size >= baseValue && order < unitsSI.length) {
|
||||
size = size / baseValue;
|
||||
order++;
|
||||
}
|
||||
|
||||
let unit = units[order], commaDigits = 2;
|
||||
let unit = useSI ? unitsSI[order] : unitsIEC[order];
|
||||
if (order === 0) {
|
||||
commaDigits = 0;
|
||||
} else if (!useSI) {
|
||||
unit += 'i';
|
||||
}
|
||||
return `${size.toFixed(commaDigits)} ${unit}B`;
|
||||
return `${size.toFixed(commaDigits)} ${unit}`;
|
||||
},
|
||||
|
||||
SizeUnits: {
|
||||
'B': 1,
|
||||
|
||||
'KiB': 1024,
|
||||
'MiB': 1024*1024,
|
||||
'GiB': 1024*1024*1024,
|
||||
'TiB': 1024*1024*1024*1024,
|
||||
'PiB': 1024*1024*1024*1024*1024,
|
||||
|
||||
'KB': 1000,
|
||||
'MB': 1000*1000,
|
||||
'GB': 1000*1000*1000,
|
||||
'TB': 1000*1000*1000*1000,
|
||||
'PB': 1000*1000*1000*1000*1000,
|
||||
},
|
||||
|
||||
parse_size_unit: function(val) {
|
||||
//let m = val.match(/([.\d])+\s?([KMGTP]?)(i?)B?\s*$/i);
|
||||
let m = val.match(/(\d+(?:\.\d+)?)\s?([KMGTP]?)(i?)B?\s*$/i);
|
||||
let size = parseFloat(m[1]);
|
||||
let scale = m[2].toUpperCase();
|
||||
let binary = m[3].toLowerCase();
|
||||
|
||||
let unit = `${scale}${binary}B`;
|
||||
let factor = Proxmox.Utils.SizeUnits[unit];
|
||||
|
||||
return { size, factor, unit, binary }; // for convenience return all we got
|
||||
},
|
||||
|
||||
size_unit_to_bytes: function(val) {
|
||||
let { size, factor } = Proxmox.Utils.parse_size_unit(val);
|
||||
return size * factor;
|
||||
},
|
||||
|
||||
autoscale_size_unit: function(val) {
|
||||
let { size, factor, binary } = Proxmox.Utils.parse_size_unit(val);
|
||||
return Proxmox.Utils.format_size(size * factor, binary !== "i");
|
||||
},
|
||||
|
||||
size_unit_ratios: function(a, b) {
|
||||
a = typeof a !== "undefined" ? a : 0;
|
||||
b = typeof b !== "undefined" ? b : Infinity;
|
||||
let aBytes = typeof a === "number" ? a : Proxmox.Utils.size_unit_to_bytes(a);
|
||||
let bBytes = typeof b === "number" ? b : Proxmox.Utils.size_unit_to_bytes(b);
|
||||
return aBytes / (bBytes || Infinity); // avoid division by zero
|
||||
},
|
||||
|
||||
render_upid: function(value, metaData, record) {
|
||||
@ -751,7 +928,7 @@ utilities: {
|
||||
let parsed = Proxmox.Utils.parse_task_status(status);
|
||||
switch (parsed) {
|
||||
case 'unknown': return Proxmox.Utils.unknownText;
|
||||
case 'error': return Proxmox.Utils.errorText + ': ' + status;
|
||||
case 'error': return Proxmox.Utils.errorText + ': ' + Ext.htmlEncode(status);
|
||||
case 'warning': return status.replace('WARNINGS', Proxmox.Utils.warningsText);
|
||||
case 'ok': // fall-through
|
||||
default: return status;
|
||||
@ -1077,34 +1254,226 @@ utilities: {
|
||||
return acme;
|
||||
},
|
||||
|
||||
updateColumns: function(container) {
|
||||
let mode = Ext.state.Manager.get('summarycolumns') || 'auto';
|
||||
let factor;
|
||||
if (mode !== 'auto') {
|
||||
factor = parseInt(mode, 10);
|
||||
if (Number.isNaN(factor)) {
|
||||
factor = 1;
|
||||
get_health_icon: function(state, circle) {
|
||||
if (circle === undefined) {
|
||||
circle = false;
|
||||
}
|
||||
|
||||
if (state === undefined) {
|
||||
state = 'uknown';
|
||||
}
|
||||
|
||||
var icon = 'faded fa-question';
|
||||
switch (state) {
|
||||
case 'good':
|
||||
icon = 'good fa-check';
|
||||
break;
|
||||
case 'upgrade':
|
||||
icon = 'warning fa-upload';
|
||||
break;
|
||||
case 'old':
|
||||
icon = 'warning fa-refresh';
|
||||
break;
|
||||
case 'warning':
|
||||
icon = 'warning fa-exclamation';
|
||||
break;
|
||||
case 'critical':
|
||||
icon = 'critical fa-times';
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (circle) {
|
||||
icon += '-circle';
|
||||
}
|
||||
|
||||
return icon;
|
||||
},
|
||||
|
||||
formatNodeRepoStatus: function(status, product) {
|
||||
let fmt = (txt, cls) => `<i class="fa fa-fw fa-lg fa-${cls}"></i>${txt}`;
|
||||
|
||||
let getUpdates = Ext.String.format(gettext('{0} updates'), product);
|
||||
let noRepo = Ext.String.format(gettext('No {0} repository enabled!'), product);
|
||||
|
||||
if (status === 'ok') {
|
||||
return fmt(getUpdates, 'check-circle good') + ' ' +
|
||||
fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good');
|
||||
} else if (status === 'no-sub') {
|
||||
return fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good') + ' ' +
|
||||
fmt(gettext('Enterprise repository needs valid subscription'), 'exclamation-circle warning');
|
||||
} else if (status === 'non-production') {
|
||||
return fmt(getUpdates, 'check-circle good') + ' ' +
|
||||
fmt(gettext('Non production-ready repository enabled!'), 'exclamation-circle warning');
|
||||
} else if (status === 'no-repo') {
|
||||
return fmt(noRepo, 'exclamation-circle critical');
|
||||
}
|
||||
|
||||
return Proxmox.Utils.unknownText;
|
||||
},
|
||||
|
||||
render_u2f_error: function(error) {
|
||||
var ErrorNames = {
|
||||
'1': gettext('Other Error'),
|
||||
'2': gettext('Bad Request'),
|
||||
'3': gettext('Configuration Unsupported'),
|
||||
'4': gettext('Device Ineligible'),
|
||||
'5': gettext('Timeout'),
|
||||
};
|
||||
return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText;
|
||||
},
|
||||
|
||||
// Convert an ArrayBuffer to a base64url encoded string.
|
||||
// A `null` value will be preserved for convenience.
|
||||
bytes_to_base64url: function(bytes) {
|
||||
if (bytes === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return btoa(Array
|
||||
.from(new Uint8Array(bytes))
|
||||
.map(val => String.fromCharCode(val))
|
||||
.join(''),
|
||||
)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/[=]/g, '');
|
||||
},
|
||||
|
||||
// Convert an a base64url string to an ArrayBuffer.
|
||||
// A `null` value will be preserved for convenience.
|
||||
base64url_to_bytes: function(b64u) {
|
||||
if (b64u === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Uint8Array(
|
||||
atob(b64u
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/'),
|
||||
)
|
||||
.split('')
|
||||
.map(val => val.charCodeAt(0)),
|
||||
);
|
||||
},
|
||||
|
||||
// Convert utf-8 string to base64.
|
||||
// This also escapes unicode characters such as emojis.
|
||||
utf8ToBase64: function(string) {
|
||||
let bytes = new TextEncoder().encode(string);
|
||||
const escapedString = Array.from(bytes, (byte) =>
|
||||
String.fromCodePoint(byte),
|
||||
).join("");
|
||||
return btoa(escapedString);
|
||||
},
|
||||
|
||||
// Converts a base64 string into a utf8 string.
|
||||
// Decodes escaped unicode characters correctly.
|
||||
base64ToUtf8: function(b64_string) {
|
||||
let string = atob(b64_string);
|
||||
let bytes = Uint8Array.from(string, (m) => m.codePointAt(0));
|
||||
return new TextDecoder().decode(bytes);
|
||||
},
|
||||
|
||||
stringToRGB: function(string) {
|
||||
let hash = 0;
|
||||
if (!string) {
|
||||
return hash;
|
||||
}
|
||||
string += 'prox'; // give short strings more variance
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash = hash & hash; // to int
|
||||
}
|
||||
|
||||
let alpha = 0.7; // make the color a bit brighter
|
||||
let bg = 255; // assume white background
|
||||
|
||||
return [
|
||||
(hash & 255) * alpha + bg * (1 - alpha),
|
||||
((hash >> 8) & 255) * alpha + bg * (1 - alpha),
|
||||
((hash >> 16) & 255) * alpha + bg * (1 - alpha),
|
||||
];
|
||||
},
|
||||
|
||||
rgbToCss: function(rgb) {
|
||||
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
||||
},
|
||||
|
||||
rgbToHex: function(rgb) {
|
||||
let r = Math.round(rgb[0]).toString(16);
|
||||
let g = Math.round(rgb[1]).toString(16);
|
||||
let b = Math.round(rgb[2]).toString(16);
|
||||
return `${r}${g}${b}`;
|
||||
},
|
||||
|
||||
hexToRGB: function(hex) {
|
||||
if (!hex) {
|
||||
return undefined;
|
||||
}
|
||||
if (hex.length === 7) {
|
||||
hex = hex.slice(1);
|
||||
}
|
||||
let r = parseInt(hex.slice(0, 2), 16);
|
||||
let g = parseInt(hex.slice(2, 4), 16);
|
||||
let b = parseInt(hex.slice(4, 6), 16);
|
||||
return [r, g, b];
|
||||
},
|
||||
|
||||
// optimized & simplified SAPC function
|
||||
// https://github.com/Myndex/SAPC-APCA
|
||||
getTextContrastClass: function(rgb) {
|
||||
const blkThrs = 0.022;
|
||||
const blkClmp = 1.414;
|
||||
|
||||
// linearize & gamma correction
|
||||
let r = (rgb[0] / 255) ** 2.4;
|
||||
let g = (rgb[1] / 255) ** 2.4;
|
||||
let b = (rgb[2] / 255) ** 2.4;
|
||||
|
||||
// relative luminance sRGB
|
||||
let bg = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
|
||||
|
||||
// black clamp
|
||||
bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp;
|
||||
|
||||
// SAPC with white text
|
||||
let contrastLight = bg ** 0.65 - 1;
|
||||
// SAPC with black text
|
||||
let contrastDark = bg ** 0.56 - 0.046134502;
|
||||
|
||||
if (Math.abs(contrastLight) >= Math.abs(contrastDark)) {
|
||||
return 'light';
|
||||
} else {
|
||||
return 'dark';
|
||||
}
|
||||
},
|
||||
|
||||
getTagElement: function(string, color_overrides) {
|
||||
let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string);
|
||||
let style = `background-color: ${Proxmox.Utils.rgbToCss(rgb)};`;
|
||||
let cls;
|
||||
if (rgb.length > 3) {
|
||||
style += `color: ${Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]])}`;
|
||||
cls = "proxmox-tag-dark";
|
||||
} else {
|
||||
factor = container.getSize().width < 1400 ? 1 : 2;
|
||||
let txtCls = Proxmox.Utils.getTextContrastClass(rgb);
|
||||
cls = `proxmox-tag-${txtCls}`;
|
||||
}
|
||||
return `<span class="${cls}" style="${style}">${string}</span>`;
|
||||
},
|
||||
|
||||
if (container.oldFactor === factor) {
|
||||
return;
|
||||
// Setting filename here when downloading from a remote url sometimes fails in chromium browsers
|
||||
// because of a bug when using attribute download in conjunction with a self signed certificate.
|
||||
// For more info see https://bugs.chromium.org/p/chromium/issues/detail?id=993362
|
||||
downloadAsFile: function(source, fileName) {
|
||||
let hiddenElement = document.createElement('a');
|
||||
hiddenElement.href = source;
|
||||
hiddenElement.target = '_blank';
|
||||
if (fileName) {
|
||||
hiddenElement.download = fileName;
|
||||
}
|
||||
|
||||
let items = container.query('>'); // direct childs
|
||||
factor = Math.min(factor, items.length);
|
||||
container.oldFactor = factor;
|
||||
|
||||
items.forEach((item) => {
|
||||
item.columnWidth = 1 / factor;
|
||||
});
|
||||
|
||||
// we have to update the layout twice, since the first layout change
|
||||
// can trigger the scrollbar which reduces the amount of space left
|
||||
container.updateLayout();
|
||||
container.updateLayout();
|
||||
hiddenElement.click();
|
||||
},
|
||||
},
|
||||
|
||||
@ -1147,12 +1516,35 @@ utilities: {
|
||||
|
||||
let DnsName_REGEXP = "(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?)\\.)*(?:[A-Za-z0-9](?:[A-Za-z0-9\\-]*[A-Za-z0-9])?))";
|
||||
me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$");
|
||||
me.DnsName_or_Wildcard_match = new RegExp("^(?:\\*\\.)?" + DnsName_REGEXP + "$");
|
||||
|
||||
me.CpuSet_match = /^[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*$/;
|
||||
|
||||
me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(?::(\\d+))?$");
|
||||
me.HostPortBrackets_match = new RegExp("^\\[(" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](?::(\\d+))?$");
|
||||
me.IP6_dotnotation_match = new RegExp("^(" + IPV6_REGEXP + ")(?:\\.(\\d+))?$");
|
||||
me.Vlan_match = /^vlan(\d+)/;
|
||||
me.VlanInterface_match = /(\w+)\.(\d+)/;
|
||||
|
||||
|
||||
// Taken from proxmox-schema and ported to JS
|
||||
let PORT_REGEX_STR = "(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])";
|
||||
let IPRE_BRACKET_STR = "(?:" + IPV4_REGEXP + "|\\[(?:" + IPV6_REGEXP + ")\\])";
|
||||
let DNS_NAME_STR = "(?:(?:" + DnsName_REGEXP + "\\.)*" + DnsName_REGEXP + ")";
|
||||
let HTTP_URL_REGEX = "^https?://(?:(?:(?:"
|
||||
+ DNS_NAME_STR
|
||||
+ "|"
|
||||
+ IPRE_BRACKET_STR
|
||||
+ ")(?::"
|
||||
+ PORT_REGEX_STR
|
||||
+ ")?)|"
|
||||
+ IPV6_REGEXP
|
||||
+ ")(?:/[^\x00-\x1F\x7F]*)?$";
|
||||
|
||||
me.httpUrlRegex = new RegExp(HTTP_URL_REGEX);
|
||||
|
||||
// Same as SAFE_ID_REGEX in proxmox-schema
|
||||
me.safeIdRegex = /^(?:[A-Za-z0-9_][A-Za-z0-9._\\-]*)$/;
|
||||
},
|
||||
});
|
||||
|
||||
@ -1160,7 +1552,7 @@ Ext.define('Proxmox.Async', {
|
||||
singleton: true,
|
||||
|
||||
// Returns a Promise resolving to the result of an `API2Request` or rejecting to the error
|
||||
// repsonse on failure
|
||||
// response on failure
|
||||
api2: function(reqOpts) {
|
||||
return new Promise((resolve, reject) => {
|
||||
delete reqOpts.callback; // not allowed in this api
|
||||
@ -1175,3 +1567,19 @@ Ext.define('Proxmox.Async', {
|
||||
return new Promise((resolve, _reject) => setTimeout(resolve, millis));
|
||||
},
|
||||
});
|
||||
|
||||
Ext.override(Ext.data.Store, {
|
||||
// If the store's proxy is changed while it is waiting for an AJAX
|
||||
// response, `onProxyLoad` will still be called for the outdated response.
|
||||
// To avoid displaying inconsistent information, only process responses
|
||||
// belonging to the current proxy. However, do not apply this workaround
|
||||
// to the mobile UI, as Sencha Touch has an incompatible internal API.
|
||||
onProxyLoad: function(operation) {
|
||||
let me = this;
|
||||
if (Proxmox.Utils.toolkit === 'touch' || operation.getProxy() === me.getProxy()) {
|
||||
me.callParent(arguments);
|
||||
} else {
|
||||
console.log(`ignored outdated response: ${operation.getRequest().getUrl()}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
552
src/api-viewer/APIViewer.js
Normal file
@ -0,0 +1,552 @@
|
||||
/*global apiSchema*/
|
||||
|
||||
Ext.onReady(function() {
|
||||
Ext.define('pmx-param-schema', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
'name', 'type', 'typetext', 'description', 'verbose_description',
|
||||
'enum', 'minimum', 'maximum', 'minLength', 'maxLength',
|
||||
'pattern', 'title', 'requires', 'format', 'default',
|
||||
'disallow', 'extends', 'links', 'instance-types',
|
||||
{
|
||||
name: 'optional',
|
||||
type: 'boolean',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let store = Ext.define('pmx-updated-treestore', {
|
||||
extend: 'Ext.data.TreeStore',
|
||||
model: Ext.define('pmx-api-doc', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
'path', 'info', 'text',
|
||||
],
|
||||
}),
|
||||
proxy: {
|
||||
type: 'memory',
|
||||
data: apiSchema,
|
||||
},
|
||||
sorters: [{
|
||||
property: 'leaf',
|
||||
direction: 'ASC',
|
||||
}, {
|
||||
property: 'text',
|
||||
direction: 'ASC',
|
||||
}],
|
||||
filterer: 'bottomup',
|
||||
doFilter: function(node) {
|
||||
this.filterNodes(node, this.getFilters().getFilterFn(), true);
|
||||
},
|
||||
|
||||
filterNodes: function(node, filterFn, parentVisible) {
|
||||
let me = this;
|
||||
|
||||
let match = filterFn(node) && (parentVisible || (node.isRoot() && !me.getRootVisible()));
|
||||
|
||||
if (node.childNodes && node.childNodes.length) {
|
||||
let bottomUpFiltering = me.filterer === 'bottomup';
|
||||
let childMatch;
|
||||
for (const child of node.childNodes) {
|
||||
childMatch = me.filterNodes(child, filterFn, match || bottomUpFiltering) || childMatch;
|
||||
}
|
||||
if (bottomUpFiltering) {
|
||||
match = childMatch || match;
|
||||
}
|
||||
}
|
||||
|
||||
node.set("visible", match, me._silentOptions);
|
||||
return match;
|
||||
},
|
||||
|
||||
}).create();
|
||||
|
||||
let render_description = function(value, metaData, record) {
|
||||
let pdef = record.data;
|
||||
|
||||
value = pdef.verbose_description || value;
|
||||
|
||||
// TODO: try to render asciidoc correctly
|
||||
|
||||
metaData.style = 'white-space:pre-wrap;';
|
||||
|
||||
return Ext.htmlEncode(value);
|
||||
};
|
||||
|
||||
let render_type = function(value, metaData, record) {
|
||||
let pdef = record.data;
|
||||
|
||||
return pdef.enum ? 'enum' : pdef.type || 'string';
|
||||
};
|
||||
|
||||
const renderFormatString = function(obj) {
|
||||
if (!Ext.isObject(obj)) {
|
||||
return obj;
|
||||
}
|
||||
const mandatory = [];
|
||||
const optional = [];
|
||||
Object.entries(obj).forEach(function([name, param]) {
|
||||
let list = param.optional ? optional : mandatory;
|
||||
let str = param.default_key ? `[${name}=]` : `${name}=`;
|
||||
if (param.alias) {
|
||||
return;
|
||||
} else if (param.enum) {
|
||||
str += `(${param.enum?.join(' | ')})`;
|
||||
} else {
|
||||
str += `<${param.format_description || param.pattern || param.type}>`;
|
||||
}
|
||||
list.push(str);
|
||||
});
|
||||
return mandatory.join(", ") + ' ' + optional.map(each => `[,${each}]`).join(' ');
|
||||
};
|
||||
|
||||
let render_simple_format = function(pdef, type_fallback) {
|
||||
if (pdef.typetext) {
|
||||
return pdef.typetext;
|
||||
}
|
||||
if (pdef.enum) {
|
||||
return pdef.enum.join(' | ');
|
||||
}
|
||||
if (pdef.format) {
|
||||
return renderFormatString(pdef.format);
|
||||
}
|
||||
if (pdef.pattern) {
|
||||
return pdef.pattern;
|
||||
}
|
||||
if (pdef.type === 'boolean') {
|
||||
return `<true|false>`;
|
||||
}
|
||||
if (type_fallback && pdef.type) {
|
||||
return `<${pdef.type}>`;
|
||||
}
|
||||
if (pdef.minimum || pdef.maximum) {
|
||||
return `${pdef.minimum || 'N'} - ${pdef.maximum || 'N'}`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
let render_format = function(value, metaData, record) {
|
||||
let pdef = record.data;
|
||||
|
||||
metaData.style = 'white-space:normal;';
|
||||
|
||||
if (pdef.type === 'array' && pdef.items) {
|
||||
let format = render_simple_format(pdef.items, true);
|
||||
return `[${Ext.htmlEncode(format)}, ...]`;
|
||||
}
|
||||
|
||||
return Ext.htmlEncode(render_simple_format(pdef));
|
||||
};
|
||||
|
||||
let real_path = function(path) {
|
||||
if (!path.match(/^[/]/)) {
|
||||
path = `/${path}`;
|
||||
}
|
||||
return path.replace(/^.*\/_upgrade_(\/)?/, "/");
|
||||
};
|
||||
|
||||
let permission_text = function(permission) {
|
||||
let permhtml = "";
|
||||
|
||||
if (permission.user) {
|
||||
if (!permission.description) {
|
||||
if (permission.user === 'world') {
|
||||
permhtml += "Accessible without any authentication.";
|
||||
} else if (permission.user === 'all') {
|
||||
permhtml += "Accessible by all authenticated users.";
|
||||
} else {
|
||||
permhtml += `Only accessible by user "${permission.user}"`;
|
||||
}
|
||||
}
|
||||
} else if (permission.check) {
|
||||
permhtml += `<pre>Check: ${Ext.htmlEncode(JSON.stringify(permission.check))}</pre>`;
|
||||
} else if (permission.userParam) {
|
||||
permhtml += `<div>Check if user matches parameter '${permission.userParam}'`;
|
||||
} else if (permission.or) {
|
||||
permhtml += "<div>Or<div style='padding-left: 10px;'>";
|
||||
permhtml += permission.or.map(v => permission_text(v)).join('');
|
||||
permhtml += "</div></div>";
|
||||
} else if (permission.and) {
|
||||
permhtml += "<div>And<div style='padding-left: 10px;'>";
|
||||
permhtml += permission.and.map(v => permission_text(v)).join('');
|
||||
permhtml += "</div></div>";
|
||||
} else {
|
||||
permhtml += "Unknown syntax!";
|
||||
}
|
||||
|
||||
return permhtml;
|
||||
};
|
||||
|
||||
let render_docu = function(data) {
|
||||
let md = data.info;
|
||||
|
||||
let items = [];
|
||||
|
||||
Ext.Array.each(['GET', 'POST', 'PUT', 'DELETE'], function(method) {
|
||||
let info = md[method];
|
||||
if (info) {
|
||||
let endpoint = real_path(data.path);
|
||||
let usage = `<table><tr><td>HTTP: </td><td>`;
|
||||
usage += `${method} /api2/json${endpoint}</td></tr>`;
|
||||
|
||||
if (typeof cliUsageRenderer === 'function') {
|
||||
usage += cliUsageRenderer(method, endpoint); // eslint-disable-line no-undef
|
||||
}
|
||||
|
||||
let sections = [
|
||||
{
|
||||
title: 'Description',
|
||||
html: Ext.htmlEncode(info.description),
|
||||
bodyPadding: 10,
|
||||
},
|
||||
{
|
||||
title: 'Usage',
|
||||
html: usage,
|
||||
bodyPadding: 10,
|
||||
},
|
||||
];
|
||||
|
||||
if (info.parameters && info.parameters.properties) {
|
||||
let pstore = Ext.create('Ext.data.Store', {
|
||||
model: 'pmx-param-schema',
|
||||
proxy: {
|
||||
type: 'memory',
|
||||
},
|
||||
groupField: 'optional',
|
||||
sorters: [
|
||||
{
|
||||
property: 'instance-types',
|
||||
direction: 'ASC',
|
||||
},
|
||||
{
|
||||
property: 'name',
|
||||
direction: 'ASC',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let has_type_properties = false;
|
||||
|
||||
Ext.Object.each(info.parameters.properties, function(name, pdef) {
|
||||
if (pdef.oneOf) {
|
||||
pdef.oneOf.forEach((alternative) => {
|
||||
alternative.name = name;
|
||||
pstore.add(alternative);
|
||||
has_type_properties = true;
|
||||
});
|
||||
} else if (pdef['instance-types']) {
|
||||
pdef['instance-types'].forEach((type) => {
|
||||
let typePdef = Ext.apply({}, pdef);
|
||||
typePdef.name = name;
|
||||
typePdef['instance-types'] = [type];
|
||||
pstore.add(typePdef);
|
||||
has_type_properties = true;
|
||||
});
|
||||
} else {
|
||||
pdef.name = name;
|
||||
pstore.add(pdef);
|
||||
}
|
||||
});
|
||||
|
||||
pstore.sort();
|
||||
|
||||
let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
|
||||
enableGroupingMenu: false,
|
||||
groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Required</tpl>',
|
||||
});
|
||||
|
||||
sections.push({
|
||||
xtype: 'gridpanel',
|
||||
title: 'Parameters',
|
||||
features: [groupingFeature],
|
||||
store: pstore,
|
||||
viewConfig: {
|
||||
trackOver: false,
|
||||
stripeRows: true,
|
||||
enableTextSelection: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
header: 'Name',
|
||||
dataIndex: 'name',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: 'Type',
|
||||
dataIndex: 'type',
|
||||
renderer: render_type,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: 'For Types',
|
||||
dataIndex: 'instance-types',
|
||||
hidden: !has_type_properties,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: 'Default',
|
||||
dataIndex: 'default',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: 'Format',
|
||||
dataIndex: 'type',
|
||||
renderer: render_format,
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
header: 'Description',
|
||||
dataIndex: 'description',
|
||||
renderer: render_description,
|
||||
flex: 6,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (info.returns) {
|
||||
let retinf = info.returns;
|
||||
let rtype = retinf.type;
|
||||
if (!rtype && retinf.items) {rtype = 'array';}
|
||||
if (!rtype) {rtype = 'object';}
|
||||
|
||||
let rpstore = Ext.create('Ext.data.Store', {
|
||||
model: 'pmx-param-schema',
|
||||
proxy: {
|
||||
type: 'memory',
|
||||
},
|
||||
groupField: 'optional',
|
||||
sorters: [
|
||||
{
|
||||
property: 'name',
|
||||
direction: 'ASC',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let properties;
|
||||
if (rtype === 'array' && retinf.items.properties) {
|
||||
properties = retinf.items.properties;
|
||||
}
|
||||
|
||||
if (rtype === 'object' && retinf.properties) {
|
||||
properties = retinf.properties;
|
||||
}
|
||||
|
||||
Ext.Object.each(properties, function(name, pdef) {
|
||||
pdef.name = name;
|
||||
rpstore.add(pdef);
|
||||
});
|
||||
|
||||
rpstore.sort();
|
||||
|
||||
let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
|
||||
enableGroupingMenu: false,
|
||||
groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Obligatory</tpl>',
|
||||
});
|
||||
let returnhtml;
|
||||
if (retinf.items) {
|
||||
returnhtml = '<pre>items: ' + Ext.htmlEncode(JSON.stringify(retinf.items, null, 4)) + '</pre>';
|
||||
}
|
||||
|
||||
if (retinf.properties) {
|
||||
returnhtml = returnhtml || '';
|
||||
returnhtml += '<pre>properties:' + Ext.htmlEncode(JSON.stringify(retinf.properties, null, 4)) + '</pre>';
|
||||
}
|
||||
|
||||
let rawSection = Ext.create('Ext.panel.Panel', {
|
||||
bodyPadding: '0px 10px 10px 10px',
|
||||
html: returnhtml,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
sections.push({
|
||||
xtype: 'gridpanel',
|
||||
title: 'Returns: ' + rtype,
|
||||
features: [groupingFeature],
|
||||
store: rpstore,
|
||||
viewConfig: {
|
||||
trackOver: false,
|
||||
stripeRows: true,
|
||||
enableTextSelection: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
header: 'Name',
|
||||
dataIndex: 'name',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: 'Type',
|
||||
dataIndex: 'type',
|
||||
renderer: render_type,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: 'Default',
|
||||
dataIndex: 'default',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: 'Format',
|
||||
dataIndex: 'type',
|
||||
renderer: render_format,
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
header: 'Description',
|
||||
dataIndex: 'description',
|
||||
renderer: render_description,
|
||||
flex: 6,
|
||||
},
|
||||
],
|
||||
bbar: [
|
||||
{
|
||||
xtype: 'button',
|
||||
text: 'Show RAW',
|
||||
handler: function(btn) {
|
||||
rawSection.setVisible(!rawSection.isVisible());
|
||||
btn.setText(rawSection.isVisible() ? 'Hide RAW' : 'Show RAW');
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
sections.push(rawSection);
|
||||
}
|
||||
|
||||
if (!data.path.match(/\/_upgrade_/)) {
|
||||
let permhtml = '';
|
||||
|
||||
if (!info.permissions) {
|
||||
permhtml = "Root only.";
|
||||
} else {
|
||||
if (info.permissions.description) {
|
||||
permhtml += "<div style='white-space:pre-wrap;padding-bottom:10px;'>" +
|
||||
Ext.htmlEncode(info.permissions.description) + "</div>";
|
||||
}
|
||||
permhtml += permission_text(info.permissions);
|
||||
}
|
||||
|
||||
if (info.allowtoken !== undefined && !info.allowtoken) {
|
||||
permhtml += "<br />This API endpoint is not available for API tokens.";
|
||||
}
|
||||
|
||||
sections.push({
|
||||
title: 'Required permissions',
|
||||
bodyPadding: 10,
|
||||
html: permhtml,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: method,
|
||||
autoScroll: true,
|
||||
defaults: {
|
||||
border: false,
|
||||
},
|
||||
items: sections,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let ct = Ext.getCmp('docview');
|
||||
ct.setTitle("Path: " + real_path(data.path));
|
||||
ct.removeAll(true);
|
||||
ct.add(items);
|
||||
ct.setActiveTab(0);
|
||||
};
|
||||
|
||||
Ext.define('Ext.form.SearchField', {
|
||||
extend: 'Ext.form.field.Text',
|
||||
alias: 'widget.searchfield',
|
||||
|
||||
emptyText: 'Search...',
|
||||
|
||||
flex: 1,
|
||||
|
||||
inputType: 'search',
|
||||
listeners: {
|
||||
'change': function() {
|
||||
let value = this.getValue();
|
||||
if (!Ext.isEmpty(value)) {
|
||||
store.filter({
|
||||
property: 'path',
|
||||
value: value,
|
||||
anyMatch: true,
|
||||
});
|
||||
} else {
|
||||
store.clearFilter();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let treePanel = Ext.create('Ext.tree.Panel', {
|
||||
title: 'Resource Tree',
|
||||
tbar: [
|
||||
{
|
||||
xtype: 'searchfield',
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
type: 'expand',
|
||||
tooltip: 'Expand all',
|
||||
tooltipType: 'title',
|
||||
callback: tree => tree.expandAll(),
|
||||
},
|
||||
{
|
||||
type: 'collapse',
|
||||
tooltip: 'Collapse all',
|
||||
tooltipType: 'title',
|
||||
callback: tree => tree.collapseAll(),
|
||||
},
|
||||
],
|
||||
store: store,
|
||||
width: 200,
|
||||
region: 'west',
|
||||
split: true,
|
||||
margins: '5 0 5 5',
|
||||
rootVisible: false,
|
||||
listeners: {
|
||||
selectionchange: function(v, selections) {
|
||||
if (!selections[0]) {return;}
|
||||
let rec = selections[0];
|
||||
render_docu(rec.data);
|
||||
location.hash = '#' + rec.data.path;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Ext.create('Ext.container.Viewport', {
|
||||
layout: 'border',
|
||||
renderTo: Ext.getBody(),
|
||||
items: [
|
||||
treePanel,
|
||||
{
|
||||
xtype: 'tabpanel',
|
||||
title: 'Documentation',
|
||||
id: 'docview',
|
||||
region: 'center',
|
||||
margins: '5 5 5 0',
|
||||
layout: 'fit',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let deepLink = function() {
|
||||
let path = window.location.hash.substring(1).replace(/\/\s*$/, '');
|
||||
let endpoint = store.findNode('path', path);
|
||||
|
||||
if (endpoint) {
|
||||
treePanel.getSelectionModel().select(endpoint);
|
||||
treePanel.expandPath(endpoint.getPath());
|
||||
render_docu(endpoint.data);
|
||||
}
|
||||
};
|
||||
window.onhashchange = deepLink;
|
||||
|
||||
deepLink();
|
||||
});
|
||||
22
src/button/AltText.js
Normal file
@ -0,0 +1,22 @@
|
||||
Ext.define('Proxmox.button.AltText', {
|
||||
extend: 'Proxmox.button.Button',
|
||||
xtype: 'proxmoxAltTextButton',
|
||||
|
||||
defaultText: "",
|
||||
altText: "",
|
||||
|
||||
listeners: {
|
||||
// HACK: calculate the max button width on first render to avoid toolbar glitches
|
||||
render: function(button) {
|
||||
let me = this;
|
||||
|
||||
button.setText(me.altText);
|
||||
let altWidth = button.getSize().width;
|
||||
|
||||
button.setText(me.defaultText);
|
||||
let defaultWidth = button.getSize().width;
|
||||
|
||||
button.setWidth(defaultWidth > altWidth ? defaultWidth : altWidth);
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -110,6 +110,7 @@ Ext.define('Proxmox.button.StdRemoveButton', {
|
||||
|
||||
config: {
|
||||
baseurl: undefined,
|
||||
customConfirmationMessage: undefined,
|
||||
},
|
||||
|
||||
getUrl: function(rec) {
|
||||
@ -133,7 +134,14 @@ Ext.define('Proxmox.button.StdRemoveButton', {
|
||||
let me = this;
|
||||
|
||||
let name = me.getRecordName(rec);
|
||||
return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${name}'`);
|
||||
|
||||
let text;
|
||||
if (me.customConfirmationMessage) {
|
||||
text = me.customConfirmationMessage;
|
||||
} else {
|
||||
text = gettext('Are you sure you want to remove entry {0}');
|
||||
}
|
||||
return Ext.String.format(text, Ext.htmlEncode(`'${name}'`));
|
||||
},
|
||||
|
||||
handler: function(btn, event, rec) {
|
||||
@ -152,9 +160,7 @@ Ext.define('Proxmox.button.StdRemoveButton', {
|
||||
callback: function(options, success, response) {
|
||||
Ext.callback(me.callback, me.scope, [options, success, response], 0, me);
|
||||
},
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
||||
});
|
||||
},
|
||||
initComponent: function() {
|
||||
|
||||
@ -5,9 +5,9 @@ CSS=ext6-pmx.css
|
||||
all:
|
||||
|
||||
.PHONY: install
|
||||
install: ${CSS}
|
||||
install -d ${WWWCSSDIR}
|
||||
for i in ${CSS}; do install -m 0755 $$i ${WWWCSSDIR}/$$i; done
|
||||
install: $(CSS)
|
||||
install -d $(WWWCSSDIR)
|
||||
for i in $(CSS); do install -m 0644 $$i $(WWWCSSDIR)/$$i; done
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
|
||||
@ -6,9 +6,82 @@
|
||||
background-color: LightYellow;
|
||||
}
|
||||
|
||||
.proxmox-tags-full .proxmox-tag-light,
|
||||
.proxmox-tags-full .proxmox-tag-dark {
|
||||
border-radius: 3px;
|
||||
padding: 1px 6px;
|
||||
margin: 0px 1px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-light,
|
||||
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-dark,
|
||||
.x-grid-cell-inner-treecolumn .proxmox-tags-full .proxmox-tag-light,
|
||||
.x-grid-cell-inner-treecolumn .proxmox-tags-full .proxmox-tag-dark {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.x-boundlist-item > .proxmox-tag-light,
|
||||
.x-boundlist-item > .proxmox-tag-dark {
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
|
||||
.proxmox-tags-circle :not(span.proxmox-tags-full) > .proxmox-tag-light,
|
||||
.proxmox-tags-circle :not(span.proxmox-tags-full) > .proxmox-tag-dark,
|
||||
.proxmox-tags-circle > .proxmox-tag-light,
|
||||
.proxmox-tags-circle > .proxmox-tag-dark {
|
||||
margin: 0px 1px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
border-radius: 6px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
display: inline-block;
|
||||
color: transparent !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-light,
|
||||
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-dark,
|
||||
.proxmox-tags-none > .proxmox-tag-light,
|
||||
.proxmox-tags-none > .proxmox-tag-dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-light,
|
||||
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-dark,
|
||||
.proxmox-tags-dense > .proxmox-tag-light,
|
||||
.proxmox-tags-dense > .proxmox-tag-dark {
|
||||
width: 6px;
|
||||
margin-right: 1px;
|
||||
display: inline-block;
|
||||
color: transparent !important;
|
||||
overflow: hidden;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.proxmox-tags-full .proxmox-tag-light {
|
||||
color: #fff;
|
||||
background-color: #383838;
|
||||
}
|
||||
|
||||
.proxmox-tags-full .proxmox-tag-dark {
|
||||
color: #000;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.x-mask-msg-text {
|
||||
text-align: center;
|
||||
}
|
||||
.x-window-header-default-top .x-mask {
|
||||
background-color: #f5f5f5B4; /* ~ 0.7 opacity */
|
||||
}
|
||||
|
||||
.proxmox-disabled-row, .proxmox-disabled-row td {
|
||||
/*color: #a0a0a0;*/
|
||||
color: #666665;
|
||||
}
|
||||
|
||||
.proxmox-invalid-row {
|
||||
background-color: #f3d6d7;
|
||||
@ -18,6 +91,10 @@
|
||||
background-color: #f5e5d8;
|
||||
}
|
||||
|
||||
.proxmox-good-row {
|
||||
background-color: #21BF4B;
|
||||
}
|
||||
|
||||
/* some icons have to be color manually */
|
||||
.black {
|
||||
color: #000;
|
||||
@ -43,6 +120,20 @@
|
||||
color: #FF6C59;
|
||||
}
|
||||
|
||||
.info-blue {
|
||||
color: #3892d4;
|
||||
}
|
||||
|
||||
.eol-notice a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
.eol-notice > i.fa {
|
||||
position: relative;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
/* reduce chart legend space usage to something more sane */
|
||||
.x-legend-item {
|
||||
padding: 0.4em 0.8em 0.4em 1.8em;
|
||||
@ -96,19 +187,177 @@ div.right-aligned {
|
||||
}
|
||||
|
||||
.pmx-itype-icon-memory,
|
||||
.pmx-itype-icon-processor
|
||||
.pmx-itype-icon-processor,
|
||||
.pmx-itype-icon /* NOTE: use this one instead of adding new specific ones! */
|
||||
{
|
||||
background-repeat: no-repeat;
|
||||
background-position:3px center;
|
||||
padding-left: 20px;
|
||||
background-size: 16px 16px; /* Chrom* needs both as else it gets cut-off due do non 1:1 ratio */
|
||||
}
|
||||
|
||||
.pmx-itype-icon-memory
|
||||
{
|
||||
background-image:url(../images/icon-ram.png);
|
||||
.pmx-itype-icon-memory {
|
||||
background-image:url(../images/icon-ram.svg);
|
||||
}
|
||||
|
||||
.pmx-itype-icon-processor
|
||||
{
|
||||
background-image:url(../images/icon-cpu.png);
|
||||
.pmx-itype-icon-processor {
|
||||
background-image:url(../images/icon-cpu.svg);
|
||||
}
|
||||
|
||||
.pmx-itype-icon-debian-swirl {
|
||||
padding-left: 22px;
|
||||
background-size: 16px 16px; /* Chrom* needs both as else it gets cut-off due do non 1:1 ratio */
|
||||
background-image:url(../images/debian-swirl-openlogo.svg);
|
||||
}
|
||||
.pmx-itype-icon-proxmox-x {
|
||||
padding-left: 22px;
|
||||
background-size: 16px 16px; /* Not really required here, as here WxH is 1:1 but cannot hurt */
|
||||
background-image:url(../images/proxmox-symbol-x.svg);
|
||||
}
|
||||
|
||||
.pmx-itype-icon-openid-logo {
|
||||
padding-left: 22px;
|
||||
background-size: 16px 16px;
|
||||
background-image:url(../images/openid-icon-100x100.png);
|
||||
}
|
||||
|
||||
/* change font for config panel back to fontawesome */
|
||||
.x-treelist-item-expanded > * > * > .x-treelist-item-expander::after,
|
||||
.x-treelist-item-expander::after {
|
||||
font: 16px/1 FontAwesome;
|
||||
}
|
||||
|
||||
.x-treelist-pve-nav {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* fix padding for legend in header */
|
||||
.x-legend-inner {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.proxmox-apt-repos .x-grid-group-hd {
|
||||
color: #000000;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.proxmox-apt-repos .x-grid-group-title {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* some general helper classes */
|
||||
.centered-flex-column {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Fix icon/text baseline */
|
||||
.x-tab-default {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.x-tab-default > span {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.x-tab-button {
|
||||
line-height: unset;
|
||||
}
|
||||
.x-tab-inner {
|
||||
display: unset;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
.x-tab-wrap {
|
||||
display: unset;
|
||||
}
|
||||
.x-tab-default-top {
|
||||
padding: 2px 6px 2px 6px;
|
||||
}
|
||||
|
||||
/* rules for the markdown content, prefix with the .pmx-md class */
|
||||
.pmx-md {
|
||||
font-size: 1.0em;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
.pmx-md p {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.pmx-md :is(h1, h2, h3, h4, h5, h6) {
|
||||
margin-top: 0.9em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.pmx-md h1 { font-size: 175%; }
|
||||
.pmx-md h2 { font-size: 150%; }
|
||||
.pmx-md h3 { font-size: 125%; }
|
||||
.pmx-md h4 { font-size: 110%; }
|
||||
.pmx-md h5 { font-size: 100%; }
|
||||
.pmx-md h6 { font-size: 100%; }
|
||||
|
||||
.pmx-md code {
|
||||
white-space: pre;
|
||||
background-color: #f5f5f5;
|
||||
padding: 1px;
|
||||
}
|
||||
.pmx-md pre code {
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
border-left: 3px solid #e0e0e0;
|
||||
}
|
||||
.pmx-md strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.pmx-md blockquote {
|
||||
border-left: 1px solid #666666;
|
||||
padding-left: 4px;
|
||||
margin: 10px 2ch;
|
||||
}
|
||||
/* markdown tables */
|
||||
.pmx-md table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.pmx-md td, .pmx-md th {
|
||||
padding: 5px;
|
||||
}
|
||||
.pmx-md td[align="center"] {
|
||||
text-align: center;
|
||||
}
|
||||
.pmx-md td[align="right"] {
|
||||
text-align: right;
|
||||
}
|
||||
.pmx-md tbody td {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
.pmx-md tbody tr:nth-of-type(even) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.pmx-md tbody tr:last-of-type td {
|
||||
border-bottom: 1px solid #666666;
|
||||
}
|
||||
.pmx-md tbody tr:hover td {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
/* markdown tables end */
|
||||
|
||||
/* markdown content end */
|
||||
|
||||
/* action column fix start */
|
||||
.x-action-col-icon {
|
||||
margin: 0 1px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.x-action-col-icon:before, .x-action-col-icon:after {
|
||||
font-size: 14px;
|
||||
}
|
||||
.x-action-col-icon:hover:before, .x-action-col-icon:hover:after {
|
||||
text-shadow: 1px 1px 1px #AAA;
|
||||
font-weight: 800;
|
||||
}
|
||||
.x-action-col-icon:before {
|
||||
color: #555;
|
||||
}
|
||||
/* action column fix end */
|
||||
|
||||
@ -28,7 +28,7 @@ Ext.define('Proxmox.data.DiffStore', {
|
||||
// config is passed instead of an existing rstore instance
|
||||
autoDestroyRstore: false,
|
||||
|
||||
onDestroy: function() {
|
||||
doDestroy: function() {
|
||||
let me = this;
|
||||
if (me.autoDestroyRstore) {
|
||||
if (Ext.isFunction(me.rstore.destroy)) {
|
||||
|
||||
@ -17,6 +17,7 @@ Ext.define('Proxmox.RestProxy', {
|
||||
constructor: function(config) {
|
||||
Ext.applyIf(config, {
|
||||
reader: {
|
||||
responseType: undefined,
|
||||
type: 'json',
|
||||
rootProperty: config.root || 'data',
|
||||
},
|
||||
|
||||
29
src/data/model/NotificationConfig.js
Normal file
@ -0,0 +1,29 @@
|
||||
Ext.define('proxmox-notification-endpoints', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: ['name', 'type', 'comment', 'disable', 'origin'],
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
},
|
||||
idProperty: 'name',
|
||||
});
|
||||
|
||||
Ext.define('proxmox-notification-matchers', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: ['name', 'comment', 'disable', 'origin'],
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
},
|
||||
idProperty: 'name',
|
||||
});
|
||||
|
||||
Ext.define('proxmox-notification-fields', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: ['name', 'description'],
|
||||
idProperty: 'name',
|
||||
});
|
||||
|
||||
Ext.define('proxmox-notification-field-values', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: ['value', 'comment', 'field'],
|
||||
idProperty: 'value',
|
||||
});
|
||||
@ -9,7 +9,7 @@
|
||||
* example2: [ {data1: "xyz", data2: "abc"} ]
|
||||
* returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}]
|
||||
*
|
||||
* If you set 'readArray', the reader expexts the object as array:
|
||||
* If you set 'readArray', the reader expects the object as array:
|
||||
*
|
||||
* example3: [ { key: "data1", value: "xyz", p2: "cde" }, { key: "data2", value: "abc", p2: "efg" }]
|
||||
* returns [{key: "data1", value: "xyz", p2: "cde}, {key: "data2", value: "abc", p2: "efg"}]
|
||||
@ -32,6 +32,7 @@ Ext.define('Proxmox.data.reader.JsonObject', {
|
||||
alias: 'reader.jsonobject',
|
||||
|
||||
readArray: false,
|
||||
responseType: undefined,
|
||||
|
||||
rows: undefined,
|
||||
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
PACKAGE ?= $(or $(DEB_SOURCE), proxmox-widget-toolkit)
|
||||
|
||||
DESTDIR=
|
||||
DOCDIR=${DESTDIR}/usr/share/doc/${PACKAGE}
|
||||
WWWBASEDIR=${DESTDIR}/usr/share/javascript/${PACKAGE}
|
||||
WWWCSSDIR=${WWWBASEDIR}/css
|
||||
WWWIMAGESDIR=${WWWBASEDIR}/images
|
||||
DOCDIR=$(DESTDIR)/usr/share/doc/$(PACKAGE)
|
||||
WWWBASEDIR=$(DESTDIR)/usr/share/javascript/$(PACKAGE)
|
||||
WWWCSSDIR=$(WWWBASEDIR)/css
|
||||
WWWIMAGESDIR=$(WWWBASEDIR)/images
|
||||
WWWTHEMEDIR=$(WWWBASEDIR)/themes
|
||||
|
||||
151
src/form/BandwidthSelector.js
Normal file
@ -0,0 +1,151 @@
|
||||
Ext.define('Proxmox.form.SizeField', {
|
||||
extend: 'Ext.form.FieldContainer',
|
||||
alias: 'widget.pmxSizeField',
|
||||
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
viewModel: {
|
||||
data: {
|
||||
unit: 'MiB',
|
||||
unitPostfix: '',
|
||||
},
|
||||
formulas: {
|
||||
unitlabel: (get) => get('unit') + get('unitPostfix'),
|
||||
},
|
||||
},
|
||||
|
||||
emptyText: '',
|
||||
|
||||
layout: 'hbox',
|
||||
defaults: {
|
||||
hideLabel: true,
|
||||
},
|
||||
|
||||
// display unit (TODO: make (optionally) selectable)
|
||||
unit: 'MiB',
|
||||
unitPostfix: '',
|
||||
|
||||
// use this if the backend saves values in another unit than bytes, e.g.,
|
||||
// for KiB set it to 'KiB'
|
||||
backendUnit: undefined,
|
||||
|
||||
// submit a canonical size unit, e.g., 20.5 MiB
|
||||
submitAutoScaledSizeUnit: false,
|
||||
|
||||
// allow setting 0 and using it as a submit value
|
||||
allowZero: false,
|
||||
|
||||
emptyValue: null,
|
||||
|
||||
items: [
|
||||
{
|
||||
xtype: 'numberfield',
|
||||
cbind: {
|
||||
name: '{name}',
|
||||
emptyText: '{emptyText}',
|
||||
allowZero: '{allowZero}',
|
||||
emptyValue: '{emptyValue}',
|
||||
},
|
||||
minValue: 0,
|
||||
step: 1,
|
||||
submitLocaleSeparator: false,
|
||||
fieldStyle: 'text-align: right',
|
||||
flex: 1,
|
||||
enableKeyEvents: true,
|
||||
setValue: function(v) {
|
||||
if (!this._transformed) {
|
||||
let fieldContainer = this.up('fieldcontainer');
|
||||
let vm = fieldContainer.getViewModel();
|
||||
let unit = vm.get('unit');
|
||||
|
||||
if (typeof v === "string") {
|
||||
v = Proxmox.Utils.size_unit_to_bytes(v);
|
||||
}
|
||||
v /= Proxmox.Utils.SizeUnits[unit];
|
||||
v *= fieldContainer.backendFactor;
|
||||
|
||||
this._transformed = true;
|
||||
}
|
||||
|
||||
if (Number(v) === 0 && !this.allowZero) {
|
||||
v = undefined;
|
||||
}
|
||||
|
||||
return Ext.form.field.Text.prototype.setValue.call(this, v);
|
||||
},
|
||||
getSubmitValue: function() {
|
||||
let v = this.processRawValue(this.getRawValue());
|
||||
v = v.replace(this.decimalSeparator, '.');
|
||||
|
||||
if (v === undefined || v === '') {
|
||||
return this.emptyValue;
|
||||
}
|
||||
|
||||
if (Number(v) === 0) {
|
||||
return this.allowZero ? 0 : null;
|
||||
}
|
||||
|
||||
let fieldContainer = this.up('fieldcontainer');
|
||||
let vm = fieldContainer.getViewModel();
|
||||
let unit = vm.get('unit');
|
||||
|
||||
v = parseFloat(v) * Proxmox.Utils.SizeUnits[unit];
|
||||
|
||||
if (fieldContainer.submitAutoScaledSizeUnit) {
|
||||
return Proxmox.Utils.format_size(v, !unit.endsWith('iB'));
|
||||
} else {
|
||||
return String(Math.floor(v / fieldContainer.backendFactor));
|
||||
}
|
||||
},
|
||||
listeners: {
|
||||
// our setValue gets only called if we have a value, avoid
|
||||
// transformation of the first user-entered value
|
||||
keydown: function() { this._transformed = true; },
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'displayfield',
|
||||
name: 'unit',
|
||||
submitValue: false,
|
||||
padding: '0 0 0 10',
|
||||
bind: {
|
||||
value: '{unitlabel}',
|
||||
},
|
||||
listeners: {
|
||||
change: (f, v) => {
|
||||
f.originalValue = v;
|
||||
},
|
||||
},
|
||||
width: 40,
|
||||
},
|
||||
],
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
me.unit = me.unit || 'MiB';
|
||||
if (!(me.unit in Proxmox.Utils.SizeUnits)) {
|
||||
throw "unknown unit: " + me.unit;
|
||||
}
|
||||
|
||||
me.backendFactor = 1;
|
||||
if (me.backendUnit !== undefined) {
|
||||
if (!(me.unit in Proxmox.Utils.SizeUnits)) {
|
||||
throw "unknown backend unit: " + me.backendUnit;
|
||||
}
|
||||
me.backendFactor = Proxmox.Utils.SizeUnits[me.backendUnit];
|
||||
}
|
||||
|
||||
me.callParent(arguments);
|
||||
|
||||
me.getViewModel().set('unit', me.unit);
|
||||
me.getViewModel().set('unitPostfix', me.unitPostfix);
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('Proxmox.form.BandwidthField', {
|
||||
extend: 'Proxmox.form.SizeField',
|
||||
alias: 'widget.pmxBandwidthField',
|
||||
|
||||
unitPostfix: '/s',
|
||||
});
|
||||
@ -6,6 +6,7 @@ Ext.define('Proxmox.form.Checkbox', {
|
||||
defaultValue: undefined,
|
||||
deleteDefaultValue: false,
|
||||
deleteEmpty: false,
|
||||
clearOnDisable: false,
|
||||
},
|
||||
|
||||
inputValue: '1',
|
||||
@ -31,6 +32,19 @@ Ext.define('Proxmox.form.Checkbox', {
|
||||
return data;
|
||||
},
|
||||
|
||||
setDisabled: function(disabled) {
|
||||
let me = this;
|
||||
|
||||
// only clear on actual transition
|
||||
let toClearValue = me.clearOnDisable && !me.disabled && disabled;
|
||||
|
||||
me.callParent(arguments);
|
||||
|
||||
if (toClearValue) {
|
||||
me.setValue(false); // TODO: could support other "reset value" or use originalValue?
|
||||
}
|
||||
},
|
||||
|
||||
// also accept integer 1 as true
|
||||
setRawValue: function(value) {
|
||||
let me = this;
|
||||
|
||||
@ -31,6 +31,10 @@ Ext.define('Proxmox.form.ComboGrid', {
|
||||
skipEmptyText: false,
|
||||
notFoundIsValid: false,
|
||||
deleteEmpty: false,
|
||||
errorHeight: 100,
|
||||
// NOTE: the trigger will always be shown if allowBlank is true, setting showClearTrigger
|
||||
// to false cannot change that
|
||||
showClearTrigger: false,
|
||||
},
|
||||
|
||||
// needed to trigger onKeyUp etc.
|
||||
@ -53,7 +57,7 @@ Ext.define('Proxmox.form.ComboGrid', {
|
||||
setValue: function(value) {
|
||||
let me = this;
|
||||
let empty = Ext.isArray(value) ? !value.length : !value;
|
||||
me.triggers.clear.setVisible(!empty && me.allowBlank);
|
||||
me.triggers.clear.setVisible(!empty && (me.allowBlank || me.showClearTrigger));
|
||||
return me.callParent([value]);
|
||||
},
|
||||
|
||||
@ -289,7 +293,7 @@ Ext.define('Proxmox.form.ComboGrid', {
|
||||
if (!me.multiSelect) {
|
||||
picker.on('itemclick', function(sm, record) {
|
||||
if (picker.getSelection()[0] === record) {
|
||||
picker.hide();
|
||||
me.collapse();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -300,12 +304,13 @@ Ext.define('Proxmox.form.ComboGrid', {
|
||||
//
|
||||
// we save the minheight to reset it after the load
|
||||
picker.on('show', function() {
|
||||
me.store.fireEvent('refresh');
|
||||
if (me.enableLoadMask) {
|
||||
me.savedMinHeight = picker.getMinHeight();
|
||||
picker.setMinHeight(100);
|
||||
me.savedMinHeight = me.savedMinHeight ?? picker.getMinHeight();
|
||||
picker.setMinHeight(me.errorHeight);
|
||||
}
|
||||
if (me.loadError) {
|
||||
Proxmox.Utils.setErrorMask(picker, me.loadError);
|
||||
Proxmox.Utils.setErrorMask(picker.getView(), me.loadError);
|
||||
delete me.loadError;
|
||||
picker.updateLayout();
|
||||
}
|
||||
@ -317,15 +322,14 @@ Ext.define('Proxmox.form.ComboGrid', {
|
||||
},
|
||||
|
||||
clearLocalFilter: function() {
|
||||
let me = this,
|
||||
filter = me.queryFilter;
|
||||
let me = this;
|
||||
|
||||
if (filter) {
|
||||
me.queryFilter = null;
|
||||
me.changingFilters = true;
|
||||
me.store.removeFilter(filter, true);
|
||||
me.changingFilters = false;
|
||||
}
|
||||
if (me.queryFilter) {
|
||||
me.changingFilters = true; // FIXME: unused?
|
||||
me.store.removeFilter(me.queryFilter, true);
|
||||
me.queryFilter = null;
|
||||
me.changingFilters = false;
|
||||
}
|
||||
},
|
||||
|
||||
isValueInStore: function(value) {
|
||||
@ -399,7 +403,7 @@ Ext.define('Proxmox.form.ComboGrid', {
|
||||
matchFieldWidth: false,
|
||||
});
|
||||
|
||||
Ext.applyIf(me, { value: '' }); // hack: avoid ExtJS validate() bug
|
||||
Ext.applyIf(me, { value: [] }); // hack: avoid ExtJS validate() bug
|
||||
|
||||
Ext.applyIf(me.listConfig, { width: 400 });
|
||||
|
||||
@ -407,7 +411,7 @@ Ext.define('Proxmox.form.ComboGrid', {
|
||||
|
||||
// Create the picker at an early stage, so it is available to store the previous selection
|
||||
if (!me.picker) {
|
||||
me.createPicker();
|
||||
me.getPicker();
|
||||
}
|
||||
|
||||
me.mon(me.store, 'beforeload', function() {
|
||||
@ -425,14 +429,12 @@ Ext.define('Proxmox.form.ComboGrid', {
|
||||
if (me.enableLoadMask) {
|
||||
delete me.enableLoadMask;
|
||||
|
||||
// if the picker exists,
|
||||
// we reset its minheight to the saved let/0
|
||||
// we have to update the layout, otherwise the height
|
||||
// gets not recalculated
|
||||
// if the picker exists, we reset its minHeight to the previous saved one or 0
|
||||
if (me.picker) {
|
||||
me.picker.setMinHeight(me.savedMinHeight || 0);
|
||||
Proxmox.Utils.setErrorMask(me.picker);
|
||||
Proxmox.Utils.setErrorMask(me.picker.getView());
|
||||
delete me.savedMinHeight;
|
||||
// we have to update the layout, otherwise the height gets not recalculated
|
||||
me.picker.updateLayout();
|
||||
}
|
||||
}
|
||||
@ -447,21 +449,28 @@ Ext.define('Proxmox.form.ComboGrid', {
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
let rec = me.store.first();
|
||||
if (me.autoSelect && rec && rec.data) {
|
||||
def = rec.data[me.valueField];
|
||||
me.setValue(def, true);
|
||||
} else if (!me.allowBlank && !(Ext.isArray(def) ? def.length : def)) {
|
||||
me.setValue(def);
|
||||
if (!me.notFoundIsValid && !me.isDisabled()) {
|
||||
me.markInvalid(me.blankText);
|
||||
if (!(Ext.isArray(def) ? def.length : def)) {
|
||||
let rec = me.store.first();
|
||||
if (me.autoSelect && rec && rec.data) {
|
||||
def = rec.data[me.valueField];
|
||||
me.setValue(def, true);
|
||||
} else if (!me.allowBlank) {
|
||||
me.setValue(def);
|
||||
if (!me.isDisabled()) {
|
||||
me.markInvalid(me.blankText);
|
||||
}
|
||||
}
|
||||
} else if (!me.notFoundIsValid && !me.isDisabled()) {
|
||||
me.markInvalid(gettext('Invalid Value'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let msg = Proxmox.Utils.getResponseErrorMessage(o.getError());
|
||||
if (me.picker) {
|
||||
Proxmox.Utils.setErrorMask(me.picker, msg);
|
||||
me.savedMinHeight = me.savedMinHeight ?? me.picker.getMinHeight();
|
||||
me.picker.setMinHeight(me.errorHeight);
|
||||
Proxmox.Utils.setErrorMask(me.picker.getView(), msg);
|
||||
me.picker.updateLayout();
|
||||
}
|
||||
me.loadError = msg;
|
||||
}
|
||||
|
||||
@ -1,151 +1,169 @@
|
||||
Ext.define('Proxmox.DateTimeField', {
|
||||
extend: 'Ext.form.FieldContainer',
|
||||
xtype: 'promxoxDateTimeField',
|
||||
// FIXME: remove once all use sites upgraded (with versioned depends on new WTK!)
|
||||
alias: ['widget.promxoxDateTimeField'],
|
||||
xtype: 'proxmoxDateTimeField',
|
||||
|
||||
layout: 'hbox',
|
||||
|
||||
referenceHolder: true,
|
||||
viewModel: {
|
||||
data: {
|
||||
datetime: null,
|
||||
minDatetime: null,
|
||||
maxDatetime: null,
|
||||
},
|
||||
|
||||
submitFormat: 'U',
|
||||
formulas: {
|
||||
date: {
|
||||
get: function(get) {
|
||||
return get('datetime');
|
||||
},
|
||||
set: function(date) {
|
||||
if (!date) {
|
||||
this.set('datetime', null);
|
||||
return;
|
||||
}
|
||||
let datetime = new Date(this.get('datetime'));
|
||||
datetime.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
this.set('datetime', datetime);
|
||||
},
|
||||
},
|
||||
|
||||
time: {
|
||||
get: function(get) {
|
||||
return get('datetime');
|
||||
},
|
||||
set: function(time) {
|
||||
if (!time) {
|
||||
this.set('datetime', null);
|
||||
return;
|
||||
}
|
||||
let datetime = new Date(this.get('datetime'));
|
||||
datetime.setHours(time.getHours());
|
||||
datetime.setMinutes(time.getMinutes());
|
||||
datetime.setSeconds(time.getSeconds());
|
||||
datetime.setMilliseconds(time.getMilliseconds());
|
||||
this.set('datetime', datetime);
|
||||
},
|
||||
},
|
||||
|
||||
minDate: {
|
||||
get: function(get) {
|
||||
let datetime = get('minDatetime');
|
||||
return datetime ? new Date(datetime) : null;
|
||||
},
|
||||
},
|
||||
|
||||
maxDate: {
|
||||
get: function(get) {
|
||||
let datetime = get('maxDatetime');
|
||||
return datetime ? new Date(datetime) : null;
|
||||
},
|
||||
},
|
||||
|
||||
minTime: {
|
||||
get: function(get) {
|
||||
let current = get('datetime');
|
||||
let min = get('minDatetime');
|
||||
if (min && current && !this.isSameDay(current, min)) {
|
||||
return new Date(min).setHours('00', '00', '00', '000');
|
||||
}
|
||||
return min;
|
||||
},
|
||||
},
|
||||
|
||||
maxTime: {
|
||||
get: function(get) {
|
||||
let current = get('datetime');
|
||||
let max = get('maxDatetime');
|
||||
if (max && current && !this.isSameDay(current, max)) {
|
||||
return new Date(max).setHours('23', '59', '59', '999');
|
||||
}
|
||||
return max;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Helper function to check if dates are the same day of the year
|
||||
isSameDay: function(date1, date2) {
|
||||
return date1.getDate() === date2.getDate() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getFullYear() === date2.getFullYear();
|
||||
},
|
||||
},
|
||||
|
||||
config: {
|
||||
value: null,
|
||||
submitFormat: 'U',
|
||||
disabled: false,
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.getViewModel().set('datetime', value);
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
let me = this;
|
||||
let d = me.lookupReference('dateentry').getValue();
|
||||
|
||||
if (d === undefined || d === null) { return null; }
|
||||
|
||||
let t = me.lookupReference('timeentry').getValue();
|
||||
|
||||
if (t === undefined || t === null) { return null; }
|
||||
|
||||
let offset = (t.getHours() * 3600 + t.getMinutes() * 60) * 1000;
|
||||
|
||||
return new Date(d.getTime() + offset);
|
||||
return this.getViewModel().get('datetime');
|
||||
},
|
||||
|
||||
getSubmitValue: function() {
|
||||
let me = this;
|
||||
let format = me.submitFormat;
|
||||
let value = me.getValue();
|
||||
let me = this;
|
||||
let value = me.getValue();
|
||||
return value ? Ext.Date.format(value, me.submitFormat) : null;
|
||||
},
|
||||
|
||||
return value ? Ext.Date.format(value, format) : null;
|
||||
setMinValue: function(value) {
|
||||
this.getViewModel().set('minDatetime', value);
|
||||
},
|
||||
|
||||
getMinValue: function() {
|
||||
return this.getViewModel().get('minDatetime');
|
||||
},
|
||||
|
||||
setMaxValue: function(value) {
|
||||
this.getViewModel().set('maxDatetime', value);
|
||||
},
|
||||
|
||||
getMaxValue: function() {
|
||||
return this.getViewModel().get('maxDatetime');
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
me.callParent();
|
||||
|
||||
let vm = me.getViewModel();
|
||||
vm.set('datetime', me.config.value);
|
||||
// Propagate state change to binding
|
||||
vm.bind('{datetime}', function(value) {
|
||||
me.publishState('value', value);
|
||||
me.fireEvent('change', value);
|
||||
});
|
||||
},
|
||||
|
||||
items: [
|
||||
{
|
||||
xtype: 'datefield',
|
||||
editable: false,
|
||||
reference: 'dateentry',
|
||||
flex: 1,
|
||||
format: 'Y-m-d',
|
||||
bind: {
|
||||
value: '{date}',
|
||||
minValue: '{minDate}',
|
||||
maxValue: '{maxDate}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'timefield',
|
||||
reference: 'timeentry',
|
||||
format: 'H:i',
|
||||
width: 80,
|
||||
value: '00:00',
|
||||
increment: 60,
|
||||
bind: {
|
||||
value: '{time}',
|
||||
minValue: '{minTime}',
|
||||
maxValue: '{maxTime}',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
setMinValue: function(value) {
|
||||
let me = this;
|
||||
let current = me.getValue();
|
||||
if (!value || !current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let minhours = value.getHours();
|
||||
let minminutes = value.getMinutes();
|
||||
|
||||
let hours = current.getHours();
|
||||
let minutes = current.getMinutes();
|
||||
|
||||
value.setHours(0);
|
||||
value.setMinutes(0);
|
||||
value.setSeconds(0);
|
||||
current.setHours(0);
|
||||
current.setMinutes(0);
|
||||
current.setSeconds(0);
|
||||
|
||||
let time = new Date();
|
||||
if (current-value > 0) {
|
||||
time.setHours(0);
|
||||
time.setMinutes(0);
|
||||
time.setSeconds(0);
|
||||
time.setMilliseconds(0);
|
||||
} else {
|
||||
time.setHours(minhours);
|
||||
time.setMinutes(minminutes);
|
||||
}
|
||||
me.lookup('timeentry').setMinValue(time);
|
||||
|
||||
// current time is smaller than the time part of the new minimum
|
||||
// so we have to add 1 to the day
|
||||
if (minhours*60+minminutes > hours*60+minutes) {
|
||||
value.setDate(value.getDate()+1);
|
||||
}
|
||||
me.lookup('dateentry').setMinValue(value);
|
||||
},
|
||||
|
||||
setMaxValue: function(value) {
|
||||
let me = this;
|
||||
let current = me.getValue();
|
||||
if (!value || !current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let maxhours = value.getHours();
|
||||
let maxminutes = value.getMinutes();
|
||||
|
||||
let hours = current.getHours();
|
||||
let minutes = current.getMinutes();
|
||||
|
||||
value.setHours(0);
|
||||
value.setMinutes(0);
|
||||
current.setHours(0);
|
||||
current.setMinutes(0);
|
||||
|
||||
let time = new Date();
|
||||
if (value-current > 0) {
|
||||
time.setHours(23);
|
||||
time.setMinutes(59);
|
||||
time.setSeconds(59);
|
||||
} else {
|
||||
time.setHours(maxhours);
|
||||
time.setMinutes(maxminutes);
|
||||
}
|
||||
me.lookup('timeentry').setMaxValue(time);
|
||||
|
||||
// current time is biger than the time part of the new maximum
|
||||
// so we have to subtract 1 to the day
|
||||
if (maxhours*60+maxminutes < hours*60+minutes) {
|
||||
value.setDate(value.getDate()-1);
|
||||
}
|
||||
|
||||
me.lookup('dateentry').setMaxValue(value);
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
me.callParent();
|
||||
|
||||
let value = me.value || new Date();
|
||||
|
||||
me.lookupReference('dateentry').setValue(value);
|
||||
me.lookupReference('timeentry').setValue(value);
|
||||
|
||||
if (me.minValue) {
|
||||
me.setMinValue(me.minValue);
|
||||
}
|
||||
|
||||
if (me.maxValue) {
|
||||
me.setMaxValue(me.maxValue);
|
||||
}
|
||||
|
||||
me.relayEvents(me.lookupReference('dateentry'), ['change']);
|
||||
me.relayEvents(me.lookupReference('timeentry'), ['change']);
|
||||
},
|
||||
});
|
||||
|
||||
@ -8,7 +8,10 @@ Ext.define('Proxmox.form.DiskSelector', {
|
||||
// journal_disk: all disks with gpt
|
||||
diskType: undefined,
|
||||
|
||||
// the property the backend wnats for the type ('type' by default)
|
||||
// use include-partitions=1 as a parameter
|
||||
includePartitions: false,
|
||||
|
||||
// the property the backend wants for the type ('type' by default)
|
||||
typeProperty: 'type',
|
||||
|
||||
valueField: 'devpath',
|
||||
@ -53,6 +56,10 @@ Ext.define('Proxmox.form.DiskSelector', {
|
||||
extraParams[me.typeProperty] = me.diskType;
|
||||
}
|
||||
|
||||
if (me.includePartitions) {
|
||||
extraParams['include-partitions'] = 1;
|
||||
}
|
||||
|
||||
var store = Ext.create('Ext.data.Store', {
|
||||
filterOnLoad: true,
|
||||
model: 'pmx-disk-list',
|
||||
|
||||
@ -24,7 +24,7 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
|
||||
getEditable: function() {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
vm.get('editable');
|
||||
return vm.get('editable');
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
@ -37,9 +37,19 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
|
||||
getValue: function() {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
// FIXME: add return, but check all use-sites for regressions then
|
||||
vm.get('value');
|
||||
},
|
||||
|
||||
setEmptyText: function(emptyText) {
|
||||
let me = this;
|
||||
me.editField.setEmptyText(emptyText);
|
||||
},
|
||||
getEmptyText: function() {
|
||||
let me = this;
|
||||
return me.editField.getEmptyText();
|
||||
},
|
||||
|
||||
layout: 'fit',
|
||||
defaults: {
|
||||
hideLabel: true,
|
||||
@ -68,6 +78,10 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
|
||||
delete displayConfig.displayConfig;
|
||||
}
|
||||
|
||||
Ext.applyIf(displayConfig, {
|
||||
renderer: v => Ext.htmlEncode(v),
|
||||
});
|
||||
|
||||
Ext.applyIf(displayConfig.bind, {
|
||||
hidden: '{editable}',
|
||||
disabled: '{editable}',
|
||||
@ -94,6 +108,11 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
|
||||
|
||||
me.callParent();
|
||||
|
||||
// save a reference to make it easier when one needs to operate on the underlying fields,
|
||||
// like when creating a passthrough getter/setter to allow easy data-binding.
|
||||
me.editField = me.down(editConfig.xtype);
|
||||
me.displayField = me.down(displayConfig.xtype);
|
||||
|
||||
me.getViewModel().set('editable', me.editable);
|
||||
},
|
||||
|
||||
|
||||
14
src/form/FingerprintField.js
Normal file
@ -0,0 +1,14 @@
|
||||
Ext.define('Proxmox.form.field.FingerprintField', {
|
||||
extend: 'Proxmox.form.field.Textfield',
|
||||
alias: ['widget.pmxFingerprintField'],
|
||||
|
||||
config: {
|
||||
fieldLabel: gettext('Fingerprint'),
|
||||
emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
|
||||
|
||||
regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
|
||||
regexText: gettext('Example') + ': AB:CD:EF:...',
|
||||
|
||||
allowBlank: true,
|
||||
},
|
||||
});
|
||||
@ -11,20 +11,19 @@ Ext.define('Proxmox.form.field.Integer', {
|
||||
step: 1,
|
||||
|
||||
getSubmitData: function() {
|
||||
let me = this,
|
||||
data = null,
|
||||
val;
|
||||
if (!me.disabled && me.submitValue && !me.isFileUpload()) {
|
||||
val = me.getSubmitValue();
|
||||
if (val !== undefined && val !== null && val !== '') {
|
||||
data = {};
|
||||
data[me.getName()] = val;
|
||||
} else if (me.getDeleteEmpty()) {
|
||||
let me = this;
|
||||
let data = null;
|
||||
if (!me.disabled && me.submitValue && !me.isFileUpload()) {
|
||||
let val = me.getSubmitValue();
|
||||
if (val !== undefined && val !== null && val !== '') {
|
||||
data = {};
|
||||
data.delete = me.getName();
|
||||
data[me.getName()] = val;
|
||||
} else if (me.getDeleteEmpty()) {
|
||||
data = {};
|
||||
data.delete = me.getName();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@ Ext.define('Proxmox.form.KVComboBox', {
|
||||
valueField: 'key',
|
||||
queryMode: 'local',
|
||||
|
||||
// overide framework function to implement deleteEmpty behaviour
|
||||
// override framework function to implement deleteEmpty behaviour
|
||||
getSubmitData: function() {
|
||||
let me = this,
|
||||
data = null,
|
||||
|
||||
@ -3,4 +3,9 @@ Ext.define('Proxmox.form.LanguageSelector', {
|
||||
xtype: 'proxmoxLanguageSelector',
|
||||
|
||||
comboItems: Proxmox.Utils.language_array(),
|
||||
|
||||
matchFieldWidth: false,
|
||||
listConfig: {
|
||||
width: 300,
|
||||
},
|
||||
});
|
||||
|
||||
@ -24,6 +24,9 @@ Ext.define('Proxmox.form.MultiDiskSelector', {
|
||||
// the type of disks to show
|
||||
diskType: 'unused',
|
||||
|
||||
// add include-partitions=1 as a request parameter
|
||||
includePartitions: false,
|
||||
|
||||
disks: [],
|
||||
|
||||
allowBlank: false,
|
||||
@ -36,6 +39,8 @@ Ext.define('Proxmox.form.MultiDiskSelector', {
|
||||
setValue: function(value) {
|
||||
let me = this;
|
||||
|
||||
value ??= [];
|
||||
|
||||
if (!Ext.isArray(value)) {
|
||||
value = value.split(/;, /);
|
||||
}
|
||||
@ -141,22 +146,31 @@ Ext.define('Proxmox.form.MultiDiskSelector', {
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
let extraParams = {};
|
||||
|
||||
if (!me.url) {
|
||||
if (!me.nodename) {
|
||||
throw "no url or nodename given";
|
||||
}
|
||||
|
||||
let node = me.nodename;
|
||||
let param = me.typeParameter;
|
||||
let type = me.diskType;
|
||||
me.url = `/api2/json/nodes/${node}/disks/list?${param}=${type}`;
|
||||
me.url = `/api2/json/nodes/${me.nodename}/disks/list`;
|
||||
|
||||
extraParams[me.typeParameter] = me.diskType;
|
||||
|
||||
if (me.includePartitions) {
|
||||
extraParams['include-partitions'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
me.disks = [];
|
||||
|
||||
me.callParent();
|
||||
let store = me.getStore();
|
||||
store.getProxy().setUrl(me.url);
|
||||
store.setProxy({
|
||||
type: 'proxmox',
|
||||
url: me.url,
|
||||
extraParams,
|
||||
});
|
||||
store.load();
|
||||
store.sort({ property: me.valueField });
|
||||
},
|
||||
|
||||
@ -45,10 +45,6 @@ Ext.define('Proxmox.form.NetworkSelector', {
|
||||
networkSelectorStore.load();
|
||||
}
|
||||
},
|
||||
// set default value to empty array, else it inits it with
|
||||
// null and after the store load it is an empty array,
|
||||
// triggering dirtychange
|
||||
value: [],
|
||||
valueField: 'cidr',
|
||||
displayField: 'cidr',
|
||||
store: {
|
||||
@ -69,8 +65,8 @@ Ext.define('Proxmox.form.NetworkSelector', {
|
||||
},
|
||||
],
|
||||
listeners: {
|
||||
load: function(store, records, successfull) {
|
||||
if (successfull) {
|
||||
load: function(store, records, successful) {
|
||||
if (successful) {
|
||||
records.forEach(function(record) {
|
||||
if (record.data.cidr6) {
|
||||
let dest = record.data.cidr ? record.copy(null) : record;
|
||||
@ -123,6 +119,7 @@ Ext.define('Proxmox.form.NetworkSelector', {
|
||||
header: gettext('Comment'),
|
||||
flex: 2,
|
||||
dataIndex: 'comments',
|
||||
renderer: Ext.String.htmlEncode,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -6,7 +6,13 @@ Ext.define('Proxmox.form.RealmComboBox', {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
init: function(view) {
|
||||
view.store.on('load', this.onLoad, view);
|
||||
let store = view.getStore();
|
||||
store.proxy.url = `/api2/json${view.baseUrl}`;
|
||||
if (view.storeFilter) {
|
||||
store.setFilters(view.storeFilter);
|
||||
}
|
||||
store.on('load', this.onLoad, view);
|
||||
store.load();
|
||||
},
|
||||
|
||||
onLoad: function(store, records, success) {
|
||||
@ -27,6 +33,9 @@ Ext.define('Proxmox.form.RealmComboBox', {
|
||||
},
|
||||
},
|
||||
|
||||
// define custom filters for the underlying store
|
||||
storeFilter: undefined,
|
||||
|
||||
fieldLabel: gettext('Realm'),
|
||||
name: 'realm',
|
||||
queryMode: 'local',
|
||||
@ -37,6 +46,7 @@ Ext.define('Proxmox.form.RealmComboBox', {
|
||||
triggerAction: 'all',
|
||||
valueField: 'realm',
|
||||
displayField: 'descr',
|
||||
baseUrl: '/access/domains',
|
||||
getState: function() {
|
||||
return { value: this.getValue() };
|
||||
},
|
||||
@ -52,6 +62,6 @@ Ext.define('Proxmox.form.RealmComboBox', {
|
||||
|
||||
store: {
|
||||
model: 'pmx-domains',
|
||||
autoLoad: true,
|
||||
autoLoad: false,
|
||||
},
|
||||
});
|
||||
|
||||
@ -18,17 +18,22 @@ Ext.define('Proxmox.form.RoleSelector', {
|
||||
displayField: 'roleid',
|
||||
|
||||
listConfig: {
|
||||
width: 560,
|
||||
resizable: true,
|
||||
columns: [
|
||||
{
|
||||
header: gettext('Role'),
|
||||
sortable: true,
|
||||
dataIndex: 'roleid',
|
||||
flex: 1,
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
header: gettext('Privileges'),
|
||||
dataIndex: 'privs',
|
||||
flex: 1,
|
||||
cellWrap: true,
|
||||
// join manually here, as ExtJS joins without whitespace which breaks cellWrap
|
||||
renderer: v => Ext.isArray(v) ? v.join(', ') : v.replaceAll(',', ', '),
|
||||
flex: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
61
src/form/TextAreaField.js
Normal file
@ -0,0 +1,61 @@
|
||||
Ext.define('Proxmox.form.field.Base64TextArea', {
|
||||
extend: 'Ext.form.field.TextArea',
|
||||
alias: ['widget.proxmoxBase64TextArea'],
|
||||
|
||||
config: {
|
||||
skipEmptyText: false,
|
||||
deleteEmpty: false,
|
||||
trimValue: false,
|
||||
editable: true,
|
||||
width: 600,
|
||||
height: 400,
|
||||
scrollable: 'y',
|
||||
emptyText: gettext('You can use Markdown for rich text formatting.'),
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
// We want to edit the decoded version of the text
|
||||
this.callParent([Proxmox.Utils.base64ToUtf8(value)]);
|
||||
},
|
||||
|
||||
processRawValue: function(value) {
|
||||
// The field could contain multi-line values
|
||||
return Proxmox.Utils.utf8ToBase64(value);
|
||||
},
|
||||
|
||||
getSubmitData: function() {
|
||||
let me = this,
|
||||
data = null,
|
||||
val;
|
||||
if (!me.disabled && me.submitValue && !me.isFileUpload()) {
|
||||
val = me.getSubmitValue();
|
||||
if (val !== null) {
|
||||
data = {};
|
||||
data[me.getName()] = val;
|
||||
} else if (me.getDeleteEmpty()) {
|
||||
data = {};
|
||||
data.delete = me.getName();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
getSubmitValue: function() {
|
||||
let me = this;
|
||||
|
||||
let value = this.processRawValue(this.getRawValue());
|
||||
if (me.getTrimValue() && typeof value === 'string') {
|
||||
value = value.trim();
|
||||
}
|
||||
if (value !== '') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return me.getSkipEmptyText() ? null: value;
|
||||
},
|
||||
|
||||
setAllowBlank: function(allowBlank) {
|
||||
this.allowBlank = allowBlank;
|
||||
this.validate();
|
||||
},
|
||||
});
|
||||
@ -6,6 +6,8 @@ Ext.define('Proxmox.form.field.Textfield', {
|
||||
skipEmptyText: true,
|
||||
|
||||
deleteEmpty: false,
|
||||
|
||||
trimValue: false,
|
||||
},
|
||||
|
||||
getSubmitData: function() {
|
||||
@ -29,6 +31,9 @@ Ext.define('Proxmox.form.field.Textfield', {
|
||||
let me = this;
|
||||
|
||||
let value = this.processRawValue(this.getRawValue());
|
||||
if (me.getTrimValue() && typeof value === 'string') {
|
||||
value = value.trim();
|
||||
}
|
||||
if (value !== '') {
|
||||
return value;
|
||||
}
|
||||
|
||||
6
src/form/ThemeSelector.js
Normal file
@ -0,0 +1,6 @@
|
||||
Ext.define('Proxmox.form.ThemeSelector', {
|
||||
extend: 'Proxmox.form.KVComboBox',
|
||||
xtype: 'proxmoxThemeSelector',
|
||||
|
||||
comboItems: Proxmox.Utils.theme_array(),
|
||||
});
|
||||
50
src/form/UserSelector.js
Normal file
@ -0,0 +1,50 @@
|
||||
Ext.define('Proxmox.form.UserSelector', {
|
||||
extend: 'Proxmox.form.ComboGrid',
|
||||
alias: 'widget.pmxUserSelector',
|
||||
|
||||
allowBlank: false,
|
||||
autoSelect: false,
|
||||
valueField: 'userid',
|
||||
displayField: 'userid',
|
||||
|
||||
editable: true,
|
||||
anyMatch: true,
|
||||
forceSelection: true,
|
||||
|
||||
store: {
|
||||
model: 'pmx-users',
|
||||
autoLoad: true,
|
||||
params: {
|
||||
enabled: 1,
|
||||
},
|
||||
sorters: 'userid',
|
||||
},
|
||||
|
||||
listConfig: {
|
||||
columns: [
|
||||
{
|
||||
header: gettext('User'),
|
||||
sortable: true,
|
||||
dataIndex: 'userid',
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: gettext('Name'),
|
||||
sortable: true,
|
||||
renderer: (first, mD, rec) => Ext.String.htmlEncode(
|
||||
`${first || ''} ${rec.data.lastname || ''}`,
|
||||
),
|
||||
dataIndex: 'firstname',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: gettext('Comment'),
|
||||
sortable: false,
|
||||
dataIndex: 'comment',
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
40
src/form/VlanField.js
Normal file
@ -0,0 +1,40 @@
|
||||
Ext.define('Proxmox.form.field.VlanField', {
|
||||
extend: 'Ext.form.field.Number',
|
||||
alias: ['widget.proxmoxvlanfield'],
|
||||
|
||||
deleteEmpty: false,
|
||||
|
||||
emptyText: gettext('no VLAN'),
|
||||
|
||||
fieldLabel: gettext('VLAN Tag'),
|
||||
|
||||
allowBlank: true,
|
||||
|
||||
getSubmitData: function() {
|
||||
var me = this,
|
||||
data = null,
|
||||
val;
|
||||
if (!me.disabled && me.submitValue) {
|
||||
val = me.getSubmitValue();
|
||||
if (val) {
|
||||
data = {};
|
||||
data[me.getName()] = val;
|
||||
} else if (me.deleteEmpty) {
|
||||
data = {};
|
||||
data.delete = me.getName();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
|
||||
Ext.apply(me, {
|
||||
minValue: 1,
|
||||
maxValue: 4094,
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
@ -1,24 +1,53 @@
|
||||
/* Renders a list of key values objets
|
||||
/** Renders a list of key values objects
|
||||
|
||||
mandatory config parameters:
|
||||
rows: an object container where each propery is a key-value object we want to render
|
||||
let rows = {
|
||||
keyboard: {
|
||||
header: gettext('Keyboard Layout'),
|
||||
editor: 'Your.KeyboardEdit',
|
||||
required: true
|
||||
},
|
||||
Mandatory Config Parameters:
|
||||
|
||||
optional:
|
||||
disabled: setting this parameter to true will disable selection and focus on the
|
||||
proxmoxObjectGrid as well as greying out input elements.
|
||||
Useful for a readonly tabular display
|
||||
rows: an object container where each property is a key-value object we want to render
|
||||
|
||||
rows: {
|
||||
keyboard: {
|
||||
header: gettext('Keyboard Layout'),
|
||||
editor: 'Your.KeyboardEdit',
|
||||
required: true
|
||||
},
|
||||
// ...
|
||||
},
|
||||
|
||||
Convenience Helper:
|
||||
|
||||
As alternative you can use the common add-row helper like `add_text_row`, but you need to
|
||||
call it in an overridden initComponent before `me.callParent(arguments)` gets executed.
|
||||
|
||||
For a declarative approach you can use the `gridRows` configuration to pass an array of
|
||||
objects with each having at least a `xtype` to match `add_XTYPE_row` and a field-name
|
||||
property, for example:
|
||||
|
||||
gridRows: [
|
||||
{
|
||||
xtype: 'text',
|
||||
name: 'http-proxy',
|
||||
text: gettext('HTTP proxy'),
|
||||
defaultValue: Proxmox.Utils.noneText,
|
||||
vtype: 'HttpProxy',
|
||||
deleteEmpty: true,
|
||||
},
|
||||
],
|
||||
|
||||
Optional Configs:
|
||||
|
||||
disabled:: setting this parameter to true will disable selection and focus on
|
||||
the proxmoxObjectGrid as well as greying out input elements. Useful for a
|
||||
readonly tabular display
|
||||
|
||||
*/
|
||||
|
||||
Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
extend: 'Ext.grid.GridPanel',
|
||||
alias: ['widget.proxmoxObjectGrid'],
|
||||
|
||||
// can be used as declarative replacement over manually calling the add_XYZ_row helpers,
|
||||
// see top-level doc-comment above for details/example
|
||||
gridRows: [],
|
||||
|
||||
disabled: false,
|
||||
hideHeaders: true,
|
||||
|
||||
@ -38,7 +67,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
subject: text,
|
||||
onlineHelp: opts.onlineHelp,
|
||||
fieldDefaults: {
|
||||
labelWidth: opts.labelWidth || 100,
|
||||
},
|
||||
@ -55,6 +83,9 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
},
|
||||
},
|
||||
};
|
||||
if (opts.onlineHelp) {
|
||||
me.rows[name].editor.onlineHelp = opts.onlineHelp;
|
||||
}
|
||||
},
|
||||
|
||||
add_text_row: function(name, text, opts) {
|
||||
@ -71,7 +102,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
subject: text,
|
||||
onlineHelp: opts.onlineHelp,
|
||||
fieldDefaults: {
|
||||
labelWidth: opts.labelWidth || 100,
|
||||
},
|
||||
@ -80,13 +110,15 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
name: name,
|
||||
deleteEmpty: !!opts.deleteEmpty,
|
||||
emptyText: opts.defaultValue,
|
||||
labelWidth: Proxmox.Utils.compute_min_label_width(
|
||||
text, opts.labelWidth),
|
||||
labelWidth: Proxmox.Utils.compute_min_label_width(text, opts.labelWidth),
|
||||
vtype: opts.vtype,
|
||||
fieldLabel: text,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (opts.onlineHelp) {
|
||||
me.rows[name].editor.onlineHelp = opts.onlineHelp;
|
||||
}
|
||||
},
|
||||
|
||||
add_boolean_row: function(name, text, opts) {
|
||||
@ -103,7 +135,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
subject: text,
|
||||
onlineHelp: opts.onlineHelp,
|
||||
fieldDefaults: {
|
||||
labelWidth: opts.labelWidth || 100,
|
||||
},
|
||||
@ -114,12 +145,14 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
defaultValue: opts.defaultValue || 0,
|
||||
checked: !!opts.defaultValue,
|
||||
deleteDefaultValue: !!opts.deleteDefaultValue,
|
||||
labelWidth: Proxmox.Utils.compute_min_label_width(
|
||||
text, opts.labelWidth),
|
||||
labelWidth: Proxmox.Utils.compute_min_label_width(text, opts.labelWidth),
|
||||
fieldLabel: text,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (opts.onlineHelp) {
|
||||
me.rows[name].editor.onlineHelp = opts.onlineHelp;
|
||||
}
|
||||
},
|
||||
|
||||
add_integer_row: function(name, text, opts) {
|
||||
@ -136,7 +169,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
subject: text,
|
||||
onlineHelp: opts.onlineHelp,
|
||||
fieldDefaults: {
|
||||
labelWidth: opts.labelWidth || 100,
|
||||
},
|
||||
@ -148,12 +180,45 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
emptyText: gettext('Default'),
|
||||
deleteEmpty: !!opts.deleteEmpty,
|
||||
value: opts.defaultValue,
|
||||
labelWidth: Proxmox.Utils.compute_min_label_width(
|
||||
text, opts.labelWidth),
|
||||
labelWidth: Proxmox.Utils.compute_min_label_width(text, opts.labelWidth),
|
||||
fieldLabel: text,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (opts.onlineHelp) {
|
||||
me.rows[name].editor.onlineHelp = opts.onlineHelp;
|
||||
}
|
||||
},
|
||||
|
||||
// adds a row that allows editing in a full TextArea that transparently de/encodes as Base64
|
||||
add_textareafield_row: function(name, text, opts) {
|
||||
let me = this;
|
||||
|
||||
opts = opts || {};
|
||||
me.rows = me.rows || {};
|
||||
let fieldOpts = opts.fieldOpts || {};
|
||||
|
||||
me.rows[name] = {
|
||||
required: true,
|
||||
defaultValue: "",
|
||||
header: text,
|
||||
renderer: value => Ext.htmlEncode(Proxmox.Utils.base64ToUtf8(value)),
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
subject: text,
|
||||
fieldDefaults: {
|
||||
labelWidth: opts.labelWidth || 600,
|
||||
},
|
||||
items: {
|
||||
xtype: 'proxmoxBase64TextArea',
|
||||
...fieldOpts,
|
||||
name,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (opts.onlineHelp) {
|
||||
me.rows[name].editor.onlineHelp = opts.onlineHelp;
|
||||
}
|
||||
},
|
||||
|
||||
editorConfig: {}, // default config passed to editor
|
||||
@ -222,7 +287,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
|
||||
let renderer = rowdef.renderer;
|
||||
if (renderer) {
|
||||
return renderer(value, metaData, record, rowIndex, colIndex, store);
|
||||
return renderer.call(me, value, metaData, record, rowIndex, colIndex, store);
|
||||
}
|
||||
|
||||
return value;
|
||||
@ -246,6 +311,17 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
for (const rowdef of me.gridRows || []) {
|
||||
let addFn = me[`add_${rowdef.xtype}_row`];
|
||||
if (typeof addFn !== 'function') {
|
||||
throw `unknown object-grid row xtype '${rowdef.xtype}'`;
|
||||
} else if (typeof rowdef.name !== 'string') {
|
||||
throw `object-grid row need a valid name string-property!`;
|
||||
} else {
|
||||
addFn.call(me, rowdef.name, rowdef.text || rowdef.name, rowdef);
|
||||
}
|
||||
}
|
||||
|
||||
let rows = me.rows;
|
||||
|
||||
if (!me.rstore) {
|
||||
@ -269,13 +345,13 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
||||
});
|
||||
|
||||
if (rows) {
|
||||
Ext.Object.each(rows, function(key, rowdef) {
|
||||
for (const [key, rowdef] of Object.entries(rows)) {
|
||||
if (Ext.isDefined(rowdef.defaultValue)) {
|
||||
store.add({ key: key, value: rowdef.defaultValue });
|
||||
} else if (rowdef.required) {
|
||||
store.add({ key: key, value: undefined });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (me.sorterFn) {
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
# icon-cpu, icon-ram
|
||||
# are self made (sources as .xcf)
|
||||
|
||||
include ../defines.mk
|
||||
|
||||
IMAGES=pmx-clear-trigger.png \
|
||||
icon-cpu.png \
|
||||
icon-ram.png \
|
||||
|
||||
IMAGES=pmx-clear-trigger.png \
|
||||
openid-icon-100x100.png \
|
||||
icon-cpu.svg \
|
||||
icon-ram.svg \
|
||||
debian-swirl-openlogo.svg \
|
||||
proxmox-symbol-x.svg \
|
||||
|
||||
all:
|
||||
|
||||
.PHONY: install
|
||||
install: ${IMAGES}
|
||||
install -d ${WWWIMAGESDIR}
|
||||
for i in ${IMAGES}; do install -m 0755 $$i ${WWWIMAGESDIR}/$$i; done
|
||||
install: $(IMAGES)
|
||||
install -d $(WWWIMAGESDIR)
|
||||
for i in $(IMAGES); do install -m 0644 $$i $(WWWIMAGESDIR)/$$i; done
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
|
||||
26
src/images/debian-swirl-openlogo.svg
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 10.0, SVG Export Plug-In . SVG Version: 3.0.0 Build 77) -->
|
||||
<svg enable-background="new 0 0 87.041 108.445" height="108.445" viewBox="0,0,87.041,108.445" width="87.041" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:x="http://ns.adobe.com/Extensibility/1.0/" xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" xmlns:graph="http://ns.adobe.com/Graphs/1.0/" i:viewOrigin="262 450" i:rulerOrigin="0 0" i:pageBounds="0 792 612 0" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/">
|
||||
<metadata>
|
||||
<sfw>
|
||||
<slices/>
|
||||
<sliceSourceBounds bottomLeftOrigin="true"/>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<g id="Layer_1" i:layer="yes" i:dimmedPercent="50" i:rgbTrio="#4F008000FFFF">
|
||||
<g>
|
||||
<path d="m51.986 57.297c-1.797.025.34.926 2.686 1.287.648-.506 1.236-1.018 1.76-1.516-1.461.358-2.948.366-4.446.229" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m61.631 54.893c1.07-1.477 1.85-3.094 2.125-4.766-.24 1.192-.887 2.221-1.496 3.307-3.359 2.115-.316-1.256-.002-2.537-3.612 4.546-.496 2.726-.627 3.996" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m65.191 45.629c.217-3.236-.637-2.213-.924-.978.335.174.6 2.281.924.978" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m45.172 1.399c.959.172 2.072.304 1.916.533 1.049-.23 1.287-.442-1.916-.533" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m47.088 1.932-.678.14.631-.056z" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m76.992 46.856c.107 2.906-.85 4.316-1.713 6.812l-1.553.776c-1.271 2.468.123 1.567-.787 3.53-1.984 1.764-6.021 5.52-7.313 5.863-.943-.021.639-1.113.846-1.541-2.656 1.824-2.131 2.738-6.193 3.846l-.119-.264c-10.018 4.713-23.934-4.627-23.751-17.371-.107.809-.304.607-.526.934-.517-6.557 3.028-13.143 9.007-15.832 5.848-2.895 12.704-1.707 16.893 2.197-2.301-3.014-6.881-6.209-12.309-5.91-5.317.084-10.291 3.463-11.951 7.131-2.724 1.715-3.04 6.611-4.227 7.507-1.597 11.737 3.004 16.808 10.787 22.773 1.225.826.345.951.511 1.58-2.586-1.211-4.954-3.039-6.901-5.277 1.033 1.512 2.148 2.982 3.589 4.137-2.438-.826-5.695-5.908-6.646-6.115 4.203 7.525 17.052 13.197 23.78 10.383-3.113.115-7.068.064-10.566-1.229-1.469-.756-3.467-2.322-3.11-2.615 9.182 3.43 18.667 2.598 26.612-3.771 2.021-1.574 4.229-4.252 4.867-4.289-.961 1.445.164.695-.574 1.971 2.014-3.248-.875-1.322 2.082-5.609l1.092 1.504c-.406-2.696 3.348-5.97 2.967-10.234.861-1.304.961 1.403.047 4.403 1.268-3.328.334-3.863.66-6.609.352.923.814 1.904 1.051 2.878-.826-3.216.848-5.416 1.262-7.285-.408-.181-1.275 1.422-1.473-2.377.029-1.65.459-.865.625-1.271-.324-.186-1.174-1.451-1.691-3.877.375-.57 1.002 1.478 1.512 1.562-.328-1.929-.893-3.4-.916-4.88-1.49-3.114-.527.415-1.736-1.337-1.586-4.947 1.316-1.148 1.512-3.396 2.404 3.483 3.775 8.881 4.404 11.117-.48-2.726-1.256-5.367-2.203-7.922.73.307-1.176-5.609.949-1.691-2.27-8.352-9.715-16.156-16.564-19.818.838.767 1.896 1.73 1.516 1.881-3.406-2.028-2.807-2.186-3.295-3.043-2.775-1.129-2.957.091-4.795.002-5.23-2.774-6.238-2.479-11.051-4.217l.219 1.023c-3.465-1.154-4.037.438-7.782.004-.228-.178 1.2-.644 2.375-.815-3.35.442-3.193-.66-6.471.122.808-.567 1.662-.942 2.524-1.424-2.732.166-6.522 1.59-5.352.295-4.456 1.988-12.37 4.779-16.811 8.943l-.14-.933c-2.035 2.443-8.874 7.296-9.419 10.46l-.544.127c-1.059 1.793-1.744 3.825-2.584 5.67-1.385 2.36-2.03.908-1.833 1.278-2.724 5.523-4.077 10.164-5.246 13.97.833 1.245.02 7.495.335 12.497-1.368 24.704 17.338 48.69 37.785 54.228 2.997 1.072 7.454 1.031 11.245 1.141-4.473-1.279-5.051-.678-9.408-2.197-3.143-1.48-3.832-3.17-6.058-5.102l.881 1.557c-4.366-1.545-2.539-1.912-6.091-3.037l.941-1.229c-1.415-.107-3.748-2.385-4.386-3.646l-1.548.061c-1.86-2.295-2.851-3.949-2.779-5.23l-.5.891c-.567-.973-6.843-8.607-3.587-6.83-.605-.553-1.409-.9-2.281-2.484l.663-.758c-1.567-2.016-2.884-4.6-2.784-5.461.836 1.129 1.416 1.34 1.99 1.533-3.957-9.818-4.179-.541-7.176-9.994l.634-.051c-.486-.732-.781-1.527-1.172-2.307l.276-2.75c-2.849-3.294-.797-14.006-.386-19.881.285-2.389 2.378-4.932 3.97-8.92l-.97-.167c1.854-3.234 10.586-12.988 14.63-12.486 1.959-2.461-.389-.009-.772-.629 4.303-4.453 5.656-3.146 8.56-3.947 3.132-1.859-2.688.725-1.203-.709 5.414-1.383 3.837-3.144 10.9-3.846.745.424-1.729.655-2.35 1.205 4.511-2.207 14.275-1.705 20.617 1.225 7.359 3.439 15.627 13.605 15.953 23.17l.371.1c-.188 3.802.582 8.199-.752 12.238z" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m32.372 59.764-.252 1.26c1.181 1.604 2.118 3.342 3.626 4.596-1.085-2.118-1.891-2.993-3.374-5.856" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m35.164 59.654c-.625-.691-.995-1.523-1.409-2.352.396 1.457 1.207 2.709 1.962 3.982z" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m84.568 48.916-.264.662c-.484 3.438-1.529 6.84-3.131 9.994 1.77-3.328 2.915-6.968 3.395-10.656" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m45.527.537c1.215-.445 2.987-.244 4.276-.537-1.68.141-3.352.225-5.003.438z" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m2.872 23.219c.28 2.592-1.95 3.598.494 1.889 1.31-2.951-.512-.815-.494-1.889" fill="#a80030" i:knockout="Off"/>
|
||||
<path d="m0 35.215c.563-1.728.665-2.766.88-3.766-1.556 1.989-.716 2.413-.88 3.766" fill="#a80030" i:knockout="Off"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 205 B |
38
src/images/icon-cpu.svg
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100"
|
||||
height="100"
|
||||
version="1.1"
|
||||
>
|
||||
|
||||
<g fill="none" stroke="#000" stroke-width="5" shape-rendering="crispEdges">
|
||||
<!-- base CPU -->
|
||||
<rect x="15" y="15" rx="0" ry="0" width="70" height="70" />
|
||||
<rect x="30" y="30" rx="0" ry="0" width="40" height="40" fill="black"/>
|
||||
<g stroke-width="8"> <!-- pins -->
|
||||
<!-- left -->
|
||||
<path d="m14,24.5 h-12.5"/>
|
||||
<path d="m14,41.5 h-12.5"/>
|
||||
<path d="m14,58.5 h-12.5"/>
|
||||
<path d="m14,75.5 h-12.5"/>
|
||||
<!-- right -->
|
||||
<path d="m86,24.5 h+12.5"/>
|
||||
<path d="m86,41.5 h+12.5"/>
|
||||
<path d="m86,58.5 h+12.5"/>
|
||||
<path d="m86,75.5 h+12.5"/>
|
||||
<!-- top -->
|
||||
<path d="m24.5,14 v-12.5"/>
|
||||
<path d="m41.5,14 v-12.5"/>
|
||||
<path d="m58.5,14 v-12.5"/>
|
||||
<path d="m75.5,14 v-12.5"/>
|
||||
<!-- bottom -->
|
||||
<path d="m24.5,86 v+12.5"/>
|
||||
<path d="m41.5,86 v+12.5"/>
|
||||
<path d="m58.5,86 v+12.5"/>
|
||||
<path d="m75.5,86 v+12.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 193 B |
29
src/images/icon-ram.svg
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100"
|
||||
height="100"
|
||||
version="1.1"
|
||||
>
|
||||
|
||||
<g fill="none" stroke="#000" stroke-width="7" shape-rendering="crispEdges">
|
||||
<rect x="3.0" y="25" rx="0" ry="0" width="94" height="40"/> <!-- outer dimm PCB border -->
|
||||
|
||||
<g stroke-width="0"> <!-- dimm chips -->
|
||||
<rect x="12.5" y="35" rx="0" ry="0" width="20" height="20" fill="black"/>
|
||||
<rect x="40.0" y="35" rx="0" ry="0" width="20" height="20" fill="black"/>
|
||||
<rect x="67.5" y="35" rx="0" ry="0" width="20" height="20" fill="black"/>
|
||||
</g>
|
||||
|
||||
<g stroke-width="8"> <!-- pins -->
|
||||
<path d="m10,67 v+10"/>
|
||||
<path d="m26,67 v+10"/>
|
||||
<path d="m42,67 v+10"/>
|
||||
<path d="m58,67 v+10"/>
|
||||
<path d="m74,67 v+10"/>
|
||||
<path d="m90,67 v+10"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/images/openid-icon-100x100.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
29
src/images/proxmox-symbol-x.svg
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg id="svg5876" height="260" viewBox="0,0,259.99998,259.99998" width="260" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:version="0.92.2 (5c3e80d, 2017-08-06)" sodipodi:docname="Proxmox_symbol_favicon.svg" inkscape:export-filename="S:\Proxmox\Marketing\Logo\Favicon\Proxmox_symbol_favicon_260.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96">
|
||||
<clipPath id="clipPath3102-4-5">
|
||||
<rect id="rect3104-1-3" height="326.40991" transform="matrix(.73449161 .67861776 -.78497193 .61953133 0 0)" width="436.40189" x="-82.999916" y="-347.71387"/>
|
||||
</clipPath>
|
||||
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1.4" inkscape:cx="-45.895771" inkscape:cy="6.3650844" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="2560" inkscape:window-height="1377" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" units="px" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0"/>
|
||||
<metadata id="metadata5881">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g id="layer1" transform="translate(-2.603202 -774.18762)" inkscape:label="Layer 1" inkscape:groupmode="layer">
|
||||
<g id="g3021" transform="matrix(9.45675 0 0 9.45675 -18.704755 -8896.4297)" inkscape:export-xdpi="300" inkscape:export-ydpi="300">
|
||||
<g id="g7607-5" clip-path="url(#clipPath3102-4-5)" font-family="Helion" font-size="144" letter-spacing="0" line-height="125%" transform="matrix(-.04778455 0 0 .04778455 29.203266 1024.7667)" word-spacing="0">
|
||||
<path id="path7609-8" d="m276.30443 226.6231 190.58784-209.602495c-7.39324-7.3925179-16.00465-13.2011729-25.83431-17.42598195-9.8304-4.22414015-20.39156-6.37699515-31.68359-6.45857095-11.99338.0917448-22.98109 2.4680153-32.96314 7.12881886-9.98263 4.66147074-18.655 11.05910904-26.0171 19.19293404l-74.09086 80.915015-73.60342-80.915015c-7.6265-8.133835-16.50198-14.5314802-26.62641-19.1929557-10.12463-4.6608083-21.07169-7.0370717-32.84124-7.1287972-11.29242.0815863-21.8536 2.2344429-31.6836 6.45857839-9.83005 4.22480401-18.441478 10.03345661-25.834313 17.42597451l190.593403 209.596915" inkscape:connector-curvature="0" sodipodi:nodetypes="ccccccccccccc"/>
|
||||
</g>
|
||||
<g id="g7611-6" clip-path="url(#clipPath3102-4-5)" font-family="Helion" font-size="144" letter-spacing="0" line-height="125%" transform="matrix(.04778455 0 0 -.04778455 2.796945 1047.9575)" word-spacing="0">
|
||||
<path id="path7613-7" d="m276.30443 226.6231 190.58784-209.602495c-7.39324-7.3925179-16.00465-13.2011729-25.83431-17.42598195-9.8304-4.22414015-20.39156-6.37699515-31.68359-6.45857095-11.99338.0917448-22.98109 2.4680153-32.96314 7.12881886-9.98263 4.66147074-18.655 11.05910904-26.0171 19.19293404l-74.09086 80.915015-73.60342-80.915015c-7.6265-8.133835-16.50198-14.5314802-26.62641-19.1929557-10.12463-4.6608083-21.07169-7.0370717-32.84124-7.1287972-11.29242.0815863-21.8536 2.2344429-31.6836 6.45857839-9.83005 4.22480401-18.441478 10.03345661-25.834313 17.42597451l190.593403 209.596915" sodipodi:nodetypes="ccccccccccccc" inkscape:connector-curvature="0"/>
|
||||
</g>
|
||||
<path id="path7615-7" d="m15.23405 1036.3622-6.8601967-7.5231c-.3990192-.4256-.8633807-.7604-1.3930839-1.0042-.5297195-.2439-1.1024686-.3682-1.7182481-.3731-.590815 0-1.1433739.1168-1.6576769.338-.5143056.221-.9648526.5249-1.3516436.9118l6.9622457 7.6508-6.9622457 7.6508c.38679.3989.837338.7103 1.3516436.934.514303.2237 1.0668619.3373 1.6576769.3411.6173744 0 1.1933086-.129 1.7278076-.373.5344943-.2439.9956679-.5787 1.3835244-1.0041l6.8601817-7.549" fill="#e57000" font-family="Helion" font-size="1059.612793" letter-spacing="0" line-height="125%" word-spacing="0" inkscape:connector-curvature="0" sodipodi:nodetypes="cccccccccscscc"/>
|
||||
<path id="path7617-2" d="m16.76594 1036.3622 6.860196-7.5231c.399019-.4256.86338-.7604 1.393084-1.0042.529719-.2439 1.10247-.3682 1.718247-.3731.590818 0 1.143377.1168 1.657677.338.51431.221.964857.5249 1.351646.9118l-6.962247 7.6508 6.962247 7.6508c-.386789.3989-.837336.7103-1.351646.934-.5143.2237-1.066859.3373-1.657673.3411-.617375 0-1.193311-.129-1.727809-.373-.534496-.2439-.99567-.5787-1.383526-1.0041l-6.860182-7.549" fill="#e57000" font-family="Helion" font-size="1059.612793" letter-spacing="0" line-height="125%" word-spacing="0" sodipodi:nodetypes="cccccccccscscc" inkscape:connector-curvature="0"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@ -1,3 +1,99 @@
|
||||
/*
|
||||
* The Proxmox CBind mixin is intended to supplement the 'bind' mechanism
|
||||
* of ExtJS. In contrast to the 'bind', 'cbind' only acts during the creation
|
||||
* of the component, not during its lifetime. It's only applied once before
|
||||
* the 'initComponent' method is executed, and thus you have only access
|
||||
* to the basic initial configuration of it.
|
||||
*
|
||||
* You can use it to get a 'declarative' approach to component declaration,
|
||||
* even when you need to set some properties of sub-components dynamically
|
||||
* (e.g., the 'nodename'). It overwrites the given properties of the 'cbind'
|
||||
* object in the component with their computed values of the computed
|
||||
* cbind configuration object of the 'cbindData' function (or object).
|
||||
*
|
||||
* The cbind syntax is inspired by ExtJS' bind syntax ('{property}'), where
|
||||
* it is possible to negate values ('{!negated}'), access sub-properties of
|
||||
* objects ('{object.property}') and even use a getter function,
|
||||
* akin to viewModel formulas ('(get) => get("prop")') to execute more
|
||||
* complicated dependencies (e.g., urls).
|
||||
*
|
||||
* The 'cbind' will be recursively applied to all properties (objects/arrays)
|
||||
* that contain an 'xtype' or 'cbind' property, but stops for a subtree if the
|
||||
* object in question does not have either (if you have one or more levels that
|
||||
* have no cbind/xtype property, you can insert empty cbind objects there to
|
||||
* reach deeper nested objects).
|
||||
*
|
||||
* This reduces the code in the 'initComponent' and instead we can statically
|
||||
* declare items, buttons, tbars, etc. while the dynamic parts are contained
|
||||
* in the 'cbind'.
|
||||
*
|
||||
* It is used like in the following example:
|
||||
*
|
||||
* Ext.define('Some.Component', {
|
||||
* extend: 'Some.other.Component',
|
||||
*
|
||||
* // first it has to be enabled
|
||||
* mixins: ['Proxmox.Mixin.CBind'],
|
||||
*
|
||||
* // then a base config has to be defined. this can be a function,
|
||||
* // which has access to the initial config and can store persistent
|
||||
* // properties, as well as return temporary ones (which only exist during
|
||||
* // the cbind process)
|
||||
* // this function will be called before 'initComponent'
|
||||
* cbindData: function(initialconfig) {
|
||||
* // 'this' here is the same as in 'initComponent'
|
||||
* let me = this;
|
||||
* me.persistentProperty = false;
|
||||
* return {
|
||||
* temporaryProperty: true,
|
||||
* };
|
||||
* },
|
||||
*
|
||||
* // if there is no need for persistent properties, it can also simply be an object
|
||||
* cbindData: {
|
||||
* temporaryProperty: true,
|
||||
* // properties itself can also be functions that will be evaluated before
|
||||
* // replacing the values
|
||||
* dynamicProperty: (cfg) => !cfg.temporaryProperty,
|
||||
* numericProp: 0,
|
||||
* objectProp: {
|
||||
* foo: 'bar',
|
||||
* bar: 'baz',
|
||||
* }
|
||||
* },
|
||||
*
|
||||
* // you can 'cbind' the component itself, here the 'target' property
|
||||
* // will be replaced with the content of 'temporaryProperty' (true)
|
||||
* // before the components initComponent
|
||||
* cbind: {
|
||||
* target: '{temporaryProperty}',
|
||||
* },
|
||||
*
|
||||
* items: [
|
||||
* {
|
||||
* xtype: 'checkbox',
|
||||
* cbind: {
|
||||
* value: '{!persistentProperty}',
|
||||
* object: '{objectProp.foo}'
|
||||
* dynamic: (get) => get('numericProp') + 1,
|
||||
* },
|
||||
* },
|
||||
* {
|
||||
* // empty cbind so that subitems are reached
|
||||
* cbind: {},
|
||||
* items: [
|
||||
* {
|
||||
* xtype: 'textfield',
|
||||
* cbind: {
|
||||
* value: '{objectProp.bar}',
|
||||
* },
|
||||
* },
|
||||
* ],
|
||||
* },
|
||||
* ],
|
||||
* });
|
||||
*/
|
||||
|
||||
Ext.define('Proxmox.Mixin.CBind', {
|
||||
extend: 'Ext.Mixin',
|
||||
|
||||
@ -88,7 +184,7 @@ Ext.define('Proxmox.Mixin.CBind', {
|
||||
found = false;
|
||||
for (i = 0; i < arrayLength; i++) {
|
||||
el = org[i];
|
||||
if (el.constructor === Object && el.xtype) {
|
||||
if (el.constructor === Object && (el.xtype || el.cbind)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@ -99,7 +195,7 @@ Ext.define('Proxmox.Mixin.CBind', {
|
||||
copy = [];
|
||||
for (i = 0; i < arrayLength; i++) {
|
||||
el = org[i];
|
||||
if (el.constructor === Object && el.xtype) {
|
||||
if (el.constructor === Object && (el.xtype || el.cbind)) {
|
||||
elcopy = cloneTemplateObject(el);
|
||||
if (elcopy.cbind) {
|
||||
applyCBind(elcopy);
|
||||
@ -123,7 +219,7 @@ Ext.define('Proxmox.Mixin.CBind', {
|
||||
res[prop] = el;
|
||||
continue;
|
||||
}
|
||||
if (el.constructor === Object && el.xtype) {
|
||||
if (el.constructor === Object && (el.xtype || el.cbind)) {
|
||||
copy = cloneTemplateObject(el);
|
||||
if (copy.cbind) {
|
||||
applyCBind(copy);
|
||||
@ -146,7 +242,7 @@ Ext.define('Proxmox.Mixin.CBind', {
|
||||
el = me[prop];
|
||||
if (el === undefined || el === null) continue;
|
||||
if (typeof el === 'object' && el.constructor === Object) {
|
||||
if (el.xtype && prop !== 'config') {
|
||||
if ((el.xtype || el.cbind) && prop !== 'config') {
|
||||
me[prop] = cloneTemplateObject(el);
|
||||
}
|
||||
} else if (el.constructor === Array) {
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
Ext.define('apt-pkglist', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: ['Package', 'Title', 'Description', 'Section', 'Arch',
|
||||
'Priority', 'Version', 'OldVersion', 'ChangeLogUrl', 'Origin'],
|
||||
fields: [
|
||||
'Package', 'Title', 'Description', 'Section', 'Arch', 'Priority', 'Version', 'OldVersion',
|
||||
'Origin',
|
||||
],
|
||||
idProperty: 'Package',
|
||||
});
|
||||
|
||||
@ -56,7 +58,7 @@ Ext.define('Proxmox.node.APT', {
|
||||
groupField: 'Origin',
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: "/api2/json/nodes/" + me.nodename + "/apt/update",
|
||||
url: `/api2/json/nodes/${me.nodename}/apt/update`,
|
||||
},
|
||||
sorters: [
|
||||
{
|
||||
@ -65,6 +67,7 @@ Ext.define('Proxmox.node.APT', {
|
||||
},
|
||||
],
|
||||
});
|
||||
Proxmox.Utils.monStoreErrors(me, store, true);
|
||||
|
||||
let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
|
||||
groupHeaderTpl: '{[ "Origin: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
|
||||
@ -76,37 +79,24 @@ Ext.define('Proxmox.node.APT', {
|
||||
let headerCt = this.view.headerCt;
|
||||
let colspan = headerCt.getColumnCount();
|
||||
return {
|
||||
rowBody: '<div style="padding: 1em">' +
|
||||
Ext.String.htmlEncode(data.Description) +
|
||||
'</div>',
|
||||
rowBody: `<div style="padding: 1em">${Ext.htmlEncode(data.Description)}</div>`,
|
||||
rowBodyCls: me.full_description ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden',
|
||||
rowBodyColspan: colspan,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
let reload = function() {
|
||||
store.load();
|
||||
};
|
||||
|
||||
Proxmox.Utils.monStoreErrors(me, store, true);
|
||||
|
||||
let apt_command = function(cmd) {
|
||||
Proxmox.Utils.API2Request({
|
||||
url: "/nodes/" + me.nodename + "/apt/" + cmd,
|
||||
url: `/nodes/${me.nodename}/apt/${cmd}`,
|
||||
method: 'POST',
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
success: function(response, opts) {
|
||||
let upid = response.result.data;
|
||||
|
||||
let win = Ext.create('Proxmox.window.TaskViewer', {
|
||||
upid: upid,
|
||||
});
|
||||
win.show();
|
||||
me.mon(win, 'close', reload);
|
||||
},
|
||||
success: ({ result }) => Ext.create('Proxmox.window.TaskViewer', {
|
||||
autoShow: true,
|
||||
upid: result.data,
|
||||
listeners: {
|
||||
close: () => store.load(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
@ -114,20 +104,18 @@ Ext.define('Proxmox.node.APT', {
|
||||
|
||||
let update_btn = new Ext.Button({
|
||||
text: gettext('Refresh'),
|
||||
handler: function() {
|
||||
Proxmox.Utils.checked_command(function() { apt_command('update'); });
|
||||
},
|
||||
handler: () => Proxmox.Utils.checked_command(function() { apt_command('update'); }),
|
||||
});
|
||||
|
||||
let show_changelog = function(rec) {
|
||||
if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
|
||||
if (!rec?.data?.Package) {
|
||||
console.debug('cannot show changelog, missing Package', rec);
|
||||
return;
|
||||
}
|
||||
|
||||
let view = Ext.createWidget('component', {
|
||||
autoScroll: true,
|
||||
style: {
|
||||
'background-color': 'white',
|
||||
'white-space': 'pre',
|
||||
'font-family': 'monospace',
|
||||
padding: '5px',
|
||||
@ -137,7 +125,7 @@ Ext.define('Proxmox.node.APT', {
|
||||
let win = Ext.create('Ext.window.Window', {
|
||||
title: gettext('Changelog') + ": " + rec.data.Package,
|
||||
width: 800,
|
||||
height: 400,
|
||||
height: 600,
|
||||
layout: 'fit',
|
||||
modal: true,
|
||||
items: [view],
|
||||
@ -166,15 +154,8 @@ Ext.define('Proxmox.node.APT', {
|
||||
text: gettext('Changelog'),
|
||||
selModel: sm,
|
||||
disabled: true,
|
||||
enableFn: function(rec) {
|
||||
if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handler: function(b, e, rec) {
|
||||
show_changelog(rec);
|
||||
},
|
||||
enableFn: rec => !!rec?.data?.Package,
|
||||
handler: (b, e, rec) => show_changelog(rec),
|
||||
});
|
||||
|
||||
let verbose_desc_checkbox = new Ext.form.field.Checkbox({
|
||||
@ -201,14 +182,12 @@ Ext.define('Proxmox.node.APT', {
|
||||
selModel: sm,
|
||||
viewConfig: {
|
||||
stripeRows: false,
|
||||
emptyText: '<div style="display:table; width:100%; height:100%;"><div style="display:table-cell; vertical-align: middle; text-align:center;"><b>' + gettext('No updates available.') + '</div></div>',
|
||||
emptyText: `<div style="display:flex;justify-content:center;"><p>${gettext('No updates available.')}</p></div>`,
|
||||
},
|
||||
features: [groupingFeature, rowBodyFeature],
|
||||
listeners: {
|
||||
activate: reload,
|
||||
itemdblclick: function(v, rec) {
|
||||
show_changelog(rec);
|
||||
},
|
||||
activate: () => store.load(),
|
||||
itemdblclick: (v, rec) => show_changelog(rec),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
801
src/node/APTRepositories.js
Normal file
@ -0,0 +1,801 @@
|
||||
Ext.define('apt-repolist', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
'Path',
|
||||
'Index',
|
||||
'Origin',
|
||||
'FileType',
|
||||
'Enabled',
|
||||
'Comment',
|
||||
'Types',
|
||||
'URIs',
|
||||
'Suites',
|
||||
'Components',
|
||||
'Options',
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('Proxmox.window.APTRepositoryAdd', {
|
||||
extend: 'Proxmox.window.Edit',
|
||||
alias: 'widget.pmxAPTRepositoryAdd',
|
||||
|
||||
isCreate: true,
|
||||
isAdd: true,
|
||||
|
||||
subject: gettext('Repository'),
|
||||
width: 600,
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
if (!me.repoInfo || me.repoInfo.length === 0) {
|
||||
throw "repository information not initialized";
|
||||
}
|
||||
|
||||
let description = Ext.create('Ext.form.field.Display', {
|
||||
fieldLabel: gettext('Description'),
|
||||
name: 'description',
|
||||
});
|
||||
|
||||
let status = Ext.create('Ext.form.field.Display', {
|
||||
fieldLabel: gettext('Status'),
|
||||
name: 'status',
|
||||
renderer: function(value) {
|
||||
let statusText = gettext('Not yet configured');
|
||||
if (value !== '') {
|
||||
statusText = Ext.String.format(
|
||||
'{0}: {1}',
|
||||
gettext('Configured'),
|
||||
value ? gettext('enabled') : gettext('disabled'),
|
||||
);
|
||||
}
|
||||
|
||||
return statusText;
|
||||
},
|
||||
});
|
||||
|
||||
let repoSelector = Ext.create('Proxmox.form.KVComboBox', {
|
||||
fieldLabel: gettext('Repository'),
|
||||
xtype: 'proxmoxKVComboBox',
|
||||
name: 'handle',
|
||||
allowBlank: false,
|
||||
comboItems: me.repoInfo.map(info => [info.handle, info.name]),
|
||||
validator: function(renderedValue) {
|
||||
let handle = this.value;
|
||||
// we cannot use this.callParent in instantiations
|
||||
let valid = Proxmox.form.KVComboBox.prototype.validator.call(this, renderedValue);
|
||||
|
||||
if (!valid || !handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const info = me.repoInfo.find(elem => elem.handle === handle);
|
||||
if (!info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.status) {
|
||||
return Ext.String.format(gettext('{0} is already configured'), renderedValue);
|
||||
}
|
||||
return valid;
|
||||
},
|
||||
listeners: {
|
||||
change: function(f, value) {
|
||||
const info = me.repoInfo.find(elem => elem.handle === value);
|
||||
description.setValue(info.description);
|
||||
status.setValue(info.status);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
repoSelector.setValue(me.repoInfo[0].handle);
|
||||
|
||||
Ext.apply(me, {
|
||||
items: [
|
||||
repoSelector,
|
||||
description,
|
||||
status,
|
||||
],
|
||||
repoSelector: repoSelector,
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('Proxmox.node.APTRepositoriesErrors', {
|
||||
extend: 'Ext.grid.GridPanel',
|
||||
|
||||
xtype: 'proxmoxNodeAPTRepositoriesErrors',
|
||||
|
||||
store: {},
|
||||
|
||||
scrollable: true,
|
||||
|
||||
viewConfig: {
|
||||
stripeRows: false,
|
||||
getRowClass: (record) => {
|
||||
switch (record.data.status) {
|
||||
case 'warning': return 'proxmox-warning-row';
|
||||
case 'critical': return 'proxmox-invalid-row';
|
||||
default: return '';
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
hideHeaders: true,
|
||||
|
||||
columns: [
|
||||
{
|
||||
dataIndex: 'status',
|
||||
renderer: (value) => `<i class="fa fa-fw ${Proxmox.Utils.get_health_icon(value, true)}"></i>`,
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
dataIndex: 'message',
|
||||
flex: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('Proxmox.node.APTRepositoriesGrid', {
|
||||
extend: 'Ext.grid.GridPanel',
|
||||
xtype: 'proxmoxNodeAPTRepositoriesGrid',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
title: gettext('APT Repositories'),
|
||||
|
||||
cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
|
||||
|
||||
border: false,
|
||||
|
||||
tbar: [
|
||||
{
|
||||
text: gettext('Reload'),
|
||||
iconCls: 'fa fa-refresh',
|
||||
handler: function() {
|
||||
let me = this;
|
||||
me.up('proxmoxNodeAPTRepositories').reload();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: gettext('Add'),
|
||||
name: 'addRepo',
|
||||
disabled: true,
|
||||
repoInfo: undefined,
|
||||
cbind: {
|
||||
onlineHelp: '{onlineHelp}',
|
||||
},
|
||||
handler: function(button, event, record) {
|
||||
Proxmox.Utils.checked_command(() => {
|
||||
let me = this;
|
||||
let panel = me.up('proxmoxNodeAPTRepositories');
|
||||
|
||||
let extraParams = {};
|
||||
if (panel.digest !== undefined) {
|
||||
extraParams.digest = panel.digest;
|
||||
}
|
||||
|
||||
Ext.create('Proxmox.window.APTRepositoryAdd', {
|
||||
repoInfo: me.repoInfo,
|
||||
url: `/api2/extjs/nodes/${panel.nodename}/apt/repositories`,
|
||||
method: 'PUT',
|
||||
extraRequestParams: extraParams,
|
||||
onlineHelp: me.onlineHelp,
|
||||
listeners: {
|
||||
destroy: function() {
|
||||
panel.reload();
|
||||
},
|
||||
},
|
||||
}).show();
|
||||
});
|
||||
},
|
||||
},
|
||||
'-',
|
||||
{
|
||||
xtype: 'proxmoxAltTextButton',
|
||||
defaultText: gettext('Enable'),
|
||||
altText: gettext('Disable'),
|
||||
name: 'repoEnable',
|
||||
disabled: true,
|
||||
bind: {
|
||||
text: '{enableButtonText}',
|
||||
},
|
||||
handler: function(button, event, record) {
|
||||
let me = this;
|
||||
let panel = me.up('proxmoxNodeAPTRepositories');
|
||||
|
||||
let params = {
|
||||
path: record.data.Path,
|
||||
index: record.data.Index,
|
||||
enabled: record.data.Enabled ? 0 : 1, // invert
|
||||
};
|
||||
|
||||
if (panel.digest !== undefined) {
|
||||
params.digest = panel.digest;
|
||||
}
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
url: `/nodes/${panel.nodename}/apt/repositories`,
|
||||
method: 'POST',
|
||||
params: params,
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
panel.reload();
|
||||
},
|
||||
success: function(response, opts) {
|
||||
panel.reload();
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
sortableColumns: false,
|
||||
viewConfig: {
|
||||
stripeRows: false,
|
||||
getRowClass: (record, index) => record.get('Enabled') ? '' : 'proxmox-disabled-row',
|
||||
},
|
||||
|
||||
columns: [
|
||||
{
|
||||
header: gettext('Enabled'),
|
||||
dataIndex: 'Enabled',
|
||||
align: 'center',
|
||||
renderer: Proxmox.Utils.renderEnabledIcon,
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
header: gettext('Types'),
|
||||
dataIndex: 'Types',
|
||||
renderer: function(types, cell, record) {
|
||||
return types.join(' ');
|
||||
},
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
header: gettext('URIs'),
|
||||
dataIndex: 'URIs',
|
||||
renderer: function(uris, cell, record) {
|
||||
return uris.join(' ');
|
||||
},
|
||||
width: 350,
|
||||
},
|
||||
{
|
||||
header: gettext('Suites'),
|
||||
dataIndex: 'Suites',
|
||||
renderer: function(suites, metaData, record) {
|
||||
let err = '';
|
||||
if (record.data.warnings && record.data.warnings.length > 0) {
|
||||
let txt = [gettext('Warning')];
|
||||
record.data.warnings.forEach((warning) => {
|
||||
if (warning.property === 'Suites') {
|
||||
txt.push(Ext.htmlEncode(warning.message));
|
||||
}
|
||||
});
|
||||
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
|
||||
if (record.data.Enabled) {
|
||||
metaData.tdCls = 'proxmox-invalid-row';
|
||||
err = '<i class="fa fa-fw critical fa-exclamation-circle"></i> ';
|
||||
} else {
|
||||
metaData.tdCls = 'proxmox-warning-row';
|
||||
err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
|
||||
}
|
||||
}
|
||||
return suites.join(' ') + err;
|
||||
},
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
header: gettext('Components'),
|
||||
dataIndex: 'Components',
|
||||
renderer: function(components, metaData, record) {
|
||||
if (components === undefined) {
|
||||
return '';
|
||||
}
|
||||
let err = '';
|
||||
if (components.length === 1) {
|
||||
// FIXME: this should be a flag set to the actual repsotiories, i.e., a tristate
|
||||
// like production-ready = <yes|no|other> (Option<bool>)
|
||||
if (components[0].match(/\w+(-no-subscription|test)\s*$/i)) {
|
||||
metaData.tdCls = 'proxmox-warning-row';
|
||||
err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
|
||||
|
||||
let qtip = components[0].match(/no-subscription/)
|
||||
? gettext('The no-subscription repository is NOT production-ready')
|
||||
: gettext('The test repository may contain unstable updates')
|
||||
;
|
||||
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(qtip))}"`;
|
||||
}
|
||||
}
|
||||
return components.join(' ') + err;
|
||||
},
|
||||
width: 170,
|
||||
},
|
||||
{
|
||||
header: gettext('Options'),
|
||||
dataIndex: 'Options',
|
||||
renderer: function(options, cell, record) {
|
||||
if (!options) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let filetype = record.data.FileType;
|
||||
let text = '';
|
||||
|
||||
options.forEach(function(option) {
|
||||
let key = option.Key;
|
||||
if (filetype === 'list') {
|
||||
let values = option.Values.join(',');
|
||||
text += `${key}=${values} `;
|
||||
} else if (filetype === 'sources') {
|
||||
let values = option.Values.join(' ');
|
||||
text += `${key}: ${values}<br>`;
|
||||
} else {
|
||||
throw "unknown file type";
|
||||
}
|
||||
});
|
||||
return text;
|
||||
},
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: gettext('Origin'),
|
||||
dataIndex: 'Origin',
|
||||
width: 120,
|
||||
renderer: function(value, meta, rec) {
|
||||
if (typeof value !== 'string' || value.length === 0) {
|
||||
value = gettext('Other');
|
||||
}
|
||||
let cls = 'fa fa-fw fa-question-circle-o';
|
||||
let originType = this.up('proxmoxNodeAPTRepositories').classifyOrigin(value);
|
||||
if (originType === 'Proxmox') {
|
||||
cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
|
||||
} else if (originType === 'Debian') {
|
||||
cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
|
||||
}
|
||||
return `<i class='${cls}'></i> ${value}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext('Comment'),
|
||||
dataIndex: 'Comment',
|
||||
flex: 2,
|
||||
renderer: Ext.String.htmlEncode,
|
||||
},
|
||||
],
|
||||
|
||||
features: [
|
||||
{
|
||||
ftype: 'grouping',
|
||||
groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} repositor{[values.rows.length > 1 ? "ies" : "y"]})',
|
||||
enableGroupingMenu: false,
|
||||
},
|
||||
],
|
||||
|
||||
store: {
|
||||
model: 'apt-repolist',
|
||||
groupField: 'Path',
|
||||
sorters: [
|
||||
{
|
||||
property: 'Index',
|
||||
direction: 'ASC',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
if (!me.nodename) {
|
||||
throw "no node name specified";
|
||||
}
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('Proxmox.node.APTRepositories', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
xtype: 'proxmoxNodeAPTRepositories',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
digest: undefined,
|
||||
|
||||
onlineHelp: undefined,
|
||||
|
||||
product: 'Proxmox VE', // default
|
||||
|
||||
classifyOrigin: function(origin) {
|
||||
origin ||= '';
|
||||
if (origin.match(/^\s*Proxmox\s*$/i)) {
|
||||
return 'Proxmox';
|
||||
} else if (origin.match(/^\s*Debian\s*(:?Backports)?$/i)) {
|
||||
return 'Debian';
|
||||
}
|
||||
return 'Other';
|
||||
},
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
selectionChange: function(grid, selection) {
|
||||
let me = this;
|
||||
if (!selection || selection.length < 1) {
|
||||
return;
|
||||
}
|
||||
let rec = selection[0];
|
||||
let vm = me.getViewModel();
|
||||
vm.set('selectionenabled', rec.get('Enabled'));
|
||||
vm.notify();
|
||||
},
|
||||
|
||||
updateState: function() {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
|
||||
let store = vm.get('errorstore');
|
||||
store.removeAll();
|
||||
|
||||
let status = 'good'; // start with best, the helper below will downgrade if needed
|
||||
let text = gettext('All OK, you have production-ready repositories configured!');
|
||||
|
||||
let addGood = message => store.add({ status: 'good', message });
|
||||
let addWarn = (message, important) => {
|
||||
if (status !== 'critical') {
|
||||
status = 'warning';
|
||||
text = important ? message : gettext('Warning');
|
||||
}
|
||||
store.add({ status: 'warning', message });
|
||||
};
|
||||
let addCritical = (message, important) => {
|
||||
status = 'critical';
|
||||
text = important ? message : gettext('Error');
|
||||
store.add({ status: 'critical', message });
|
||||
};
|
||||
|
||||
let errors = vm.get('errors');
|
||||
errors.forEach(error => addCritical(`${error.path} - ${error.error}`));
|
||||
|
||||
let activeSubscription = vm.get('subscriptionActive');
|
||||
let enterprise = vm.get('enterpriseRepo');
|
||||
let nosubscription = vm.get('noSubscriptionRepo');
|
||||
let test = vm.get('testRepo');
|
||||
let cephRepos = {
|
||||
enterprise: vm.get('cephEnterpriseRepo'),
|
||||
nosubscription: vm.get('cephNoSubscriptionRepo'),
|
||||
test: vm.get('cephTestRepo'),
|
||||
};
|
||||
let wrongSuites = vm.get('suitesWarning');
|
||||
let mixedSuites = vm.get('mixedSuites');
|
||||
|
||||
if (!enterprise && !nosubscription && !test) {
|
||||
addCritical(
|
||||
Ext.String.format(gettext('No {0} repository is enabled, you do not get any updates!'), vm.get('product')),
|
||||
);
|
||||
} else if (errors.length > 0) {
|
||||
// nothing extra, just avoid that we show "get updates"
|
||||
} else if (enterprise && !nosubscription && !test && activeSubscription) {
|
||||
addGood(Ext.String.format(gettext('You get supported updates for {0}'), vm.get('product')));
|
||||
} else if (nosubscription || test) {
|
||||
addGood(Ext.String.format(gettext('You get updates for {0}'), vm.get('product')));
|
||||
}
|
||||
|
||||
if (wrongSuites) {
|
||||
addWarn(gettext('Some suites are misconfigured'));
|
||||
}
|
||||
|
||||
if (mixedSuites) {
|
||||
addWarn(gettext('Detected mixed suites before upgrade'));
|
||||
}
|
||||
|
||||
let productionReadyCheck = (repos, type, noSubAlternateName) => {
|
||||
if (!activeSubscription && repos.enterprise) {
|
||||
addWarn(Ext.String.format(
|
||||
gettext('The {0}enterprise repository is enabled, but there is no active subscription!'),
|
||||
type,
|
||||
));
|
||||
}
|
||||
|
||||
if (repos.nosubscription) {
|
||||
addWarn(Ext.String.format(
|
||||
gettext('The {0}no-subscription{1} repository is not recommended for production use!'),
|
||||
type,
|
||||
noSubAlternateName,
|
||||
));
|
||||
}
|
||||
|
||||
if (repos.test) {
|
||||
addWarn(Ext.String.format(
|
||||
gettext('The {0}test repository may pull in unstable updates and is not recommended for production use!'),
|
||||
type,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
productionReadyCheck({ enterprise, nosubscription, test }, '', '');
|
||||
// TODO drop alternate 'main' name when no longer relevant
|
||||
productionReadyCheck(cephRepos, 'Ceph ', '/main');
|
||||
|
||||
if (errors.length > 0) {
|
||||
text = gettext('Fatal parsing error for at least one repository');
|
||||
}
|
||||
|
||||
let iconCls = Proxmox.Utils.get_health_icon(status, true);
|
||||
|
||||
vm.set('state', {
|
||||
iconCls,
|
||||
text,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
viewModel: {
|
||||
data: {
|
||||
product: 'Proxmox VE', // default
|
||||
errors: [],
|
||||
suitesWarning: false,
|
||||
mixedSuites: false, // used before major upgrade
|
||||
subscriptionActive: '',
|
||||
noSubscriptionRepo: '',
|
||||
enterpriseRepo: '',
|
||||
testRepo: '',
|
||||
cephEnterpriseRepo: '',
|
||||
cephNoSubscriptionRepo: '',
|
||||
cephTestRepo: '',
|
||||
selectionenabled: false,
|
||||
state: {},
|
||||
},
|
||||
formulas: {
|
||||
enableButtonText: (get) => get('selectionenabled')
|
||||
? gettext('Disable') : gettext('Enable'),
|
||||
},
|
||||
stores: {
|
||||
errorstore: {
|
||||
fields: ['status', 'message'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
scrollable: true,
|
||||
layout: {
|
||||
type: 'vbox',
|
||||
align: 'stretch',
|
||||
},
|
||||
|
||||
items: [
|
||||
{
|
||||
xtype: 'panel',
|
||||
border: false,
|
||||
layout: {
|
||||
type: 'hbox',
|
||||
align: 'stretch',
|
||||
},
|
||||
height: 200,
|
||||
title: gettext('Status'),
|
||||
items: [
|
||||
{
|
||||
xtype: 'box',
|
||||
flex: 2,
|
||||
margin: 10,
|
||||
data: {
|
||||
iconCls: Proxmox.Utils.get_health_icon(undefined, true),
|
||||
text: '',
|
||||
},
|
||||
bind: {
|
||||
data: '{state}',
|
||||
},
|
||||
tpl: [
|
||||
'<center class="centered-flex-column" style="font-size:15px;line-height: 25px;">',
|
||||
'<i class="fa fa-4x {iconCls}"></i>',
|
||||
'{text}',
|
||||
'</center>',
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxNodeAPTRepositoriesErrors',
|
||||
name: 'repositoriesErrors',
|
||||
flex: 7,
|
||||
margin: 10,
|
||||
bind: {
|
||||
store: '{errorstore}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxNodeAPTRepositoriesGrid',
|
||||
name: 'repositoriesGrid',
|
||||
flex: 1,
|
||||
cbind: {
|
||||
nodename: '{nodename}',
|
||||
onlineHelp: '{onlineHelp}',
|
||||
},
|
||||
majorUpgradeAllowed: false, // TODO get release information from an API call?
|
||||
listeners: {
|
||||
selectionchange: 'selectionChange',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
check_subscription: function() {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
url: `/nodes/${me.nodename}/subscription`,
|
||||
method: 'GET',
|
||||
failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
||||
success: function(response, opts) {
|
||||
const res = response.result;
|
||||
const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
|
||||
vm.set('subscriptionActive', subscription);
|
||||
me.getController().updateState();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
updateStandardRepos: function(standardRepos) {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
|
||||
let addButton = me.down('button[name=addRepo]');
|
||||
|
||||
addButton.repoInfo = [];
|
||||
for (const standardRepo of standardRepos) {
|
||||
const handle = standardRepo.handle;
|
||||
const status = standardRepo.status;
|
||||
|
||||
if (handle === "enterprise") {
|
||||
vm.set('enterpriseRepo', status);
|
||||
} else if (handle === "no-subscription") {
|
||||
vm.set('noSubscriptionRepo', status);
|
||||
} else if (handle === 'test') {
|
||||
vm.set('testRepo', status);
|
||||
} else if (handle.match(/^ceph-[a-zA-Z]+-enterprise$/)) {
|
||||
vm.set('cephEnterpriseRepo', status);
|
||||
} else if (handle.match(/^ceph-[a-zA-Z]+-no-subscription$/)) {
|
||||
vm.set('cephNoSubscriptionRepo', status);
|
||||
} else if (handle.match(/^ceph-[a-zA-Z]+-test$/)) {
|
||||
vm.set('cephTestRepo', status);
|
||||
}
|
||||
me.getController().updateState();
|
||||
|
||||
addButton.repoInfo.push(standardRepo);
|
||||
addButton.digest = me.digest;
|
||||
}
|
||||
|
||||
addButton.setDisabled(false);
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
|
||||
|
||||
me.store.load(function(records, operation, success) {
|
||||
let gridData = [];
|
||||
let errors = [];
|
||||
let digest;
|
||||
let suitesWarning = false;
|
||||
|
||||
// Usually different suites will give errors anyways, but before a major upgrade the
|
||||
// current and the next suite are allowed, so it makes sense to check for mixed suites.
|
||||
let checkMixedSuites = false;
|
||||
let mixedSuites = false;
|
||||
|
||||
if (success && records.length > 0) {
|
||||
let data = records[0].data;
|
||||
let files = data.files;
|
||||
errors = data.errors;
|
||||
digest = data.digest;
|
||||
|
||||
let infos = {};
|
||||
for (const info of data.infos) {
|
||||
let path = info.path;
|
||||
let idx = info.index;
|
||||
|
||||
if (!infos[path]) {
|
||||
infos[path] = {};
|
||||
}
|
||||
if (!infos[path][idx]) {
|
||||
infos[path][idx] = {
|
||||
origin: '',
|
||||
warnings: [],
|
||||
// Used as a heuristic to detect mixed repositories pre-upgrade. The
|
||||
// warning is set on all repositories that do configure the next suite.
|
||||
gotIgnorePreUpgradeWarning: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (info.kind === 'origin') {
|
||||
infos[path][idx].origin = info.message;
|
||||
} else if (info.kind === 'warning') {
|
||||
infos[path][idx].warnings.push(info);
|
||||
} else if (info.kind === 'ignore-pre-upgrade-warning') {
|
||||
infos[path][idx].gotIgnorePreUpgradeWarning = true;
|
||||
if (!repoGrid.majorUpgradeAllowed) {
|
||||
infos[path][idx].warnings.push(info);
|
||||
} else {
|
||||
checkMixedSuites = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
files.forEach(function(file) {
|
||||
for (let n = 0; n < file.repositories.length; n++) {
|
||||
let repo = file.repositories[n];
|
||||
repo.Path = file.path;
|
||||
repo.Index = n;
|
||||
if (infos[file.path] && infos[file.path][n]) {
|
||||
repo.Origin = infos[file.path][n].origin || Proxmox.Utils.unknownText;
|
||||
repo.warnings = infos[file.path][n].warnings || [];
|
||||
|
||||
if (repo.Enabled) {
|
||||
if (repo.warnings.some(w => w.property === 'Suites')) {
|
||||
suitesWarning = true;
|
||||
}
|
||||
|
||||
let originType = me.classifyOrigin(repo.Origin);
|
||||
// Only Proxmox and Debian repositories checked here, because the
|
||||
// warning can be missing for others for a different reason (e.g.
|
||||
// using 'stable' or non-Debian code names).
|
||||
if (checkMixedSuites && repo.Types.includes('deb') &&
|
||||
(originType === 'Proxmox' || originType === 'Debian') &&
|
||||
!infos[file.path][n].gotIgnorePreUpgradeWarning
|
||||
) {
|
||||
mixedSuites = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
gridData.push(repo);
|
||||
}
|
||||
});
|
||||
|
||||
repoGrid.store.loadData(gridData);
|
||||
|
||||
me.updateStandardRepos(data['standard-repos']);
|
||||
}
|
||||
|
||||
me.digest = digest;
|
||||
|
||||
vm.set('errors', errors);
|
||||
vm.set('suitesWarning', suitesWarning);
|
||||
vm.set('mixedSuites', mixedSuites);
|
||||
me.getController().updateState();
|
||||
});
|
||||
|
||||
me.check_subscription();
|
||||
},
|
||||
|
||||
listeners: {
|
||||
activate: function() {
|
||||
let me = this;
|
||||
me.reload();
|
||||
},
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
if (!me.nodename) {
|
||||
throw "no node name specified";
|
||||
}
|
||||
|
||||
let store = Ext.create('Ext.data.Store', {
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
|
||||
},
|
||||
});
|
||||
|
||||
Ext.apply(me, { store: store });
|
||||
|
||||
Proxmox.Utils.monStoreErrors(me, me.store, true);
|
||||
|
||||
me.callParent();
|
||||
|
||||
me.getViewModel().set('product', me.product);
|
||||
},
|
||||
});
|
||||
@ -2,6 +2,10 @@ Ext.define('Proxmox.node.DNSEdit', {
|
||||
extend: 'Proxmox.window.Edit',
|
||||
alias: ['widget.proxmoxNodeDNSEdit'],
|
||||
|
||||
// Some longer existing APIs use a brittle "replace whole config" style, you can set this option
|
||||
// if the DNSEdit component is used in an API that has more modern, granular update semantics.
|
||||
deleteEmpty: false,
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
@ -21,6 +25,7 @@ Ext.define('Proxmox.node.DNSEdit', {
|
||||
fieldLabel: gettext('DNS server') + " 1",
|
||||
vtype: 'IP64Address',
|
||||
skipEmptyText: true,
|
||||
deleteEmpty: me.deleteEmpty,
|
||||
name: 'dns1',
|
||||
},
|
||||
{
|
||||
@ -28,6 +33,7 @@ Ext.define('Proxmox.node.DNSEdit', {
|
||||
fieldLabel: gettext('DNS server') + " 2",
|
||||
vtype: 'IP64Address',
|
||||
skipEmptyText: true,
|
||||
deleteEmpty: me.deleteEmpty,
|
||||
name: 'dns2',
|
||||
},
|
||||
{
|
||||
@ -35,6 +41,7 @@ Ext.define('Proxmox.node.DNSEdit', {
|
||||
fieldLabel: gettext('DNS server') + " 3",
|
||||
vtype: 'IP64Address',
|
||||
skipEmptyText: true,
|
||||
deleteEmpty: me.deleteEmpty,
|
||||
name: 'dns3',
|
||||
},
|
||||
];
|
||||
|
||||
@ -2,6 +2,10 @@ Ext.define('Proxmox.node.DNSView', {
|
||||
extend: 'Proxmox.grid.ObjectGrid',
|
||||
alias: ['widget.proxmoxNodeDNSView'],
|
||||
|
||||
// Some longer existing APIs use a brittle "replace whole config" style, you can set this option
|
||||
// if the DNSView component is used in an API that has more modern, granular update semantics.
|
||||
deleteEmpty: false,
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
@ -9,21 +13,20 @@ Ext.define('Proxmox.node.DNSView', {
|
||||
throw "no node name specified";
|
||||
}
|
||||
|
||||
let run_editor = function() {
|
||||
let win = Ext.create('Proxmox.node.DNSEdit', {
|
||||
nodename: me.nodename,
|
||||
});
|
||||
win.show();
|
||||
};
|
||||
let run_editor = () => Ext.create('Proxmox.node.DNSEdit', {
|
||||
autoShow: true,
|
||||
nodename: me.nodename,
|
||||
deleteEmpty: me.deleteEmpty,
|
||||
});
|
||||
|
||||
Ext.apply(me, {
|
||||
url: "/api2/json/nodes/" + me.nodename + "/dns",
|
||||
url: `/api2/json/nodes/${me.nodename}/dns`,
|
||||
cwidth1: 130,
|
||||
interval: 1000,
|
||||
interval: 10 * 1000,
|
||||
run_editor: run_editor,
|
||||
rows: {
|
||||
search: {
|
||||
header: 'Search domain',
|
||||
header: gettext('Search domain'),
|
||||
required: true,
|
||||
renderer: Ext.htmlEncode,
|
||||
},
|
||||
|
||||
@ -2,6 +2,9 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
||||
extend: 'Proxmox.window.Edit',
|
||||
alias: ['widget.proxmoxNodeNetworkEdit'],
|
||||
|
||||
// Enable to show the VLAN ID field
|
||||
enableBridgeVlanIds: false,
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
@ -57,22 +60,75 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
||||
}
|
||||
|
||||
if (me.iftype === 'bridge') {
|
||||
let vlanIdsField = !me.enableBridgeVlanIds ? undefined : Ext.create('Ext.form.field.Text', {
|
||||
fieldLabel: gettext('VLAN IDs'),
|
||||
name: 'bridge_vids',
|
||||
emptyText: '2-4094',
|
||||
disabled: true,
|
||||
autoEl: {
|
||||
tag: 'div',
|
||||
'data-qtip': gettext("List of VLAN IDs and ranges, useful for NICs with restricted VLAN offloading support. For example: '2 4 100-200'"),
|
||||
},
|
||||
validator: function(value) {
|
||||
if (!value) { // empty
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const vid of value.split(/\s+[,;]?/)) {
|
||||
if (!vid) {
|
||||
continue;
|
||||
}
|
||||
let res = vid.match(/^(\d+)(?:-(\d+))?$/);
|
||||
if (!res) {
|
||||
return Ext.String.format(gettext("not a valid bridge VLAN ID entry: {0}"), vid);
|
||||
}
|
||||
let start = Number(res[1]), end = Number(res[2] ?? res[1]); // end=start for single IDs
|
||||
|
||||
if (Number.isNaN(start) || Number.isNaN(end)) {
|
||||
return Ext.String.format(gettext('VID range includes not-a-number: {0}'), vid);
|
||||
} else if (start > end) {
|
||||
return Ext.String.format(gettext('VID range must go from lower to higher tag: {0}'), vid);
|
||||
} else if (start < 2 || end > 4094) { // check just one each, we already ensured start < end
|
||||
return Ext.String.format(gettext('VID range outside of allowed 2 and 4094 limit: {0}'), vid);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
column2.push({
|
||||
xtype: 'proxmoxcheckbox',
|
||||
fieldLabel: gettext('VLAN aware'),
|
||||
name: 'bridge_vlan_aware',
|
||||
deleteEmpty: !me.isCreate,
|
||||
listeners: {
|
||||
change: function(f, newVal) {
|
||||
if (vlanIdsField) {
|
||||
vlanIdsField.setDisabled(!newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
column2.push({
|
||||
xtype: 'textfield',
|
||||
fieldLabel: gettext('Bridge ports'),
|
||||
name: 'bridge_ports',
|
||||
autoEl: {
|
||||
tag: 'div',
|
||||
'data-qtip': gettext('Space-separated list of interfaces, for example: enp0s0 enp1s0'),
|
||||
},
|
||||
});
|
||||
if (vlanIdsField) {
|
||||
advancedColumn2.push(vlanIdsField);
|
||||
}
|
||||
} else if (me.iftype === 'OVSBridge') {
|
||||
column2.push({
|
||||
xtype: 'textfield',
|
||||
fieldLabel: gettext('Bridge ports'),
|
||||
name: 'ovs_ports',
|
||||
autoEl: {
|
||||
tag: 'div',
|
||||
'data-qtip': gettext('Space-separated list of interfaces, for example: enp0s0 enp1s0'),
|
||||
},
|
||||
});
|
||||
column2.push({
|
||||
xtype: 'textfield',
|
||||
@ -89,7 +145,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
||||
name: 'ovs_bridge',
|
||||
});
|
||||
column2.push({
|
||||
xtype: 'pveVlanField',
|
||||
xtype: 'proxmoxvlanfield',
|
||||
deleteEmpty: !me.isCreate,
|
||||
name: 'ovs_tag',
|
||||
value: '',
|
||||
@ -132,7 +188,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
||||
});
|
||||
|
||||
column2.push({
|
||||
xtype: 'pveVlanField',
|
||||
xtype: 'proxmoxvlanfield',
|
||||
name: 'vlan-id',
|
||||
value: me.vlanidvalue,
|
||||
disabled: me.disablevlanid,
|
||||
@ -203,7 +259,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
||||
name: 'ovs_bridge',
|
||||
});
|
||||
column2.push({
|
||||
xtype: 'pveVlanField',
|
||||
xtype: 'proxmoxvlanfield',
|
||||
deleteEmpty: !me.isCreate,
|
||||
name: 'ovs_tag',
|
||||
value: '',
|
||||
@ -246,6 +302,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
||||
value: me.iface,
|
||||
vtype: iface_vtype,
|
||||
allowBlank: false,
|
||||
maxLength: iface_vtype === 'BridgeName' ? 10 : 15,
|
||||
autoEl: {
|
||||
tag: 'div',
|
||||
'data-qtip': gettext('For example, vmbr0.100, vmbr0, vlan0.100, vlan0'),
|
||||
|
||||
@ -16,6 +16,8 @@ Ext.define('proxmox-networks', {
|
||||
'netmask6',
|
||||
'slaves',
|
||||
'type',
|
||||
'vlan-id',
|
||||
'vlan-raw-device',
|
||||
],
|
||||
idProperty: 'iface',
|
||||
});
|
||||
@ -31,6 +33,9 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
|
||||
showApplyBtn: false,
|
||||
|
||||
// for options passed down to the network edit window
|
||||
editOptions: {},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
@ -38,7 +43,7 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
throw "no node name specified";
|
||||
}
|
||||
|
||||
let baseUrl = '/nodes/' + me.nodename + '/network';
|
||||
let baseUrl = `/nodes/${me.nodename}/network`;
|
||||
|
||||
let store = Ext.create('Ext.data.Store', {
|
||||
model: 'proxmox-networks',
|
||||
@ -93,13 +98,16 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
return;
|
||||
}
|
||||
|
||||
let win = Ext.create('Proxmox.node.NetworkEdit', {
|
||||
Ext.create('Proxmox.node.NetworkEdit', {
|
||||
autoShow: true,
|
||||
nodename: me.nodename,
|
||||
iface: rec.data.iface,
|
||||
iftype: rec.data.type,
|
||||
...me.editOptions,
|
||||
listeners: {
|
||||
destroy: () => reload(),
|
||||
},
|
||||
});
|
||||
win.show();
|
||||
win.on('destroy', reload);
|
||||
};
|
||||
|
||||
let edit_btn = new Ext.Button({
|
||||
@ -108,31 +116,12 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
handler: run_editor,
|
||||
});
|
||||
|
||||
let del_btn = new Ext.Button({
|
||||
text: gettext('Remove'),
|
||||
disabled: true,
|
||||
handler: function() {
|
||||
let grid = me.down('gridpanel');
|
||||
let sm = grid.getSelectionModel();
|
||||
let rec = sm.getSelection()[0];
|
||||
if (!rec) {
|
||||
return;
|
||||
}
|
||||
let sm = Ext.create('Ext.selection.RowModel', {});
|
||||
|
||||
let iface = rec.data.iface;
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
url: baseUrl + '/' + iface,
|
||||
method: 'DELETE',
|
||||
waitMsgTarget: me,
|
||||
callback: function() {
|
||||
reload();
|
||||
},
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
});
|
||||
},
|
||||
let del_btn = new Proxmox.button.StdRemoveButton({
|
||||
selModel: sm,
|
||||
getUrl: ({ data }) => `${baseUrl}/${data.iface}`,
|
||||
callback: () => reload(),
|
||||
});
|
||||
|
||||
let apply_btn = Ext.create('Proxmox.button.Button', {
|
||||
@ -146,102 +135,64 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
url: baseUrl,
|
||||
method: 'PUT',
|
||||
waitMsgTarget: me,
|
||||
success: function(response, opts) {
|
||||
let upid = response.result.data;
|
||||
|
||||
let win = Ext.create('Proxmox.window.TaskProgress', {
|
||||
success: function({ result }, opts) {
|
||||
Ext.create('Proxmox.window.TaskProgress', {
|
||||
autoShow: true,
|
||||
taskDone: reload,
|
||||
upid: upid,
|
||||
upid: result.data,
|
||||
});
|
||||
win.show();
|
||||
},
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let set_button_status = function() {
|
||||
let grid = me.down('gridpanel');
|
||||
let sm = grid.getSelectionModel();
|
||||
let rec = sm.getSelection()[0];
|
||||
|
||||
edit_btn.setDisabled(!rec);
|
||||
del_btn.setDisabled(!rec);
|
||||
};
|
||||
|
||||
let render_ports = function(value, metaData, record) {
|
||||
if (value === 'bridge') {
|
||||
return record.data.bridge_ports;
|
||||
} else if (value === 'bond') {
|
||||
return record.data.slaves;
|
||||
} else if (value === 'OVSBridge') {
|
||||
return record.data.ovs_ports;
|
||||
} else if (value === 'OVSBond') {
|
||||
return record.data.ovs_bonds;
|
||||
let findNextFreeInterfaceId = function(prefix) {
|
||||
for (let next = 0; next <= 9999; next++) {
|
||||
let id = `${prefix}${next.toString()}`;
|
||||
if (!store.getById(id)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
Ext.Msg.alert('Error', `No free ID for ${prefix} found!`);
|
||||
return '';
|
||||
};
|
||||
|
||||
let find_next_iface_id = function(prefix) {
|
||||
let next;
|
||||
for (next = 0; next <= 9999; next++) {
|
||||
if (!store.getById(prefix + next.toString())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return prefix + next.toString();
|
||||
let menu_items = [];
|
||||
let addEditWindowToMenu = (iType, iDefault) => {
|
||||
menu_items.push({
|
||||
text: Proxmox.Utils.render_network_iface_type(iType),
|
||||
handler: () => Ext.create('Proxmox.node.NetworkEdit', {
|
||||
autoShow: true,
|
||||
nodename: me.nodename,
|
||||
iftype: iType,
|
||||
iface_default: findNextFreeInterfaceId(iDefault ?? iType),
|
||||
...me.editOptions,
|
||||
onlineHelp: 'sysadmin_network_configuration',
|
||||
listeners: {
|
||||
destroy: () => reload(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
let menu_items = [];
|
||||
|
||||
if (me.types.indexOf('bridge') !== -1) {
|
||||
menu_items.push({
|
||||
text: Proxmox.Utils.render_network_iface_type('bridge'),
|
||||
handler: function() {
|
||||
let win = Ext.create('Proxmox.node.NetworkEdit', {
|
||||
nodename: me.nodename,
|
||||
iftype: 'bridge',
|
||||
iface_default: find_next_iface_id('vmbr'),
|
||||
onlineHelp: 'sysadmin_network_configuration',
|
||||
});
|
||||
win.on('destroy', reload);
|
||||
win.show();
|
||||
},
|
||||
});
|
||||
addEditWindowToMenu('bridge', 'vmbr');
|
||||
}
|
||||
|
||||
if (me.types.indexOf('bond') !== -1) {
|
||||
menu_items.push({
|
||||
text: Proxmox.Utils.render_network_iface_type('bond'),
|
||||
handler: function() {
|
||||
let win = Ext.create('Proxmox.node.NetworkEdit', {
|
||||
nodename: me.nodename,
|
||||
iftype: 'bond',
|
||||
iface_default: find_next_iface_id('bond'),
|
||||
onlineHelp: 'sysadmin_network_configuration',
|
||||
});
|
||||
win.on('destroy', reload);
|
||||
win.show();
|
||||
},
|
||||
});
|
||||
addEditWindowToMenu('bond');
|
||||
}
|
||||
|
||||
if (me.types.indexOf('vlan') !== -1) {
|
||||
menu_items.push({
|
||||
text: Proxmox.Utils.render_network_iface_type('vlan'),
|
||||
handler: function() {
|
||||
let win = Ext.create('Proxmox.node.NetworkEdit', {
|
||||
nodename: me.nodename,
|
||||
iftype: 'vlan',
|
||||
iface_default: find_next_iface_id('vlan'),
|
||||
onlineHelp: 'sysadmin_network_configuration',
|
||||
});
|
||||
win.on('destroy', reload);
|
||||
win.show();
|
||||
},
|
||||
});
|
||||
addEditWindowToMenu('vlan');
|
||||
}
|
||||
|
||||
if (me.types.indexOf('ovs') !== -1) {
|
||||
@ -249,43 +200,20 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
menu_items.push({ xtype: 'menuseparator' });
|
||||
}
|
||||
|
||||
menu_items.push(
|
||||
{
|
||||
text: Proxmox.Utils.render_network_iface_type('OVSBridge'),
|
||||
handler: function() {
|
||||
let win = Ext.create('Proxmox.node.NetworkEdit', {
|
||||
nodename: me.nodename,
|
||||
iftype: 'OVSBridge',
|
||||
iface_default: find_next_iface_id('vmbr'),
|
||||
});
|
||||
win.on('destroy', reload);
|
||||
win.show();
|
||||
addEditWindowToMenu('OVSBridge', 'vmbr');
|
||||
addEditWindowToMenu('OVSBond', 'bond');
|
||||
|
||||
menu_items.push({
|
||||
text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
|
||||
handler: () => Ext.create('Proxmox.node.NetworkEdit', {
|
||||
autoShow: true,
|
||||
nodename: me.nodename,
|
||||
iftype: 'OVSIntPort',
|
||||
listeners: {
|
||||
destroy: () => reload(),
|
||||
},
|
||||
},
|
||||
{
|
||||
text: Proxmox.Utils.render_network_iface_type('OVSBond'),
|
||||
handler: function() {
|
||||
let win = Ext.create('Proxmox.node.NetworkEdit', {
|
||||
nodename: me.nodename,
|
||||
iftype: 'OVSBond',
|
||||
iface_default: find_next_iface_id('bond'),
|
||||
});
|
||||
win.on('destroy', reload);
|
||||
win.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
|
||||
handler: function() {
|
||||
let win = Ext.create('Proxmox.node.NetworkEdit', {
|
||||
nodename: me.nodename,
|
||||
iftype: 'OVSIntPort',
|
||||
});
|
||||
win.on('destroy', reload);
|
||||
win.show();
|
||||
},
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
let renderer_generator = function(fieldname) {
|
||||
@ -322,9 +250,7 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
callback: function() {
|
||||
reload();
|
||||
},
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
||||
});
|
||||
},
|
||||
},
|
||||
@ -339,6 +265,7 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
stateful: true,
|
||||
stateId: 'grid-node-network',
|
||||
store: store,
|
||||
selModel: sm,
|
||||
region: 'center',
|
||||
border: false,
|
||||
columns: [
|
||||
@ -387,7 +314,18 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
{
|
||||
header: gettext('Ports/Slaves'),
|
||||
dataIndex: 'type',
|
||||
renderer: render_ports,
|
||||
renderer: (value, metaData, { data }) => {
|
||||
if (value === 'bridge') {
|
||||
return data.bridge_ports;
|
||||
} else if (value === 'bond') {
|
||||
return data.slaves;
|
||||
} else if (value === 'OVSBridge') {
|
||||
return data.ovs_ports;
|
||||
} else if (value === 'OVSBond') {
|
||||
return data.ovs_bonds;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext('Bond Mode'),
|
||||
@ -429,6 +367,24 @@ Ext.define('Proxmox.node.NetworkView', {
|
||||
dataIndex: 'gateway',
|
||||
renderer: renderer_generator('gateway'),
|
||||
},
|
||||
{
|
||||
header: gettext('VLAN ID'),
|
||||
hidden: true,
|
||||
sortable: true,
|
||||
dataIndex: 'vlan-id',
|
||||
},
|
||||
{
|
||||
header: gettext('VLAN raw device'),
|
||||
hidden: true,
|
||||
sortable: true,
|
||||
dataIndex: 'vlan-raw-device',
|
||||
},
|
||||
{
|
||||
header: 'MTU',
|
||||
hidden: true,
|
||||
sortable: true,
|
||||
dataIndex: 'mtu',
|
||||
},
|
||||
{
|
||||
header: gettext('Comment'),
|
||||
dataIndex: 'comments',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
Ext.define('proxmox-services', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: ['service', 'name', 'desc', 'state'],
|
||||
fields: ['service', 'name', 'desc', 'state', 'unit-state', 'active-state'],
|
||||
idProperty: 'service',
|
||||
});
|
||||
|
||||
@ -24,11 +24,13 @@ Ext.define('Proxmox.node.ServiceView', {
|
||||
interval: 1000,
|
||||
model: 'proxmox-services',
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: "/api2/json/nodes/" + me.nodename + "/services",
|
||||
type: 'proxmox',
|
||||
url: `/api2/json/nodes/${me.nodename}/services`,
|
||||
},
|
||||
});
|
||||
|
||||
let filterInstalledOnly = record => record.get('unit-state') !== 'not-found';
|
||||
|
||||
let store = Ext.create('Proxmox.data.DiffStore', {
|
||||
rstore: rstore,
|
||||
sortAfterUpdate: true,
|
||||
@ -38,30 +40,45 @@ Ext.define('Proxmox.node.ServiceView', {
|
||||
direction: 'ASC',
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
filterInstalledOnly,
|
||||
],
|
||||
});
|
||||
|
||||
let unHideCB = Ext.create('Ext.form.field.Checkbox', {
|
||||
boxLabel: gettext('Show only installed services'),
|
||||
value: true,
|
||||
boxLabelAlign: 'before',
|
||||
listeners: {
|
||||
change: function(_cb, value) {
|
||||
if (value) {
|
||||
store.addFilter([filterInstalledOnly]);
|
||||
} else {
|
||||
store.clearFilter();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let view_service_log = function() {
|
||||
let sm = me.getSelectionModel();
|
||||
let rec = sm.getSelection()[0];
|
||||
let win = Ext.create('Ext.window.Window', {
|
||||
title: gettext('Syslog') + ': ' + rec.data.service,
|
||||
let { data: { service } } = me.getSelectionModel().getSelection()[0];
|
||||
Ext.create('Ext.window.Window', {
|
||||
title: gettext('Syslog') + ': ' + service,
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 400,
|
||||
layout: 'fit',
|
||||
items: {
|
||||
xtype: 'proxmoxLogView',
|
||||
url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" +
|
||||
rec.data.service,
|
||||
url: `/api2/extjs/nodes/${me.nodename}/syslog?service=${service}`,
|
||||
log_select_timespan: 1,
|
||||
},
|
||||
autoShow: true,
|
||||
});
|
||||
win.show();
|
||||
};
|
||||
|
||||
let service_cmd = function(cmd) {
|
||||
let rec = me.getSelectionModel().getSelection()[0];
|
||||
let service = rec.data.service;
|
||||
let { data: { service } } = me.getSelectionModel().getSelection()[0];
|
||||
Proxmox.Utils.API2Request({
|
||||
url: `/nodes/${me.nodename}/services/${service}/${cmd}`,
|
||||
method: 'POST',
|
||||
@ -71,12 +88,10 @@ Ext.define('Proxmox.node.ServiceView', {
|
||||
},
|
||||
success: function(response, opts) {
|
||||
rstore.startUpdate();
|
||||
let upid = response.result.data;
|
||||
|
||||
let win = Ext.create('Proxmox.window.TaskProgress', {
|
||||
upid: upid,
|
||||
Ext.create('Proxmox.window.TaskProgress', {
|
||||
upid: response.result.data,
|
||||
autoShow: true,
|
||||
});
|
||||
win.show();
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -84,27 +99,18 @@ Ext.define('Proxmox.node.ServiceView', {
|
||||
let start_btn = new Ext.Button({
|
||||
text: gettext('Start'),
|
||||
disabled: true,
|
||||
handler: function() {
|
||||
service_cmd("start");
|
||||
},
|
||||
handler: () => service_cmd("start"),
|
||||
});
|
||||
|
||||
let stop_btn = new Ext.Button({
|
||||
text: gettext('Stop'),
|
||||
disabled: true,
|
||||
handler: function() {
|
||||
service_cmd("stop");
|
||||
},
|
||||
handler: () => service_cmd("stop"),
|
||||
});
|
||||
|
||||
let restart_btn = new Ext.Button({
|
||||
text: gettext('Restart'),
|
||||
disabled: true,
|
||||
handler: function() {
|
||||
service_cmd(me.restartCommand || "restart");
|
||||
},
|
||||
handler: () => service_cmd(me.restartCommand || "restart"),
|
||||
});
|
||||
|
||||
let syslog_btn = new Ext.Button({
|
||||
text: gettext('Syslog'),
|
||||
disabled: true,
|
||||
@ -124,23 +130,28 @@ Ext.define('Proxmox.node.ServiceView', {
|
||||
}
|
||||
let service = rec.data.service;
|
||||
let state = rec.data.state;
|
||||
let unit = rec.data['unit-state'];
|
||||
|
||||
syslog_btn.enable();
|
||||
|
||||
if (state === 'running') {
|
||||
if (me.startOnlyServices[service]) {
|
||||
stop_btn.disable();
|
||||
restart_btn.enable();
|
||||
} else {
|
||||
stop_btn.enable();
|
||||
restart_btn.enable();
|
||||
start_btn.disable();
|
||||
}
|
||||
} else if (unit !== undefined && (unit === 'masked' || unit === 'unknown' || unit === 'not-found')) {
|
||||
start_btn.disable();
|
||||
restart_btn.enable();
|
||||
restart_btn.disable();
|
||||
stop_btn.disable();
|
||||
} else {
|
||||
start_btn.enable();
|
||||
stop_btn.disable();
|
||||
restart_btn.disable();
|
||||
}
|
||||
if (!me.startOnlyServices[service]) {
|
||||
if (state === 'running') {
|
||||
stop_btn.enable();
|
||||
} else {
|
||||
stop_btn.disable();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
me.mon(store, 'refresh', set_button_status);
|
||||
@ -148,9 +159,36 @@ Ext.define('Proxmox.node.ServiceView', {
|
||||
Proxmox.Utils.monStoreErrors(me, rstore);
|
||||
|
||||
Ext.apply(me, {
|
||||
viewConfig: {
|
||||
trackOver: false,
|
||||
stripeRows: false, // does not work with getRowClass()
|
||||
getRowClass: function(record, index) {
|
||||
let unitState = record.get('unit-state');
|
||||
if (!unitState) {
|
||||
return '';
|
||||
}
|
||||
if (unitState === 'masked' || unitState === 'not-found') {
|
||||
return "proxmox-disabled-row";
|
||||
} else if (unitState === 'unknown') {
|
||||
if (record.get('name') === 'syslog') {
|
||||
return "proxmox-disabled-row"; // replaced by journal on most hosts
|
||||
}
|
||||
return "proxmox-warning-row";
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
store: store,
|
||||
stateful: false,
|
||||
tbar: [start_btn, stop_btn, restart_btn, syslog_btn],
|
||||
tbar: [
|
||||
start_btn,
|
||||
stop_btn,
|
||||
restart_btn,
|
||||
'-',
|
||||
syslog_btn,
|
||||
'->',
|
||||
unHideCB,
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
header: gettext('Name'),
|
||||
@ -163,6 +201,30 @@ Ext.define('Proxmox.node.ServiceView', {
|
||||
width: 100,
|
||||
sortable: true,
|
||||
dataIndex: 'state',
|
||||
renderer: (value, meta, rec) => {
|
||||
const unitState = rec.get('unit-state');
|
||||
if (unitState === 'masked') {
|
||||
return gettext('disabled');
|
||||
} else if (unitState === 'not-found') {
|
||||
return gettext('not installed');
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext('Active'),
|
||||
width: 100,
|
||||
sortable: true,
|
||||
hidden: true,
|
||||
dataIndex: 'active-state',
|
||||
},
|
||||
{
|
||||
header: gettext('Unit'),
|
||||
width: 120,
|
||||
sortable: true,
|
||||
hidden: !Ext.Array.contains(['PVEAuthCookie', 'PBSAuthCookie'], Proxmox?.Setup?.auth_cookie_name),
|
||||
dataIndex: 'unit-state',
|
||||
},
|
||||
{
|
||||
header: gettext('Description'),
|
||||
|
||||
@ -1,217 +1,515 @@
|
||||
Ext.define('Proxmox.node.Tasks', {
|
||||
extend: 'Ext.grid.GridPanel',
|
||||
|
||||
alias: ['widget.proxmoxNodeTasks'],
|
||||
alias: 'widget.proxmoxNodeTasks',
|
||||
|
||||
stateful: true,
|
||||
stateId: 'grid-node-tasks',
|
||||
stateId: 'pve-grid-node-tasks',
|
||||
|
||||
loadMask: true,
|
||||
sortableColumns: false,
|
||||
vmidFilter: 0,
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
// set extra filter components, must have a 'name' property for the parameter, and must
|
||||
// trigger a 'change' event if the value is 'undefined', it will not be sent to the api
|
||||
extraFilter: [],
|
||||
|
||||
if (!me.nodename) {
|
||||
throw "no node name specified";
|
||||
}
|
||||
|
||||
let store = Ext.create('Ext.data.BufferedStore', {
|
||||
pageSize: 500,
|
||||
autoLoad: true,
|
||||
remoteFilter: true,
|
||||
model: 'proxmox-tasks',
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
startParam: 'start',
|
||||
limitParam: 'limit',
|
||||
url: "/api2/json/nodes/" + me.nodename + "/tasks",
|
||||
},
|
||||
});
|
||||
// fixed filters which cannot be changed after instantiation, for example:
|
||||
// { vmid: 100 }
|
||||
preFilter: {},
|
||||
|
||||
store.on('prefetch', function() {
|
||||
// we want to update the scrollbar on every store load
|
||||
// since the total count might be different
|
||||
// the buffered grid plugin does this only on scrolling itself
|
||||
// and even reduces the scrollheight again when scrolling up
|
||||
me.updateLayout();
|
||||
});
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
let userfilter = '';
|
||||
let filter_errors = 0;
|
||||
|
||||
let updateProxyParams = function() {
|
||||
let params = {
|
||||
errors: filter_errors,
|
||||
};
|
||||
if (userfilter) {
|
||||
params.userfilter = userfilter;
|
||||
}
|
||||
if (me.vmidFilter) {
|
||||
params.vmid = me.vmidFilter;
|
||||
}
|
||||
store.proxy.extraParams = params;
|
||||
};
|
||||
|
||||
updateProxyParams();
|
||||
|
||||
let reload_task = Ext.create('Ext.util.DelayedTask', function() {
|
||||
updateProxyParams();
|
||||
store.reload();
|
||||
});
|
||||
|
||||
let run_task_viewer = function() {
|
||||
let sm = me.getSelectionModel();
|
||||
let rec = sm.getSelection()[0];
|
||||
if (!rec) {
|
||||
showTaskLog: function() {
|
||||
let me = this;
|
||||
let selection = me.getView().getSelection();
|
||||
if (selection.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let win = Ext.create('Proxmox.window.TaskViewer', {
|
||||
let rec = selection[0];
|
||||
|
||||
Ext.create('Proxmox.window.TaskViewer', {
|
||||
upid: rec.data.upid,
|
||||
endtime: rec.data.endtime,
|
||||
}).show();
|
||||
},
|
||||
|
||||
updateLayout: function(store, records, success, operation) {
|
||||
let me = this;
|
||||
let view = me.getView().getView(); // the table view, not the whole grid
|
||||
Proxmox.Utils.setErrorMask(view, false);
|
||||
// update the scrollbar on every store load since the total count might be different.
|
||||
// the buffered grid plugin does this only on (user) scrolling itself and even reduces
|
||||
// the scrollheight again when scrolling up
|
||||
me.getView().updateLayout();
|
||||
|
||||
if (!success) {
|
||||
Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
|
||||
let selection = view.getSelection();
|
||||
let store = me.getViewModel().get('bufferedstore');
|
||||
if (selection && selection.length > 0) {
|
||||
// deselect if selection is not there anymore
|
||||
if (!store.contains(selection[0])) {
|
||||
view.setSelection(undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
sinceChange: function(field, newval) {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
|
||||
vm.set('since', newval);
|
||||
},
|
||||
|
||||
untilChange: function(field, newval, oldval) {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
|
||||
vm.set('until', newval);
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
view.getStore().load();
|
||||
},
|
||||
|
||||
showFilter: function(btn, pressed) {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
vm.set('showFilter', pressed);
|
||||
},
|
||||
|
||||
clearFilter: function() {
|
||||
let me = this;
|
||||
me.lookup('filtertoolbar').query('field').forEach((field) => {
|
||||
field.setValue(undefined);
|
||||
});
|
||||
win.show();
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
let view_btn = new Ext.Button({
|
||||
text: gettext('View'),
|
||||
disabled: true,
|
||||
handler: run_task_viewer,
|
||||
});
|
||||
listeners: {
|
||||
itemdblclick: 'showTaskLog',
|
||||
},
|
||||
|
||||
Proxmox.Utils.monStoreErrors(me, store, true);
|
||||
viewModel: {
|
||||
data: {
|
||||
typefilter: '',
|
||||
statusfilter: '',
|
||||
showFilter: false,
|
||||
extraFilter: {},
|
||||
since: null,
|
||||
until: null,
|
||||
},
|
||||
|
||||
Ext.apply(me, {
|
||||
store: store,
|
||||
viewConfig: {
|
||||
trackOver: false,
|
||||
stripeRows: false, // does not work with getRowClass()
|
||||
formulas: {
|
||||
filterIcon: (get) => 'fa fa-filter' + (get('showFilter') ? ' info-blue' : ''),
|
||||
extraParams: function(get) {
|
||||
let me = this;
|
||||
let params = {};
|
||||
if (get('typefilter')) {
|
||||
params.typefilter = get('typefilter');
|
||||
}
|
||||
if (get('statusfilter')) {
|
||||
params.statusfilter = get('statusfilter');
|
||||
}
|
||||
|
||||
getRowClass: function(record, index) {
|
||||
let status = record.get('status');
|
||||
|
||||
if (status) {
|
||||
let parsed = Proxmox.Utils.parse_task_status(status);
|
||||
if (parsed === 'error') {
|
||||
return "proxmox-invalid-row";
|
||||
} else if (parsed === 'warning') {
|
||||
return "proxmox-warning-row";
|
||||
if (get('extraFilter')) {
|
||||
let extraFilter = get('extraFilter');
|
||||
for (const [name, value] of Object.entries(extraFilter)) {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
params[name] = value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
if (get('since')) {
|
||||
params.since = get('since').valueOf()/1000;
|
||||
}
|
||||
|
||||
if (get('until')) {
|
||||
let until = new Date(get('until').getTime()); // copy object
|
||||
until.setDate(until.getDate() + 1); // end of the day
|
||||
params.until = until.valueOf()/1000;
|
||||
}
|
||||
|
||||
me.getView().getStore().load();
|
||||
|
||||
return params;
|
||||
},
|
||||
filterCount: function(get) {
|
||||
let count = 0;
|
||||
if (get('typefilter')) {
|
||||
count++;
|
||||
}
|
||||
let status = get('statusfilter');
|
||||
if ((Ext.isArray(status) && status.length > 0) ||
|
||||
(!Ext.isArray(status) && status)) {
|
||||
count++;
|
||||
}
|
||||
if (get('since')) {
|
||||
count++;
|
||||
}
|
||||
if (get('until')) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (get('extraFilter')) {
|
||||
let preFilter = get('preFilter') || {};
|
||||
let extraFilter = get('extraFilter');
|
||||
for (const [name, value] of Object.entries(extraFilter)) {
|
||||
if (value !== undefined && value !== null && value !== "" &&
|
||||
preFilter[name] === undefined
|
||||
) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
},
|
||||
clearFilterText: function(get) {
|
||||
let count = get('filterCount');
|
||||
let fieldMsg = '';
|
||||
if (count > 1) {
|
||||
fieldMsg = ` (${count} ${gettext('Fields')})`;
|
||||
} else if (count > 0) {
|
||||
fieldMsg = ` (1 ${gettext('Field')})`;
|
||||
}
|
||||
return gettext('Clear Filter') + fieldMsg;
|
||||
},
|
||||
},
|
||||
|
||||
stores: {
|
||||
bufferedstore: {
|
||||
type: 'buffered',
|
||||
pageSize: 500,
|
||||
autoLoad: true,
|
||||
remoteFilter: true,
|
||||
model: 'proxmox-tasks',
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
startParam: 'start',
|
||||
limitParam: 'limit',
|
||||
extraParams: '{extraParams}',
|
||||
url: '{url}',
|
||||
},
|
||||
listeners: {
|
||||
prefetch: 'updateLayout',
|
||||
refresh: 'refresh',
|
||||
},
|
||||
},
|
||||
tbar: [
|
||||
view_btn,
|
||||
},
|
||||
},
|
||||
|
||||
bind: {
|
||||
store: '{bufferedstore}',
|
||||
},
|
||||
|
||||
dockedItems: [
|
||||
{
|
||||
xtype: 'toolbar',
|
||||
items: [
|
||||
{
|
||||
text: gettext('Refresh'), // FIXME: smart-auto-refresh store
|
||||
handler: () => store.reload(),
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('View'),
|
||||
iconCls: 'fa fa-window-restore',
|
||||
disabled: true,
|
||||
handler: 'showTaskLog',
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
text: gettext('Reload'),
|
||||
iconCls: 'fa fa-refresh',
|
||||
handler: 'reload',
|
||||
},
|
||||
'->',
|
||||
gettext('User name') +':',
|
||||
' ',
|
||||
{
|
||||
xtype: 'textfield',
|
||||
width: 200,
|
||||
value: userfilter,
|
||||
enableKeyEvents: true,
|
||||
xtype: 'button',
|
||||
bind: {
|
||||
text: '{clearFilterText}',
|
||||
disabled: '{!filterCount}',
|
||||
},
|
||||
text: gettext('Clear Filter'),
|
||||
enabled: false,
|
||||
handler: 'clearFilter',
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
enableToggle: true,
|
||||
bind: {
|
||||
iconCls: '{filterIcon}',
|
||||
},
|
||||
text: gettext('Filter'),
|
||||
stateful: true,
|
||||
stateId: 'task-showfilter',
|
||||
stateEvents: ['toggle'],
|
||||
applyState: function(state) {
|
||||
if (state.pressed !== undefined) {
|
||||
this.setPressed(state.pressed);
|
||||
}
|
||||
},
|
||||
getState: function() {
|
||||
return {
|
||||
pressed: this.pressed,
|
||||
};
|
||||
},
|
||||
listeners: {
|
||||
keyup: function(field, e) {
|
||||
userfilter = field.getValue();
|
||||
reload_task.delay(500);
|
||||
},
|
||||
},
|
||||
}, ' ', gettext('Only Errors') + ':', ' ',
|
||||
{
|
||||
xtype: 'checkbox',
|
||||
hideLabel: true,
|
||||
checked: filter_errors,
|
||||
listeners: {
|
||||
change: function(field, checked) {
|
||||
filter_errors = checked ? 1 : 0;
|
||||
reload_task.delay(10);
|
||||
},
|
||||
},
|
||||
}, ' ',
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
header: gettext("Start Time"),
|
||||
dataIndex: 'starttime',
|
||||
width: 130,
|
||||
renderer: function(value) {
|
||||
return Ext.Date.format(value, "M d H:i:s");
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext("End Time"),
|
||||
dataIndex: 'endtime',
|
||||
width: 130,
|
||||
renderer: function(value, metaData, record) {
|
||||
if (!value) {
|
||||
metaData.tdCls = "x-grid-row-loading";
|
||||
return '';
|
||||
}
|
||||
return Ext.Date.format(value, "M d H:i:s");
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext("Duration"),
|
||||
hidden: true,
|
||||
width: 80,
|
||||
renderer: function(value, metaData, record) {
|
||||
let start = record.data.starttime;
|
||||
if (start) {
|
||||
let end = record.data.endtime || Date.now();
|
||||
let duration = end - start;
|
||||
if (duration > 0) {
|
||||
duration /= 1000;
|
||||
}
|
||||
return Proxmox.Utils.format_duration_human(duration);
|
||||
}
|
||||
return Proxmox.Utils.unknownText;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext("Node"),
|
||||
dataIndex: 'node',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
header: gettext("User name"),
|
||||
dataIndex: 'user',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
header: gettext("Description"),
|
||||
dataIndex: 'upid',
|
||||
flex: 1,
|
||||
renderer: Proxmox.Utils.render_upid,
|
||||
},
|
||||
{
|
||||
header: gettext("Status"),
|
||||
dataIndex: 'status',
|
||||
width: 200,
|
||||
renderer: function(value, metaData, record) {
|
||||
if (value === undefined && !record.data.endtime) {
|
||||
metaData.tdCls = "x-grid-row-loading";
|
||||
return '';
|
||||
}
|
||||
|
||||
return Proxmox.Utils.format_task_status(value);
|
||||
toggle: 'showFilter',
|
||||
},
|
||||
},
|
||||
],
|
||||
listeners: {
|
||||
itemdblclick: run_task_viewer,
|
||||
selectionchange: function(v, selections) {
|
||||
view_btn.setDisabled(!(selections && selections[0]));
|
||||
},
|
||||
show: function() { reload_task.delay(10); },
|
||||
destroy: function() { reload_task.cancel(); },
|
||||
},
|
||||
{
|
||||
xtype: 'toolbar',
|
||||
dock: 'top',
|
||||
reference: 'filtertoolbar',
|
||||
layout: {
|
||||
type: 'hbox',
|
||||
align: 'top',
|
||||
},
|
||||
});
|
||||
bind: {
|
||||
hidden: '{!showFilter}',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
xtype: 'container',
|
||||
padding: 10,
|
||||
layout: {
|
||||
type: 'vbox',
|
||||
align: 'stretch',
|
||||
},
|
||||
defaults: {
|
||||
labelWidth: 80,
|
||||
},
|
||||
// cannot bind the values directly, as it then changes also
|
||||
// on blur, causing wrong reloads of the store
|
||||
items: [
|
||||
{
|
||||
xtype: 'datefield',
|
||||
fieldLabel: gettext('Since'),
|
||||
format: 'Y-m-d',
|
||||
bind: {
|
||||
maxValue: '{until}',
|
||||
},
|
||||
listeners: {
|
||||
change: 'sinceChange',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'datefield',
|
||||
fieldLabel: gettext('Until'),
|
||||
format: 'Y-m-d',
|
||||
bind: {
|
||||
minValue: '{since}',
|
||||
},
|
||||
listeners: {
|
||||
change: 'untilChange',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'container',
|
||||
padding: 10,
|
||||
layout: {
|
||||
type: 'vbox',
|
||||
align: 'stretch',
|
||||
},
|
||||
defaults: {
|
||||
labelWidth: 80,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
xtype: 'pmxTaskTypeSelector',
|
||||
fieldLabel: gettext('Task Type'),
|
||||
emptyText: gettext('All'),
|
||||
bind: {
|
||||
value: '{typefilter}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'combobox',
|
||||
fieldLabel: gettext('Task Result'),
|
||||
emptyText: gettext('All'),
|
||||
multiSelect: true,
|
||||
store: [
|
||||
['ok', gettext('OK')],
|
||||
['unknown', Proxmox.Utils.unknownText],
|
||||
['warning', gettext('Warnings')],
|
||||
['error', gettext('Errors')],
|
||||
],
|
||||
bind: {
|
||||
value: '{statusfilter}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
viewConfig: {
|
||||
trackOver: false,
|
||||
stripeRows: false, // does not work with getRowClass()
|
||||
emptyText: gettext('No Tasks found'),
|
||||
|
||||
getRowClass: function(record, index) {
|
||||
let status = record.get('status');
|
||||
|
||||
if (status) {
|
||||
let parsed = Proxmox.Utils.parse_task_status(status);
|
||||
if (parsed === 'error') {
|
||||
return "proxmox-invalid-row";
|
||||
} else if (parsed === 'warning') {
|
||||
return "proxmox-warning-row";
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
||||
columns: [
|
||||
{
|
||||
header: gettext("Start Time"),
|
||||
dataIndex: 'starttime',
|
||||
width: 130,
|
||||
renderer: function(value) {
|
||||
return Ext.Date.format(value, "M d H:i:s");
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext("End Time"),
|
||||
dataIndex: 'endtime',
|
||||
width: 130,
|
||||
renderer: function(value, metaData, record) {
|
||||
if (!value) {
|
||||
metaData.tdCls = "x-grid-row-loading";
|
||||
return '';
|
||||
}
|
||||
return Ext.Date.format(value, "M d H:i:s");
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext("Duration"),
|
||||
hidden: true,
|
||||
width: 80,
|
||||
renderer: function(value, metaData, record) {
|
||||
let start = record.data.starttime;
|
||||
if (start) {
|
||||
let end = record.data.endtime || Date.now();
|
||||
let duration = end - start;
|
||||
if (duration > 0) {
|
||||
duration /= 1000;
|
||||
}
|
||||
return Proxmox.Utils.format_duration_human(duration);
|
||||
}
|
||||
return Proxmox.Utils.unknownText;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext("User name"),
|
||||
dataIndex: 'user',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
header: gettext("Description"),
|
||||
dataIndex: 'upid',
|
||||
flex: 1,
|
||||
renderer: Proxmox.Utils.render_upid,
|
||||
},
|
||||
{
|
||||
header: gettext("Status"),
|
||||
dataIndex: 'status',
|
||||
width: 200,
|
||||
renderer: function(value, metaData, record) {
|
||||
if (value === undefined && !record.data.endtime) {
|
||||
metaData.tdCls = "x-grid-row-loading";
|
||||
return '';
|
||||
}
|
||||
|
||||
return Proxmox.Utils.format_task_status(value);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
initComponent: function() {
|
||||
const me = this;
|
||||
|
||||
let nodename = me.nodename || 'localhost';
|
||||
let url = me.url || `/api2/json/nodes/${nodename}/tasks`;
|
||||
me.getViewModel().set('url', url);
|
||||
|
||||
let updateExtraFilters = function(name, value) {
|
||||
let vm = me.getViewModel();
|
||||
let extraFilter = Ext.clone(vm.get('extraFilter'));
|
||||
extraFilter[name] = value;
|
||||
vm.set('extraFilter', extraFilter);
|
||||
};
|
||||
|
||||
for (const [name, value] of Object.entries(me.preFilter)) {
|
||||
updateExtraFilters(name, value);
|
||||
}
|
||||
|
||||
me.getViewModel().set('preFilter', me.preFilter);
|
||||
|
||||
me.callParent();
|
||||
|
||||
let addFields = function(items) {
|
||||
me.lookup('filtertoolbar').add({
|
||||
xtype: 'container',
|
||||
padding: 10,
|
||||
layout: {
|
||||
type: 'vbox',
|
||||
align: 'stretch',
|
||||
},
|
||||
defaults: {
|
||||
labelWidth: 80,
|
||||
},
|
||||
items,
|
||||
});
|
||||
};
|
||||
|
||||
// start with a userfilter
|
||||
me.extraFilter = [
|
||||
{
|
||||
xtype: 'textfield',
|
||||
fieldLabel: gettext('User name'),
|
||||
changeOptions: {
|
||||
buffer: 500,
|
||||
},
|
||||
name: 'userfilter',
|
||||
},
|
||||
...me.extraFilter,
|
||||
];
|
||||
let items = [];
|
||||
for (const filterTemplate of me.extraFilter) {
|
||||
let filter = Ext.clone(filterTemplate);
|
||||
|
||||
filter.listeners = filter.listeners || {};
|
||||
filter.listeners.change = Ext.apply(filter.changeOptions || {}, {
|
||||
fn: function(field, value) {
|
||||
updateExtraFilters(filter.name, value);
|
||||
},
|
||||
});
|
||||
|
||||
items.push(filter);
|
||||
if (items.length === 2) {
|
||||
addFields(items);
|
||||
items = [];
|
||||
}
|
||||
}
|
||||
|
||||
addFields(items);
|
||||
},
|
||||
});
|
||||
|
||||
@ -9,21 +9,19 @@ Ext.define('Proxmox.node.TimeView', {
|
||||
throw "no node name specified";
|
||||
}
|
||||
|
||||
let tzoffset = new Date().getTimezoneOffset()*60000;
|
||||
let renderlocaltime = function(value) {
|
||||
let servertime = new Date((value * 1000) + tzoffset);
|
||||
let tzOffset = new Date().getTimezoneOffset() * 60000;
|
||||
let renderLocaltime = function(value) {
|
||||
let servertime = new Date((value * 1000) + tzOffset);
|
||||
return Ext.Date.format(servertime, 'Y-m-d H:i:s');
|
||||
};
|
||||
|
||||
let run_editor = function() {
|
||||
let win = Ext.create('Proxmox.node.TimeEdit', {
|
||||
nodename: me.nodename,
|
||||
});
|
||||
win.show();
|
||||
};
|
||||
let run_editor = () => Ext.create('Proxmox.node.TimeEdit', {
|
||||
autoShow: true,
|
||||
nodename: me.nodename,
|
||||
});
|
||||
|
||||
Ext.apply(me, {
|
||||
url: "/api2/json/nodes/" + me.nodename + "/time",
|
||||
url: `/api2/json/nodes/${me.nodename}/time`,
|
||||
cwidth1: 150,
|
||||
interval: 1000,
|
||||
run_editor: run_editor,
|
||||
@ -35,7 +33,7 @@ Ext.define('Proxmox.node.TimeView', {
|
||||
localtime: {
|
||||
header: gettext('Server time'),
|
||||
required: true,
|
||||
renderer: renderlocaltime,
|
||||
renderer: renderLocaltime,
|
||||
},
|
||||
},
|
||||
tbar: [
|
||||
|
||||
182
src/panel/AuthView.js
Normal file
@ -0,0 +1,182 @@
|
||||
Ext.define('Proxmox.panel.AuthView', {
|
||||
extend: 'Ext.grid.GridPanel',
|
||||
alias: 'widget.pmxAuthView',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
showDefaultRealm: false,
|
||||
|
||||
stateful: true,
|
||||
stateId: 'grid-authrealms',
|
||||
|
||||
viewConfig: {
|
||||
trackOver: false,
|
||||
},
|
||||
|
||||
baseUrl: '/access/domains',
|
||||
storeBaseUrl: '/access/domains',
|
||||
|
||||
columns: [
|
||||
{
|
||||
header: gettext('Realm'),
|
||||
width: 100,
|
||||
sortable: true,
|
||||
dataIndex: 'realm',
|
||||
},
|
||||
{
|
||||
header: gettext('Type'),
|
||||
width: 100,
|
||||
sortable: true,
|
||||
dataIndex: 'type',
|
||||
},
|
||||
{
|
||||
header: gettext('Default'),
|
||||
width: 80,
|
||||
sortable: true,
|
||||
dataIndex: 'default',
|
||||
renderer: isDefault => isDefault ? Proxmox.Utils.renderEnabledIcon(true) : '',
|
||||
align: 'center',
|
||||
cbind: {
|
||||
hidden: '{!showDefaultRealm}',
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext('Comment'),
|
||||
sortable: false,
|
||||
dataIndex: 'comment',
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 1,
|
||||
},
|
||||
],
|
||||
|
||||
openEditWindow: function(authType, realm) {
|
||||
let me = this;
|
||||
const { useTypeInUrl, onlineHelp } = Proxmox.Schema.authDomains[authType];
|
||||
|
||||
Ext.create('Proxmox.window.AuthEditBase', {
|
||||
baseUrl: me.baseUrl,
|
||||
useTypeInUrl,
|
||||
onlineHelp,
|
||||
authType,
|
||||
realm,
|
||||
showDefaultRealm: me.showDefaultRealm,
|
||||
listeners: {
|
||||
destroy: () => me.reload(),
|
||||
},
|
||||
}).show();
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
let me = this;
|
||||
me.getStore().load();
|
||||
},
|
||||
|
||||
run_editor: function() {
|
||||
let me = this;
|
||||
let rec = me.getSelection()[0];
|
||||
if (!rec) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Proxmox.Schema.authDomains[rec.data.type].edit) {
|
||||
return;
|
||||
}
|
||||
|
||||
me.openEditWindow(rec.data.type, rec.data.realm);
|
||||
},
|
||||
|
||||
open_sync_window: function() {
|
||||
let rec = this.getSelection()[0];
|
||||
if (!rec) {
|
||||
return;
|
||||
}
|
||||
if (!Proxmox.Schema.authDomains[rec.data.type].sync) {
|
||||
return;
|
||||
}
|
||||
Ext.create('Proxmox.window.SyncWindow', {
|
||||
type: rec.data.type,
|
||||
realm: rec.data.realm,
|
||||
listeners: {
|
||||
destroy: () => this.reload(),
|
||||
},
|
||||
}).show();
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
|
||||
me.store = {
|
||||
model: 'pmx-domains',
|
||||
sorters: {
|
||||
property: 'realm',
|
||||
direction: 'ASC',
|
||||
},
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: `/api2/json${me.storeBaseUrl}`,
|
||||
},
|
||||
};
|
||||
|
||||
let menuitems = [];
|
||||
for (const [authType, config] of Object.entries(Proxmox.Schema.authDomains).sort()) {
|
||||
if (!config.add) { continue; }
|
||||
menuitems.push({
|
||||
text: config.name,
|
||||
iconCls: 'fa fa-fw ' + (config.iconCls || 'fa-address-book-o'),
|
||||
handler: () => me.openEditWindow(authType),
|
||||
});
|
||||
}
|
||||
|
||||
let tbar = [
|
||||
{
|
||||
text: gettext('Add'),
|
||||
menu: {
|
||||
items: menuitems,
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Edit'),
|
||||
disabled: true,
|
||||
enableFn: (rec) => Proxmox.Schema.authDomains[rec.data.type].edit,
|
||||
handler: () => me.run_editor(),
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxStdRemoveButton',
|
||||
getUrl: (rec) => {
|
||||
let url = me.baseUrl;
|
||||
if (Proxmox.Schema.authDomains[rec.data.type].useTypeInUrl) {
|
||||
url += `/${rec.get('type')}`;
|
||||
}
|
||||
url += `/${rec.getId()}`;
|
||||
return url;
|
||||
},
|
||||
enableFn: (rec) => Proxmox.Schema.authDomains[rec.data.type].add,
|
||||
callback: () => me.reload(),
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Sync'),
|
||||
disabled: true,
|
||||
enableFn: (rec) => Proxmox.Schema.authDomains[rec.data.type].sync,
|
||||
handler: () => me.open_sync_window(),
|
||||
},
|
||||
];
|
||||
|
||||
if (me.extraButtons) {
|
||||
tbar.push('-');
|
||||
for (const button of me.extraButtons) {
|
||||
tbar.push(button);
|
||||
}
|
||||
}
|
||||
|
||||
Ext.apply(me, {
|
||||
tbar,
|
||||
listeners: {
|
||||
activate: () => me.reload(),
|
||||
itemdblclick: () => me.run_editor(),
|
||||
},
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
@ -85,7 +85,7 @@ Ext.define('Proxmox.panel.Certificates', {
|
||||
url: `/api2/extjs/${url}?restart=1`,
|
||||
method: 'DELETE',
|
||||
success: function(response, opt) {
|
||||
if (cert.reloadUid) {
|
||||
if (cert.reloadUi) {
|
||||
Ext.getBody().mask(
|
||||
gettext('API server will be restarted to use new certificates, please reload web-interface!'),
|
||||
['pve-static-mask'],
|
||||
@ -237,10 +237,16 @@ Ext.define('Proxmox.panel.Certificates', {
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Delete Custom Certificate'),
|
||||
confirmMsg: rec => Ext.String.format(
|
||||
gettext('Are you sure you want to remove the certificate used for {0}'),
|
||||
me.certById[rec.id].name,
|
||||
),
|
||||
confirmMsg: rec => {
|
||||
let cert = me.certById[rec.id];
|
||||
if (cert.name) {
|
||||
return Ext.String.format(
|
||||
gettext('Are you sure you want to remove the certificate used for {0}'),
|
||||
cert.name,
|
||||
);
|
||||
}
|
||||
return gettext('Are you sure you want to remove the certificate');
|
||||
},
|
||||
callback: () => me.reload(),
|
||||
selModel: me.selModel,
|
||||
disabled: true,
|
||||
|
||||
@ -35,7 +35,7 @@ Ext.define('pmx-disk-list', {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
'vendor', 'model', 'serial', 'rpm', 'type', 'wearout', 'health',
|
||||
'vendor', 'model', 'serial', 'rpm', 'type', 'wearout', 'health', 'mounted',
|
||||
],
|
||||
idProperty: 'devpath',
|
||||
});
|
||||
@ -44,6 +44,8 @@ Ext.define('Proxmox.DiskList', {
|
||||
extend: 'Ext.tree.Panel',
|
||||
alias: 'widget.pmxDiskList',
|
||||
|
||||
supportsWipeDisk: false,
|
||||
|
||||
rootVisible: false,
|
||||
|
||||
emptyText: gettext('No Disks found'),
|
||||
@ -97,18 +99,40 @@ Ext.define('Proxmox.DiskList', {
|
||||
waitMsgTarget: view,
|
||||
method: 'POST',
|
||||
params: { disk: rec.data.name },
|
||||
failure: function(response, options) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
||||
success: function(response, options) {
|
||||
var upid = response.result.data;
|
||||
var win = Ext.create('Proxmox.window.TaskProgress', {
|
||||
upid: upid,
|
||||
Ext.create('Proxmox.window.TaskProgress', {
|
||||
upid: response.result.data,
|
||||
taskDone: function() {
|
||||
me.reload();
|
||||
},
|
||||
autoShow: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
wipeDisk: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
let selection = view.getSelection();
|
||||
if (!selection || selection.length < 1) return;
|
||||
|
||||
let rec = selection[0];
|
||||
Proxmox.Utils.API2Request({
|
||||
url: `${view.exturl}/wipedisk`,
|
||||
waitMsgTarget: view,
|
||||
method: 'PUT',
|
||||
params: { disk: rec.data.name },
|
||||
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
||||
success: function(response, options) {
|
||||
Ext.create('Proxmox.window.TaskProgress', {
|
||||
upid: response.result.data,
|
||||
taskDone: function() {
|
||||
me.reload();
|
||||
},
|
||||
autoShow: true,
|
||||
});
|
||||
win.show();
|
||||
},
|
||||
});
|
||||
},
|
||||
@ -143,10 +167,19 @@ Ext.define('Proxmox.DiskList', {
|
||||
|
||||
for (const item of records) {
|
||||
let data = item.data;
|
||||
data.leaf = true;
|
||||
data.expanded = true;
|
||||
data.children = [];
|
||||
data.children = data.partitions ?? [];
|
||||
for (let p of data.children) {
|
||||
p['disk-type'] = 'partition';
|
||||
p.iconCls = 'fa fa-fw fa-hdd-o x-fa-tree';
|
||||
p.used = p.used === 'filesystem' ? p.filesystem : p.used;
|
||||
p.parent = data.devpath;
|
||||
p.children = [];
|
||||
p.leaf = true;
|
||||
}
|
||||
data.iconCls = 'fa fa-fw fa-hdd-o x-fa-tree';
|
||||
data.leaf = data.children.length === 0;
|
||||
|
||||
if (!data.parent) {
|
||||
disks[data.devpath] = data;
|
||||
}
|
||||
@ -187,7 +220,11 @@ Ext.define('Proxmox.DiskList', {
|
||||
let extendedInfo = '';
|
||||
if (rec) {
|
||||
let types = [];
|
||||
if (rec.data.osdid !== undefined && rec.data.osdid >= 0) {
|
||||
if (rec.data['osdid-list'] && rec.data['osdid-list'].length > 0) {
|
||||
for (const id of rec.data['osdid-list'].sort()) {
|
||||
types.push(`OSD.${id.toString()}`);
|
||||
}
|
||||
} else if (rec.data.osdid !== undefined && rec.data.osdid >= 0) {
|
||||
types.push(`OSD.${rec.data.osdid.toString()}`);
|
||||
}
|
||||
if (rec.data.journals > 0) {
|
||||
@ -203,45 +240,18 @@ Ext.define('Proxmox.DiskList', {
|
||||
extendedInfo = `, Ceph (${types.join(', ')})`;
|
||||
}
|
||||
}
|
||||
const formatMap = {
|
||||
'bios': 'BIOS boot',
|
||||
'zfsreserved': 'ZFS reserved',
|
||||
'efi': 'EFI',
|
||||
'lvm': 'LVM',
|
||||
'zfs': 'ZFS',
|
||||
};
|
||||
|
||||
v = formatMap[v] || v;
|
||||
return v ? `${v}${extendedInfo}` : Proxmox.Utils.noText;
|
||||
},
|
||||
|
||||
tbar: [
|
||||
{
|
||||
text: gettext('Reload'),
|
||||
handler: 'reload',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Show S.M.A.R.T. values'),
|
||||
parentXType: 'treepanel',
|
||||
disabled: true,
|
||||
enableFn: function(rec) {
|
||||
if (!rec || rec.data.parent) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
handler: 'openSmartWindow',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Initialize Disk with GPT'),
|
||||
parentXType: 'treepanel',
|
||||
disabled: true,
|
||||
enableFn: function(rec) {
|
||||
if (!rec || rec.data.parent ||
|
||||
(rec.data.used && rec.data.used !== 'unused')) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
handler: 'initGPT',
|
||||
},
|
||||
],
|
||||
|
||||
columns: [
|
||||
{
|
||||
xtype: 'treecolumn',
|
||||
@ -315,7 +325,14 @@ Ext.define('Proxmox.DiskList', {
|
||||
dataIndex: 'status',
|
||||
},
|
||||
{
|
||||
header: 'Wearout',
|
||||
header: gettext('Mounted'),
|
||||
width: 60,
|
||||
align: 'right',
|
||||
renderer: Proxmox.Utils.format_boolean,
|
||||
dataIndex: 'mounted',
|
||||
},
|
||||
{
|
||||
header: gettext('Wearout'),
|
||||
width: 90,
|
||||
sortable: true,
|
||||
align: 'right',
|
||||
@ -324,7 +341,7 @@ Ext.define('Proxmox.DiskList', {
|
||||
if (Ext.isNumeric(value)) {
|
||||
return (100 - value).toString() + '%';
|
||||
}
|
||||
return 'N/A';
|
||||
return gettext('N/A');
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -332,4 +349,91 @@ Ext.define('Proxmox.DiskList', {
|
||||
listeners: {
|
||||
itemdblclick: 'openSmartWindow',
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
let tbar = [
|
||||
{
|
||||
text: gettext('Reload'),
|
||||
handler: 'reload',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Show S.M.A.R.T. values'),
|
||||
parentXType: 'treepanel',
|
||||
disabled: true,
|
||||
enableFn: function(rec) {
|
||||
if (!rec || rec.data.parent) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
handler: 'openSmartWindow',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Initialize Disk with GPT'),
|
||||
parentXType: 'treepanel',
|
||||
disabled: true,
|
||||
enableFn: function(rec) {
|
||||
if (!rec || rec.data.parent ||
|
||||
(rec.data.used && rec.data.used !== 'unused')) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
handler: 'initGPT',
|
||||
},
|
||||
];
|
||||
|
||||
if (me.supportsWipeDisk) {
|
||||
tbar.push('-');
|
||||
tbar.push({
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Wipe Disk'),
|
||||
parentXType: 'treepanel',
|
||||
dangerous: true,
|
||||
confirmMsg: function(rec) {
|
||||
const data = rec.data;
|
||||
|
||||
let mainMessage = Ext.String.format(
|
||||
gettext('Are you sure you want to wipe {0}?'),
|
||||
data.devpath,
|
||||
);
|
||||
mainMessage += `<br> ${gettext('All data on the device will be lost!')}`;
|
||||
|
||||
const type = me.renderDiskType(data["disk-type"]);
|
||||
|
||||
let usage;
|
||||
if (data.children.length > 0) {
|
||||
const partitionUsage = data.children.map(
|
||||
partition => me.renderDiskUsage(partition.used),
|
||||
).join(', ');
|
||||
usage = `${gettext('Partitions')} (${partitionUsage})`;
|
||||
} else {
|
||||
usage = me.renderDiskUsage(data.used, undefined, rec);
|
||||
}
|
||||
|
||||
const size = Proxmox.Utils.format_size(data.size);
|
||||
const serial = Ext.String.htmlEncode(data.serial);
|
||||
|
||||
let additionalInfo = `${gettext('Type')}: ${type}<br>`;
|
||||
additionalInfo += `${gettext('Usage')}: ${usage}<br>`;
|
||||
additionalInfo += `${gettext('Size')}: ${size}<br>`;
|
||||
additionalInfo += `${gettext('Serial')}: ${serial}`;
|
||||
|
||||
return `${mainMessage}<br><br>${additionalInfo}`;
|
||||
},
|
||||
disabled: true,
|
||||
handler: 'wipeDisk',
|
||||
});
|
||||
}
|
||||
|
||||
me.tbar = tbar;
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
|
||||
45
src/panel/EOLNotice.js
Normal file
@ -0,0 +1,45 @@
|
||||
// not realy a panel descendant, but its the best (existing) place for this
|
||||
Ext.define('Proxmox.EOLNotice', {
|
||||
extend: 'Ext.Component',
|
||||
alias: 'widget.proxmoxEOLNotice',
|
||||
|
||||
userCls: 'eol-notice',
|
||||
padding: '0 5',
|
||||
|
||||
config: {
|
||||
product: '',
|
||||
version: '',
|
||||
eolDate: '',
|
||||
href: '',
|
||||
},
|
||||
|
||||
autoEl: {
|
||||
tag: 'div',
|
||||
'data-qtip': gettext("You won't get any security fixes after the End-Of-Life date. Please consider upgrading."),
|
||||
},
|
||||
|
||||
getIconCls: function() {
|
||||
let me = this;
|
||||
|
||||
const now = new Date();
|
||||
const eolDate = new Date(me.eolDate);
|
||||
const warningCutoff = new Date(eolDate.getTime() - (21 * 24 * 60 * 60 * 1000)); // 3 weeks
|
||||
|
||||
return now > warningCutoff ? 'critical fa-exclamation-triangle' : 'info-blue fa-info-circle';
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
let iconCls = me.getIconCls();
|
||||
let href = me.href.startsWith('http') ? me.href : `https://${me.href}`;
|
||||
let message = Ext.String.format(
|
||||
gettext('Support for {0} {1} ends on {2}'), me.product, me.version, me.eolDate);
|
||||
|
||||
me.html = `<i class="fa ${iconCls}"></i>
|
||||
<a href="${href}" target="_blank">${message} <i class="fa fa-external-link"></i></a>
|
||||
`;
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
91
src/panel/EmailRecipientPanel.js
Normal file
@ -0,0 +1,91 @@
|
||||
Ext.define('Proxmox.panel.EmailRecipientPanel', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
xtype: 'pmxEmailRecipientPanel',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
border: false,
|
||||
|
||||
mailValidator: function() {
|
||||
let mailto_user = this.down(`[name=mailto-user]`);
|
||||
let mailto = this.down(`[name=mailto]`);
|
||||
|
||||
if (!mailto_user.getValue()?.length && !mailto.getValue()) {
|
||||
return gettext('Either mailto or mailto-user must be set');
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
items: [
|
||||
{
|
||||
layout: 'anchor',
|
||||
border: false,
|
||||
cbind: {
|
||||
isCreate: '{isCreate}',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
xtype: 'pmxUserSelector',
|
||||
name: 'mailto-user',
|
||||
multiSelect: true,
|
||||
allowBlank: true,
|
||||
editable: false,
|
||||
skipEmptyText: true,
|
||||
fieldLabel: gettext('Recipient(s)'),
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
validator: function() {
|
||||
return this.up('pmxEmailRecipientPanel').mailValidator();
|
||||
},
|
||||
autoEl: {
|
||||
tag: 'div',
|
||||
'data-qtip': gettext('The notification will be sent to the user\'s configured mail address'),
|
||||
},
|
||||
listConfig: {
|
||||
width: 600,
|
||||
columns: [
|
||||
{
|
||||
header: gettext('User'),
|
||||
sortable: true,
|
||||
dataIndex: 'userid',
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: gettext('E-Mail'),
|
||||
sortable: true,
|
||||
dataIndex: 'email',
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: gettext('Comment'),
|
||||
sortable: false,
|
||||
dataIndex: 'comment',
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
fieldLabel: gettext('Additional Recipient(s)'),
|
||||
name: 'mailto',
|
||||
allowBlank: true,
|
||||
emptyText: 'user@example.com, ...',
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
autoEl: {
|
||||
tag: 'div',
|
||||
'data-qtip': gettext('Multiple recipients must be separated by spaces, commas or semicolons'),
|
||||
},
|
||||
validator: function() {
|
||||
return this.up('pmxEmailRecipientPanel').mailValidator();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -20,6 +20,8 @@ Ext.define('Proxmox.panel.GaugeWidget', {
|
||||
xtype: 'polar',
|
||||
height: 120,
|
||||
border: false,
|
||||
// set to '-' to suppress warning in debug mode
|
||||
downloadServerUrl: '-',
|
||||
itemId: 'chart',
|
||||
series: [{
|
||||
type: 'gauge',
|
||||
@ -59,6 +61,36 @@ Ext.define('Proxmox.panel.GaugeWidget', {
|
||||
|
||||
initialValue: 0,
|
||||
|
||||
checkThemeColors: function() {
|
||||
let me = this;
|
||||
let rootStyle = getComputedStyle(document.documentElement);
|
||||
|
||||
// get colors
|
||||
let panelBg = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff";
|
||||
let textColor = rootStyle.getPropertyValue("--pwt-text-color").trim() || "#000000";
|
||||
me.defaultColor = rootStyle.getPropertyValue("--pwt-gauge-default").trim() || '#c2ddf2';
|
||||
me.criticalColor = rootStyle.getPropertyValue("--pwt-gauge-crit").trim() || '#ff6c59';
|
||||
me.warningColor = rootStyle.getPropertyValue("--pwt-gauge-warn").trim() || '#fc0';
|
||||
me.backgroundColor = rootStyle.getPropertyValue("--pwt-gauge-back").trim() || '#f5f5f5';
|
||||
|
||||
// set gauge colors
|
||||
let value = me.chart.series[0].getValue() / 100;
|
||||
|
||||
let color = me.defaultColor;
|
||||
|
||||
if (value >= me.criticalThreshold) {
|
||||
color = me.criticalColor;
|
||||
} else if (value >= me.warningThreshold) {
|
||||
color = me.warningColor;
|
||||
}
|
||||
|
||||
me.chart.series[0].setColors([color, me.backgroundColor]);
|
||||
|
||||
// set text and background colors
|
||||
me.chart.setBackground(panelBg);
|
||||
me.valueSprite.setAttributes({ fillStyle: textColor }, true);
|
||||
me.chart.redraw();
|
||||
},
|
||||
|
||||
updateValue: function(value, text) {
|
||||
let me = this;
|
||||
@ -98,5 +130,20 @@ Ext.define('Proxmox.panel.GaugeWidget', {
|
||||
me.text = me.getComponent('text');
|
||||
me.chart = me.getComponent('chart');
|
||||
me.valueSprite = me.chart.getSurface('chart').get('valueSprite');
|
||||
|
||||
me.checkThemeColors();
|
||||
|
||||
// switch colors on media query changes
|
||||
me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
me.themeListener = (e) => { me.checkThemeColors(); };
|
||||
me.mediaQueryList.addEventListener("change", me.themeListener);
|
||||
},
|
||||
|
||||
doDestroy: function() {
|
||||
let me = this;
|
||||
|
||||
me.mediaQueryList.removeEventListener("change", me.themeListener);
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
|
||||
75
src/panel/GotifyEditPanel.js
Normal file
@ -0,0 +1,75 @@
|
||||
Ext.define('Proxmox.panel.GotifyEditPanel', {
|
||||
extend: 'Proxmox.panel.InputPanel',
|
||||
xtype: 'pmxGotifyEditPanel',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
onlineHelp: 'notification_targets_gotify',
|
||||
|
||||
type: 'gotify',
|
||||
|
||||
items: [
|
||||
{
|
||||
xtype: 'pmxDisplayEditField',
|
||||
name: 'name',
|
||||
cbind: {
|
||||
value: '{name}',
|
||||
editable: '{isCreate}',
|
||||
},
|
||||
fieldLabel: gettext('Endpoint Name'),
|
||||
allowBlank: false,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxcheckbox',
|
||||
name: 'enable',
|
||||
fieldLabel: gettext('Enable'),
|
||||
allowBlank: false,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
fieldLabel: gettext('Server URL'),
|
||||
name: 'server',
|
||||
allowBlank: false,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
inputType: 'password',
|
||||
fieldLabel: gettext('API Token'),
|
||||
name: 'token',
|
||||
cbind: {
|
||||
emptyText: get => !get('isCreate') ? gettext('Unchanged') : '',
|
||||
allowBlank: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
name: 'comment',
|
||||
fieldLabel: gettext('Comment'),
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
onSetValues: (values) => {
|
||||
values.enable = !values.disable;
|
||||
|
||||
delete values.disable;
|
||||
return values;
|
||||
},
|
||||
|
||||
onGetValues: function(values) {
|
||||
let me = this;
|
||||
|
||||
if (values.enable) {
|
||||
if (!me.isCreate) {
|
||||
Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
|
||||
}
|
||||
} else {
|
||||
values.disable = 1;
|
||||
}
|
||||
|
||||
delete values.enable;
|
||||
|
||||
return values;
|
||||
},
|
||||
});
|
||||
@ -59,7 +59,14 @@ Ext.define('Proxmox.widget.Info', {
|
||||
},
|
||||
|
||||
updateValue: function(text, usage) {
|
||||
var me = this;
|
||||
let me = this;
|
||||
|
||||
if (me.lastText === text && me.lastUsage === usage) {
|
||||
return;
|
||||
}
|
||||
me.lastText = text;
|
||||
me.lastUsage = usage;
|
||||
|
||||
var label = me.getComponent('label');
|
||||
label.update(Ext.apply(label.data, { title: me.title, usage: text }));
|
||||
|
||||
|
||||
@ -23,8 +23,7 @@ Ext.define('Proxmox.panel.InputPanel', {
|
||||
// will be set if the inputpanel has advanced items
|
||||
hasAdvanced: false,
|
||||
|
||||
// if the panel has advanced items,
|
||||
// this will determine if they are shown by default
|
||||
// if the panel has advanced items, this will determine if they are shown by default
|
||||
showAdvanced: false,
|
||||
|
||||
// overwrite this to modify submit data
|
||||
@ -58,12 +57,18 @@ Ext.define('Proxmox.panel.InputPanel', {
|
||||
}
|
||||
},
|
||||
|
||||
onSetValues: function(values) {
|
||||
return values;
|
||||
},
|
||||
|
||||
setValues: function(values) {
|
||||
let me = this;
|
||||
|
||||
let form = me.up('form');
|
||||
|
||||
Ext.iterate(values, function(fieldId, val) {
|
||||
values = me.onSetValues(values);
|
||||
|
||||
Ext.iterate(values, function(fieldId, val) {
|
||||
let fields = me.query('[isFormField][name=' + fieldId + ']');
|
||||
for (const field of fields) {
|
||||
if (field) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Display log entries in a panel with scrollbar
|
||||
* The log entries are automatically refreshed via a background task,
|
||||
* with newest entries comming at the bottom
|
||||
* with newest entries coming at the bottom
|
||||
*/
|
||||
Ext.define('Proxmox.panel.JournalView', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
@ -77,6 +77,8 @@ Ext.define('Proxmox.panel.JournalView', {
|
||||
let num = lines.length;
|
||||
let text = lines.map(Ext.htmlEncode).join('<br>');
|
||||
|
||||
let contentChanged = true;
|
||||
|
||||
if (!livemode) {
|
||||
if (num) {
|
||||
view.content = text;
|
||||
@ -89,6 +91,8 @@ Ext.define('Proxmox.panel.JournalView', {
|
||||
view.content = view.content ? text + '<br>' + view.content : text;
|
||||
} else if (!top && num) {
|
||||
view.content = view.content ? view.content + '<br>' + text : text;
|
||||
} else {
|
||||
contentChanged = false;
|
||||
}
|
||||
|
||||
// update cursors
|
||||
@ -101,7 +105,9 @@ Ext.define('Proxmox.panel.JournalView', {
|
||||
}
|
||||
}
|
||||
|
||||
contentEl.update(view.content);
|
||||
if (contentChanged) {
|
||||
contentEl.update(view.content);
|
||||
}
|
||||
|
||||
me.updateScroll(livemode, num, scrollPos, scrollPosTop);
|
||||
},
|
||||
@ -137,6 +143,9 @@ Ext.define('Proxmox.panel.JournalView', {
|
||||
waitMsgTarget: !livemode ? view : undefined,
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
if (me.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
Proxmox.Utils.setErrorMask(me, false);
|
||||
let lines = response.result.data;
|
||||
me.updateView(lines, livemode, top);
|
||||
@ -192,7 +201,6 @@ Ext.define('Proxmox.panel.JournalView', {
|
||||
|
||||
view.loadTask = new Ext.util.DelayedTask(me.doLoad, me, [true, false]);
|
||||
|
||||
me.updateParams();
|
||||
view.task = Ext.TaskManager.start({
|
||||
run: function() {
|
||||
if (!view.isVisible() || !view.scrollToEnd || !viewmodel.get('livemode')) {
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
/*
|
||||
* Display log entries in a panel with scrollbar
|
||||
* The log entries are automatically refreshed via a background task,
|
||||
* with newest entries comming at the bottom
|
||||
* with newest entries coming at the bottom
|
||||
*/
|
||||
Ext.define('Proxmox.panel.LogView', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
xtype: 'proxmoxLogView',
|
||||
|
||||
pageSize: 500,
|
||||
pageSize: 510,
|
||||
viewBuffer: 50,
|
||||
lineHeight: 16,
|
||||
|
||||
@ -22,19 +22,28 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
updateParams: function() {
|
||||
let me = this;
|
||||
let viewModel = me.getViewModel();
|
||||
let since = viewModel.get('since');
|
||||
let until = viewModel.get('until');
|
||||
if (viewModel.get('hide_timespan')) {
|
||||
|
||||
if (viewModel.get('hide_timespan') || viewModel.get('livemode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let since = viewModel.get('since');
|
||||
let until = viewModel.get('until');
|
||||
|
||||
if (since > until) {
|
||||
Ext.Msg.alert('Error', 'Since date must be less equal than Until date.');
|
||||
return;
|
||||
}
|
||||
|
||||
viewModel.set('params.since', Ext.Date.format(since, 'Y-m-d'));
|
||||
viewModel.set('params.until', Ext.Date.format(until, 'Y-m-d') + ' 23:59:59');
|
||||
let submitFormat = viewModel.get('submitFormat');
|
||||
|
||||
viewModel.set('params.since', Ext.Date.format(since, submitFormat));
|
||||
if (submitFormat === 'Y-m-d') {
|
||||
viewModel.set('params.until', Ext.Date.format(until, submitFormat) + ' 23:59:59');
|
||||
} else {
|
||||
viewModel.set('params.until', Ext.Date.format(until, submitFormat));
|
||||
}
|
||||
|
||||
me.getView().loadTask.delay(200);
|
||||
},
|
||||
|
||||
@ -45,29 +54,42 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
return maxPos - pos;
|
||||
},
|
||||
|
||||
updateView: function(text, first, total) {
|
||||
updateView: function(lines, first, total) {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
let viewModel = me.getViewModel();
|
||||
let content = me.lookup('content');
|
||||
let data = viewModel.get('data');
|
||||
|
||||
if (first === data.first && total === data.total && text.length === data.textlen) {
|
||||
return; // same content, skip setting and scrolling
|
||||
if (first === data.first && total === data.total && lines.length === data.lines) {
|
||||
// before there is any real output, we get 'no output' as a single line, so always
|
||||
// update if we only have one to be sure to catch the first real line of output
|
||||
if (total !== 1) {
|
||||
return; // same content, skip setting and scrolling
|
||||
}
|
||||
}
|
||||
viewModel.set('data', {
|
||||
first: first,
|
||||
total: total,
|
||||
textlen: text.length,
|
||||
lines: lines.length,
|
||||
});
|
||||
|
||||
let scrollPos = me.scrollPosBottom();
|
||||
let scrollToBottom = view.scrollToEnd && scrollPos <= 5;
|
||||
|
||||
content.update(text);
|
||||
if (!scrollToBottom) {
|
||||
// so that we have the 'correct' height for the text
|
||||
lines.length = total;
|
||||
}
|
||||
|
||||
if (view.scrollToEnd && scrollPos <= 5) {
|
||||
// we use setTimeout to work around scroll handling on touchscreens
|
||||
setTimeout(function() { view.scrollTo(0, Infinity); }, 10);
|
||||
content.update(lines.join('<br>'));
|
||||
|
||||
if (scrollToBottom) {
|
||||
let scroller = view.getScrollable();
|
||||
scroller.suspendEvent('scroll');
|
||||
view.scrollTo(0, Infinity);
|
||||
me.updateStart(true);
|
||||
scroller.resumeEvent('scroll');
|
||||
}
|
||||
},
|
||||
|
||||
@ -85,6 +107,9 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
params: viewModel.get('params'),
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
if (me.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
Proxmox.Utils.setErrorMask(me, false);
|
||||
let total = response.result.total;
|
||||
let lines = [];
|
||||
@ -97,8 +122,7 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
lines[line.n - 1] = Ext.htmlEncode(line.t);
|
||||
});
|
||||
|
||||
lines.length = total;
|
||||
me.updateView(lines.join('<br>'), first - 1, total);
|
||||
me.updateView(lines, first - 1, total);
|
||||
me.running = false;
|
||||
if (me.requested) {
|
||||
me.requested = false;
|
||||
@ -121,27 +145,66 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
});
|
||||
},
|
||||
|
||||
updateStart: function(scrolledToBottom, targetLine) {
|
||||
let me = this;
|
||||
let view = me.getView(), viewModel = me.getViewModel();
|
||||
|
||||
let limit = viewModel.get('params.limit');
|
||||
let total = viewModel.get('data.total');
|
||||
|
||||
// heuristic: scroll up? -> load more in front; scroll down? -> load more at end
|
||||
let startRatio = view.lastTargetLine && view.lastTargetLine > targetLine ? 2/3 : 1/3;
|
||||
view.lastTargetLine = targetLine;
|
||||
|
||||
let newStart = scrolledToBottom
|
||||
? Math.trunc(total - limit, 10)
|
||||
: Math.trunc(targetLine - (startRatio * limit) + 10);
|
||||
|
||||
viewModel.set('params.start', Math.max(newStart, 0));
|
||||
|
||||
view.loadTask.delay(200);
|
||||
},
|
||||
|
||||
onScroll: function(x, y) {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
let viewModel = me.getViewModel();
|
||||
let view = me.getView(), viewModel = me.getViewModel();
|
||||
|
||||
let lineHeight = view.lineHeight;
|
||||
let line = view.getScrollY()/lineHeight;
|
||||
let start = viewModel.get('params.start');
|
||||
let limit = viewModel.get('params.limit');
|
||||
let viewLines = view.getHeight()/lineHeight;
|
||||
let line = view.getScrollY() / view.lineHeight;
|
||||
let viewLines = view.getHeight() / view.lineHeight;
|
||||
|
||||
let viewStart = Math.max(parseInt(line - 1 - view.viewBuffer, 10), 0);
|
||||
let viewEnd = parseInt(line + viewLines + 1 + view.viewBuffer, 10);
|
||||
let viewStart = Math.max(Math.trunc(line - 1 - view.viewBuffer), 0);
|
||||
let viewEnd = Math.trunc(line + viewLines + 1 + view.viewBuffer);
|
||||
|
||||
if (viewStart < start || viewEnd > start+limit) {
|
||||
viewModel.set('params.start',
|
||||
Math.max(parseInt(line - (limit / 2) + 10, 10), 0));
|
||||
view.loadTask.delay(200);
|
||||
let { start, limit } = viewModel.get('params');
|
||||
|
||||
let margin = start < 20 ? 0 : 20;
|
||||
|
||||
if (viewStart < start + margin || viewEnd > start + limit - margin) {
|
||||
me.updateStart(false, line);
|
||||
}
|
||||
},
|
||||
|
||||
onLiveMode: function() {
|
||||
let me = this;
|
||||
let viewModel = me.getViewModel();
|
||||
viewModel.set('livemode', true);
|
||||
viewModel.set('params', { start: 0, limit: 510 });
|
||||
|
||||
let view = me.getView();
|
||||
delete view.content;
|
||||
view.scrollToEnd = true;
|
||||
me.updateView([], true, false);
|
||||
},
|
||||
|
||||
onTimespan: function() {
|
||||
let me = this;
|
||||
me.getViewModel().set('livemode', false);
|
||||
me.updateView([], false);
|
||||
// Directly apply currently selected values without update
|
||||
// button click.
|
||||
me.updateParams();
|
||||
},
|
||||
|
||||
init: function(view) {
|
||||
let me = this;
|
||||
|
||||
@ -156,17 +219,17 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
viewModel.set('since', since);
|
||||
viewModel.set('params.limit', view.pageSize);
|
||||
viewModel.set('hide_timespan', !view.log_select_timespan);
|
||||
me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
|
||||
viewModel.set('submitFormat', view.submitFormat);
|
||||
me.lookup('content').setStyle('line-height', `${view.lineHeight}px`);
|
||||
|
||||
view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
|
||||
|
||||
me.updateParams();
|
||||
view.task = Ext.TaskManager.start({
|
||||
run: function() {
|
||||
run: () => {
|
||||
if (!view.isVisible() || !view.scrollToEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (me.scrollPosBottom() <= 5) {
|
||||
view.loadTask.delay(200);
|
||||
}
|
||||
@ -192,6 +255,8 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
data: {
|
||||
until: null,
|
||||
since: null,
|
||||
submitFormat: 'Y-m-d',
|
||||
livemode: true,
|
||||
hide_timespan: false,
|
||||
data: {
|
||||
start: 0,
|
||||
@ -200,7 +265,7 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
},
|
||||
params: {
|
||||
start: 0,
|
||||
limit: 500,
|
||||
limit: 510,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -211,9 +276,8 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
x: 'auto',
|
||||
y: 'auto',
|
||||
listeners: {
|
||||
// we have to have this here, since we cannot listen to events
|
||||
// of the scroller in the viewcontroller (extjs bug?), nor does
|
||||
// the panel have a 'scroll' event'
|
||||
// we have to have this here, since we cannot listen to events of the scroller in
|
||||
// the viewcontroller (extjs bug?), nor does the panel have a 'scroll' event'
|
||||
scroll: {
|
||||
fn: function(scroller, x, y) {
|
||||
let controller = this.component.getController();
|
||||
@ -232,32 +296,70 @@ Ext.define('Proxmox.panel.LogView', {
|
||||
},
|
||||
items: [
|
||||
'->',
|
||||
'Since: ',
|
||||
{
|
||||
xtype: 'datefield',
|
||||
xtype: 'segmentedbutton',
|
||||
items: [
|
||||
{
|
||||
text: gettext('Live Mode'),
|
||||
bind: {
|
||||
pressed: '{livemode}',
|
||||
},
|
||||
handler: 'onLiveMode',
|
||||
},
|
||||
{
|
||||
text: gettext('Select Timespan'),
|
||||
bind: {
|
||||
pressed: '{!livemode}',
|
||||
},
|
||||
handler: 'onTimespan',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'box',
|
||||
autoEl: { cn: gettext('Since') + ':' },
|
||||
bind: {
|
||||
disabled: '{livemode}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxDateTimeField',
|
||||
name: 'since_date',
|
||||
reference: 'since',
|
||||
format: 'Y-m-d',
|
||||
bind: {
|
||||
disabled: '{livemode}',
|
||||
value: '{since}',
|
||||
maxValue: '{until}',
|
||||
submitFormat: '{submitFormat}',
|
||||
},
|
||||
},
|
||||
'Until: ',
|
||||
{
|
||||
xtype: 'datefield',
|
||||
xtype: 'box',
|
||||
autoEl: { cn: gettext('Until') + ':' },
|
||||
bind: {
|
||||
disabled: '{livemode}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxDateTimeField',
|
||||
name: 'until_date',
|
||||
reference: 'until',
|
||||
format: 'Y-m-d',
|
||||
bind: {
|
||||
disabled: '{livemode}',
|
||||
value: '{until}',
|
||||
minValue: '{since}',
|
||||
submitFormat: '{submitFormat}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
text: 'Update',
|
||||
handler: 'updateParams',
|
||||
bind: {
|
||||
disabled: '{livemode}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
102
src/panel/NodeInfoRepoStatus.js
Normal file
@ -0,0 +1,102 @@
|
||||
Ext.define('Proxmox.widget.NodeInfoRepoStatus', {
|
||||
extend: 'Proxmox.widget.Info',
|
||||
alias: 'widget.pmxNodeInfoRepoStatus',
|
||||
|
||||
title: gettext('Repository Status'),
|
||||
|
||||
colspan: 2,
|
||||
|
||||
printBar: false,
|
||||
|
||||
product: undefined,
|
||||
repoLink: undefined,
|
||||
|
||||
viewModel: {
|
||||
data: {
|
||||
subscriptionActive: '',
|
||||
noSubscriptionRepo: '',
|
||||
enterpriseRepo: '',
|
||||
testRepo: '',
|
||||
},
|
||||
|
||||
formulas: {
|
||||
repoStatus: function(get) {
|
||||
if (get('subscriptionActive') === '' || get('enterpriseRepo') === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (get('noSubscriptionRepo') || get('testRepo')) {
|
||||
return 'non-production';
|
||||
} else if (get('subscriptionActive') && get('enterpriseRepo')) {
|
||||
return 'ok';
|
||||
} else if (!get('subscriptionActive') && get('enterpriseRepo')) {
|
||||
return 'no-sub';
|
||||
} else if (!get('enterpriseRepo') || !get('noSubscriptionRepo') || !get('testRepo')) {
|
||||
return 'no-repo';
|
||||
}
|
||||
return 'unknown';
|
||||
},
|
||||
|
||||
repoStatusMessage: function(get) {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
|
||||
const status = get('repoStatus');
|
||||
|
||||
let repoLink = ` <a data-qtip="${gettext("Open Repositories Panel")}"
|
||||
href="${view.repoLink}">
|
||||
<i class="fa black fa-chevron-right txt-shadow-hover"></i>
|
||||
</a>`;
|
||||
|
||||
return Proxmox.Utils.formatNodeRepoStatus(status, view.product) + repoLink;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
setValue: function(value) { // for binding below
|
||||
this.updateValue(value);
|
||||
},
|
||||
|
||||
bind: {
|
||||
value: '{repoStatusMessage}',
|
||||
},
|
||||
|
||||
setRepositoryInfo: function(standardRepos) {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
|
||||
for (const standardRepo of standardRepos) {
|
||||
const handle = standardRepo.handle;
|
||||
const status = standardRepo.status || 0;
|
||||
|
||||
if (handle === "enterprise") {
|
||||
vm.set('enterpriseRepo', status);
|
||||
} else if (handle === "no-subscription") {
|
||||
vm.set('noSubscriptionRepo', status);
|
||||
} else if (handle === "test") {
|
||||
vm.set('testRepo', status);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setSubscriptionStatus: function(status) {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
|
||||
vm.set('subscriptionActive', status);
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
if (me.product === undefined) {
|
||||
throw "no product name provided";
|
||||
}
|
||||
|
||||
if (me.repoLink === undefined) {
|
||||
throw "no repo link href provided";
|
||||
}
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
172
src/panel/NotesView.js
Normal file
@ -0,0 +1,172 @@
|
||||
Ext.define('Proxmox.panel.NotesView', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
xtype: 'pmxNotesView',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
title: gettext("Notes"),
|
||||
bodyPadding: 10,
|
||||
scrollable: true,
|
||||
animCollapse: false,
|
||||
collapseFirst: false,
|
||||
|
||||
maxLength: 64 * 1024,
|
||||
enableTBar: false,
|
||||
onlineHelp: 'markdown_basics',
|
||||
|
||||
tbar: {
|
||||
itemId: 'tbar',
|
||||
hidden: true,
|
||||
items: [
|
||||
{
|
||||
text: gettext('Edit'),
|
||||
iconCls: 'fa fa-pencil-square-o',
|
||||
handler: function() {
|
||||
let view = this.up('panel');
|
||||
view.run_editor();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
cbindData: function(initalConfig) {
|
||||
let me = this;
|
||||
let type = '';
|
||||
|
||||
if (me.node) {
|
||||
me.url = `/api2/extjs/nodes/${me.node}/config`;
|
||||
} else if (me.pveSelNode?.data?.id === 'root') {
|
||||
me.url = '/api2/extjs/cluster/options';
|
||||
type = me.pveSelNode?.data?.type;
|
||||
} else {
|
||||
const nodename = me.pveSelNode?.data?.node;
|
||||
type = me.pveSelNode?.data?.type;
|
||||
|
||||
if (!nodename) {
|
||||
throw "no node name specified";
|
||||
}
|
||||
|
||||
if (!Ext.Array.contains(['node', 'qemu', 'lxc'], type)) {
|
||||
throw 'invalid type specified';
|
||||
}
|
||||
|
||||
const vmid = me.pveSelNode?.data?.vmid;
|
||||
|
||||
if (!vmid && type !== 'node') {
|
||||
throw "no VM ID specified";
|
||||
}
|
||||
|
||||
me.url = `/api2/extjs/nodes/${nodename}/`;
|
||||
|
||||
// add the type specific path if qemu/lxc and set the backend's maxLen
|
||||
if (type === 'qemu' || type === 'lxc') {
|
||||
me.url += `${type}/${vmid}/`;
|
||||
me.maxLength = 8 * 1024;
|
||||
}
|
||||
|
||||
me.url += 'config';
|
||||
}
|
||||
|
||||
me.pveType = type;
|
||||
|
||||
me.load();
|
||||
return {};
|
||||
},
|
||||
|
||||
run_editor: function() {
|
||||
let me = this;
|
||||
Ext.create('Proxmox.window.NotesEdit', {
|
||||
url: me.url,
|
||||
onlineHelp: me.onlineHelp,
|
||||
listeners: {
|
||||
destroy: () => me.load(),
|
||||
},
|
||||
autoShow: true,
|
||||
}).setMaxLength(me.maxLength);
|
||||
},
|
||||
|
||||
setNotes: function(value = '') {
|
||||
let me = this;
|
||||
|
||||
let mdHtml = Proxmox.Markdown.parse(value);
|
||||
me.update(mdHtml);
|
||||
|
||||
if (me.collapsible && me.collapseMode === 'auto') {
|
||||
me.setCollapsed(!value);
|
||||
}
|
||||
},
|
||||
|
||||
load: function() {
|
||||
let me = this;
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
url: me.url,
|
||||
waitMsgTarget: me,
|
||||
failure: (response, opts) => {
|
||||
me.update(gettext('Error') + " " + response.htmlStatus);
|
||||
me.setCollapsed(false);
|
||||
},
|
||||
success: ({ result }) => me.setNotes(result.data.description),
|
||||
});
|
||||
},
|
||||
|
||||
listeners: {
|
||||
render: function(c) {
|
||||
let me = this;
|
||||
let sp = Ext.state.Manager.getProvider();
|
||||
// to cover live changes to the browser setting
|
||||
me.mon(sp, 'statechange', function(provider, key, value) {
|
||||
if (value === null || key !== 'edit-notes-on-double-click') {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
me.getEl().on('dblclick', me.run_editor, me);
|
||||
} else {
|
||||
// there's only the me.run_editor listener, and removing just that did not work
|
||||
me.getEl().clearListeners();
|
||||
}
|
||||
});
|
||||
// to cover initial setting value
|
||||
if (sp.get('edit-notes-on-double-click', false)) {
|
||||
me.getEl().on('dblclick', me.run_editor, me);
|
||||
}
|
||||
},
|
||||
afterlayout: function() {
|
||||
let me = this;
|
||||
if (me.collapsible && !me.getCollapsed() && me.collapseMode === 'always') {
|
||||
me.setCollapsed(true);
|
||||
me.collapseMode = ''; // only once, on initial load!
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
tools: [
|
||||
{
|
||||
glyph: 'xf044@FontAwesome', // fa-pencil-square-o
|
||||
tooltip: gettext('Edit notes'),
|
||||
callback: view => view.run_editor(),
|
||||
style: {
|
||||
paddingRight: '5px',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
me.callParent();
|
||||
|
||||
// '' is for datacenter
|
||||
if (me.enableTBar === true || me.pveType === 'node' || me.pveType === '') {
|
||||
me.down('#tbar').setVisible(true);
|
||||
} else if (me.pveSelNode?.data?.template !== 1) {
|
||||
me.setCollapsible(true);
|
||||
me.collapseDirection = 'right';
|
||||
|
||||
let sp = Ext.state.Manager.getProvider();
|
||||
me.collapseMode = sp.get('guest-notes-collapse', 'never');
|
||||
|
||||
if (me.collapseMode === 'auto') {
|
||||
me.setCollapsed(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
410
src/panel/NotificationConfigView.js
Normal file
@ -0,0 +1,410 @@
|
||||
Ext.define('Proxmox.panel.NotificationConfigViewModel', {
|
||||
extend: 'Ext.app.ViewModel',
|
||||
|
||||
alias: 'viewmodel.pmxNotificationConfigPanel',
|
||||
|
||||
formulas: {
|
||||
builtinSelected: function(get) {
|
||||
let origin = get('selection')?.get('origin');
|
||||
return origin === 'modified-builtin' || origin === 'builtin';
|
||||
},
|
||||
removeButtonText: get => get('builtinSelected') ? gettext('Reset') : gettext('Remove'),
|
||||
removeButtonConfirmMessage: function(get) {
|
||||
if (get('builtinSelected')) {
|
||||
return gettext('Do you want to reset {0} to its default settings?');
|
||||
} else {
|
||||
// Use default message provided by the button
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
Ext.define('Proxmox.panel.NotificationConfigView', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
alias: 'widget.pmxNotificationConfigView',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
onlineHelp: 'chapter_notifications',
|
||||
layout: {
|
||||
type: 'border',
|
||||
},
|
||||
|
||||
items: [
|
||||
{
|
||||
region: 'center',
|
||||
border: false,
|
||||
xtype: 'pmxNotificationEndpointView',
|
||||
cbind: {
|
||||
baseUrl: '{baseUrl}',
|
||||
},
|
||||
},
|
||||
{
|
||||
region: 'south',
|
||||
height: '50%',
|
||||
border: false,
|
||||
collapsible: true,
|
||||
animCollapse: false,
|
||||
xtype: 'pmxNotificationMatcherView',
|
||||
cbind: {
|
||||
baseUrl: '{baseUrl}',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('Proxmox.panel.NotificationEndpointView', {
|
||||
extend: 'Ext.grid.Panel',
|
||||
alias: 'widget.pmxNotificationEndpointView',
|
||||
|
||||
title: gettext('Notification Targets'),
|
||||
|
||||
viewModel: {
|
||||
type: 'pmxNotificationConfigPanel',
|
||||
},
|
||||
|
||||
bind: {
|
||||
selection: '{selection}',
|
||||
},
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
openEditWindow: function(endpointType, endpoint) {
|
||||
let me = this;
|
||||
|
||||
Ext.create('Proxmox.window.EndpointEditBase', {
|
||||
baseUrl: me.getView().baseUrl,
|
||||
type: endpointType,
|
||||
|
||||
name: endpoint,
|
||||
autoShow: true,
|
||||
listeners: {
|
||||
destroy: () => me.reload(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
openEditForSelectedItem: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
|
||||
let selection = view.getSelection();
|
||||
if (selection.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
me.openEditWindow(selection[0].data.type, selection[0].data.name);
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
view.getStore().rstore.load();
|
||||
this.getView().setSelection(null);
|
||||
},
|
||||
|
||||
testEndpoint: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
|
||||
let selection = view.getSelection();
|
||||
if (selection.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target = selection[0].data.name;
|
||||
|
||||
Ext.Msg.confirm(
|
||||
gettext("Notification Target Test"),
|
||||
Ext.String.format(gettext("Do you want to send a test notification to '{0}'?"), target),
|
||||
function(decision) {
|
||||
if (decision !== "yes") {
|
||||
return;
|
||||
}
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
method: 'POST',
|
||||
url: `${view.baseUrl}/targets/${target}/test`,
|
||||
|
||||
success: function(response, opt) {
|
||||
Ext.Msg.show({
|
||||
title: gettext('Notification Target Test'),
|
||||
message: Ext.String.format(
|
||||
gettext("Sent test notification to '{0}'."),
|
||||
target,
|
||||
),
|
||||
buttons: Ext.Msg.OK,
|
||||
icon: Ext.Msg.INFO,
|
||||
});
|
||||
},
|
||||
autoErrorAlert: true,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
itemdblclick: 'openEditForSelectedItem',
|
||||
activate: 'reload',
|
||||
},
|
||||
|
||||
emptyText: gettext('No notification targets configured'),
|
||||
|
||||
columns: [
|
||||
{
|
||||
dataIndex: 'disable',
|
||||
text: gettext('Enable'),
|
||||
renderer: (disable) => Proxmox.Utils.renderEnabledIcon(!disable),
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
text: gettext('Target Name'),
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
dataIndex: 'type',
|
||||
text: gettext('Type'),
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
dataIndex: 'comment',
|
||||
text: gettext('Comment'),
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 3,
|
||||
},
|
||||
{
|
||||
dataIndex: 'origin',
|
||||
text: gettext('Origin'),
|
||||
renderer: (origin) => {
|
||||
switch (origin) {
|
||||
case 'user-created': return gettext('Custom');
|
||||
case 'modified-builtin': return gettext('Built-In (modified)');
|
||||
case 'builtin': return gettext('Built-In');
|
||||
}
|
||||
|
||||
// Should not happen...
|
||||
return 'unknown';
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
store: {
|
||||
type: 'diff',
|
||||
autoDestroy: true,
|
||||
autoDestroyRstore: true,
|
||||
rstore: {
|
||||
type: 'update',
|
||||
storeid: 'proxmox-notification-endpoints',
|
||||
model: 'proxmox-notification-endpoints',
|
||||
autoStart: true,
|
||||
},
|
||||
sorters: 'name',
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
if (!me.baseUrl) {
|
||||
throw "baseUrl is not set!";
|
||||
}
|
||||
|
||||
let menuItems = [];
|
||||
for (const [endpointType, config] of Object.entries(
|
||||
Proxmox.Schema.notificationEndpointTypes).sort()) {
|
||||
menuItems.push({
|
||||
text: config.name,
|
||||
iconCls: 'fa fa-fw ' + (config.iconCls || 'fa-bell-o'),
|
||||
handler: () => me.controller.openEditWindow(endpointType),
|
||||
});
|
||||
}
|
||||
|
||||
Ext.apply(me, {
|
||||
tbar: [
|
||||
{
|
||||
text: gettext('Add'),
|
||||
menu: menuItems,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Modify'),
|
||||
handler: 'openEditForSelectedItem',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxStdRemoveButton',
|
||||
callback: 'reload',
|
||||
bind: {
|
||||
text: '{removeButtonText}',
|
||||
customConfirmationMessage: '{removeButtonConfirmMessage}',
|
||||
},
|
||||
getUrl: function(rec) {
|
||||
return `${me.baseUrl}/endpoints/${rec.data.type}/${rec.getId()}`;
|
||||
},
|
||||
enableFn: (rec) => {
|
||||
let origin = rec.get('origin');
|
||||
return origin === 'user-created' || origin === 'modified-builtin';
|
||||
},
|
||||
},
|
||||
'-',
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Test'),
|
||||
handler: 'testEndpoint',
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
me.store.rstore.proxy.setUrl(`/api2/json/${me.baseUrl}/targets`);
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('Proxmox.panel.NotificationMatcherView', {
|
||||
extend: 'Ext.grid.Panel',
|
||||
alias: 'widget.pmxNotificationMatcherView',
|
||||
|
||||
title: gettext('Notification Matchers'),
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
openEditWindow: function(matcher) {
|
||||
let me = this;
|
||||
|
||||
Ext.create('Proxmox.window.NotificationMatcherEdit', {
|
||||
baseUrl: me.getView().baseUrl,
|
||||
name: matcher,
|
||||
autoShow: true,
|
||||
listeners: {
|
||||
destroy: () => me.reload(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
openEditForSelectedItem: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
|
||||
let selection = view.getSelection();
|
||||
if (selection.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
me.openEditWindow(selection[0].data.name);
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
this.getView().getStore().rstore.load();
|
||||
this.getView().setSelection(null);
|
||||
},
|
||||
},
|
||||
|
||||
viewModel: {
|
||||
type: 'pmxNotificationConfigPanel',
|
||||
},
|
||||
|
||||
bind: {
|
||||
selection: '{selection}',
|
||||
},
|
||||
|
||||
listeners: {
|
||||
itemdblclick: 'openEditForSelectedItem',
|
||||
activate: 'reload',
|
||||
},
|
||||
|
||||
emptyText: gettext('No notification matchers configured'),
|
||||
|
||||
columns: [
|
||||
{
|
||||
dataIndex: 'disable',
|
||||
text: gettext('Enable'),
|
||||
renderer: (disable) => Proxmox.Utils.renderEnabledIcon(!disable),
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
text: gettext('Matcher Name'),
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
dataIndex: 'comment',
|
||||
text: gettext('Comment'),
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
dataIndex: 'origin',
|
||||
text: gettext('Origin'),
|
||||
renderer: (origin) => {
|
||||
switch (origin) {
|
||||
case 'user-created': return gettext('Custom');
|
||||
case 'modified-builtin': return gettext('Built-In (modified)');
|
||||
case 'builtin': return gettext('Built-In');
|
||||
}
|
||||
|
||||
// Should not happen...
|
||||
return 'unknown';
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
store: {
|
||||
type: 'diff',
|
||||
autoDestroy: true,
|
||||
autoDestroyRstore: true,
|
||||
rstore: {
|
||||
type: 'update',
|
||||
storeid: 'proxmox-notification-matchers',
|
||||
model: 'proxmox-notification-matchers',
|
||||
autoStart: true,
|
||||
},
|
||||
sorters: 'name',
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
if (!me.baseUrl) {
|
||||
throw "baseUrl is not set!";
|
||||
}
|
||||
|
||||
Ext.apply(me, {
|
||||
tbar: [
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Add'),
|
||||
handler: () => me.getController().openEditWindow(),
|
||||
selModel: false,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Modify'),
|
||||
handler: 'openEditForSelectedItem',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxStdRemoveButton',
|
||||
callback: 'reload',
|
||||
bind: {
|
||||
text: '{removeButtonText}',
|
||||
customConfirmationMessage: '{removeButtonConfirmMessage}',
|
||||
},
|
||||
baseurl: `${me.baseUrl}/matchers`,
|
||||
enableFn: (rec) => {
|
||||
let origin = rec.get('origin');
|
||||
return origin === 'user-created' || origin === 'modified-builtin';
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
me.store.rstore.proxy.setUrl(`/api2/json/${me.baseUrl}/matchers`);
|
||||
},
|
||||
});
|
||||
@ -1,3 +1,6 @@
|
||||
// override the download server url globally, for privacy reasons
|
||||
Ext.draw.Container.prototype.defaultDownloadServerUrl = "-";
|
||||
|
||||
Ext.define('Proxmox.chart.axis.segmenter.NumericBase2', {
|
||||
extend: 'Ext.chart.axis.segmenter.Numeric',
|
||||
alias: 'segmenter.numericBase2',
|
||||
@ -61,6 +64,9 @@ Ext.define('Proxmox.widget.RRDChart', {
|
||||
|
||||
powerOfTwo: false,
|
||||
|
||||
// set to empty string to suppress warning in debug mode
|
||||
downloadServerUrl: '-',
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
@ -86,7 +92,7 @@ Ext.define('Proxmox.widget.RRDChart', {
|
||||
value = Ext.util.Format.number(value, format);
|
||||
|
||||
let unit = units[si];
|
||||
if (this.powerOfTwo) unit += 'i';
|
||||
if (unit && this.powerOfTwo) unit += 'i';
|
||||
|
||||
return `${value.toString()} ${unit}`;
|
||||
},
|
||||
@ -118,6 +124,9 @@ Ext.define('Proxmox.widget.RRDChart', {
|
||||
},
|
||||
|
||||
onAfterAnimation: function(chart, eopts) {
|
||||
if (!chart.header || !chart.header.tools) {
|
||||
return;
|
||||
}
|
||||
// if the undo button is disabled, disable our tool
|
||||
let ourUndoZoomButton = chart.header.tools[0];
|
||||
let undoButton = chart.interactions[0].getUndoButton();
|
||||
@ -134,10 +143,21 @@ Ext.define('Proxmox.widget.RRDChart', {
|
||||
},
|
||||
],
|
||||
legend: {
|
||||
type: 'dom',
|
||||
padding: 0,
|
||||
},
|
||||
listeners: {
|
||||
animationend: 'onAfterAnimation',
|
||||
redraw: {
|
||||
fn: 'onAfterAnimation',
|
||||
options: {
|
||||
buffer: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
touchAction: {
|
||||
panX: true,
|
||||
panY: true,
|
||||
},
|
||||
|
||||
constructor: function(config) {
|
||||
@ -163,6 +183,27 @@ Ext.define('Proxmox.widget.RRDChart', {
|
||||
me.callParent([config]);
|
||||
},
|
||||
|
||||
checkThemeColors: function() {
|
||||
let me = this;
|
||||
let rootStyle = getComputedStyle(document.documentElement);
|
||||
|
||||
// get colors
|
||||
let background = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff";
|
||||
let text = rootStyle.getPropertyValue("--pwt-text-color").trim() || "#000000";
|
||||
let primary = rootStyle.getPropertyValue("--pwt-chart-primary").trim() || "#000000";
|
||||
let gridStroke = rootStyle.getPropertyValue("--pwt-chart-grid-stroke").trim() || "#dddddd";
|
||||
|
||||
// set the colors
|
||||
me.setBackground(background);
|
||||
me.axes.forEach((axis) => {
|
||||
axis.setLabel({ color: text });
|
||||
axis.setTitle({ color: text });
|
||||
axis.setStyle({ strokeStyle: primary });
|
||||
axis.setGrid({ stroke: gridStroke });
|
||||
});
|
||||
me.redraw();
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
@ -197,6 +238,7 @@ Ext.define('Proxmox.widget.RRDChart', {
|
||||
if (me.header && me.legend) {
|
||||
me.header.padding = '4 9 4';
|
||||
me.header.add(me.legend);
|
||||
me.legend = undefined;
|
||||
}
|
||||
|
||||
if (!me.noTool) {
|
||||
@ -233,10 +275,6 @@ Ext.define('Proxmox.widget.RRDChart', {
|
||||
marker: {
|
||||
opacity: 0,
|
||||
scaling: 0.01,
|
||||
fx: {
|
||||
duration: 200,
|
||||
easing: 'easeOut',
|
||||
},
|
||||
},
|
||||
highlightCfg: {
|
||||
opacity: 1,
|
||||
@ -253,7 +291,26 @@ Ext.define('Proxmox.widget.RRDChart', {
|
||||
|
||||
// enable animation after the store is loaded
|
||||
me.store.onAfter('load', function() {
|
||||
me.setAnimation(true);
|
||||
me.setAnimation({
|
||||
duration: 200,
|
||||
easing: 'easeIn',
|
||||
});
|
||||
}, this, { single: true });
|
||||
|
||||
|
||||
me.checkThemeColors();
|
||||
|
||||
// switch colors on media query changes
|
||||
me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
me.themeListener = (e) => { me.checkThemeColors(); };
|
||||
me.mediaQueryList.addEventListener("change", me.themeListener);
|
||||
},
|
||||
|
||||
doDestroy: function() {
|
||||
let me = this;
|
||||
|
||||
me.mediaQueryList.removeEventListener("change", me.themeListener);
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
|
||||
103
src/panel/SendmailEditPanel.js
Normal file
@ -0,0 +1,103 @@
|
||||
Ext.define('Proxmox.panel.SendmailEditPanel', {
|
||||
extend: 'Proxmox.panel.InputPanel',
|
||||
xtype: 'pmxSendmailEditPanel',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
type: 'sendmail',
|
||||
onlineHelp: 'notification_targets_sendmail',
|
||||
|
||||
mailValidator: function() {
|
||||
let mailto_user = this.down(`[name=mailto-user]`);
|
||||
let mailto = this.down(`[name=mailto]`);
|
||||
|
||||
if (!mailto_user.getValue()?.length && !mailto.getValue()) {
|
||||
return gettext('Either mailto or mailto-user must be set');
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
items: [
|
||||
{
|
||||
xtype: 'pmxDisplayEditField',
|
||||
name: 'name',
|
||||
cbind: {
|
||||
value: '{name}',
|
||||
editable: '{isCreate}',
|
||||
},
|
||||
fieldLabel: gettext('Endpoint Name'),
|
||||
allowBlank: false,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxcheckbox',
|
||||
name: 'enable',
|
||||
fieldLabel: gettext('Enable'),
|
||||
allowBlank: false,
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
// provides 'mailto' and 'mailto-user' fields
|
||||
xtype: 'pmxEmailRecipientPanel',
|
||||
cbind: {
|
||||
isCreate: '{isCreate}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
name: 'comment',
|
||||
fieldLabel: gettext('Comment'),
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
advancedItems: [
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
fieldLabel: gettext('Author'),
|
||||
name: 'author',
|
||||
allowBlank: true,
|
||||
cbind: {
|
||||
emptyText: '{defaultMailAuthor}',
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
fieldLabel: gettext('From Address'),
|
||||
name: 'from-address',
|
||||
allowBlank: true,
|
||||
emptyText: gettext('Defaults to datacenter configuration, or root@$hostname'),
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
onSetValues: (values) => {
|
||||
values.enable = !values.disable;
|
||||
|
||||
delete values.disable;
|
||||
return values;
|
||||
},
|
||||
|
||||
onGetValues: function(values) {
|
||||
let me = this;
|
||||
|
||||
if (values.enable) {
|
||||
if (!me.isCreate) {
|
||||
Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
|
||||
}
|
||||
} else {
|
||||
values.disable = 1;
|
||||
}
|
||||
|
||||
delete values.enable;
|
||||
|
||||
if (values.mailto) {
|
||||
values.mailto = values.mailto.split(/[\s,;]+/);
|
||||
}
|
||||
return values;
|
||||
},
|
||||
});
|
||||
205
src/panel/SmtpEditPanel.js
Normal file
@ -0,0 +1,205 @@
|
||||
Ext.define('Proxmox.panel.SmtpEditPanel', {
|
||||
extend: 'Proxmox.panel.InputPanel',
|
||||
xtype: 'pmxSmtpEditPanel',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
onlineHelp: 'notification_targets_smtp',
|
||||
|
||||
type: 'smtp',
|
||||
|
||||
viewModel: {
|
||||
xtype: 'viewmodel',
|
||||
cbind: {
|
||||
isCreate: "{isCreate}",
|
||||
},
|
||||
data: {
|
||||
mode: 'tls',
|
||||
authentication: true,
|
||||
},
|
||||
formulas: {
|
||||
portEmptyText: function(get) {
|
||||
let port;
|
||||
|
||||
switch (get('mode')) {
|
||||
case 'insecure':
|
||||
port = 25;
|
||||
break;
|
||||
case 'starttls':
|
||||
port = 587;
|
||||
break;
|
||||
case 'tls':
|
||||
port = 465;
|
||||
break;
|
||||
}
|
||||
return `${Proxmox.Utils.defaultText} (${port})`;
|
||||
},
|
||||
passwordEmptyText: function(get) {
|
||||
let isCreate = this.isCreate;
|
||||
return get('authentication') && !isCreate ? gettext('Unchanged') : '';
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
columnT: [
|
||||
{
|
||||
xtype: 'pmxDisplayEditField',
|
||||
name: 'name',
|
||||
cbind: {
|
||||
value: '{name}',
|
||||
editable: '{isCreate}',
|
||||
},
|
||||
fieldLabel: gettext('Endpoint Name'),
|
||||
allowBlank: false,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxcheckbox',
|
||||
name: 'enable',
|
||||
fieldLabel: gettext('Enable'),
|
||||
allowBlank: false,
|
||||
checked: true,
|
||||
},
|
||||
],
|
||||
|
||||
column1: [
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
fieldLabel: gettext('Server'),
|
||||
name: 'server',
|
||||
allowBlank: false,
|
||||
emptyText: gettext('mail.example.com'),
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxKVComboBox',
|
||||
name: 'mode',
|
||||
fieldLabel: gettext('Encryption'),
|
||||
editable: false,
|
||||
comboItems: [
|
||||
['insecure', Proxmox.Utils.noneText + ' (' + gettext('insecure') + ')'],
|
||||
['starttls', 'STARTTLS'],
|
||||
['tls', 'TLS'],
|
||||
],
|
||||
bind: "{mode}",
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxintegerfield',
|
||||
name: 'port',
|
||||
fieldLabel: gettext('Port'),
|
||||
minValue: 1,
|
||||
maxValue: 65535,
|
||||
bind: {
|
||||
emptyText: "{portEmptyText}",
|
||||
},
|
||||
submitEmptyText: false,
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
],
|
||||
column2: [
|
||||
{
|
||||
xtype: 'proxmoxcheckbox',
|
||||
fieldLabel: gettext('Authenticate'),
|
||||
name: 'authentication',
|
||||
bind: {
|
||||
value: '{authentication}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
fieldLabel: gettext('Username'),
|
||||
name: 'username',
|
||||
allowBlank: false,
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
bind: {
|
||||
disabled: '{!authentication}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
inputType: 'password',
|
||||
fieldLabel: gettext('Password'),
|
||||
name: 'password',
|
||||
allowBlank: true,
|
||||
bind: {
|
||||
disabled: '{!authentication}',
|
||||
emptyText: '{passwordEmptyText}',
|
||||
},
|
||||
},
|
||||
],
|
||||
columnB: [
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
fieldLabel: gettext('From Address'),
|
||||
name: 'from-address',
|
||||
allowBlank: false,
|
||||
emptyText: gettext('user@example.com'),
|
||||
},
|
||||
{
|
||||
// provides 'mailto' and 'mailto-user' fields
|
||||
xtype: 'pmxEmailRecipientPanel',
|
||||
cbind: {
|
||||
isCreate: '{isCreate}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
name: 'comment',
|
||||
fieldLabel: gettext('Comment'),
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
advancedColumnB: [
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
fieldLabel: gettext('Author'),
|
||||
name: 'author',
|
||||
allowBlank: true,
|
||||
cbind: {
|
||||
emptyText: '{defaultMailAuthor}',
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
onGetValues: function(values) {
|
||||
let me = this;
|
||||
|
||||
if (values.mailto) {
|
||||
values.mailto = values.mailto.split(/[\s,;]+/);
|
||||
}
|
||||
|
||||
if (!values.authentication && !me.isCreate) {
|
||||
Proxmox.Utils.assemble_field_data(values, { 'delete': 'username' });
|
||||
Proxmox.Utils.assemble_field_data(values, { 'delete': 'password' });
|
||||
}
|
||||
|
||||
if (values.enable) {
|
||||
if (!me.isCreate) {
|
||||
Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
|
||||
}
|
||||
} else {
|
||||
values.disable = 1;
|
||||
}
|
||||
|
||||
delete values.enable;
|
||||
|
||||
delete values.authentication;
|
||||
|
||||
return values;
|
||||
},
|
||||
|
||||
onSetValues: function(values) {
|
||||
values.authentication = !!values.username;
|
||||
values.enable = !values.disable;
|
||||
delete values.disable;
|
||||
|
||||
return values;
|
||||
},
|
||||
});
|
||||
@ -47,7 +47,7 @@ Ext.define('Proxmox.panel.StatusView', {
|
||||
*/
|
||||
if (used.used !== undefined &&
|
||||
used.total !== undefined) {
|
||||
return used.used/used.total;
|
||||
return used.total > 0 ? used.used/used.total : 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
304
src/panel/TfaView.js
Normal file
@ -0,0 +1,304 @@
|
||||
Ext.define('pmx-tfa-users', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: ['userid'],
|
||||
idProperty: 'userid',
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: '/api2/json/access/tfa',
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('pmx-tfa-entry', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: ['fullid', 'userid', 'type', 'description', 'created', 'enable'],
|
||||
idProperty: 'fullid',
|
||||
});
|
||||
|
||||
|
||||
Ext.define('Proxmox.panel.TfaView', {
|
||||
extend: 'Ext.grid.GridPanel',
|
||||
alias: 'widget.pmxTfaView',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
title: gettext('Second Factors'),
|
||||
reference: 'tfaview',
|
||||
|
||||
issuerName: 'Proxmox',
|
||||
yubicoEnabled: false,
|
||||
|
||||
cbindData: function(initialConfig) {
|
||||
let me = this;
|
||||
return {
|
||||
yubicoEnabled: me.yubicoEnabled,
|
||||
};
|
||||
},
|
||||
|
||||
store: {
|
||||
type: 'diff',
|
||||
autoDestroy: true,
|
||||
autoDestroyRstore: true,
|
||||
model: 'pmx-tfa-entry',
|
||||
rstore: {
|
||||
type: 'store',
|
||||
proxy: 'memory',
|
||||
storeid: 'pmx-tfa-entry',
|
||||
model: 'pmx-tfa-entry',
|
||||
},
|
||||
},
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
init: function(view) {
|
||||
let me = this;
|
||||
view.tfaStore = Ext.create('Proxmox.data.UpdateStore', {
|
||||
autoStart: true,
|
||||
interval: 5 * 1000,
|
||||
storeid: 'pmx-tfa-users',
|
||||
model: 'pmx-tfa-users',
|
||||
});
|
||||
view.tfaStore.on('load', this.onLoad, this);
|
||||
view.on('destroy', view.tfaStore.stopUpdate);
|
||||
Proxmox.Utils.monStoreErrors(view, view.tfaStore);
|
||||
},
|
||||
|
||||
reload: function() { this.getView().tfaStore.load(); },
|
||||
|
||||
onLoad: function(store, data, success) {
|
||||
if (!success) return;
|
||||
|
||||
let now = new Date().getTime() / 1000;
|
||||
let records = [];
|
||||
Ext.Array.each(data, user => {
|
||||
let tfa_locked = (user.data['tfa-locked-until'] || 0) > now;
|
||||
let totp_locked = user.data['totp-locked'];
|
||||
Ext.Array.each(user.data.entries, entry => {
|
||||
records.push({
|
||||
fullid: `${user.id}/${entry.id}`,
|
||||
userid: user.id,
|
||||
type: entry.type,
|
||||
description: entry.description,
|
||||
created: entry.created,
|
||||
enable: entry.enable,
|
||||
locked: tfa_locked || (entry.type === 'totp' && totp_locked),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let rstore = this.getView().store.rstore;
|
||||
rstore.loadData(records);
|
||||
rstore.fireEvent('load', rstore, records, true);
|
||||
},
|
||||
|
||||
addTotp: function() {
|
||||
let me = this;
|
||||
|
||||
Ext.create('Proxmox.window.AddTotp', {
|
||||
isCreate: true,
|
||||
issuerName: me.getView().issuerName,
|
||||
listeners: {
|
||||
destroy: function() {
|
||||
me.reload();
|
||||
},
|
||||
},
|
||||
}).show();
|
||||
},
|
||||
|
||||
addWebauthn: function() {
|
||||
let me = this;
|
||||
|
||||
Ext.create('Proxmox.window.AddWebauthn', {
|
||||
isCreate: true,
|
||||
autoShow: true,
|
||||
listeners: {
|
||||
destroy: () => me.reload(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
addRecovery: async function() {
|
||||
let me = this;
|
||||
|
||||
Ext.create('Proxmox.window.AddTfaRecovery', {
|
||||
autoShow: true,
|
||||
listeners: {
|
||||
destroy: () => me.reload(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
addYubico: function() {
|
||||
let me = this;
|
||||
|
||||
Ext.create('Proxmox.window.AddYubico', {
|
||||
isCreate: true,
|
||||
autoShow: true,
|
||||
listeners: {
|
||||
destroy: () => me.reload(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
editItem: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
let selection = view.getSelection();
|
||||
if (selection.length !== 1 || selection[0].id.endsWith("/recovery")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Ext.create('Proxmox.window.TfaEdit', {
|
||||
'tfa-id': selection[0].data.fullid,
|
||||
autoShow: true,
|
||||
listeners: {
|
||||
destroy: () => me.reload(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
renderUser: fullid => fullid.split('/')[0],
|
||||
|
||||
renderEnabled: function(enabled, metaData, record) {
|
||||
if (record.data.locked) {
|
||||
return gettext("Locked");
|
||||
} else if (enabled === undefined) {
|
||||
return Proxmox.Utils.yesText;
|
||||
} else {
|
||||
return Proxmox.Utils.format_boolean(enabled);
|
||||
}
|
||||
},
|
||||
|
||||
onRemoveButton: function(btn, event, record) {
|
||||
let me = this;
|
||||
|
||||
Ext.create('Proxmox.tfa.confirmRemove', {
|
||||
...record.data,
|
||||
callback: password => me.removeItem(password, record),
|
||||
autoShow: true,
|
||||
});
|
||||
},
|
||||
|
||||
removeItem: async function(password, record) {
|
||||
let me = this;
|
||||
|
||||
if (password !== null) {
|
||||
password = '?password=' + encodeURIComponent(password);
|
||||
} else {
|
||||
password = '';
|
||||
}
|
||||
|
||||
try {
|
||||
me.getView().mask(gettext('Please wait...'), 'x-mask-loading');
|
||||
await Proxmox.Async.api2({
|
||||
url: `/api2/extjs/access/tfa/${record.id}${password}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
me.reload();
|
||||
} catch (response) {
|
||||
Ext.Msg.alert(gettext('Error'), response.result.message);
|
||||
} finally {
|
||||
me.getView().unmask();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
viewConfig: {
|
||||
trackOver: false,
|
||||
},
|
||||
|
||||
listeners: {
|
||||
itemdblclick: 'editItem',
|
||||
},
|
||||
|
||||
columns: [
|
||||
{
|
||||
header: gettext('User'),
|
||||
width: 200,
|
||||
sortable: true,
|
||||
dataIndex: 'fullid',
|
||||
renderer: 'renderUser',
|
||||
},
|
||||
{
|
||||
header: gettext('Enabled'),
|
||||
width: 80,
|
||||
sortable: true,
|
||||
dataIndex: 'enable',
|
||||
renderer: 'renderEnabled',
|
||||
},
|
||||
{
|
||||
header: gettext('TFA Type'),
|
||||
width: 80,
|
||||
sortable: true,
|
||||
dataIndex: 'type',
|
||||
},
|
||||
{
|
||||
header: gettext('Created'),
|
||||
width: 150,
|
||||
sortable: true,
|
||||
dataIndex: 'created',
|
||||
renderer: t => !t ? 'N/A' : Proxmox.Utils.render_timestamp(t),
|
||||
},
|
||||
{
|
||||
header: gettext('Description'),
|
||||
width: 300,
|
||||
sortable: true,
|
||||
dataIndex: 'description',
|
||||
renderer: Ext.String.htmlEncode,
|
||||
flex: 1,
|
||||
},
|
||||
],
|
||||
|
||||
tbar: [
|
||||
{
|
||||
text: gettext('Add'),
|
||||
cbind: {},
|
||||
menu: {
|
||||
xtype: 'menu',
|
||||
items: [
|
||||
{
|
||||
text: gettext('TOTP'),
|
||||
itemId: 'totp',
|
||||
iconCls: 'fa fa-fw fa-clock-o',
|
||||
handler: 'addTotp',
|
||||
},
|
||||
{
|
||||
text: gettext('WebAuthn'),
|
||||
itemId: 'webauthn',
|
||||
iconCls: 'fa fa-fw fa-shield',
|
||||
handler: 'addWebauthn',
|
||||
},
|
||||
{
|
||||
text: gettext('Recovery Keys'),
|
||||
itemId: 'recovery',
|
||||
iconCls: 'fa fa-fw fa-file-text-o',
|
||||
handler: 'addRecovery',
|
||||
},
|
||||
{
|
||||
text: gettext('Yubico OTP'),
|
||||
itemId: 'yubico',
|
||||
iconCls: 'fa fa-fw fa-yahoo', // close enough
|
||||
handler: 'addYubico',
|
||||
cbind: {
|
||||
hidden: '{!yubicoEnabled}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'-',
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Edit'),
|
||||
handler: 'editItem',
|
||||
enableFn: rec => !rec.id.endsWith("/recovery"),
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
disabled: true,
|
||||
text: gettext('Remove'),
|
||||
getRecordName: rec => rec.data.description,
|
||||
handler: 'onRemoveButton',
|
||||
},
|
||||
],
|
||||
});
|
||||
423
src/panel/WebhookEditPanel.js
Normal file
@ -0,0 +1,423 @@
|
||||
Ext.define('Proxmox.panel.WebhookEditPanel', {
|
||||
extend: 'Proxmox.panel.InputPanel',
|
||||
xtype: 'pmxWebhookEditPanel',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
onlineHelp: 'notification_targets_webhook',
|
||||
|
||||
type: 'webhook',
|
||||
|
||||
columnT: [
|
||||
|
||||
],
|
||||
|
||||
column1: [
|
||||
{
|
||||
xtype: 'pmxDisplayEditField',
|
||||
name: 'name',
|
||||
cbind: {
|
||||
value: '{name}',
|
||||
editable: '{isCreate}',
|
||||
},
|
||||
fieldLabel: gettext('Endpoint Name'),
|
||||
regex: Proxmox.Utils.safeIdRegex,
|
||||
allowBlank: false,
|
||||
},
|
||||
],
|
||||
|
||||
column2: [
|
||||
{
|
||||
xtype: 'proxmoxcheckbox',
|
||||
name: 'enable',
|
||||
fieldLabel: gettext('Enable'),
|
||||
allowBlank: false,
|
||||
checked: true,
|
||||
},
|
||||
],
|
||||
|
||||
columnB: [
|
||||
{
|
||||
xtype: 'fieldcontainer',
|
||||
fieldLabel: gettext('Method/URL'),
|
||||
layout: 'hbox',
|
||||
border: false,
|
||||
margin: '0 0 5 0',
|
||||
items: [
|
||||
{
|
||||
xtype: 'proxmoxKVComboBox',
|
||||
name: 'method',
|
||||
editable: false,
|
||||
value: 'post',
|
||||
comboItems: [
|
||||
['post', 'POST'],
|
||||
['put', 'PUT'],
|
||||
['get', 'GET'],
|
||||
],
|
||||
width: 80,
|
||||
margin: '0 5 0 0',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
name: 'url',
|
||||
allowBlank: false,
|
||||
emptyText: "https://example.com/hook",
|
||||
regex: Proxmox.Utils.httpUrlRegex,
|
||||
regexText: gettext('Must be a valid URL'),
|
||||
flex: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'pmxWebhookKeyValueList',
|
||||
name: 'header',
|
||||
fieldLabel: gettext('Headers'),
|
||||
addLabel: gettext('Add Header'),
|
||||
maskValues: false,
|
||||
cbind: {
|
||||
isCreate: '{isCreate}',
|
||||
},
|
||||
margin: '0 0 10 0',
|
||||
},
|
||||
{
|
||||
xtype: 'textarea',
|
||||
fieldLabel: gettext('Body'),
|
||||
name: 'body',
|
||||
allowBlank: true,
|
||||
minHeight: '150',
|
||||
fieldStyle: {
|
||||
'font-family': 'monospace',
|
||||
},
|
||||
margin: '0 0 5 0',
|
||||
},
|
||||
{
|
||||
xtype: 'pmxWebhookKeyValueList',
|
||||
name: 'secret',
|
||||
fieldLabel: gettext('Secrets'),
|
||||
addLabel: gettext('Add Secret'),
|
||||
maskValues: true,
|
||||
cbind: {
|
||||
isCreate: '{isCreate}',
|
||||
},
|
||||
margin: '0 0 10 0',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
name: 'comment',
|
||||
fieldLabel: gettext('Comment'),
|
||||
cbind: {
|
||||
deleteEmpty: '{!isCreate}',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
onSetValues: (values) => {
|
||||
values.enable = !values.disable;
|
||||
|
||||
if (values.body) {
|
||||
values.body = Proxmox.Utils.base64ToUtf8(values.body);
|
||||
}
|
||||
|
||||
delete values.disable;
|
||||
return values;
|
||||
},
|
||||
|
||||
onGetValues: function(values) {
|
||||
let me = this;
|
||||
|
||||
if (values.enable) {
|
||||
if (!me.isCreate) {
|
||||
Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
|
||||
}
|
||||
} else {
|
||||
values.disable = 1;
|
||||
}
|
||||
|
||||
if (values.body) {
|
||||
values.body = Proxmox.Utils.utf8ToBase64(values.body);
|
||||
} else {
|
||||
delete values.body;
|
||||
if (!me.isCreate) {
|
||||
Proxmox.Utils.assemble_field_data(values, { 'delete': 'body' });
|
||||
}
|
||||
}
|
||||
|
||||
if (Ext.isArray(values.header) && !values.header.length) {
|
||||
delete values.header;
|
||||
if (!me.isCreate) {
|
||||
Proxmox.Utils.assemble_field_data(values, { 'delete': 'header' });
|
||||
}
|
||||
}
|
||||
|
||||
if (Ext.isArray(values.secret) && !values.secret.length) {
|
||||
delete values.secret;
|
||||
if (!me.isCreate) {
|
||||
Proxmox.Utils.assemble_field_data(values, { 'delete': 'secret' });
|
||||
}
|
||||
}
|
||||
delete values.enable;
|
||||
|
||||
return values;
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('Proxmox.form.WebhookKeyValueList', {
|
||||
extend: 'Ext.form.FieldContainer',
|
||||
alias: 'widget.pmxWebhookKeyValueList',
|
||||
|
||||
mixins: [
|
||||
'Ext.form.field.Field',
|
||||
],
|
||||
|
||||
// override for column header
|
||||
fieldTitle: gettext('Item'),
|
||||
|
||||
// label displayed in the "Add" button
|
||||
addLabel: undefined,
|
||||
|
||||
// will be applied to the textfields
|
||||
maskRe: undefined,
|
||||
|
||||
allowBlank: true,
|
||||
selectAll: false,
|
||||
isFormField: true,
|
||||
deleteEmpty: false,
|
||||
config: {
|
||||
deleteEmpty: false,
|
||||
maskValues: false,
|
||||
},
|
||||
|
||||
setValue: function(list) {
|
||||
let me = this;
|
||||
|
||||
list = Ext.isArray(list) ? list : (list ?? '').split(';').filter(t => t !== '');
|
||||
|
||||
let store = me.lookup('grid').getStore();
|
||||
if (list.length > 0) {
|
||||
store.setData(list.map(item => {
|
||||
let properties = Proxmox.Utils.parsePropertyString(item);
|
||||
|
||||
// decode base64
|
||||
let value = me.maskValues ? '' : Proxmox.Utils.base64ToUtf8(properties.value);
|
||||
|
||||
let obj = {
|
||||
headerName: properties.name,
|
||||
headerValue: value,
|
||||
};
|
||||
|
||||
if (!me.isCreate && me.maskValues) {
|
||||
obj.emptyText = gettext('Unchanged');
|
||||
}
|
||||
|
||||
return obj;
|
||||
}));
|
||||
} else {
|
||||
store.removeAll();
|
||||
}
|
||||
me.checkChange();
|
||||
return me;
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
let me = this;
|
||||
let values = [];
|
||||
me.lookup('grid').getStore().each((rec) => {
|
||||
if (rec.data.headerName) {
|
||||
let obj = {
|
||||
name: rec.data.headerName,
|
||||
value: Proxmox.Utils.utf8ToBase64(rec.data.headerValue),
|
||||
};
|
||||
|
||||
values.push(Proxmox.Utils.printPropertyString(obj));
|
||||
}
|
||||
});
|
||||
|
||||
return values;
|
||||
},
|
||||
|
||||
getErrors: function(value) {
|
||||
let me = this;
|
||||
let empty = false;
|
||||
|
||||
me.lookup('grid').getStore().each((rec) => {
|
||||
if (!rec.data.headerName) {
|
||||
empty = true;
|
||||
}
|
||||
|
||||
if (!rec.data.headerValue && rec.data.newValue) {
|
||||
empty = true;
|
||||
}
|
||||
|
||||
if (!rec.data.headerValue && !me.maskValues) {
|
||||
empty = true;
|
||||
}
|
||||
});
|
||||
if (empty) {
|
||||
return [gettext('Name/value must not be empty.')];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
// override framework function to implement deleteEmpty behaviour
|
||||
getSubmitData: function() {
|
||||
let me = this,
|
||||
data = null,
|
||||
val;
|
||||
if (!me.disabled && me.submitValue) {
|
||||
val = me.getValue();
|
||||
if (val !== null && val !== '') {
|
||||
data = {};
|
||||
data[me.getName()] = val;
|
||||
} else if (me.getDeleteEmpty()) {
|
||||
data = {};
|
||||
data.delete = me.getName();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
addLine: function() {
|
||||
let me = this;
|
||||
me.lookup('grid').getStore().add({
|
||||
headerName: '',
|
||||
headerValue: '',
|
||||
emptyText: gettext('Value'),
|
||||
newValue: true,
|
||||
});
|
||||
},
|
||||
|
||||
removeSelection: function(field) {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
let grid = me.lookup('grid');
|
||||
|
||||
let record = field.getWidgetRecord();
|
||||
if (record === undefined) {
|
||||
// this is sometimes called before a record/column is initialized
|
||||
return;
|
||||
}
|
||||
|
||||
grid.getStore().remove(record);
|
||||
view.checkChange();
|
||||
view.validate();
|
||||
},
|
||||
|
||||
itemChange: function(field, newValue) {
|
||||
let rec = field.getWidgetRecord();
|
||||
if (!rec) {
|
||||
return;
|
||||
}
|
||||
|
||||
let column = field.getWidgetColumn();
|
||||
rec.set(column.dataIndex, newValue);
|
||||
let list = field.up('pmxWebhookKeyValueList');
|
||||
list.checkChange();
|
||||
list.validate();
|
||||
},
|
||||
|
||||
control: {
|
||||
'grid button': {
|
||||
click: 'removeSelection',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
let items = [
|
||||
{
|
||||
xtype: 'grid',
|
||||
reference: 'grid',
|
||||
minHeight: 100,
|
||||
maxHeight: 100,
|
||||
scrollable: 'vertical',
|
||||
|
||||
viewConfig: {
|
||||
deferEmptyText: false,
|
||||
},
|
||||
|
||||
store: {
|
||||
listeners: {
|
||||
update: function() {
|
||||
this.commitChanges();
|
||||
},
|
||||
},
|
||||
},
|
||||
margin: '5 0 5 0',
|
||||
columns: [
|
||||
{
|
||||
header: me.fieldTtitle,
|
||||
dataIndex: 'headerName',
|
||||
xtype: 'widgetcolumn',
|
||||
widget: {
|
||||
xtype: 'textfield',
|
||||
isFormField: false,
|
||||
maskRe: me.maskRe,
|
||||
allowBlank: false,
|
||||
queryMode: 'local',
|
||||
emptyText: gettext('Key'),
|
||||
listeners: {
|
||||
change: 'itemChange',
|
||||
},
|
||||
},
|
||||
onWidgetAttach: function(_col, widget) {
|
||||
widget.isValid();
|
||||
},
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: me.fieldTtitle,
|
||||
dataIndex: 'headerValue',
|
||||
xtype: 'widgetcolumn',
|
||||
widget: {
|
||||
xtype: 'proxmoxtextfield',
|
||||
inputType: me.maskValues ? 'password' : 'text',
|
||||
isFormField: false,
|
||||
maskRe: me.maskRe,
|
||||
queryMode: 'local',
|
||||
listeners: {
|
||||
change: 'itemChange',
|
||||
},
|
||||
allowBlank: !me.isCreate && me.maskValues,
|
||||
|
||||
bind: {
|
||||
emptyText: '{record.emptyText}',
|
||||
},
|
||||
},
|
||||
onWidgetAttach: function(_col, widget) {
|
||||
widget.isValid();
|
||||
},
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
xtype: 'widgetcolumn',
|
||||
width: 40,
|
||||
widget: {
|
||||
xtype: 'button',
|
||||
iconCls: 'fa fa-trash-o',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
text: me.addLabel ? me.addLabel : gettext('Add'),
|
||||
iconCls: 'fa fa-plus-circle',
|
||||
handler: 'addLine',
|
||||
},
|
||||
];
|
||||
|
||||
for (const [key, value] of Object.entries(me.gridConfig ?? {})) {
|
||||
items[0][key] = value;
|
||||
}
|
||||
|
||||
Ext.apply(me, {
|
||||
items,
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
me.initField();
|
||||
},
|
||||
});
|
||||
47
src/proxmox-dark/Makefile
Normal file
@ -0,0 +1,47 @@
|
||||
include ../defines.mk
|
||||
|
||||
SCSSSRC=scss/ProxmoxDark.scss \
|
||||
scss/abstracts/_mixins.scss \
|
||||
scss/abstracts/_variables.scss \
|
||||
scss/extjs/_body.scss \
|
||||
scss/extjs/form/_button.scss \
|
||||
scss/extjs/form/_combobox.scss \
|
||||
scss/extjs/form/_formfield.scss \
|
||||
scss/extjs/_grid.scss \
|
||||
scss/extjs/_menu.scss \
|
||||
scss/extjs/_panel.scss \
|
||||
scss/extjs/_presentation.scss \
|
||||
scss/extjs/_progress.scss \
|
||||
scss/extjs/_splitter.scss \
|
||||
scss/extjs/_tabbar.scss \
|
||||
scss/extjs/_tip.scss \
|
||||
scss/extjs/_toolbar.scss \
|
||||
scss/extjs/_treepanel.scss \
|
||||
scss/extjs/_window.scss \
|
||||
scss/other/_charts.scss \
|
||||
scss/other/_icons.scss \
|
||||
scss/proxmox/_general.scss \
|
||||
scss/proxmox/_helpbutton.scss \
|
||||
scss/proxmox/_loadingindicator.scss \
|
||||
scss/proxmox/_markdown.scss \
|
||||
scss/proxmox/_nodes.scss \
|
||||
scss/proxmox/_quarantine.scss \
|
||||
scss/proxmox/_storages.scss \
|
||||
scss/proxmox/_tags.scss \
|
||||
scss/proxmox/_datepicker.scss
|
||||
|
||||
.PHONY: all
|
||||
all: theme-proxmox-dark.css
|
||||
|
||||
.PHONY: install
|
||||
install: theme-proxmox-dark.css
|
||||
install -d $(WWWTHEMEDIR)/
|
||||
install -m 0664 theme-proxmox-dark.css $(WWWTHEMEDIR)/
|
||||
|
||||
theme-proxmox-dark.css: $(SCSSSRC)
|
||||
sassc -t compressed $< $@.tmp
|
||||
mv $@.tmp $@
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf theme-proxmox-dark.css
|
||||
37
src/proxmox-dark/scss/ProxmoxDark.scss
Normal file
@ -0,0 +1,37 @@
|
||||
@charset "utf-8";
|
||||
|
||||
// Abstracts
|
||||
@import "abstracts/mixins";
|
||||
@import "abstracts/variables";
|
||||
|
||||
// Chart, Icon, Keyboar-mode fixups
|
||||
@import "other/charts";
|
||||
@import "other/icons";
|
||||
|
||||
// ExtJS re-stylings
|
||||
@import "extjs/form/button";
|
||||
@import "extjs/form/combobox";
|
||||
@import "extjs/form/formfield";
|
||||
@import "extjs/grid";
|
||||
@import "extjs/menu";
|
||||
@import "extjs/panel";
|
||||
@import "extjs/progress";
|
||||
@import "extjs/splitter";
|
||||
@import "extjs/tabbar";
|
||||
@import "extjs/tip";
|
||||
@import "extjs/toolbar";
|
||||
@import "extjs/treepanel";
|
||||
@import "extjs/window";
|
||||
@import "extjs/body";
|
||||
@import "extjs/presentation";
|
||||
|
||||
// Proxmox re-stylings
|
||||
@import "proxmox/general";
|
||||
@import "proxmox/helpbutton";
|
||||
@import "proxmox/loadingindicator";
|
||||
@import "proxmox/markdown";
|
||||
@import "proxmox/nodes";
|
||||
@import "proxmox/quarantine";
|
||||
@import "proxmox/storages";
|
||||
@import "proxmox/tags";
|
||||
@import "proxmox/datepicker";
|
||||
5
src/proxmox-dark/scss/abstracts/_mixins.scss
Normal file
@ -0,0 +1,5 @@
|
||||
// selected items in dropdown etc
|
||||
@mixin selection {
|
||||
background-color: $selection-background-color;
|
||||
color: $selection-background-text-color;
|
||||
}
|
||||
67
src/proxmox-dark/scss/abstracts/_variables.scss
Normal file
@ -0,0 +1,67 @@
|
||||
// Primary colors
|
||||
$primary-color: hsl(205deg, 100%, 32.25%);
|
||||
$primary-light: hsl(205deg, 100%, 40.5%);
|
||||
$primary-dark: hsl(205deg, 100%, 25%);
|
||||
|
||||
// Hightlighted Text (Links, Headers, etc.)
|
||||
$highlighted-text: hsl(205deg, 100%, 65%);
|
||||
$highlighted-text-alt: hsl(205deg, 100%, 80%);
|
||||
$highlighted-text-crit: hsl(360deg, 100%, 65%);
|
||||
|
||||
// Icon and Text colors
|
||||
$text-color: hsl(0deg, 0%, 95%);
|
||||
$text-color-inactive: hsl(0deg, 0%, 60%);
|
||||
$icon-color: hsl(0deg, 0%, 90%);
|
||||
$icon-color-alt: hsl(0deg, 0%, 55%);
|
||||
|
||||
// Borders
|
||||
$border-color: hsl(0deg, 0%, 40%);
|
||||
$border-color-alt: hsl(0deg, 0%, 25%);
|
||||
|
||||
// Backgrounds
|
||||
$content-background-color: hsl(0deg, 0%, 20%);
|
||||
$content-background-selected: hsl(0deg, 0%, 30%);
|
||||
$background-dark: hsl(0deg, 0%, 20%);
|
||||
$background-darker: hsl(0deg, 0%, 15%);
|
||||
$background-darkest: hsl(0deg, 0%, 10%);
|
||||
$background-invalid: hsl(360deg, 60%, 20%);
|
||||
$background-warning: hsl(40deg, 100%, 20%);
|
||||
|
||||
// Buttons
|
||||
$neutral-button-color: hsl(0deg, 0%, 25%);
|
||||
$neutral-button-color-alt: hsl(0deg, 0%, 35%);
|
||||
$neutral-button-text-color: hsl(0deg, 0%, 95%);
|
||||
$neutral-button-icon-color: $neutral-button-text-color;
|
||||
|
||||
// Help Buttons
|
||||
$help-button-color: hsl(0deg, 0%, 65%);
|
||||
$help-button-color-alt: hsl(0deg, 0%, 75%);
|
||||
$help-button-text-color: hsl(0deg, 0%, 10%);
|
||||
$help-button-icon-color: $help-button-text-color;
|
||||
|
||||
// Selection Colors
|
||||
$selection-background-color: hsl(0deg, 0%, 35%);
|
||||
$selection-background-text-color: hsl(0deg, 0%, 100%);
|
||||
|
||||
// Other
|
||||
$form-field-body-color: $background-dark;
|
||||
$bottom-splitter-color: hsl(0deg, 0%, 5%);
|
||||
|
||||
// Some icons are black and do not respect the 'color' style property.
|
||||
// For the dark mode these can be turned grey or white with the
|
||||
// 'filter: invert(value)' attribute
|
||||
$icon-brightness: lightness($icon-color);
|
||||
|
||||
// Spam score colors
|
||||
|
||||
// for spam scores with an absolute score >= 3
|
||||
$spam-high-neg: hsl(205deg, 65%, 20%);
|
||||
$spam-high-pos: hsl(360deg, 55%, 20%);
|
||||
|
||||
// for spam scores with an absolute score between 0.1 and 3
|
||||
$spam-mid-neg: hsl(205deg, 65%, 30%);
|
||||
$spam-mid-pos: hsl(360deg, 55%, 30%);
|
||||
|
||||
// for spam scores with an absolute score <= 0.1
|
||||
$spam-low-neg: hsl(205deg, 65%, 40%);
|
||||
$spam-low-pos: hsl(360deg, 55%, 40%);
|
||||
23
src/proxmox-dark/scss/extjs/_body.scss
Normal file
@ -0,0 +1,23 @@
|
||||
// Chrome 81, Firefox 96 and Safari 13 support a dark version of the
|
||||
// scrollbar and form controls source
|
||||
// https://stackoverflow.com/q/65940522
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.x-body {
|
||||
color: $text-color;
|
||||
background-color: $background-darkest;
|
||||
}
|
||||
|
||||
// Should be the absolute background of the document
|
||||
.x-viewport > .x-body {
|
||||
background-color: $background-darkest;
|
||||
}
|
||||
|
||||
// necessary for some masks to work properly (e.g. when hidding the
|
||||
// attachment grid in pmg)
|
||||
body.x-border-layout-ct,
|
||||
div.x-border-layout-ct {
|
||||
background-color: $background-darkest;
|
||||
}
|
||||
148
src/proxmox-dark/scss/extjs/_grid.scss
Normal file
@ -0,0 +1,148 @@
|
||||
.x-column-header {
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
|
||||
.x-grid-item,
|
||||
.x-column-header-default,
|
||||
// the row number field (e.g. in the ipsets in pve)
|
||||
.x-grid-cell-row-numberer {
|
||||
color: $text-color;
|
||||
background-color: $background-darker;
|
||||
}
|
||||
|
||||
// Trigger in grid/table header cells
|
||||
.x-column-header-trigger {
|
||||
border-color: $border-color;
|
||||
}
|
||||
|
||||
.x-grid-cell-special {
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
|
||||
.x-grid-group-hd {
|
||||
background-color: $background-darker;
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
|
||||
.x-grid-group-title {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
// Border-top in tables
|
||||
.x-grid-header-ct {
|
||||
border: solid 1px $background-dark;
|
||||
background-color: $background-dark;
|
||||
}
|
||||
|
||||
// alternating row colors
|
||||
.x-grid-item-alt {
|
||||
background-color: $background-darkest;
|
||||
}
|
||||
|
||||
.x-grid-with-row-lines {
|
||||
.x-grid-item {
|
||||
border-color: $border-color-alt;
|
||||
border-right: 0;
|
||||
|
||||
// A border at the bottom of tables
|
||||
&:last-child {
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
|
||||
// A border at the top of tables
|
||||
&:first-child {
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
|
||||
// hovered row in a grid
|
||||
&.x-grid-item-over,
|
||||
&.x-grid-item-selected {
|
||||
background-color: $selection-background-color;
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
}
|
||||
|
||||
// borders on selected elements
|
||||
.x-grid-item-selected + .x-grid-item,
|
||||
.x-grid-item-over + .x-grid-item {
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
}
|
||||
|
||||
// Sometimes a selected node in the ResourceTree loses the
|
||||
// selection-background-color
|
||||
.x-grid-item-over,
|
||||
.x-grid-item-selected {
|
||||
// Otherwise .x-grid-item overrides the background color
|
||||
background-color: $selection-background-color;
|
||||
}
|
||||
|
||||
// Hovering over a grid/table header cell
|
||||
.x-column-header-over,
|
||||
// When opening the sort/settings header of a table/grid header cell
|
||||
.x-column-header-open,
|
||||
.x-column-header-last .x-column-header-over .x-column-header-trigger, {
|
||||
background-color: $content-background-selected;
|
||||
}
|
||||
|
||||
// header element that the grid is currently sorted by
|
||||
.x-column-header-sort-ASC,
|
||||
.x-column-header-sort-DESC {
|
||||
background-color: mix($background-darker, $primary-color, 70%);
|
||||
}
|
||||
|
||||
// summary rows (e.g. ceph pools last row)
|
||||
.x-grid-row-summary {
|
||||
.x-grid-cell,
|
||||
.x-grid-rowwrap,
|
||||
.x-grid-cell-rowbody {
|
||||
// the "!important" is needed here, because crisp also sets this
|
||||
// as important
|
||||
background-color: $background-darker !important;
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
}
|
||||
|
||||
.x-grid-with-col-lines {
|
||||
.x-grid-cell,
|
||||
.x-grid-item-over .x-grid-cell,
|
||||
.x-grid-item-selected .x-grid-cell {
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
}
|
||||
|
||||
// drag and drop proxy
|
||||
.x-dd-drag-proxy {
|
||||
background-color: $background-darkest;
|
||||
border-color: $border-color-alt;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.x-keyboard-mode .x-grid-item-focused {
|
||||
@include selection;
|
||||
|
||||
.x-grid-cell-inner::before {
|
||||
border-color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Grid/table headers that are selected and active
|
||||
.x-keyboard-mode .x-column-header.x-column-header-focus {
|
||||
color: $text-color;
|
||||
|
||||
// Elements in table
|
||||
.x-column-header-inner::after {
|
||||
border-color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.x-keyboard-mode .proxmox-invalid-row .x-grid-item-focused {
|
||||
background-color: $background-invalid;
|
||||
}
|
||||
|
||||
// As far as I can tell only used under Node > "System" >
|
||||
// "Certificates"
|
||||
.x-grid-empty {
|
||||
background-color: $background-darker;
|
||||
color: $text-color;
|
||||
}
|
||||
40
src/proxmox-dark/scss/extjs/_menu.scss
Normal file
@ -0,0 +1,40 @@
|
||||
.x-menu-default {
|
||||
border-color: $form-field-body-color;
|
||||
}
|
||||
|
||||
.x-menu-body-default {
|
||||
background-color: $form-field-body-color;
|
||||
}
|
||||
|
||||
// E.g. the content menu in the resource tree displays a header
|
||||
.x-menu-header {
|
||||
background-color: $primary-color;
|
||||
}
|
||||
|
||||
.x-menu-item-default {
|
||||
// Horizontal divider in menu (e.g. in UserInfo above "Logout")
|
||||
&.x-menu-item-separator {
|
||||
background-color: $background-dark;
|
||||
border-color: $border-color;
|
||||
}
|
||||
|
||||
// When hovering over a menu item
|
||||
&.x-menu-item-focus,
|
||||
&.x-menu-item-active {
|
||||
@include selection;
|
||||
}
|
||||
}
|
||||
|
||||
.x-menu-item-text-default {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.x-menu-item-icon-default {
|
||||
color: $icon-color;
|
||||
}
|
||||
|
||||
// Vertical divider (e.g. in UserInfo between icons and text)
|
||||
.x-menu-icon-separator-default {
|
||||
background-color: $background-dark;
|
||||
border-color: $border-color;
|
||||
}
|
||||
58
src/proxmox-dark/scss/extjs/_panel.scss
Normal file
@ -0,0 +1,58 @@
|
||||
.x-panel-header-default {
|
||||
background-color: $content-background-color;
|
||||
border: none;
|
||||
|
||||
.x-tool-tool-el {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// The small navigation elements in the panel header bar e.g. to
|
||||
// collapse a panel
|
||||
.x-tool-img {
|
||||
filter: brightness(175%);
|
||||
|
||||
// these are brighter per default, so they don't need to be
|
||||
// brigthened as much
|
||||
&.x-tool-expand,
|
||||
&.x-tool-collapse,
|
||||
&.x-tool-refresh {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
// this icon uses multiple tones, to have them behave appropriatelly
|
||||
// invert them before brightening them
|
||||
&.x-tool-print {
|
||||
filter: invert(100%) hue-rotate(180deg) brightness(125%);
|
||||
}
|
||||
|
||||
.x-tool-over & {
|
||||
filter: brightness(200%);
|
||||
}
|
||||
|
||||
.x-tool-over &.x-tool-expand,
|
||||
.x-tool-over &.x-tool-collapse,
|
||||
.x-tool-over &.x-tool-refresh {
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
.x-tool-over &.x-tool-print {
|
||||
filter: invert(100%) hue-rotate(180deg) brightness(150%);
|
||||
}
|
||||
}
|
||||
|
||||
.x-panel-header-title-default {
|
||||
color: $highlighted-text;
|
||||
}
|
||||
|
||||
.x-panel-body-default {
|
||||
background-color: $background-darker;
|
||||
border-color: $border-color-alt;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
// override the border around the pve-resource-tree specifically to be
|
||||
// more consistent with crisp, while keep allignments "correct"
|
||||
div[id^="pveResourceTree-"][id$="-body"] {
|
||||
border-color: $background-darker;
|
||||
}
|
||||
13
src/proxmox-dark/scss/extjs/_presentation.scss
Normal file
@ -0,0 +1,13 @@
|
||||
// The mask that is applied when the window is unaccessible (Login
|
||||
// screen, Loading, ...)
|
||||
.x-mask {
|
||||
background-color: rgba($background-darker, 0.5);
|
||||
}
|
||||
|
||||
// Shadows of floating windows like window modals, form selectors and
|
||||
// message boxes
|
||||
.x-css-shadow {
|
||||
// the additional styling from the pve css overwrites the setting on
|
||||
// the element with "!important", that's why we need it here.
|
||||
box-shadow: black 0 -1px 15px 5px !important;
|
||||
}
|
||||
19
src/proxmox-dark/scss/extjs/_progress.scss
Normal file
@ -0,0 +1,19 @@
|
||||
.x-progress-default {
|
||||
background-color: $form-field-body-color;
|
||||
|
||||
.x-progress-bar-default {
|
||||
background-color: $primary-color; // Taken from the chart
|
||||
}
|
||||
|
||||
.x-progress-text {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.x-progress.warning .x-progress-bar {
|
||||
background-color: var(--pwt-gauge-warn);
|
||||
}
|
||||
|
||||
.x-progress.critical .x-progress-bar {
|
||||
background-color: var(--pwt-gauge-crit);
|
||||
}
|
||||
18
src/proxmox-dark/scss/extjs/_splitter.scss
Normal file
@ -0,0 +1,18 @@
|
||||
// Splitters separating two views (e.g. Firewall > "Security Group",
|
||||
// "IPSet", ...)
|
||||
.x-splitter {
|
||||
background-color: $background-darkest;
|
||||
}
|
||||
|
||||
.x-splitter-horizontal {
|
||||
background-color: $bottom-splitter-color;
|
||||
}
|
||||
|
||||
// Splitters that separate content and resize parts of the window
|
||||
.x-keyboard-mode .x-splitter-focus::after {
|
||||
border-color: $primary-color;
|
||||
}
|
||||
|
||||
.x-layout-split-bottom {
|
||||
opacity: 0.7;
|
||||
}
|
||||
45
src/proxmox-dark/scss/extjs/_tabbar.scss
Normal file
@ -0,0 +1,45 @@
|
||||
// The header of the tabbar
|
||||
.x-tab-bar-default {
|
||||
background-color: $background-darker;
|
||||
}
|
||||
|
||||
.x-tab-default {
|
||||
// Hovering over a tab button
|
||||
&.x-tab-over {
|
||||
background-color: $primary-dark;
|
||||
border-color: $primary-dark;
|
||||
}
|
||||
|
||||
// Selected tab buttons
|
||||
&.x-tab.x-tab-active {
|
||||
background-color: $primary-light;
|
||||
border-color: $primary-light;
|
||||
}
|
||||
|
||||
// Disabled tab buttons
|
||||
&.x-tab.x-tab-disabled {
|
||||
background-color: $background-darker;
|
||||
|
||||
// make the border invisible so it matches the light theme, setting
|
||||
// it to none messes with the allignment of the elements.
|
||||
border-color: transparent;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.x-keyboard-mode &.x-tab-focus,
|
||||
.x-keyboard-mode &.x-tab-focus.x-tab-over,
|
||||
.x-keyboard-mode &.x-tab-focus.x-tab-active {
|
||||
background-color: $primary-color;
|
||||
border-color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Not selected tab buttons
|
||||
.x-tab-default-top {
|
||||
background-color: $background-darker;
|
||||
border-color: $background-darker;
|
||||
}
|
||||
|
||||
.x-tab-inner-default {
|
||||
color: $text-color;
|
||||
}
|
||||
18
src/proxmox-dark/scss/extjs/_tip.scss
Normal file
@ -0,0 +1,18 @@
|
||||
.x-tip-default {
|
||||
background-color: $background-darkest;
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
|
||||
.x-tip-body-default {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
// Form error tip
|
||||
.x-tip-form-invalid {
|
||||
background-color: $background-dark;
|
||||
border-color: $border-color-alt;
|
||||
}
|
||||
|
||||
.x-tip-body-form-invalid {
|
||||
color: $text-color;
|
||||
}
|
||||