mirror of
https://github.com/google/nomulus
synced 2026-05-14 11:51:43 +00:00
Compare commits
571 Commits
nomulus-20
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f44642fe28 | ||
|
|
6e77a6b0e7 | ||
|
|
859f356466 | ||
|
|
fa15a66d9a | ||
|
|
76131fbd4e | ||
|
|
1196cd163b | ||
|
|
4a412546f9 | ||
|
|
6be0a32741 | ||
|
|
8cf222d1c9 | ||
|
|
903414c76b | ||
|
|
925482ea58 | ||
|
|
653704811b | ||
|
|
3de790fb00 | ||
|
|
9d5650132b | ||
|
|
30b507cc79 | ||
|
|
ea1ad7901c | ||
|
|
c427f2cc9f | ||
|
|
db733aa50f | ||
|
|
409a7ba66f | ||
|
|
e85f48beba | ||
|
|
b78d12e73f | ||
|
|
4a1d0609f3 | ||
|
|
61b121f464 | ||
|
|
074f78cfb3 | ||
|
|
7be5fe4c01 | ||
|
|
1876e2c3e8 | ||
|
|
49f14b5e1b | ||
|
|
d2881b47dc | ||
|
|
9f3dfec118 | ||
|
|
60e84e72d7 | ||
|
|
aedfdd47f1 | ||
|
|
9ca75b2294 | ||
|
|
03b3f9f5a0 | ||
|
|
193ccb5ad3 | ||
|
|
a129a0dc21 | ||
|
|
3513364c97 | ||
|
|
59b44b60df | ||
|
|
8c9b38e6af | ||
|
|
e5c0c27458 | ||
|
|
301a6681f5 | ||
|
|
2dd7cee3e5 | ||
|
|
497874eaa2 | ||
|
|
f2cfd36b73 | ||
|
|
8ea5fe3774 | ||
|
|
9544d70048 | ||
|
|
50a639937a | ||
|
|
72016b1e5f | ||
|
|
25fcef8a5b | ||
|
|
186dd80567 | ||
|
|
c52983fb61 | ||
|
|
8a3ab00e58 | ||
|
|
49df9c325a | ||
|
|
929dccbfe3 | ||
|
|
ee8746c857 | ||
|
|
c7f2db177b | ||
|
|
6747cc894d | ||
|
|
e4c4149033 | ||
|
|
e24c90fea6 | ||
|
|
8ff4d7dc8a | ||
|
|
88906f1bd9 | ||
|
|
bca05f3982 | ||
|
|
763630bca5 | ||
|
|
140b19e919 | ||
|
|
a787660b27 | ||
|
|
4aadcf818a | ||
|
|
ab29e481fa | ||
|
|
f2f9694a94 | ||
|
|
3f8145b44f | ||
|
|
1fdacf25dc | ||
|
|
41d26d8385 | ||
|
|
71c9407f07 | ||
|
|
a138806199 | ||
|
|
a5c1412aac | ||
|
|
41393e5f8d | ||
|
|
a7387e975b | ||
|
|
5c6667507b | ||
|
|
c187c92ae4 | ||
|
|
22ca4e3f2b | ||
|
|
f27136458a | ||
|
|
d8e647316e | ||
|
|
d6e0a7b979 | ||
|
|
5725eb95e0 | ||
|
|
aa12998276 | ||
|
|
d415416bc5 | ||
|
|
3a1068f313 | ||
|
|
69e5d40f04 | ||
|
|
64f6cd9af4 | ||
|
|
40184689ca | ||
|
|
826ad85d20 | ||
|
|
2b47bc9b0a | ||
|
|
9555dca8c6 | ||
|
|
49484c06d3 | ||
|
|
81d222e7d6 | ||
|
|
7e9d4c27d1 | ||
|
|
f9c22ff1c5 | ||
|
|
2562d582f3 | ||
|
|
6f0bc1ded9 | ||
|
|
db9fc3271d | ||
|
|
84491fde70 | ||
|
|
0519e2ffcf | ||
|
|
85f75494ab | ||
|
|
cbba91558a | ||
|
|
c24f09febc | ||
|
|
fd51035f23 | ||
|
|
90eb078e3f | ||
|
|
2a94bdc257 | ||
|
|
50fa49e0c0 | ||
|
|
a581259edb | ||
|
|
fcdac3e86e | ||
|
|
b652f81193 | ||
|
|
d98d65eee5 | ||
|
|
28e72bd0d0 | ||
|
|
0777be3d6c | ||
|
|
f9cd167ae4 | ||
|
|
eed1886121 | ||
|
|
7149fd3307 | ||
|
|
0dc7ab99d7 | ||
|
|
76d4dfbb04 | ||
|
|
8547ad7941 | ||
|
|
b1266c95e8 | ||
|
|
bc9aab6790 | ||
|
|
6cb669a5a7 | ||
|
|
0f92e98028 | ||
|
|
5f0526c07a | ||
|
|
759aaddb5f | ||
|
|
816180f3b3 | ||
|
|
bf66b374c6 | ||
|
|
666cee1d9f | ||
|
|
d4a70c29a8 | ||
|
|
7b8d07954b | ||
|
|
34bea69a48 | ||
|
|
363800bd86 | ||
|
|
dee132d04b | ||
|
|
847ef12a4f | ||
|
|
d9349be18e | ||
|
|
0c74883428 | ||
|
|
b357fc79f7 | ||
|
|
754e7fbddc | ||
|
|
ad07b32638 | ||
|
|
8f69b48e87 | ||
|
|
c33f0dc07f | ||
|
|
969353d4e2 | ||
|
|
6cd351ec7c | ||
|
|
19e03dbd2e | ||
|
|
fc1eb162f2 | ||
|
|
ed25854fbc | ||
|
|
0aa6bc6aaa | ||
|
|
ff4c326ebe | ||
|
|
51b579871a | ||
|
|
b144aafb22 | ||
|
|
ddd955e156 | ||
|
|
6863f678f1 | ||
|
|
6bd90e967b | ||
|
|
5faf3d283c | ||
|
|
149fb66ac5 | ||
|
|
8c96940a27 | ||
|
|
9c5510f05d | ||
|
|
84884de77b | ||
|
|
d6c35df9bc | ||
|
|
7caa0ec9d6 | ||
|
|
ee3866ec4a | ||
|
|
97d0b7680f | ||
|
|
5700a008d6 | ||
|
|
dc9f5b99bc | ||
|
|
d3c6de7a38 | ||
|
|
3c3303c16a | ||
|
|
2a86a1bbe9 | ||
|
|
ea148ac13e | ||
|
|
06299ccb86 | ||
|
|
732c30b359 | ||
|
|
ee5a2d3916 | ||
|
|
2b5643df4c | ||
|
|
6bbd7a2290 | ||
|
|
77ab80f3dc | ||
|
|
5e1cd0120f | ||
|
|
0167dad85f | ||
|
|
1eaf3d4aa8 | ||
|
|
d9c46170dd | ||
|
|
e8a475f48b | ||
|
|
bdaab9daa5 | ||
|
|
7e07fabf7e | ||
|
|
16859bb36a | ||
|
|
7c92928f2c | ||
|
|
de8d205657 | ||
|
|
4738b979e4 | ||
|
|
a61a667992 | ||
|
|
1164070576 | ||
|
|
d23640a54f | ||
|
|
cc347264f1 | ||
|
|
e5d4cbb9fc | ||
|
|
8c1e0ff4de | ||
|
|
5cef2dd8b5 | ||
|
|
62b2585220 | ||
|
|
8692fe35db | ||
|
|
18614ba11e | ||
|
|
427f6db820 | ||
|
|
5aa40b2208 | ||
|
|
95c89bc856 | ||
|
|
c21b66f0fb | ||
|
|
b070c46231 | ||
|
|
a20eb8d1e9 | ||
|
|
338b8edb97 | ||
|
|
9f191e9392 | ||
|
|
39c2a79898 | ||
|
|
e2e9d4cfc7 | ||
|
|
2948dcc1be | ||
|
|
c5644d5c8b | ||
|
|
514d24ed67 | ||
|
|
c6868b771b | ||
|
|
f34aec8b56 | ||
|
|
b27b077638 | ||
|
|
0e8cd75a58 | ||
|
|
2a1748ba9c | ||
|
|
f4889191a4 | ||
|
|
9eddecf70f | ||
|
|
d4bcff0c31 | ||
|
|
62065f88fb | ||
|
|
c9ac9437fd | ||
|
|
1f6a09182d | ||
|
|
a0eff00031 | ||
|
|
89698c6ed6 | ||
|
|
a7696c3fac | ||
|
|
7ec599f849 | ||
|
|
70291af9ad | ||
|
|
5fb95f38ed | ||
|
|
dfe8e24761 | ||
|
|
bd30fcc81c | ||
|
|
8cecc8d3a8 | ||
|
|
c5a39bccc5 | ||
|
|
a90a117341 | ||
|
|
b40ad54daf | ||
|
|
b4d239c329 | ||
|
|
daa7ab3bfa | ||
|
|
56cd2ad282 | ||
|
|
0472dda860 | ||
|
|
083a9dc8c9 | ||
|
|
0153c6284a | ||
|
|
ca240adfb6 | ||
|
|
b17125ae9a | ||
|
|
dfef733360 | ||
|
|
04a0659197 | ||
|
|
70010886b1 | ||
|
|
3cd50dc929 | ||
|
|
03872b508f | ||
|
|
1096f201cd | ||
|
|
9dc3215624 | ||
|
|
af321fb65e | ||
|
|
c5132c04be | ||
|
|
a64dc21f96 | ||
|
|
0381533a35 | ||
|
|
4999a72d96 | ||
|
|
2d072c3844 | ||
|
|
c15dec4419 | ||
|
|
8340125bf4 | ||
|
|
98ba80d94e | ||
|
|
967d04efce | ||
|
|
20fd944e83 | ||
|
|
daa56e6d85 | ||
|
|
ed33c7424d | ||
|
|
04b30f5c04 | ||
|
|
11702bc940 | ||
|
|
2d82646421 | ||
|
|
50260dca5f | ||
|
|
3cc10bfe0d | ||
|
|
5645b2e218 | ||
|
|
2a01c12b14 | ||
|
|
93d77e558f | ||
|
|
92ebd0dedb | ||
|
|
b49e37feee | ||
|
|
bede56598c | ||
|
|
467d9c7bf1 | ||
|
|
e5ebe96c74 | ||
|
|
2ff4d97b0a | ||
|
|
6b0beeb477 | ||
|
|
d2d43f4115 | ||
|
|
12fd206c35 | ||
|
|
a3f510d0db | ||
|
|
fa54c26ee2 | ||
|
|
8896fb94f4 | ||
|
|
6c7bf5e5dd | ||
|
|
ea1e8d5cc5 | ||
|
|
7fb846c5b0 | ||
|
|
5180095cb6 | ||
|
|
9fe64bf9ec | ||
|
|
0f3b62d5ce | ||
|
|
bd4701647b | ||
|
|
fb816d7a2c | ||
|
|
8fbf363195 | ||
|
|
397f800614 | ||
|
|
bcf42bd287 | ||
|
|
ed95d19b93 | ||
|
|
97fc2c0b66 | ||
|
|
00728c40ba | ||
|
|
3f2a42ab8d | ||
|
|
b73e342820 | ||
|
|
df7fec7a3e | ||
|
|
6f7ae1eabc | ||
|
|
eb978ebbd5 | ||
|
|
95831bc8b7 | ||
|
|
538260521b | ||
|
|
612708f0a8 | ||
|
|
e78de98060 | ||
|
|
c918258fb1 | ||
|
|
34103ec815 | ||
|
|
a63812160e | ||
|
|
9aaf7ee36a | ||
|
|
96a864dbd6 | ||
|
|
8a36fb5f1f | ||
|
|
6c138420b0 | ||
|
|
08570511f5 | ||
|
|
e62d970d34 | ||
|
|
067927b735 | ||
|
|
4ec2919ce3 | ||
|
|
19422075fa | ||
|
|
40b6984ffb | ||
|
|
6952e0f653 | ||
|
|
dcb55d27bb | ||
|
|
765bd9834a | ||
|
|
221088e738 | ||
|
|
6649e00df7 | ||
|
|
2ceb52a7c4 | ||
|
|
120bcc33be | ||
|
|
8987fd37c2 | ||
|
|
653e092ad4 | ||
|
|
5e97a8b412 | ||
|
|
229fcf3946 | ||
|
|
b775e4a178 | ||
|
|
e3c386a8a7 | ||
|
|
799f0449ad | ||
|
|
bf025445d5 | ||
|
|
9f22f2e8ae | ||
|
|
45c8b81823 | ||
|
|
4cfcc60655 | ||
|
|
e4ee63b8f3 | ||
|
|
f8407c74bc | ||
|
|
693467a165 | ||
|
|
cea3da01a0 | ||
|
|
c2030e5859 | ||
|
|
1cbbc660d2 | ||
|
|
e0bbff827e | ||
|
|
10925f2447 | ||
|
|
7641b05f12 | ||
|
|
d130e74004 | ||
|
|
c9c61e4f17 | ||
|
|
da8df1f4d9 | ||
|
|
f649d960c1 | ||
|
|
e5ebc5a2bb | ||
|
|
f9d2839590 | ||
|
|
c6a6bc7e25 | ||
|
|
fce126d426 | ||
|
|
8e41278717 | ||
|
|
cb3738d540 | ||
|
|
71afc25110 | ||
|
|
fa377733be | ||
|
|
21950f7d82 | ||
|
|
e66aee0416 | ||
|
|
c7e1fc17d2 | ||
|
|
0c0b0df36e | ||
|
|
304f0002b4 | ||
|
|
15cf3e1bc0 | ||
|
|
eeed166310 | ||
|
|
e54075fea3 | ||
|
|
78cc1b2937 | ||
|
|
35f95bbbe4 | ||
|
|
ae61cd443d | ||
|
|
cc20f7d76d | ||
|
|
5603b91526 | ||
|
|
332f491ac7 | ||
|
|
4bd7c18fe9 | ||
|
|
fdb0664841 | ||
|
|
a9ba770bfa | ||
|
|
4d96e5a6b1 | ||
|
|
1171c5cfcb | ||
|
|
91e241374d | ||
|
|
634202c0e9 | ||
|
|
020ed33003 | ||
|
|
0f61066b1d | ||
|
|
03711481cd | ||
|
|
c32fb2fc71 | ||
|
|
6e77c89cd6 | ||
|
|
5e41e84b8d | ||
|
|
bfd569ee44 | ||
|
|
b13a33347f | ||
|
|
d17a6edf12 | ||
|
|
7255ebff29 | ||
|
|
cacc90097a | ||
|
|
0ef8984767 | ||
|
|
7a4abd93dc | ||
|
|
142c910e3b | ||
|
|
c68d54a5ed | ||
|
|
d17188b820 | ||
|
|
cbe59b6950 | ||
|
|
2b3c6525ff | ||
|
|
72dd8658cf | ||
|
|
c0490f7777 | ||
|
|
a22a38527b | ||
|
|
08203033a2 | ||
|
|
d0482a8f2c | ||
|
|
e6a2db8075 | ||
|
|
7929322e95 | ||
|
|
5c35811eb9 | ||
|
|
4ba0f4a2cd | ||
|
|
e167b4b753 | ||
|
|
c47f821754 | ||
|
|
febdbc0468 | ||
|
|
a988732d65 | ||
|
|
7ee541e1b1 | ||
|
|
b07769bdee | ||
|
|
9db016638e | ||
|
|
c3d164d462 | ||
|
|
352618b3b7 | ||
|
|
0389b0d2d9 | ||
|
|
8906a82e3b | ||
|
|
f6e42896c3 | ||
|
|
4d3dec54cf | ||
|
|
f082ffffe3 | ||
|
|
5f23f2a15a | ||
|
|
7ed7cf3340 | ||
|
|
ab60ac44fd | ||
|
|
d9ad39cdad | ||
|
|
bac4e22bff | ||
|
|
ab5f6cc229 | ||
|
|
1765f4f0b4 | ||
|
|
e88c6e1550 | ||
|
|
1739c6d74f | ||
|
|
66513a114e | ||
|
|
0e808a4c01 | ||
|
|
4e013603be | ||
|
|
730585cd14 | ||
|
|
fd7820759d | ||
|
|
69359bb1e6 | ||
|
|
35b602a76e | ||
|
|
82002d1f75 | ||
|
|
2fd9b062df | ||
|
|
ec3804e87e | ||
|
|
d0d28cc7e6 | ||
|
|
2d1260c01b | ||
|
|
06da6a2cc6 | ||
|
|
858a22f82e | ||
|
|
3c126ddfd4 | ||
|
|
2b98e6f177 | ||
|
|
20036b6a74 | ||
|
|
396cbd6bd3 | ||
|
|
71ea16ff69 | ||
|
|
45331be166 | ||
|
|
beb7c14adb | ||
|
|
d33571dde3 | ||
|
|
53a7d1b66c | ||
|
|
fa721e82ff | ||
|
|
d4faa77ee4 | ||
|
|
96d3d88c2f | ||
|
|
213e06f02e | ||
|
|
d5445dd049 | ||
|
|
af5adcb0ba | ||
|
|
ca238a8578 | ||
|
|
1a8f133d54 | ||
|
|
233ee09efe | ||
|
|
35ff768176 | ||
|
|
c4e5bc913e | ||
|
|
0241937dee | ||
|
|
68b46735cd | ||
|
|
bfeaf4a23e | ||
|
|
5f9f157494 | ||
|
|
c23eed6ec4 | ||
|
|
04a4431d6a | ||
|
|
d9c5d71f40 | ||
|
|
75f09c2fdf | ||
|
|
74f0a8dd7b | ||
|
|
092e3dca47 | ||
|
|
b8a6ac72dd | ||
|
|
b602aac09a | ||
|
|
d86c002132 | ||
|
|
54c5a9450d | ||
|
|
0f0097c15c | ||
|
|
c9437d8c72 | ||
|
|
19819444af | ||
|
|
15df3aea44 | ||
|
|
d000a5dff8 | ||
|
|
34694b4aef | ||
|
|
7ce7b23450 | ||
|
|
a5d1469281 | ||
|
|
a90a85afae | ||
|
|
6e68876a14 | ||
|
|
11231703d5 | ||
|
|
b77a219e19 | ||
|
|
bd8e6354b5 | ||
|
|
361094f537 | ||
|
|
d53177e44c | ||
|
|
e73f646e1f | ||
|
|
1a5dfb0ac2 | ||
|
|
49cb1875d1 | ||
|
|
61eee45ad0 | ||
|
|
e99a18f54f | ||
|
|
0c123e1676 | ||
|
|
81b239c6b3 | ||
|
|
ea8c34bf8b | ||
|
|
b3e67e58b5 | ||
|
|
589041b3ed | ||
|
|
455364ff29 | ||
|
|
d90bc1a3e4 | ||
|
|
0e3875c1ff | ||
|
|
3b565b96b7 | ||
|
|
ec6c77927f | ||
|
|
e88ff77ecb | ||
|
|
0781010b16 | ||
|
|
ab4bac05d1 | ||
|
|
8e22ce7c70 | ||
|
|
d96a5547ce | ||
|
|
05b43965d1 | ||
|
|
43000a5f80 | ||
|
|
a66b9ea749 | ||
|
|
36a660a8ad | ||
|
|
d09bb4ff74 | ||
|
|
6ca3cc230f | ||
|
|
6a5d8ed3b5 | ||
|
|
53dcba1189 | ||
|
|
3e77004274 | ||
|
|
fd21fcdb84 | ||
|
|
ae14e35df7 | ||
|
|
94dc9fd0d5 | ||
|
|
7b34659a8f | ||
|
|
808432e709 | ||
|
|
73d3b76a89 | ||
|
|
ca072b4861 | ||
|
|
54c896cbb9 | ||
|
|
2c7bf2cfdb | ||
|
|
49d2e34e12 | ||
|
|
5511b41f93 | ||
|
|
147cdff555 | ||
|
|
4b6ade0b14 | ||
|
|
570618705e | ||
|
|
e791608098 | ||
|
|
03b358726a | ||
|
|
d121f8f547 | ||
|
|
b27218d799 | ||
|
|
e78ce42dd5 | ||
|
|
55fade497d | ||
|
|
e7501b621a | ||
|
|
9c443bede1 | ||
|
|
6d0a746b76 | ||
|
|
0765e7b209 | ||
|
|
f729802094 | ||
|
|
e809e967a3 | ||
|
|
4de2bd5901 | ||
|
|
b5629ff16f | ||
|
|
91615aef54 | ||
|
|
fa6898167b | ||
|
|
903b7979de | ||
|
|
8721085d14 | ||
|
|
e434528cd3 | ||
|
|
9ca54e4364 | ||
|
|
a16794e2af | ||
|
|
496a781572 | ||
|
|
2df583df1a | ||
|
|
4f1ca920a7 | ||
|
|
96e33f5b4f | ||
|
|
dff2d90325 | ||
|
|
fa344776a1 | ||
|
|
eb164809de | ||
|
|
4ddbeb6d06 | ||
|
|
fa53391395 | ||
|
|
856e70cf8e | ||
|
|
2037611931 | ||
|
|
af1f6e5708 | ||
|
|
0df8372407 | ||
|
|
59f4129ee0 | ||
|
|
de3af34b66 | ||
|
|
5c62dc78ba | ||
|
|
3fdecde6e9 | ||
|
|
e7bf74d91d | ||
|
|
ff211fb4f9 |
12
.github/workflows/codeql.yml
vendored
12
.github/workflows/codeql.yml
vendored
@@ -6,8 +6,6 @@ on:
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ 'master' ]
|
||||
schedule:
|
||||
- cron: '24 4 * * *'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -33,7 +31,7 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
@@ -49,13 +47,13 @@ jobs:
|
||||
|
||||
# Build with Gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-service-url: "https://gradle.com/terms-of-service"
|
||||
build-scan-terms-of-service-agree: "yes"
|
||||
build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
|
||||
build-scan-terms-of-use-agree: "yes"
|
||||
- name: Execute Gradle build
|
||||
run: ./gradlew build -x test -x jIFC
|
||||
run: ./gradlew --no-daemon --no-build-cache --no-configuration-cache --rerun-tasks clean build -x test -x jIFC
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
|
||||
2
.github/workflows/dependency-submission.yml
vendored
2
.github/workflows/dependency-submission.yml
vendored
@@ -20,6 +20,6 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
- name: Generate and submit dependency graph
|
||||
uses: gradle/actions/dependency-submission@v3
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -18,6 +18,13 @@ gjf.out
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
# Environment-specific configuration files
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-alpha.yaml
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-crash.yaml
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-production.yaml
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-qa.yaml
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-sandbox.yaml
|
||||
|
||||
######################################################################
|
||||
# Eclipse Ignores
|
||||
|
||||
@@ -114,9 +121,5 @@ core/**/registrar_dbg*.js
|
||||
core/**/registrar_bin*.css
|
||||
core/**/registrar_dbg*.css
|
||||
|
||||
# Appengine generated files
|
||||
core/WEB-INF/appengine-generated/*.bin
|
||||
core/WEB-INF/appengine-generated/*.xml
|
||||
|
||||
# jEnv
|
||||
.java-version
|
||||
|
||||
@@ -34,6 +34,8 @@ Guy Bensky <guyben@google.com>
|
||||
Weimin Yu <weiminyu@google.com>
|
||||
Shicong Huang <shicong@google.com>
|
||||
Gustav Brodman <gbrodman@google.com>
|
||||
Aman Sanger <sangera@google.com>
|
||||
Sarah Botwinick <sarahbot@google.com>
|
||||
Legina Chen <legina@google.com>
|
||||
Rachel Guan <rachelguan@google.com>
|
||||
Juan Celhay <jicelhay@google.com>
|
||||
|
||||
175
GEMINI.md
Normal file
175
GEMINI.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Engineering Standards for Gemini CLI
|
||||
|
||||
This document outlines foundational mandates, architectural patterns, and project-specific conventions to ensure high-quality, idiomatic, and consistent code from the first iteration. When modifying this file, always review the full document to prevent the introduction of duplicate instructions and ensure the content remains coherent and logically organized.
|
||||
|
||||
## Core Mandates
|
||||
|
||||
### 1. Rigorous Import Management
|
||||
- **Addition:** When adding new symbols, ensure the corresponding import is added.
|
||||
- **Removal:** When removing the last usage of a class or symbol from a file (e.g., removing a `@Inject Clock clock;` field), **immediately remove the associated import**. Do not wait for a build failure to identify unused imports.
|
||||
- **No Redundant Qualifications:** NEVER use fully qualified class names (e.g., `java.time.temporal.ChronoUnit.DAYS`) in code when an import can be used instead. Always prefer adding an import and using the simple name.
|
||||
- **Static Imports for Utilities:** Always statically import methods from utility classes like `DateTimeUtils` or `CacheUtils`. (e.g., use `toInstant(...)` instead of `DateTimeUtils.toInstant(...)`).
|
||||
- **Checkstyle:** Proactively fix common checkstyle errors (line length > 100, formatting, unused imports) during the initial code write. Do not wait for CI/build failures to address these, as iterative fixes are inefficient.
|
||||
- **Verification**: Before finalizing any change, scan the imports section for redundancy.
|
||||
- **License Headers**: When creating new files, ensure the license header uses the current year (e.g., 2026). Existing files should retain their original year.
|
||||
|
||||
## 2. Time and Precision Handling (java.time Migration)
|
||||
|
||||
- **Idiomatic java.time Usage:** Avoid redundant conversions between `Instant` and `DateTime`. If a field or parameter is an `Instant`, use it directly. Do not convert to `DateTime` just to call a deprecated method if an `Instant` alternative exists or can be easily created. Furthermore, you should not call `toInstant()` or `toDateTime()` conversion methods when not strictly necessary; always prefer to use an alternative method that returns the correct type if one exists (e.g. use `tm().getTxTime()` which returns an `Instant` instead of calling `tm().getTransactionTime().toInstant()`).
|
||||
- **CRITICAL MISTAKES TO AVOID:**
|
||||
- NEVER use `toInstant(clock.nowUtc())` or `toInstant(fakeClock.nowUtc())`. Both `Clock` and `FakeClock` have a `now()` method that natively returns a `java.time.Instant`. You MUST use `clock.now()` or `fakeClock.now()` directly.
|
||||
- NEVER double-wrap conversions like `toInstant(toDateTime(...))` or `toDateTime(toInstant(...))`.
|
||||
- NEVER mark `Instant` parameters or local variables as `final` unnecessarily, as it clutters the codebase.
|
||||
- When using test helpers like `assertThatCommand().atTime(...)` or `ForeignKeyUtils.loadResource(...)`, ALWAYS use the `Instant` overloads. DO NOT wrap `Instant` instances in `toDateTime(...)` just to pass them to deprecated overloads.
|
||||
- **UTC Timezones:** Do not use `ZoneId.of("UTC")`. Use a statically imported `UTC` from `ZoneOffset` instead (`import static java.time.ZoneOffset.UTC;`).
|
||||
- **Millisecond Precision:** Always truncate `Instant.now()` to milliseconds (using `.truncatedTo(ChronoUnit.MILLIS)`) to maintain consistency with Joda `DateTime` and the PostgreSQL schema (which enforces millisecond precision via JPA converters).
|
||||
- **Clock Injection:**
|
||||
- Avoid direct calls to `Instant.now()`, `DateTime.now()`, `ZonedDateTime.now()`, or `System.currentTimeMillis()`.
|
||||
- Inject `google.registry.util.Clock` (production) or `google.registry.testing.FakeClock` (tests).
|
||||
- Use `clock.nowDate()` to get a `ZonedDateTime` in UTC.
|
||||
- When defining timestamps for tests, prefer using a fixed, static constant (e.g., `Instant.parse("2024-03-27T10:15:30.105Z")`) over capturing `clock.now()` to prevent flaky tests caused by the passage of real time. Avoid using the Unix epoch (`START_INSTANT`) unless specifically testing epoch-related logic; instead, use realistic dates and vary them across different test suites to ensure logic isn't dependent on a specific "standard" date.
|
||||
- **Beam Pipelines:**
|
||||
- Ensure `Clock` is serializable (it is by default in this project) when used in Beam `DoFn`s.
|
||||
- Pass the `Clock` through the constructor or via Dagger provider methods in the pipeline module.
|
||||
- **Command-Line Tools:**
|
||||
- Use `@Inject Clock clock;` in `Command` implementations.
|
||||
- The `clock` field should be **package-private** (no access modifier) to allow manual initialization in corresponding test classes.
|
||||
- In test classes (e.g., `UpdateDomainCommandTest`), manually set `command.clock = fakeClock;` in the `@BeforeEach` method.
|
||||
- Base test classes like `EppToolCommandTestCase` should handle this assignment for their generic command types where applicable.
|
||||
|
||||
### 3. Dependency Injection (Dagger)
|
||||
- **Concrete Types:** Dagger `inject` methods must use explicit concrete types. Generic `inject(Command)` methods will not work.
|
||||
- **Test Components:** Use `TestRegistryToolComponent` for command-line tool tests to bridge the gap between `main` and `nonprod/test` source sets.
|
||||
|
||||
### 4. Database Consistency
|
||||
- **JPA Converters:** Be aware that JPA converters (like `DateTimeConverter`) may perform truncation or transformation. Ensure application-level logic matches these transformations to avoid "dirty" state or unexpected diffs.
|
||||
- **Transaction Management:**
|
||||
- **Top-Level:** Define database transactions (`tm().transact(...)`) at the highest possible level in the call chain (e.g., in an Action, a Command, or a Flow). This ensures all operations are atomic and handled by the retry logic.
|
||||
- **DAO Methods:** Avoid declaring transactions inside low-level DAO methods. Use `tm().assertInTransaction()` to ensure that these methods are only called within a valid transactional context.
|
||||
- **Utility/Cache Methods:** Use `tm().reTransact(...)` for utility methods or Caffeine cache loaders that might be invoked from both transactional and non-transactional paths.
|
||||
- `reTransact` will join an existing transaction if one is present (acting as a no-op) or start a new one if not.
|
||||
- This is particularly useful for in-memory caches where the loader must be able to fetch data regardless of whether the caller is currently in a transaction.
|
||||
- **Transactional Time:** Ensure code that relies on `tm().getTransactionTime()` is executed within a transaction context.
|
||||
|
||||
### 5. Testing Best Practices
|
||||
- **FakeClock and Sleeper:** Use `FakeClock` and `Sleeper` for any logic involving timeouts, delays, or expiration.
|
||||
- **Empirical Reproduction:** Before fixing a bug, always create a test case that reproduces the failure.
|
||||
- **Base Classes:** Leverage `CommandTestCase`, `EppToolCommandTestCase`, etc., to reduce boilerplate and ensure consistent setup (e.g., clock initialization).
|
||||
|
||||
### 6. Project Dependencies
|
||||
- **Common Module:** When using `Clock` or other core utilities in a new or separate module (like `load-testing`), ensure `implementation project(':common')` is added to the module's `build.gradle`.
|
||||
|
||||
### 7. Search and Discovery
|
||||
- **No CodeSearch:** This project is hosted on GitHub, not Google3. Do NOT use `mcp_Coding_search_for_files_codesearch` or other internal Google3 search tools.
|
||||
- **Local Grep:** Use local shell commands like `git grep` or `grep` via `run_shell_command` to search the codebase.
|
||||
|
||||
## Performance and Efficiency
|
||||
- **Turn Minimization:** Aim for "perfect" code in the first iteration. Iterative fixes for checkstyle or compilation errors consume significant context and time.
|
||||
- **Context Management:** Use sub-agents for batch refactoring or high-volume output tasks to keep the main session history lean and efficient.
|
||||
- **Code Formatting:** Do not write custom Python scripts or manual regex replacements to fix code formatting issues (e.g., unused imports, import ordering, line length). Instead, use the project's built-in formatting tools: run `./gradlew spotlessApply` to fix unused/unordered imports and `./gradlew javaIncrementalFormatApply` (or `google-java-format --replace <files>`) to automatically fix Java formatting and indentation errors.
|
||||
|
||||
## General Code Review Lessons & Avoidable Mistakes
|
||||
Based on historical PR reviews, avoid the following common mistakes:
|
||||
- **No Unnecessary Casts:** Do not unnecessarily cast objects if the method signature accepts the type directly (e.g., avoid `(Instant) fakeClock.now()` or `(ImmutableSet<String>) bsaQuery(...)` if it compiles without it).
|
||||
- **Visibility Modifiers:** Do not use `/* package */` comments to denote package-private visibility. Just leave the modifier blank; it is an established idiom in this codebase.
|
||||
|
||||
### Advanced Java & Guava Idioms
|
||||
- **Immutable Types:** Declare variables, fields, and return types explicitly as Guava immutable types (e.g., `ImmutableList<T>`, `ImmutableMap<K, V>`) instead of their generic interfaces (`List<T>`, `Map<K, V>`) to clearly communicate immutability contracts to callers. Use `toImmutableList()` and `toImmutableMap()` collectors in streams rather than manually accumulating into an `ArrayList` or `HashMap`.
|
||||
- **Constructors:** Do not perform heavy logic, I/O, or external API calls inside constructors. Initialization logic should be deferred or handled in a factory method or a dedicated startup routine.
|
||||
- **Exception Handling:** Do not catch generic `Exception` or `Throwable` if a more specific exception is expected. Never "log and re-throw" the same exception; either handle it entirely (and log), or throw it up the chain. For batch processes, catch exceptions at the individual item/chunk level so one failure doesn't abort the entire batch.
|
||||
- **Fail Fast:** Validate inputs and fail fast (using `Preconditions.checkArgument` or similar) at the highest level possible rather than passing invalid state (like `null`s) deeper into business logic.
|
||||
- **Magic Numbers:** Always document magic numbers or hardcoded limits (like `50.0` or `30`) with inline comments explaining the rationale.
|
||||
- **Null Safety and Optional:** Prefer using `Optional` for any variable that is expected to potentially be null. For any other variable that can be null but cannot use an `Optional` (e.g., function parameters or return types where `Optional` is not idiomatic), it MUST be annotated with `@Nullable`. Always use the `javax.annotation.Nullable` annotation.
|
||||
|
||||
---
|
||||
|
||||
# Gemini Engineering Guide: Nomulus Codebase
|
||||
|
||||
This document captures high-level architectural patterns, lessons learned from large-scale refactorings (like the Joda-Time to `java.time` migration), and specific instructions to avoid common pitfalls in this environment.
|
||||
|
||||
## 🏛 Architecture Overview
|
||||
|
||||
- **Transaction Management:** The codebase uses a custom wrapper around JPA. Always use `tm()` (from `TransactionManagerFactory`) to interact with the database.
|
||||
- **Dependency Injection:** Dagger 2 is used extensively. If you see "cannot find symbol" errors for classes starting with `Dagger...`, the project is in a state where annotation processing failed. Fix compilation in core models first to restore generated code.
|
||||
- **Value Types:** AutoValue and "ImmutableObject" patterns are dominant. Most models follow a `Buildable` pattern with a nested `Builder`.
|
||||
- **Temporal Logic:** The project is migrating from Joda-Time to `java.time`.
|
||||
- Core boundaries: `DateTimeUtils.START_OF_TIME_INSTANT` (Unix Epoch) and `END_OF_TIME_INSTANT` (Long.MAX_VALUE / 1000).
|
||||
- Year Arithmetic: Use `DateTimeUtils.plusYears()` and `DateTimeUtils.minusYears()` to handle February 29th logic correctly.
|
||||
|
||||
## Source Control
|
||||
- **Committing:** Always create a new commit on the branch if one hasn't been created yet for the branch's specific work. Only perform amending (`git commit --amend --no-edit`) for subsequent changes once the initial commit has been successfully created.
|
||||
- **One Commit Per PR:** All changes for a single PR must be squashed into a single commit before merging.
|
||||
- **Default to Amend:** Once an initial commit is created for a PR, all subsequent functional changes should be amended into that same commit by default (`git commit --amend --no-edit`). This ensures the PR remains a single, clean unit of work throughout the development lifecycle.
|
||||
- **Commit Message Style:** Follow standard Git commit best practices. The subject line (first line) MUST be a maximum of 50 characters, concise, capitalized, and **must not end with punctuation** (e.g., a period). The body MUST explicitly encapsulate and summarize all changes made across the entire diff, detailing the "what" and "why" comprehensively.
|
||||
- **Strict Completion Verification:** You MUST NEVER declare a task, commit, or amendment as complete until you have explicitly verified that the workspace is clean. You MUST follow this exact sequence of actions across multiple conversational turns if necessary:
|
||||
1. Execute the `git commit` or `git commit --amend` command.
|
||||
2. Wait for the tool to return successfully.
|
||||
3. Execute `git status`.
|
||||
4. Wait for the tool to return and explicitly verify the output contains `nothing to commit, working tree clean` (or similar indication that no unstaged changes remain). If changes remain, stage them and amend the commit, then repeat this verification loop.
|
||||
5. **Only after** step 4 has successfully returned a clean working directory may you generate a text response to the user declaring that the task is complete.
|
||||
- **Diff Review:** Before finalizing a task, review the full diff (e.g., `git diff HEAD^`) to ensure all changes are functional and relevant. Identify and revert any formatting-only changes in files that do not contain functional updates to keep the commit focused.
|
||||
|
||||
## Self-Review Guidelines
|
||||
Before finalizing any PR or declaring a task complete, you MUST perform a thorough, rigorous self-review of your entire diff. Run `git diff HEAD^` (or review the staged changes) and actively verify the following against every modified line:
|
||||
|
||||
1. **Imports & FQNs:** Did I leave any fully-qualified class names or static variables inline? Did I add the necessary imports for them? *Crucial Exception:* If the file already imports a class with the identical name (e.g., it uses both `java.time.Duration` and `org.joda.time.Duration`), one MUST remain fully qualified to avoid a compilation conflict.
|
||||
2. **Redundant Conversions:** Did I use `toDateTime(clock.now())` where `clock.nowUtc()` would suffice? Did I use `toDateTime(END_INSTANT)` instead of `END_OF_TIME`? Did I use `.toInstant()` or `.toDateTime()` on something that could be avoided by using a different method overload (e.g., `tm().getTxTime()`)?
|
||||
3. **Verbose Math:** Did I write any verbose time conversions inline? Are there `DateTimeUtils` methods I should be using instead? If not, should I abstract this math into `DateTimeUtils`?
|
||||
4. **Assertion Cleanliness:** Am I polluting test assertions with `toDateTime(...)` wraps? If so, I need to add overloaded assertions to the Truth Subjects instead.
|
||||
5. **Diff Scope:** Are there any formatting-only changes in files that I did not functionally modify? If so, revert them. Does the total line count of the diff align with the approved scope (e.g., ~1,000 lines for migrations)?
|
||||
6. **Commit Message:** Does the commit message title fit within 50 characters? Does the body encapsulate the entirety of the changes across the diff cleanly and professionally?
|
||||
7. **Missing Tests & Coverage:** *Perform a structured check for any new methods or modified behavior.* Did I add a new utility method (like `plusMonths(Instant, int)`) or change core logic? If so, I MUST open the corresponding test file and write tests to cover the new functionality (including edge cases, negative values, and leap years) before considering the task complete. A code review is not thorough if it only checks for compilation. I must actively ensure every new branch of logic has a test.
|
||||
|
||||
Only after actively confirming these checks against your diff are you permitted to finalize the task.
|
||||
|
||||
## Refactoring & Migration Guardrails
|
||||
|
||||
|
||||
### 1. Compiler Warnings are Errors (`-Werror`)
|
||||
This project treats Error Prone warnings as errors.
|
||||
- **`@InlineMeSuggester`**: When creating deprecated Joda-Time bridge methods (e.g., `getTimestamp() -> return toDateTime(getTimestampInstant())`), you **MUST** immediately add `@SuppressWarnings("InlineMeSuggester")`. If you don't, the build will fail.
|
||||
- **Repeatable Annotations**: `@SuppressWarnings` is **NOT** repeatable in this environment. If a method or class already has a suppression (e.g., `@SuppressWarnings("unchecked")`), you must merge them:
|
||||
- ❌ `@SuppressWarnings("unchecked") @SuppressWarnings("InlineMeSuggester")`
|
||||
- ✅ `@SuppressWarnings({"unchecked", "InlineMeSuggester"})`
|
||||
|
||||
### 2. Resolving Ambiguity
|
||||
- **Null Overloads**: Adding an `Instant` overload to a method that previously took `DateTime` will break all `create(null)` calls. You must cast them: `create((Instant) null)`.
|
||||
- **Type Erasure**: Methods taking `Optional<DateTime>` and `Optional<Instant>` will clash due to erasure. Use distinct names, e.g., `setAutorenewEndTimeInstant(Optional<Instant> time)`.
|
||||
|
||||
### 3. Build Strategy
|
||||
- **Surgical Changes**: In large-scale migrations, focus on "leaf" nodes first (Utilities -> Models -> Flows -> Actions).
|
||||
- **PR Size**: Minimize PR size by retaining Joda-Time bridge methods for high-level "Action" and "Flow" classes unless a full migration is requested. Reverting changes to DNS and Reporting logic while updating the underlying models is a valid strategy to keep PRs reviewable.
|
||||
- **Validation**: Always run `./gradlew build -x test` before attempting to run unit tests. Unit tests will not run if there are compilation errors in any part of the `core` module. Before finalizing a PR or declaring a task done, you MUST verify your changes. **Prefer scoped builds** (e.g., `./gradlew :core:build`) if you are only modifying backend Java code. Running the global `./gradlew build` triggers the frontend `console-webapp` build, which unnecessarily runs `npmInstallDeps` and modifies `package-lock.json`. If you must run a global build, you must revert `console-webapp/package-lock.json` afterwards. Do not declare success if formatting checks (e.g., `spotlessCheck` or `javaIncrementalFormatCheck`) or tests fail. If formatting fails, run `./gradlew spotlessApply` and then re-run your build command to verify everything passes.
|
||||
|
||||
## 🚫 Common Pitfalls to Avoid
|
||||
|
||||
- **Mixing Joda and Java Durations:** Methods like `Tld.get().getRenewGracePeriodLength()` return a **Joda** `Duration`, which cannot be passed directly to `Instant.plus(...)` because it doesn't implement `TemporalAmount`. You MUST use `.plusMillis(duration.getMillis())` instead.
|
||||
- **Serialization Precision (`.000Z`):** When asserting against or generating XML/YAML files, remember that millisecond precision (`.000Z`) is required. Always use `DateTimeUtils.formatInstant(...)` to format `Instant` objects (it preserves the `.000Z` suffix) instead of `Instant.toString()` (which drops it for exact seconds). We have added custom Jackson `InstantKeySerializer`s for this purpose, but you must keep this precision in mind when manually updating `.xml` or `.yaml` test data.
|
||||
- **Static Imports:** Methods like `toDateTime`, `toInstant`, `plusYears`, `plusMonths`, and `minusDays` from `DateTimeUtils` MUST be statically imported. Do NOT use them fully qualified (e.g., `DateTimeUtils.plusMonths(...)`).
|
||||
|
||||
- **Redundant Parses:** Never write `toDateTime(Instant.parse(...))` or `toInstant(DateTime.parse(...))`. If you need a `DateTime`, use `DateTime.parse(...)` directly. If you need an `Instant`, use `Instant.parse(...)` directly.
|
||||
- **cloneProjectedAtTime vs cloneProjectedAtInstant:** When converting tests and logic that use `clock.now()` to project resource state into the future or past, do not wrap the Java `Instant` in `toDateTime()` just to call `cloneProjectedAtTime()`. Instead, switch the method call to use the native `cloneProjectedAtInstant()` method which is available on all `EppResource` models.
|
||||
- **Do not go in circles with the build:** If you see an `InlineMeSuggester` error, apply the suppression to **ALL** similar methods in that file and related files in one turn. Do not fix them one by one. Furthermore, do not run a global `./gradlew build` when a scoped `./gradlew :core:build` or `./gradlew :core:test` is faster and more appropriate. Run global builds only when doing final verification.
|
||||
- **Exception Conversion in Tests:** When migrating time types (e.g., from Joda `DateTime` to Java `Instant`), be extremely careful with tests that verify parsing failures (e.g., `assertThrows(IllegalArgumentException.class, ...)`). Joda's `DateTime.parse()` throws an `IllegalArgumentException` on failure, but `Instant.parse()` throws a `java.time.format.DateTimeParseException`. You must update the expected exception type in these tests to ensure they actually test the correct behavior, and verify the tests are not failing prematurely on the first line if it contains invalid data meant to be ignored.
|
||||
- Dagger/AutoValue corruption: If you modify a builder or a component incorrectly, Dagger will fail to generate code, leading to hundreds of "cannot find symbol" errors. If this happens, `git checkout` the last working state of the specific file and re-apply changes more surgically.
|
||||
- **`replace` tool context**: When using `replace` on large files (like `Tld.java` or `DomainBase.java`), provide significant surrounding context. These files have many similar method signatures (getters/setters) that can lead to incorrect replacements.
|
||||
|
||||
---
|
||||
|
||||
# GitHub and Pull Request Protocol
|
||||
|
||||
This protocol defines the standard for interacting with GitHub repositories and processing Pull Request (PR) feedback.
|
||||
|
||||
## 1. Interaction via `gh` CLI
|
||||
- **Primary Tool:** ALWAYS use the `gh` CLI for all GitHub-related operations (listing PRs, viewing PR content, checking status, adding comments).
|
||||
- **Credential Safety:** Never expose tokens or credentials in shell commands.
|
||||
|
||||
## 2. Processing PR Feedback
|
||||
- **Systematic Review:** When asked to address PR comments, first fetch all comments using `gh pr view <number> --json reviews,comments`.
|
||||
- **Minimal Scope Expansion:** Address comments surgically. If a fix requires changes beyond a few lines or expands the PR's original scope significantly, DO NOT implement it without explicit user approval. Instead, report the issue to the user.
|
||||
- **Verification:** After addressing feedback, run the full build (`./gradlew build`) and relevant tests to ensure no regressions were introduced.
|
||||
|
||||
## 3. PR Lifecycle Management
|
||||
- **One Commit Per PR:** Ensure all changes are squashed into a single, clean commit. Use `git commit --amend --no-edit` for follow-up fixes.
|
||||
- **Clean Workspace:** Always run `git status` and verify the repository state before declaring a task complete.
|
||||
- **Package Lock:** The Gradle build automatically modifies `console-webapp/package-lock.json` via the `npmInstallDeps` task. ALWAYS revert this file (`git checkout console-webapp/package-lock.json`) before staging changes or finalizing a commit unless you explicitly modified NPM dependencies.
|
||||
84
README.md
84
README.md
@@ -12,16 +12,16 @@ Nomulus is an open source, scalable, cloud-based service for operating
|
||||
[top-level domains](https://en.wikipedia.org/wiki/Top-level_domain) (TLDs). It
|
||||
is the authoritative source for the TLDs that it runs, meaning that it is
|
||||
responsible for tracking domain name ownership and handling registrations,
|
||||
renewals, availability checks, and WHOIS requests. End-user registrants (i.e.
|
||||
renewals, availability checks, and WHOIS requests. End-user registrants (i.e.,
|
||||
people or companies that want to register a domain name) use an intermediate
|
||||
domain name registrar acting on their behalf to interact with the registry.
|
||||
|
||||
Nomulus runs on [Google App Engine][gae] and is written primarily in Java. It is
|
||||
the software that [Google Registry](https://www.registry.google/) uses to
|
||||
operate TLDs such as .google, .app, .how, .soy, and .みんな. It can run any
|
||||
number of TLDs in a single shared registry system using horizontal scaling. Its
|
||||
source code is publicly available in this repository under the [Apache 2.0 free
|
||||
and open source license](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
Nomulus runs on [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine)
|
||||
and is written primarily in Java. It is the software that
|
||||
[Google Registry](https://www.registry.google/) uses to operate TLDs such as .google,
|
||||
.app, .how, .soy, and .みんな. It can run any number of TLDs in a single shared registry
|
||||
system using horizontal scaling. Its source code is publicly available in this
|
||||
repository under the [Apache 2.0 free and open source license](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -30,8 +30,8 @@ running system:
|
||||
|
||||
* [Install
|
||||
guide](https://github.com/google/nomulus/blob/master/docs/install.md)
|
||||
* View the source code for the [GAE app](https://github.com/google/nomulus/tree/master/core/src/main/java/google/registry)
|
||||
and for the [GKE proxy](https://github.com/google/nomulus/tree/master/proxy/src/main/java/google/registry)
|
||||
* View the source code for the [Main HTTP server](https://github.com/google/nomulus/tree/master/core/src/main/java/google/registry)
|
||||
and for the [EPP proxy](https://github.com/google/nomulus/tree/master/proxy/src/main/java/google/registry)
|
||||
* [Other docs](https://github.com/google/nomulus/tree/master/docs)
|
||||
* [Javadoc](https://javadoc.nomulus.foo/)
|
||||
* [Nomulus discussion
|
||||
@@ -54,11 +54,11 @@ Nomulus has the following capabilities:
|
||||
checking, updating, and transferring domain names.
|
||||
* **[DNS](https://en.wikipedia.org/wiki/Domain_Name_System) interface**: The
|
||||
registry provides a pluggable interface that can be implemented to handle
|
||||
different DNS providers. It includes a sample implementation using Google
|
||||
Cloud DNS as well as an RFC 2136 compliant implementation that works with
|
||||
BIND.
|
||||
* **[WHOIS](https://en.wikipedia.org/wiki/WHOIS)**: A text-based protocol that
|
||||
returns ownership and contact information on registered domain names.
|
||||
different DNS providers. It includes a sample implementation using [Google
|
||||
Cloud DNS](https://cloud.google.com/dns/), as well as an RFC 2136 compliant
|
||||
implementation that works with BIND. If you are using Google Cloud DNS, you
|
||||
may need to understand its capabilities and provide your own
|
||||
multi-[AS](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\)) solution.
|
||||
* **[Registration Data Access Protocol
|
||||
(RDAP)](https://en.wikipedia.org/wiki/Registration_Data_Access_Protocol)**:
|
||||
A JSON API that returns structured, machine-readable information about
|
||||
@@ -68,7 +68,7 @@ Nomulus has the following capabilities:
|
||||
provider to allow take-over by another registry operator in the event of
|
||||
serious failure. This is required by ICANN for all [new
|
||||
gTLDs](https://newgtlds.icann.org/).
|
||||
* **Premium pricing**: Communicates prices for premium domain names (i.e.
|
||||
* **Premium pricing**: Communicates prices for premium domain names (i.e.,
|
||||
those that are highly desirable) and supports configurable premium
|
||||
registration and renewal prices. An extensible interface allows fully
|
||||
programmatic pricing.
|
||||
@@ -91,56 +91,50 @@ Nomulus has the following capabilities:
|
||||
* **Administrative tool**: Performs the full range of administrative tasks
|
||||
needed to manage a running registry system, including creating and
|
||||
configuring new TLDs.
|
||||
* **DNS interface**: An interface for DNS operations is provided so you can
|
||||
write an implementation for your chosen provider, along with a sample
|
||||
implementation that uses [Google Cloud DNS](https://cloud.google.com/dns/).
|
||||
If you are using Google Cloud DNS you may need to understand its
|
||||
capabilities and provide your own
|
||||
multi-[AS](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\))
|
||||
solution.
|
||||
* **GAE Proxy**: App Engine Standard only serves HTTP/S traffic. A proxy to
|
||||
forward traffic on EPP and WHOIS ports to App Engine via HTTPS is provided.
|
||||
Instructions on setting up the proxy on
|
||||
[Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)
|
||||
is [available](https://github.com/google/nomulus/blob/master/docs/proxy-setup.md).
|
||||
Running the proxy on GKE supports IPv4 and IPv6 access, per ICANN's
|
||||
requirements for gTLDs. The proxy can also run as a single jar file, or on
|
||||
other Kubernetes providers, with modifications.
|
||||
* **Secure storage of cryptographic keys**: A keyring interface is
|
||||
provided for plugging in your own implementation (see [configuration
|
||||
doc](https://github.com/google/nomulus/blob/master/docs/configuration.md)
|
||||
for details), and an implementation based on
|
||||
[Google Cloud Secret Manager](https://cloud.google.com/security/products/secret-manager) is
|
||||
available.
|
||||
* **TPC Proxy**: Nomulus is built on top of the [Jetty](https://jetty.org/)
|
||||
container that implements the [Jakarta Servlet](https://jakarta.ee/specifications/servlet/)
|
||||
specification and only serves HTTP/S traffic. A proxy to translate raw TCP traffic (e.g., EPP)
|
||||
to and from HTTP is provided.
|
||||
Instructions on setting up the proxy
|
||||
are [available](https://github.com/google/nomulus/blob/master/docs/proxy-setup.md).
|
||||
The proxy can either run in a separate cluster and communicate to Nomulus public HTTP
|
||||
endpoints via the Internet, or as a sidecar with the Nomulus image in the same pod and
|
||||
communicate to it via loopback.
|
||||
|
||||
## Additional components
|
||||
|
||||
Registry operators interested in deploying Nomulus will likely require some
|
||||
additional components that are need to be configured separately.
|
||||
additional components that need to be configured separately.
|
||||
|
||||
* A way to invoice registrars for domain name registrations and accept
|
||||
payments. Nomulus records the information required to generate invoices in
|
||||
[billing
|
||||
events](https://github.com/google/nomulus/blob/master/docs/code-structure.md#billing-events).
|
||||
* Fully automated reporting to meet ICANN's requirements for gTLDs. Nomulus
|
||||
includes substantial reporting functionality but some additional work will
|
||||
includes substantial reporting functionality, but some additional work will
|
||||
be required by the operator in this area.
|
||||
* A secure method for storing cryptographic keys. A keyring interface is
|
||||
provided for plugging in your own implementation (see [configuration
|
||||
doc](https://github.com/google/nomulus/blob/master/docs/configuration.md)
|
||||
for details).
|
||||
|
||||
* System status and uptime monitoring.
|
||||
|
||||
## Outside references
|
||||
|
||||
* [Donuts](http://donuts.domains) Registry has helped review the code and
|
||||
provided valuable feedback
|
||||
* [Identity Digital](http://identity.digital) has helped review the code and
|
||||
provided valuable feedback.
|
||||
* [CoCCa](http://cocca.org.nz) and [FRED](https://fred.nic.cz) are other
|
||||
open-source registry platforms in use by many TLDs
|
||||
open-source registry platforms in use by many TLDs.
|
||||
* We are not aware of any fully open source domain registrar projects, but
|
||||
open source EPP Toolkits (not yet tested with Nomulus; may require
|
||||
integration work) include:
|
||||
* [EPP RTK Project](http://epp-rtk.sourceforge.net/)
|
||||
* [CentralNic](https://www.centralnic.com/registry/labs)
|
||||
* [Universal Registry/Registrar Toolkit](https://sourceforge.net/projects/epp-rtk/)
|
||||
* [ari-toolkit](https://github.com/AusRegistry/ari-toolkit)
|
||||
* [Net::DRI](https://metacpan.org/pod/Net::DRI)
|
||||
* Some Open Source DNS Projects that may be useful, but which we have not
|
||||
tested:
|
||||
* [AtomiaDNS](http://atomiadns.com/)
|
||||
* [PowerDNS](https://doc.powerdns.com/md/)
|
||||
|
||||
[gae]:https://cloud.google.com/appengine/docs/about-the-standard-environment
|
||||
* [AtomiaDNS](https://github.com/atomia/atomiadns)
|
||||
* [PowerDNS](https://github.com/PowerDNS/pdns)
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
apply plugin: 'war'
|
||||
|
||||
def environment = rootProject.environment
|
||||
def gcpProject = rootProject.gcpProject
|
||||
|
||||
// Set this directory before applying the appengine plugin so that the
|
||||
// plugin will recognize this as an app-engine standard app (and also
|
||||
// obtains the appengine-web.xml from the correct location)
|
||||
project.convention.plugins['war'].webAppDirName =
|
||||
"../../core/src/main/java/google/registry/env/${environment}/${project.name}"
|
||||
|
||||
apply plugin: 'com.google.cloud.tools.appengine'
|
||||
|
||||
def coreResourcesDir = "${rootDir}/core/build/resources/main"
|
||||
def coreLibsDir = "${rootDir}/core/build/libs"
|
||||
|
||||
// Get the web.xml file for the service.
|
||||
war {
|
||||
webInf {
|
||||
from "../../core/src/main/java/google/registry/env/common/${project.name}/WEB-INF"
|
||||
}
|
||||
}
|
||||
|
||||
war {
|
||||
from("${coreResourcesDir}/google/registry/ui/html") {
|
||||
include "*.html"
|
||||
}
|
||||
from("${coreLibsDir}") {
|
||||
include "core.jar"
|
||||
into("WEB-INF/lib")
|
||||
}
|
||||
}
|
||||
|
||||
if (project.path == ":services:default") {
|
||||
war {
|
||||
from("${rootDir}/console-webapp/dist/console-webapp") {
|
||||
include "**/*"
|
||||
into("console")
|
||||
}
|
||||
from("${coreResourcesDir}/google/registry/ui") {
|
||||
include "registrar_bin.js"
|
||||
if (environment != "production") {
|
||||
include "registrar_bin.js.map"
|
||||
}
|
||||
into("assets/js")
|
||||
}
|
||||
from("${coreResourcesDir}/google/registry/ui/css") {
|
||||
include "registrar*"
|
||||
into("assets/css")
|
||||
}
|
||||
from("${coreResourcesDir}/google/registry/ui/assets/images") {
|
||||
include "**/*"
|
||||
into("assets/images")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appengine {
|
||||
deploy {
|
||||
// appengineDeployAll task requires the version to be set. So,
|
||||
// this config lets gcloud select a version name when deploying
|
||||
// to alpha or sandbox from our workstation.
|
||||
if (!rootProject.prodOrSandboxEnv) {
|
||||
version = 'GCLOUD_CONFIG'
|
||||
}
|
||||
|
||||
// Don't set gcpProject directly, it gets overriden in ./build.gradle.
|
||||
// Do -P environment={crash,alpha} instead. For sandbox/production,
|
||||
// use Spinnaker.
|
||||
projectId = gcpProject
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':core', configuration: 'deploy_jar')
|
||||
}
|
||||
|
||||
// The tools.jar file gets pulled in from the java environment and for some
|
||||
// reason gets exploded "readonly", causing subsequent builds to fail when
|
||||
// they can't overwrite it. The hack below makes the file writable after
|
||||
// we're done exploding it.
|
||||
//
|
||||
// Fun fact: We only use this jar for documentation generation and as such we
|
||||
// don't need it in our warfile, as it is not used by the application at
|
||||
// runtime. But it's not clear how to exclude it, as we seem to be
|
||||
// constructing the jar from the entire WEB-INF directory and per-file
|
||||
// exclude rules don't seem to work on it. Better solutions are welcome :-)
|
||||
explodeWar.doLast {
|
||||
file("${it.explodedAppDirectory}/WEB-INF/lib/tools.jar").setWritable(true)
|
||||
}
|
||||
|
||||
appengineDeployAll.finalizedBy ':deployCloudSchedulerAndQueue'
|
||||
rootProject.deploy.dependsOn appengineDeployAll
|
||||
rootProject.stage.dependsOn appengineStage
|
||||
tasks['war'].dependsOn ':console-webapp:buildConsoleWebappProd'
|
||||
tasks['war'].dependsOn ':core:compileProdJS'
|
||||
tasks['war'].dependsOn ':core:processResources'
|
||||
tasks['war'].dependsOn ':core:jar'
|
||||
|
||||
// Impose verification for all of the deployment tasks. We haven't found a
|
||||
// better way to do this other than to apply to each of them independently.
|
||||
// If a new task gets added, it will still fail if "environment" is not defined
|
||||
// because gcpProject is null. We just won't get as friendly an error message.
|
||||
appengineDeployAll.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeploy.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployCron.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployDispatch.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployDos.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployIndex.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployQueue.configure rootProject.verifyDeploymentConfig
|
||||
185
build.gradle
185
build.gradle
@@ -11,7 +11,6 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
|
||||
@@ -28,28 +27,28 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.4.1'
|
||||
classpath 'net.ltgt.gradle:gradle-errorprone-plugin:3.1.0'
|
||||
classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.5.0'
|
||||
classpath 'net.ltgt.gradle:gradle-errorprone-plugin:5.1.0'
|
||||
classpath 'com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.4.0'
|
||||
classpath 'org.sonatype.aether:aether-api:1.13.1'
|
||||
classpath 'org.sonatype.aether:aether-impl:1.13.1'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
// Java static analysis plugins. Keep versions consistent with
|
||||
// ./buildSrc/build.gradle.kts
|
||||
// Java static analysis plugins.
|
||||
|
||||
// Re-enable when compatible with Gradle 8
|
||||
// id 'nebula.lint' version '16.0.2'
|
||||
id 'net.ltgt.errorprone' version '3.1.0'
|
||||
id 'net.ltgt.errorprone' version '5.1.0'
|
||||
id 'checkstyle'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'com.gradleup.shadow' version '9.4.0' apply false
|
||||
|
||||
// NodeJs plugin
|
||||
id "com.github.node-gradle.node" version "3.0.1"
|
||||
id "com.github.node-gradle.node" version "7.1.0"
|
||||
|
||||
id 'idea'
|
||||
id 'com.diffplug.spotless' version '6.20.0'
|
||||
id 'com.diffplug.spotless' version '8.4.0'
|
||||
|
||||
id 'jacoco'
|
||||
id 'com.dorongold.task-tree' version '2.1.0'
|
||||
@@ -61,52 +60,28 @@ dependencyLocking {
|
||||
|
||||
node {
|
||||
download = false
|
||||
version = "20.10.0"
|
||||
version = "22.7.0"
|
||||
}
|
||||
|
||||
wrapper {
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
}
|
||||
|
||||
apply plugin: google.registry.gradle.plugin.ReportUploaderPlugin
|
||||
|
||||
reportUploader {
|
||||
// Set the location where we want to upload the build results.
|
||||
// e.g. -P uploaderDestination=gcs://domain-registry-alpha-build-result-test
|
||||
//
|
||||
// If not set - the upload will be skipped
|
||||
destination = uploaderDestination
|
||||
|
||||
// The location of the file containing the OAuth2 Google Cloud credentials.
|
||||
//
|
||||
// The file can contain a Service Account key file in JSON format from the
|
||||
// Google Developers Console or a stored user credential using the format
|
||||
// supported by the Cloud SDK.
|
||||
//
|
||||
// If no file is given - the default credentials are used.
|
||||
credentialsFile = uploaderCredentialsFile
|
||||
|
||||
// If set to 'yes', each file will be uploaded to GCS in a separate thread.
|
||||
// This is MUCH faster.
|
||||
multithreadedUpload = uploaderMultithreadedUpload
|
||||
}
|
||||
|
||||
apply from: 'dependencies.gradle'
|
||||
|
||||
apply from: 'dependency_lic.gradle'
|
||||
|
||||
apply from: 'utils.gradle'
|
||||
|
||||
// Custom task to run checkLicense in buildSrc, which is not triggered
|
||||
// by root project tasks. A shell task is used because buildSrc tasks
|
||||
// cannot be referenced in the same way as tasks from a regular included
|
||||
// build.
|
||||
task checkBuildSrcLicense(type:Exec) {
|
||||
workingDir "${rootDir}/buildSrc"
|
||||
commandLine '../gradlew', 'checkLicense'
|
||||
}
|
||||
tasks.checkLicense.dependsOn(tasks.checkBuildSrcLicense)
|
||||
tasks.build.dependsOn(tasks.checkLicense)
|
||||
// The license-report plugin must run with --no-parallel due to
|
||||
// complex cross-subject references. The `mutex` pattern does not
|
||||
// help because a mutex does not enforce task execution order.
|
||||
// For now we separate checkLicense from build so that the latter may
|
||||
// still take advantage of parallelism, which cuts down the build time
|
||||
// by about 20%. The presubmit and release procedures that want to check
|
||||
// licenses must invoke checkLicense explicitly with the `--no-parallel`
|
||||
// flag.
|
||||
// tasks.build.dependsOn(tasks.checkLicense)
|
||||
|
||||
// Provide defaults for all of the project properties.
|
||||
|
||||
@@ -117,10 +92,10 @@ tasks.build.dependsOn(tasks.checkLicense)
|
||||
// Paths to main and test sources.
|
||||
ext.projectRootDir = "${rootDir}"
|
||||
|
||||
// Tasks to deploy/stage all App Engine services
|
||||
// Tasks to deploy/stage all services
|
||||
task deploy {
|
||||
group = 'deployment'
|
||||
description = 'Deploys all services to App Engine.'
|
||||
description = 'Deploys all services.'
|
||||
}
|
||||
|
||||
task stage {
|
||||
@@ -128,30 +103,27 @@ task stage {
|
||||
description = 'Generates application directories for all services.'
|
||||
}
|
||||
|
||||
// App-engine environment configuration. We set up all of the variables in
|
||||
// the root project.
|
||||
|
||||
def environments = ['production', 'sandbox', 'alpha', 'crash', 'qa']
|
||||
|
||||
def gcpProject = null
|
||||
|
||||
apply from: "${rootDir.path}/projects.gradle"
|
||||
|
||||
if (environment == '') {
|
||||
// Keep the project null, this will prevent deployment. Set the
|
||||
// Keep the project null, this will prevent deployment. Set the
|
||||
// environment to "alpha" because other code needs this property to
|
||||
// explode the war file.
|
||||
environment = 'alpha'
|
||||
} else if (environment != 'production' && environment != 'sandbox') {
|
||||
} else {
|
||||
gcpProject = projects[environment]
|
||||
if (gcpProject == null) {
|
||||
throw new GradleException("-Penvironment must be one of " +
|
||||
"${projects.keySet()}.")
|
||||
}
|
||||
project(':console-webapp').setProperty('configuration', environment)
|
||||
}
|
||||
|
||||
rootProject.ext.environment = environment
|
||||
rootProject.ext.gcpProject = gcpProject
|
||||
rootProject.ext.baseDomain = baseDomains[environment]
|
||||
rootProject.ext.prodOrSandboxEnv = environment in ['production', 'sandbox']
|
||||
|
||||
// Function to verify that the deployment parameters have been set.
|
||||
@@ -189,7 +161,7 @@ allprojects {
|
||||
if (!mavenUrl.isEmpty()) {
|
||||
maven {
|
||||
println "Java dependencies: Using repo ${mavenUrl}..."
|
||||
url mavenUrl
|
||||
url = mavenUrl
|
||||
allowInsecureProtocol = allowInsecure == "true"
|
||||
}
|
||||
} else {
|
||||
@@ -197,7 +169,7 @@ allprojects {
|
||||
mavenCentral()
|
||||
google()
|
||||
maven {
|
||||
url "https://packages.confluent.io/maven/"
|
||||
url = "https://packages.confluent.io/maven/"
|
||||
content {
|
||||
includeGroup "io.confluent"
|
||||
}
|
||||
@@ -228,6 +200,7 @@ allprojects {
|
||||
"--add-exports",
|
||||
"jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"]
|
||||
options.forkOptions.jvmArgs = ["-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
|
||||
"-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
|
||||
"-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
|
||||
"-J--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
|
||||
"-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
|
||||
@@ -291,6 +264,14 @@ subprojects {
|
||||
// Skip no-op project
|
||||
if (project.name == 'services') return
|
||||
|
||||
apply plugin: 'com.gradleup.shadow'
|
||||
|
||||
tasks.configureEach {
|
||||
if (it.class.name.contains('ShadowJar')) {
|
||||
it.zip64 = true
|
||||
}
|
||||
}
|
||||
|
||||
ext.createUberJar = {
|
||||
taskName,
|
||||
binaryName,
|
||||
@@ -299,7 +280,8 @@ subprojects {
|
||||
List<SourceSetOutput> srcOutput = [project.sourceSets.main.output],
|
||||
List<String> excludes = [] ->
|
||||
project.tasks.create(
|
||||
taskName, com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
|
||||
taskName, project.tasks.shadowJar.class) {
|
||||
zip64 = true
|
||||
mergeServiceFiles()
|
||||
archiveBaseName = binaryName
|
||||
if (mainClass != '') {
|
||||
@@ -307,6 +289,11 @@ subprojects {
|
||||
attributes 'Main-Class': mainClass
|
||||
}
|
||||
}
|
||||
// Build as a multi-release jar since we've got member jars (e.g., dnsjava
|
||||
// and snakeyaml) that are multi-release.
|
||||
manifest {
|
||||
attributes 'Multi-Release': true
|
||||
}
|
||||
zip64 = true
|
||||
archiveClassifier = ''
|
||||
archiveVersion = ''
|
||||
@@ -322,7 +309,7 @@ subprojects {
|
||||
// We do seem to get duplicates when constructing uber-jars, either
|
||||
// this is a product of something in gradle 7 or a product of gradle 7
|
||||
// now giving an error about them when it didn't previously.
|
||||
duplicatesStrategy DuplicatesStrategy.WARN
|
||||
duplicatesStrategy = DuplicatesStrategy.WARN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,14 +333,14 @@ subprojects {
|
||||
// exception.
|
||||
//
|
||||
// For all other projects, due to problem with the gradle-license-report
|
||||
// plugin, the dependencyLicenseReport configuration must opt out of
|
||||
// plugin, the `detached` configurations by this plugin must opt out of
|
||||
// dependency-locking. See dependency_lic.gradle for the reason why.
|
||||
//
|
||||
// To selectively activate dependency locking without hardcoding them
|
||||
// in the 'configurations' block, the following code must run after
|
||||
// project evaluation, when all configurations have been created.
|
||||
configurations.each {
|
||||
if (it.name != 'dependencyLicenseReport' && it.name != 'integration') {
|
||||
configurations.all {
|
||||
if (!it.name.contains('detachedConfiguration')) {
|
||||
it.resolutionStrategy.activateDependencyLocking()
|
||||
}
|
||||
}
|
||||
@@ -362,9 +349,6 @@ subprojects {
|
||||
|
||||
// Set up all of the deployment projects.
|
||||
if (services.contains(project.path)) {
|
||||
|
||||
apply from: "${rootDir.path}/appengine_war.gradle"
|
||||
|
||||
// Return early, do not apply the settings below.
|
||||
return
|
||||
}
|
||||
@@ -375,8 +359,14 @@ subprojects {
|
||||
// search for `flex-template-base-image` and update the parameter value.
|
||||
// There are at least two instances, one in core/build.gradle, one in
|
||||
// release/stage_beam_pipeline.sh
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
// Also need to change:
|
||||
// - base images in Dockerfiles under core, jetty, and proxy.
|
||||
// - Java installation command in the builder image under release.
|
||||
// - cloudbuild-release.yaml under release.
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_25
|
||||
targetCompatibility = JavaVersion.VERSION_25
|
||||
}
|
||||
|
||||
project.tasks.test.dependsOn runPresubmits
|
||||
|
||||
@@ -403,25 +393,19 @@ subprojects {
|
||||
|
||||
// No need to produce javadoc for the jetty subproject, which has no APIs to
|
||||
// expose to users.
|
||||
if (project.name != 'jetty') {
|
||||
if (project.name != 'jetty' && !services.contains(project.path)) {
|
||||
javadocSource << project.sourceSets.main.allJava
|
||||
javadocClasspath << project.sourceSets.main.runtimeClasspath
|
||||
javadocClasspath << { project.sourceSets.main.runtimeClasspath.files }
|
||||
javadocClasspath << "${buildDir}/generated/sources/annotationProcessor/java/main"
|
||||
javadocDependentTasks << project.tasks.compileJava
|
||||
if (project.tasks.findByName('compileJava')) {
|
||||
javadocDependentTasks << project.tasks.compileJava
|
||||
}
|
||||
if (project.tasks.findByName('processResources')) {
|
||||
javadocDependentTasks << project.tasks.processResources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force SDK download and deployment to be sequential, otherwise parallel tasks
|
||||
// will fail. For SDK download, they will try to write to the same location to
|
||||
// upgrade gcloud. For deployment, they will try to deploy different services to
|
||||
// the same project at the same time.
|
||||
for (int i = 1; i < services.size(); i++) {
|
||||
project("${services[i]}").downloadCloudSdk
|
||||
.dependsOn(project("${services[i - 1]}").downloadCloudSdk)
|
||||
project("${services[i]}").appengineDeployAll
|
||||
.dependsOn(project("${services[i - 1]}").appengineDeployAll)
|
||||
}
|
||||
|
||||
// If "-P verboseTestOutput=true" is passed in, configure all subprojects to dump all of their
|
||||
// output and final test status (pass/fail, errors) for each test class.
|
||||
//
|
||||
@@ -452,9 +436,6 @@ if (verboseTestOutput.toBoolean()) {
|
||||
}
|
||||
|
||||
task checkDependenciesDotGradle {
|
||||
def buildSrcDepsFile = File.createTempFile('buildSrc', 'deps')
|
||||
buildSrcDepsFile.deleteOnExit()
|
||||
dependsOn createGetBuildSrcDirectDepsTask(buildSrcDepsFile)
|
||||
|
||||
doLast {
|
||||
Set<String> depsInUse = []
|
||||
@@ -467,9 +448,7 @@ task checkDependenciesDotGradle {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (buildSrcDepsFile.exists()) {
|
||||
depsInUse.addAll(buildSrcDepsFile.readLines())
|
||||
}
|
||||
|
||||
def unusedDeps =
|
||||
rootProject.dependencyMap.keySet()
|
||||
.findAll { !depsInUse.contains(it) }
|
||||
@@ -486,17 +465,6 @@ task checkDependenciesDotGradle {
|
||||
}
|
||||
tasks.build.dependsOn(tasks.checkDependenciesDotGradle)
|
||||
|
||||
def createGetBuildSrcDirectDepsTask(outputFileName) {
|
||||
return tasks
|
||||
.create(
|
||||
"getBuildSrcDeps_${java.util.UUID.randomUUID()}".toString(),
|
||||
Exec) {
|
||||
workingDir "${rootDir}/buildSrc"
|
||||
commandLine '../gradlew', 'exportDependencies',
|
||||
"-PdependencyExportFile=${outputFileName}"
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.ext {
|
||||
invokeJavaDiffFormatScript = { action ->
|
||||
def javaHome = project.findProperty('org.gradle.java.home')
|
||||
@@ -507,12 +475,8 @@ rootProject.ext {
|
||||
javaBin = ext.execInBash("which java", rootDir)
|
||||
}
|
||||
println("Running the formatting tool with $javaBin")
|
||||
def scriptDir = rootDir.path.endsWith('buildSrc')
|
||||
? "${rootDir}/../java-format"
|
||||
: "${rootDir}/java-format"
|
||||
def workingDir = rootDir.path.endsWith('buildSrc')
|
||||
? "${rootDir}/.."
|
||||
: rootDir
|
||||
def scriptDir = "${rootDir}/java-format"
|
||||
def workingDir = rootDir
|
||||
def formatDiffScript = "${scriptDir}/google-java-format-git-diff.sh"
|
||||
def pythonExe = getPythonExecutable()
|
||||
|
||||
@@ -574,7 +538,10 @@ task javadoc(type: Javadoc) {
|
||||
// In a lot of places we don't write @return so suppress warnings about that.
|
||||
// We don't report HTML lint errors because XJB-generated POJO files have
|
||||
// incorrect tags (like dangling </p> without the corresponding open tag.
|
||||
options.addBooleanOption('Xdoclint:all,-missing,-html', true)
|
||||
// Starting in Java 25, references to primitives and arrays are forbidden.
|
||||
// The JAXB-generated classes have array references, and we suppress the
|
||||
// error with '-reference'.
|
||||
options.addBooleanOption('Xdoclint:all,-missing,-html,-reference', true)
|
||||
options.addBooleanOption("-allow-script-in-comments",true)
|
||||
options.tags = ["type:a:Generic Type",
|
||||
"error:a:Expected Error",
|
||||
@@ -592,10 +559,16 @@ task coreDev {
|
||||
dependsOn 'javadoc'
|
||||
dependsOn 'checkDependenciesDotGradle'
|
||||
dependsOn 'checkLicense'
|
||||
// TODO: @ptkach reenable after console design merged
|
||||
// dependsOn ':console-webapp:runConsoleWebappUnitTests'
|
||||
dependsOn ':core:check'
|
||||
dependsOn 'assemble'
|
||||
|
||||
if (gradle.startParameter.parallelProjectExecutionEnabled
|
||||
&& gradle.startParameter.taskNames.contains("coreDev")) {
|
||||
throw new GradleException(
|
||||
"ERROR: 'coreDev' cannot run with --parallel due to checkLicense constraints.\n"
|
||||
+ "Please run: ./gradlew coreDev --no-parallel"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
javadocDependentTasks.each { tasks.javadoc.dependsOn(it) }
|
||||
@@ -610,7 +583,7 @@ task deployCloudSchedulerAndQueue {
|
||||
commandLine 'go', 'run',
|
||||
"./deployCloudSchedulerAndQueue.go",
|
||||
"${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml",
|
||||
"${rootDir}/core/src/main/java/google/registry/env/${env}/default/WEB-INF/cloud-scheduler-tasks.xml",
|
||||
"${rootDir}/core/src/main/java/google/registry/config/files/tasks/cloud-scheduler-tasks-${env}.xml",
|
||||
"domain-registry-${env}"
|
||||
}
|
||||
exec {
|
||||
@@ -618,7 +591,7 @@ task deployCloudSchedulerAndQueue {
|
||||
commandLine 'go', 'run',
|
||||
"./deployCloudSchedulerAndQueue.go",
|
||||
"${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml",
|
||||
"${rootDir}/core/src/main/java/google/registry/env/common/default/WEB-INF/cloud-tasks-queue.xml",
|
||||
"${rootDir}/core/src/main/java/google/registry/config/files/cloud-tasks-queue.xml",
|
||||
"domain-registry-${env}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import java.io.PrintStream;
|
||||
|
||||
val enableDependencyLocking: String by project
|
||||
val allowInsecureProtocol: String by project
|
||||
val allowInsecure = allowInsecureProtocol
|
||||
|
||||
buildscript {
|
||||
// We need to do this again within "buildscript" because setting it in the
|
||||
// main script doesn't affect build dependencies.
|
||||
val enableDependencyLocking: String by project
|
||||
if (enableDependencyLocking.toBoolean()) {
|
||||
// Lock application dependencies.
|
||||
dependencyLocking {
|
||||
lockAllConfigurations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
// Java static analysis plugins. Keep versions consistent with ../build.gradle
|
||||
|
||||
// We don't anticipate enabling the Gradle lint plugin because they will not support Kotlin
|
||||
// See https://github.com/nebula-plugins/gradle-lint-plugin/issues/166
|
||||
// id 'nebula.lint' version '16.0.2'
|
||||
id("net.ltgt.errorprone") version "3.1.0"
|
||||
checkstyle
|
||||
id("com.diffplug.spotless") version "6.20.0"
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
configDirectory.set(file("../config/checkstyle"))
|
||||
}
|
||||
|
||||
println("enableDependencyLocking is $enableDependencyLocking")
|
||||
if (enableDependencyLocking.toBoolean()) {
|
||||
// Lock application dependencies.
|
||||
dependencyLocking {
|
||||
lockAllConfigurations()
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
val mavenUrl = (project.ext.properties.get("mavenUrl") ?: "") as String
|
||||
if (mavenUrl.isEmpty()) {
|
||||
println("Java dependencies: Using Maven central...")
|
||||
mavenCentral()
|
||||
google()
|
||||
} else {
|
||||
maven {
|
||||
println("Java dependencies: Using repo ${mavenUrl}...")
|
||||
url = uri(mavenUrl)
|
||||
isAllowInsecureProtocol = allowInsecureProtocol == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply(from = "../dependencies.gradle")
|
||||
apply(from = "../dependency_lic.gradle")
|
||||
apply(from = "../java_common.gradle")
|
||||
|
||||
// 'listenablefuture' is folded into guava since v32. This block is required
|
||||
// until all transitive dependencies have upgraded past guava v32.
|
||||
// TODO(periodically): remove this block and see if build succeeds.
|
||||
configurations.all {
|
||||
resolutionStrategy
|
||||
.capabilitiesResolution
|
||||
.withCapability("com.google.guava:listenablefuture") {
|
||||
select("com.google.guava:guava:0")
|
||||
}
|
||||
}
|
||||
|
||||
project.the<SourceSetContainer>()["main"].java {
|
||||
srcDir("${project.buildDir}/generated/source/apt/main")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val deps = project.ext["dependencyMap"] as Map<*, *>
|
||||
val implementation by configurations
|
||||
val testImplementation by configurations
|
||||
val annotationProcessor by configurations
|
||||
implementation(deps["com.google.auth:google-auth-library-credentials"]!!)
|
||||
implementation(deps["com.google.auth:google-auth-library-oauth2-http"]!!)
|
||||
implementation(deps["com.google.auto.value:auto-value-annotations"]!!)
|
||||
// implementation(deps["com.google.common.html.types:types"]!!)
|
||||
implementation(deps["com.google.cloud:google-cloud-core"]!!)
|
||||
implementation(deps["com.google.cloud:google-cloud-storage"]!!)
|
||||
implementation(deps["com.google.guava:guava"]!!)
|
||||
implementation(deps["com.google.protobuf:protobuf-java"]!!)
|
||||
implementation(deps["com.google.template:soy"]!!)
|
||||
implementation(deps["org.apache.commons:commons-text"]!!)
|
||||
annotationProcessor(deps["com.google.auto.value:auto-value"]!!)
|
||||
testImplementation(deps["com.google.truth:truth"]!!)
|
||||
testImplementation(deps["org.junit.jupiter:junit-jupiter-api"]!!)
|
||||
testImplementation(deps["org.junit.jupiter:junit-jupiter-engine"]!!)
|
||||
testImplementation(deps["org.mockito:mockito-core"]!!)
|
||||
}
|
||||
|
||||
gradle.projectsEvaluated {
|
||||
tasks.withType<JavaCompile> {
|
||||
options.compilerArgs.add("-Xlint:unchecked")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("exportDependencies") {
|
||||
doLast {
|
||||
project.configurations.forEach {
|
||||
println("dependency is $it")
|
||||
// it.dependencies.findAll {
|
||||
// it.group != null
|
||||
// }.each {
|
||||
// output.println("${it.group}:${it.name}")
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.diffplug.durian:durian-collect:1.2.0=classpath
|
||||
com.diffplug.durian:durian-core:1.2.0=classpath
|
||||
com.diffplug.durian:durian-io:1.2.0=classpath
|
||||
com.diffplug.durian:durian-swt.os:4.2.0=classpath
|
||||
com.diffplug.spotless:com.diffplug.spotless.gradle.plugin:6.20.0=classpath
|
||||
com.diffplug.spotless:spotless-lib-extra:2.40.0=classpath
|
||||
com.diffplug.spotless:spotless-lib:2.40.0=classpath
|
||||
com.diffplug.spotless:spotless-plugin-gradle:6.20.0=classpath
|
||||
com.googlecode.concurrent-trees:concurrent-trees:2.6.1=classpath
|
||||
com.googlecode.javaewah:JavaEWAH:1.2.3=classpath
|
||||
com.squareup.okhttp3:okhttp:4.10.0=classpath
|
||||
com.squareup.okio:okio-jvm:3.0.0=classpath
|
||||
com.squareup.okio:okio:3.0.0=classpath
|
||||
dev.equo.ide:solstice:1.3.1=classpath
|
||||
net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:3.1.0=classpath
|
||||
net.ltgt.gradle:gradle-errorprone-plugin:3.1.0=classpath
|
||||
org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r=classpath
|
||||
org.eclipse.platform:org.eclipse.osgi:3.18.300=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.20=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib:1.9.20=classpath
|
||||
org.jetbrains:annotations:13.0=classpath
|
||||
org.slf4j:slf4j-api:1.7.36=classpath
|
||||
org.tukaani:xz:1.9=classpath
|
||||
empty=
|
||||
@@ -1,109 +0,0 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
aopalliance:aopalliance:1.0=annotationProcessor,buildScriptClasspath,compileClasspath
|
||||
args4j:args4j:2.0.23=buildScriptClasspath,compileClasspath
|
||||
com.fasterxml.jackson.core:jackson-core:2.16.1=buildScriptClasspath,compileClasspath
|
||||
com.fasterxml.jackson:jackson-bom:2.16.1=buildScriptClasspath,compileClasspath
|
||||
com.github.ben-manes.caffeine:caffeine:3.0.5=annotationProcessor
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor
|
||||
com.google.android:annotations:4.1.1.4=buildScriptClasspath
|
||||
com.google.api-client:google-api-client:2.3.0=buildScriptClasspath,compileClasspath
|
||||
com.google.api.grpc:gapic-google-cloud-storage-v2:2.35.0-alpha=buildScriptClasspath,compileClasspath
|
||||
com.google.api.grpc:grpc-google-cloud-storage-v2:2.35.0-alpha=buildScriptClasspath,compileClasspath
|
||||
com.google.api.grpc:proto-google-cloud-storage-v2:2.35.0-alpha=buildScriptClasspath,compileClasspath
|
||||
com.google.api.grpc:proto-google-common-protos:2.36.0=buildScriptClasspath,compileClasspath
|
||||
com.google.api.grpc:proto-google-iam-v1:1.31.0=buildScriptClasspath,compileClasspath
|
||||
com.google.api:api-common:2.28.0=buildScriptClasspath,compileClasspath
|
||||
com.google.api:gax-grpc:2.45.0=buildScriptClasspath,compileClasspath
|
||||
com.google.api:gax-httpjson:2.45.0=buildScriptClasspath,compileClasspath
|
||||
com.google.api:gax:2.45.0=buildScriptClasspath,compileClasspath
|
||||
com.google.apis:google-api-services-storage:v1-rev20240209-2.0.0=buildScriptClasspath,compileClasspath
|
||||
com.google.auth:google-auth-library-credentials:1.23.0=buildScriptClasspath,compileClasspath
|
||||
com.google.auth:google-auth-library-oauth2-http:1.23.0=buildScriptClasspath,compileClasspath
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.10.4=buildScriptClasspath,compileClasspath
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor
|
||||
com.google.auto.value:auto-value:1.10.4=annotationProcessor
|
||||
com.google.auto:auto-common:1.2.1=annotationProcessor
|
||||
com.google.cloud:google-cloud-core-grpc:2.35.0=buildScriptClasspath,compileClasspath
|
||||
com.google.cloud:google-cloud-core-http:2.35.0=buildScriptClasspath,compileClasspath
|
||||
com.google.cloud:google-cloud-core:2.35.0=buildScriptClasspath,compileClasspath
|
||||
com.google.cloud:google-cloud-storage:2.35.0=buildScriptClasspath,compileClasspath
|
||||
com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,buildScriptClasspath,compileClasspath
|
||||
com.google.code.gson:gson:2.10.1=buildScriptClasspath,compileClasspath
|
||||
com.google.common.html.types:types:1.0.6=buildScriptClasspath,compileClasspath
|
||||
com.google.errorprone:error_prone_annotation:2.23.0=annotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.23.0=annotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.26.1=buildScriptClasspath,compileClasspath
|
||||
com.google.errorprone:error_prone_check_api:2.23.0=annotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.23.0=annotationProcessor
|
||||
com.google.errorprone:error_prone_type_annotations:2.23.0=annotationProcessor
|
||||
com.google.escapevelocity:escapevelocity:0.9.1=buildScriptClasspath,compileClasspath
|
||||
com.google.guava:failureaccess:1.0.1=annotationProcessor
|
||||
com.google.guava:failureaccess:1.0.2=buildScriptClasspath,compileClasspath
|
||||
com.google.guava:guava-parent:32.1.1-jre=annotationProcessor
|
||||
com.google.guava:guava:32.1.1-jre=annotationProcessor
|
||||
com.google.guava:guava:33.1.0-jre=buildScriptClasspath,compileClasspath
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=buildScriptClasspath,compileClasspath
|
||||
com.google.http-client:google-http-client-apache-v2:1.44.1=buildScriptClasspath,compileClasspath
|
||||
com.google.http-client:google-http-client-appengine:1.44.1=buildScriptClasspath,compileClasspath
|
||||
com.google.http-client:google-http-client-gson:1.44.1=buildScriptClasspath,compileClasspath
|
||||
com.google.http-client:google-http-client-jackson2:1.44.1=buildScriptClasspath,compileClasspath
|
||||
com.google.http-client:google-http-client:1.44.1=buildScriptClasspath,compileClasspath
|
||||
com.google.inject.extensions:guice-multibindings:4.1.0=buildScriptClasspath,compileClasspath
|
||||
com.google.inject:guice:4.1.0=buildScriptClasspath,compileClasspath
|
||||
com.google.inject:guice:5.1.0=annotationProcessor
|
||||
com.google.j2objc:j2objc-annotations:2.8=buildScriptClasspath
|
||||
com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath
|
||||
com.google.jsinterop:jsinterop-annotations:1.0.1=buildScriptClasspath,compileClasspath
|
||||
com.google.oauth-client:google-oauth-client:1.35.0=buildScriptClasspath,compileClasspath
|
||||
com.google.protobuf:protobuf-java-util:3.25.2=buildScriptClasspath,compileClasspath
|
||||
com.google.protobuf:protobuf-java:3.19.6=annotationProcessor
|
||||
com.google.protobuf:protobuf-java:3.25.2=buildScriptClasspath,compileClasspath
|
||||
com.google.re2j:re2j:1.7=buildScriptClasspath
|
||||
com.google.template:soy:2021-02-01=buildScriptClasspath,compileClasspath
|
||||
com.ibm.icu:icu4j:57.1=buildScriptClasspath,compileClasspath
|
||||
commons-codec:commons-codec:1.16.1=buildScriptClasspath,compileClasspath
|
||||
io.github.eisop:dataflow-errorprone:3.34.0-eisop1=annotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor
|
||||
io.grpc:grpc-alts:1.62.2=buildScriptClasspath,compileClasspath
|
||||
io.grpc:grpc-api:1.62.2=buildScriptClasspath,compileClasspath
|
||||
io.grpc:grpc-auth:1.62.2=buildScriptClasspath,compileClasspath
|
||||
io.grpc:grpc-context:1.62.2=buildScriptClasspath,compileClasspath
|
||||
io.grpc:grpc-core:1.62.2=buildScriptClasspath,compileClasspath
|
||||
io.grpc:grpc-googleapis:1.62.2=buildScriptClasspath
|
||||
io.grpc:grpc-grpclb:1.62.2=buildScriptClasspath,compileClasspath
|
||||
io.grpc:grpc-inprocess:1.62.2=buildScriptClasspath,compileClasspath
|
||||
io.grpc:grpc-netty-shaded:1.62.2=buildScriptClasspath
|
||||
io.grpc:grpc-protobuf-lite:1.62.2=buildScriptClasspath
|
||||
io.grpc:grpc-protobuf:1.62.2=buildScriptClasspath,compileClasspath
|
||||
io.grpc:grpc-rls:1.62.2=buildScriptClasspath
|
||||
io.grpc:grpc-services:1.62.2=buildScriptClasspath
|
||||
io.grpc:grpc-stub:1.62.2=buildScriptClasspath,compileClasspath
|
||||
io.grpc:grpc-util:1.62.2=buildScriptClasspath
|
||||
io.grpc:grpc-xds:1.62.2=buildScriptClasspath
|
||||
io.opencensus:opencensus-api:0.31.1=buildScriptClasspath,compileClasspath
|
||||
io.opencensus:opencensus-contrib-http-util:0.31.1=buildScriptClasspath,compileClasspath
|
||||
io.opencensus:opencensus-proto:0.2.0=buildScriptClasspath
|
||||
io.perfmark:perfmark-api:0.27.0=buildScriptClasspath
|
||||
javax.annotation:javax.annotation-api:1.3.2=buildScriptClasspath,compileClasspath
|
||||
javax.annotation:jsr250-api:1.0=buildScriptClasspath,compileClasspath
|
||||
javax.inject:javax.inject:1=annotationProcessor,buildScriptClasspath,compileClasspath
|
||||
org.apache.commons:commons-lang3:3.13.0=buildScriptClasspath,compileClasspath
|
||||
org.apache.commons:commons-text:1.11.0=buildScriptClasspath,compileClasspath
|
||||
org.apache.httpcomponents:httpclient:4.5.14=buildScriptClasspath,compileClasspath
|
||||
org.apache.httpcomponents:httpcore:4.4.16=buildScriptClasspath,compileClasspath
|
||||
org.checkerframework:checker-qual:3.33.0=annotationProcessor
|
||||
org.checkerframework:checker-qual:3.42.0=buildScriptClasspath,compileClasspath
|
||||
org.codehaus.mojo:animal-sniffer-annotations:1.23=buildScriptClasspath
|
||||
org.conscrypt:conscrypt-openjdk-uber:2.5.2=buildScriptClasspath,compileClasspath
|
||||
org.json:json:20160212=buildScriptClasspath,compileClasspath
|
||||
org.ow2.asm:asm-analysis:7.0=buildScriptClasspath,compileClasspath
|
||||
org.ow2.asm:asm-commons:7.0=buildScriptClasspath,compileClasspath
|
||||
org.ow2.asm:asm-tree:7.0=buildScriptClasspath,compileClasspath
|
||||
org.ow2.asm:asm-util:7.0=buildScriptClasspath,compileClasspath
|
||||
org.ow2.asm:asm:7.0=buildScriptClasspath,compileClasspath
|
||||
org.pcollections:pcollections:3.1.4=annotationProcessor
|
||||
org.threeten:threetenbp:1.6.8=buildScriptClasspath,compileClasspath
|
||||
empty=
|
||||
@@ -1,3 +0,0 @@
|
||||
pluginsUrl=https://plugins.gradle.org/m2/
|
||||
allowInsecureProtocol=
|
||||
enableDependencyLocking=true
|
||||
@@ -1,211 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.gradle.plugin;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
|
||||
import static com.google.common.io.Resources.getResource;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.toByteArraySupplier;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.html.types.TrustedResourceUrls;
|
||||
import com.google.template.soy.SoyFileSet;
|
||||
import com.google.template.soy.tofu.SoyTofu;
|
||||
import google.registry.gradle.plugin.ProjectData.TaskData;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Creates the files for a web-page summary of a given {@link ProjectData}.
|
||||
*
|
||||
* <p>The main job of this class is rendering a tailored cover page that includes information about
|
||||
* the project and any task that ran.
|
||||
*
|
||||
* <p>It returns all the files that need uploading for the cover page to work. This includes any
|
||||
* report and log files linked to in the ProjectData, as well as a cover page (and associated
|
||||
* resources such as CSS files).
|
||||
*/
|
||||
final class CoverPageGenerator {
|
||||
|
||||
/** List of all resource files that will be uploaded as-is. */
|
||||
private static final ImmutableSet<Path> STATIC_RESOURCE_FILES =
|
||||
ImmutableSet.of(Paths.get("css", "style.css"));
|
||||
/** Name of the entry-point file that will be created. */
|
||||
private static final Path ENTRY_POINT = Paths.get("index.html");
|
||||
|
||||
private final ProjectData projectData;
|
||||
private final ImmutableSetMultimap<TaskData.State, TaskData> tasksByState;
|
||||
/**
|
||||
* The compiled SOY files.
|
||||
*
|
||||
* <p>Will be generated only when actually needed, because it takes a while to compile and we
|
||||
* don't want that to happen unless we actually use it.
|
||||
*/
|
||||
private SoyTofu tofu = null;
|
||||
|
||||
CoverPageGenerator(ProjectData projectData) {
|
||||
this.projectData = projectData;
|
||||
this.tasksByState =
|
||||
projectData.tasks().stream().collect(toImmutableSetMultimap(TaskData::state, task -> task));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the files that need uploading for the cover page to work.
|
||||
*
|
||||
* <p>This includes all the report files as well, to make sure that the link works.
|
||||
*/
|
||||
FilesWithEntryPoint getFilesToUpload() {
|
||||
ImmutableMap.Builder<Path, Supplier<byte[]>> builder = new ImmutableMap.Builder<>();
|
||||
// Add all the static resource pages
|
||||
STATIC_RESOURCE_FILES.stream().forEach(file -> builder.put(file, resourceLoader(file)));
|
||||
// Create the cover page
|
||||
// Note that the ByteArraySupplier here is lazy - the createCoverPage function is only called
|
||||
// when the resulting Supplier's get function is called.
|
||||
builder.put(ENTRY_POINT, toByteArraySupplier(this::createCoverPage));
|
||||
// Add all the files from the tasks
|
||||
tasksByState.values().stream()
|
||||
.flatMap(task -> task.reports().values().stream())
|
||||
.forEach(reportFiles -> builder.putAll(reportFiles.files()));
|
||||
// Add the logs of every test
|
||||
tasksByState.values().stream()
|
||||
.filter(task -> task.log().isPresent())
|
||||
.forEach(task -> builder.put(getLogPath(task), task.log().get()));
|
||||
|
||||
return FilesWithEntryPoint.create(builder.build(), ENTRY_POINT);
|
||||
}
|
||||
|
||||
/** Renders the cover page. */
|
||||
private String createCoverPage() {
|
||||
return getTofu()
|
||||
.newRenderer("google.registry.gradle.plugin.coverPage")
|
||||
.setData(getSoyData())
|
||||
.render();
|
||||
}
|
||||
|
||||
/** Converts the projectData and all taskData into all the data the soy template needs. */
|
||||
private ImmutableMap<String, Object> getSoyData() {
|
||||
ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
|
||||
|
||||
TaskData.State state =
|
||||
tasksByState.containsKey(TaskData.State.FAILURE)
|
||||
? TaskData.State.FAILURE
|
||||
: TaskData.State.SUCCESS;
|
||||
String title =
|
||||
state != TaskData.State.FAILURE
|
||||
? "Success!"
|
||||
: "Failed: "
|
||||
+ tasksByState.get(state).stream()
|
||||
.map(TaskData::uniqueName)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
builder.put("projectState", state.toString());
|
||||
builder.put("title", title);
|
||||
builder.put("cssFiles", ImmutableSet.of(TrustedResourceUrls.fromConstant("css/style.css")));
|
||||
builder.put("invocation", getInvocation());
|
||||
builder.put("tasksByState", getTasksByStateSoyData());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a soy-friendly map from the TaskData.State to the task itslef.
|
||||
*
|
||||
* <p>The key order in the resulting map is always the same (the order from the enum definition)
|
||||
* no matter the key order in the original tasksByState map.
|
||||
*/
|
||||
private ImmutableMap<String, Object> getTasksByStateSoyData() {
|
||||
ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
|
||||
|
||||
// We go over the States in the order they are defined rather than the order in which they
|
||||
// happen to be in the tasksByState Map.
|
||||
//
|
||||
// That way we guarantee a consistent order.
|
||||
for (TaskData.State state : TaskData.State.values()) {
|
||||
builder.put(
|
||||
state.toString(),
|
||||
tasksByState.get(state).stream()
|
||||
.map(task -> taskDataToSoy(task))
|
||||
.collect(toImmutableList()));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** returns a soy-friendly version of the given task data. */
|
||||
static ImmutableMap<String, Object> taskDataToSoy(TaskData task) {
|
||||
// Note that all instances of File.separator are replaced with forward slashes so that we can
|
||||
// generate a valid href on Windows.
|
||||
return new ImmutableMap.Builder<String, Object>()
|
||||
.put("uniqueName", task.uniqueName())
|
||||
.put("description", task.description())
|
||||
.put(
|
||||
"log",
|
||||
task.log().isPresent() ? getLogPath(task).toString().replace(File.separator, "/") : "")
|
||||
.put(
|
||||
"reports",
|
||||
task.reports().entrySet().stream()
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
Map.Entry::getKey,
|
||||
entry ->
|
||||
entry.getValue().files().isEmpty()
|
||||
? ""
|
||||
: entry
|
||||
.getValue()
|
||||
.entryPoint()
|
||||
.toString()
|
||||
.replace(File.separator, "/"))))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String getInvocation() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("./gradlew");
|
||||
projectData.tasksRequested().forEach(task -> builder.append(" ").append(task));
|
||||
projectData
|
||||
.projectProperties()
|
||||
.forEach((key, value) -> builder.append(String.format(" -P %s=%s", key, value)));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/** Returns a lazily created soy renderer */
|
||||
private SoyTofu getTofu() {
|
||||
if (tofu == null) {
|
||||
tofu =
|
||||
SoyFileSet.builder()
|
||||
.add(getResource(CoverPageGenerator.class, "soy/coverpage.soy"))
|
||||
.build()
|
||||
.compileToTofu();
|
||||
}
|
||||
return tofu;
|
||||
}
|
||||
|
||||
private static Path getLogPath(TaskData task) {
|
||||
// We replace colons with dashes so that the resulting filename is always valid, even in
|
||||
// Windows. As a dash is not a valid character in Java identifies, a task name cannot include
|
||||
// it, so the uniqueness of the name is perserved.
|
||||
return Paths.get("logs", task.uniqueName().replace(":", "-") + ".log");
|
||||
}
|
||||
|
||||
private static Supplier<byte[]> resourceLoader(Path path) {
|
||||
return toByteArraySupplier(getResource(CoverPageGenerator.class, path.toString()));
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.gradle.plugin;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Holds a set of files with a browser-friendly entry point to those files.
|
||||
*
|
||||
* <p>The file data is lazily generated.
|
||||
*
|
||||
* <p>If there is at least one file, it's guaranteed that the entry point is one of these files.
|
||||
*/
|
||||
@AutoValue
|
||||
abstract class FilesWithEntryPoint {
|
||||
|
||||
/**
|
||||
* All files that are part of this report, keyed from their path to a supplier of their content.
|
||||
*
|
||||
* <p>The reason we use a supplier instead of loading the content is in case the content is very
|
||||
* large...
|
||||
*
|
||||
* <p>Also, no point in doing IO before we need it!
|
||||
*/
|
||||
abstract ImmutableMap<Path, Supplier<byte[]>> files();
|
||||
|
||||
/**
|
||||
* The file that gives access (links...) to all the data in the report.
|
||||
*
|
||||
* <p>Guaranteed to be a key in {@link #files} if and only if files isn't empty.
|
||||
*/
|
||||
abstract Path entryPoint();
|
||||
|
||||
static FilesWithEntryPoint create(ImmutableMap<Path, Supplier<byte[]>> files, Path entryPoint) {
|
||||
checkArgument(files.isEmpty() || files.containsKey(entryPoint));
|
||||
return new AutoValue_FilesWithEntryPoint(files, entryPoint);
|
||||
}
|
||||
|
||||
static FilesWithEntryPoint createSingleFile(Path path, Supplier<byte[]> data) {
|
||||
return create(ImmutableMap.of(path, data), path);
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.gradle.plugin;
|
||||
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.collect.Iterables.getOnlyElement;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.cloud.storage.BlobInfo;
|
||||
import com.google.cloud.storage.Storage;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.common.io.Resources;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/** Utility functions used in the GCS plugin. */
|
||||
final class GcsPluginUtils {
|
||||
|
||||
private static final ImmutableMap<String, String> EXTENSION_TO_CONTENT_TYPE =
|
||||
new ImmutableMap.Builder<String, String>()
|
||||
.put("html", "text/html")
|
||||
.put("htm", "text/html")
|
||||
.put("log", "text/plain")
|
||||
.put("txt", "text/plain")
|
||||
.put("css", "text/css")
|
||||
.put("xml", "text/xml")
|
||||
.put("zip", "application/zip")
|
||||
.put("js", "text/javascript")
|
||||
.build();
|
||||
|
||||
private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
||||
|
||||
static Path toNormalizedPath(File file) {
|
||||
return file.toPath().toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
static Path toNormalizedPath(Path file) {
|
||||
return file.toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
static String getContentType(String fileName) {
|
||||
return EXTENSION_TO_CONTENT_TYPE.getOrDefault(
|
||||
Files.getFileExtension(fileName), DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
static void uploadFileToGcs(
|
||||
Storage storage, String bucket, Path path, Supplier<byte[]> dataSupplier) {
|
||||
// Replace Windows file separators with forward slashes.
|
||||
String filename = path.toString().replace(File.separator, "/");
|
||||
storage.create(
|
||||
BlobInfo.newBuilder(bucket, filename).setContentType(getContentType(filename)).build(),
|
||||
dataSupplier.get());
|
||||
}
|
||||
|
||||
static void uploadFilesToGcsMultithread(
|
||||
Storage storage, String bucket, Path folder, Map<Path, Supplier<byte[]>> files) {
|
||||
ImmutableMap.Builder<Path, Thread> threads = new ImmutableMap.Builder<>();
|
||||
files.forEach(
|
||||
(path, dataSupplier) -> {
|
||||
Thread thread =
|
||||
new Thread(
|
||||
() -> uploadFileToGcs(storage, bucket, folder.resolve(path), dataSupplier));
|
||||
thread.start();
|
||||
threads.put(path, thread);
|
||||
});
|
||||
threads
|
||||
.build()
|
||||
.forEach(
|
||||
(path, thread) -> {
|
||||
try {
|
||||
thread.join();
|
||||
} catch (InterruptedException e) {
|
||||
System.out.format("Upload of %s interrupted", path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static Supplier<byte[]> toByteArraySupplier(String data) {
|
||||
return () -> data.getBytes(UTF_8);
|
||||
}
|
||||
|
||||
static Supplier<byte[]> toByteArraySupplier(Supplier<String> dataSupplier) {
|
||||
return () -> dataSupplier.get().getBytes(UTF_8);
|
||||
}
|
||||
|
||||
static Supplier<byte[]> toByteArraySupplier(File file) {
|
||||
return () -> {
|
||||
try {
|
||||
return Files.toByteArray(file);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static Supplier<byte[]> toByteArraySupplier(URL url) {
|
||||
return () -> {
|
||||
try {
|
||||
return Resources.toByteArray(url);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all the files generated by a Report into a FilesWithEntryPoint object.
|
||||
*
|
||||
* <p>Every FilesWithEntryPoint must have a single link "entry point" that gives users access to
|
||||
* all the files. If the report generated just one file - we will just link to that file.
|
||||
*
|
||||
* <p>However, if the report generated more than one file - the only thing we can safely do is to
|
||||
* zip all the files and link to the zip file.
|
||||
*
|
||||
* <p>As an alternative to using a zip file, we allow the caller to supply an optional "entry
|
||||
* point" file that will link to all the other files. If that file is given and is "appropriate"
|
||||
* (exists and is in the correct location) - we will upload all the report files "as is" and link
|
||||
* to the entry file.
|
||||
*
|
||||
* @param destination the location of the output. Either a file or a directory. If a directory -
|
||||
* then all the files inside that directory are the outputs we're looking for.
|
||||
* @param entryPointHint If present - a hint to what the entry point to this directory tree is.
|
||||
* Will only be used if all of the following apply: (a) {@code
|
||||
* destination.isDirectory()==true}, (b) there are 2 or more files in the {@code destination}
|
||||
* directory, and (c) {@code entryPointHint.get()} is one of the files nested inside of the
|
||||
* {@code destination} directory.
|
||||
*/
|
||||
static FilesWithEntryPoint readFilesWithEntryPoint(
|
||||
File destination, Optional<File> entryPointHint, Path rootDir) {
|
||||
|
||||
Path destinationPath = rootDir.relativize(toNormalizedPath(destination));
|
||||
|
||||
if (destination.isFile()) {
|
||||
// The destination is a single file - find its root, and add this single file to the
|
||||
// FilesWithEntryPoint.
|
||||
return FilesWithEntryPoint.createSingleFile(
|
||||
destinationPath, toByteArraySupplier(destination));
|
||||
}
|
||||
|
||||
if (!destination.isDirectory()) {
|
||||
// This isn't a file nor a directory - so it doesn't exist! Return empty FilesWithEntryPoint
|
||||
return FilesWithEntryPoint.create(ImmutableMap.of(), destinationPath);
|
||||
}
|
||||
|
||||
// The destination is a directory - find all the actual files first
|
||||
ImmutableMap<Path, Supplier<byte[]>> files =
|
||||
Streams.stream(Files.fileTraverser().depthFirstPreOrder(destination))
|
||||
.filter(File::isFile)
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
file -> rootDir.relativize(toNormalizedPath(file)),
|
||||
GcsPluginUtils::toByteArraySupplier));
|
||||
|
||||
if (files.isEmpty()) {
|
||||
// The directory exists, but is empty. Return empty FilesWithEntryPoint
|
||||
return FilesWithEntryPoint.create(ImmutableMap.of(), destinationPath);
|
||||
}
|
||||
|
||||
if (files.size() == 1) {
|
||||
// We got a directory, but it only has a single file. We can link to that.
|
||||
return FilesWithEntryPoint.create(files, getOnlyElement(files.keySet()));
|
||||
}
|
||||
|
||||
// There are multiple files in the report! We need to check the entryPointHint
|
||||
Optional<Path> entryPointPath =
|
||||
entryPointHint.map(file -> rootDir.relativize(toNormalizedPath(file)));
|
||||
|
||||
if (entryPointPath.isPresent() && files.containsKey(entryPointPath.get())) {
|
||||
// We were given the entry point! Use it!
|
||||
return FilesWithEntryPoint.create(files, entryPointPath.get());
|
||||
}
|
||||
|
||||
// We weren't given an appropriate entry point. But we still need a single link to all this data
|
||||
// - so we'll zip it and just host a single file.
|
||||
Path zipFilePath = destinationPath.resolve(destinationPath.getFileName().toString() + ".zip");
|
||||
return FilesWithEntryPoint.createSingleFile(zipFilePath, createZippedByteArraySupplier(files));
|
||||
}
|
||||
|
||||
static Supplier<byte[]> createZippedByteArraySupplier(Map<Path, Supplier<byte[]>> files) {
|
||||
return () -> zipFiles(files);
|
||||
}
|
||||
|
||||
private static byte[] zipFiles(Map<Path, Supplier<byte[]>> files) {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zip = new ZipOutputStream(output)) {
|
||||
for (Path path : files.keySet()) {
|
||||
zip.putNextEntry(new ZipEntry(path.toString()));
|
||||
zip.write(files.get(path).get());
|
||||
zip.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private GcsPluginUtils() {}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.gradle.plugin;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* All the data of a root Gradle project.
|
||||
*
|
||||
* <p>This is basically all the "relevant" data from a Gradle Project, arranged in an immutable and
|
||||
* more convenient way.
|
||||
*/
|
||||
@AutoValue
|
||||
abstract class ProjectData {
|
||||
|
||||
abstract String name();
|
||||
|
||||
abstract String description();
|
||||
|
||||
abstract String gradleVersion();
|
||||
|
||||
abstract ImmutableMap<String, String> projectProperties();
|
||||
|
||||
abstract ImmutableMap<String, String> systemProperties();
|
||||
|
||||
abstract ImmutableSet<String> tasksRequested();
|
||||
|
||||
abstract ImmutableSet<TaskData> tasks();
|
||||
|
||||
abstract Builder toBuilder();
|
||||
|
||||
static Builder builder() {
|
||||
return new AutoValue_ProjectData.Builder();
|
||||
}
|
||||
|
||||
@AutoValue.Builder
|
||||
abstract static class Builder {
|
||||
abstract Builder setName(String name);
|
||||
|
||||
abstract Builder setDescription(String description);
|
||||
|
||||
abstract Builder setGradleVersion(String gradleVersion);
|
||||
|
||||
abstract Builder setProjectProperties(Map<String, String> projectProperties);
|
||||
|
||||
abstract Builder setSystemProperties(Map<String, String> systemProperties);
|
||||
|
||||
abstract Builder setTasksRequested(Iterable<String> tasksRequested);
|
||||
|
||||
abstract ImmutableSet.Builder<TaskData> tasksBuilder();
|
||||
|
||||
Builder addTask(TaskData task) {
|
||||
tasksBuilder().add(task);
|
||||
return this;
|
||||
}
|
||||
|
||||
abstract ProjectData build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Relevant data to a single Task's.
|
||||
*
|
||||
* <p>Some Tasks are also "Reporting", meaning they create file outputs we want to upload in
|
||||
* various formats. The format that interests us the most is "html", as that's nicely browsable,
|
||||
* but they might also have other formats.
|
||||
*/
|
||||
@AutoValue
|
||||
abstract static class TaskData {
|
||||
|
||||
enum State {
|
||||
/** The task has failed for some reason. */
|
||||
FAILURE,
|
||||
/** The task was actually run and has finished successfully. */
|
||||
SUCCESS,
|
||||
/** The task was up-to-date and successful, and hence didn't need to run again. */
|
||||
UP_TO_DATE
|
||||
}
|
||||
|
||||
abstract String uniqueName();
|
||||
|
||||
abstract String description();
|
||||
|
||||
abstract State state();
|
||||
|
||||
abstract Optional<Supplier<byte[]>> log();
|
||||
|
||||
/**
|
||||
* Returns the FilesWithEntryPoint for every report, keyed on the report type.
|
||||
*
|
||||
* <p>The "html" report type is the most interesting, but there are other report formats.
|
||||
*/
|
||||
abstract ImmutableMap<String, FilesWithEntryPoint> reports();
|
||||
|
||||
abstract Builder toBuilder();
|
||||
|
||||
static Builder builder() {
|
||||
return new AutoValue_ProjectData_TaskData.Builder();
|
||||
}
|
||||
|
||||
@AutoValue.Builder
|
||||
abstract static class Builder {
|
||||
abstract Builder setUniqueName(String name);
|
||||
|
||||
abstract Builder setDescription(String description);
|
||||
|
||||
abstract Builder setState(State state);
|
||||
|
||||
abstract Builder setLog(Supplier<byte[]> log);
|
||||
|
||||
abstract ImmutableMap.Builder<String, FilesWithEntryPoint> reportsBuilder();
|
||||
|
||||
Builder putReport(String type, FilesWithEntryPoint reportFiles) {
|
||||
reportsBuilder().put(type, reportFiles);
|
||||
return this;
|
||||
}
|
||||
|
||||
abstract TaskData build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.gradle.plugin;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.readFilesWithEntryPoint;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.toByteArraySupplier;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.toNormalizedPath;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.uploadFileToGcs;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.uploadFilesToGcsMultithread;
|
||||
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
import com.google.cloud.storage.Storage;
|
||||
import com.google.cloud.storage.StorageOptions;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.io.Files;
|
||||
import google.registry.gradle.plugin.ProjectData.TaskData;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.reporting.DirectoryReport;
|
||||
import org.gradle.api.reporting.Report;
|
||||
import org.gradle.api.reporting.ReportContainer;
|
||||
import org.gradle.api.reporting.Reporting;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
/** A task that uploads the Reports generated by other tasks to GCS. */
|
||||
public class ReportUploader extends DefaultTask {
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
private static final ImmutableMap<String, BiConsumer<ReportUploader, String>> UPLOAD_FUNCTIONS =
|
||||
ImmutableMap.of(
|
||||
"file://", ReportUploader::saveResultsToLocalFolder,
|
||||
"gcs://", ReportUploader::uploadResultsToGcs);
|
||||
|
||||
private final ArrayList<Task> tasks = new ArrayList<>();
|
||||
private final HashMap<String, StringBuilder> logs = new HashMap<>();
|
||||
private Project project;
|
||||
|
||||
private String destination = null;
|
||||
private String credentialsFile = null;
|
||||
private String multithreadedUpload = null;
|
||||
|
||||
/**
|
||||
* Sets the destination of the reports.
|
||||
*
|
||||
* <p>Currently supports two types of destinations:
|
||||
*
|
||||
* <ul>
|
||||
* <li>file://[absulute local path], e.g. file:///tmp/buildOutputs/
|
||||
* <li>gcs://[bucket name]/[optional path], e.g. gcs://my-bucket/buildOutputs/
|
||||
* </ul>
|
||||
*/
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public void setCredentialsFile(String credentialsFile) {
|
||||
this.credentialsFile = credentialsFile;
|
||||
}
|
||||
|
||||
public void setMultithreadedUpload(String multithreadedUpload) {
|
||||
this.multithreadedUpload = multithreadedUpload;
|
||||
}
|
||||
|
||||
/** Converts the given Gradle Project into a ProjectData. */
|
||||
private ProjectData createProjectData() {
|
||||
ProjectData.Builder builder =
|
||||
ProjectData.builder()
|
||||
.setName(project.getPath() + project.getName())
|
||||
.setDescription(
|
||||
Optional.ofNullable(project.getDescription()).orElse("[No description available]"))
|
||||
.setGradleVersion(project.getGradle().getGradleVersion())
|
||||
.setProjectProperties(project.getGradle().getStartParameter().getProjectProperties())
|
||||
.setSystemProperties(project.getGradle().getStartParameter().getSystemPropertiesArgs())
|
||||
.setTasksRequested(project.getGradle().getStartParameter().getTaskNames());
|
||||
|
||||
Path rootDir = toNormalizedPath(project.getRootDir());
|
||||
tasks.stream()
|
||||
.filter(task -> task.getState().getExecuted() || task.getState().getUpToDate())
|
||||
.map(task -> createTaskData(task, rootDir))
|
||||
.forEach(builder.tasksBuilder()::add);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Gradle Task into a TaskData.
|
||||
*
|
||||
* @param rootDir the root directory of the main Project - used to get the relative path of any
|
||||
* Task files.
|
||||
*/
|
||||
private TaskData createTaskData(Task task, Path rootDir) {
|
||||
TaskData.State state =
|
||||
task.getState().getFailure() != null
|
||||
? TaskData.State.FAILURE
|
||||
: task.getState().getUpToDate() ? TaskData.State.UP_TO_DATE : TaskData.State.SUCCESS;
|
||||
String log = logs.get(task.getPath()).toString();
|
||||
|
||||
TaskData.Builder builder =
|
||||
TaskData.builder()
|
||||
.setState(state)
|
||||
.setUniqueName(task.getPath())
|
||||
.setDescription(
|
||||
Optional.ofNullable(task.getDescription()).orElse("[No description available]"));
|
||||
if (!log.isEmpty()) {
|
||||
builder.setLog(toByteArraySupplier(log));
|
||||
}
|
||||
|
||||
Reporting<? extends ReportContainer<? extends Report>> reporting = asReporting(task);
|
||||
|
||||
if (reporting != null) {
|
||||
// This Task is also a Reporting task! It has a destination file/directory for every supported
|
||||
// format.
|
||||
// Add the files for each of the formats into the ReportData.
|
||||
reporting
|
||||
.getReports()
|
||||
.getAsMap()
|
||||
.forEach(
|
||||
(type, report) -> {
|
||||
File destination = report.getOutputLocation().get().getAsFile();
|
||||
// The destination could be a file, or a directory. If it's a directory - the Report
|
||||
// could have created multiple files - and we need to know to which one of those to
|
||||
// link.
|
||||
//
|
||||
// If we're lucky, whoever implemented the Report made sure to extend
|
||||
// DirectoryReport, which gives us the entry point to all the files.
|
||||
//
|
||||
// This isn't guaranteed though, as it depends on the implementer.
|
||||
Optional<File> entryPointHint =
|
||||
destination.isDirectory() && (report instanceof DirectoryReport)
|
||||
? Optional.ofNullable(((DirectoryReport) report).getEntryPoint())
|
||||
: Optional.empty();
|
||||
builder
|
||||
.reportsBuilder()
|
||||
.put(type, readFilesWithEntryPoint(destination, entryPointHint, rootDir));
|
||||
});
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private FilesWithEntryPoint generateFilesToUpload() {
|
||||
ProjectData projectData = createProjectData();
|
||||
CoverPageGenerator coverPageGenerator = new CoverPageGenerator(projectData);
|
||||
return coverPageGenerator.getFilesToUpload();
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void uploadResults() {
|
||||
System.out.format("ReportUploader: destination= '%s'\n", destination);
|
||||
|
||||
try {
|
||||
|
||||
if (isNullOrEmpty(destination)) {
|
||||
System.out.format("ReportUploader: no destination given, skipping...\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for (String key : UPLOAD_FUNCTIONS.keySet()) {
|
||||
if (destination.startsWith(key)) {
|
||||
UPLOAD_FUNCTIONS.get(key).accept(this, destination.substring(key.length()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
System.out.format(
|
||||
"ReportUploader: given destination '%s' doesn't start with one of %s."
|
||||
+ " Defaulting to saving in /tmp\n",
|
||||
destination, UPLOAD_FUNCTIONS.keySet());
|
||||
saveResultsToLocalFolder("/tmp/");
|
||||
} catch (Throwable e) {
|
||||
System.out.format("ReportUploader: Encountered error %s\n", e);
|
||||
e.printStackTrace(System.out);
|
||||
System.out.format("ReportUploader: skipping upload\n");
|
||||
}
|
||||
}
|
||||
|
||||
private void saveResultsToLocalFolder(String absoluteFolderName) {
|
||||
Path folder = Paths.get(absoluteFolderName, createUniqueFolderName());
|
||||
checkArgument(
|
||||
folder.isAbsolute(),
|
||||
"Local files destination must be an absolute path, but is %s",
|
||||
absoluteFolderName);
|
||||
FilesWithEntryPoint filesToUpload = generateFilesToUpload();
|
||||
System.out.format(
|
||||
"ReportUploader: going to save %s files to %s\n", filesToUpload.files().size(), folder);
|
||||
filesToUpload
|
||||
.files()
|
||||
.forEach((path, dataSupplier) -> saveFile(folder.resolve(path), dataSupplier));
|
||||
System.out.format(
|
||||
"ReportUploader: report saved to file://%s\n", folder.resolve(filesToUpload.entryPoint()));
|
||||
}
|
||||
|
||||
private void saveFile(Path path, Supplier<byte[]> dataSupplier) {
|
||||
File dir = path.getParent().toFile();
|
||||
if (!dir.isDirectory()) {
|
||||
checkState(dir.mkdirs(), "Couldn't create directory %s", dir);
|
||||
}
|
||||
try {
|
||||
Files.write(dataSupplier.get(), path.toFile());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void uploadResultsToGcs(String destination) {
|
||||
checkArgument(
|
||||
!destination.isEmpty(), "destination must include at least the bucket name, but is empty");
|
||||
Path bucketWithFolder = Paths.get(destination, createUniqueFolderName());
|
||||
String bucket = bucketWithFolder.getName(0).toString();
|
||||
Path folder = bucketWithFolder.subpath(1, bucketWithFolder.getNameCount());
|
||||
|
||||
StorageOptions.Builder storageOptions = StorageOptions.newBuilder();
|
||||
if (!isNullOrEmpty(credentialsFile)) {
|
||||
try {
|
||||
storageOptions.setCredentials(
|
||||
GoogleCredentials.fromStream(new FileInputStream(credentialsFile)));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
Storage storage = storageOptions.build().getService();
|
||||
|
||||
FilesWithEntryPoint filesToUpload = generateFilesToUpload();
|
||||
|
||||
System.out.format(
|
||||
"ReportUploader: going to upload %s files to %s/%s\n",
|
||||
filesToUpload.files().size(), bucket, folder);
|
||||
if ("yes".equals(multithreadedUpload)) {
|
||||
System.out.format("ReportUploader: multi-threaded upload\n");
|
||||
uploadFilesToGcsMultithread(storage, bucket, folder, filesToUpload.files());
|
||||
} else {
|
||||
System.out.format("ReportUploader: single threaded upload\n");
|
||||
filesToUpload
|
||||
.files()
|
||||
.forEach(
|
||||
(path, dataSupplier) -> {
|
||||
System.out.format("ReportUploader: Uploading %s\n", path);
|
||||
uploadFileToGcs(storage, bucket, folder.resolve(path), dataSupplier);
|
||||
});
|
||||
}
|
||||
System.out.format(
|
||||
"ReportUploader: report uploaded to https://storage.googleapis.com/%s/%s\n",
|
||||
bucket, folder.resolve(filesToUpload.entryPoint()));
|
||||
}
|
||||
|
||||
void setProject(Project project) {
|
||||
this.project = project;
|
||||
|
||||
for (Project subProject : project.getAllprojects()) {
|
||||
subProject.getTasks().all(this::addTask);
|
||||
}
|
||||
}
|
||||
|
||||
private void addTask(Task task) {
|
||||
if (task instanceof ReportUploader) {
|
||||
return;
|
||||
}
|
||||
tasks.add(task);
|
||||
StringBuilder log = new StringBuilder();
|
||||
checkArgument(
|
||||
!logs.containsKey(task.getPath()),
|
||||
"Multiple tasks with the same .getPath()=%s",
|
||||
task.getPath());
|
||||
logs.put(task.getPath(), log);
|
||||
task.getLogging().addStandardOutputListener(output -> log.append(output));
|
||||
task.getLogging().addStandardErrorListener(output -> log.append(output));
|
||||
task.finalizedBy(this);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Reporting<? extends ReportContainer<? extends Report>> asReporting(Task task) {
|
||||
if (task instanceof Reporting) {
|
||||
return (Reporting<? extends ReportContainer<? extends Report>>) task;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String createUniqueFolderName() {
|
||||
return String.format(
|
||||
"%h-%h-%h-%h",
|
||||
SECURE_RANDOM.nextInt(),
|
||||
SECURE_RANDOM.nextInt(),
|
||||
SECURE_RANDOM.nextInt(),
|
||||
SECURE_RANDOM.nextInt());
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.gradle.plugin;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
|
||||
/**
|
||||
* Plugin setting up the ReportUploader task.
|
||||
*
|
||||
* <p>It goes over all the tasks in a project and pass them on to the ReportUploader task for set
|
||||
* up.
|
||||
*
|
||||
* <p>Note that since we're passing in all the projects' tasks - this includes the ReportUploader
|
||||
* itself! It's up to the ReportUploader to take care of not having "infinite loops" caused by
|
||||
* waiting for itself to end before finishing.
|
||||
*/
|
||||
public class ReportUploaderPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
ReportUploader reportUploader =
|
||||
project
|
||||
.getTasks()
|
||||
.create(
|
||||
"reportUploader",
|
||||
ReportUploader.class,
|
||||
task -> {
|
||||
task.setDescription("Uploads the reports to GCS bucket");
|
||||
task.setGroup("uploads");
|
||||
});
|
||||
|
||||
reportUploader.setProject(project);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.task_state_SUCCESS {
|
||||
color: green;
|
||||
}
|
||||
.task_state_FAILURE {
|
||||
color: red;
|
||||
}
|
||||
.task_name {
|
||||
display: block;
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
}
|
||||
.task_description {
|
||||
display: block;
|
||||
margin-left: 1em;
|
||||
color: gray;
|
||||
}
|
||||
.report_links {
|
||||
margin-left: 1em;
|
||||
}
|
||||
.report_link_broken {
|
||||
text-decoration: line-through;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
{namespace google.registry.gradle.plugin}
|
||||
|
||||
{template .coverPage}
|
||||
{@param title: string}
|
||||
{@param cssFiles: list<trusted_resource_uri>}
|
||||
{@param projectState: string}
|
||||
{@param invocation: string}
|
||||
{@param tasksByState: map<string, list<[uniqueName: string, description: string, log: string, reports: map<string, string>]>>}
|
||||
|
||||
<title>{$title}</title>
|
||||
{for $cssFile in $cssFiles}
|
||||
<link rel="stylesheet" type="text/css" href="{$cssFile}">
|
||||
{/for}
|
||||
<body>
|
||||
<div class="project">
|
||||
<h1 class="project_title {'task_state_' + $projectState}">{$title}</h1>
|
||||
<span class="project_subtitle">
|
||||
Build results for <span class="project_invocation">{$invocation}</span>
|
||||
</span>
|
||||
|
||||
{for $taskState in mapKeys($tasksByState)}
|
||||
{if length($tasksByState[$taskState]) > 0}
|
||||
{call .tasksOfState}
|
||||
{param state: $taskState /}
|
||||
{param tasks: $tasksByState[$taskState] /}
|
||||
{/call}
|
||||
{/if}
|
||||
{/for}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
{/template}
|
||||
|
||||
{template .tasksOfState}
|
||||
{@param state: string}
|
||||
{@param tasks: list<[uniqueName: string, description: string, log: string, reports: map<string, string>]>}
|
||||
|
||||
<div class="{'task_state_' + $state}">
|
||||
<p>{$state}</p>
|
||||
// Place the tasks with actual reports first, since those are more likely to be useful
|
||||
{for $task in $tasks}
|
||||
{if length(mapKeys($task.reports)) > 0}
|
||||
{call .task}
|
||||
{param task: $task /}
|
||||
{/call}
|
||||
{/if}
|
||||
{/for}
|
||||
// Followup with reports without links
|
||||
{for $task in $tasks}
|
||||
{if length(mapKeys($task.reports)) == 0}
|
||||
{call .task}
|
||||
{param task: $task /}
|
||||
{/call}
|
||||
{/if}
|
||||
{/for}
|
||||
</div>
|
||||
{/template}
|
||||
|
||||
{template .task}
|
||||
{@param task: [uniqueName: string, description: string, log: string, reports: map<string, string>]}
|
||||
{call .taskInternal}
|
||||
{param uniqueName: $task.uniqueName /}
|
||||
{param description: $task.description /}
|
||||
{param log: $task.log /}
|
||||
{param reports: $task.reports /}
|
||||
{/call}
|
||||
{/template}
|
||||
|
||||
{template .taskInternal}
|
||||
{@param uniqueName: string}
|
||||
{@param description: string}
|
||||
{@param log: string}
|
||||
{@param reports: map<string, string>}
|
||||
|
||||
<div class="task">
|
||||
<span class="task_name">{$uniqueName}</span>
|
||||
<span class="task_description">{$description}</span>
|
||||
<span class="report_links">
|
||||
{if $log}
|
||||
<a href="{$log}">[log]</a>
|
||||
{else}
|
||||
<span class="report_link_broken">[log]</span>
|
||||
{/if}
|
||||
{for $type in mapKeys($reports)}
|
||||
{if $reports[$type]}
|
||||
<a href="{$reports[$type]}">[{$type}]</a>
|
||||
{else}
|
||||
<span class="report_link_broken">[{$type}]</span>
|
||||
{/if}
|
||||
{/for}
|
||||
</span>
|
||||
</div>
|
||||
{/template}
|
||||
@@ -1,278 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.gradle.plugin;
|
||||
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.toByteArraySupplier;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.gradle.plugin.ProjectData.TaskData;
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Tests for {@link CoverPageGenerator} */
|
||||
final class CoverPageGeneratorTest {
|
||||
|
||||
private static final ProjectData EMPTY_PROJECT =
|
||||
ProjectData.builder()
|
||||
.setName("project-name")
|
||||
.setDescription("project-description")
|
||||
.setGradleVersion("gradle-version")
|
||||
.setProjectProperties(ImmutableMap.of("key", "value"))
|
||||
.setSystemProperties(ImmutableMap.of())
|
||||
.setTasksRequested(ImmutableSet.of(":a:task1", ":a:task2"))
|
||||
.build();
|
||||
|
||||
private static final TaskData EMPTY_TASK_SUCCESS =
|
||||
TaskData.builder()
|
||||
.setUniqueName("task-success")
|
||||
.setDescription("a successful task")
|
||||
.setState(TaskData.State.SUCCESS)
|
||||
.build();
|
||||
|
||||
private static final TaskData EMPTY_TASK_FAILURE =
|
||||
TaskData.builder()
|
||||
.setUniqueName("task-failure")
|
||||
.setDescription("a failed task")
|
||||
.setState(TaskData.State.FAILURE)
|
||||
.build();
|
||||
|
||||
private static final TaskData EMPTY_TASK_UP_TO_DATE =
|
||||
TaskData.builder()
|
||||
.setUniqueName("task-up-to-date")
|
||||
.setDescription("an up-to-date task")
|
||||
.setState(TaskData.State.UP_TO_DATE)
|
||||
.build();
|
||||
|
||||
private static final Joiner filenameJoiner = Joiner.on(File.separator);
|
||||
|
||||
private ImmutableMap<String, String> getGeneratedFiles(ProjectData project) {
|
||||
CoverPageGenerator coverPageGenerator = new CoverPageGenerator(project);
|
||||
FilesWithEntryPoint files = coverPageGenerator.getFilesToUpload();
|
||||
return files.files().entrySet().stream()
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
entry -> entry.getKey().toString(),
|
||||
entry -> new String(entry.getValue().get(), UTF_8)));
|
||||
}
|
||||
|
||||
private String getContentOfGeneratedFile(ProjectData project, String expectedPath) {
|
||||
ImmutableMap<String, String> files = getGeneratedFiles(project);
|
||||
assertThat(files).containsKey(expectedPath);
|
||||
return files.get(expectedPath);
|
||||
}
|
||||
|
||||
private String getCoverPage(ProjectData project) {
|
||||
return getContentOfGeneratedFile(project, "index.html");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFilesToUpload_entryPoint_isIndexHtml() {
|
||||
CoverPageGenerator coverPageGenerator = new CoverPageGenerator(EMPTY_PROJECT);
|
||||
assertThat(coverPageGenerator.getFilesToUpload().entryPoint())
|
||||
.isEqualTo(Paths.get("index.html"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFilesToUpload_containsEntryFile() {
|
||||
String content = getContentOfGeneratedFile(EMPTY_PROJECT, "index.html");
|
||||
assertThat(content)
|
||||
.contains(
|
||||
"<span class=\"project_invocation\">./gradlew :a:task1 :a:task2 -P key=value</span>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoverPage_showsFailedTask() {
|
||||
String content = getCoverPage(EMPTY_PROJECT.toBuilder().addTask(EMPTY_TASK_FAILURE).build());
|
||||
assertThat(content).contains("task-failure");
|
||||
assertThat(content).contains("<p>FAILURE</p>");
|
||||
assertThat(content).doesNotContain("<p>SUCCESS</p>");
|
||||
assertThat(content).doesNotContain("<p>UP_TO_DATE</p>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoverPage_showsSuccessfulTask() {
|
||||
String content = getCoverPage(EMPTY_PROJECT.toBuilder().addTask(EMPTY_TASK_SUCCESS).build());
|
||||
assertThat(content).contains("task-success");
|
||||
assertThat(content).doesNotContain("<p>FAILURE</p>");
|
||||
assertThat(content).contains("<p>SUCCESS</p>");
|
||||
assertThat(content).doesNotContain("<p>UP_TO_DATE</p>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoverPage_showsUpToDateTask() {
|
||||
String content = getCoverPage(EMPTY_PROJECT.toBuilder().addTask(EMPTY_TASK_UP_TO_DATE).build());
|
||||
assertThat(content).contains("task-up-to-date");
|
||||
assertThat(content).doesNotContain("<p>FAILURE</p>");
|
||||
assertThat(content).doesNotContain("<p>SUCCESS</p>");
|
||||
assertThat(content).contains("<p>UP_TO_DATE</p>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoverPage_failedAreFirst() {
|
||||
String content =
|
||||
getCoverPage(
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(EMPTY_TASK_UP_TO_DATE)
|
||||
.addTask(EMPTY_TASK_FAILURE)
|
||||
.addTask(EMPTY_TASK_SUCCESS)
|
||||
.build());
|
||||
assertThat(content).contains("<p>FAILURE</p>");
|
||||
assertThat(content).contains("<p>SUCCESS</p>");
|
||||
assertThat(content).contains("<p>UP_TO_DATE</p>");
|
||||
assertThat(content).containsMatch("(?s)<p>FAILURE</p>.*<p>SUCCESS</p>");
|
||||
assertThat(content).containsMatch("(?s)<p>FAILURE</p>.*<p>UP_TO_DATE</p>");
|
||||
assertThat(content).doesNotContainMatch("(?s)<p>SUCCESS</p>.*<p>FAILURE</p>");
|
||||
assertThat(content).doesNotContainMatch("(?s)<p>UP_TO_DATE</p>.*<p>FAILURE</p>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoverPage_failingTask_statusIsFailure() {
|
||||
String content =
|
||||
getCoverPage(
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(EMPTY_TASK_UP_TO_DATE)
|
||||
.addTask(EMPTY_TASK_FAILURE)
|
||||
.addTask(EMPTY_TASK_SUCCESS)
|
||||
.build());
|
||||
assertThat(content).contains("<title>Failed: task-failure</title>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoverPage_noFailingTask_statusIsSuccess() {
|
||||
String content =
|
||||
getCoverPage(
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(EMPTY_TASK_UP_TO_DATE)
|
||||
.addTask(EMPTY_TASK_SUCCESS)
|
||||
.build());
|
||||
assertThat(content).contains("<title>Success!</title>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFilesToUpload_containsCssFile() {
|
||||
ImmutableMap<String, String> files = getGeneratedFiles(EMPTY_PROJECT);
|
||||
assertThat(files).containsKey(filenameJoiner.join("css", "style.css"));
|
||||
assertThat(files.get(filenameJoiner.join("css", "style.css"))).contains("body {");
|
||||
assertThat(files.get("index.html"))
|
||||
.contains("<link rel=\"stylesheet\" type=\"text/css\" href=\"css/style.css\">");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_taskWithLog() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(
|
||||
EMPTY_TASK_SUCCESS.toBuilder()
|
||||
.setUniqueName("my:name")
|
||||
.setLog(toByteArraySupplier("my log data"))
|
||||
.build())
|
||||
.build());
|
||||
assertThat(files).containsEntry(filenameJoiner.join("logs", "my-name.log"), "my log data");
|
||||
assertThat(files.get("index.html")).contains("<a href=\"logs/my-name.log\">[log]</a>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_taskWithoutLog() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(EMPTY_TASK_SUCCESS.toBuilder().setUniqueName("my:name").build())
|
||||
.build());
|
||||
assertThat(files).doesNotContainKey("logs/my-name.log");
|
||||
assertThat(files.get("index.html")).contains("<span class=\"report_link_broken\">[log]</span>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_taskWithFilledReport() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(
|
||||
EMPTY_TASK_SUCCESS.toBuilder()
|
||||
.putReport(
|
||||
"someReport",
|
||||
FilesWithEntryPoint.create(
|
||||
ImmutableMap.of(
|
||||
Paths.get("path", "report.txt"),
|
||||
toByteArraySupplier("report content")),
|
||||
Paths.get("path", "report.txt")))
|
||||
.build())
|
||||
.build());
|
||||
assertThat(files).containsEntry(filenameJoiner.join("path", "report.txt"), "report content");
|
||||
assertThat(files.get("index.html")).contains("<a href=\"path/report.txt\">[someReport]</a>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_taskWithEmptyReport() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(
|
||||
EMPTY_TASK_SUCCESS.toBuilder()
|
||||
.putReport(
|
||||
"someReport",
|
||||
FilesWithEntryPoint.create(
|
||||
ImmutableMap.of(), Paths.get("path", "report.txt")))
|
||||
.build())
|
||||
.build());
|
||||
assertThat(files).doesNotContainKey(filenameJoiner.join("path", "report.txt"));
|
||||
assertThat(files.get("index.html"))
|
||||
.contains("<span class=\"report_link_broken\">[someReport]</span>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_taskWithLogAndMultipleReports() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(
|
||||
EMPTY_TASK_SUCCESS.toBuilder()
|
||||
.setUniqueName("my:name")
|
||||
.setLog(toByteArraySupplier("log data"))
|
||||
.putReport(
|
||||
"filledReport",
|
||||
FilesWithEntryPoint.create(
|
||||
ImmutableMap.of(
|
||||
Paths.get("path-filled", "report.txt"),
|
||||
toByteArraySupplier("report content"),
|
||||
Paths.get("path-filled", "other", "file.txt"),
|
||||
toByteArraySupplier("some other content")),
|
||||
Paths.get("path-filled", "report.txt")))
|
||||
.putReport(
|
||||
"emptyReport",
|
||||
FilesWithEntryPoint.create(
|
||||
ImmutableMap.of(), Paths.get("path-empty", "report.txt")))
|
||||
.build())
|
||||
.build());
|
||||
assertThat(files)
|
||||
.containsEntry(filenameJoiner.join("path-filled", "report.txt"), "report content");
|
||||
assertThat(files)
|
||||
.containsEntry(
|
||||
filenameJoiner.join("path-filled", "other", "file.txt"), "some other content");
|
||||
assertThat(files).containsEntry(filenameJoiner.join("logs", "my-name.log"), "log data");
|
||||
assertThat(files.get("index.html"))
|
||||
.contains("<a href=\"path-filled/report.txt\">[filledReport]</a>");
|
||||
assertThat(files.get("index.html")).contains("<a href=\"logs/my-name.log\">[log]</a>");
|
||||
assertThat(files.get("index.html"))
|
||||
.contains("<span class=\"report_link_broken\">[emptyReport]</span>");
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.gradle.plugin;
|
||||
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.getContentType;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.readFilesWithEntryPoint;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.toByteArraySupplier;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.toNormalizedPath;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.uploadFileToGcs;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.uploadFilesToGcsMultithread;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.nio.file.Files.createDirectories;
|
||||
import static java.nio.file.Files.createDirectory;
|
||||
import static java.nio.file.Files.createFile;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import com.google.cloud.storage.BlobInfo;
|
||||
import com.google.cloud.storage.Storage;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
/** Tests for {@link GcsPluginUtilsTest} */
|
||||
final class GcsPluginUtilsTest {
|
||||
|
||||
private static final Joiner filenameJoiner = Joiner.on(File.separator);
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@TempDir
|
||||
Path tmpDir;
|
||||
|
||||
@Test
|
||||
void testGetContentType_knownTypes() {
|
||||
assertThat(getContentType("path/to/file.html")).isEqualTo("text/html");
|
||||
assertThat(getContentType("path/to/file.htm")).isEqualTo("text/html");
|
||||
assertThat(getContentType("path/to/file.log")).isEqualTo("text/plain");
|
||||
assertThat(getContentType("path/to/file.txt")).isEqualTo("text/plain");
|
||||
assertThat(getContentType("path/to/file.css")).isEqualTo("text/css");
|
||||
assertThat(getContentType("path/to/file.xml")).isEqualTo("text/xml");
|
||||
assertThat(getContentType("path/to/file.zip")).isEqualTo("application/zip");
|
||||
assertThat(getContentType("path/to/file.js")).isEqualTo("text/javascript");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetContentType_unknownTypes() {
|
||||
assertThat(getContentType("path/to/file.unknown")).isEqualTo("application/octet-stream");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUploadFileToGcs() {
|
||||
Storage storage = mock(Storage.class);
|
||||
uploadFileToGcs(
|
||||
storage, "my-bucket", Paths.get("my", "filename.txt"), toByteArraySupplier("my data"));
|
||||
verify(storage)
|
||||
.create(
|
||||
BlobInfo.newBuilder("my-bucket", "my/filename.txt")
|
||||
.setContentType("text/plain")
|
||||
.build(),
|
||||
"my data".getBytes(UTF_8));
|
||||
verifyNoMoreInteractions(storage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUploadFilesToGcsMultithread() {
|
||||
Storage storage = mock(Storage.class);
|
||||
uploadFilesToGcsMultithread(
|
||||
storage,
|
||||
"my-bucket",
|
||||
Paths.get("my", "folder"),
|
||||
ImmutableMap.of(
|
||||
Paths.get("some", "index.html"), toByteArraySupplier("some web page"),
|
||||
Paths.get("some", "style.css"), toByteArraySupplier("some style"),
|
||||
Paths.get("other", "index.html"), toByteArraySupplier("other web page"),
|
||||
Paths.get("other", "style.css"), toByteArraySupplier("other style")));
|
||||
verify(storage)
|
||||
.create(
|
||||
BlobInfo.newBuilder("my-bucket", "my/folder/some/index.html")
|
||||
.setContentType("text/html")
|
||||
.build(),
|
||||
"some web page".getBytes(UTF_8));
|
||||
verify(storage)
|
||||
.create(
|
||||
BlobInfo.newBuilder("my-bucket", "my/folder/some/style.css")
|
||||
.setContentType("text/css")
|
||||
.build(),
|
||||
"some style".getBytes(UTF_8));
|
||||
verify(storage)
|
||||
.create(
|
||||
BlobInfo.newBuilder("my-bucket", "my/folder/other/index.html")
|
||||
.setContentType("text/html")
|
||||
.build(),
|
||||
"other web page".getBytes(UTF_8));
|
||||
verify(storage)
|
||||
.create(
|
||||
BlobInfo.newBuilder("my-bucket", "my/folder/other/style.css")
|
||||
.setContentType("text/css")
|
||||
.build(),
|
||||
"other style".getBytes(UTF_8));
|
||||
verifyNoMoreInteractions(storage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToByteArraySupplier_string() {
|
||||
assertThat(toByteArraySupplier("my string").get()).isEqualTo("my string".getBytes(UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToByteArraySupplier_stringSupplier() {
|
||||
assertThat(toByteArraySupplier(() -> "my string").get()).isEqualTo("my string".getBytes(UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToByteArraySupplier_file() throws Exception {
|
||||
Path dir = createDirectory(tmpDir.resolve("arbitrary"));
|
||||
Path file = createFile(dir.resolve("file.txt"));
|
||||
Files.write(file, "some data".getBytes(UTF_8));
|
||||
assertThat(toByteArraySupplier(file.toFile()).get()).isEqualTo("some data".getBytes(UTF_8));
|
||||
}
|
||||
|
||||
private ImmutableMap<String, String> readAllFiles(FilesWithEntryPoint reportFiles) {
|
||||
return reportFiles.files().entrySet().stream()
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
entry -> entry.getKey().toString(),
|
||||
entry -> new String(entry.getValue().get(), UTF_8)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_destinationIsFile() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path somePath = createDirectories(root.resolve("some/path"));
|
||||
Path destination = createFile(somePath.resolve("file.txt"));
|
||||
Files.write(destination, "some data".getBytes(UTF_8));
|
||||
// Since the entry point is obvious here - any hint given is just ignored.
|
||||
File ignoredHint = createFile(root.resolve("ignored.txt")).toFile();
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(ignoredHint), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "file.txt"));
|
||||
assertThat(readAllFiles(files))
|
||||
.containsExactly(filenameJoiner.join("some", "path", "file.txt"), "some data");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_destinationDoesntExist() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
File destination = root.resolve("non/existing.txt").toFile();
|
||||
assertThat(destination.isFile()).isFalse();
|
||||
assertThat(destination.isDirectory()).isFalse();
|
||||
// Since there are no files, any hint given is obviously wrong and will be ignored.
|
||||
File ignoredHint = createFile(root.resolve("ignored.txt")).toFile();
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination, Optional.of(ignoredHint), root);
|
||||
|
||||
assertThat(files.entryPoint().toString()).isEqualTo(filenameJoiner.join("non", "existing.txt"));
|
||||
assertThat(files.files()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_noFiles() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
createDirectories(destination.resolve("a/b"));
|
||||
createDirectory(destination.resolve("c"));
|
||||
// Since there are not files, any hint given is obviously wrong and will be ignored.
|
||||
File ignoredHint = createFile(root.resolve("ignored.txt")).toFile();
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(ignoredHint), root);
|
||||
|
||||
assertThat(files.entryPoint().toString()).isEqualTo(filenameJoiner.join("some", "path"));
|
||||
assertThat(files.files()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_oneFile() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
createDirectories(destination.resolve("a/b"));
|
||||
createDirectory(destination.resolve("c"));
|
||||
Files.write(createFile(destination.resolve("a/file.txt")), "some data".getBytes(UTF_8));
|
||||
// Since the entry point is obvious here - any hint given is just ignored.
|
||||
File ignoredHint = createFile(root.resolve("ignored.txt")).toFile();
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(ignoredHint), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "a", "file.txt"));
|
||||
assertThat(readAllFiles(files))
|
||||
.containsExactly(filenameJoiner.join("some", "path", "a", "file.txt"), "some data");
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently tests the "unimplemented" behavior.
|
||||
*
|
||||
* <p>TODO(guyben): switch to checking zip file instead.
|
||||
*/
|
||||
@Test
|
||||
void testCreateReportFiles_multipleFiles_noHint() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
createDirectories(destination.resolve("a/b"));
|
||||
createDirectory(destination.resolve("c"));
|
||||
|
||||
Files.write(createFile(destination.resolve("index.html")), "some data".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("a/index.html")), "wrong index".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("c/style.css")), "css file".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("my_image.png")), "images".getBytes(UTF_8));
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.empty(), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "path.zip"));
|
||||
assertThat(readAllFiles(files).keySet())
|
||||
.containsExactly(filenameJoiner.join("some", "path", "path.zip"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently tests the "unimplemented" behavior.
|
||||
*
|
||||
* <p>TODO(guyben): switch to checking zip file instead.
|
||||
*/
|
||||
@Test
|
||||
void testCreateReportFiles_multipleFiles_withBadHint() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
// This entry point points to a directory, which isn't an appropriate entry point
|
||||
File badEntryPoint = createDirectories(destination.resolve("a/b")).toFile();
|
||||
createDirectory(destination.resolve("c"));
|
||||
|
||||
Files.write(createFile(destination.resolve("index.html")), "some data".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("a/index.html")), "wrong index".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("c/style.css")), "css file".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("my_image.png")), "images".getBytes(UTF_8));
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(badEntryPoint), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "path.zip"));
|
||||
assertThat(readAllFiles(files).keySet())
|
||||
.containsExactly(filenameJoiner.join("some", "path", "path.zip"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateReportFiles_multipleFiles_withGoodHint() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
createDirectories(destination.resolve("a/b"));
|
||||
createDirectory(destination.resolve("c"));
|
||||
// The hint is an actual file nested in the destination directory!
|
||||
Path goodEntryPoint = createFile(destination.resolve("index.html"));
|
||||
|
||||
Files.write(goodEntryPoint, "some data".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("a/index.html")), "wrong index".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("c/style.css")), "css file".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("my_image.png")), "images".getBytes(UTF_8));
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(goodEntryPoint.toFile()), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "index.html"));
|
||||
assertThat(readAllFiles(files))
|
||||
.containsExactly(
|
||||
filenameJoiner.join("some", "path", "index.html"), "some data",
|
||||
filenameJoiner.join("some", "path", "a", "index.html"), "wrong index",
|
||||
filenameJoiner.join("some", "path", "c", "style.css"), "css file",
|
||||
filenameJoiner.join("some", "path", "my_image.png"), "images");
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
mock-maker-inline
|
||||
@@ -4,59 +4,68 @@
|
||||
com.diffplug.durian:durian-collect:1.2.0=classpath
|
||||
com.diffplug.durian:durian-core:1.2.0=classpath
|
||||
com.diffplug.durian:durian-io:1.2.0=classpath
|
||||
com.diffplug.durian:durian-swt.os:4.2.0=classpath
|
||||
com.diffplug.spotless:com.diffplug.spotless.gradle.plugin:6.20.0=classpath
|
||||
com.diffplug.spotless:spotless-lib-extra:2.40.0=classpath
|
||||
com.diffplug.spotless:spotless-lib:2.40.0=classpath
|
||||
com.diffplug.spotless:spotless-plugin-gradle:6.20.0=classpath
|
||||
com.diffplug.durian:durian-swt.os:4.3.0=classpath
|
||||
com.diffplug.spotless:com.diffplug.spotless.gradle.plugin:8.4.0=classpath
|
||||
com.diffplug.spotless:spotless-lib-extra:4.5.0=classpath
|
||||
com.diffplug.spotless:spotless-lib:4.5.0=classpath
|
||||
com.diffplug.spotless:spotless-plugin-gradle:8.4.0=classpath
|
||||
com.dorongold.plugins:task-tree:2.1.0=classpath
|
||||
com.dorongold.task-tree:com.dorongold.task-tree.gradle.plugin:2.1.0=classpath
|
||||
com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:8.1.1=classpath
|
||||
com.github.johnrengelman:shadow:8.1.1=classpath
|
||||
com.github.node-gradle.node:com.github.node-gradle.node.gradle.plugin:3.0.1=classpath
|
||||
com.github.node-gradle:gradle-node-plugin:3.0.1=classpath
|
||||
com.google.cloud.tools:appengine-gradle-plugin:2.4.1=classpath
|
||||
com.google.cloud.tools:appengine-plugins-core:0.9.1=classpath
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.14.2=classpath
|
||||
com.fasterxml.jackson.core:jackson-core:2.14.2=classpath
|
||||
com.fasterxml.jackson.core:jackson-databind:2.14.2=classpath
|
||||
com.fasterxml.jackson:jackson-bom:2.14.2=classpath
|
||||
com.fasterxml.woodstox:woodstox-core:7.1.1=classpath
|
||||
com.github.node-gradle.node:com.github.node-gradle.node.gradle.plugin:7.1.0=classpath
|
||||
com.github.node-gradle:gradle-node-plugin:7.1.0=classpath
|
||||
com.google.cloud.tools:appengine-gradle-plugin:2.5.0=classpath
|
||||
com.google.cloud.tools:appengine-plugins-core:0.10.0=classpath
|
||||
com.google.code.findbugs:jsr305:3.0.2=classpath
|
||||
com.google.code.gson:gson:2.8.6=classpath
|
||||
com.google.errorprone:error_prone_annotations:2.3.4=classpath
|
||||
com.google.code.gson:gson:2.10.1=classpath
|
||||
com.google.errorprone:error_prone_annotations:2.18.0=classpath
|
||||
com.google.guava:failureaccess:1.0.1=classpath
|
||||
com.google.guava:guava:28.2-jre=classpath
|
||||
com.google.guava:guava-parent:32.1.2-jre=classpath
|
||||
com.google.guava:guava:32.1.2-jre=classpath
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath
|
||||
com.google.j2objc:j2objc-annotations:1.3=classpath
|
||||
com.googlecode.concurrent-trees:concurrent-trees:2.6.1=classpath
|
||||
com.googlecode.javaewah:JavaEWAH:1.2.3=classpath
|
||||
com.squareup.okhttp3:okhttp:4.10.0=classpath
|
||||
com.squareup.okio:okio-jvm:3.0.0=classpath
|
||||
com.squareup.okio:okio:3.0.0=classpath
|
||||
commons-io:commons-io:2.11.0=classpath
|
||||
dev.equo.ide:solstice:1.3.1=classpath
|
||||
net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:3.1.0=classpath
|
||||
net.ltgt.gradle:gradle-errorprone-plugin:3.1.0=classpath
|
||||
org.apache.ant:ant-launcher:1.10.13=classpath
|
||||
org.apache.ant:ant:1.10.13=classpath
|
||||
org.apache.commons:commons-compress:1.20=classpath
|
||||
com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.4.0=classpath
|
||||
com.gradleup.shadow:shadow-gradle-plugin:9.4.0=classpath
|
||||
com.squareup.okhttp3:okhttp:4.12.0=classpath
|
||||
com.squareup.okio:okio-jvm:3.6.0=classpath
|
||||
com.squareup.okio:okio:3.6.0=classpath
|
||||
commons-codec:commons-codec:1.21.0=classpath
|
||||
commons-io:commons-io:2.21.0=classpath
|
||||
dev.equo.ide:solstice:1.8.1=classpath
|
||||
net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:5.1.0=classpath
|
||||
net.ltgt.gradle:gradle-errorprone-plugin:5.1.0=classpath
|
||||
org.apache.ant:ant-launcher:1.10.15=classpath
|
||||
org.apache.ant:ant:1.10.15=classpath
|
||||
org.apache.commons:commons-compress:1.21=classpath
|
||||
org.apache.commons:commons-lang3:3.5=classpath
|
||||
org.checkerframework:checker-qual:2.10.0=classpath
|
||||
org.codehaus.plexus:plexus-utils:3.5.1=classpath
|
||||
org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r=classpath
|
||||
org.eclipse.platform:org.eclipse.osgi:3.18.300=classpath
|
||||
org.apache.maven:maven-api-annotations:4.0.0-rc-5=classpath
|
||||
org.apache.maven:maven-api-xml:4.0.0-rc-5=classpath
|
||||
org.apache.maven:maven-xml:4.0.0-rc-5=classpath
|
||||
org.checkerframework:checker-qual:3.33.0=classpath
|
||||
org.codehaus.plexus:plexus-utils:4.0.2=classpath
|
||||
org.codehaus.plexus:plexus-xml:4.1.1=classpath
|
||||
org.codehaus.woodstox:stax2-api:4.2.2=classpath
|
||||
org.eclipse.jgit:org.eclipse.jgit:7.5.0.202512021534-r=classpath
|
||||
org.eclipse.platform:org.eclipse.osgi:3.24.100=classpath
|
||||
org.glassfish:javax.json:1.0.4=classpath
|
||||
org.jdom:jdom2:2.0.6.1=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-common:1.6.20=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib:1.6.20=classpath
|
||||
org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.20-RC3=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-common:2.3.20-RC3=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib:2.3.20-RC3=classpath
|
||||
org.jetbrains:annotations:13.0=classpath
|
||||
org.ow2.asm:asm-commons:9.4=classpath
|
||||
org.ow2.asm:asm-tree:9.4=classpath
|
||||
org.ow2.asm:asm:9.4=classpath
|
||||
org.slf4j:slf4j-api:1.7.36=classpath
|
||||
org.slf4j:slf4j-api:2.0.17=classpath
|
||||
org.sonatype.aether:aether-api:1.13.1=classpath
|
||||
org.sonatype.aether:aether-impl:1.13.1=classpath
|
||||
org.sonatype.aether:aether-spi:1.13.1=classpath
|
||||
org.sonatype.aether:aether-util:1.13.1=classpath
|
||||
org.tukaani:xz:1.9=classpath
|
||||
org.vafer:jdependency:2.8.0=classpath
|
||||
org.yaml:snakeyaml:1.21=classpath
|
||||
org.vafer:jdependency:2.15=classpath
|
||||
org.yaml:snakeyaml:2.0=classpath
|
||||
empty=
|
||||
|
||||
@@ -65,4 +65,5 @@ dependencies {
|
||||
|
||||
testImplementation deps['org.junit.jupiter:junit-jupiter-api']
|
||||
testImplementation deps['org.junit.jupiter:junit-jupiter-engine']
|
||||
testImplementation deps['org.junit.platform:junit-platform-launcher']
|
||||
}
|
||||
|
||||
@@ -1,68 +1,85 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
aopalliance:aopalliance:1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.github.ben-manes.caffeine:caffeine:3.0.5=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.github.ben-manes.caffeine:caffeine:3.1.8=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.10.4=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,checkstyle,compileClasspath,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
|
||||
com.google.errorprone:error_prone_annotation:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.26.1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.errorprone:error_prone_annotations:2.7.1=checkstyle
|
||||
com.google.errorprone:error_prone_check_api:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_type_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:javac:9+181-r4173-1=errorproneJavac
|
||||
com.google.flogger:flogger:0.8=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.guava:failureaccess:1.0.1=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.github.ben-manes.caffeine:caffeine:3.0.5=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.github.ben-manes.caffeine:caffeine:3.2.3=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.11.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.2=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.code.findbugs:jsr305:3.0.2=checkstyle,compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.errorprone:error_prone_annotation:2.48.0=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.36.0=checkstyle
|
||||
com.google.errorprone:error_prone_annotations:2.43.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.errorprone:error_prone_annotations:2.48.0=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_check_api:2.48.0=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.48.0=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.flogger:flogger:0.9=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.googlejavaformat:google-java-format:1.34.1=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.guava:failureaccess:1.0.2=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.guava:guava-parent:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.guava:guava:31.0.1-jre=checkstyle
|
||||
com.google.guava:guava:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.guava:guava:33.1.0-jre=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=checkstyle,compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.inject:guice:5.1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.j2objc:j2objc-annotations:1.3=checkstyle
|
||||
com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath,testCompileClasspath,testingCompileClasspath
|
||||
com.google.protobuf:protobuf-java:3.19.6=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.truth:truth:1.4.2=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.puppycrawl.tools:checkstyle:9.3=checkstyle
|
||||
commons-beanutils:commons-beanutils:1.9.4=checkstyle
|
||||
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.guava:guava:33.4.3-android=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.guava:guava:33.4.8-jre=checkstyle
|
||||
com.google.guava:guava:33.5.0-jre=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,compileClasspath,deploy_jar,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
|
||||
com.google.j2objc:j2objc-annotations:3.0.0=checkstyle,compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.truth:truth:1.4.5=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.puppycrawl.tools:checkstyle:10.24.0=checkstyle
|
||||
commons-beanutils:commons-beanutils:1.10.1=checkstyle
|
||||
commons-codec:commons-codec:1.15=checkstyle
|
||||
commons-collections:commons-collections:3.2.2=checkstyle
|
||||
info.picocli:picocli:4.6.2=checkstyle
|
||||
io.github.eisop:dataflow-errorprone:3.34.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,compileClasspath,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
|
||||
jakarta.inject:jakarta.inject-api:1.0.5=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
joda-time:joda-time:2.12.7=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
info.picocli:picocli:4.7.7=checkstyle
|
||||
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.16=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
javax.inject:javax.inject:1=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
joda-time:joda-time:2.14.1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
net.sf.saxon:Saxon-HE:10.6=checkstyle
|
||||
org.antlr:antlr4-runtime:4.9.3=checkstyle
|
||||
net.sf.saxon:Saxon-HE:12.5=checkstyle
|
||||
org.antlr:antlr4-runtime:4.13.2=checkstyle
|
||||
org.apache.commons:commons-lang3:3.8.1=checkstyle
|
||||
org.apache.commons:commons-text:1.3=checkstyle
|
||||
org.apache.httpcomponents.client5:httpclient5:5.1.3=checkstyle
|
||||
org.apache.httpcomponents.core5:httpcore5-h2:5.1.3=checkstyle
|
||||
org.apache.httpcomponents.core5:httpcore5:5.1.3=checkstyle
|
||||
org.apache.httpcomponents:httpclient:4.5.13=checkstyle
|
||||
org.apache.httpcomponents:httpcore:4.4.14=checkstyle
|
||||
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle
|
||||
org.apache.xbean:xbean-reflect:3.7=checkstyle
|
||||
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
|
||||
org.checkerframework:checker-compat-qual:2.5.3=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.checkerframework:checker-qual:3.12.0=checkstyle
|
||||
org.checkerframework:checker-qual:3.33.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
org.checkerframework:checker-qual:3.42.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.checkerframework:checker-qual:3.19.0=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
org.checkerframework:checker-qual:3.43.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.checkerframework:checker-qual:3.49.3=checkstyle
|
||||
org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
|
||||
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
|
||||
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
|
||||
org.codehaus.plexus:plexus-utils:3.3.0=checkstyle
|
||||
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt
|
||||
org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.core:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.report:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.agent:0.8.14=jacocoAgent,jacocoAnt
|
||||
org.jacoco:org.jacoco.ant:0.8.14=jacocoAnt
|
||||
org.jacoco:org.jacoco.core:0.8.14=jacocoAnt
|
||||
org.jacoco:org.jacoco.report:0.8.14=jacocoAnt
|
||||
org.javassist:javassist:3.28.0-GA=checkstyle
|
||||
org.junit.jupiter:junit-jupiter-api:5.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:5.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-commons:1.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-engine:1.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit:junit-bom:5.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,deploy_jar,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
|
||||
org.junit.jupiter:junit-jupiter-api:5.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:5.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-commons:1.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-engine:1.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-launcher:1.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit:junit-bom:5.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.ow2.asm:asm-commons:9.6=jacocoAnt
|
||||
org.ow2.asm:asm-tree:9.6=jacocoAnt
|
||||
org.ow2.asm:asm:9.6=compileClasspath,deploy_jar,jacocoAnt,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.pcollections:pcollections:3.1.4=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
org.ow2.asm:asm-commons:9.9=jacocoAnt
|
||||
org.ow2.asm:asm-tree:9.9=jacocoAnt
|
||||
org.ow2.asm:asm:9.8=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.ow2.asm:asm:9.9=jacocoAnt
|
||||
org.pcollections:pcollections:4.0.1=annotationProcessor,testAnnotationProcessor,testingAnnotationProcessor
|
||||
org.reflections:reflections:0.10.2=checkstyle
|
||||
empty=testingCompile,testingRuntime,testingRuntimeClasspath
|
||||
org.xmlresolver:xmlresolver:5.2.2=checkstyle
|
||||
empty=shadow,testingCompile,testingRuntime,testingRuntimeClasspath
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
package google.registry.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
@@ -30,5 +33,14 @@ import org.joda.time.DateTime;
|
||||
public interface Clock extends Serializable {
|
||||
|
||||
/** Returns current time in UTC timezone. */
|
||||
@Deprecated
|
||||
DateTime nowUtc();
|
||||
|
||||
/** Returns current Instant (which is always in UTC). */
|
||||
Instant now();
|
||||
|
||||
/** Returns the current time as a {@link ZonedDateTime} in UTC. */
|
||||
default ZonedDateTime nowDate() {
|
||||
return ZonedDateTime.ofInstant(now(), ZoneOffset.UTC);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,15 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Ordering;
|
||||
import java.sql.Date;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.format.SignStyle;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import javax.annotation.Nullable;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.joda.time.LocalDate;
|
||||
@@ -28,67 +37,201 @@ import org.joda.time.LocalDate;
|
||||
public abstract class DateTimeUtils {
|
||||
|
||||
/** The start of the epoch, in a convenient constant. */
|
||||
public static final DateTime START_OF_TIME = new DateTime(0, DateTimeZone.UTC);
|
||||
@Deprecated public static final DateTime START_OF_TIME = new DateTime(0, DateTimeZone.UTC);
|
||||
|
||||
/** The start of the UNIX epoch (which is defined in UTC), in a convenient constant. */
|
||||
public static final Instant START_INSTANT = Instant.ofEpochMilli(0);
|
||||
|
||||
/**
|
||||
* A date in the far future that we can treat as infinity.
|
||||
*
|
||||
* <p>This value is (2^63-1)/1000 rounded down. AppEngine stores dates as 64 bit microseconds, but
|
||||
* Java uses milliseconds, so this is the largest representable date that will survive a
|
||||
* <p>This value is (2^63-1)/1000 rounded down. Postgres can store dates as 64 bit microseconds,
|
||||
* but Java uses milliseconds, so this is the largest representable date that will survive a
|
||||
* round-trip through the database.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final DateTime END_OF_TIME = new DateTime(Long.MAX_VALUE / 1000, DateTimeZone.UTC);
|
||||
|
||||
/**
|
||||
* An instant in the far future that we can treat as infinity.
|
||||
*
|
||||
* <p>This value is (2^63-1)/1000 rounded down. Postgres can store dates as 64 bit microseconds,
|
||||
* but Java uses milliseconds, so this is the largest representable date that will survive a
|
||||
* round-trip through the database.
|
||||
*/
|
||||
public static final Instant END_INSTANT = Instant.ofEpochMilli(Long.MAX_VALUE / 1000);
|
||||
|
||||
/**
|
||||
* Standard ISO 8601 formatter with millisecond precision in UTC.
|
||||
*
|
||||
* <p>Example: {@code 2024-03-27T10:15:30.105Z}
|
||||
*
|
||||
* <p>Handles large/negative years by using a sign prefix if necessary, compatible with {@link
|
||||
* Instant#parse}.
|
||||
*/
|
||||
private static final DateTimeFormatter ISO_8601_FORMATTER =
|
||||
new DateTimeFormatterBuilder()
|
||||
.appendValue(ChronoField.YEAR, 4, 10, SignStyle.NORMAL)
|
||||
.appendPattern("-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
.toFormatter()
|
||||
.withZone(ZoneOffset.UTC);
|
||||
|
||||
/** Formats an {@link Instant} to an ISO-8601 string. */
|
||||
public static String formatInstant(Instant instant) {
|
||||
return ISO_8601_FORMATTER.format(instant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an ISO-8601 string to an {@link Instant}.
|
||||
*
|
||||
* <p>This method is lenient and supports both strings with and without millisecond precision
|
||||
* (e.g. {@code 2024-03-27T10:15:30Z} and {@code 2024-03-27T10:15:30.105Z}). It also supports
|
||||
* large years (e.g. {@code 294247-01-10T04:00:54.775Z}).
|
||||
*/
|
||||
public static Instant parseInstant(String timestamp) {
|
||||
try {
|
||||
// Try the standard millisecond precision format first.
|
||||
return Instant.from(ISO_8601_FORMATTER.parse(timestamp));
|
||||
} catch (DateTimeParseException e) {
|
||||
// Fall back to the standard ISO instant parser which handles varied precision.
|
||||
return Instant.parse(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the earliest of a number of given {@link DateTime} instances. */
|
||||
public static DateTime earliestOf(DateTime first, DateTime... rest) {
|
||||
return earliestDateTimeOf(Lists.asList(first, rest));
|
||||
}
|
||||
|
||||
/** Returns the earliest of a number of given {@link Instant} instances. */
|
||||
public static Instant earliestOf(Instant first, Instant... rest) {
|
||||
return earliestOf(Lists.asList(first, rest));
|
||||
}
|
||||
|
||||
/** Returns the earliest element in a {@link DateTime} iterable. */
|
||||
public static DateTime earliestOf(Iterable<DateTime> dates) {
|
||||
public static DateTime earliestDateTimeOf(Iterable<DateTime> dates) {
|
||||
checkArgument(!Iterables.isEmpty(dates));
|
||||
return Ordering.<DateTime>natural().min(dates);
|
||||
}
|
||||
|
||||
/** Returns the earliest element in a {@link Instant} iterable. */
|
||||
public static Instant earliestOf(Iterable<Instant> instants) {
|
||||
checkArgument(!Iterables.isEmpty(instants));
|
||||
return Ordering.<Instant>natural().min(instants);
|
||||
}
|
||||
|
||||
/** Returns the latest of a number of given {@link DateTime} instances. */
|
||||
public static DateTime latestOf(DateTime first, DateTime... rest) {
|
||||
return latestDateTimeOf(Lists.asList(first, rest));
|
||||
}
|
||||
|
||||
/** Returns the latest of a number of given {@link Instant} instances. */
|
||||
public static Instant latestOf(Instant first, Instant... rest) {
|
||||
return latestOf(Lists.asList(first, rest));
|
||||
}
|
||||
|
||||
/** Returns the latest element in a {@link DateTime} iterable. */
|
||||
public static DateTime latestOf(Iterable<DateTime> dates) {
|
||||
public static DateTime latestDateTimeOf(Iterable<DateTime> dates) {
|
||||
checkArgument(!Iterables.isEmpty(dates));
|
||||
return Ordering.<DateTime>natural().max(dates);
|
||||
}
|
||||
|
||||
/** Returns the latest element in a {@link Instant} iterable. */
|
||||
public static Instant latestOf(Iterable<Instant> instants) {
|
||||
checkArgument(!Iterables.isEmpty(instants));
|
||||
return Ordering.<Instant>natural().max(instants);
|
||||
}
|
||||
|
||||
/** Returns whether the first {@link DateTime} is equal to or earlier than the second. */
|
||||
public static boolean isBeforeOrAt(DateTime timeToCheck, DateTime timeToCompareTo) {
|
||||
return !timeToCheck.isAfter(timeToCompareTo);
|
||||
}
|
||||
|
||||
/** Returns whether the first {@link Instant} is equal to or earlier than the second. */
|
||||
public static boolean isBeforeOrAt(Instant timeToCheck, Instant timeToCompareTo) {
|
||||
return !timeToCheck.isAfter(timeToCompareTo);
|
||||
}
|
||||
|
||||
/** Returns whether the first {@link DateTime} is equal to or later than the second. */
|
||||
public static boolean isAtOrAfter(DateTime timeToCheck, DateTime timeToCompareTo) {
|
||||
return !timeToCheck.isBefore(timeToCompareTo);
|
||||
}
|
||||
|
||||
/** Returns whether the first {@link Instant} is equal to or later than the second. */
|
||||
public static boolean isAtOrAfter(Instant timeToCheck, Instant timeToCompareTo) {
|
||||
return !timeToCheck.isBefore(timeToCompareTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds years to a date, in the {@code Duration} sense of semantic years. Use this instead of
|
||||
* {@link DateTime#plusYears} to ensure that we never end up on February 29.
|
||||
*/
|
||||
public static DateTime leapSafeAddYears(DateTime now, int years) {
|
||||
public static DateTime plusYears(DateTime now, int years) {
|
||||
checkArgument(years >= 0);
|
||||
return years == 0 ? now : now.plusYears(1).plusYears(years - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds years to a date, in the {@code Duration} sense of semantic years. Use this instead of
|
||||
* {@link java.time.ZonedDateTime#plusYears} to ensure that we never end up on February 29.
|
||||
*/
|
||||
public static Instant plusYears(Instant now, int years) {
|
||||
checkArgument(years >= 0);
|
||||
return (years == 0)
|
||||
? now
|
||||
: now.atZone(ZoneOffset.UTC).plusYears(1).plusYears(years - 1).toInstant();
|
||||
}
|
||||
|
||||
/** Adds months to a date. */
|
||||
public static Instant plusMonths(Instant now, int months) {
|
||||
checkArgument(months >= 0);
|
||||
return now.atZone(ZoneOffset.UTC).plusMonths(months).toInstant();
|
||||
}
|
||||
|
||||
/** Subtracts months from a date. */
|
||||
public static Instant minusMonths(Instant now, int months) {
|
||||
checkArgument(months >= 0);
|
||||
return now.atZone(ZoneOffset.UTC).minusMonths(months).toInstant();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts years from a date, in the {@code Duration} sense of semantic years. Use this instead
|
||||
* of {@link DateTime#minusYears} to ensure that we never end up on February 29.
|
||||
*/
|
||||
public static DateTime leapSafeSubtractYears(DateTime now, int years) {
|
||||
public static DateTime minusYears(DateTime now, int years) {
|
||||
checkArgument(years >= 0);
|
||||
return years == 0 ? now : now.minusYears(1).minusYears(years - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts years from a date, in the {@code Duration} sense of semantic years. Use this instead
|
||||
* of {@link java.time.ZonedDateTime#minusYears} to ensure that we never end up on February 29.
|
||||
*/
|
||||
public static Instant minusYears(Instant now, long years) {
|
||||
checkArgument(years >= 0);
|
||||
return (years == 0)
|
||||
? now
|
||||
: now.atZone(ZoneOffset.UTC).minusYears(1).minusYears(years - 1).toInstant();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #plusYears(DateTime, int)}
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("InlineMeSuggester")
|
||||
public static DateTime leapSafeAddYears(DateTime now, int years) {
|
||||
return plusYears(now, years);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #minusYears(DateTime, int)}
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("InlineMeSuggester")
|
||||
public static DateTime leapSafeSubtractYears(DateTime now, int years) {
|
||||
return minusYears(now, years);
|
||||
}
|
||||
|
||||
public static Date toSqlDate(LocalDate localDate) {
|
||||
return new Date(localDate.toDateTimeAtStartOfDay().getMillis());
|
||||
}
|
||||
@@ -96,4 +239,46 @@ public abstract class DateTimeUtils {
|
||||
public static LocalDate toLocalDate(Date date) {
|
||||
return new LocalDate(date.getTime(), DateTimeZone.UTC);
|
||||
}
|
||||
|
||||
/** Convert a joda {@link DateTime} to a java.time {@link Instant}, null-safe. */
|
||||
@Nullable
|
||||
public static Instant toInstant(@Nullable DateTime dateTime) {
|
||||
return (dateTime == null) ? null : Instant.ofEpochMilli(dateTime.getMillis());
|
||||
}
|
||||
|
||||
/** Convert a java.time {@link Instant} to a joda {@link DateTime}, null-safe. */
|
||||
@Nullable
|
||||
public static DateTime toDateTime(@Nullable Instant instant) {
|
||||
return (instant == null) ? null : new DateTime(instant.toEpochMilli(), DateTimeZone.UTC);
|
||||
}
|
||||
|
||||
/** Convert a java.time {@link java.time.Instant} to a joda {@link org.joda.time.Instant}. */
|
||||
@Nullable
|
||||
public static org.joda.time.Instant toJodaInstant(@Nullable java.time.Instant instant) {
|
||||
return (instant == null) ? null : org.joda.time.Instant.ofEpochMilli(instant.toEpochMilli());
|
||||
}
|
||||
|
||||
public static Instant plusHours(Instant instant, long hours) {
|
||||
return instant.plus(hours, ChronoUnit.HOURS);
|
||||
}
|
||||
|
||||
public static Instant minusHours(Instant instant, long hours) {
|
||||
return instant.minus(hours, ChronoUnit.HOURS);
|
||||
}
|
||||
|
||||
public static Instant plusMinutes(Instant instant, long minutes) {
|
||||
return instant.plus(minutes, ChronoUnit.MINUTES);
|
||||
}
|
||||
|
||||
public static Instant minusMinutes(Instant instant, long minutes) {
|
||||
return instant.minus(minutes, ChronoUnit.MINUTES);
|
||||
}
|
||||
|
||||
public static Instant plusDays(Instant instant, int days) {
|
||||
return instant.atZone(ZoneOffset.UTC).plusDays(days).toInstant();
|
||||
}
|
||||
|
||||
public static Instant minusDays(Instant instant, int days) {
|
||||
return instant.atZone(ZoneOffset.UTC).minusDays(days).toInstant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ package google.registry.util;
|
||||
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Clock implementation that proxies to the real system clock. */
|
||||
@@ -34,4 +36,13 @@ public class SystemClock implements Clock {
|
||||
public DateTime nowUtc() {
|
||||
return DateTime.now(UTC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant now() {
|
||||
// Truncate to milliseconds to match the precision of Joda DateTime and our database schema
|
||||
// (which uses millisecond precision via DateTimeConverter). This prevents subtle comparison
|
||||
// bugs where a high-precision Instant would be considered "after" a truncated database
|
||||
// timestamp.
|
||||
return Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ package google.registry.util;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.common.util.concurrent.Uninterruptibles;
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.ReadableDuration;
|
||||
|
||||
/** Implementation of {@link Sleeper} for production use. */
|
||||
@@ -40,6 +41,6 @@ public final class SystemSleeper implements Sleeper, Serializable {
|
||||
@Override
|
||||
public void sleepUninterruptibly(ReadableDuration duration) {
|
||||
checkArgument(duration.getMillis() >= 0);
|
||||
Uninterruptibles.sleepUninterruptibly(java.time.Duration.ofMillis(duration.getMillis()));
|
||||
Uninterruptibles.sleepUninterruptibly(Duration.ofMillis(duration.getMillis()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.joda.time.Duration.millis;
|
||||
|
||||
import google.registry.util.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import org.joda.time.DateTime;
|
||||
@@ -48,12 +49,22 @@ public final class FakeClock implements Clock {
|
||||
setTo(startTime);
|
||||
}
|
||||
|
||||
/** Creates a FakeClock initialized to a specific time. */
|
||||
public FakeClock(Instant startTime) {
|
||||
setTo(startTime);
|
||||
}
|
||||
|
||||
/** Returns the current time. */
|
||||
@Override
|
||||
public DateTime nowUtc() {
|
||||
return new DateTime(currentTimeMillis.addAndGet(autoIncrementStepMs), UTC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant now() {
|
||||
return Instant.ofEpochMilli(currentTimeMillis.addAndGet(autoIncrementStepMs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the increment applied to the clock whenever it is queried. The increment is zero by
|
||||
* default: the clock is left unchanged when queried.
|
||||
@@ -83,6 +94,11 @@ public final class FakeClock implements Clock {
|
||||
currentTimeMillis.set(time.getMillis());
|
||||
}
|
||||
|
||||
/** Sets the time to the specified instant. */
|
||||
public void setTo(Instant time) {
|
||||
currentTimeMillis.set(time.toEpochMilli());
|
||||
}
|
||||
|
||||
/** Invokes {@link #setAutoIncrementStep} with one millisecond-step. */
|
||||
public FakeClock setAutoIncrementByOneMilli() {
|
||||
return setAutoIncrementStep(Duration.millis(1));
|
||||
|
||||
@@ -33,6 +33,8 @@ import com.google.common.truth.Fact;
|
||||
import com.google.common.truth.FailureMetadata;
|
||||
import com.google.common.truth.SimpleSubjectBuilder;
|
||||
import com.google.common.truth.Subject;
|
||||
import com.google.errorprone.annotations.FormatMethod;
|
||||
import com.google.errorprone.annotations.FormatString;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
@@ -63,6 +65,7 @@ public class TextDiffSubject extends Subject {
|
||||
|
||||
private final ImmutableList<String> actual;
|
||||
private DiffFormat diffFormat = DiffFormat.SIDE_BY_SIDE_MARKDOWN;
|
||||
private ImmutableList<String> comments = ImmutableList.of();
|
||||
|
||||
protected TextDiffSubject(FailureMetadata metadata, List<String> actual) {
|
||||
super(metadata, actual);
|
||||
@@ -83,13 +86,27 @@ public class TextDiffSubject extends Subject {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** If set, ignore lines that start with the given string. */
|
||||
public TextDiffSubject ignoringLinesStartingWith(String... comments) {
|
||||
this.comments = ImmutableList.copyOf(comments);
|
||||
return this;
|
||||
}
|
||||
|
||||
private ImmutableList<String> filterComments(List<String> lines) {
|
||||
return lines.stream()
|
||||
.filter(line -> !line.isBlank())
|
||||
.filter(line -> comments.stream().noneMatch(line::startsWith))
|
||||
.collect(ImmutableList.toImmutableList());
|
||||
}
|
||||
|
||||
public void hasSameContentAs(List<String> expectedContent) {
|
||||
checkNotNull(expectedContent, "expectedContent");
|
||||
ImmutableList<String> expected = ImmutableList.copyOf(expectedContent);
|
||||
if (expected.equals(actual)) {
|
||||
ImmutableList<String> filteredExpected = filterComments(expectedContent);
|
||||
ImmutableList<String> filteredActual = filterComments(actual);
|
||||
if (filteredExpected.equals(filteredActual)) {
|
||||
return;
|
||||
}
|
||||
String diffString = diffFormat.generateDiff(expected, actual);
|
||||
String diffString = diffFormat.generateDiff(filteredExpected, filteredActual);
|
||||
failWithoutActual(
|
||||
Fact.simpleFact(
|
||||
Joiner.on('\n')
|
||||
@@ -110,8 +127,9 @@ public class TextDiffSubject extends Subject {
|
||||
return assertThat(Resources.asCharSource(resourceUrl, UTF_8).readLines());
|
||||
}
|
||||
|
||||
@FormatMethod
|
||||
public static SimpleSubjectBuilder<TextDiffSubject, URL> assertWithMessageAboutUrlSource(
|
||||
String format, Object... params) {
|
||||
@FormatString String format, Object... params) {
|
||||
return assertWithMessage(format, params).about(urlFactory());
|
||||
}
|
||||
|
||||
@@ -175,16 +193,9 @@ public class TextDiffSubject extends Subject {
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
private static class SideBySideRowFormatter {
|
||||
private final int maxExpectedLineLength;
|
||||
private final int maxActualLineLength;
|
||||
private record SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
|
||||
|
||||
private SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
|
||||
this.maxExpectedLineLength = maxExpectedLineLength;
|
||||
this.maxActualLineLength = maxActualLineLength;
|
||||
}
|
||||
|
||||
public String formatRow(String expected, String actual, char padChar) {
|
||||
String formatRow(String expected, String actual, char padChar) {
|
||||
return String.format(
|
||||
"|%s|%s|",
|
||||
Strings.padEnd(expected, maxExpectedLineLength, padChar),
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
{
|
||||
"moduleLicense": "Apache License V2.0"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Apache License Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Apache License, Version 2.0"
|
||||
},
|
||||
@@ -102,6 +105,12 @@
|
||||
{
|
||||
"moduleLicense": "BSD-2-Clause"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "BSD 3-Clause \"New\" or \"Revised\" License (BSD-3-Clause)"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "BSD licence"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "New BSD License"
|
||||
},
|
||||
@@ -141,6 +150,9 @@
|
||||
{
|
||||
"moduleLicense": "\\n Dual license consisting of the CDDL v1.1 and GPL v2\\n "
|
||||
},
|
||||
{
|
||||
"moduleLicense": "EPL-2.0"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Eclipse Distribution License (New BSD License)"
|
||||
},
|
||||
@@ -192,6 +204,9 @@
|
||||
{
|
||||
"moduleLicense": "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "(GPL-2.0-only WITH Classpath-exception-2.0)"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "GNU Library General Public License v2.1 or later"
|
||||
},
|
||||
@@ -229,6 +244,12 @@
|
||||
{
|
||||
"moduleLicense": "The JSON License"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "LGPL-2.1-or-later"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Apache License version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "LGPL-2.1+"
|
||||
},
|
||||
@@ -262,6 +283,12 @@
|
||||
{
|
||||
"moduleLicense": "Unicode/ICU License"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Unicode-3.0"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "The W3C Software License"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Public Domain",
|
||||
"moduleName": "aopalliance:aopalliance"
|
||||
@@ -274,12 +301,6 @@
|
||||
"moduleLicense": "Public Domain",
|
||||
"moduleName": "org.json:json"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "2.10.0",
|
||||
"moduleName": "com.google.gwt:gwt-user"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0". The plugin is able to parse up to
|
||||
// 2.11.3 correctly but then something changed with 2.12.* and it no
|
||||
@@ -287,6 +308,16 @@
|
||||
"moduleLicense": null,
|
||||
"moduleName": "com.fasterxml.jackson:jackson-bom"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleName": "tools.jackson:jackson-bom"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleName": "com.google.cloud:libraries-bom"
|
||||
},
|
||||
{
|
||||
// Part of Guava with "Apache License, Version 2.0". The plugin is unable
|
||||
// to parse its license for unknown reason.
|
||||
@@ -294,38 +325,27 @@
|
||||
"moduleName": "com.google.guava:guava-parent"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0". The plugin is able to parse up to
|
||||
// 2.0.33.Final but not this verson.
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "2.0.46.Final",
|
||||
"moduleName": "io.netty:netty-tcnative-classes"
|
||||
},
|
||||
{
|
||||
// Actually Eclipse Public License v2.0
|
||||
"moduleLicense": null,
|
||||
"moduleName": "org.junit:junit-bom"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "The W3C Software License"
|
||||
"moduleVersion": "2.10.0",
|
||||
"moduleName": "com.google.gwt:gwt-user"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleName": "com.squareup.okhttp3:okhttp"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.15.1",
|
||||
"moduleName": "com.squareup:kotlinpoet"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleName": "com.squareup.okio:okio"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "(GPL-2.0-only WITH Classpath-exception-2.0)",
|
||||
"moduleName": "io.github.eisop:dataflow-errorprone"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "GNU General Public License, version 2 (GPL2), with the classpath exception",
|
||||
"moduleName": "io.github.eisop:dataflow-errorprone"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
@@ -351,15 +371,27 @@
|
||||
"moduleName": "com.squareup.wire:wire-schema"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
// "Apache License, Version 2.0". The plugin is able to parse up to
|
||||
// 2.0.33.Final but not this verson.
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.15.1",
|
||||
"moduleName": "com.squareup:kotlinpoet"
|
||||
"moduleVersion": "2.0.46.Final",
|
||||
"moduleName": "io.netty:netty-tcnative-classes"
|
||||
},
|
||||
// "Apache License, Version 2.0".
|
||||
{
|
||||
"moduleLicense": null,
|
||||
"moduleName": "io.opentelemetry:opentelemetry-bom"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Apache License Version 2.0",
|
||||
"moduleVersion": "3.0.0.M2",
|
||||
"moduleName": "io.apicurio:apicurio-registry-protobuf-schema-utilities"
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.4",
|
||||
"moduleName": "jakarta-regexp:jakarta-regexp"
|
||||
},
|
||||
{
|
||||
// Actually Eclipse Public License v2.0
|
||||
"moduleLicense": null,
|
||||
"moduleName": "org.junit:junit-bom"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
@@ -390,12 +422,6 @@
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.0.1",
|
||||
"moduleName": "org.jetbrains.kotlinx:kotlinx-serialization-core"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.4",
|
||||
"moduleName": "jakarta-regexp:jakarta-regexp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ PROPERTIES_HEADER = """\
|
||||
# nom_build), run ./nom_build --help.
|
||||
#
|
||||
# DO NOT EDIT THIS FILE BY HAND
|
||||
org.gradle.jvmargs=-Xmx1024m
|
||||
org.gradle.jvmargs=-Xmx4096m
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
"""
|
||||
@@ -89,13 +89,6 @@ PROPERTIES = [
|
||||
'Allow connecting to plain HTTP repositories. This is provided '
|
||||
'to allow us to communicate to a local proxy when doing '
|
||||
'dependency updates.'),
|
||||
Property('uploaderDestination',
|
||||
'Location to upload test reports to. Normally this should be a '
|
||||
'GCS url (see also uploaderCredentialsFile)'),
|
||||
Property('uploaderCredentialsFile',
|
||||
'json credentials file to use to upload test reports.'),
|
||||
Property('uploaderMultithreadedUpload',
|
||||
'Whether to enable multithread upload.'),
|
||||
Property('verboseTestOutput',
|
||||
'If true, show all test output in near-realtime.',
|
||||
'false',
|
||||
@@ -111,7 +104,7 @@ PROPERTIES = [
|
||||
Property('testFilter',
|
||||
'Comma separated list of test patterns, if specified run only '
|
||||
'these.'),
|
||||
Property('environment', 'GAE Environment for deployment and staging.'),
|
||||
Property('environment', 'Environment for deployment and staging.'),
|
||||
|
||||
# Cloud SQL properties
|
||||
Property('dbServer',
|
||||
@@ -124,28 +117,19 @@ PROPERTIES = [
|
||||
Property('dbUser', 'Database user name for use in connection'),
|
||||
Property('dbPassword', 'Database password for use in connection'),
|
||||
|
||||
Property('publish_repo',
|
||||
'Maven repository that hosts the Cloud SQL schema jar and the '
|
||||
'registry server test jars. Such jars are needed for '
|
||||
'server/schema integration tests. Please refer to <a '
|
||||
'href="./integration/README.md">integration project</a> for more '
|
||||
'information.'),
|
||||
Property('baseSchemaTag',
|
||||
'The nomulus version tag of the schema for use in the schema'
|
||||
'deployment integration test (:db:schemaIncrementalDeployTest)'),
|
||||
Property('schema_version',
|
||||
'The nomulus version tag of the schema for use in a database'
|
||||
'integration test.'),
|
||||
Property('nomulus_version',
|
||||
'The version of nomulus to test against in a database '
|
||||
'integration test.'),
|
||||
Property('dot_path',
|
||||
'The path to "dot", part of the graphviz package that converts '
|
||||
'a BEAM pipeline to image. Setting this property to empty string '
|
||||
'will disable image generation.',
|
||||
'/usr/bin/dot'),
|
||||
Property('pipeline',
|
||||
'The name of the Beam pipeline being staged.')
|
||||
'The name of the Beam pipeline being staged.'),
|
||||
Property('nomulus_env',
|
||||
'For use by scripts. Normally not set manually.'),
|
||||
Property('schema_env',
|
||||
'For use by scripts. Normally not set manually.'),
|
||||
Property('schemaTestArtifactsDir',
|
||||
'For use by scripts. Normally not set manually.')
|
||||
]
|
||||
|
||||
GRADLE_FLAGS = [
|
||||
@@ -277,8 +261,7 @@ def generate_gradle_properties() -> str:
|
||||
def get_root() -> str:
|
||||
"""Returns the root of the nomulus build tree."""
|
||||
cur_dir = os.getcwd()
|
||||
if not os.path.exists(os.path.join(cur_dir, 'buildSrc')) or \
|
||||
not os.path.exists(os.path.join(cur_dir, 'core')) or \
|
||||
if not os.path.exists(os.path.join(cur_dir, 'core')) or \
|
||||
not os.path.exists(os.path.join(cur_dir, 'gradle.properties')):
|
||||
raise Exception('You must run this script from the root directory')
|
||||
return cur_dir
|
||||
|
||||
@@ -25,7 +25,11 @@ import textwrap
|
||||
import re
|
||||
|
||||
# We should never analyze any generated files
|
||||
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/", ".gradle/", "/dist/", "karma.conf.js", "polyfills.ts", "test.ts", "/docs/console-endpoints/"}
|
||||
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/",
|
||||
".gradle/", "/dist/", "/console-alpha/", "/console-crash/", "/console-qa",
|
||||
"/console-sandbox", "/console-production", "karma.conf.js", "polyfills.ts",
|
||||
"test.ts", "/docs/console-endpoints/", "/bin/generated-sources/",
|
||||
"/bin/generated-test-sources/", "src/main/generated", "src/test/generated"}
|
||||
# We can't rely on CI to have the Enum package installed so we do this instead.
|
||||
FORBIDDEN = 1
|
||||
REQUIRED = 2
|
||||
@@ -87,25 +91,22 @@ PRESUBMITS = {
|
||||
PresubmitCheck(
|
||||
r".*Copyright 20\d{2} The Nomulus Authors\. All Rights Reserved\.",
|
||||
("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"), {
|
||||
".git", "/build/", "/bin/generated-sources/", "/bin/generated-test-sources/",
|
||||
"node_modules/", "LoggerConfig.java", "registrar_bin.",
|
||||
".git", "/build/", "node_modules/", "LoggerConfig.java", "registrar_bin.",
|
||||
"registrar_dbg.", "google-java-format-diff.py",
|
||||
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js",
|
||||
"/src/main/generated", "/src/test/generated"
|
||||
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js"
|
||||
}, REQUIRED):
|
||||
"File did not include the license header.",
|
||||
|
||||
# Files must end in a newline
|
||||
PresubmitCheck(r".*\n$", ("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"),
|
||||
{"node_modules/"}, REQUIRED):
|
||||
PresubmitCheck(r".*\n$", ("java", "js", "soy", "sql", "py", "sh", "gradle", "ts", "xml"),
|
||||
{"node_modules/", ".idea"}, REQUIRED):
|
||||
"Source files must end in a newline.",
|
||||
|
||||
# System.(out|err).println should only appear in tools/
|
||||
# System.(out|err).println should only appear in tools/ or load-testing/
|
||||
PresubmitCheck(
|
||||
r".*\bSystem\.(out|err)\.print", "java", {
|
||||
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
|
||||
"RegistryTestServerMain.java", "TestServerExtension.java",
|
||||
"FlowDocumentationTool.java"
|
||||
r".*\bSystem\s*\.\s*(?:out|err)\s*\.\s*print.*", "java", {
|
||||
"/tools/", "/example/", "/load-testing/",
|
||||
"RegistryTestServerMain.java", "TestServerExtension.java"
|
||||
}):
|
||||
"System.(out|err).println is only allowed in tools/ packages. Please "
|
||||
"use a logger instead.",
|
||||
@@ -118,7 +119,7 @@ PRESUBMITS = {
|
||||
):
|
||||
"In SOY please use the ({@param name: string} /** User name. */) style"
|
||||
" parameter passing instead of the ( * @param name User name.) style "
|
||||
"parameter pasing.",
|
||||
"parameter passing.",
|
||||
PresubmitCheck(
|
||||
r'.*\{[^}]+\w+:\s+"',
|
||||
"soy",
|
||||
@@ -137,47 +138,12 @@ PRESUBMITS = {
|
||||
{},
|
||||
):
|
||||
"All soy templates must use strict autoescaping",
|
||||
|
||||
# various JS linting checks
|
||||
PresubmitCheck(
|
||||
r".*goog\.base\(",
|
||||
"js",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Use of goog.base is not allowed.",
|
||||
PresubmitCheck(
|
||||
r".*goog\.dom\.classes",
|
||||
"js",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Instead of goog.dom.classes, use goog.dom.classlist which is smaller "
|
||||
"and faster.",
|
||||
PresubmitCheck(
|
||||
r".*goog\.getMsg",
|
||||
"js",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Put messages in Soy, instead of using goog.getMsg().",
|
||||
PresubmitCheck(
|
||||
r".*(innerHTML|outerHTML)\s*(=|[+]=)([^=]|$)",
|
||||
"js",
|
||||
{"/node_modules/", "registrar_bin."},
|
||||
):
|
||||
"Do not assign directly to the dom. Use goog.dom.setTextContent to set"
|
||||
" to plain text, goog.dom.removeChildren to clear, or "
|
||||
"soy.renderElement to render anything else",
|
||||
PresubmitCheck(
|
||||
r".*console\.(log|info|warn|error)",
|
||||
"js",
|
||||
{"/node_modules/", "google/registry/ui/js/util.js", "registrar_bin."},
|
||||
):
|
||||
"JavaScript files should not include console logging.",
|
||||
PresubmitCheck(
|
||||
r".*org\.testcontainers\.shaded.*",
|
||||
r".*\nimport\s+(?:static\s+)?.*\.shaded\..*",
|
||||
"java",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Do not use shaded dependencies from testcontainers.",
|
||||
"Do not use shaded dependencies",
|
||||
PresubmitCheck(
|
||||
r".*com\.google\.common\.truth\.Truth8.*",
|
||||
"java",
|
||||
@@ -187,10 +153,150 @@ PRESUBMITS = {
|
||||
PresubmitCheck(
|
||||
r".*java\.util\.Date.*",
|
||||
"java",
|
||||
{"/node_modules/", "JpaTransactionManagerImpl.java"},
|
||||
{"/node_modules/", "JpaTransactionManagerImpl.java", "DateTimeUtils.java"},
|
||||
):
|
||||
"Do not use java.util.Date. Use classes in java.time package instead.",
|
||||
|
||||
PresubmitCheck(
|
||||
r".*com\.google\.api\.client\.http\.HttpStatusCodes.*",
|
||||
"java",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Use status code from jakarta.servlet.http.HttpServletResponse.",
|
||||
PresubmitCheck(
|
||||
r".*mock\(\s*Response\.class\s*\).*",
|
||||
"java",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Do not mock Response, use FakeResponse.",
|
||||
PresubmitCheck(
|
||||
r".*javax\.servlet\..*",
|
||||
"java",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Do not use javax.servlet.* Use jakarta.servlet.* instead.",
|
||||
PresubmitCheck(
|
||||
r".*javax\.inject\..*",
|
||||
"java",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Do not use javax.inject.* Use jakarta.inject.* instead.",
|
||||
PresubmitCheck(
|
||||
r".*import\s+jakarta\.persistence\.(?:Pre|Post)(?:Persist|Load|Remove|Update)\s*;",
|
||||
"java",
|
||||
{"EntityCallbacksListener.java"},
|
||||
):
|
||||
"Hibernate lifecycle events aren't called for embedded entities, so it's "
|
||||
"usually best to avoid them. Instead, use the annotations defined in "
|
||||
"EntityCallbacksListener.java",
|
||||
PresubmitCheck(
|
||||
r".*\.isEqualTo\(\s*Optional\.of\(.*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not use .isEqualTo(Optional.of(...)). Use Truth's .hasValue(...) instead.",
|
||||
# TODO: Remove the java.time migration presubmit checks below once the entire codebase has been migrated to java.time.
|
||||
PresubmitCheck(
|
||||
r".*toDateTime\(\s*toInstant\(.*",
|
||||
"java",
|
||||
{"DateTimeUtilsTest.java"},
|
||||
):
|
||||
"Do not double-wrap toDateTime(toInstant(...)).",
|
||||
PresubmitCheck(
|
||||
r".*toInstant\(\s*toDateTime\(.*",
|
||||
"java",
|
||||
{"DateTimeUtilsTest.java"},
|
||||
):
|
||||
"Do not double-wrap toInstant(toDateTime(...)).",
|
||||
PresubmitCheck(
|
||||
r".*toInstant\([^;]*[cC]lock\.nowUtc\(\).*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not use toInstant(clock.nowUtc()). Use clock.now() instead.",
|
||||
PresubmitCheck(
|
||||
r".*toDateTime\([^;]*[cC]lock\.now\(\).*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not use toDateTime(clock.now()). Use clock.nowUtc() instead.",
|
||||
PresubmitCheck(
|
||||
r".*toInstant\([^;]*tm\(\)\.getTransactionTime\(\).*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not use toInstant(tm().getTransactionTime()). Use tm().getTxTime() instead.",
|
||||
PresubmitCheck(
|
||||
r".*toDateTime\([^;]*tm\(\)\.getTxTime\(\).*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not use toDateTime(tm().getTxTime()). Use tm().getTransactionTime() instead.",
|
||||
PresubmitCheck(
|
||||
r".*\(\s*Instant\s*\)\s*(?:this\.)?(?:fakeClock|clock)\.now\(\s*\).*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not unnecessarily cast clock.now() to Instant.",
|
||||
PresubmitCheck(
|
||||
r".*toDateTime\(\s*Instant\.now\(.*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not wrap Instant.now() in toDateTime. Use DateTime.now(UTC) directly.",
|
||||
PresubmitCheck(
|
||||
r".*toInstant\(\s*DateTime\.now\(.*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not wrap DateTime.now() in toInstant. Use Instant.now().truncatedTo(ChronoUnit.MILLIS) directly.",
|
||||
PresubmitCheck(
|
||||
r".*toDateTime\(\s*Instant\.parse\(.*",
|
||||
"java",
|
||||
{"DateTimeUtilsTest.java"},
|
||||
):
|
||||
"Do not wrap Instant.parse in toDateTime. Use DateTime.parse directly.",
|
||||
PresubmitCheck(
|
||||
r".*toInstant\(\s*DateTime\.parse\(.*",
|
||||
"java",
|
||||
{"DateTimeUtilsTest.java"},
|
||||
):
|
||||
"Do not wrap DateTime.parse in toInstant. Use Instant.parse directly.",
|
||||
PresubmitCheck(
|
||||
r".*cloneProjectedAtTime\(\s*toDateTime\(.*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not use cloneProjectedAtTime(toDateTime(...)). Use cloneProjectedAtInstant(...) instead.",
|
||||
PresubmitCheck(
|
||||
r".*ZoneId\.of\(\s*\"UTC\"\s*\).*",
|
||||
"java",
|
||||
{},
|
||||
):
|
||||
"Do not use ZoneId.of(\"UTC\"). Use java.time.ZoneOffset.UTC.",
|
||||
PresubmitCheck(
|
||||
r".*toDateTime\(\s*END_INSTANT\s*\).*",
|
||||
"java",
|
||||
{"DateTimeUtilsTest.java"},
|
||||
):
|
||||
"Do not wrap END_INSTANT in toDateTime. Use END_OF_TIME.",
|
||||
PresubmitCheck(
|
||||
r".*toInstant\(\s*END_OF_TIME\s*\).*",
|
||||
"java",
|
||||
{"DateTimeUtilsTest.java"},
|
||||
):
|
||||
"Do not wrap END_OF_TIME in toInstant. Use END_INSTANT.",
|
||||
PresubmitCheck(
|
||||
r".*toDateTime\(\s*START_INSTANT\s*\).*",
|
||||
"java",
|
||||
{"DateTimeUtilsTest.java"},
|
||||
):
|
||||
"Do not wrap START_INSTANT in toDateTime. Use START_OF_TIME.",
|
||||
PresubmitCheck(
|
||||
r".*toInstant\(\s*START_OF_TIME\s*\).*",
|
||||
"java",
|
||||
{"DateTimeUtilsTest.java"},
|
||||
):
|
||||
"Do not wrap START_OF_TIME in toInstant. Use START_INSTANT."
|
||||
}
|
||||
|
||||
# Note that this regex only works for one kind of Flyway file. If we want to
|
||||
@@ -278,26 +384,6 @@ def verify_flyway_index():
|
||||
return not success
|
||||
|
||||
|
||||
def verify_javascript_deps():
|
||||
"""Verifies that we haven't introduced any new javascript dependencies."""
|
||||
with open('package.json') as f:
|
||||
package = json.load(f)
|
||||
|
||||
deps = list(package['dependencies'].keys())
|
||||
if deps != EXPECTED_JS_PACKAGES:
|
||||
print('Unexpected javascript dependencies. Was expecting '
|
||||
'%s, got %s.' % (EXPECTED_JS_PACKAGES, deps))
|
||||
print(textwrap.dedent("""
|
||||
* If the new dependencies are intentional, please verify that the
|
||||
* license is one of the allowed licenses (see
|
||||
* config/dependency-license/allowed_licenses.json) and add an entry
|
||||
* for the package (with the license in a comment) to the
|
||||
* EXPECTED_JS_PACKAGES variable in config/presubmits.py.
|
||||
"""))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_files():
|
||||
for root, dirnames, filenames in os.walk("."):
|
||||
for filename in filenames:
|
||||
@@ -305,7 +391,6 @@ def get_files():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print('python version is %s' % sys.version)
|
||||
failed = False
|
||||
for file in get_files():
|
||||
error_messages = []
|
||||
@@ -322,8 +407,5 @@ if __name__ == "__main__":
|
||||
# when we put it here it fails fast before all of the tests are run.
|
||||
failed |= verify_flyway_index()
|
||||
|
||||
# Make sure we haven't introduced any javascript dependencies.
|
||||
failed |= verify_javascript_deps()
|
||||
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
|
||||
5
console-webapp/.gitignore
vendored
5
console-webapp/.gitignore
vendored
@@ -36,7 +36,12 @@ yarn-error.log
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
.nx/
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build artifact
|
||||
/staged/dist
|
||||
/staged/console-*
|
||||
|
||||
@@ -9,11 +9,11 @@ expected to change.
|
||||
|
||||
## Deployment
|
||||
|
||||
Webapp is deployed with the nomulus default service war to Google App Engine.
|
||||
The webapp is deployed with the nomulus default service war to GKE.
|
||||
During nomulus default service war build task, gradle script triggers the
|
||||
following:
|
||||
|
||||
1) Console webapp build script `buildConsoleWebappProd`, which installs
|
||||
1) Console webapp build script `buildConsoleWebapp`, which installs
|
||||
dependencies, assembles a compiled ts -> js, minified, optimized static
|
||||
artifact (html, css, js)
|
||||
2) Artifact assembled in step 1 then gets copied to core project web artifact
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist/console-webapp",
|
||||
"outputPath": {
|
||||
"base": "staged/dist/",
|
||||
"browser": ""
|
||||
},
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
@@ -34,7 +38,8 @@
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": ["node_modules/"]
|
||||
},
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"browser": "src/main.ts"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -58,40 +63,91 @@
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"sandbox": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.sandbox.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"crash": {
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"alpha": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"qa": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true,
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "console-webapp:build:production"
|
||||
"buildTarget": "console-webapp:build:production"
|
||||
},
|
||||
"alpha": {
|
||||
"buildTarget": "console-webapp:build:alpha"
|
||||
},
|
||||
"crash": {
|
||||
"buildTarget": "console-webapp:build:crash"
|
||||
},
|
||||
"sandbox": {
|
||||
"buildTarget": "console-webapp:build:sandbox"
|
||||
},
|
||||
"qa": {
|
||||
"buildTarget": "console-webapp:build:qa"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "console-webapp:build:development"
|
||||
"buildTarget": "console-webapp:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"builder": "@angular/build:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "console-webapp:build"
|
||||
"buildTarget": "console-webapp:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
@@ -122,9 +178,38 @@
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"cache": {
|
||||
"enabled": false
|
||||
},
|
||||
"analytics": false,
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
]
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,12 +11,12 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
def consoleDir = "${rootDir}/console-webapp"
|
||||
def projectParam = "--project=${rootProject.gcpProject}"
|
||||
|
||||
clean {
|
||||
delete "${consoleDir}/node_modules"
|
||||
delete "${consoleDir}/dist"
|
||||
delete "${consoleDir}/staged/dist"
|
||||
}
|
||||
|
||||
task npmInstallDeps(type: Exec) {
|
||||
@@ -37,19 +37,58 @@ task runConsoleWebappUnitTests(type: Exec) {
|
||||
args 'run', 'test'
|
||||
}
|
||||
|
||||
task buildConsoleWebappNonProd(type: Exec) {
|
||||
task buildConsoleWebapp(type: Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npm'
|
||||
args 'run', 'build'
|
||||
def configuration = project.getProperty('configuration')
|
||||
args 'run', "build", "--configuration=${configuration}"
|
||||
doFirst {
|
||||
println "Building console for environment: ${configuration}"
|
||||
}
|
||||
}
|
||||
|
||||
// Keeping the same as non prod for now before we figure out optimization we want to include
|
||||
task buildConsoleWebappProd(type: Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npm'
|
||||
args 'run', 'build'
|
||||
task buildConsoleForAll() {}
|
||||
|
||||
def createConsoleTask = { env ->
|
||||
project.tasks.register("buildConsoleFor${env.capitalize()}", Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npm'
|
||||
args 'run', 'build', "--configuration=${env}"
|
||||
doFirst {
|
||||
println "Building console for environment: ${env}"
|
||||
}
|
||||
doLast {
|
||||
copy {
|
||||
from "${consoleDir}/staged/dist/"
|
||||
into "${consoleDir}/staged/console-${env}"
|
||||
}
|
||||
delete "${consoleDir}/staged/dist"
|
||||
}
|
||||
dependsOn(tasks.npmInstallDeps)
|
||||
}
|
||||
project.tasks.register("deleteConsoleFor${env.capitalize()}", Delete) {
|
||||
delete "${consoleDir}/staged/console-${env}"
|
||||
}
|
||||
tasks.named('clean') {
|
||||
dependsOn(tasks.named("deleteConsoleFor${env.capitalize()}"))
|
||||
}
|
||||
tasks.named('buildConsoleForAll') {
|
||||
dependsOn(tasks.named("buildConsoleFor${env.capitalize()}"))
|
||||
}
|
||||
}
|
||||
|
||||
['alpha', 'crash', 'qa', 'sandbox', 'production'].forEach {env ->
|
||||
createConsoleTask(env)
|
||||
}
|
||||
|
||||
// Force an order so we don't run these tasks in parallel.
|
||||
tasks.buildConsoleForCrash.mustRunAfter(tasks.buildConsoleForAlpha)
|
||||
tasks.buildConsoleForQa.mustRunAfter(tasks.buildConsoleForCrash)
|
||||
tasks.buildConsoleForSandbox.mustRunAfter(tasks.buildConsoleForQa)
|
||||
tasks.buildConsoleForProduction.mustRunAfter(tasks.buildConsoleForSandbox)
|
||||
// This task must run last, otherwise the previous tasks will have deleted the "dist" folder.
|
||||
tasks.buildConsoleWebapp.mustRunAfter(tasks.buildConsoleForProduction)
|
||||
|
||||
task applyFormatting(type: Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npm'
|
||||
@@ -62,8 +101,9 @@ task checkFormatting(type: Exec) {
|
||||
args 'run', 'prettify:check'
|
||||
}
|
||||
|
||||
tasks.buildConsoleWebapp.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.buildConsoleWebappProd.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.build.dependsOn(tasks.checkFormatting)
|
||||
tasks.build.dependsOn(tasks.runConsoleWebappUnitTests)
|
||||
|
||||
@@ -1,47 +1,63 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
aopalliance:aopalliance:1.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.github.ben-manes.caffeine:caffeine:3.0.5=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.1=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotation:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.7.1=checkstyle
|
||||
com.google.errorprone:error_prone_check_api:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_type_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.errorprone:javac:9+181-r4173-1=errorproneJavac
|
||||
com.google.guava:failureaccess:1.0.1=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor
|
||||
com.google.guava:guava-parent:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.guava:guava:31.0.1-jre=checkstyle
|
||||
com.google.guava:guava:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=checkstyle
|
||||
com.google.inject:guice:5.1.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.j2objc:j2objc-annotations:1.3=checkstyle
|
||||
com.google.protobuf:protobuf-java:3.19.6=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.puppycrawl.tools:checkstyle:9.3=checkstyle
|
||||
commons-beanutils:commons-beanutils:1.9.4=checkstyle
|
||||
com.github.ben-manes.caffeine:caffeine:3.0.5=annotationProcessor,testAnnotationProcessor
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,testAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,testAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.2=annotationProcessor,testAnnotationProcessor
|
||||
com.google.code.findbugs:jsr305:3.0.2=checkstyle
|
||||
com.google.errorprone:error_prone_annotation:2.48.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.36.0=checkstyle
|
||||
com.google.errorprone:error_prone_annotations:2.48.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_check_api:2.48.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.48.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.googlejavaformat:google-java-format:1.34.1=annotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.guava:guava:33.4.8-jre=checkstyle
|
||||
com.google.guava:guava:33.5.0-jre=annotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.j2objc:j2objc-annotations:3.0.0=checkstyle
|
||||
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,testAnnotationProcessor
|
||||
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,testAnnotationProcessor
|
||||
com.puppycrawl.tools:checkstyle:10.24.0=checkstyle
|
||||
commons-beanutils:commons-beanutils:1.10.1=checkstyle
|
||||
commons-codec:commons-codec:1.15=checkstyle
|
||||
commons-collections:commons-collections:3.2.2=checkstyle
|
||||
info.picocli:picocli:4.6.2=checkstyle
|
||||
io.github.eisop:dataflow-errorprone:3.34.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
net.sf.saxon:Saxon-HE:10.6=checkstyle
|
||||
org.antlr:antlr4-runtime:4.9.3=checkstyle
|
||||
org.checkerframework:checker-qual:3.12.0=checkstyle
|
||||
org.checkerframework:checker-qual:3.33.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt
|
||||
org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.core:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.report:0.8.11=jacocoAnt
|
||||
info.picocli:picocli:4.7.7=checkstyle
|
||||
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,testAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,testAnnotationProcessor
|
||||
javax.inject:javax.inject:1=annotationProcessor,testAnnotationProcessor
|
||||
net.sf.saxon:Saxon-HE:12.5=checkstyle
|
||||
org.antlr:antlr4-runtime:4.13.2=checkstyle
|
||||
org.apache.commons:commons-lang3:3.8.1=checkstyle
|
||||
org.apache.commons:commons-text:1.3=checkstyle
|
||||
org.apache.httpcomponents.client5:httpclient5:5.1.3=checkstyle
|
||||
org.apache.httpcomponents.core5:httpcore5-h2:5.1.3=checkstyle
|
||||
org.apache.httpcomponents.core5:httpcore5:5.1.3=checkstyle
|
||||
org.apache.httpcomponents:httpclient:4.5.13=checkstyle
|
||||
org.apache.httpcomponents:httpcore:4.4.14=checkstyle
|
||||
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle
|
||||
org.apache.xbean:xbean-reflect:3.7=checkstyle
|
||||
org.checkerframework:checker-qual:3.19.0=annotationProcessor,testAnnotationProcessor
|
||||
org.checkerframework:checker-qual:3.49.3=checkstyle
|
||||
org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
|
||||
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
|
||||
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
|
||||
org.codehaus.plexus:plexus-utils:3.3.0=checkstyle
|
||||
org.jacoco:org.jacoco.agent:0.8.14=jacocoAgent,jacocoAnt
|
||||
org.jacoco:org.jacoco.ant:0.8.14=jacocoAnt
|
||||
org.jacoco:org.jacoco.core:0.8.14=jacocoAnt
|
||||
org.jacoco:org.jacoco.report:0.8.14=jacocoAnt
|
||||
org.javassist:javassist:3.28.0-GA=checkstyle
|
||||
org.ow2.asm:asm-commons:9.6=jacocoAnt
|
||||
org.ow2.asm:asm-tree:9.6=jacocoAnt
|
||||
org.ow2.asm:asm:9.6=jacocoAnt
|
||||
org.pcollections:pcollections:3.1.4=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
org.ow2.asm:asm-commons:9.9=jacocoAnt
|
||||
org.ow2.asm:asm-tree:9.9=jacocoAnt
|
||||
org.ow2.asm:asm:9.9=jacocoAnt
|
||||
org.pcollections:pcollections:4.0.1=annotationProcessor,testAnnotationProcessor
|
||||
org.reflections:reflections:0.10.2=checkstyle
|
||||
empty=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.xmlresolver:xmlresolver:5.2.2=checkstyle
|
||||
empty=compileClasspath,deploy_jar,runtimeClasspath,shadow,testCompileClasspath,testRuntimeClasspath
|
||||
|
||||
1
console-webapp/gradle.properties
Normal file
1
console-webapp/gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
configuration=production
|
||||
@@ -3,6 +3,17 @@
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
customLaunchers: {
|
||||
ChromeHeadless: {
|
||||
base: 'Chrome',
|
||||
flags: [
|
||||
'--no-sandbox',
|
||||
'--disable-gpu',
|
||||
'--headless',
|
||||
'--remote-debugging-port=9222'
|
||||
]
|
||||
}
|
||||
},
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
@@ -10,7 +21,7 @@ module.exports = function (config) {
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
|
||||
17405
console-webapp/package-lock.json
generated
17405
console-webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,9 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config dev-proxy.config.json",
|
||||
"build": "ng build --base-href=/console/",
|
||||
"build": "ng build --base-href=/console/ --configuration=$npm_config_configuration",
|
||||
"build:local": "ng build --base-href=/default/console/",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"watch": "ng build --watch --configuration=development",
|
||||
"test": "ng test --browsers=ChromeHeadless --watch=false",
|
||||
"run:dev": "",
|
||||
"prettify": "npx prettier --write ./src/",
|
||||
@@ -16,35 +16,35 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.7",
|
||||
"@angular/cdk": "^17.0.4",
|
||||
"@angular/common": "^17.0.7",
|
||||
"@angular/compiler": "^17.0.7",
|
||||
"@angular/core": "^17.0.7",
|
||||
"@angular/forms": "^17.0.7",
|
||||
"@angular/material": "^17.0.4",
|
||||
"@angular/platform-browser": "^17.0.7",
|
||||
"@angular/platform-browser-dynamic": "^17.0.7",
|
||||
"@angular/router": "^17.0.7",
|
||||
"@angular/animations": "^21.1.5",
|
||||
"@angular/cdk": "^21.1.5",
|
||||
"@angular/common": "^21.1.5",
|
||||
"@angular/compiler": "^21.1.5",
|
||||
"@angular/core": "^21.1.5",
|
||||
"@angular/forms": "^21.1.5",
|
||||
"@angular/material": "^21.1.5",
|
||||
"@angular/platform-browser": "^21.1.5",
|
||||
"@angular/platform-browser-dynamic": "^21.1.5",
|
||||
"@angular/router": "^21.1.5",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.2"
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.0.7",
|
||||
"@angular-eslint/builder": "17.1.1",
|
||||
"@angular-eslint/eslint-plugin": "17.1.1",
|
||||
"@angular-eslint/eslint-plugin-template": "17.1.1",
|
||||
"@angular-eslint/schematics": "17.1.1",
|
||||
"@angular-eslint/template-parser": "17.1.1",
|
||||
"@angular/cli": "~17.0.7",
|
||||
"@angular/compiler-cli": "^17.0.7",
|
||||
"@angular-eslint/builder": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "19.0.2",
|
||||
"@angular-eslint/schematics": "19.0.2",
|
||||
"@angular-eslint/template-parser": "19.0.2",
|
||||
"@angular/build": "^21.1.4",
|
||||
"@angular/cli": "~21.1.4",
|
||||
"@angular/compiler-cli": "^21.1.5",
|
||||
"@types/jasmine": "~4.0.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
"@types/node": "^18.19.74",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint": "^8.57.0",
|
||||
"jasmine-core": "~4.3.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
@@ -52,6 +52,6 @@
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"prettier": "2.8.7",
|
||||
"typescript": "~5.2.2"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -13,69 +13,139 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { TldsComponent } from './tlds/tlds.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import SettingsContactComponent from './settings/contact/contact.component';
|
||||
import SettingsWhoisComponent from './settings/whois/whois.component';
|
||||
import SettingsUsersComponent from './settings/users/users.component';
|
||||
import SettingsSecurityComponent from './settings/security/security.component';
|
||||
import { RegistrarGuard } from './registrar/registrar.guard';
|
||||
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
||||
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
|
||||
import ContactComponent from './settings/contact/contact.component';
|
||||
import WhoisComponent from './settings/whois/whois.component';
|
||||
import SecurityComponent from './settings/security/security.component';
|
||||
import UsersComponent from './settings/users/users.component';
|
||||
import { Route, RouterModule } from '@angular/router';
|
||||
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
|
||||
import { DomainListComponent } from './domains/domainList.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
|
||||
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
||||
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
||||
import { ResourcesComponent } from './resources/resources.component';
|
||||
import ContactComponent from './settings/contact/contact.component';
|
||||
import SecurityComponent from './settings/security/security.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { SupportComponent } from './support/support.component';
|
||||
import RdapComponent from './settings/rdap/rdap.component';
|
||||
import { HistoryComponent } from './history/history.component';
|
||||
import { PasswordResetVerifyComponent } from './shared/components/passwordReset/passwordResetVerify.component';
|
||||
|
||||
const routes: Routes = [
|
||||
export interface RouteWithIcon extends Route {
|
||||
iconName?: string;
|
||||
}
|
||||
|
||||
export const PATHS = {
|
||||
NewOteComponent: 'new-ote',
|
||||
OteStatusComponent: 'ote-status/:registrarId',
|
||||
UsersComponent: 'users',
|
||||
};
|
||||
export const routes: RouteWithIcon[] = [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
path: PasswordResetVerifyComponent.PATH,
|
||||
component: PasswordResetVerifyComponent,
|
||||
},
|
||||
{
|
||||
path: RegistryLockVerifyComponent.PATH,
|
||||
component: RegistryLockVerifyComponent,
|
||||
},
|
||||
{
|
||||
path: PATHS.NewOteComponent,
|
||||
loadComponent: () =>
|
||||
import('./ote/newOte.component').then((mod) => mod.NewOteComponent),
|
||||
},
|
||||
{
|
||||
path: PATHS.OteStatusComponent,
|
||||
loadComponent: () =>
|
||||
import('./ote/oteStatus.component').then((mod) => mod.OteStatusComponent),
|
||||
},
|
||||
{ path: 'registrars', component: RegistrarComponent },
|
||||
{ path: 'empty-registrar', component: EmptyRegistrar },
|
||||
{ path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
|
||||
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
|
||||
{
|
||||
path: 'home',
|
||||
component: HomeComponent,
|
||||
title: 'Dashboard',
|
||||
iconName: 'view_comfy_alt',
|
||||
},
|
||||
{
|
||||
path: DomainListComponent.PATH,
|
||||
component: DomainListComponent,
|
||||
canActivate: [RegistrarGuard],
|
||||
title: 'Domains',
|
||||
iconName: 'view_list',
|
||||
},
|
||||
{
|
||||
path: HistoryComponent.PATH,
|
||||
component: HistoryComponent,
|
||||
// title: 'History',
|
||||
// iconName: 'history',
|
||||
},
|
||||
{
|
||||
path: SettingsComponent.PATH,
|
||||
component: SettingsComponent,
|
||||
title: 'Settings',
|
||||
iconName: 'settings',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'registrars',
|
||||
redirectTo: ContactComponent.PATH,
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: ContactComponent.PATH,
|
||||
component: SettingsContactComponent,
|
||||
canActivate: [RegistrarGuard],
|
||||
component: ContactComponent,
|
||||
title: 'Contacts',
|
||||
},
|
||||
{
|
||||
path: WhoisComponent.PATH,
|
||||
component: SettingsWhoisComponent,
|
||||
canActivate: [RegistrarGuard],
|
||||
path: RdapComponent.PATH,
|
||||
component: RdapComponent,
|
||||
title: 'RDAP Info',
|
||||
},
|
||||
{
|
||||
path: SecurityComponent.PATH,
|
||||
component: SettingsSecurityComponent,
|
||||
canActivate: [RegistrarGuard],
|
||||
},
|
||||
{
|
||||
path: UsersComponent.PATH,
|
||||
component: SettingsUsersComponent,
|
||||
canActivate: [RegistrarGuard],
|
||||
},
|
||||
{
|
||||
path: RegistrarComponent.PATH,
|
||||
component: RegistrarComponent,
|
||||
component: SecurityComponent,
|
||||
title: 'Security',
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// path: EppConsole.PATH,
|
||||
// component: EppConsoleComponent,
|
||||
// title: "EPP Console",
|
||||
// iconName: "upgrade"
|
||||
// },
|
||||
{
|
||||
path: RegistrarComponent.PATH,
|
||||
component: RegistrarComponent,
|
||||
title: 'Registrars',
|
||||
iconName: 'account_circle',
|
||||
},
|
||||
{
|
||||
path: RegistrarDetailsComponent.PATH,
|
||||
component: RegistrarDetailsComponent,
|
||||
},
|
||||
{
|
||||
path: BillingInfoComponent.PATH,
|
||||
component: BillingInfoComponent,
|
||||
title: 'Billing Info',
|
||||
iconName: 'credit_card',
|
||||
},
|
||||
{
|
||||
path: ResourcesComponent.PATH,
|
||||
component: ResourcesComponent,
|
||||
title: 'Resources',
|
||||
iconName: 'description',
|
||||
},
|
||||
{
|
||||
path: PATHS.UsersComponent,
|
||||
title: 'Users',
|
||||
iconName: 'manage_accounts',
|
||||
loadComponent: () =>
|
||||
import('./users/users.component').then((mod) => mod.UsersComponent),
|
||||
},
|
||||
{
|
||||
path: SupportComponent.PATH,
|
||||
component: SupportComponent,
|
||||
title: 'Support',
|
||||
iconName: 'help',
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
<div class="console-app">
|
||||
<app-header (toggleNavOpen)="sidenav.toggle()"></app-header>
|
||||
<div class="console-app mat-typography">
|
||||
<app-header (toggleNavOpen)="toggleSidenav()"></app-header>
|
||||
<div class="console-app__global-spinner">
|
||||
@if (globalLoader.isLoading) {
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
}
|
||||
</div>
|
||||
<mat-sidenav-container class="console-app__container">
|
||||
<mat-sidenav #sidenav class="console-app__sidebar">
|
||||
<mat-nav-list>
|
||||
<a mat-list-item [routerLink]="'/home'" routerLinkActive="active">
|
||||
Home page
|
||||
</a>
|
||||
<a mat-list-item [routerLink]="'/tlds'" routerLinkActive="active">
|
||||
TLDS
|
||||
</a>
|
||||
<a mat-list-item [routerLink]="'/settings'" routerLinkActive="active">
|
||||
Settings
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content class="console-app__content-wrapper">
|
||||
<div *ngIf="globalLoader.isLoading" class="console-app__global-spinner">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<div class="console-app__content" *ngIf="renderRouter">
|
||||
<div class="console-app__content" role="main">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
<mat-sidenav
|
||||
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
|
||||
[opened]="!breakpointObserver.isMobileView()"
|
||||
[disableClose]="!breakpointObserver.isMobileView()"
|
||||
#sidenav
|
||||
class="console-app__sidebar"
|
||||
>
|
||||
<app-navigation />
|
||||
</mat-sidenav>
|
||||
</mat-sidenav-container>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
:host {
|
||||
font-family: Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol" !important;
|
||||
font-family: "Google Sans", Roboto, Helvetica, Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
@@ -26,28 +26,21 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
&__container {
|
||||
flex: 1;
|
||||
margin-top: -12px;
|
||||
padding-bottom: 36px;
|
||||
background: transparent;
|
||||
}
|
||||
&__sidebar {
|
||||
min-width: 300px;
|
||||
a::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
.active {
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
}
|
||||
&__content-wrapper {
|
||||
margin: 12px 24px;
|
||||
min-width: 240px;
|
||||
border: 0;
|
||||
}
|
||||
&__content {
|
||||
max-width: 1340px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
&__global-spinner {
|
||||
margin-bottom: 2rem;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,31 +12,126 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from './material.module';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { BackendService } from './shared/services/backend.service';
|
||||
import { routes } from './app-routing.module';
|
||||
import { AppModule } from './app.module';
|
||||
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { UserData, UserDataService } from './shared/services/userData.service';
|
||||
import { Registrar, RegistrarService } from './registrar/registrar.service';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { signal, WritableSignal } from '@angular/core';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
let mockRegistrarService: {
|
||||
registrar: WritableSignal<Partial<Registrar> | null | undefined>;
|
||||
registrarId: WritableSignal<string>;
|
||||
registrars: WritableSignal<Array<Partial<Registrar>>>;
|
||||
};
|
||||
let mockUserDataService: { userData: WritableSignal<Partial<UserData>> };
|
||||
let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
|
||||
|
||||
const dummyPocReminderComponent = class {}; // Dummy class for type checking
|
||||
|
||||
beforeEach(async () => {
|
||||
mockRegistrarService = {
|
||||
registrar: signal<Registrar | null | undefined>(undefined),
|
||||
registrarId: signal('123'),
|
||||
registrars: signal([]),
|
||||
};
|
||||
|
||||
mockUserDataService = {
|
||||
userData: signal({
|
||||
globalRole: 'NONE',
|
||||
}),
|
||||
};
|
||||
|
||||
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
MatSidenavModule,
|
||||
NoopAnimationsModule,
|
||||
MatSnackBarModule,
|
||||
AppModule,
|
||||
RouterModule.forRoot(routes),
|
||||
],
|
||||
providers: [
|
||||
{ provide: RegistrarService, useValue: mockRegistrarService },
|
||||
{ provide: UserDataService, useValue: mockUserDataService },
|
||||
{ provide: MatSnackBar, useValue: mockSnackBar },
|
||||
{ provide: PocReminderComponent, useClass: dummyPocReminderComponent },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
providers: [BackendService],
|
||||
declarations: [AppComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('PoC Verification Reminder', () => {
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
});
|
||||
|
||||
it('should open snackbar if lastPocVerificationDate is older than one year', fakeAsync(() => {
|
||||
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
|
||||
jasmine.clock().mockDate(MOCK_TODAY);
|
||||
|
||||
const twoYearsAgo = new Date(MOCK_TODAY);
|
||||
twoYearsAgo.setFullYear(MOCK_TODAY.getFullYear() - 2);
|
||||
|
||||
mockRegistrarService.registrar.set({
|
||||
lastPocVerificationDate: twoYearsAgo.toISOString(),
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(mockSnackBar.openFromComponent).toHaveBeenCalledWith(
|
||||
PocReminderComponent,
|
||||
{
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'top',
|
||||
duration: 1000000000,
|
||||
}
|
||||
);
|
||||
}));
|
||||
|
||||
it('should NOT open snackbar if lastPocVerificationDate is within last year', fakeAsync(() => {
|
||||
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
|
||||
jasmine.clock().mockDate(MOCK_TODAY);
|
||||
|
||||
const sixMonthsAgo = new Date(MOCK_TODAY);
|
||||
sixMonthsAgo.setMonth(MOCK_TODAY.getMonth() - 6);
|
||||
|
||||
mockRegistrarService.registrar.set({
|
||||
lastPocVerificationDate: sixMonthsAgo.toISOString(),
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(mockSnackBar.openFromComponent).not.toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,36 +12,50 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { AfterViewInit, Component, ViewChild, effect } from '@angular/core';
|
||||
import { RegistrarService } from './registrar/registrar.service';
|
||||
import { UserDataService } from './shared/services/userData.service';
|
||||
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { AfterViewInit, Component, effect, ViewChild } from '@angular/core';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { RegistrarService } from './registrar/registrar.service';
|
||||
import { BreakPointObserverService } from './shared/services/breakPoint.service';
|
||||
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
||||
import { UserDataService } from './shared/services/userData.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements AfterViewInit {
|
||||
renderRouter: boolean = true;
|
||||
|
||||
@ViewChild('sidenav')
|
||||
@ViewChild(MatSidenav)
|
||||
sidenav!: MatSidenav;
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
protected userDataService: UserDataService,
|
||||
protected globalLoader: GlobalLoaderService,
|
||||
protected router: Router
|
||||
protected breakpointObserver: BreakPointObserverService,
|
||||
private router: Router,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
effect(() => {
|
||||
if (registrarService.registrarId()) {
|
||||
this.renderRouter = false;
|
||||
setTimeout(() => {
|
||||
this.renderRouter = true;
|
||||
}, 400);
|
||||
const registrar = this.registrarService.registrar();
|
||||
const oneYearAgo = new Date();
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||
oneYearAgo.setHours(0, 0, 0, 0);
|
||||
if (
|
||||
registrar &&
|
||||
registrar.lastPocVerificationDate &&
|
||||
new Date(registrar.lastPocVerificationDate) < oneYearAgo &&
|
||||
this.userDataService?.userData()?.globalRole === 'NONE'
|
||||
) {
|
||||
this._snackBar.openFromComponent(PocReminderComponent, {
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'top',
|
||||
duration: 1000000000,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -49,8 +63,18 @@ export class AppComponent implements AfterViewInit {
|
||||
ngAfterViewInit() {
|
||||
this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.sidenav.close();
|
||||
if (this.breakpointObserver.isMobileView()) {
|
||||
this.sidenav.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleSidenav() {
|
||||
if (this.sidenav.opened) {
|
||||
this.sidenav.close();
|
||||
} else {
|
||||
this.sidenav.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -13,85 +13,116 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from './material.module';
|
||||
|
||||
import { BackendService } from './shared/services/backend.service';
|
||||
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { TldsComponent } from './tlds/tlds.component';
|
||||
import { HeaderComponent } from './header/header.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import SettingsContactComponent, {
|
||||
ContactDetailsDialogComponent,
|
||||
} from './settings/contact/contact.component';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
||||
import { RegistrarGuard } from './registrar/registrar.guard';
|
||||
import SecurityComponent from './settings/security/security.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
|
||||
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
|
||||
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
||||
import { ContactWidgetComponent } from './home/widgets/contactWidget.component';
|
||||
import { PromotionsWidgetComponent } from './home/widgets/promotionsWidget.component';
|
||||
import { TldsWidgetComponent } from './home/widgets/tldsWidget.component';
|
||||
import { ResourcesWidgetComponent } from './home/widgets/resourcesWidget.component';
|
||||
import { EppWidgetComponent } from './home/widgets/eppWidget.component';
|
||||
import { BillingWidgetComponent } from './home/widgets/billingWidget.component';
|
||||
import { DomainsWidgetComponent } from './home/widgets/domainsWidget.component';
|
||||
import { SettingsWidgetComponent } from './home/widgets/settingsWidget.component';
|
||||
import { UserDataService } from './shared/services/userData.service';
|
||||
import WhoisComponent from './settings/whois/whois.component';
|
||||
import { SnackBarModule } from './snackbar.module';
|
||||
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
|
||||
import {
|
||||
DomainListComponent,
|
||||
ReasonDialogComponent,
|
||||
ResponseDialogComponent,
|
||||
} from './domains/domainList.component';
|
||||
import { RegistryLockComponent } from './domains/registryLock.component';
|
||||
import { HeaderComponent } from './header/header.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
|
||||
import { NavigationComponent } from './navigation/navigation.component';
|
||||
import NewRegistrarComponent from './registrar/newRegistrar.component';
|
||||
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
||||
import { DomainListComponent } from './domains/domainList.component';
|
||||
import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.component';
|
||||
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
|
||||
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
||||
import { ResourcesComponent } from './resources/resources.component';
|
||||
import SettingsContactComponent from './settings/contact/contact.component';
|
||||
import { ContactDetailsComponent } from './settings/contact/contactDetails.component';
|
||||
import EppPasswordEditComponent from './settings/security/eppPasswordEdit.component';
|
||||
import SecurityComponent from './settings/security/security.component';
|
||||
import SecurityEditComponent from './settings/security/securityEdit.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { NotificationsComponent } from './shared/components/notifications/notifications.component';
|
||||
import { SelectedRegistrarWrapper } from './shared/components/selectedRegistrarWrapper/selectedRegistrarWrapper.component';
|
||||
import { LocationBackDirective } from './shared/directives/locationBack.directive';
|
||||
import { UserLevelVisibility } from './shared/directives/userLevelVisiblity.directive';
|
||||
import { BreakPointObserverService } from './shared/services/breakPoint.service';
|
||||
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
||||
import { UserDataService } from './shared/services/userData.service';
|
||||
import { SnackBarModule } from './snackbar.module';
|
||||
import { SupportComponent } from './support/support.component';
|
||||
import { ForceFocusDirective } from './shared/directives/forceFocus.directive';
|
||||
import RdapComponent from './settings/rdap/rdap.component';
|
||||
import RdapEditComponent from './settings/rdap/rdapEdit.component';
|
||||
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
|
||||
import { PasswordResetVerifyComponent } from './shared/components/passwordReset/passwordResetVerify.component';
|
||||
import { PasswordInputForm } from './shared/components/passwordReset/passwordInputForm.component';
|
||||
import { HistoryComponent } from './history/history.component';
|
||||
import { HistoryListComponent } from './history/historyList.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SelectedRegistrarWrapper],
|
||||
imports: [MaterialModule],
|
||||
exports: [SelectedRegistrarWrapper],
|
||||
providers: [],
|
||||
})
|
||||
export class SelectedRegistrarModule {}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
DialogBottomSheetWrapper,
|
||||
BillingWidgetComponent,
|
||||
ContactDetailsDialogComponent,
|
||||
ContactWidgetComponent,
|
||||
BillingInfoComponent,
|
||||
ContactDetailsComponent,
|
||||
DomainListComponent,
|
||||
DomainsWidgetComponent,
|
||||
EmptyRegistrar,
|
||||
EppWidgetComponent,
|
||||
EppPasswordEditComponent,
|
||||
ForceFocusDirective,
|
||||
HeaderComponent,
|
||||
HistoryComponent,
|
||||
HistoryListComponent,
|
||||
HomeComponent,
|
||||
PromotionsWidgetComponent,
|
||||
LocationBackDirective,
|
||||
NavigationComponent,
|
||||
NewRegistrarComponent,
|
||||
NotificationsComponent,
|
||||
PasswordInputForm,
|
||||
PasswordResetVerifyComponent,
|
||||
PocReminderComponent,
|
||||
RdapComponent,
|
||||
RdapEditComponent,
|
||||
ReasonDialogComponent,
|
||||
RegistrarComponent,
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarSelectorComponent,
|
||||
ResourcesWidgetComponent,
|
||||
RegistryLockComponent,
|
||||
RegistryLockVerifyComponent,
|
||||
ResourcesComponent,
|
||||
ResponseDialogComponent,
|
||||
SecurityComponent,
|
||||
SecurityEditComponent,
|
||||
SettingsComponent,
|
||||
SettingsContactComponent,
|
||||
SettingsWidgetComponent,
|
||||
TldsComponent,
|
||||
TldsWidgetComponent,
|
||||
WhoisComponent,
|
||||
SupportComponent,
|
||||
UserLevelVisibility,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
MaterialModule,
|
||||
SelectedRegistrarModule,
|
||||
SnackBarModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
BreakPointObserverService,
|
||||
GlobalLoaderService,
|
||||
RegistrarGuard,
|
||||
UserDataService,
|
||||
{
|
||||
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||
@@ -99,7 +130,7 @@ import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.
|
||||
subscriptSizing: 'dynamic',
|
||||
},
|
||||
},
|
||||
provideHttpClient(),
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<app-selected-registrar-wrapper>
|
||||
<h1 class="mat-headline-4" forceFocus>Billing Info</h1>
|
||||
<div class="console-app__billing">
|
||||
<div>
|
||||
<div class="console-app__billing-subhead">
|
||||
Billing records and information
|
||||
</div>
|
||||
<a
|
||||
class="text-l"
|
||||
href="{{ driveFolderUrl() }}"
|
||||
target="_blank"
|
||||
aria-label="View billing records on Google Drive"
|
||||
>View on Google Drive</a
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<img src="./assets/billing.png" alt="Generic billing image" />
|
||||
</div>
|
||||
</div>
|
||||
</app-selected-registrar-wrapper>
|
||||
@@ -0,0 +1,22 @@
|
||||
.console-app__billing {
|
||||
display: flex;
|
||||
flex-wrap: wrap-reverse;
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
margin-top: -40px;
|
||||
}
|
||||
img {
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 100%;
|
||||
}
|
||||
&-subhead {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
48
console-webapp/src/app/billingInfo/billingInfo.component.ts
Normal file
48
console-webapp/src/app/billingInfo/billingInfo.component.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, computed } from '@angular/core';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-billingInfo',
|
||||
templateUrl: './billingInfo.component.html',
|
||||
styleUrls: ['./billingInfo.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class BillingInfoComponent {
|
||||
public static PATH = 'billingInfo';
|
||||
constructor(
|
||||
public registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
driveFolderUrl = computed<string>(() => {
|
||||
if (this.registrarService.registrar()?.driveFolderId) {
|
||||
return (
|
||||
'https://drive.google.com/drive/folders/' +
|
||||
this.registrarService.registrar()?.driveFolderId
|
||||
);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
openBillingDetails(e: MouseEvent) {
|
||||
if (!this.registrarService.registrar()?.driveFolderId) {
|
||||
e.preventDefault();
|
||||
this._snackBar.open('Billing Folder ID has not been assigned');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,231 @@
|
||||
<div class="console-domains">
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<input
|
||||
type="search"
|
||||
matInput
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="sendInput()"
|
||||
#input
|
||||
/>
|
||||
</mat-form-field>
|
||||
<app-selected-registrar-wrapper>
|
||||
<div class="console-app-domains">
|
||||
<h1 class="mat-headline-4" forceFocus>Domains</h1>
|
||||
|
||||
<div *ngIf="isLoading; else domains_content" class="console-domains__loading">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
<div
|
||||
class="console-app-domains__actions-wrapper"
|
||||
[hidden]="!domainListService.activeActionComponent"
|
||||
>
|
||||
<ng-container
|
||||
v-if="domainListService.activeActionComponent"
|
||||
*ngComponentOutlet="domainListService.activeActionComponent"
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@if (!isLoading && totalResults == 0) {
|
||||
<div class="console-app__empty-domains">
|
||||
<h1>
|
||||
<mat-icon class="console-app__empty-domains-icon secondary-text"
|
||||
>apps_outage</mat-icon
|
||||
>
|
||||
</h1>
|
||||
<h1>No domains found</h1>
|
||||
</div>
|
||||
} @else {
|
||||
<mat-menu #actions="matMenu">
|
||||
<ng-template
|
||||
matMenuContent
|
||||
let-domainName="domainName"
|
||||
let-domain="domain"
|
||||
>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="openRegistryLock(domainName)"
|
||||
aria-label="Access registry lock for domain"
|
||||
>
|
||||
<mat-icon>key</mat-icon>
|
||||
<span>Registry Lock</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onSuspendClick(domainName)"
|
||||
[elementId]="getElementIdForSuspendUnsuspend()"
|
||||
[disabled]="isDomainUnsuspendable(domain)"
|
||||
>
|
||||
<mat-icon>lock_clock</mat-icon>
|
||||
<span>Suspend</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onUnsuspendClick(domainName)"
|
||||
[elementId]="getElementIdForSuspendUnsuspend()"
|
||||
[disabled]="!isDomainUnsuspendable(domain)"
|
||||
>
|
||||
<mat-icon>lock_open</mat-icon>
|
||||
<span>Unsuspend</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</mat-menu>
|
||||
<div
|
||||
class="console-app__domains-table-parent"
|
||||
[hidden]="domainListService.activeActionComponent"
|
||||
>
|
||||
<div class="console-app__scrollable-wrapper">
|
||||
<div class="console-app__scrollable">
|
||||
@if (isLoading) {
|
||||
<div class="console-app__domains-spinner">
|
||||
<mat-spinner />
|
||||
</div>
|
||||
}
|
||||
<a
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
href="/console-api/dum-download?registrarId={{
|
||||
registrarService.registrarId()
|
||||
}}"
|
||||
class="console-app-domains__download"
|
||||
>
|
||||
<mat-icon>download</mat-icon>
|
||||
Download domains (.csv)
|
||||
</a>
|
||||
|
||||
<mat-form-field class="console-app__domains-filter">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input
|
||||
type="search"
|
||||
matInput
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="sendInput()"
|
||||
#input
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<div
|
||||
class="console-app__domains-selection"
|
||||
[elementId]="getElementIdForBulkDelete()"
|
||||
[ngClass]="{ active: selection.hasValue() }"
|
||||
>
|
||||
<div class="console-app__domains-selection-text">
|
||||
{{ selection.selected.length }} Selected
|
||||
</div>
|
||||
<div class="console-app__domains-selection-actions">
|
||||
<button
|
||||
mat-flat-button
|
||||
aria-label="Delete Selected Domains"
|
||||
[attr.aria-hidden]="!selection.hasValue()"
|
||||
(click)="deleteSelectedDomains()"
|
||||
>
|
||||
Delete Selected Domains
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z0"
|
||||
class="console-app__domains-table"
|
||||
>
|
||||
<!-- Checkbox Column -->
|
||||
<ng-container matColumnDef="select">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox
|
||||
(change)="$event ? toggleAllRows() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected"
|
||||
[aria-label]="checkboxLabel()"
|
||||
[elementId]="getElementIdForBulkDelete()"
|
||||
>
|
||||
</mat-checkbox>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<mat-checkbox
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(row) : null"
|
||||
[checked]="selection.isSelected(row)"
|
||||
[aria-label]="checkboxLabel(row)"
|
||||
[elementId]="getElementIdForBulkDelete()"
|
||||
>
|
||||
</mat-checkbox>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="domainName">
|
||||
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
@if (getOperationMessage(element.domainName)) {
|
||||
<mat-icon
|
||||
[matTooltip]="getOperationMessage(element.domainName)"
|
||||
matTooltipPosition="above"
|
||||
class="primary-text"
|
||||
>info</mat-icon
|
||||
>
|
||||
}
|
||||
<span>{{ element.domainName }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="creationTime">
|
||||
<mat-header-cell *matHeaderCellDef>Creation Time</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
{{ element.creationTime.creationTime }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="registrationExpirationTime">
|
||||
<mat-header-cell *matHeaderCellDef
|
||||
>Expiration Time</mat-header-cell
|
||||
>
|
||||
<mat-cell *matCellDef="let element">
|
||||
{{ element.registrationExpirationTime }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="statuses">
|
||||
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<span>{{ element.statuses?.join(", ") }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="registryLock">
|
||||
<mat-header-cell *matHeaderCellDef
|
||||
>Registry-Locked</mat-header-cell
|
||||
>
|
||||
<mat-cell *matCellDef="let element">{{
|
||||
isDomainLocked(element.domainName)
|
||||
}}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="actions"
|
||||
[matMenuTriggerData]="{
|
||||
domainName: element.domainName,
|
||||
domain: element
|
||||
}"
|
||||
aria-label="Domain actions"
|
||||
>
|
||||
<mat-icon>more_horiz</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row
|
||||
*matHeaderRowDef="displayedColumns"
|
||||
></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
|
||||
<!-- Row shown when there is no matching data. -->
|
||||
<tr class="mat-row" *matNoDataRow>
|
||||
<td class="mat-cell" colspan="6">No domains found</td>
|
||||
</tr>
|
||||
</mat-table>
|
||||
<mat-paginator
|
||||
[length]="totalResults"
|
||||
[pageIndex]="pageNumber"
|
||||
[pageSize]="resultsPerPage"
|
||||
[pageSizeOptions]="[10, 25, 50, 100, 500]"
|
||||
(page)="onPageChange($event)"
|
||||
aria-label="Select page of domain results"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ng-template #domains_content>
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="domainName">
|
||||
<th mat-header-cell *matHeaderCellDef>Domain Name</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.domainName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="creationTime">
|
||||
<th mat-header-cell *matHeaderCellDef>Creation Time</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{ element.creationTime.creationTime }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="registrationExpirationTime">
|
||||
<th mat-header-cell *matHeaderCellDef>Expiration Time</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{ element.registrationExpirationTime }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="statuses">
|
||||
<th mat-header-cell *matHeaderCellDef>Statuses</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.statuses }}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
|
||||
<!-- Row shown when there is no matching data. -->
|
||||
<tr class="mat-row" *matNoDataRow>
|
||||
<td class="mat-cell" colspan="4">No domains found</td>
|
||||
</tr>
|
||||
</table>
|
||||
<mat-paginator
|
||||
[length]="totalResults"
|
||||
[pageIndex]="pageNumber"
|
||||
[pageSize]="resultsPerPage"
|
||||
[pageSizeOptions]="[10, 25, 50, 100, 500]"
|
||||
(page)="onPageChange($event)"
|
||||
aria-label="Select page of domain results"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
</ng-template>
|
||||
</div>
|
||||
</app-selected-registrar-wrapper>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
.console-app {
|
||||
$min-width: 756px;
|
||||
|
||||
&__empty-domains {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
&-icon {
|
||||
transform: scale(3);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__domains-selection {
|
||||
height: 60px;
|
||||
max-height: 0;
|
||||
transition: max-height 0.2s linear;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
gap: 20px;
|
||||
&-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
&.active {
|
||||
max-height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
&-domains__download {
|
||||
position: absolute;
|
||||
top: -55px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&__domains-filter {
|
||||
min-width: $min-width !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__domains-table-parent {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
&__domains-table {
|
||||
min-width: $min-width !important;
|
||||
.mat-column-actions {
|
||||
max-width: 100px;
|
||||
}
|
||||
.mat-column-registryLock {
|
||||
max-width: 150px;
|
||||
}
|
||||
.mat-column-statuses span {
|
||||
padding: 10px 0;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
.mat-column-select {
|
||||
max-width: 60px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.mat-column-domainName {
|
||||
position: relative;
|
||||
padding-left: 25px;
|
||||
mat-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
mat-cell:has([style*="display: none"]),
|
||||
mat-header-cell:has([style*="display: none"]) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__domains-spinner {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mat-mdc-paginator {
|
||||
min-width: $min-width !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,11 +14,14 @@
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DomainListComponent } from './domainList.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { DomainListComponent } from './domainList.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AppModule } from '../app.module';
|
||||
|
||||
describe('DomainListComponent', () => {
|
||||
let component: DomainListComponent;
|
||||
@@ -28,11 +31,16 @@ describe('DomainListComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DomainListComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
AppModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
providers: [BackendService],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DomainListComponent);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,32 +12,135 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
|
||||
import { Component, effect, Inject, ViewChild } from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { debounceTime, filter, Subject, take } from 'rxjs';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { Domain, DomainListService } from './domainList.service';
|
||||
import { Subject, debounceTime } from 'rxjs';
|
||||
import {
|
||||
BULK_ACTION_NAME,
|
||||
Domain,
|
||||
DomainListService,
|
||||
} from './domainList.service';
|
||||
import { RegistryLockComponent } from './registryLock.component';
|
||||
import { RegistryLockService } from './registryLock.service';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
import { CdkColumnDef } from '@angular/cdk/table';
|
||||
|
||||
interface DomainResponse {
|
||||
message: string;
|
||||
responseCode: string;
|
||||
}
|
||||
|
||||
interface DomainData {
|
||||
[domain: string]: DomainResponse;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-response-dialog',
|
||||
template: `
|
||||
<h2 mat-dialog-title>{{ data.title }}</h2>
|
||||
<mat-dialog-content [innerHTML]="data.content" />
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onClose()">Close</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
standalone: false,
|
||||
})
|
||||
export class ResponseDialogComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ReasonDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
public data: { title: string; content: string }
|
||||
) {}
|
||||
|
||||
onClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
enum Operation {
|
||||
deleting = 'deleting',
|
||||
suspending = 'suspending',
|
||||
unsuspending = 'unsuspending',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-reason-dialog',
|
||||
template: `
|
||||
<h2 mat-dialog-title>
|
||||
Please provide the (EPP) reason for {{ data.operation }} the domain(s):
|
||||
</h2>
|
||||
<mat-dialog-content>
|
||||
<mat-form-field appearance="outline" style="width:100%">
|
||||
<textarea matInput [(ngModel)]="reason" rows="4"></textarea>
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
<button mat-button color="warn" (click)="onSave()" [disabled]="!reason">
|
||||
Save
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
standalone: false,
|
||||
})
|
||||
export class ReasonDialogComponent {
|
||||
reason: string = '';
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ReasonDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
public data: { operation: Operation }
|
||||
) {}
|
||||
|
||||
onSave(): void {
|
||||
this.dialogRef.close(this.reason);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-domain-list',
|
||||
templateUrl: './domainList.component.html',
|
||||
styleUrls: ['./domainList.component.scss'],
|
||||
providers: [DomainListService],
|
||||
standalone: false,
|
||||
providers: [CdkColumnDef],
|
||||
})
|
||||
export class DomainListComponent {
|
||||
public static PATH = 'domain-list';
|
||||
private static SUSPENDED_STATUSES = [
|
||||
'SERVER_RENEW_PROHIBITED',
|
||||
'SERVER_TRANSFER_PROHIBITED',
|
||||
'SERVER_UPDATE_PROHIBITED',
|
||||
'SERVER_DELETE_PROHIBITED',
|
||||
'SERVER_HOLD',
|
||||
];
|
||||
private readonly DEBOUNCE_MS = 500;
|
||||
isAllSelected = false;
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'select',
|
||||
'domainName',
|
||||
'creationTime',
|
||||
'registrationExpirationTime',
|
||||
'statuses',
|
||||
'registryLock',
|
||||
'actions',
|
||||
];
|
||||
|
||||
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
|
||||
selection = new SelectionModel<Domain>(true, [], undefined, this.isChecked());
|
||||
isLoading = true;
|
||||
|
||||
searchTermSubject = new Subject<string>();
|
||||
@@ -45,15 +148,30 @@ export class DomainListComponent {
|
||||
|
||||
pageNumber?: number;
|
||||
resultsPerPage = 50;
|
||||
totalResults?: number;
|
||||
totalResults?: number = 0;
|
||||
|
||||
reason: string = '';
|
||||
|
||||
operationResult: DomainData | undefined;
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private domainListService: DomainListService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
protected domainListService: DomainListService,
|
||||
protected registrarService: RegistrarService,
|
||||
protected registryLockService: RegistryLockService,
|
||||
private _snackBar: MatSnackBar,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
effect(() => {
|
||||
this.pageNumber = 0;
|
||||
this.totalResults = 0;
|
||||
if (this.registrarService.registrarId()) {
|
||||
this.loadLocks();
|
||||
this.reloadData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
@@ -63,13 +181,34 @@ export class DomainListComponent {
|
||||
.subscribe((searchTermValue) => {
|
||||
this.reloadData();
|
||||
});
|
||||
this.reloadData();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.searchTermSubject.complete();
|
||||
}
|
||||
|
||||
openRegistryLock(domainName: string) {
|
||||
this.domainListService.selectedDomain = domainName;
|
||||
this.domainListService.activeActionComponent = RegistryLockComponent;
|
||||
}
|
||||
|
||||
loadLocks() {
|
||||
this.registryLockService.retrieveLocks().subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
if (err.status !== HttpStatusCode.Forbidden) {
|
||||
// Some users may not have registry lock permissions and that's OK
|
||||
this._snackBar.open(err.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
isDomainLocked(domainName: string) {
|
||||
return this.registryLockService.domainsLocks.some(
|
||||
(d) => d.domainName === domainName
|
||||
);
|
||||
}
|
||||
|
||||
reloadData() {
|
||||
this.isLoading = true;
|
||||
this.domainListService
|
||||
@@ -79,10 +218,16 @@ export class DomainListComponent {
|
||||
this.totalResults,
|
||||
this.searchTerm
|
||||
)
|
||||
.subscribe((domainListResult) => {
|
||||
this.dataSource.data = domainListResult.domains;
|
||||
this.totalResults = domainListResult.totalResults;
|
||||
this.isLoading = false;
|
||||
.subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.message);
|
||||
this.isLoading = false;
|
||||
},
|
||||
next: (domainListResult) => {
|
||||
this.dataSource.data = this.domainListService.domainsList;
|
||||
this.totalResults = (domainListResult || {}).totalResults || 0;
|
||||
this.isLoading = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,6 +238,184 @@ export class DomainListComponent {
|
||||
onPageChange(event: PageEvent) {
|
||||
this.pageNumber = event.pageIndex;
|
||||
this.resultsPerPage = event.pageSize;
|
||||
this.selection.clear();
|
||||
this.reloadData();
|
||||
}
|
||||
|
||||
toggleAllRows() {
|
||||
if (this.isAllSelected) {
|
||||
this.selection.clear();
|
||||
this.isAllSelected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.selection.select(...this.dataSource.data);
|
||||
this.isAllSelected = true;
|
||||
}
|
||||
|
||||
checkboxLabel(row?: Domain): string {
|
||||
if (!row) {
|
||||
return `${this.isAllSelected ? 'deselect' : 'select'} all`;
|
||||
}
|
||||
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${
|
||||
row.domainName
|
||||
}`;
|
||||
}
|
||||
|
||||
private isChecked(): ((o1: Domain, o2: Domain) => boolean) | undefined {
|
||||
return (o1: Domain, o2: Domain) => {
|
||||
if (!o1.domainName || !o2.domainName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isAllSelected || o1.domainName === o2.domainName;
|
||||
};
|
||||
}
|
||||
|
||||
getElementIdForBulkDelete() {
|
||||
return RESTRICTED_ELEMENTS.BULK_DELETE;
|
||||
}
|
||||
|
||||
getElementIdForSuspendUnsuspend() {
|
||||
return RESTRICTED_ELEMENTS.SUSPEND;
|
||||
}
|
||||
|
||||
getOperationMessage(domain: string) {
|
||||
if (this.operationResult && this.operationResult[domain])
|
||||
return this.operationResult[domain].message;
|
||||
return '';
|
||||
}
|
||||
|
||||
isDomainUnsuspendable(domain: Domain) {
|
||||
return DomainListComponent.SUSPENDED_STATUSES.every((s) =>
|
||||
domain.statuses.includes(s)
|
||||
);
|
||||
}
|
||||
|
||||
sendDeleteRequest(reason: string) {
|
||||
this.isLoading = true;
|
||||
this.domainListService
|
||||
.bulkDomainAction(
|
||||
this.selection.selected.map((d) => d.domainName),
|
||||
reason,
|
||||
this.registrarService.registrarId(),
|
||||
BULK_ACTION_NAME.DELETE
|
||||
)
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: (result: DomainData) => {
|
||||
this.isLoading = false;
|
||||
const successCount = Object.keys(result).filter((domainName) =>
|
||||
result[domainName].responseCode.toString().startsWith('1')
|
||||
).length;
|
||||
const failureCount = Object.keys(result).length - successCount;
|
||||
this.dialog.open(ResponseDialogComponent, {
|
||||
data: {
|
||||
title: 'Domain Deletion Results',
|
||||
content: `Successfully deleted - ${successCount} domain(s)<br/>Failed to delete - ${failureCount} domain(s)<br/>${
|
||||
failureCount
|
||||
? 'Some domains could not be deleted due to ongoing processes or server errors. '
|
||||
: ''
|
||||
}Please check the table for more information.`,
|
||||
},
|
||||
});
|
||||
this.selection.clear();
|
||||
this.operationResult = result;
|
||||
this.reloadData();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.isLoading = false;
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteSelectedDomains() {
|
||||
const dialogRef = this.dialog.open(ReasonDialogComponent, {
|
||||
data: {
|
||||
operation: Operation.deleting,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((reason) => !!reason)
|
||||
)
|
||||
.subscribe(this.sendDeleteRequest.bind(this));
|
||||
}
|
||||
|
||||
sendSuspendUnsuspendRequest(
|
||||
domainName: string,
|
||||
reason: string,
|
||||
actionName: BULK_ACTION_NAME
|
||||
) {
|
||||
this.isLoading = true;
|
||||
this.domainListService
|
||||
.bulkDomainAction(
|
||||
[domainName],
|
||||
reason,
|
||||
this.registrarService.registrarId(),
|
||||
actionName
|
||||
)
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: (result: DomainData) => {
|
||||
this.isLoading = false;
|
||||
if (result[domainName].responseCode.toString().startsWith('2')) {
|
||||
this._snackBar.open(result[domainName].message);
|
||||
} else {
|
||||
this.reloadData();
|
||||
}
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.isLoading = false;
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
onSuspendClick(domainName: string) {
|
||||
const dialogRef = this.dialog.open(ReasonDialogComponent, {
|
||||
data: {
|
||||
operation: Operation.suspending,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((reason) => !!reason)
|
||||
)
|
||||
.subscribe((reason) => {
|
||||
this.sendSuspendUnsuspendRequest(
|
||||
domainName,
|
||||
reason,
|
||||
BULK_ACTION_NAME.SUSPEND
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onUnsuspendClick(domainName: string) {
|
||||
const dialogRef = this.dialog.open(ReasonDialogComponent, {
|
||||
data: {
|
||||
operation: Operation.unsuspending,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((reason) => !!reason)
|
||||
)
|
||||
.subscribe((reason) => {
|
||||
this.sendSuspendUnsuspendRequest(
|
||||
domainName,
|
||||
reason,
|
||||
BULK_ACTION_NAME.UNSUSPEND
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,10 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { tap } from 'rxjs';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
|
||||
export interface CreateAutoTimestamp {
|
||||
creationTime: string;
|
||||
@@ -35,15 +35,25 @@ export interface DomainListResult {
|
||||
totalResults: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export enum BULK_ACTION_NAME {
|
||||
DELETE = 'DELETE',
|
||||
SUSPEND = 'SUSPEND',
|
||||
UNSUSPEND = 'UNSUSPEND',
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DomainListService {
|
||||
checkpointTime?: string;
|
||||
selectedDomain?: string;
|
||||
public activeActionComponent: Type<any> | null = null;
|
||||
public domainsList: Domain[] = [];
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
retrieveDomains(
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
@@ -61,8 +71,23 @@ export class DomainListService {
|
||||
)
|
||||
.pipe(
|
||||
tap((domainListResult: DomainListResult) => {
|
||||
this.checkpointTime = domainListResult.checkpointTime;
|
||||
this.checkpointTime = domainListResult?.checkpointTime;
|
||||
this.domainsList = domainListResult?.domains;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
bulkDomainAction(
|
||||
domains: string[],
|
||||
reason: string,
|
||||
registrarId: string,
|
||||
actionName: BULK_ACTION_NAME
|
||||
) {
|
||||
return this.backendService.bulkDomainAction(
|
||||
domains,
|
||||
reason,
|
||||
actionName,
|
||||
registrarId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
83
console-webapp/src/app/domains/registryLock.component.html
Normal file
83
console-webapp/src/app/domains/registryLock.component.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<div class="console-app__registry-lock">
|
||||
<p>
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Back to domains list"
|
||||
(click)="goBack()"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@if(!registrarService.registrar()?.registryLockAllowed) {
|
||||
<h1>
|
||||
Sorry, your registrar hasn't enrolled in registry lock yet. To do so, please
|
||||
contact {{ userDataService.userData()?.supportEmail }}.
|
||||
</h1>
|
||||
} @else if (isLocked()) {
|
||||
<h1>Unlock the domain {{ domainListService.selectedDomain }}</h1>
|
||||
<form (ngSubmit)="save(false)" [formGroup]="unlockDomain">
|
||||
<p>
|
||||
<mat-label for="password">Password: </mat-label>
|
||||
<mat-form-field name="password" appearance="outline">
|
||||
<input matInput type="text" formControlName="password" required />
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<p>
|
||||
<mat-label for="relockTime"
|
||||
>Automatically re-lock the domain after:</mat-label
|
||||
>
|
||||
<mat-radio-group
|
||||
name="relockTime"
|
||||
formControlName="relockTime"
|
||||
aria-label="Automatically relock option"
|
||||
>
|
||||
@for (option of relockOptions; track option.name) {
|
||||
<mat-radio-button [value]="option.duration">{{
|
||||
option.name
|
||||
}}</mat-radio-button>
|
||||
}
|
||||
</mat-radio-group>
|
||||
</p>
|
||||
|
||||
<div class="console-app__registry-lock-notification">
|
||||
<mat-icon>priority_high</mat-icon>Confirmation email will be sent to your
|
||||
email address to confirm the unlock
|
||||
</div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="!unlockDomain.valid"
|
||||
aria-label="Submit domain unlock request"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
} @else {
|
||||
<h1>Lock the domain {{ domainListService.selectedDomain }}</h1>
|
||||
<form (ngSubmit)="save(true)" [formGroup]="lockDomain">
|
||||
<p>
|
||||
<mat-label for="password">Password: </mat-label>
|
||||
<mat-form-field name="password" appearance="outline">
|
||||
<input matInput type="text" formControlName="password" required />
|
||||
</mat-form-field>
|
||||
</p>
|
||||
|
||||
<div class="console-app__registry-lock-notification">
|
||||
<mat-icon>priority_high</mat-icon>The lock will not take effect until you
|
||||
click the confirmation link that will be emailed to you. When it takes
|
||||
effect, you will be billed the standard server status change billing cost.
|
||||
</div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="!lockDomain.valid"
|
||||
aria-label="Submit domain lock request"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
20
console-webapp/src/app/domains/registryLock.component.scss
Normal file
20
console-webapp/src/app/domains/registryLock.component.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.console-app {
|
||||
&__registry-lock {
|
||||
mat-label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
&__registry-lock-notification {
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--light-highlight);
|
||||
margin-bottom: 20px;
|
||||
width: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
93
console-webapp/src/app/domains/registryLock.component.ts
Normal file
93
console-webapp/src/app/domains/registryLock.component.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, computed } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { UserDataService } from '../shared/services/userData.service';
|
||||
import { DomainListService } from './domainList.service';
|
||||
import { RegistryLockService } from './registryLock.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registry-lock',
|
||||
templateUrl: './registryLock.component.html',
|
||||
styleUrls: ['./registryLock.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistryLockComponent {
|
||||
readonly isLocked = computed(() =>
|
||||
this.registryLockService.domainsLocks.some(
|
||||
(dl) => dl.domainName === this.domainListService.selectedDomain
|
||||
)
|
||||
);
|
||||
|
||||
relockOptions = [
|
||||
{ name: '1 hour', duration: 3600000 },
|
||||
{ name: '6 hours', duration: 21600000 },
|
||||
{ name: '24 hours', duration: 86400000 },
|
||||
{ name: 'Never', duration: undefined },
|
||||
];
|
||||
|
||||
lockDomain = new FormGroup({
|
||||
password: new FormControl(''),
|
||||
});
|
||||
|
||||
unlockDomain = new FormGroup({
|
||||
password: new FormControl(''),
|
||||
relockTime: new FormControl(undefined),
|
||||
});
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
protected domainListService: DomainListService,
|
||||
protected registryLockService: RegistryLockService,
|
||||
protected userDataService: UserDataService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
goBack() {
|
||||
this.domainListService.selectedDomain = undefined;
|
||||
this.domainListService.activeActionComponent = null;
|
||||
}
|
||||
|
||||
save(isLock: boolean) {
|
||||
let request;
|
||||
if (!isLock) {
|
||||
request = this.registryLockService.registryLockDomain(
|
||||
this.domainListService.selectedDomain || '',
|
||||
this.unlockDomain.value.password || '',
|
||||
this.unlockDomain.value.relockTime || undefined,
|
||||
isLock
|
||||
);
|
||||
} else {
|
||||
request = this.registryLockService.registryLockDomain(
|
||||
this.domainListService.selectedDomain || '',
|
||||
this.lockDomain.value.password || '',
|
||||
undefined,
|
||||
isLock
|
||||
);
|
||||
}
|
||||
|
||||
request.subscribe({
|
||||
complete: () => {
|
||||
this.goBack();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
59
console-webapp/src/app/domains/registryLock.service.ts
Normal file
59
console-webapp/src/app/domains/registryLock.service.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { tap } from 'rxjs';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
|
||||
export interface DomainLocksResult {
|
||||
domainName: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RegistryLockService {
|
||||
public domainsLocks: DomainLocksResult[] = [];
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
retrieveLocks() {
|
||||
return this.backendService
|
||||
.getLocks(this.registrarService.registrarId())
|
||||
.pipe(
|
||||
tap((domainLocksResult) => {
|
||||
this.domainsLocks = domainLocksResult;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
registryLockDomain(
|
||||
domainName: string,
|
||||
password: string,
|
||||
relockDurationMillis: number | undefined,
|
||||
isLock: boolean
|
||||
) {
|
||||
return this.backendService.registryLockDomain(
|
||||
domainName,
|
||||
password,
|
||||
relockDurationMillis,
|
||||
this.registrarService.registrarId(),
|
||||
isLock
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,25 +16,24 @@
|
||||
&__logo {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
margin-left: -15px;
|
||||
}
|
||||
&__menu-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
&__header {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
@media (max-width: 599px) {
|
||||
.mat-toolbar {
|
||||
padding: 0;
|
||||
}
|
||||
.console-app__logo {
|
||||
font-size: 16px;
|
||||
}
|
||||
button {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
width: 30px;
|
||||
}
|
||||
margin-bottom: 15px;
|
||||
|
||||
.mat-toolbar {
|
||||
background: transparent;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&-user-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HeaderComponent } from './header.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AppModule, SelectedRegistrarModule } from '../app.module';
|
||||
import { AppRoutingModule } from '../app-routing.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
|
||||
describe('HeaderComponent', () => {
|
||||
let component: HeaderComponent;
|
||||
@@ -24,7 +30,19 @@ describe('HeaderComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MaterialModule, BrowserAnimationsModule],
|
||||
imports: [
|
||||
SelectedRegistrarModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
AppModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
declarations: [HeaderComponent],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -13,15 +13,19 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class HeaderComponent {
|
||||
private isNavOpen = false;
|
||||
|
||||
constructor(protected breakpointObserver: BreakPointObserverService) {}
|
||||
|
||||
@Output() toggleNavOpen = new EventEmitter<boolean>();
|
||||
|
||||
toggleNavPane() {
|
||||
|
||||
62
console-webapp/src/app/history/history.component.html
Normal file
62
console-webapp/src/app/history/history.component.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<app-selected-registrar-wrapper>
|
||||
<div class="history-log">
|
||||
<h1 class="mat-headline-4" forceFocus>
|
||||
Registrar Console Activity History
|
||||
</h1>
|
||||
<mat-tab-group
|
||||
[elementId]="getElementIdForUserLog()"
|
||||
class="history-log__tabs"
|
||||
>
|
||||
<mat-tab label="Registrar Activity">
|
||||
<div class="spacer"></div>
|
||||
|
||||
<app-history-list
|
||||
[historyRecords]="historyService.historyRecordsRegistrar()"
|
||||
[isLoading]="isLoading"
|
||||
/>
|
||||
</mat-tab>
|
||||
<mat-tab label="User Activity">
|
||||
<div class="spacer"></div>
|
||||
<form (ngSubmit)="loadHistory()" #form="ngForm">
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Console User Email: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
id="email"
|
||||
type="email"
|
||||
name="consoleUserEmail"
|
||||
required
|
||||
email
|
||||
[(ngModel)]="consoleUserEmail"
|
||||
#emailControl="ngModel"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Search user history"
|
||||
[disabled]="!form.valid"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
<div class="spacer"></div>
|
||||
<app-history-list
|
||||
[historyRecords]="historyService.historyRecordsUser()"
|
||||
[isLoading]="isLoading"
|
||||
/>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
<app-history-list
|
||||
[elementId]="getElementIdForUserLog()"
|
||||
[isReverse]="true"
|
||||
[historyRecords]="historyService.historyRecordsUser()"
|
||||
[isLoading]="isLoading"
|
||||
/>
|
||||
</app-selected-registrar-wrapper>
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,5 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package google.registry.module.backend;
|
||||
.history-log {
|
||||
font-family: "Roboto", sans-serif;
|
||||
max-width: 760px;
|
||||
|
||||
.spacer {
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
80
console-webapp/src/app/history/history.component.ts
Normal file
80
console-webapp/src/app/history/history.component.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, effect } from '@angular/core';
|
||||
import { UserDataService } from '../shared/services/userData.service';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { HistoryService } from './history.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {
|
||||
GlobalLoader,
|
||||
GlobalLoaderService,
|
||||
} from '../shared/services/globalLoader.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-history',
|
||||
templateUrl: './history.component.html',
|
||||
styleUrls: ['./history.component.scss'],
|
||||
providers: [HistoryService],
|
||||
standalone: false,
|
||||
})
|
||||
export class HistoryComponent implements GlobalLoader {
|
||||
public static PATH = 'history';
|
||||
|
||||
consoleUserEmail: string = '';
|
||||
isLoading: boolean = false;
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private registrarService: RegistrarService,
|
||||
protected historyService: HistoryService,
|
||||
protected globalLoader: GlobalLoaderService,
|
||||
protected userDataService: UserDataService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
effect(() => {
|
||||
if (registrarService.registrarId()) {
|
||||
this.loadHistory();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getElementIdForUserLog() {
|
||||
return RESTRICTED_ELEMENTS.ACTIVITY_PER_USER;
|
||||
}
|
||||
|
||||
loadingTimeout() {
|
||||
this._snackBar.open('Timeout loading records history');
|
||||
}
|
||||
|
||||
loadHistory() {
|
||||
this.globalLoader.startGlobalLoader(this);
|
||||
this.isLoading = true;
|
||||
this.historyService
|
||||
.getHistoryLog(this.registrarService.registrarId(), this.consoleUserEmail)
|
||||
.subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
this.isLoading = false;
|
||||
},
|
||||
next: () => {
|
||||
this.globalLoader.stopGlobalLoader(this);
|
||||
this.isLoading = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
46
console-webapp/src/app/history/history.service.ts
Normal file
46
console-webapp/src/app/history/history.service.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { tap } from 'rxjs';
|
||||
|
||||
export interface HistoryRecord {
|
||||
modificationTime: string;
|
||||
type: string;
|
||||
description: string;
|
||||
actingUser: {
|
||||
emailAddress: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HistoryService {
|
||||
historyRecordsRegistrar = signal<HistoryRecord[]>([]);
|
||||
historyRecordsUser = signal<HistoryRecord[]>([]);
|
||||
|
||||
constructor(private backendService: BackendService) {}
|
||||
|
||||
getHistoryLog(registrarId: string, userEmail?: string) {
|
||||
return this.backendService.getHistoryLog(registrarId, userEmail).pipe(
|
||||
tap((historyRecords: HistoryRecord[]) => {
|
||||
if (userEmail) {
|
||||
this.historyRecordsUser.set(historyRecords);
|
||||
} else {
|
||||
this.historyRecordsRegistrar.set(historyRecords);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
45
console-webapp/src/app/history/historyList.component.html
Normal file
45
console-webapp/src/app/history/historyList.component.html
Normal file
@@ -0,0 +1,45 @@
|
||||
@if (!isLoading && historyRecords.length == 0) {
|
||||
<div class="history-list__no-records">
|
||||
<mat-icon class="history-list__no-records-icon secondary-text"
|
||||
>apps_outage</mat-icon
|
||||
>
|
||||
<h1>No records found</h1>
|
||||
</div>
|
||||
} @else {
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
@for (item of historyRecords; track item; let last = $last) {
|
||||
<mat-list-item class="history-list__item">
|
||||
<mat-icon
|
||||
[ngClass]="getIconClass(item.type)"
|
||||
class="history-list__icon"
|
||||
>
|
||||
{{ getIconForType(item.type) }}
|
||||
</mat-icon>
|
||||
<div class="history-list__content">
|
||||
<div class="history-list__description">
|
||||
<span class="history-list__description--main">{{ item.type }}</span>
|
||||
<div>
|
||||
@if (parseDescription(item.description).detail) {
|
||||
<mat-chip class="history-list__chip">
|
||||
{{ parseDescription(item.description).detail }}
|
||||
</mat-chip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-list__user">
|
||||
<b>User - {{ item.actingUser.emailAddress }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<span class="history-list__timestamp">
|
||||
{{ item.modificationTime | date : "MMM d, y, h:mm a" }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
@if (!last) {
|
||||
<mat-divider></mat-divider>
|
||||
} }
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
81
console-webapp/src/app/history/historyList.component.scss
Normal file
81
console-webapp/src/app/history/historyList.component.scss
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
.history-list {
|
||||
font-family: "Roboto", sans-serif;
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// Override default mat-list-item height to fit content
|
||||
height: auto !important;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
&__no-records {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
&__no-records-icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
font-size: 4rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 16px;
|
||||
|
||||
&--update {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&--security {
|
||||
color: #d32f2f;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
&--main {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&__chip {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
&__user {
|
||||
font-size: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
66
console-webapp/src/app/history/historyList.component.ts
Normal file
66
console-webapp/src/app/history/historyList.component.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { HistoryRecord } from './history.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-history-list',
|
||||
templateUrl: './historyList.component.html',
|
||||
styleUrls: ['./historyList.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class HistoryListComponent {
|
||||
@Input() historyRecords: HistoryRecord[] = [];
|
||||
@Input() isLoading: boolean = false;
|
||||
|
||||
getIconForType(type: string): string {
|
||||
switch (type) {
|
||||
case 'REGISTRAR_UPDATE':
|
||||
return 'edit';
|
||||
case 'REGISTRAR_SECURITY_UPDATE':
|
||||
return 'security';
|
||||
default:
|
||||
return 'history'; // A fallback icon
|
||||
}
|
||||
}
|
||||
|
||||
getIconClass(type: string): string {
|
||||
switch (type) {
|
||||
case 'REGISTRAR_UPDATE':
|
||||
return 'history-log__icon--update';
|
||||
case 'REGISTRAR_SECURITY_UPDATE':
|
||||
return 'history-log__icon--security';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
parseDescription(description: string): {
|
||||
main: string;
|
||||
detail: string | null;
|
||||
} {
|
||||
if (!description) {
|
||||
return { main: 'N/A', detail: null };
|
||||
}
|
||||
const parts = description.split('|');
|
||||
const detail = parts.length > 1 ? parts[1].replace(/_/g, ' ') : parts[0];
|
||||
|
||||
return {
|
||||
main: parts[0],
|
||||
detail: detail,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,55 @@
|
||||
<div class="console-app__home">
|
||||
<h1>Welcome to the Google Registry Console</h1>
|
||||
<div
|
||||
class="console-app__home"
|
||||
[class.console-app__home_tablet]="breakPointObserverService.isTabletView()"
|
||||
>
|
||||
<h1 class="mat-headline-4">Dashboard</h1>
|
||||
<div class="console-app__home-widgets">
|
||||
<div app-domains-widget class="console-app__widget-wrapper__wide"></div>
|
||||
<div app-contact-widget class="console-app__widget-wrapper__wide"></div>
|
||||
<div app-tlds-widget></div>
|
||||
<div app-promotions-widget class="console-app__widget-wrapper__wide"></div>
|
||||
<div app-settings-widget class="console-app__widget-wrapper__wide"></div>
|
||||
<div app-resources-widget></div>
|
||||
<div app-billing-widget></div>
|
||||
<div app-epp-widget></div>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<h3>
|
||||
<mat-icon class="secondary-text">view_list</mat-icon>
|
||||
DUMs
|
||||
</h3>
|
||||
<p class="secondary-text">View Domains Under Management</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-button color="primary" (click)="viewDums()">
|
||||
View DUMs
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<h3>
|
||||
<mat-icon class="secondary-text">settings</mat-icon>
|
||||
EPP Passwords
|
||||
</h3>
|
||||
<p class="secondary-text">View / Update EPP Password</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-button color="primary" (click)="updateEppPassword()">
|
||||
Update EPP Password
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<mat-card
|
||||
appearance="outlined"
|
||||
[elementId]="getElementIdForRegistrarsBlock()"
|
||||
>
|
||||
<mat-card-content>
|
||||
<h3>
|
||||
<mat-icon class="secondary-text">account_circle</mat-icon>
|
||||
Registrars
|
||||
</h3>
|
||||
<p class="secondary-text">View all registrars</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-button color="primary" (click)="viewRegistrars()">
|
||||
Manage Registrars
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -13,25 +13,23 @@
|
||||
// limitations under the License.
|
||||
|
||||
.console-app {
|
||||
&__home {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
&__home-widgets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
grid-gap: 20px;
|
||||
grid-auto-flow: dense;
|
||||
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
h3 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
mat-card {
|
||||
height: 100%;
|
||||
h1 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@media (max-width: 510px) {
|
||||
.console-app__widget-wrapper__wide {
|
||||
grid-column: initial;
|
||||
&__home_tablet {
|
||||
.console-app__home-widgets {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { AppModule } from '../app.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
@@ -23,8 +27,13 @@ describe('HomeComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MaterialModule],
|
||||
imports: [MaterialModule, AppModule],
|
||||
declarations: [HomeComponent],
|
||||
providers: [
|
||||
BackendService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,12 +12,43 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DomainListComponent } from '../domains/domainList.component';
|
||||
import { RegistrarComponent } from '../registrar/registrarsTable.component';
|
||||
import SecurityComponent from '../settings/security/security.component';
|
||||
import { SettingsComponent } from '../settings/settings.component';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false,
|
||||
})
|
||||
export class HomeComponent {}
|
||||
export class HomeComponent {
|
||||
constructor(
|
||||
protected breakPointObserverService: BreakPointObserverService,
|
||||
private router: Router
|
||||
) {}
|
||||
getElementIdForRegistrarsBlock() {
|
||||
return RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT;
|
||||
}
|
||||
viewRegistrars() {
|
||||
this.router.navigate([RegistrarComponent.PATH], {
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
updateEppPassword() {
|
||||
this.router.navigate(
|
||||
[SettingsComponent.PATH + '/' + SecurityComponent.PATH],
|
||||
{ queryParamsHandling: 'merge' }
|
||||
);
|
||||
}
|
||||
viewDums() {
|
||||
this.router.navigate([DomainListComponent.PATH], {
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="console-app__widget">
|
||||
<a
|
||||
class="console-app__widget_left"
|
||||
href="{{ driveFolderUrl }}"
|
||||
(click)="openBillingDetails($event)"
|
||||
>
|
||||
<mat-icon class="console-app__widget-icon">account_balance</mat-icon>
|
||||
<h1 class="console-app__widget-title">Billing Info</h1>
|
||||
<h4 class="secondary-text text-center">
|
||||
<span *ngIf="driveFolderUrl; else noDriveFolderUrl">
|
||||
View important billing and payments information.
|
||||
</span>
|
||||
<ng-template #noDriveFolderUrl>
|
||||
<span> Your billing folder is pending allocation. </span>
|
||||
</ng-template>
|
||||
</h4>
|
||||
</a>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -1,29 +0,0 @@
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="console-app__widget">
|
||||
<div class="console-app__widget_left">
|
||||
<mat-icon class="console-app__widget-icon">call</mat-icon>
|
||||
<h1 class="console-app__widget-title">Contact Support</h1>
|
||||
<h4 class="secondary-text text-center">
|
||||
Let us know if you have any questions
|
||||
</h4>
|
||||
</div>
|
||||
<div class="console-app__widget_right">
|
||||
<div class="console-app__widget-section-header">Give us a Call</div>
|
||||
<p class="secondary-text">
|
||||
Call {{ userDataService.userData?.productName }} support at
|
||||
<a href="tel:{{ userDataService.userData?.supportPhoneNumber }}">{{
|
||||
userDataService.userData?.supportPhoneNumber
|
||||
}}</a>
|
||||
</p>
|
||||
<div class="console-app__widget-section-header">Send us an Email</div>
|
||||
<p class="secondary-text">
|
||||
Email {{ userDataService.userData?.productName }} at
|
||||
<a href="mailto:{{ userDataService.userData?.supportEmail }}">{{
|
||||
userDataService.userData?.supportEmail
|
||||
}}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -1,30 +0,0 @@
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="console-app__widget">
|
||||
<div class="console-app__widget_left">
|
||||
<mat-icon class="console-app__widget-icon">view_list</mat-icon>
|
||||
<h1 class="console-app__widget-title">Domains</h1>
|
||||
<h4 class="secondary-text text-center">
|
||||
Manage domain names and registry lock settings.
|
||||
</h4>
|
||||
</div>
|
||||
<div class="console-app__widget_right">
|
||||
<button mat-button color="primary" class="console-app__widget-link">
|
||||
Create a Domain
|
||||
</button>
|
||||
<p class="secondary-text">Register a new domain name</p>
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
class="console-app__widget-link"
|
||||
(click)="openDomainsPage()"
|
||||
>
|
||||
View DUMs
|
||||
</button>
|
||||
<p class="secondary-text">
|
||||
Download a csv of all domains under management
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -1,13 +0,0 @@
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="console-app__widget">
|
||||
<div class="console-app__widget_left">
|
||||
<mat-icon class="console-app__widget-icon">computer</mat-icon>
|
||||
<h1 class="console-app__widget-title">EPP Console</h1>
|
||||
<h4 class="secondary-text text-center">
|
||||
Test run and execute EPP commands using XML files.
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -1,27 +0,0 @@
|
||||
<mat-card class="console-app__widget-wrapper__wide">
|
||||
<mat-card-content>
|
||||
<div class="console-app__widget">
|
||||
<div class="console-app__widget_left">
|
||||
<mat-icon class="console-app__widget-icon">subject</mat-icon>
|
||||
<h1 class="console-app__widget-title">Promotions</h1>
|
||||
<h4 class="secondary-text text-center">
|
||||
Manage Google Registry promotions and view onboarding process.
|
||||
</h4>
|
||||
</div>
|
||||
<div class="console-app__widget_right">
|
||||
<button mat-button color="primary" class="console-app__widget-link">
|
||||
Manage Preferred Partner Program
|
||||
</button>
|
||||
<p class="secondary-text">
|
||||
Onboard or view current preferred partner status
|
||||
</p>
|
||||
<button mat-button color="primary" class="console-app__widget-link">
|
||||
Manage Registry Lock Program
|
||||
</button>
|
||||
<p class="secondary-text">
|
||||
Onboard or view current registry lock status
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -1,17 +0,0 @@
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="console-app__widget">
|
||||
<a
|
||||
class="console-app__widget_left"
|
||||
href="{{ userDataService.userData?.technicalDocsUrl }}"
|
||||
target="_blank"
|
||||
>
|
||||
<mat-icon class="console-app__widget-icon">menu_book</mat-icon>
|
||||
<h1 class="console-app__widget-title">Resources</h1>
|
||||
<h4 class="secondary-text text-center">
|
||||
Use Google Drive to view onboarding FAQs, and technical documentation.
|
||||
</h4>
|
||||
</a>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -1,53 +0,0 @@
|
||||
<mat-card class="console-app__widget-wrapper__wide">
|
||||
<mat-card-content>
|
||||
<div class="console-app__widget">
|
||||
<div class="console-app__widget_left">
|
||||
<mat-icon class="console-app__widget-icon">settings</mat-icon>
|
||||
<h1 class="console-app__widget-title">Settings</h1>
|
||||
<h4 class="secondary-text text-center">
|
||||
Configure registrar settings, manage console users, and view activity
|
||||
log.
|
||||
</h4>
|
||||
</div>
|
||||
<div class="console-app__widget_right">
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
class="console-app__widget-link"
|
||||
(click)="openContactsPage()"
|
||||
>
|
||||
Contact Information
|
||||
</button>
|
||||
<p class="secondary-text">Manage Primary, Technical, etc contacts.</p>
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
class="console-app__widget-link"
|
||||
(click)="openSecurityPage()"
|
||||
>
|
||||
Security
|
||||
</button>
|
||||
<p class="secondary-text">
|
||||
Manage IP allow lists and SSL certificates.
|
||||
</p>
|
||||
<button mat-button color="primary" class="console-app__widget-link">
|
||||
Nomulus Password
|
||||
</button>
|
||||
<p class="secondary-text">Reset your Nomulus password.</p>
|
||||
<button mat-button color="primary" class="console-app__widget-link">
|
||||
User Management
|
||||
</button>
|
||||
<p class="secondary-text">Create and manage console user accounts</p>
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
class="console-app__widget-link"
|
||||
(click)="openRegistrarsPage()"
|
||||
>
|
||||
Registrar Management
|
||||
</button>
|
||||
<p class="secondary-text">Create and manage registrar accounts</p>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { RegistrarComponent } from 'src/app/registrar/registrarsTable.component';
|
||||
import ContactComponent from 'src/app/settings/contact/contact.component';
|
||||
import SecurityComponent from 'src/app/settings/security/security.component';
|
||||
import { SettingsComponent } from 'src/app/settings/settings.component';
|
||||
|
||||
@Component({
|
||||
selector: '[app-settings-widget]',
|
||||
templateUrl: './settingsWidget.component.html',
|
||||
})
|
||||
export class SettingsWidgetComponent {
|
||||
constructor(private router: Router) {}
|
||||
|
||||
openRegistrarsPage() {
|
||||
this.navigate(RegistrarComponent.PATH);
|
||||
}
|
||||
|
||||
openSecurityPage() {
|
||||
this.navigate(SecurityComponent.PATH);
|
||||
}
|
||||
|
||||
openContactsPage() {
|
||||
this.navigate(ContactComponent.PATH);
|
||||
}
|
||||
|
||||
private navigate(route: string) {
|
||||
this.router.navigate([SettingsComponent.PATH, route]);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="console-app__widget">
|
||||
<div class="console-app__widget_left">
|
||||
<mat-icon class="console-app__widget-icon">list_alt</mat-icon>
|
||||
<h1 class="console-app__widget-title">TLDs</h1>
|
||||
<h4 class="secondary-text text-center">
|
||||
View launch phase information for all onboarded and available TLDs.
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,28 @@
|
||||
@if (isLoading) {
|
||||
<div class="console-app__registry-lock-verify-spinner">
|
||||
<mat-spinner />
|
||||
</div>
|
||||
} @else if (domainName) {
|
||||
<h1 class="mat-headline-4">Success!</h1>
|
||||
<div class="console-app__registry-lock-content">
|
||||
<div class="console-app__registry-lock-subhead">
|
||||
The domain {{ domainName }} has been successfully {{ action }}ed.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
class="text-l"
|
||||
routerLink="{{ DOMAIN_LIST_COMPONENT_PATH }}"
|
||||
[queryParams]="{ registrarId: this.registrarService.registrarId() }"
|
||||
>Return to the list of domains</a
|
||||
>
|
||||
</div>
|
||||
} @else {
|
||||
<h1 class="mat-headline-4">Failure</h1>
|
||||
<div class="console-app__registry-lock-content">
|
||||
<div class="console-app__registry-lock-subhead">
|
||||
An error occurred: {{ errorMessage }}.<br /><br />Please double-check the
|
||||
verification code and try again.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.console-app__registry-lock {
|
||||
&-content {
|
||||
margin-top: 30px;
|
||||
}
|
||||
&-subhead {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
66
console-webapp/src/app/lock/registryLockVerify.component.ts
Normal file
66
console-webapp/src/app/lock/registryLockVerify.component.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { RegistryLockVerifyService } from './registryLockVerify.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { take } from 'rxjs';
|
||||
import { DomainListComponent } from '../domains/domainList.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registry-lock-verify',
|
||||
templateUrl: './registryLockVerify.component.html',
|
||||
styleUrls: ['./registryLockVerify.component.scss'],
|
||||
providers: [RegistryLockVerifyService],
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistryLockVerifyComponent {
|
||||
public static PATH = 'registry-lock-verify';
|
||||
|
||||
readonly DOMAIN_LIST_COMPONENT_PATH = `/${DomainListComponent.PATH}`;
|
||||
|
||||
isLoading = true;
|
||||
domainName?: string;
|
||||
action?: string;
|
||||
errorMessage?: string;
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
protected registryLockVerifyService: RegistryLockVerifyService,
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParamMap.pipe(take(1)).subscribe((params: ParamMap) => {
|
||||
this.registryLockVerifyService
|
||||
.verifyRequest(params.get('lockVerificationCode') || '')
|
||||
.subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage = err.error;
|
||||
},
|
||||
next: (verificationResponse) => {
|
||||
this.domainName = verificationResponse.domainName;
|
||||
this.action = verificationResponse.action;
|
||||
this.registrarService.registrarId.set(
|
||||
verificationResponse.registrarId
|
||||
);
|
||||
this.isLoading = false;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,22 +12,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.persistence.converter;
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
|
||||
import javax.persistence.AttributeConverter;
|
||||
import javax.persistence.Converter;
|
||||
export interface RegistryLockVerificationResponse {
|
||||
action: string;
|
||||
domainName: string;
|
||||
registrarId: string;
|
||||
}
|
||||
|
||||
/** JPA {@link AttributeConverter} for storing/retrieving {@code Set<String>}. */
|
||||
@Converter(autoApply = true)
|
||||
public class StringSetConverter extends StringSetConverterBase<String> {
|
||||
@Injectable()
|
||||
export class RegistryLockVerifyService {
|
||||
constructor(private backendService: BackendService) {}
|
||||
|
||||
@Override
|
||||
String toString(String element) {
|
||||
return element;
|
||||
}
|
||||
|
||||
@Override
|
||||
String fromString(String value) {
|
||||
return value;
|
||||
verifyRequest(lockVerificationCode: string) {
|
||||
return this.backendService.verifyRegistryLockRequest(lockVerificationCode);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,16 +12,23 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { DialogModule } from '@angular/cdk/dialog';
|
||||
import { CdkMenuModule } from '@angular/cdk/menu';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { CdkTableModule } from '@angular/cdk/table';
|
||||
import { CdkTreeModule } from '@angular/cdk/tree';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatBadgeModule } from '@angular/material/badge';
|
||||
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatGridListModule } from '@angular/material/grid-list';
|
||||
@@ -29,23 +36,18 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatTreeModule } from '@angular/material/tree';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { CdkMenuModule } from '@angular/cdk/menu';
|
||||
import { DialogModule } from '@angular/cdk/dialog';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
|
||||
@NgModule({
|
||||
exports: [
|
||||
@@ -83,6 +85,8 @@ import { MatChipsModule } from '@angular/material/chips';
|
||||
MatSnackBarModule,
|
||||
MatPaginatorModule,
|
||||
MatChipsModule,
|
||||
MatAutocompleteModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class MaterialModule {}
|
||||
|
||||
53
console-webapp/src/app/navigation/navigation.component.html
Normal file
53
console-webapp/src/app/navigation/navigation.component.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<mat-tree
|
||||
[dataSource]="dataSource"
|
||||
[treeControl]="treeControl"
|
||||
class="console-app__nav-tree"
|
||||
>
|
||||
<mat-tree-node
|
||||
*matTreeNodeDef="let node"
|
||||
matTreeNodeToggle
|
||||
tabindex="0"
|
||||
(click)="onClick(node)"
|
||||
(keyup.enter)="onClick(node)"
|
||||
[class.active]="router.url.includes(node.path)"
|
||||
[elementId]="getElementId(node)"
|
||||
>
|
||||
@if (node.iconName) {
|
||||
<mat-icon class="console-app__nav-icon">
|
||||
{{ node.iconName }}
|
||||
</mat-icon>
|
||||
}
|
||||
{{ node.title }}
|
||||
</mat-tree-node>
|
||||
<mat-nested-tree-node
|
||||
*matTreeNodeDef="let node; when: hasChild"
|
||||
(click)="onClick(node)"
|
||||
tabindex="0"
|
||||
(keyup.enter)="onClick(node)"
|
||||
>
|
||||
<div class="mat-tree-node" [class.active]="router.url.includes(node.path)">
|
||||
<button
|
||||
class="console-app__nav-icon_expand"
|
||||
mat-icon-button
|
||||
matTreeNodeToggle
|
||||
[attr.aria-label]="'Toggle ' + node.title"
|
||||
>
|
||||
<mat-icon>
|
||||
{{ treeControl.isExpanded(node) ? "expand_more" : "chevron_right" }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
@if (node.iconName) {
|
||||
<mat-icon class="console-app__nav-icon">
|
||||
{{ node.iconName }}
|
||||
</mat-icon>
|
||||
}
|
||||
{{ node.title }}
|
||||
</div>
|
||||
<div
|
||||
[class.console-app__nav-tree_invisible]="!treeControl.isExpanded(node)"
|
||||
role="group"
|
||||
>
|
||||
<ng-container matTreeNodeOutlet></ng-container>
|
||||
</div>
|
||||
</mat-nested-tree-node>
|
||||
</mat-tree>
|
||||
71
console-webapp/src/app/navigation/navigation.component.scss
Normal file
71
console-webapp/src/app/navigation/navigation.component.scss
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
$expand-icon-size: 26px;
|
||||
|
||||
.console-app {
|
||||
&__sidebar {
|
||||
min-width: 300px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&__nav-icon {
|
||||
width: $expand-icon-size;
|
||||
height: $expand-icon-size;
|
||||
color: var(--secondary) !important;
|
||||
margin-right: $expand-icon-size;
|
||||
|
||||
&_expand {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
padding: 0px;
|
||||
width: $expand-icon-size;
|
||||
height: $expand-icon-size;
|
||||
}
|
||||
}
|
||||
|
||||
&__nav-tree {
|
||||
.mat-tree-node {
|
||||
cursor: pointer;
|
||||
padding-left: $expand-icon-size;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--light-highlight);
|
||||
border-radius: 0 15px 15px 0;
|
||||
}
|
||||
&.active {
|
||||
border-radius: 0 15px 15px 0;
|
||||
background-color: var(--lightest);
|
||||
}
|
||||
}
|
||||
|
||||
div[role="group"] > .mat-tree-node {
|
||||
// expand icon + regular icon + spacing = 3 * $expand-icon-size
|
||||
padding-left: calc($expand-icon-size * 3);
|
||||
}
|
||||
|
||||
ul,
|
||||
li {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
&_invisible {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
120
console-webapp/src/app/navigation/navigation.component.ts
Normal file
120
console-webapp/src/app/navigation/navigation.component.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NestedTreeControl } from '@angular/cdk/tree';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatTreeNestedDataSource } from '@angular/material/tree';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { RouteWithIcon, routes, PATHS } from '../app-routing.module';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
import { RegistrarComponent } from '../registrar/registrarsTable.component';
|
||||
|
||||
interface NavMenuNode extends RouteWithIcon {
|
||||
parentRoute?: RouteWithIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is responsible for rendering navigation menu based on the allowed routes
|
||||
* and keeping UI in sync when route changes (eg highlights selected route in the menu).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-navigation',
|
||||
templateUrl: './navigation.component.html',
|
||||
styleUrls: ['./navigation.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class NavigationComponent {
|
||||
renderRouter: boolean = true;
|
||||
treeControl = new NestedTreeControl<RouteWithIcon>((node) => node.children);
|
||||
dataSource = new MatTreeNestedDataSource<RouteWithIcon>();
|
||||
private subscription!: Subscription;
|
||||
|
||||
hasChild = (_: number, node: RouteWithIcon) =>
|
||||
!!node.children && node.children.length > 0;
|
||||
|
||||
constructor(protected router: Router) {
|
||||
this.dataSource.data = this.ngRoutesToNavMenuNodes(routes);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.subscription = this.router.events.subscribe((navigationParams) => {
|
||||
if (navigationParams instanceof NavigationEnd) {
|
||||
this.syncExpandedNavigationWithRoute(navigationParams.url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
getElementId(node: RouteWithIcon) {
|
||||
if (node.path === RegistrarComponent.PATH) {
|
||||
return RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT;
|
||||
} else if (node.path === PATHS.UsersComponent) {
|
||||
return RESTRICTED_ELEMENTS.USERS;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
syncExpandedNavigationWithRoute(url: string) {
|
||||
const maybeComponentWithChildren = this.dataSource.data.find((menuNode) => {
|
||||
return (
|
||||
// @ts-ignore - optional function added to components with children,
|
||||
// there's no availble tools to get current active router component
|
||||
typeof menuNode.component?.matchesUrl === 'function' &&
|
||||
// @ts-ignore
|
||||
menuNode.component?.matchesUrl(url)
|
||||
);
|
||||
});
|
||||
if (maybeComponentWithChildren) {
|
||||
this.treeControl.expand(maybeComponentWithChildren);
|
||||
}
|
||||
}
|
||||
|
||||
onClick(node: NavMenuNode) {
|
||||
if (node.parentRoute) {
|
||||
this.router.navigate([node.parentRoute.path + '/' + node.path], {
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
} else {
|
||||
this.router.navigate([node.path], { queryParamsHandling: 'merge' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We only want to use routes with titles and we want to provide easy reference to parent node
|
||||
*/
|
||||
ngRoutesToNavMenuNodes(routes: RouteWithIcon[]): NavMenuNode[] {
|
||||
return routes
|
||||
.filter((r) => r.title)
|
||||
.map((r) => {
|
||||
if (r.children) {
|
||||
return {
|
||||
...r,
|
||||
children: r.children
|
||||
.filter((r) => r.title)
|
||||
.map((childRoute) => {
|
||||
return {
|
||||
...childRoute,
|
||||
parentRoute: r,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
return r;
|
||||
});
|
||||
}
|
||||
}
|
||||
43
console-webapp/src/app/ote/newOte.component.html
Normal file
43
console-webapp/src/app/ote/newOte.component.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<h1 class="mat-headline-4">Generate OT&E Accounts</h1>
|
||||
<div class="console-app__new-ote">
|
||||
@if (oteCreateResponseFormatted()) {
|
||||
<h1>Generated Successfully</h1>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Epp Credentials</mat-card-title>
|
||||
<mat-card-subtitle
|
||||
>Copy and paste this into an email to the registrars</mat-card-subtitle
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>{{ oteCreateResponseFormatted() }}</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
} @else {
|
||||
<form (ngSubmit)="onSubmit()" [formGroup]="createOte">
|
||||
<p>
|
||||
<mat-form-field name="registrarId" appearance="outline">
|
||||
<mat-label>Base Registrar Id: </mat-label>
|
||||
<input matInput type="text" formControlName="registrarId" required />
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<p>
|
||||
<mat-form-field name="registrarEmail" appearance="outline">
|
||||
<mat-label>Contact Email: </mat-label>
|
||||
<input matInput type="text" formControlName="registrarEmail" required />
|
||||
<mat-hint
|
||||
>Will be granted web-console access to the OTE registrars.</mat-hint
|
||||
>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Submit new OT&E account"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
11
console-webapp/src/app/ote/newOte.component.scss
Normal file
11
console-webapp/src/app/ote/newOte.component.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.console-app__new-ote {
|
||||
max-width: 720px;
|
||||
mat-card-content {
|
||||
white-space: break-spaces;
|
||||
padding: 20px;
|
||||
}
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
}
|
||||
}
|
||||
82
console-webapp/src/app/ote/newOte.component.ts
Normal file
82
console-webapp/src/app/ote/newOte.component.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, computed, signal } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { SnackBarModule } from '../snackbar.module';
|
||||
|
||||
export interface OteCreateResponse extends Map<string, string> {
|
||||
password: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-ote',
|
||||
imports: [MaterialModule, SnackBarModule],
|
||||
templateUrl: './newOte.component.html',
|
||||
styleUrls: ['./newOte.component.scss'],
|
||||
})
|
||||
export class NewOteComponent {
|
||||
oteCreateResponse = signal<OteCreateResponse | undefined>(undefined);
|
||||
|
||||
readonly oteCreateResponseFormatted = computed(() => {
|
||||
const oteCreateResponse = this.oteCreateResponse();
|
||||
if (oteCreateResponse) {
|
||||
const { password } = oteCreateResponse;
|
||||
return Object.entries(oteCreateResponse)
|
||||
.filter((entry) => entry[0] !== 'password')
|
||||
.map(
|
||||
([login, tld]) =>
|
||||
`Login: ${login}\t\tPassword: ${password}\t\tTLD: ${tld}`
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
createOte = new FormGroup({
|
||||
registrarId: new FormControl('', [Validators.required]),
|
||||
registrarEmail: new FormControl('', [Validators.required]),
|
||||
});
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
onSubmit() {
|
||||
if (this.createOte.valid) {
|
||||
const { registrarId, registrarEmail } = this.createOte.value;
|
||||
this.registrarService
|
||||
.generateOte(
|
||||
{
|
||||
registrarId,
|
||||
registrarEmail,
|
||||
},
|
||||
registrarId || ''
|
||||
)
|
||||
.subscribe({
|
||||
next: (oteCreateResponse: OteCreateResponse) => {
|
||||
this.oteCreateResponse.set(oteCreateResponse);
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
31
console-webapp/src/app/ote/oteStatus.component.html
Normal file
31
console-webapp/src/app/ote/oteStatus.component.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<h1 class="mat-headline-4">OT&E Status Check</h1>
|
||||
@if(registrarId() === null) {
|
||||
<h1>Missing registrarId param</h1>
|
||||
} @else if(isOte()) { @if (oteStatusResponse().length) {
|
||||
<h1>
|
||||
Status:
|
||||
<span>{{ oteStatusUnfinished().length ? "Unfinished" : "Completed" }}</span>
|
||||
</h1>
|
||||
}
|
||||
<div class="console-app__ote-status">
|
||||
@if(oteStatusCompleted().length) {
|
||||
<div class="console-app__ote-status_completed">
|
||||
<h1>Completed</h1>
|
||||
@for (entry of oteStatusCompleted(); track entry) {
|
||||
<div><mat-icon>check_box</mat-icon>{{ entry.description }}</div>
|
||||
}
|
||||
</div>
|
||||
} @if(oteStatusUnfinished().length) {
|
||||
<div class="console-app__ote-status_unfinished">
|
||||
<h1>Unfinished</h1>
|
||||
@for (entry of oteStatusUnfinished(); track entry) {
|
||||
<div>
|
||||
<mat-icon>check_box_outline_blank</mat-icon>{{ entry.description }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<h1>Registrar {{ registrarId() }} is not an OT&E registrar</h1>
|
||||
}
|
||||
28
console-webapp/src/app/ote/oteStatus.component.scss
Normal file
28
console-webapp/src/app/ote/oteStatus.component.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.console-app__ote-status {
|
||||
max-width: 730px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
&_completed,
|
||||
&_unfinished {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 0 20px 30px 0;
|
||||
div {
|
||||
display: flex;
|
||||
min-width: 300px;
|
||||
align-items: flex-start;
|
||||
max-width: 300px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
min-width: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
console-webapp/src/app/ote/oteStatus.component.ts
Normal file
78
console-webapp/src/app/ote/oteStatus.component.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, computed, OnInit, signal } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { SnackBarModule } from '../snackbar.module';
|
||||
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
|
||||
export interface OteStatusResponse {
|
||||
description: string;
|
||||
requirement: number;
|
||||
timesPerformed: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-ote-status',
|
||||
imports: [MaterialModule, SnackBarModule],
|
||||
templateUrl: './oteStatus.component.html',
|
||||
styleUrls: ['./oteStatus.component.scss'],
|
||||
})
|
||||
export class OteStatusComponent implements OnInit {
|
||||
registrarId = signal<string | null>(null);
|
||||
oteStatusResponse = signal<OteStatusResponse[]>([]);
|
||||
|
||||
oteStatusCompleted = computed(() =>
|
||||
this.oteStatusResponse().filter((v) => v.completed)
|
||||
);
|
||||
oteStatusUnfinished = computed(() =>
|
||||
this.oteStatusResponse().filter((v) => !v.completed)
|
||||
);
|
||||
isOte = computed(
|
||||
() =>
|
||||
this.registrarService
|
||||
.registrars()
|
||||
.find((r) => r.registrarId === this.registrarId())
|
||||
?.type?.toLowerCase() === 'ote'
|
||||
);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
protected registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.paramMap.pipe(take(1)).subscribe((params: ParamMap) => {
|
||||
this.registrarId.set(params.get('registrarId'));
|
||||
const registrarId = this.registrarId();
|
||||
if (!registrarId) throw 'Missing registrarId param';
|
||||
|
||||
this.registrarService.oteStatus(registrarId).subscribe({
|
||||
next: (oteStatusResponse: OteStatusResponse[]) => {
|
||||
this.oteStatusResponse.set(oteStatusResponse);
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user