1
0
mirror of https://github.com/google/nomulus synced 2026-01-23 06:03:05 +00:00

Compare commits

..

209 Commits

Author SHA1 Message Date
gbrodman
54c5a9450d Add RegistrationBehavior.NONPREMIUM_CREATE (#2481)
When using this token (which must be tied to a particular domain), the
first year price (and only the first year price, i.e. the creation
price) will be the standard price for this TLD. Future years (i.e.
renewals) will continue at the normal premium price.
2024-06-26 20:01:32 +00:00
gbrodman
0f0097c15c Wait to load domain list until a registrar is selected (#2485)
This isn't the worst thing in the world but it does result in a bad
request to the server otherwise, and log/error spam. So, only load the
domains list if we have a registrar selected.
2024-06-25 18:39:53 +00:00
Ben McIlwain
c9437d8c72 Make registrant nullable on domains (#2477)
This is the first step in migrating to the minimum registration data set. Note
that our database model already permits null domain registrants, so this just
makes the code accept it as well. Note that I haven't changed any requirements
in EPP flows yet; a later step will be to check the migration schedule and then
not require the registrant to be present if in a suitable state.

This does potentially affect the output of WHOIS/RDAP, but that's a NOOP so long
as EPP commands and other tools continue to enforce the requirement of a
registrant.
2024-06-20 15:22:38 +00:00
Weimin Yu
19819444af Set upper bound of netty version (#2482)
A new alpha version is introduced and breaks our tests.
2024-06-17 19:43:36 +00:00
Pavlo Tkach
15df3aea44 Update Angular 17 -> 18 (#2479) 2024-06-14 23:09:34 +00:00
Weimin Yu
d000a5dff8 Use replica db for non-mutating epp flows (#2478)
* Use replica db for non-mutating epp flows

* Add a test
2024-06-13 23:18:56 +00:00
sarahcaseybot
34694b4aef Add the FeatureFlag entity (#2464)
* Add FeatureFlag entity

* Add converter

* Add loading cache

* Add more tests

* Fix NPE in cache

* small fixes
2024-06-12 16:44:08 +00:00
dependabot[bot]
7ce7b23450 Bump braces (#2476)
Bumps the npm_and_yarn group with 1 update in the /console-webapp directory: [braces](https://github.com/micromatch/braces).


Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-11 19:19:16 +00:00
Lai Jiang
a5d1469281 Upgrade to Gradle 8.8 (#2475) 2024-06-10 14:56:10 +00:00
Pavlo Tkach
a90a85afae Fix domain page "not found" layout issue (#2474) 2024-06-08 11:36:23 +00:00
Weimin Yu
6e68876a14 Use replica for whois/rdap (#2470) 2024-06-07 17:03:55 +00:00
Lai Jiang
11231703d5 Upgrade to jakarta mail (#2473) 2024-06-07 15:28:36 +00:00
gbrodman
b77a219e19 Move domain-list search+download outside of loading bar (#2457)
This means that they'll stick around even while we're loading domains
from the server.

https://b.corp.google.com/issues/343213150
2024-06-06 20:35:20 +00:00
Pavlo Tkach
bd8e6354b5 Add new registrar screen to the console (#2469) 2024-06-07 00:21:53 +00:00
Weimin Yu
361094f537 BSA check in DomainCheckFlow should check TLD (#2472)
Should not block labels if the tld is not enrolled with BSA.
2024-06-06 19:30:36 +00:00
sarahcaseybot
d53177e44c Add domain creates to the load testing client (#2458)
* Add domain creates to the load testing client

* Update contact create
2024-06-06 17:30:12 +00:00
sarahcaseybot
e73f646e1f Add FeatureFlag table to the database (#2463)
* Add FeatureFlag table to the database

* Change status to hstore
2024-06-06 17:17:11 +00:00
Lai Jiang
1a5dfb0ac2 Upgrade schemacrawler (#2471) 2024-06-06 14:51:13 +00:00
Lai Jiang
49cb1875d1 Upgrade dependencies (#2468) 2024-06-05 15:50:42 +00:00
gbrodman
61eee45ad0 Add renewalPrice fields to AllocationToken in SQL (#2462)
This is an optional field (will be required when the renewal price
behavior is SPECIFIED). This will allow us to set arbitrary renewal
prices for domains as part of one-off negotiations.

https://b.corp.google.com/issues/332928676
2024-06-03 19:50:58 +00:00
Weimin Yu
e99a18f54f Pass log trace_id to TimeLimiter task (#2466)
Code executed by TimeLimiter is in another thread. Pass on the log
trace_id if exists.
2024-06-03 19:38:17 +00:00
Pavlo Tkach
0c123e1676 Unify email notifications for console updates (#2459) 2024-05-31 19:20:56 +00:00
Lai Jiang
81b239c6b3 Add a presubmit test to catch accidental usage of javax.servlet (#2461) 2024-05-31 13:34:50 +00:00
Weimin Yu
ea8c34bf8b Fix Flyway Gradle tasks (#2460) 2024-05-30 19:23:29 +00:00
Weimin Yu
b3e67e58b5 Change billing for multi-year domain creation (#2446)
* Change billing for multi-year domain creation

From the second year on, charge the renewal price.

See b/322833077
2024-05-29 13:19:54 -04:00
gbrodman
589041b3ed Fully reset domain-list page on registrar change (#2456)
When the registrar changes we should reset the page and the total
results to 0 (since we haven't loaded them yet)

https://b.corp.google.com/issues/343193698
2024-05-29 12:54:01 -04:00
Lai Jiang
455364ff29 Remove GAE Users service API usage (#2414)
This is the last remaining GAE API that we depend on. By removing it, we are able to remove all common GAE dependencies as well.

To merge this PR, we need to create console User objects that have the same email address as the RegistrarPoc objects' login_email_address and copy over the existing registry lock hashes and salts.

We are also able to simply the code base by removing some redundant logic like AuthMethod (API is now the only supported one) and UserAuthInfo (console user is now the only supported one)

There are several behavioral changes that are worth noting:

The XsrfTokenManager now uses the console user's email address to mint and verify the token. Previously, only email addresses returned by the GAE Users service are used, whereas a blank email address will be used if the user is logged in as a console user. I believe this was an oversight that is now corrected.
The legacy console will return 401 when no user is logged in, instead of redirecting to the Users service login flow.
The logout URL in the legacy console is changed to use the IAP logout flow. It will clear the cookie and redirect the users to IAP login page (tested on QA).
The screenshot changes are mostly due to the console users lacking a display name and therefore showing the email address instead. Some changes are due to using the console user's email address as the registry lock email address, which is being fixed in Add DB column for separate rlock email address #2413 and its follow-up RPs.
2024-05-29 12:37:44 -04:00
Lai Jiang
d90bc1a3e4 Update db README (#2449) 2024-05-29 11:54:31 -04:00
Lai Jiang
0e3875c1ff Removing leading newline from GKE log messages (#2454)
GKE now displays log messages correctly. There is no need for an extra
leading newline, which now results in a useless blank line for each log
entry in Log Explorer.
2024-05-29 11:54:19 -04:00
Lai Jiang
3b565b96b7 Add the ability to add/remove console users from a Google Group (#2450)
# Conflicts:
#	config/presubmits.py
2024-05-28 17:00:37 +00:00
Pavlo Tkach
ec6c77927f Add console backend for editing registrar (#2452) 2024-05-28 00:53:32 +00:00
Lai Jiang
e88ff77ecb Harmonize http status code usage (#2451)
Given that we run servlets, it makes sense to always use the status
code contants from the servlet class.
2024-05-24 18:46:37 +00:00
sarahcaseybot
0781010b16 Create a load testing EPP client (#2415)
* Create a load testing EPP client.

This code is mostly based off of what was used for a past EPP load testing client that can be found in Google3 at https://source.corp.google.com/piper///depot/google3/experimental/users/jianglai/proxy/java/google/registry/proxy/client/

I modified the old client to be open-source friendly and use Gradle.

For now, this only performs a login and logout command, I will further expand on this in later PRs to add other EPP commands so that we can truly load test the system.

* Small changes

* Remove unnecessary build dep

* Add gradle build tasks

* Small fixes

* Add an instances setUp and cleanUp script

* More modifications to instance setup scripts

* change to ubuntu instance

* Add comment to make ssh work
2024-05-23 21:37:34 +00:00
Pavlo Tkach
ab4bac05d1 Replace RegistryTestServerMain start address with ipv6 loopback (#2448) 2024-05-21 19:00:25 +00:00
Pavlo Tkach
8e22ce7c70 Add phone and fax number to console whois endpoint (#2447) 2024-05-20 20:32:23 +00:00
Lai Jiang
d96a5547ce Store stack trace in a separate filed during logging (#2444)
For reasons unclear to me, if the stack trace is appended directly to
the message, the log entry will be lumped together with following logs
on GKE.

Also updated the GKE service account for Nomulus in the manifest so we
can use workload identity just for Nomulus, not other pods on the same
cluster.
2024-05-20 16:17:56 +00:00
Pavlo Tkach
05b43965d1 Fix console EPP password form, minor adjustments (#2445) 2024-05-17 18:44:44 +00:00
gbrodman
43000a5f80 Refactor common exception handling in ConsoleApiAction (#2443)
There are a bunch of cases where we want common exception handling and
it's annoying to have to deal with the common "set failed response and
make sure to return" a bunch of times.

This allows us to break up request methods more easily, since we can now
often throw exceptions that will break all the way back up to
ConsoleApiAction. Previously, any error handling had to exist in the
primary handler method so it could return.
2024-05-16 18:29:14 +00:00
Pavlo Tkach
a66b9ea749 Add OTE registrars to console /registrars response (#2440) 2024-05-16 14:49:32 +00:00
sarahcaseybot
36a660a8ad Remove createBillingCost columns from db schema (#2438) 2024-05-15 18:19:36 +00:00
gbrodman
d09bb4ff74 Refactor some registry lock verification code (#2434)
The user, on the front end, should not be required to provide whether or
not they're trying to verify a lock or an unlock. They should only need
the verification code. We can inspect the lock object itself (and the
domain in question) to see whether or not we're verifying a lock or an
unlock.
2024-05-14 16:56:32 +00:00
Lai Jiang
6ca3cc230f Make logging work correctly on Jetty (#2442) 2024-05-14 14:36:26 +00:00
Pavlo Tkach
6a5d8ed3b5 Allow console access for all not NONE-type global users (#2441) 2024-05-13 19:24:11 +00:00
Pavlo Tkach
53dcba1189 Update jetty's console build path (#2439) 2024-05-13 16:08:57 +00:00
Pavlo Tkach
3e77004274 Create gcp console service (#2433) 2024-05-10 21:29:58 +00:00
gbrodman
fd21fcdb84 Add a GetUserCommand (#2435)
This is fairly simple, similar to most of the other Get*Command classes
2024-05-10 21:25:52 +00:00
gbrodman
ae14e35df7 Change a few wording bits in the console (#2436)
These probably aren't perfect but they seemed to make sense given the
old console. Nothing major.
2024-05-10 18:27:55 +00:00
Lai Jiang
94dc9fd0d5 Update GcpJsonFormatter (#2437)
Use the correct JSON field to store the source location info so it can
be parsed by Stackdriver.
2024-05-10 16:47:52 +00:00
gbrodman
7b34659a8f Add registryLockEmailAddress field to User object (#2418)
We've added the field in the database in a previous PR. This is only
used in the old console for now because the new console does not have
registry lock functionality yet
2024-05-09 21:42:45 +00:00
sarahcaseybot
808432e709 Remove the createBillingCost field from Tld (#2425)
* Remove the createBillingCost field from Tld

* fix spacing

* Change field name of map

* Rename getter

* Fix formatting

* Fix todo

* unchange column name
2024-05-08 18:14:03 +00:00
Lai Jiang
73d3b76a89 Remove more usage of AutoValue (#2432)
This PR also removes `SerializedForm` used to serialize
`PendingDeposit`, as it is now a simple record.
2024-05-08 00:50:01 +00:00
Weimin Yu
ca072b4861 Add log traces to Nomulus service on GKE (#2427)
* Add log traces to Nomulus service on GKE

Add request-scope log traces to Nomulus on GKE which, unlike
AppEngine and Cloud Run etc, does not generate traces for hosted
applications. This change only affects the GKE image. It does not affect
the AppEngine services.

Log traces are added to Nomulus-generated logs in request-processing
threads. Forked threads are not covered yet. The single relevant use
case (TimeLimiter) will be addressed in a followup PR.

The main change is in the logging configuration:

*  Use gcp-cloud-logging's LoggingHandler

*  Add gcp-cloud-logging's TraceLoggingEnhancer to the handler.

*  Set a thread-local trace id through the TraceLoggingEnhancer in
   ServletBase on request's entry and clear it on completion.

Also removed an unused class (`RequestLogId`).

* CR

* CR
2024-05-07 19:15:46 +00:00
Pavlo Tkach
54c896cbb9 Add console epp password integration (#2426) 2024-05-06 18:32:54 +00:00
Pavlo Tkach
2c7bf2cfdb Update cloudbuild-release.yaml with nokeycheck option (#2431) 2024-05-06 18:01:31 +00:00
gbrodman
49d2e34e12 Add a separate RegistryLock action for the console (#2411)
This handles both GET and POST requests. For POST requests it doesn't
actually change anything about the domains because we will need to add a
verification action (this will be done in a future PR).
2024-05-03 22:37:22 +00:00
Weimin Yu
5511b41f93 Avoid contention over the RefreshDnsRequest table (#2428)
* Avoid contention over the RefreshDnsRequest table

This table can be small at times, when PSQL may use table scan in
queries by keys. At the SERIALIZABLE isolation level, updates to
unrelated rows may conflict due to this `optimization`.

Lower the isolation level to repeatable read.

* Code review
2024-05-03 20:31:54 +00:00
gbrodman
147cdff555 Add registry lock email address col to UserUpdateHistory (#2424) 2024-05-02 15:51:48 +00:00
Ben McIlwain
4b6ade0b14 Bring codebase up to more recent Java standards (#2422)
This includes using the new switch format (though IntelliJ does not yet
understand patterns including default so those aren't used), multiline strings,
replacing some unnecessary type declarations with <>, converting some classes to
records, replacing some Guava predicates with native Java code, and some other
miscellaneous Code Inspection fixes.
2024-05-01 20:48:38 +00:00
Pavlo Tkach
570618705e Allow console access for FTE globar role (#2419) 2024-05-01 16:19:29 +00:00
sarahcaseybot
e791608098 Add more indexes to speed up deleteProberDataAction (#2423)
This adds an index on transfer_billing_cancellation_id to Domain and superordinate_domain to Host. When tested on crash with the action limited to only delete 10,000 domains, before these indexes were added the action took about 2 hours to delete 10,000 domains. Once these indexes were added, the action was able to delete the 10,000 domains in a little under 2 minutes.
2024-05-01 15:44:08 +00:00
gbrodman
03b358726a Add Java classes for console history objects (#2350)
This also creates base classes for the objects contained within the
history classes, e.g. RegistrarBase. This is the same way that objects
stored in the HistoryEntry subclasses have base classes, e.g.
DomainBase.
2024-04-30 20:42:40 +00:00
gbrodman
d121f8f547 Generate fake XSRF token in FakeConsoleApiParams for tests (#2421) 2024-04-30 17:47:53 +00:00
gbrodman
b27218d799 Fix a couple Checkstyle warnings (#2420) 2024-04-29 20:08:53 +00:00
Pavlo Tkach
e78ce42dd5 Add console DUM download (#2402)
* Add console DUM download

* Add console DUM download
2024-04-26 15:56:50 +00:00
Ben McIlwain
55fade497d Convert a bunch more @AutoValues to records (#2412) 2024-04-25 16:59:31 +00:00
gbrodman
e7501b621a Add DB column for separate rlock email address (#2413)
We cannot rely on the user checking their login email, so we'll want to
send the emails to the other address if configured. This is already the
case in RegistrarPoc.
2024-04-25 15:38:57 +00:00
Weimin Yu
9c443bede1 Fix conflicts between locks (#2407)
Use REPEATABLE READ for lock acquire/release operation to avoid conlicts
between locks.

Postgresql uses table scan on small tables, causing false sharing at
the SERIALIZABLE isolation level.

See b/333537928 for details.
2024-04-24 18:51:18 +00:00
Lai Jiang
6d0a746b76 Bind console users to the appropriate IAP roles upon creation (#2403)
Console users need IAP to inject the necessary OIDC tokens into their
request headers and therefore need to be bound to appropriate roles. Note
that in environments managed by latchkey, the bindings will need to be
present in latchkey config files as well, otherwise the changes made by
the nomulus tool will be reverted.

TESTED=ran the nomulus command against alpha and verified that the
bindings are created/removed upon console user creation/deletion.
2024-04-24 15:03:43 +00:00
Pavlo Tkach
0765e7b209 Console deps update (#2409)
* Update angular/core to 17.3.5

* Update angular/material 17.3.5

* Update angular/cli 17.3.5

* Update angular-eslint 17.3.0

* Disable cli cache

* General console deps update
2024-04-23 19:38:32 +00:00
sarahcaseybot
f729802094 Make createBillingCostTransitions not null (#2405)
* Make createBillingCostTransitions not null

* Set up createBillingCost field to be removed form config files

* Add clarifying comment
2024-04-23 18:22:45 +00:00
Ben McIlwain
e809e967a3 Convert more @AutoValues to records, particularly in custom flow classes (#2408) 2024-04-22 20:25:33 +00:00
Pavlo Tkach
4de2bd5901 Add console backend for EPP password change (#2396) 2024-04-20 10:44:26 +00:00
sarahcaseybot
b5629ff16f Run deleteProberData cron job daily (#2406)
* Run deleteProberData cron job daily

* Sign the commits

* try signing again
2024-04-19 19:32:14 +00:00
Ben McIlwain
91615aef54 Handle bad header names in registrar sheet syncing action (#2404)
The existing behavior was to ignore bad header names, in a way that was
counter-intuitive as a user of the Google Sheet. If a header name was bad (which
could just be someone accidentally changing it not realizing it needs to
correspond exactly to the name of the field on the Java object), then all of the
data in that column was just silently left as-is and never updated. This led to
gradually worsening sync and offset shift errors over time.

Now, it will write out an error message into every single cell in the bad
column, so it's clear that the column name is wrong and does not correspond to any
actual data in the DB.

BUG=http://b/332336068
2024-04-19 17:59:58 +00:00
Ben McIlwain
fa6898167b Convert more @AutoValues to Java records (#2378) 2024-04-17 19:30:23 +00:00
Lai Jiang
903b7979de Upgrade to jline 3 (#2400)
jline 3 contains API breaking changes, necessitating changes in
ShellCommand.
2024-04-12 19:57:02 +00:00
Weimin Yu
8721085d14 Fix BSA validation (#2401)
Unblocked reserved names wrongly reported as missing unblockable domain.
2024-04-12 19:54:59 +00:00
Lai Jiang
e434528cd3 Add nomulus deployment and service manifests (#2389) 2024-04-11 19:01:09 +00:00
Pavlo Tkach
9ca54e4364 Add UI for EPP Password update (#2393) 2024-04-10 22:29:52 +00:00
Weimin Yu
a16794e2af Run BSA Validate without lock (#2399)
As a read-only action that tolerates staleness, locking is unnecessary.
This should help with the lock contention we are observing.

Also reduces the number of VM instances provisioned for BSA and increase
the idle timeout. This should reduce invocation delay. Longer delay may
cause AppEngine to return `Timeout` status to Cloud Scheduler even
though the cron job succeeds.
2024-04-10 19:58:24 +00:00
Lai Jiang
496a781572 Upgrade jcommander (#2398) 2024-04-10 17:34:11 +00:00
Ben McIlwain
2df583df1a Statically import Truth.assertThat() in tests (#2395)
This also involved breaking out an improperly done assertThat() helper overload
method for JsonObjects into a proper Subject that doesn't further overload
assertThat().
2024-04-09 16:27:26 +00:00
sarahcaseybot
4f1ca920a7 Use the createBillingCostTransitions map to get the create cost for a domain (#2390)
* Use the createBillingCostTransitions map to get the create cost for a domain

* Add comment

* Add some TODOs

* use streams to check currency unit
2024-04-05 21:27:55 +00:00
Weimin Yu
96e33f5b4f Check for missing BSA unblockable domains (#2394)
* Check for missing BSA unblockable domains

All unblockable domains created before the last refresh run should be
reported as unblockable (registered).

All reserved domains that are not registered should be reported as
unblockable (reserved). Note that transient errors may be reported for
newly added reserved domains since we do not maintain update time for
when a reserved label is associated with a TLD. However, this scenario
is extremely rare in operations.

* Addressing review
2024-04-03 00:44:05 +00:00
sarahcaseybot
dff2d90325 Add batching to DeleteProberDataAction (#2322)
* Add batching to DeleteProberDataAction

* Only get time once

* Add separate query for dry run

* Update querries to actually properly delete all the data

* Fix merge conflicts

* Add test for foreign key constraints

* Make transaction repeatable read

* Make queries to subtables native

* Add native query for GracePeriodHistory

* Kill job after 20 hours

* remove extra time check from read query
2024-03-29 20:51:19 +00:00
sarahcaseybot
fa344776a1 Drop the should_publish column from ReservedList (#2392) 2024-03-29 16:21:11 +00:00
Pavlo Tkach
eb164809de Add console favicon (#2391) 2024-03-27 20:32:01 +00:00
Pavlo Tkach
4ddbeb6d06 Add console registrar field focus handler, split whois address field (#2388) 2024-03-27 18:55:57 +00:00
dependabot[bot]
fa53391395 Bump express from 4.19.1 to 4.19.2 in /console-webapp (#2387)
Bumps [express](https://github.com/expressjs/express) from 4.19.1 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.1...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-26 17:28:43 +00:00
sarahcaseybot
856e70cf8e Add indexes on domainRepoId to DomainHistoryHost and PollMessage (#2380)
* Add index for domainRepoId to PollMessage and DomainHistoryHost

* Add flyway fix for Concurrent

* fix gradle.properties

* Modify lockfiles

* Update the release tool and add IF NOT EXISTS

* Test removing transactional lock from deploy script

* Add transactional lock flag to actual flyway commands in script

* Remove flag from info command

* Add configuration for integration test
2024-03-26 16:44:14 +00:00
Lai Jiang
2037611931 Upgrade to Gradle 8.7 (#2386) 2024-03-25 23:57:40 +00:00
Lai Jiang
af1f6e5708 Compile to Java 21 bytecode (#2374)
We have been running in Java 21 runtime for a couple of weeks and every
works as expected.
2024-03-25 13:50:39 +00:00
Weimin Yu
0df8372407 Change BSA job status notifications (#2385)
Add error notifications for BsaDownload.

Stop sending success notifications.
2024-03-22 19:27:25 +00:00
Pavlo Tkach
59f4129ee0 Restyle registrar console based on the new design proposal (#2336) 2024-03-21 22:05:09 +00:00
Weimin Yu
de3af34b66 Verify unblockables are truly unblockable (#2381)
* Verify unblockables are truly unblockable

Unblockable domains may become blockable due to deregistration or
removal from the reserved list. The BSA refresh job is responsible
for removing them from the database. This PR verifies that the refreshes
are correct.

Note that recent changes since last refresh are not reflected in the
result, and inconsistency due to recent deregistrations are ignored.
Changes in reserved status or IDN validity are not timestamped,
therefore we cannot ignore recent inconsistencies. However, these
changes are rare.

* Addressing code review

* Addressing code review
2024-03-20 18:52:17 +00:00
Weimin Yu
5c62dc78ba Fix nom_build command (#2383) 2024-03-20 13:12:59 +00:00
Lai Jiang
3fdecde6e9 Add a method to test if Nomulus is running on Jetty (#2382) 2024-03-19 15:43:52 +00:00
Lai Jiang
e7bf74d91d Upgrade to Jakarta EE 10 Servlet (#2362)
Upgrade to using Jakarta EE 10 from Java EE 8 by mostly following the upgrade instructions. Only the servlet package is upgrade. Other Jakarta EE components (like the persistence package that Hibernate depends on) need to be upgraded separately.

TESTED=deployed and successfully communicated with the pubapi endpoint for web WHOIS.

Note that this currently requires packaing the App Engine runtime per instructions here due to GoogleCloudPlatform/appengine-java-standard#98. This PR will only be merged until the fix is deployed to production (https://rapid.corp.google.com/#/release/serverless_runtimes_run_java/java21_20240310_21_0).
2024-03-18 18:00:55 +00:00
Lai Jiang
ff211fb4f9 Remove buildSrc (#2379)
We don't use the upload results feature (kokoro picks the results
artifacts directly and uploads them).

Keeping it around is a maintenance burden.

Also fixed a deprecation warning.
2024-03-18 14:29:51 +00:00
dependabot[bot]
3a7c53d895 Bump follow-redirects from 1.15.4 to 1.15.6 in /docs/console-endpoints (#2375)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-15 19:06:26 -04:00
Weimin Yu
f5b279a288 Add IDN check to BSA validate (#2370)
Labels that are not in any supported IDN are not added to the database.

Remove such labels from those loaded from the block list files before
comparing with DB.
2024-03-15 19:06:12 -04:00
Lai Jiang
c68583f666 Remove java.util.Date (#2373)
There is one remaining instance in JpaTransactionManagerImpl that cannot
be removed because DetachingTypedQuery is implementing TypedQuery, which has
a method that expectred java.util.Date.
2024-03-15 19:06:00 -04:00
Pavlo Tkach
6d2eb2e140 Update build.gradle (#2377)
Console tests fail for the files that are affected by redesign. There's no point in fixing it here. I will reenable the task after the console redesign PR is merged
2024-03-15 17:23:02 +00:00
dependabot[bot]
00a2022292 Bump follow-redirects from 1.15.5 to 1.15.6 in /console-webapp (#2376)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-15 14:21:46 +00:00
Lai Jiang
de372c4d47 Replace javax.inject with jakarta.inject (#2372)
Note that Dagger currently doesn't work with the Jakarta namespace and
we have to cap the jakarta inject package version below 2.0 so that it
sill provides classes in the old namespace.
2024-03-15 03:26:53 +00:00
sarahcaseybot
5f9c7de516 Drop should publish field from ReservedList entity (#2369)
* Remove the shouldPublish field from the reservedLIst entity

* Add generated schema file
2024-03-14 22:04:18 +00:00
Lai Jiang
6e57d93507 Upgrade a few more dependencies (#2371)
* jaxb and gmail

* admin-directory

* Upgrade a few more dependencies
2024-03-14 17:37:54 +00:00
Lai Jiang
b9cfa65546 Refactor some code related to the transaction manager (#2366)
Removed the deprecation mark as it is natural to expose methods related
to a transaction like getting the entity manager or checking if one is
in a transaction through the transaction manager interface.
2024-03-14 14:37:44 +00:00
Weimin Yu
9af006836c Add email notification of BSA job status (#2368) 2024-03-13 19:14:02 +00:00
Lai Jiang
cd95be4776 Upgrade a few pinned dependencies (#2359) 2024-03-13 11:52:34 +00:00
Lai Jiang
bdc9a1fd1d Fix nomulus tool when the environment is localhost (#2365)
Also only caches/resets the original TM when in unit tests (TBT I'm not so sure
that even this is necessary as we don't seem to call the tool from tests
that often. There is only ShellCommandTest that calls the run() function
in RegistryCli and we could just put these tests in fragileTest and make
them run sequentially and fork every time to get around issue with
inference).

The issue with caching is that it tries to first create the to-be-cached
TM, and when the environment given is prod/sandbox/... It will try to
retrieve SQL credentials from prod/sandbox/... secret manager. This
works fine locally as we all have access to prod/sandbox/..., but fails
in Cloud Build jobs such as sync-db-objects where it provides it own
credential that has direct SQL access, but not access to
prod/sandbox/... secret manager.

TESTED=ran `./gradlew devTool --args="-e localhost generate_sql_er_diagram -o ../db/src/main/resources/sql/er_diagram"`
2024-03-13 04:49:07 +00:00
Lai Jiang
d0b036227a Add a GitHub action to block merging based on the labels (#2367) 2024-03-13 02:47:37 +00:00
gbrodman
0f02858965 Rename console update mod time to history_modification_time (#2363) 2024-03-12 20:38:15 +00:00
Lai Jiang
6acb14c60d Add a test to ensure all actions are routable by the RegistryServlet (#2361) 2024-03-12 17:18:44 +00:00
Lai Jiang
e881f254f8 Add a GitHub Action (#2360)
* Add a GitHub Action workflow

This allows us to create Gradle depedency graphs for Dependabot analysis (as the ones we already get for Javascript dependencies).

* Update Java version

* Add build scan

* codeql 3

* run with gradle

* exclude jIFC

* build scan

* Finalize
2024-03-11 18:55:13 +00:00
Lai Jiang
1fb27fcf8e Make nomulus work locally (#2349)
Chose the default transaction manager based on RegistryEnvironment. This
makes it possible to run nomulus on Jetty locally. Tested with the
following:

```bash
./gradle :jetty:run -Penvironment=alpha
curl http://localhost:8080/beta.app
```

The docker image is also updated to take an argument that specifies the
environment. It runs locally as well but the container doesn't get
access to locally stored credentials, so it fails to initialize the
transaction manager.
2024-03-11 16:05:44 +00:00
Weimin Yu
34a8a94083 Add BSA validation job (#2356)
* Add BSA validation job

Add the BsaValidateAction class with a first check (for inconsistency
between downloaded and persisted labels).

* Addressing comments

* Addressing reviews
2024-03-08 22:08:09 +00:00
Lai Jiang
779dc36858 Remove some dead gradle code (#2358)
runtime is not actually a predefined confiugration and it doesn't affect
the runtime classpath in anyway.

See: https://docs.gradle.org/current/userguide/java_plugin.html#tab:configurations
2024-03-08 15:12:11 +00:00
Lai Jiang
40174b825a Change ICANN upload cursor time (#2346)
The staging job runs at 9AM on the 2nd day of each month, we should set
the cursor to be after that time, otherwise we attempt to upload reports
on the 1st day of each month before they are ready, causing an error
email to be sent to us.
2024-03-07 15:52:14 +00:00
Weimin Yu
df4e345961 Remove appengine-based email client (#2354)
Remove email classes that depend on AppEngine API. They have been
replaced by the gmail-based client.

Remove `EmailMessage.from` method, which is no longer used.
There is a fixed sender address for the entire domain, and is
set by the gmail client.

The configs remain to be cleaned up. There is a bug (b/279671974) that
tracks it.
2024-03-07 03:26:12 +00:00
Lai Jiang
1cac9c9684 Make Kythe work with JDK 21 (#2355) 2024-03-06 20:23:53 +00:00
Lai Jiang
11883812b3 Update runtime to Java 21 (#2353)
This PR makes the runtime of most of our workload Java 21.

1. App Engine. Java 21 is in GA and it supports Java EE 8. I had to add
   an environmental variable so that we don't get an
   AppEngineCredentails by default (we have been using
   ComputeEngineCredentials for a couple of years). The uprade to Java
   21 runtime changed a system property that controls how jetty logging
   works, which also control if AppEngineCredential is return. Tested by
   deploying to alpha.
2. Proxy base image upgradedd to Java 21 (distroless still doesn't
   support Java 21 and it looks like Temurin is the way to go
   b/306728455). Tested by deploying to alpha.
3. Nomulus tool image upgrade to Temurin 21 as well. Tested locally.
4. Beam pipeline base image upgrade to Java 21. The JAVA21 flag is not
   supported by gcloud yet, but specifying the image URL directly works
   (and is supported). Tested by running in alpha.
5. Jetty base image upgraded to Java 21. Tested locally.
2024-03-06 15:10:11 +00:00
Lai Jiang
742481932e Upgrade builder base image (#2352)
This allows us to install Java 21 in the image.
2024-03-05 23:17:13 +00:00
Lai Jiang
37e4607c91 Temporarily revert builder to Java 17 (#2351)
Debian 11 repo does not have Java 21. Revert to Java 17 for now so we
can build the builder image, which is needed for release.
2024-03-05 21:30:23 +00:00
Lai Jiang
c896c022a6 Remove caps/pins on some dependencies (#2348)
Also re-organized the dependencies.gradle file.

Not all caps/pins are removed at this point, but I think this is enough
change for one PR.
2024-03-05 17:00:40 +00:00
Lai Jiang
805a34be96 Fix gax and gax-grpc dependency mismatch (#2347)
Also address a deprecation warning.

TESTED=build the nomulus tool locally and it run the `list_cursors`
command correctly. It used to fail.
2024-03-04 23:39:42 +00:00
Lai Jiang
dcf0412f11 Compile Nomulus with Java 21 (#2344)
Make the necessary changes for the code base to compile with JDK 21.

Other changes:

1. Upgraded testcontainer version and the SQL image version (to be the
   same as what we use in Cloud SQL). This led to some schema changes and
   also changed the order of results in some test queries (for the
   better I think, as the new order appears to be alphabetical).
2. Remove dependency on Truth8, which is deprecated.
3. Enable parallel Gradle task execution and greatly increased the
   number of parallel tests in standardTest. Removed outcastTest.
2024-03-04 19:31:08 +00:00
Lai Jiang
fbe0f4e0f2 Do not use shaded dependencies from testcontainers (#2343)
Also fixed a flaky test where it depends on the current date. On a leap
day, now + 1 year - 1 year results in 2/28 instead of 2/29.
2024-02-29 19:02:02 +00:00
Lai Jiang
d1f678bba7 Set up a unified registry servlet for Jetty (#2338)
This PR creates a unified RegistryServlet that will serve all
non-console traffic. It also creates a jetty subproject that allows one
to run Nomulus on top of a standard Jetty 12 runtime.

`./gradlew :jetty:stage` will create a jetty base folder at
`jetty/build/jetty-base` where one is able spin up a local Nomulus server
by running the following command inside the folder:

```bash
java -jar ${JETTY_HOME}/start.jar
```

`JETTY_HOME` is a folder where the [Jetty runtime](https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/12.0.6/jetty-home-12.0.6.zip) is located.

This PR also adds a Gradle task to create a Nomulus image based on the
official Jetty image:

```bash
./gradlew :jetty:buildNomulusImage
```
2024-02-29 17:37:51 +00:00
gbrodman
78c7d44546 Add SQL code for ConsoleUpdateHistory subclasses (#2337)
https://github.com/google/nomulus/pull/2330/ has an example of what this
will look like in Java
2024-02-29 17:05:45 +00:00
Lai Jiang
af2a7540d9 Upgrade to Gradle 8.6 (#2340) 2024-02-28 16:39:38 -05:00
Lai Jiang
f82e8e006d Use Java 17 feature (#2341)
This was somehow missed in #2333

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/2341)
<!-- Reviewable:end -->
2024-02-28 16:39:31 -05:00
Lai Jiang
bf877f469c Revert "Include a better error message to debug nomulus tool not working (#2275)" (#2342)
This reverts commit 64f5971275.

The catch block is too broad and most of the times the errors caught is
because `command.run()` failed and it had nothing to do with getting
the transaction manager. The `runCommand` method is already wrapped in a try
block that checks for `LoginRequiredException` and gives the appropriate
error message.

We need to re-assess the situation when the next time we encounter a
login issue that did not trigger `LoginRequiredException`. A blanket try
catch block is not the solution and only makes the situation more
confusing.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/2342)
<!-- Reviewable:end -->
2024-02-28 16:39:15 -05:00
sarahcaseybot
02fd6d4756 Add a check so newly saved createCostTransitions get recognized and saved to the database (#2335)
* Add a check so newly saved createCostTransitions get recognized and saved to the database

* Fix equals check

* Rename equals method

* Add comment explaining need for createBillingCostTransitionEqualCheck
2024-02-28 19:21:58 +00:00
sarahcaseybot
a4bd85068b Remove use of shouldPublishField from ReservedList (#2324)
* Remove use of shouldPublishField from ReservedList

* Remove from tests

* Update test comment

* Fix indentation

* fix test comment

* Fix test

* fix test

* Make shouldPublish column nullable
2024-02-27 20:39:58 +00:00
Pavlo Tkach
15368ee1c6 Console webapp dependencies update (#2339) 2024-02-27 19:07:43 +00:00
dependabot[bot]
f13fda2c15 Bump ip from 2.0.0 to 2.0.1 in /console-webapp (#2331)
Bumps [ip](https://github.com/indutny/node-ip) from 2.0.0 to 2.0.1.
- [Commits](https://github.com/indutny/node-ip/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 23:03:36 +00:00
Lai Jiang
f72a0d2f16 Remove SHA256 as a supported password hashing algorithm (#2310)
We introduced Scrypt as the default password hashing algorithm in
November 2023 and have been auto-converting saved hashes whenever a
successful EPP login or registry lock/unlock request is processed.

We will send comms to registrars to inform them the upcoming removal of
SHA256 support and urge them to log in at least once before the change.
Otherwise, they will need to contact support to reset the password out of
band after the change.

This PR will NOT be submitted until comms are out and the effective date
is immediate.

Co-authored-by: Weimin Yu <weiminyu@google.com>
2024-02-26 15:28:12 +00:00
Ben McIlwain
1eef260da9 Convert some more @AutoValues to records (#2334) 2024-02-23 18:56:40 +00:00
Lai Jiang
9d0ff74377 Re-enable Java 17 features (#2333) 2024-02-21 20:04:07 +00:00
Ben McIlwain
7a301edab7 Make transaction isolation level the first argument to transact() (#2329)
This makes the callsites look neater, as the work to execute itself is often a
many line lambda, whereas the transaction isolation level is not more than a
couple dozen characters.
2024-02-17 00:07:48 +00:00
Lai Jiang
08bcf579a5 Remove Duplicate billing events from the invoicing pipeline (#2326)
The Distinct transform removes duplicates based on the serialized format
of the elements. By providing a deterministic coder, we can guarantee
that no duplicates exist.
2024-02-16 20:43:40 +00:00
Lai Jiang
7d2330c943 Update beam pipeline base Java version to Java 17 (#2328) 2024-02-16 17:57:14 +00:00
Ben McIlwain
670941bec8 Convert a couple of @AutoValue classes to Java 15 Records (#2327)
This is the start of a long and low priority migration, but for now I wanted to do a couple of them just to see what it looks like.

This also demonstrates the pattern for use of an @AutoBuilder to replace an @AutoValue.Builder. See https://github.com/google/auto/blob/main/value/userguide/records.md#builders for full details on that.
2024-02-16 16:14:24 +00:00
Ben McIlwain
1f516e34b6 Add some shortcut flags to update allocation tokens command (#2321) 2024-02-15 23:25:14 +00:00
Ben McIlwain
70942c87d1 Change !Optional.isPresent() to Optional.isEmpty() (#2325)
Also uses the new Optional.stream() in one class.

Thank you Java 17!
2024-02-15 17:55:09 +00:00
Lai Jiang
406059db72 Use standard JVM shutdown hook (#2323)
This removes a dependency on the App Engine SDK. It also looks like
(from the logs at least) that shutdown hooks registered the old  way stopped
working after the runtime is upgraded to Java 17.

Also removed some random leftover dependencies on the App Engine SKD
that are not needed any more.
2024-02-14 21:36:25 +00:00
sarahcaseybot
abc1a0ef3d Add java changes for createBillingCostTransitions (#2314)
* Add java changes for createBillingCostTransitions

* Add negative cost test

* Remove default value

* remove unused variable

* Add check that create cost and trnasitions map are the same

* inject clock, only use key set when checking for missing fields

* Add test for removing map
2024-02-09 17:08:51 +00:00
Weimin Yu
7b47ecb1f1 Add REGISTER_BSA allocation type (#2319)
* Add ALLOW_BSA allocation type

Add a new type to allow creation of domains blocked by BSA.
Except for the BSA semantics, the new type behaves exactly
like SINGLE_USE.

* Addressing reviews

* Addressing review
2024-02-08 21:45:13 +00:00
Ben McIlwain
469d62703a Fix the test class name for UpdateRecurrenceCommand (#2320)
It looks like the command was renamed at some point to be shorter but then the test class itself was forgotten.
2024-02-08 19:34:18 +00:00
Lai Jiang
009fda67b7 Do not retry transactions inside Beam (#2318) 2024-02-05 18:40:56 +00:00
sarahcaseybot
e492936cec Add check for build_environment flag in updateReservedListCommand and updatePremiumListCommand (#2317)
* Add check for build_environment flag in updateReservedListCommand

* Do the same for premium list
2024-02-02 16:43:45 -05:00
Weimin Yu
d1d59c1afd Add a reminder for BEAM at Java version declaration (#2316) 2024-02-02 12:05:10 -05:00
Weimin Yu
7b786eaf1f Update dataflow java runtime to 17 (#2315) 2024-02-01 15:37:21 -05:00
Pavlo Tkach
45c5d12743 Add angular signals and computed to the console (#2308) 2024-02-01 14:15:05 -05:00
sarahcaseybot
73ab95bd9d Add Cloud Build sync job for reserved and premium lists (#2302)
* Change tld-update to db-object-updater

* rename sync_tlds.sh to sync_db_objects.sh

* Change to configured command name

* Change environment to sandbox explicitly for testing on alpha

* Add remaining object steps and change cloudbuild-tld-sync to cloudbuild-sync-db-objects

* Add build_environment flag

* Change order of command and directory

* Uncomment out reserved list part
2024-01-31 14:50:54 -05:00
Weimin Yu
f85cf57e36 Reduce query batch size for BSA unavailables (#2313)
Query size is borderline too-large for the replica.

At 50000, about 2 out of 30 took more than 30 seconds and were retried.
Lower to 40000 and we will keep monitoring the executions.
2024-01-30 13:18:41 -05:00
Ben McIlwain
5e36cf30c3 Don't override existing registrar email address when setting referral email (#2300)
The fallback should only apply on creates, not on updates, otherwise it can
override an existing value for the email address when only the referral email
should be what's updated.

This fixes a bug introduced back in commit in 0ead4f8d9d.

BUG= http://b/322026165
2024-01-30 18:31:54 +01:00
sarahcaseybot
829be0777b Add a createBillingCostTransitions column to TLD (#2312) 2024-01-29 18:06:02 -05:00
Lai Jiang
c0ac9bdba4 Compile to Java 17 bytecode (#2304)
Also fix a linter warning.
2024-01-25 18:29:07 -05:00
Weimin Yu
58ec0f826d Stop saving BSA empty refresh changes (#2307)
* Stop saving BSA empty refresh changes

We thought that as a way to verify the refresh job to be running, browsing
the GCS bucket with empty files is easier than quering the DB or go to GCP
logging dashboard, but there are too many of them to be useful.
2024-01-25 16:02:04 -05:00
Pavlo Tkach
f9e0908022 Replace invoice email attachement with bucket link (#2299) 2024-01-25 14:08:08 -05:00
sarahcaseybot
b21e1a1935 Add required --build_environment flag to tld-sync Cloud Build job (#2306) 2024-01-25 12:27:05 -05:00
Lai Jiang
0112b3ae06 Make the formatting tasks work with Java 17 (take 2) (#2305)
We should not assume org.gradle.java.home to exist on kokoro or GCB.
2024-01-25 12:08:30 -05:00
Lai Jiang
a4903c27b9 Make the formatting tasks work with Java 17 (#2301)
TESTED=ran gradle jIFA locally after intentionally mis-formatting a Java
file.
2024-01-24 17:15:13 -05:00
sarahcaseybot
2166c28d6d Update to only include changes to check for production required tags (#2273) 2024-01-24 17:12:46 -05:00
Lai Jiang
891e7c0174 Make Kythe work with Java 17 (#2293)
TESTED=submitted a GCB job locally and it ran successfully.
2024-01-24 13:26:45 -05:00
Ben McIlwain
64f5971275 Include a better error message to debug nomulus tool not working (#2275)
Failures to initialize the tool transaction manager seem to often be caused by
stale local credentials.
2024-01-24 13:08:33 -05:00
sarahcaseybot
818944317f Add some updates to UpdateReservedListCommand to facilitate internal config presubmits and syncing (#2292)
* Add some updates to UpdateReservedListCommand to facilitate internal config presubmits and syncing

Added a dry-run tag for presubmit tests

Added early exit behavior when there are no new changes to the list

Added a new --build_environment tag to be used to indicate command runs from build tools. This tag was also added to UpdatePremiumListCommand. Once this new tag is deployed, and break glass behavior is added, these commands will be modified to prevent runs on the command line in the production environment unless the --build_environment or --break_glass flag is used.

* Fix capitalization

* Added in commented out production environment check for buildEnv flag
2024-01-23 17:32:33 -05:00
Weimin Yu
ea96ed300f Drop the BsaDomainInUse table (#2298)
Already renamed to BsaUnlockableDomain table.
2024-01-23 17:07:35 -05:00
Weimin Yu
8415c8bbe4 Fix typo in BsaRefreshAction (#2297) 2024-01-22 16:03:35 -05:00
Lai Jiang
dc48c257b5 Use Java 17 runtime on sandbox and production (#2296)
The blocking issue is fixed in
https://github.com/google/nomulus/pull/2224.

Java 8 support is being deprecated on 2024-01-31 and no further deployment is
possible afterwards without exception:

https://cloud.google.com/appengine/docs/legacy/standard/java/deprecations

We have been using Java 17 on alpha/crash/qa for several months and have
not oberved any other blocking issue other than possible missing email
attachements, which is being mitigated by including a link to the
attachments saved in GCS.
2024-01-22 15:21:17 -05:00
sarahcaseybot
2bf3867532 Add an example tld YAML config file (#2295) 2024-01-22 13:32:36 -05:00
Weimin Yu
44f44be643 Add bsa-refresh cron job to sandbox and prod (#2290)
This is the job that updates the unblockable domains according to recent
changes in domain registration and reservation.
2024-01-22 12:24:09 -05:00
Weimin Yu
f61579b350 Fix BsaRefreshAction bugs (#2294)
* Fix BsaRefreshAction bugs

Added functional tests for BsaRefreshAction, which checks for changes in
domain registration and reservation, and apply them to the Unblockable
domain list.

Fixed a few bugs exposed by the tests.

Also refactored a few other tests.
2024-01-22 12:23:29 -05:00
Ben McIlwain
c414e38a98 Add batching to BSA unavailable domains list generation (#2282)
This also moves it back to the replica transaction manager now that it shouldn't be timing
out its queries.

And this adds a test as well (more to come!).
2024-01-19 14:58:09 -05:00
sarahcaseybot
2cf2d7e7b1 Define the --build_environment flag and change --break_glass flag to a Boolean type (#2277)
* Define the --end_breakglass and --build_environment flags

It is necessary to define these flags in a deployment before merging go/r3pr/2273 in order to prevent breaking the exisitng TLD syncing and entity presubmit testing that has already been enabled

* make break glass 2 words

* Change break_glass flag to take a Boolean and use false value to end break glass mode

* small fixes

* Fix spacing

* Add missing G

* Add clarifying comment
2024-01-19 14:23:13 -05:00
Weimin Yu
432871add9 Fix a BSA bug and refactor some unit tests (#2291)
* Refactor a few BSA unit tests

Added a few helpers for managing reserved list in tests and updated the
tests to use them.

Also fixed a bug: when quering for newly created domains, the query
should be restricted to bsa-enrolled tlds.
2024-01-18 16:12:59 -05:00
dependabot[bot]
2621b2d679 Bump follow-redirects from 1.15.2 to 1.15.4 in /docs/console-endpoints (#2278)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lai Jiang <jianglai@google.com>
2024-01-17 09:20:29 -05:00
Lai Jiang
7a5db3b8fe Upgrade builder image to use Java 17 (#2289)
TESTED=ran nomulus-release on alpha with the new image
2024-01-16 17:05:11 -05:00
dependabot[bot]
055f9c012c Bump follow-redirects from 1.15.3 to 1.15.4 in /console-webapp (#2283)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 15:34:45 -05:00
Pavlo Tkach
14ab9423f8 Update Angular to v17 (#2260) 2024-01-16 13:45:56 -05:00
sarahcaseybot
9223b81ab3 Remove create_tld and update_tld commands (#2261)
* Remove create_tld and update_tld commands

These commands are no longer necessary now that configure_tld command is available. However, the configure_tld command should only be used for crash, QA, and alpha environments. TLDs in production and sandbox must be modified using modifications to their config files in Gerrit unless using the configure_tld command in breakglass mode. Check the "How to configure TLDs" procedure doc for more info.

* re-delete file
2024-01-16 11:32:59 -05:00
Weimin Yu
1dcf34ccc2 Report BSA block status in DomainCheckFlow (#2288)
- Registered names are not affected.

- Reserved names are not affected.

- Names that are none of the above and match some BSA labels are
  reported as blocked.
2024-01-12 17:17:51 -05:00
Weimin Yu
9273d2bf15 Remove deleted BSA labels from database (#2286)
Fixed the bug that retains deleted BSA labels in the database.

Added a few simple end-to-end tests for BSA download.
2024-01-12 14:20:56 -05:00
Ben McIlwain
036d35c11a Make the BSA upload unvailable domains task work with HTTP GET as well (#2287)
Apparently Google Cloud Scheduler can only do GET, not POST, for some reason.
2024-01-12 12:17:52 -05:00
Ben McIlwain
a8ce34586d Add production cronjobs for BSA download/upload actions (#2285)
* Add production cronjob for uploading BSA unavailable names

* Add production cronjob for BSA download action
2024-01-11 18:38:52 -05:00
Ben McIlwain
26fb04f00c Add sandbox cronjob for upload BSA unavailable names (#2284) 2024-01-11 12:21:40 -05:00
Ben McIlwain
9d4c38684a Add a cron schedule for the BSA upload unavailable domains task (#2280)
Also fixes the action taken in the case where zero unavailable domains are
found, and temporarily changes over to using the primary DB (because the replica
transaction was timing out at 30 seconds on large databases). I'll switch this
over to use batching and move it back to replica afterwards, but this should
unblock us temporarily.
2024-01-10 14:34:06 -05:00
Pavlo Tkach
d7edd27cdd Add support for Ubuntu20 on kokoro (#2279) 2024-01-10 14:32:34 -05:00
Ben McIlwain
265d69051b Map /_dr/task/uploadBsaUnavailableNames in BSA service's web.xml (#2276)
This should have been part of PR #2265 but we all missed it.

Also adds a couple of useful logging statements.
2024-01-09 18:51:23 -05:00
Lai Jiang
b5d2b56426 Build Nomulus with Java 17 (#2255)
This PR makes it possible to build the Nomulus code base using Java 17.
Building with Java 11 continue to be possible and the resulting bytecodes are
still at Java 8 level. Also upgraded Gradle to 8.5.

There are several necessary changes to make this happen:

1. Some Gradle plugins need to be upgraded to support Java 17, notably
errorprone. As a result, a lot more "errors" were caught and corrected.

2. All test code are now built and run at Java 8 level. Previously it was left
undefined (which defaults to the version of the compiler) and had led to
situations where we inadvertently called Java 8+ features in production that
are not caught by tests. The change also made the java8compatibility subproject
obsolete, which is therefore removed.

3. Removed the docs subproject. Its main use is to generate flows.md, but it
relies heavily on Java internal APIs that have changed significant with each
version. Upgrading to Java 11 required extensive refactoring of the code there,
and Java 17 again removed many APIs that were used. I don't think it is worth
the maintenance effort just to have a tool to generate flows.md which no one
actually reads.

4. Capped a few GCP dependencies because the latest version depends on
 grpc-java >= 1.59.0, which includes a runtime incompatibility
 (https://github.com/grpc/grpc-java/releases/tag/v1.59.0).
2024-01-09 15:56:37 -05:00
Ben McIlwain
e79c63142a Add a daily batch action to upload unavailable domains to BSA (#2265) 2024-01-09 14:52:07 -05:00
Weimin Yu
f8ac7afc33 Check BSA block status in CheckApi (#2271)
* Check BSA block status in CheckApi

Checks for and reports BSA block status if the name is not registered or
reserved.

Also moves CheckApiActionTest to standardTest. Whatever problem forcing
it to another suite has apparently disappeared.
2024-01-09 13:19:07 -05:00
Ben McIlwain
e56e751652 Fix build warnings (#2274)
All of these were causing warnings to appear during a build of the codebase.
2024-01-09 13:15:54 -05:00
Ben McIlwain
cfdf12aa7d Add OkHttp as a library used by the core Nomulus project (#2272)
This will be used in a subsequent PR (see #2265) to upload the unavailable
domain names list to the BSA endpoint.
2024-01-09 12:53:15 -05:00
Weimin Yu
811b385544 Add cron config for the bsaDownload job in Sandbox (#2267)
* Add cron config for the bsaDownload job in Sandbox
2024-01-05 11:10:48 -05:00
Weimin Yu
3f5c9d1246 BSA for integration test (#2256)
Supports the full blocklist download cycle (download, diffing, diff-apply, and order-status reporting) and the refreshing of unblockable domains.

Submitted due to tight deadline. We will conduct post-submit review and refactoring.
2024-01-05 11:09:40 -05:00
Pavlo Tkach
5315752bc0 Add ICANN csv response GZIP decoding (#2269) 2024-01-04 18:35:21 -05:00
Pavlo Tkach
4eee7b8c0d Add support for bsa service to cloud tasks config (#2268) 2024-01-03 17:38:42 -05:00
Weimin Yu
ecb39d5899 Use custom whois message for bsa-blocked domain (#2241)
* Use custom whois message for bsa-blocked domain
2024-01-02 14:40:34 -05:00
Lai Jiang
42b508427b Bypass SCRYPT hashing in tests (#2262)
SCRYPT is much computationally heavier than SHA265 (by design), which
resulted in test run time doubling due to most tests initializing canned
data that uses hashing.

Since out tests are not verifying the correctness of a specific hashing
algorithm anyway, this PR makes it so that simple concatenation is used
in tests.

Also moved RegistryEnvironment to the util subproject so it can be called by
PasswordUtils, which makes sense as it is a utility class.
2023-12-21 16:17:37 -05:00
sarahcaseybot
20b5b43501 Add type conversion to TimedTransitionProperty<Money> deserializer to handle JPY currency (#2258)
* Add BigInt conversion to TimedTransitionProperty<Money> deserializer to handle JPY currency

* Remove unnecessary lines in test

* Add eap schedule check

* Don't use raw LinkedHashMap type

* add timezone
2023-12-21 12:59:54 -05:00
Lai Jiang
08285f5de7 Greatly increase the upper limit of proxy instances in production (#2259)
From our investigation, the Monday night WHOIS storm does not cause any
strain to the backend system. The backend latency metrics are all well within
the limits. The latency measured from the proxy matches observed latency
by the prober, and we see that the "used" CPU is 1.5x of "requested" CPU
during the time when the latency is above the threshold.

Making this change hopefully removes the proxy as the bottleneck and
ameliorate the pages.
2023-12-20 15:37:29 -05:00
Pavlo Tkach
fb4c5b457d Prevent reusing ianaId for real registrars (#2257) 2023-12-20 15:20:04 -05:00
Pavlo Tkach
781c212275 Add IcannHttpReporter failed response logging (#2252) 2023-12-18 11:03:33 -05:00
Weimin Yu
c73f7a6bd3 Add the BsaDomainRefresh entity (#2250)
Add the BsaDomainRefresh class which tracks the refresh actions.

The refresh actions checks for changes in the set of registered and
reserved domains, which are called unblockables to BSA.
2023-12-13 16:08:37 -05:00
Lai Jiang
8d793b2349 Do not double-enqueue NordnVerifyAction (#2253)
Currently, a verify action is enqueued every time the upload method
succeeds. Because the upload job is wrapped in a transaction, the
same task will be enqueued again if the transaction retries.

We cannot move the upload method outside the transaction because the
read-upload-write logic needs to be atomic, and the upload part itself
is idempotent (therefore retri-able). We can, however, move the
enqueuing part outside the transaction as we only need to enqueue the
verify task once the transaction succeeds. This should fix the issue
where multiple verify jobs try to hit the same marksdb endpoints,
resulting in 429 (Too Many Requests) errors.
2023-12-12 16:00:35 -05:00
Weimin Yu
55d5f8c6f8 Forbid domain creation with label blocked by BSA (#2236)
* Forbid domain creation with label blocked by BSA

Add a BSA label check in the DomainCreation flow.
2023-12-11 22:14:12 -05:00
1228 changed files with 47624 additions and 49210 deletions

View File

@@ -7,7 +7,7 @@ on:
# The branches below must be a subset of the branches above
branches: [ 'master' ]
schedule:
- cron: '24 4 * * 2'
- cron: '24 4 * * *'
jobs:
analyze:
@@ -27,17 +27,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set Java version
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '11'
java-version: '21'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -47,11 +47,20 @@ jobs:
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
queries: security-and-quality
# Build with Gradle
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
build-scan-publish: true
build-scan-terms-of-service-url: "https://gradle.com/terms-of-service"
build-scan-terms-of-service-agree: "yes"
- name: Execute Gradle build
run: ./gradlew 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)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
#- name: Autobuild
# uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -64,6 +73,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -0,0 +1,25 @@
name: Dependency Submission
on:
push:
branches: [ 'master' ]
schedule:
- cron: '24 3 * * *'
permissions:
contents: write
jobs:
dependency-submission:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Set Java version
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Generate and submit dependency graph
uses: gradle/actions/dependency-submission@v3

23
.github/workflows/do-not-merge.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: "Check labels"
on:
pull_request:
branches: ["master"]
types:
- opened
- synchronize
- labeled
- unlabeled
merge_group:
branches: ["master"]
types: [checks_requested]
jobs:
fail-by-label:
runs-on: ubuntu-latest
steps:
- name: Fail if PR is labeled as "do not merge"
if: contains(github.event.pull_request.labels.*.name, 'do not merge')
run: |
echo "This PR is labeled as do not merge!"
exit 1

3
.gitignore vendored
View File

@@ -117,3 +117,6 @@ core/**/registrar_dbg*.css
# Appengine generated files
core/WEB-INF/appengine-generated/*.bin
core/WEB-INF/appengine-generated/*.xml
# jEnv
.java-version

1
.java-version Normal file
View File

@@ -0,0 +1 @@
21

View File

@@ -47,10 +47,6 @@ war {
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") {
@@ -103,10 +99,11 @@ explodeWar.doLast {
file("${it.explodedAppDirectory}/WEB-INF/lib/tools.jar").setWritable(true)
}
appengineDeployAll.mustRunAfter ':console-webapp:deploy'
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'

View File

@@ -29,21 +29,20 @@ buildscript {
dependencies {
classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.4.1'
classpath 'net.ltgt.gradle:gradle-errorprone-plugin:2.0.2'
classpath 'net.ltgt.gradle:gradle-errorprone-plugin:3.1.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 '2.0.2'
id 'net.ltgt.errorprone' version '3.1.0'
id 'checkstyle'
id 'com.github.johnrengelman.shadow' version '5.1.0'
id 'com.github.johnrengelman.shadow' version '8.1.1'
// NodeJs plugin
id "com.github.node-gradle.node" version "3.0.1"
@@ -61,51 +60,19 @@ dependencyLocking {
node {
download = false
version = "16.19.0"
version = "20.10.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)
// Provide defaults for all of the project properties.
@@ -211,6 +178,30 @@ allprojects {
options.fork = true
options.forkOptions.executable =
file("${System.env.JAVA_HOME}/bin/javac")
options.compilerArgs = ["--add-exports",
"jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
"--add-exports",
"jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-exports",
"jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
"--add-exports",
"jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-exports",
"jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-exports",
"jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-exports",
"jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--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.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",
"-J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"]
}
}
}
@@ -257,6 +248,12 @@ def javadocSource = []
def javadocClasspath = []
def javadocDependentTasks = []
def services = [':services:default',
':services:backend',
':services:bsa',
':services:tools',
':services:pubapi']
subprojects {
// Skip no-op project
if (project.name == 'services') return
@@ -307,8 +304,7 @@ subprojects {
afterEvaluate {
if (rootProject.enableDependencyLocking.toBoolean()
&& project.name != 'integration'
&& project.name != 'java8compatibility') {
&& project.name != 'integration') {
// The ':integration' project runs server/schema integration tests using
// dynamically specified jars with no transitive dependency. Therefore
// dependency-locking does not make sense. Furthermore, during
@@ -316,9 +312,6 @@ subprojects {
// immutable. Locking activation would trigger an invalid operation
// exception.
//
// The ':java8compatibility' project is test-only. Its source does not go
// into production.
//
// For all other projects, due to problem with the gradle-license-report
// plugin, the dependencyLicenseReport configuration must opt out of
// dependency-locking. See dependency_lic.gradle for the reason why.
@@ -334,23 +327,6 @@ subprojects {
}
}
// '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")
}
}
def services = [':services:default',
':services:backend',
':services:bsa',
':services:tools',
':services:pubapi']
// Set up all of the deployment projects.
if (services.contains(project.path)) {
@@ -362,12 +338,12 @@ subprojects {
apply from: "${rootDir.path}/java_common.gradle"
if (project.name != 'docs') {
compileJava {
// TODO: Remove this once we migrate off AppEngine.
options.release = 8
}
}
// When changing Java version here, be sure to update BEAM Java runtime:
// 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_21
targetCompatibility = JavaVersion.VERSION_21
project.tasks.test.dependsOn runPresubmits
@@ -392,9 +368,9 @@ subprojects {
}
}
// No need to produce javadoc for the docs subproject, which has no APIs to
// No need to produce javadoc for the jetty subproject, which has no APIs to
// expose to users.
if (project.name != 'docs') {
if (project.name != 'jetty') {
javadocSource << project.sourceSets.main.allJava
javadocClasspath << project.sourceSets.main.runtimeClasspath
javadocClasspath << "${buildDir}/generated/sources/annotationProcessor/java/main"
@@ -402,6 +378,17 @@ subprojects {
}
}
// 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.
//
@@ -432,9 +419,6 @@ if (verboseTestOutput.toBoolean()) {
}
task checkDependenciesDotGradle {
def buildSrcDepsFile = File.createTempFile('buildSrc', 'deps')
buildSrcDepsFile.deleteOnExit()
dependsOn createGetBuildSrcDirectDepsTask(buildSrcDepsFile)
doLast {
Set<String> depsInUse = []
@@ -447,9 +431,7 @@ task checkDependenciesDotGradle {
}
}
}
if (buildSrcDepsFile.exists()) {
depsInUse.addAll(buildSrcDepsFile.readLines())
}
def unusedDeps =
rootProject.dependencyMap.keySet()
.findAll { !depsInUse.contains(it) }
@@ -466,34 +448,23 @@ 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 ->
println("JAVA_HOME=${System.env.JAVA_HOME}")
println("PATH=${System.env.PATH}")
println(ext.execInBash("type java", "${rootDir}"))
println(ext.execInBash("java -version", "${rootDir}"))
def scriptDir = rootDir.path.endsWith('buildSrc')
? "${rootDir}/../java-format"
: "${rootDir}/java-format"
def workingDir = rootDir.path.endsWith('buildSrc')
? "${rootDir}/.."
: rootDir
def javaHome = project.findProperty('org.gradle.java.home')
def javaBin
if (javaHome != null) {
javaBin = "$javaHome/bin/java"
} else {
javaBin = ext.execInBash("which java", rootDir)
}
println("Running the formatting tool with $javaBin")
def scriptDir = "${rootDir}/java-format"
def workingDir = rootDir
def formatDiffScript = "${scriptDir}/google-java-format-git-diff.sh"
def pythonExe = getPythonExecutable()
return ext.execInBash(
"PYTHON=${pythonExe} ${formatDiffScript} ${action}", "${workingDir}")
"JAVA=${javaBin} PYTHON=${pythonExe} ${formatDiffScript} ${action}", "${workingDir}")
}
}
@@ -548,7 +519,9 @@ task javadoc(type: Javadoc) {
destinationDir = file("${buildDir}/docs/javadoc")
options.encoding = "UTF-8"
// In a lot of places we don't write @return so suppress warnings about that.
options.addBooleanOption('Xdoclint:all,-missing', true)
// 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)
options.addBooleanOption("-allow-script-in-comments",true)
options.tags = ["type:a:Generic Type",
"error:a:Expected Error",
@@ -566,7 +539,6 @@ task coreDev {
dependsOn 'javadoc'
dependsOn 'checkDependenciesDotGradle'
dependsOn 'checkLicense'
dependsOn ':console-webapp:runConsoleWebappUnitTests'
dependsOn ':core:check'
dependsOn 'assemble'
}

View File

@@ -1,131 +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 "2.0.2"
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["com.google.truth.extensions:truth-java8-extension"]!!)
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}")
// }
}
}
}

View File

@@ -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:2.0.2=classpath
net.ltgt.gradle:gradle-errorprone-plugin:2.0.2=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.0=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0=classpath
org.jetbrains.kotlin:kotlin-stdlib:1.9.0=classpath
org.jetbrains:annotations:13.0=classpath
org.slf4j:slf4j-api:1.7.36=classpath
org.tukaani:xz:1.9=classpath
empty=

View File

@@ -1,110 +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=buildScriptClasspath,compileClasspath
args4j:args4j:2.0.23=buildScriptClasspath,compileClasspath
com.fasterxml.jackson.core:jackson-core:2.14.2=buildScriptClasspath,compileClasspath
com.fasterxml.jackson:jackson-bom:2.14.2=buildScriptClasspath,compileClasspath
com.github.ben-manes.caffeine:caffeine:2.7.0=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.2.0=buildScriptClasspath,compileClasspath
com.google.api.grpc:gapic-google-cloud-storage-v2:2.22.6-alpha=buildScriptClasspath,compileClasspath
com.google.api.grpc:grpc-google-cloud-storage-v2:2.22.6-alpha=buildScriptClasspath,compileClasspath
com.google.api.grpc:proto-google-cloud-storage-v2:2.22.6-alpha=buildScriptClasspath,compileClasspath
com.google.api.grpc:proto-google-common-protos:2.22.0=buildScriptClasspath,compileClasspath
com.google.api.grpc:proto-google-iam-v1:1.17.0=buildScriptClasspath,compileClasspath
com.google.api:api-common:2.14.0=buildScriptClasspath,compileClasspath
com.google.api:gax-grpc:2.31.0=buildScriptClasspath,compileClasspath
com.google.api:gax-httpjson:2.31.0=buildScriptClasspath,compileClasspath
com.google.api:gax:2.31.0=buildScriptClasspath,compileClasspath
com.google.apis:google-api-services-storage:v1-rev20230301-2.0.0=buildScriptClasspath,compileClasspath
com.google.auth:google-auth-library-credentials:1.19.0=buildScriptClasspath,compileClasspath
com.google.auth:google-auth-library-oauth2-http:1.19.0=buildScriptClasspath,compileClasspath
com.google.auto.value:auto-value-annotations:1.10.1=buildScriptClasspath,compileClasspath
com.google.auto.value:auto-value:1.10.4=annotationProcessor
com.google.auto:auto-common:0.10=annotationProcessor
com.google.cloud:google-cloud-core-grpc:2.21.0=buildScriptClasspath,compileClasspath
com.google.cloud:google-cloud-core-http:2.21.0=buildScriptClasspath,compileClasspath
com.google.cloud:google-cloud-core:2.21.0=buildScriptClasspath,compileClasspath
com.google.cloud:google-cloud-storage:2.22.6=buildScriptClasspath,compileClasspath
com.google.code.findbugs:jFormatString:3.0.0=annotationProcessor
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.3.4=annotationProcessor
com.google.errorprone:error_prone_annotations:2.18.0=buildScriptClasspath,compileClasspath
com.google.errorprone:error_prone_annotations:2.3.4=annotationProcessor
com.google.errorprone:error_prone_check_api:2.3.4=annotationProcessor
com.google.errorprone:error_prone_core:2.3.4=annotationProcessor
com.google.errorprone:error_prone_type_annotations:2.3.4=annotationProcessor
com.google.escapevelocity:escapevelocity:0.9.1=buildScriptClasspath,compileClasspath
com.google.guava:failureaccess:1.0.1=annotationProcessor,buildScriptClasspath,compileClasspath
com.google.guava:guava-parent:32.1.1-jre=buildScriptClasspath,compileClasspath
com.google.guava:guava:27.0.1-jre=annotationProcessor
com.google.guava:guava:32.1.1-jre=buildScriptClasspath,compileClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor
com.google.http-client:google-http-client-apache-v2:1.43.3=buildScriptClasspath,compileClasspath
com.google.http-client:google-http-client-appengine:1.43.3=buildScriptClasspath,compileClasspath
com.google.http-client:google-http-client-gson:1.43.3=buildScriptClasspath,compileClasspath
com.google.http-client:google-http-client-jackson2:1.43.3=buildScriptClasspath,compileClasspath
com.google.http-client:google-http-client:1.43.3=buildScriptClasspath,compileClasspath
com.google.inject.extensions:guice-multibindings:4.1.0=buildScriptClasspath,compileClasspath
com.google.inject:guice:4.1.0=buildScriptClasspath,compileClasspath
com.google.j2objc:j2objc-annotations:1.1=annotationProcessor
com.google.j2objc:j2objc-annotations:2.8=buildScriptClasspath,compileClasspath
com.google.jsinterop:jsinterop-annotations:1.0.1=buildScriptClasspath,compileClasspath
com.google.oauth-client:google-oauth-client:1.34.1=buildScriptClasspath,compileClasspath
com.google.protobuf:protobuf-java-util:3.23.2=buildScriptClasspath,compileClasspath
com.google.protobuf:protobuf-java:3.23.2=buildScriptClasspath,compileClasspath
com.google.protobuf:protobuf-java:3.4.0=annotationProcessor
com.google.re2j:re2j:1.6=buildScriptClasspath
com.google.template:soy:2021-02-01=buildScriptClasspath,compileClasspath
com.googlecode.java-diff-utils:diffutils:1.3.0=annotationProcessor
com.ibm.icu:icu4j:57.1=buildScriptClasspath,compileClasspath
commons-codec:commons-codec:1.15=buildScriptClasspath,compileClasspath
commons-logging:commons-logging:1.2=buildScriptClasspath,compileClasspath
io.grpc:grpc-alts:1.55.3=buildScriptClasspath,compileClasspath
io.grpc:grpc-api:1.55.3=buildScriptClasspath,compileClasspath
io.grpc:grpc-auth:1.55.3=buildScriptClasspath,compileClasspath
io.grpc:grpc-context:1.55.3=buildScriptClasspath,compileClasspath
io.grpc:grpc-core:1.55.3=buildScriptClasspath
io.grpc:grpc-googleapis:1.55.3=buildScriptClasspath
io.grpc:grpc-grpclb:1.55.3=buildScriptClasspath,compileClasspath
io.grpc:grpc-netty-shaded:1.55.3=buildScriptClasspath
io.grpc:grpc-protobuf-lite:1.55.3=buildScriptClasspath,compileClasspath
io.grpc:grpc-protobuf:1.55.3=buildScriptClasspath,compileClasspath
io.grpc:grpc-rls:1.55.3=buildScriptClasspath
io.grpc:grpc-services:1.55.3=buildScriptClasspath
io.grpc:grpc-stub:1.55.3=buildScriptClasspath,compileClasspath
io.grpc:grpc-xds:1.55.3=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.26.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=buildScriptClasspath,compileClasspath
org.apache.commons:commons-lang3:3.12.0=buildScriptClasspath,compileClasspath
org.apache.commons:commons-text:1.10.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.0.0=annotationProcessor
org.checkerframework:checker-qual:3.33.0=buildScriptClasspath,compileClasspath
org.checkerframework:dataflow:3.0.0=annotationProcessor
org.checkerframework:javacutil:3.0.0=annotationProcessor
org.codehaus.mojo:animal-sniffer-annotations:1.17=annotationProcessor
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:2.1.2=annotationProcessor
org.plumelib:plume-util:1.0.6=annotationProcessor
org.plumelib:reflection-util:0.0.2=annotationProcessor
org.plumelib:require-javadoc:0.1.0=annotationProcessor
org.threeten:threetenbp:1.6.8=buildScriptClasspath,compileClasspath
empty=

View File

@@ -1,3 +0,0 @@
pluginsUrl=https://plugins.gradle.org/m2/
allowInsecureProtocol=
enableDependencyLocking=true

View File

@@ -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()));
}
}

View File

@@ -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);
}
}

View File

@@ -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() {}
}

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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}

View File

@@ -1,279 +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 com.google.common.truth.Truth8.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>");
}
}

View File

@@ -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");
}
}

View File

@@ -11,8 +11,8 @@ com.diffplug.spotless:spotless-lib:2.40.0=classpath
com.diffplug.spotless:spotless-plugin-gradle:6.20.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.jengelman.gradle.plugins:shadow:5.1.0=classpath
com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:5.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
@@ -29,35 +29,34 @@ 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.6=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:2.0.2=classpath
net.ltgt.gradle:gradle-errorprone-plugin:2.0.2=classpath
org.apache.ant:ant-launcher:1.9.7=classpath
org.apache.ant:ant:1.9.7=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
org.apache.commons:commons-lang3:3.5=classpath
org.checkerframework:checker-qual:2.10.0=classpath
org.codehaus.plexus:plexus-utils:3.0.24=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.glassfish:javax.json:1.0.4=classpath
org.jdom:jdom2:2.0.6=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:annotations:13.0=classpath
org.ow2.asm:asm-analysis:7.0-beta=classpath
org.ow2.asm:asm-commons:7.0-beta=classpath
org.ow2.asm:asm-tree:7.0-beta=classpath
org.ow2.asm:asm:7.0-beta=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.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.1.1=classpath
org.vafer:jdependency:2.8.0=classpath
org.yaml:snakeyaml:1.21=classpath
empty=

View File

@@ -57,7 +57,7 @@ dependencies {
implementation deps['com.github.ben-manes.caffeine:caffeine']
implementation deps['com.google.code.findbugs:jsr305']
implementation deps['com.google.guava:guava']
implementation deps['javax.inject:javax.inject']
implementation deps['jakarta.inject:jakarta.inject-api']
implementation deps['joda-time:joda-time']
implementation deps['com.google.flogger:flogger']
implementation deps['io.github.java-diff-utils:java-diff-utils']

View File

@@ -1,69 +1,68 @@
# 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.github.ben-manes.caffeine:caffeine:2.7.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.github.ben-manes.caffeine:caffeine:2.9.3=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
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.value:auto-value-annotations:1.10.1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.auto:auto-common:0.10=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.code.findbugs:jFormatString:3.0.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.3.4=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.19.1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.errorprone:error_prone_annotations:2.3.4=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
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.3.4=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_core:2.3.4=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_type_annotations:2.3.4=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.flogger:flogger:0.7.4=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:failureaccess:1.0.1=annotationProcessor,checkstyle,compileClasspath,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
com.google.guava:guava-parent:32.1.2-jre=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:guava:27.0.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
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.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.2-jre=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,compileClasspath,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
com.google.j2objc:j2objc-annotations:1.1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:guava:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:guava:33.2.1-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:2.8=compileClasspath,testCompileClasspath,testingCompileClasspath
com.google.protobuf:protobuf-java:3.4.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.truth:truth:1.1.5=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.googlecode.java-diff-utils:diffutils:1.3.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
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
commons-collections:commons-collections:3.2.2=checkstyle
info.picocli:picocli:4.6.2=checkstyle
io.github.java-diff-utils:java-diff-utils:4.12=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
javax.inject:javax.inject:1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
joda-time:joda-time:2.12.5=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
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
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
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.0.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
org.checkerframework:checker-qual:3.12.0=checkstyle
org.checkerframework:checker-qual:3.35.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.checkerframework:dataflow:3.0.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
org.checkerframework:javacutil:3.0.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
org.codehaus.mojo:animal-sniffer-annotations:1.17=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
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.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.jacoco:org.jacoco.agent:0.8.9=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.9=jacocoAnt
org.jacoco:org.jacoco.core:0.8.9=jacocoAnt
org.jacoco:org.jacoco.report:0.8.9=jacocoAnt
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.javassist:javassist:3.28.0-GA=checkstyle
org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.10.0=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.10.0=testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:5.10.0=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.11.0-M2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.11.0-M2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.11.0-M2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.11.0-M2=testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:5.11.0-M2=testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.5=jacocoAnt
org.ow2.asm:asm-tree:9.5=jacocoAnt
org.ow2.asm:asm:9.5=compileClasspath,deploy_jar,jacocoAnt,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.pcollections:pcollections:2.1.2=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
org.plumelib:plume-util:1.0.6=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
org.plumelib:reflection-util:0.0.2=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
org.plumelib:require-javadoc:0.1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
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.reflections:reflections:0.10.2=checkstyle
empty=errorproneJavac,testingCompile,testingRuntime,testingRuntimeClasspath
empty=testingCompile,testingRuntime,testingRuntimeClasspath

View File

@@ -0,0 +1,43 @@
// 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.
package google.registry.util;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Iterators.partition;
import static com.google.common.collect.Iterators.transform;
import static com.google.common.collect.Streams.stream;
import static java.lang.Math.min;
import com.google.common.collect.ImmutableList;
import java.util.stream.Stream;
/** Utilities for breaking up a {@link Stream} into batches. */
public final class BatchedStreams {
static final int MAX_BATCH = 1024 * 1024;
private BatchedStreams() {}
/**
* Transform a flat {@link Stream} into a {@code Stream} of batches.
*
* <p>Closing the returned stream does not close the original stream.
*/
public static <T> Stream<ImmutableList<T>> toBatches(Stream<T> stream, int batchSize) {
checkArgument(batchSize > 0, "batchSize must be a positive integer.");
return stream(
transform(partition(stream.iterator(), min(MAX_BATCH, batchSize)), ImmutableList::copyOf));
}
}

View File

@@ -0,0 +1,65 @@
// 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.
package google.registry.util;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.util.BatchedStreams.toBatches;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link BatchedStreams}. */
public class BatchedStreamsTest {
@Test
void invalidBatchSize() {
assertThat(assertThrows(IllegalArgumentException.class, () -> toBatches(Stream.of(), 0)))
.hasMessageThat()
.contains("must be a positive integer");
}
@Test
void batch_success() {
// 900_002 elements -> 900 1K-batches + 1 2-element-batch
Stream<Integer> data = IntStream.rangeClosed(0, 900_001).boxed();
assertThat(
toBatches(data, 1000).map(ImmutableList::size).collect(groupingBy(x -> x, counting())))
.containsExactly(1000, 900L, 2, 1L);
}
@Test
void batch_partialBatch() {
Stream<Integer> data = Stream.of(1, 2, 3);
assertThat(
toBatches(data, 1000).map(ImmutableList::size).collect(groupingBy(x -> x, counting())))
.containsExactly(3, 1L);
}
@Test
void batch_truncateBatchSize() {
// 2M elements -> 2 1M-batches despite the user-specified 2M batch size.
Stream<Integer> data = IntStream.range(0, 1024 * 2048).boxed();
assertThat(
toBatches(data, 2_000_000)
.map(ImmutableList::size)
.collect(groupingBy(x -> x, counting())))
.containsExactly(1024 * 1024, 2L);
}
}

View File

@@ -14,7 +14,6 @@
package google.registry.testing;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.flogger.FluentLogger;
@@ -31,31 +30,26 @@ public final class SystemInfo {
private static final LoadingCache<String, Boolean> hasCommandCache =
Caffeine.newBuilder()
.build(
new CacheLoader<String, Boolean>() {
@Override
public Boolean load(String cmd) throws InterruptedException {
try {
Process pid = Runtime.getRuntime().exec(cmd);
pid.getOutputStream().close();
pid.waitFor();
} catch (IOException e) {
logger.atWarning().withCause(e).log("%s command not available.", cmd);
return false;
}
return true;
cmd -> {
try {
Process pid = Runtime.getRuntime().exec(cmd.split(" "));
pid.getOutputStream().close();
pid.waitFor();
} catch (IOException e) {
logger.atWarning().withCause(e).log("%s command not available.", cmd);
return false;
}
return true;
});
/**
* Returns {@code true} if system command can be run from path.
* Returns {@code true} if system command can be run from the path.
*
* <p><b>Warning:</b> The command is actually run! So there could be side-effects. You might
* need to specify a version flag or something. Return code is ignored.
* <p><b>Warning:</b> The command is actually run! So there could be side effects. You might need
* to specify a version flag or something. Return code is ignored.
*
* <p>This result is a memoized. If multiple therads try to get the same result at once, the
* heavy lifting will only be performed by the first thread and the rest will wait.
*
* @throws ExecutionException
* <p>This result is a memoized. If multiple threads try to get the same result at once, the heavy
* lifting will only be performed by the first thread and the rest will wait.
*/
public static boolean hasCommand(String cmd) throws ExecutionException {
return hasCommandCache.get(cmd);

View File

@@ -175,14 +175,7 @@ public class TextDiffSubject extends Subject {
.orElse(0);
}
private static class SideBySideRowFormatter {
private final int maxExpectedLineLength;
private final int maxActualLineLength;
private SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
this.maxExpectedLineLength = maxExpectedLineLength;
this.maxActualLineLength = maxActualLineLength;
}
private record SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
public String formatRow(String expected, String actual, char padChar) {
return String.format(

View File

@@ -262,6 +262,9 @@
{
"moduleLicense": "Unicode/ICU License"
},
{
"moduleLicense": "Unicode-3.0"
},
{
"moduleLicense": "Public Domain",
"moduleName": "aopalliance:aopalliance"
@@ -307,6 +310,95 @@
},
{
"moduleLicense": "The W3C Software License"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleName": "com.squareup.okhttp3:okhttp"
},
{
// "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,
"moduleVersion": "3.0.0",
"moduleName": "com.squareup.okio:okio-bom"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleVersion": "3.4.0",
"moduleName": "com.squareup.okio:okio-fakefilesystem"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleVersion": "4.9.3",
"moduleName": "com.squareup.wire:wire-runtime"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleVersion": "4.8.0",
"moduleName": "com.squareup.wire:wire-schema"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleVersion": "1.15.1",
"moduleName": "com.squareup:kotlinpoet"
},
{
"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.0",
"moduleName": "org.jetbrains.kotlin:kotlin-bom"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleVersion": "1.9.20",
"moduleName": "org.jetbrains.kotlin:kotlin-stdlib-common"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleVersion": "1.5.2",
"moduleName": "org.jetbrains.kotlinx:kotlinx-coroutines-core"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleVersion": "0.4.0",
"moduleName": "org.jetbrains.kotlinx:kotlinx-datetime"
},
{
// "Apache License, Version 2.0".
"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"
}
]
}

View File

@@ -47,7 +47,7 @@ class GradleFlag:
PROPERTIES_HEADER = """\
# This file defines properties used by the gradle build. It must be kept in
# This file defines properties used by the gradle build. It must be kept in
# sync with config/nom_build.py.
#
# To regenerate, run ./nom_build --generate-gradle-properties
@@ -58,6 +58,7 @@ PROPERTIES_HEADER = """\
# DO NOT EDIT THIS FILE BY HAND
org.gradle.jvmargs=-Xmx1024m
org.gradle.caching=true
org.gradle.parallel=true
"""
# Help text to be displayed (in addition to the synopsis and flag help, which
@@ -88,19 +89,10 @@ 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',
bool),
Property('flowDocsFile',
'Output filename for the flowDocsTool command.'),
Property('enableDependencyLocking',
'Enables dependency locking.',
'true',
@@ -278,8 +270,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

View File

@@ -87,10 +87,11 @@ PRESUBMITS = {
PresubmitCheck(
r".*Copyright 20\d{2} The Nomulus Authors\. All Rights Reserved\.",
("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"), {
".git", "/build/", "/generated/", "/generated_tests/",
".git", "/build/", "/bin/generated-sources/", "/bin/generated-test-sources/",
"node_modules/", "LoggerConfig.java", "registrar_bin.",
"registrar_dbg.", "google-java-format-diff.py",
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js"
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js",
"/src/main/generated", "/src/test/generated"
}, REQUIRED):
"File did not include the license header.",
@@ -99,12 +100,12 @@ PRESUBMITS = {
{"node_modules/"}, 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"
"/load-testing/", "RegistryTestServerMain.java",
"TestServerExtension.java", "FlowDocumentationTool.java"
}):
"System.(out|err).println is only allowed in tools/ packages. Please "
"use a logger instead.",
@@ -171,6 +172,42 @@ PRESUBMITS = {
{"/node_modules/", "google/registry/ui/js/util.js", "registrar_bin."},
):
"JavaScript files should not include console logging.",
PresubmitCheck(
r".*\nimport (static )?.*\.shaded\..*",
"java",
{"/node_modules/"},
):
"Do not use shaded dependencies",
PresubmitCheck(
r".*com\.google\.common\.truth\.Truth8.*",
"java",
{"/node_modules/"},
):
"Truth8 is deprecated. Use Truth instead.",
PresubmitCheck(
r".*java\.util\.Date.*",
"java",
{"/node_modules/", "JpaTransactionManagerImpl.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\(Response\.class\).*",
"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.",
}
# Note that this regex only works for one kind of Flyway file. If we want to

View File

@@ -31,7 +31,10 @@
"style": "kebab-case"
}
],
"eol-last": ["error", "always"]
"eol-last": [
"error",
"always"
]
}
},
{

View File

@@ -36,7 +36,11 @@ yarn-error.log
/libpeerconnection.log
testem.log
/typings
.nx/
# System files
.DS_Store
Thumbs.db
# Build artifact
/staged/dist

View File

@@ -15,12 +15,16 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular-devkit/build-angular: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": {
@@ -59,9 +64,7 @@
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
@@ -73,10 +76,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "console-webapp:build:production"
"buildTarget": "console-webapp:build:production"
},
"development": {
"browserTarget": "console-webapp:build:development"
"buildTarget": "console-webapp:build:development"
}
},
"defaultConfiguration": "development"
@@ -84,7 +87,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "console-webapp:build"
"buildTarget": "console-webapp:build"
}
},
"test": {
@@ -122,6 +125,9 @@
}
},
"cli": {
"cache": {
"enabled": false
},
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"

View File

@@ -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) {
@@ -62,8 +62,14 @@ task checkFormatting(type: Exec) {
args 'run', 'prettify:check'
}
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
task deploy(type: Exec) {
workingDir "${consoleDir}/staged"
executable 'gcloud'
args 'app', 'deploy', "${projectParam}", '--quiet'
}
tasks.buildConsoleWebappProd.dependsOn(tasks.npmInstallDeps)
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
tasks.build.dependsOn(tasks.checkFormatting)
tasks.deploy.dependsOn(tasks.buildConsoleWebappProd)

View File

@@ -1,47 +1,47 @@
# 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.github.ben-manes.caffeine:caffeine:2.7.0=annotationProcessor,errorprone,testAnnotationProcessor
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:auto-common:0.10=annotationProcessor,errorprone,testAnnotationProcessor
com.google.code.findbugs:jFormatString:3.0.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.3.4=annotationProcessor,errorprone,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.3.4=annotationProcessor,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.3.4=annotationProcessor,errorprone,testAnnotationProcessor
com.google.errorprone:error_prone_core:2.3.4=annotationProcessor,errorprone,testAnnotationProcessor
com.google.errorprone:error_prone_type_annotations:2.3.4=annotationProcessor,errorprone,testAnnotationProcessor
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:27.0.1-jre=annotationProcessor,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:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor
com.google.j2objc:j2objc-annotations:1.1=annotationProcessor,errorprone,testAnnotationProcessor
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.4.0=annotationProcessor,errorprone,testAnnotationProcessor
com.googlecode.java-diff-utils:diffutils:1.3.0=annotationProcessor,errorprone,testAnnotationProcessor
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
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.0.0=annotationProcessor,errorprone,testAnnotationProcessor
org.checkerframework:checker-qual:3.12.0=checkstyle
org.checkerframework:dataflow:3.0.0=annotationProcessor,errorprone,testAnnotationProcessor
org.checkerframework:javacutil:3.0.0=annotationProcessor,errorprone,testAnnotationProcessor
org.codehaus.mojo:animal-sniffer-annotations:1.17=annotationProcessor,errorprone,testAnnotationProcessor
org.jacoco:org.jacoco.agent:0.8.9=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.9=jacocoAnt
org.jacoco:org.jacoco.core:0.8.9=jacocoAnt
org.jacoco:org.jacoco.report:0.8.9=jacocoAnt
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
org.javassist:javassist:3.28.0-GA=checkstyle
org.ow2.asm:asm-commons:9.5=jacocoAnt
org.ow2.asm:asm-tree:9.5=jacocoAnt
org.ow2.asm:asm:9.5=jacocoAnt
org.pcollections:pcollections:2.1.2=annotationProcessor,errorprone,testAnnotationProcessor
org.plumelib:plume-util:1.0.6=annotationProcessor,errorprone,testAnnotationProcessor
org.plumelib:reflection-util:0.0.2=annotationProcessor,errorprone,testAnnotationProcessor
org.plumelib:require-javadoc:0.1.0=annotationProcessor,errorprone,testAnnotationProcessor
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.reflections:reflections:0.10.2=checkstyle
empty=compileClasspath,deploy_jar,errorproneJavac,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath

File diff suppressed because it is too large Load Diff

View File

@@ -16,35 +16,35 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.2",
"@angular/cdk": "^15.2.2",
"@angular/common": "^15.2.2",
"@angular/compiler": "^15.2.2",
"@angular/core": "^15.2.2",
"@angular/forms": "^15.2.2",
"@angular/material": "^15.2.2",
"@angular/platform-browser": "^15.2.2",
"@angular/platform-browser-dynamic": "^15.2.2",
"@angular/router": "^15.2.2",
"@angular/animations": "^18.0.2",
"@angular/cdk": "^18.0.2",
"@angular/common": "^18.0.2",
"@angular/compiler": "^18.0.2",
"@angular/core": "^18.0.2",
"@angular/forms": "^18.0.2",
"@angular/material": "^18.0.2",
"@angular/platform-browser": "^18.0.2",
"@angular/platform-browser-dynamic": "^18.0.2",
"@angular/router": "^18.0.2",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.4",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "~15.2.4",
"@angular/compiler-cli": "^15.2.2",
"@angular-devkit/build-angular": "^18.0.3",
"@angular-eslint/builder": "18.0.1",
"@angular-eslint/eslint-plugin": "18.0.1",
"@angular-eslint/eslint-plugin-template": "18.0.1",
"@angular-eslint/schematics": "18.0.1",
"@angular-eslint/template-parser": "18.0.1",
"@angular/cli": "~18.0.3",
"@angular/compiler-cli": "^18.0.2",
"@types/jasmine": "~4.0.0",
"@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.2",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"concurrently": "^7.6.0",
"eslint": "^8.33.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": "~4.9.4"
"typescript": "~5.4.5"
}
}
}

View File

@@ -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,106 @@
// 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 { 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 UsersComponent from './settings/users/users.component';
import WhoisComponent from './settings/whois/whois.component';
import { SupportComponent } from './support/support.component';
const routes: Routes = [
export interface RouteWithIcon extends Route {
iconName?: string;
}
export const routes: RouteWithIcon[] = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ 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: 'tlds', component: TldsComponent, title: "TLDs", iconName: "event_list" },
{
path: DomainListComponent.PATH,
component: DomainListComponent,
canActivate: [RegistrarGuard],
title: 'Domains',
iconName: 'view_list',
},
{
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],
component: WhoisComponent,
title: 'WHOIS Info',
},
{
path: SecurityComponent.PATH,
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
component: SecurityComponent,
title: 'Security',
},
{
path: UsersComponent.PATH,
component: SettingsUsersComponent,
canActivate: [RegistrarGuard],
},
{
path: RegistrarComponent.PATH,
component: RegistrarComponent,
component: UsersComponent,
},
],
},
// {
// 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: SupportComponent.PATH,
component: SupportComponent,
title: 'Support',
iconName: 'help',
},
];
@NgModule({

View File

@@ -1,24 +1,19 @@
<div class="console-app">
<app-header (toggleNavOpen)="sidenav.toggle()"></app-header>
<div class="console-app mat-typography">
<app-header (toggleNavOpen)="toggleSidenav()"></app-header>
<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
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
[opened]="!breakpointObserver.isMobileView()"
#sidenav
class="console-app__sidebar"
>
<app-navigation />
</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">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>

View File

@@ -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,26 +26,18 @@
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;

View File

@@ -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,17 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule, MaterialModule, BrowserAnimationsModule],
declarations: [AppComponent],
imports: [RouterTestingModule, MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
});

View File

@@ -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,11 +13,12 @@
// limitations under the License.
import { AfterViewInit, Component, ViewChild } 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 { 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';
@Component({
selector: 'app-root',
@@ -25,30 +26,32 @@ import { MatSidenav } from '@angular/material/sidenav';
styleUrls: ['./app.component.scss'],
})
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
) {
registrarService.activeRegistrarIdChange.subscribe(() => {
this.renderRouter = false;
setTimeout(() => {
this.renderRouter = true;
}, 400);
});
}
protected breakpointObserver: BreakPointObserverService,
private router: Router
) {}
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();
}
}
}

View File

@@ -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,86 @@
// 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 { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { NavigationComponent } from './navigation/navigation.component';
import NewRegistrarComponent from './registrar/newRegistrar.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.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 WhoisComponent from './settings/whois/whois.component';
import WhoisEditComponent from './settings/whois/whoisEdit.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 { 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 { TldsComponent } from './tlds/tlds.component';
@NgModule({
declarations: [
AppComponent,
DialogBottomSheetWrapper,
BillingWidgetComponent,
ContactDetailsDialogComponent,
ContactWidgetComponent,
BillingInfoComponent,
ContactDetailsComponent,
DomainListComponent,
DomainsWidgetComponent,
EmptyRegistrar,
EppWidgetComponent,
EppPasswordEditComponent,
HeaderComponent,
HomeComponent,
PromotionsWidgetComponent,
LocationBackDirective,
NavigationComponent,
NewRegistrarComponent,
NotificationsComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistrarSelectorComponent,
ResourcesWidgetComponent,
ResourcesComponent,
SecurityComponent,
SecurityEditComponent,
SelectedRegistrarWrapper,
SettingsComponent,
SettingsContactComponent,
SettingsWidgetComponent,
SupportComponent,
TldsComponent,
TldsWidgetComponent,
WhoisComponent,
WhoisEditComponent,
],
bootstrap: [AppComponent],
imports: [
AppRoutingModule,
BrowserAnimationsModule,
BrowserModule,
FormsModule,
HttpClientModule,
MaterialModule,
SnackBarModule,
],
providers: [
BackendService,
BreakPointObserverService,
GlobalLoaderService,
RegistrarGuard,
UserDataService,
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
@@ -99,7 +100,7 @@ import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.
subscriptSizing: 'dynamic',
},
},
provideHttpClient(),
],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -0,0 +1,16 @@
<app-selected-registrar-wrapper>
<h1 class="mat-headline-4">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"
>View on Google Drive</a
>
</div>
<div>
<img src="./assets/billing.png" />
</div>
</div>
</app-selected-registrar-wrapper>

View File

@@ -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: 20px;
margin-bottom: 20px;
}
}

View File

@@ -0,0 +1,47 @@
// 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'],
})
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');
}
}
}

View File

@@ -1,60 +1,95 @@
<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">Domains</h1>
<div *ngIf="isLoading; else domains_content" class="console-domains__loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
@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 {
<div class="console-app__domains-table-parent">
@if (isLoading) {
<div class="console-app__domains-spinner">
<mat-spinner />
</div>
} @else {
<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()"
[disabled]="isLoading"
#input
/>
</mat-form-field>
}
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__domains-table"
>
<ng-container matColumnDef="domainName">
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{
element.domainName
}}</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">{{ element.statuses }}</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. -->
<mat-row *matNoDataRow>
<mat-cell colspan="4">No domains found</mat-cell>
</mat-row>
</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>
<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>

View File

@@ -0,0 +1,54 @@
.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__download {
position: absolute;
top: -55px;
right: 0;
}
&__domains {
width: 100%;
overflow: auto;
}
&__domains-filter {
min-width: $min-width !important;
width: 100%;
}
&__domains-table-parent {
position: relative;
width: fit-content;
min-width: 100%;
}
&__domains-table {
min-width: $min-width !important;
}
&__domains-spinner {
align-items: center;
display: flex;
justify-content: center;
position: absolute;
height: 100%;
width: 100%;
background: rgba(255, 255, 255, 0.6);
}
.mat-mdc-paginator {
min-width: $min-width !important;
}
}

View File

@@ -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,6 +14,11 @@
import { ComponentFixture, TestBed } from '@angular/core/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 { DomainListComponent } from './domainList.component';
describe('DomainListComponent', () => {
@@ -23,6 +28,12 @@ describe('DomainListComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DomainListComponent],
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(DomainListComponent);

View File

@@ -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,13 +12,15 @@
// 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 { HttpErrorResponse } from '@angular/common/http';
import { Component, ViewChild, effect } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { RegistrarService } from '../registrar/registrar.service';
import { Domain, DomainListService } from './domainList.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
import { Subject, debounceTime } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
import { Domain, DomainListService } from './domainList.service';
@Component({
selector: 'app-domain-list',
@@ -45,15 +47,24 @@ export class DomainListComponent {
pageNumber?: number;
resultsPerPage = 50;
totalResults?: number;
totalResults?: number = 0;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
constructor(
private backendService: BackendService,
private domainListService: DomainListService,
private registrarService: RegistrarService
) {}
protected registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
effect(() => {
this.pageNumber = 0;
this.totalResults = 0;
if (this.registrarService.registrarId()) {
this.reloadData();
}
});
}
ngOnInit() {
this.dataSource.paginator = this.paginator;
@@ -63,7 +74,6 @@ export class DomainListComponent {
.subscribe((searchTermValue) => {
this.reloadData();
});
this.reloadData();
}
ngOnDestroy() {
@@ -79,10 +89,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 = (domainListResult || {}).domains;
this.totalResults = (domainListResult || {}).totalResults || 0;
this.isLoading = false;
},
});
}

View File

@@ -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,9 +13,9 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarService } from '../registrar/registrar.service';
import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
export interface CreateAutoTimestamp {
creationTime: string;
@@ -52,7 +52,7 @@ export class DomainListService {
) {
return this.backendService
.getDomains(
this.registrarService.activeRegistrarId,
this.registrarService.registrarId(),
this.checkpointTime,
pageNumber,
resultsPerPage,
@@ -61,7 +61,7 @@ export class DomainListService {
)
.pipe(
tap((domainListResult: DomainListResult) => {
this.checkpointTime = domainListResult.checkpointTime;
this.checkpointTime = domainListResult?.checkpointTime;
})
);
}

File diff suppressed because one or more lines are too long

View File

@@ -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,23 +16,27 @@
&__logo {
color: inherit;
text-decoration: none;
margin-left: -10px;
}
&__menu-btn {
width: 25px;
height: 25px;
padding: 0;
}
&__header {
@media (max-width: 599px) {
.mat-toolbar {
padding: 0;
}
.console-app__logo {
font-size: 16px;
}
button {
padding-left: 0;
padding-right: 0;
width: 30px;
}
margin-top: 0;
margin-bottom: 15px;
.mat-toolbar {
background: transparent;
margin-bottom: 10px;
}
@media (max-width: 480px) {
}
&-user-icon {
margin-left: 20px;
}
}
}
.spacer {
flex: 1;
}

View File

@@ -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.

View File

@@ -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,6 +13,7 @@
// limitations under the License.
import { Component, EventEmitter, Output } from '@angular/core';
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
@Component({
selector: 'app-header',
@@ -22,6 +23,8 @@ import { Component, EventEmitter, Output } from '@angular/core';
export class HeaderComponent {
private isNavOpen = false;
constructor(protected breakpointObserver: BreakPointObserverService) {}
@Output() toggleNavOpen = new EventEmitter<boolean>();
toggleNavPane() {

View File

@@ -1,13 +1,52 @@
<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">
<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>

View File

@@ -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;
}
}
}

View File

@@ -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.

View File

@@ -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,38 @@
// 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 { BreakPointObserverService } from '../shared/services/breakPoint.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class HomeComponent {}
export class HomeComponent {
constructor(
protected breakPointObserverService: BreakPointObserverService,
private router: Router
) {}
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',
});
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,24 +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 { UserDataService } from 'src/app/shared/services/userData.service';
@Component({
selector: '[app-resources-widget]',
templateUrl: './resourcesWidget.component.html',
})
export class ResourcesWidgetComponent {
constructor(public userDataService: UserDataService) {}
}

View File

@@ -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>

View File

@@ -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]);
}
}

View File

@@ -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>

View File

@@ -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 {}

View File

@@ -0,0 +1,44 @@
<mat-tree
[dataSource]="dataSource"
[treeControl]="treeControl"
class="console-app__nav-tree"
>
<mat-tree-node
*matTreeNodeDef="let node"
matTreeNodeToggle
(click)="onClick(node)"
[class.active]="router.url.includes(node.path)"
>
<mat-icon class="console-app__nav-icon" *ngIf="node.iconName">
{{ node.iconName }}
</mat-icon>
{{ node.title }}
</mat-tree-node>
<mat-nested-tree-node
*matTreeNodeDef="let node; when: hasChild"
(click)="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>
<mat-icon class="console-app__nav-icon" *ngIf="node.iconName">
{{ 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>

View 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 25px 25px 0;
}
&.active {
border-radius: 0 25px 25px 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;
}
}
}

View File

@@ -0,0 +1,107 @@
// 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 } from '../app-routing.module';
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'],
})
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.unsubscribe();
}
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;
});
}
}

View File

@@ -1,7 +0,0 @@
<div class="console-app__emty-registrar">
<h1>
<mat-icon class="console-app__emty-registrar-icon">block</mat-icon>
</h1>
<h1>No registrar selected</h1>
<h4 class="mat-body-2">Please select a registrar</h4>
</div>

View File

@@ -1,45 +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 { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { RegistrarService } from './registrar.service';
@Component({
selector: 'app-empty-registrar',
templateUrl: './emptyRegistrar.component.html',
styleUrls: ['./emptyRegistrar.component.scss'],
})
export class EmptyRegistrar {
private registrarIdChangeSubscription?: Subscription;
constructor(
private route: ActivatedRoute,
protected registrarService: RegistrarService,
private router: Router
) {
this.registrarIdChangeSubscription =
registrarService.activeRegistrarIdChange.subscribe((newRegistrarId) => {
if (newRegistrarId) {
this.router.navigate([this.route.snapshot.paramMap.get('nextUrl')]);
}
});
}
ngOnDestroy() {
this.registrarIdChangeSubscription?.unsubscribe();
}
}

View File

@@ -0,0 +1,182 @@
<div class="console-new-registrar">
<button
mat-icon-button
aria-label="Back to registrars list"
(click)="goBack()"
>
<mat-icon>arrow_back</mat-icon>
</button>
<div class="spacer"></div>
<h1>Create a registrar</h1>
<form (ngSubmit)="save($event)" #form>
<h2>General</h2>
<section>
<mat-form-field appearance="outline">
<mat-label>Registrar Name: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.registrarName"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Registrar ID: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.registrarId"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Registrar email address: </mat-label>
<input
matInput
type="email"
[required]="true"
[(ngModel)]="newRegistrar.emailAddress"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Billing Accounts: </mat-label>
<textarea
matInput
required="true"
placeholder="USD=billing-id-for-usd
JPY=billing-id-for-yen"
[ngModel]="billingAccountMap"
(ngModelChange)="onBillingAccountMapChange($event)"
[ngModelOptions]="{ standalone: true }"
></textarea>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>IANA ID: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.ianaIdentifier"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>ICANN referral email: </mat-label>
<input
matInput
[required]="true"
type="email"
[(ngModel)]="newRegistrar.icannReferralEmail"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Drive ID: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.driveFolderId"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<h2>Contact Info</h2>
<section>
<mat-form-field appearance="outline">
<mat-label>Street address (Line 1): </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="localizedAddressStreet.line1"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Street address (Line 2)</mat-label>
<input
matInput
[required]="false"
[(ngModel)]="localizedAddressStreet.line2"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Street address (Line 3)</mat-label>
<input
matInput
[required]="false"
[(ngModel)]="localizedAddressStreet.line3"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>City: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.localizedAddress.city"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>State/Region: </mat-label>
<input
matInput
[required]="false"
[(ngModel)]="newRegistrar.localizedAddress.state"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>ZIP/Postal Code: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.localizedAddress.zip"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Country Code (e.g. US): </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.localizedAddress.countryCode"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<button
class="console-new-registrar__submit"
mat-flat-button
color="primary"
type="submit"
>
Save
</button>
</form>
</div>

View File

@@ -0,0 +1,20 @@
.console-new-registrar {
max-width: 616px;
h2 {
margin: 40px 0 25px 0 !important;
}
section {
margin-bottom: 20px;
}
mat-form-field {
display: block;
width: 100%;
}
&__submit {
margin: 30px 0;
}
}

View File

@@ -0,0 +1,99 @@
// 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,
ElementRef,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Registrar, RegistrarService } from './registrar.service';
interface LocalizedAddressStreet {
line1: string;
line2: string;
line3: string;
}
@Component({
selector: 'app-new-registrar',
templateUrl: './newRegistrar.component.html',
styleUrls: ['./newRegistrar.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export default class NewRegistrarComponent {
protected newRegistrar: Registrar;
protected localizedAddressStreet: LocalizedAddressStreet;
protected billingAccountMap: String = '';
@ViewChild('form') form!: ElementRef;
constructor(
private registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
this.newRegistrar = {
registrarId: '',
url: '',
whoisServer: '',
registrarName: '',
icannReferralEmail: '',
localizedAddress: {
city: '',
state: '',
zip: '',
countryCode: '',
},
};
this.localizedAddressStreet = {
line1: '',
line2: '',
line3: '',
};
}
onBillingAccountMapChange(val: String) {
const billingAccountMap: { [key: string]: string } = {};
this.newRegistrar.billingAccountMap = val.split('\n').reduce((acc, val) => {
const [currency, billingCode] = val.split('=');
acc[currency] = billingCode;
return acc;
}, billingAccountMap);
}
save(e: SubmitEvent) {
e.preventDefault();
if (this.form.nativeElement.checkValidity()) {
const { line1, line2, line3 } = this.localizedAddressStreet;
this.newRegistrar.localizedAddress.street = [line1, line2, line3].filter(
(v) => !!v
);
this.registrarService.createRegistrar(this.newRegistrar).subscribe({
complete: () => {
this.goBack();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
});
} else {
this.form.nativeElement.reportValidity();
}
}
goBack() {
this.registrarService.inNewRegistrarMode.set(false);
}
}

View File

@@ -1,64 +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 { TestBed } from '@angular/core/testing';
import { RegistrarGuard } from './registrar.guard';
import { Router, RouterStateSnapshot } from '@angular/router';
import { RegistrarService } from './registrar.service';
describe('RegistrarGuard', () => {
let guard: RegistrarGuard;
let dummyRegistrarService: RegistrarService;
let routeSpy: Router;
let dummyRoute: RouterStateSnapshot = {} as RouterStateSnapshot;
beforeEach(() => {
routeSpy = jasmine.createSpyObj<Router>('Router', ['navigate']);
dummyRegistrarService = { activeRegistrarId: '' } as RegistrarService;
TestBed.configureTestingModule({
providers: [
RegistrarGuard,
{ provide: Router, useValue: routeSpy },
{ provide: RegistrarService, useValue: dummyRegistrarService },
],
});
});
it('should not be able to activate when activeRegistrarId is empty', () => {
guard = TestBed.inject(RegistrarGuard);
const res = guard.canActivate();
expect(res).toBeFalsy();
});
it('should be able to activate when activeRegistrarId is not empty', () => {
TestBed.overrideProvider(RegistrarService, {
useValue: { activeRegistrarId: 'value' },
});
guard = TestBed.inject(RegistrarGuard);
const res = guard.canActivate();
expect(res).toBeTrue();
});
it('should navigate to registrars when activeRegistrarId is empty', () => {
const dummyRoute = { url: '/value' } as RouterStateSnapshot;
guard = TestBed.inject(RegistrarGuard);
guard.canActivate();
expect(routeSpy.navigate).toHaveBeenCalledOnceWith([
'/registrars',
{ nextUrl: '/value' },
]);
});
});

View File

@@ -1,45 +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 { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { RegistrarService } from './registrar.service';
@Injectable({
providedIn: 'root',
})
export class RegistrarGuard {
constructor(
private router: Router,
private registrarService: RegistrarService
) {}
canActivate(
_: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> | boolean {
if (this.registrarService.activeRegistrarId) {
return true;
}
return this.router.navigate([
`/empty-registrar`,
{ nextUrl: state.url || '' },
]);
}
}

View File

@@ -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,17 +14,24 @@
import { TestBed } from '@angular/core/testing';
import { RegistrarService } from './registrar.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { MatSnackBar } from '@angular/material/snack-bar';
import { BackendService } from '../shared/services/backend.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RegistrarService } from './registrar.service';
describe('RegistrarService', () => {
let service: RegistrarService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [BackendService],
imports: [],
providers: [
BackendService,
MatSnackBar,
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(RegistrarService);
});

View File

@@ -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,82 +12,140 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Observable, Subject, tap } from 'rxjs';
import { Injectable, computed, signal } from '@angular/core';
import { Observable, switchMap, tap } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { BackendService } from '../shared/services/backend.service';
import {
GlobalLoader,
GlobalLoaderService,
} from '../shared/services/globalLoader.service';
import { MatSnackBar } from '@angular/material/snack-bar';
export interface Address {
street?: string[];
city?: string;
countryCode?: string;
zip?: string;
state?: string;
export interface IpAllowListItem {
value: string;
}
export interface Registrar {
export interface Address {
city?: string;
countryCode?: string;
state?: string;
street?: string[];
zip?: string;
}
export interface SecuritySettingsBackendModel {
clientCertificate?: string;
failoverClientCertificate?: string;
ipAddressAllowList?: Array<string>;
// TODO: @ptkach At some point we want to add a back-end support for this
eppPasswordLastUpdated?: string;
}
export interface SecuritySettings
extends Omit<SecuritySettingsBackendModel, 'ipAddressAllowList'> {
ipAddressAllowList?: Array<IpAllowListItem>;
}
export interface WhoisRegistrarFields {
ianaIdentifier?: number;
icannReferralEmail: string;
localizedAddress: Address;
registrarId: string;
url: string;
whoisServer: string;
}
export interface Registrar
extends WhoisRegistrarFields,
SecuritySettingsBackendModel {
allowedTlds?: string[];
billingAccountMap?: object;
driveFolderId?: string;
emailAddress?: string;
faxNumber?: string;
ianaIdentifier?: number;
icannReferralEmail?: string;
ipAddressAllowList?: string[];
localizedAddress?: Address;
phoneNumber?: string;
registrarId: string;
registrarName: string;
registryLockAllowed?: boolean;
url?: string;
whoisServer?: string;
}
@Injectable({
providedIn: 'root',
})
export class RegistrarService implements GlobalLoader {
activeRegistrarId: string = '';
registrars: Registrar[] = [];
activeRegistrarIdChange: Subject<string> = new Subject<string>();
registrarId = signal<string>(
new URLSearchParams(document.location.hash.split('?')[1]).get(
'registrarId'
) || ''
);
registrars = signal<Registrar[]>([]);
registrar = computed<Registrar | undefined>(() =>
this.registrars().find((r) => r.registrarId === this.registrarId())
);
inNewRegistrarMode = signal(false);
registrarsLoaded: Promise<void>;
constructor(
private backend: BackendService,
private globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar
private _snackBar: MatSnackBar,
private router: Router
) {
this.loadRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
this.registrarsLoaded = new Promise((resolve) => {
this.loadRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
resolve();
});
});
this.globalLoader.startGlobalLoader(this);
}
public get registrar(): Registrar {
return this.registrars.filter(
(r) => r.registrarId === this.activeRegistrarId
)[0];
}
public updateSelectedRegistrar(registrarId: string) {
this.activeRegistrarId = registrarId;
this.activeRegistrarIdChange.next(registrarId);
if (registrarId !== this.registrarId()) {
this.registrarId.set(registrarId);
// add registrarId to url query params, so that we can pick it up after page refresh
this.router.navigate([], {
queryParams: { registrarId },
queryParamsHandling: 'merge',
});
}
}
public loadRegistrars(): Observable<Registrar[]> {
return this.backend.getRegistrars().pipe(
tap((registrars) => {
if (registrars) {
this.registrars = registrars;
this.registrars.set(registrars);
}
})
);
}
createRegistrar(registrar: Registrar) {
return this.backend
.createRegistrar(registrar)
.pipe(switchMap((_) => this.loadRegistrars()));
}
updateRegistrar(updatedRegistrar: Registrar) {
return this.backend.updateRegistrar(updatedRegistrar).pipe(
tap(() => {
this.registrars.set(
this.registrars().map((r) => {
if (r.registrarId === updatedRegistrar.registrarId) {
return updatedRegistrar;
}
return r;
})
);
})
);
}
loadingTimeout() {
this._snackBar.open('Timeout loading registrars');
}

View File

@@ -1,40 +1,99 @@
<div class="registrarDetails" *ngIf="registrarInEdit">
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
<div mat-dialog-content>
<div class="console-app__registrar-view">
<h1 class="mat-headline-4">Registrars</h1>
<mat-divider></mat-divider>
<div class="console-app__registrar-view-content">
<div class="console-app__registrar-view-controls">
<button mat-icon-button aria-label="Back to registrars list" backButton>
<mat-icon>arrow_back</mat-icon>
</button>
<div class="spacer"></div>
@if(!inEdit && !registrarNotFound) {
<button
mat-flat-button
color="primary"
aria-label="Edit Registrar"
(click)="inEdit = true"
>
<mat-icon>edit</mat-icon>
Edit
</button>
<button mat-icon-button aria-label="Delete Registrar">
<mat-icon>delete</mat-icon>
</button>
}
</div>
@if(registrarNotFound) {
<h1>Registrar not found</h1>
} @else {
<h1>{{ registrarInEdit.registrarId }}</h1>
<h2 *ngIf="registrarInEdit.registrarName !== registrarInEdit.registrarId">
{{ registrarInEdit.registrarName }}
</h2>
@if(inEdit) {
<form (ngSubmit)="saveAndClose()">
<mat-form-field class="registrarDetails__input">
<mat-label>Registry Lock:</mat-label>
<mat-select
[(ngModel)]="registrarInEdit.registryLockAllowed"
name="registryLockAllowed"
>
<mat-option [value]="true">True</mat-option>
<mat-option [value]="false">False</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="registrarDetails__input">
<mat-label>Onboarded TLDs: </mat-label>
<mat-chip-grid #chipGrid aria-label="Enter TLD">
<mat-chip-row
*ngFor="let tld of registrarInEdit.allowedTlds"
(removed)="removeTLD(tld)"
<div>
<mat-form-field appearance="outline">
<mat-label>Registry Lock:</mat-label>
<mat-select
[(ngModel)]="registrarInEdit.registryLockAllowed"
name="registryLockAllowed"
>
{{ tld }}
<button matChipRemove aria-label="'remove ' + tld">
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
</mat-chip-grid>
<input
placeholder="New tld..."
[matChipInputFor]="chipGrid"
(matChipInputTokenEnd)="addTLD($event)"
/>
</mat-form-field>
<mat-dialog-actions>
<button mat-button (click)="this.params?.close()">Cancel</button>
<button type="submit" mat-button color="primary">Save</button>
</mat-dialog-actions>
<mat-option [value]="true">True</mat-option>
<mat-option [value]="false">False</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline">
<mat-label>Onboarded TLDs: </mat-label>
<mat-chip-grid #chipGrid aria-label="Enter TLD">
<mat-chip-row
*ngFor="let tld of registrarInEdit.allowedTlds"
(removed)="removeTLD(tld)"
>
{{ tld }}
<button matChipRemove aria-label="'remove ' + tld">
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
</mat-chip-grid>
<input
placeholder="New tld..."
[matChipInputFor]="chipGrid"
(matChipInputTokenEnd)="addTLD($event)"
/>
</mat-form-field>
</div>
<button
mat-flat-button
color="primary"
aria-label="Edit Registrar"
type="submit"
>
Save
</button>
</form>
} @else {
<mat-card appearance="outlined">
<mat-card-content>
<mat-list role="list">
<mat-list-item role="listitem">
<h2>Registrar details</h2>
</mat-list-item>
<mat-divider></mat-divider>
@for (column of columns; track column.columnDef) {
<mat-list-item role="listitem">
<span class="console-app__list-key">{{ column.header }} </span>
<span
class="console-app__list-value"
[innerHTML]="column.cell(registrarInEdit).replace('<br/>', ' ')"
></span>
</mat-list-item>
<mat-divider></mat-divider>
}
</mat-list>
</mat-card-content>
</mat-card>
} }
</div>
</div>

View File

@@ -1,8 +1,19 @@
.registrarDetails {
min-width: 30vw;
&__input {
display: block;
margin-top: 0.5rem;
.console-app {
&__registrar-view {
&-controls {
display: flex;
align-items: center;
margin: 20px 0;
}
}
&__registrar-view-content {
max-width: 616px;
mat-divider:last-child {
display: none;
}
mat-form-field {
margin-bottom: 20px;
width: 100%;
}
}
}

View File

@@ -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,45 +12,62 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Registrar, RegistrarService } from './registrar.service';
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { MatChipInputEvent } from '@angular/material/chips';
import { DialogBottomSheetContent } from '../shared/components/dialogBottomSheet.component';
type RegistrarDetailsParams = {
close: Function;
data: {
registrar: Registrar;
};
};
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { Registrar, RegistrarService } from './registrar.service';
import { RegistrarComponent, columns } from './registrarsTable.component';
@Component({
selector: 'app-registrar-details',
templateUrl: './registrarDetails.component.html',
styleUrls: ['./registrarDetails.component.scss'],
})
export class RegistrarDetailsComponent implements DialogBottomSheetContent {
export class RegistrarDetailsComponent implements OnInit {
public static PATH = 'registrars/:id';
inEdit: boolean = false;
registrarInEdit!: Registrar;
params?: RegistrarDetailsParams;
registrarNotFound: boolean = false;
columns = columns.filter((c) => !c.hiddenOnDetailsCard);
private subscription!: Subscription;
constructor(protected registrarService: RegistrarService) {}
constructor(
protected registrarService: RegistrarService,
private route: ActivatedRoute,
private _snackBar: MatSnackBar,
private router: Router
) {}
init(params: RegistrarDetailsParams) {
this.params = params;
this.registrarInEdit = JSON.parse(
JSON.stringify(this.params.data.registrar)
);
}
saveAndClose() {
this.params?.close();
ngOnInit(): void {
this.registrarService.registrarsLoaded.then(() => {
this.subscription = this.route.paramMap.subscribe((params: ParamMap) => {
this.registrarInEdit = structuredClone(
this.registrarService
.registrars()
.filter((r) => r.registrarId === params.get('id'))[0]
);
if (!this.registrarInEdit) {
this._snackBar.open(
`Registrar with id ${params.get('id')} is not available`
);
this.registrarNotFound = true;
} else {
this.registrarNotFound = false;
}
});
});
}
addTLD(e: MatChipInputEvent) {
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds || [];
this.removeTLD(e.value); // Prevent dups
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.concat(
[e.value.toLowerCase()]
);
this.registrarInEdit.allowedTlds = [
...this.registrarInEdit.allowedTlds,
e.value.toLowerCase(),
];
}
removeTLD(tld: string) {
@@ -58,4 +75,22 @@ export class RegistrarDetailsComponent implements DialogBottomSheetContent {
(v) => v != tld
);
}
saveAndClose() {
this.registrarService.updateRegistrar(this.registrarInEdit).subscribe({
complete: () => {
this.router.navigate([RegistrarComponent.PATH], {
queryParamsHandling: 'merge',
});
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
});
this.inEdit = false;
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}

View File

@@ -1,29 +1,29 @@
<div class="console-app__registrar">
<button
mat-button
[routerLink]="'/settings/registrars'"
routerLinkActive="active"
*ngIf="isMobile; else desktop"
>
{{ registrarService.activeRegistrarId || "Select registrar" }}
<mat-icon>open_in_new</mat-icon>
</button>
<ng-template #desktop>
<mat-form-field class="mat-form-field-density-5" appearance="fill">
<mat-label>Registrar</mat-label>
<mat-select
[ngModel]="registrarService.activeRegistrarId"
(selectionChange)="
registrarService.updateSelectedRegistrar($event.value)
"
<mat-form-field class="example-full-width" appearance="outline">
<mat-label>Registrar</mat-label>
<input
type="text"
placeholder=""
aria-label="Select Registrar"
matInput
[ngModel]="registrarInput()"
(ngModelChange)="registrarInput.set($event)"
[ngModelOptions]="{ standalone: true }"
(focus)="onFocus()"
[matAutocomplete]="auto"
/>
<mat-autocomplete
autoActiveFirstOption
#auto="matAutocomplete"
(optionSelected)="onSelect($event.option.value)"
>
@for (registrarId of filteredOptions; track registrarId) {
<mat-option
[value]="registrarId"
selected="registrarId === registrarService.registrarId()"
>{{ registrarId }}</mat-option
>
<mat-option
*ngFor="let registrar of registrarService.registrars"
[value]="registrar.registrarId"
>
{{ registrar.registrarId }}
</mat-option>
</mat-select>
</mat-form-field>
</ng-template>
}
</mat-autocomplete>
</mat-form-field>
</div>

View File

@@ -3,16 +3,5 @@
display: flex;
justify-content: center;
align-items: baseline;
// Fix for angular v15 label issue
// https://github.com/angular/components/issues/26579
.mat-mdc-floating-label {
display: inline !important;
}
.mat-mdc-select-arrow-wrapper {
transform: translateY(0) !important;
}
}
&__title {
margin-right: 1rem;
}
}

View File

@@ -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,6 +14,11 @@
import { ComponentFixture, TestBed } from '@angular/core/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 { RegistrarSelectorComponent } from './registrarSelector.component';
describe('RegistrarSelectorComponent', () => {
@@ -23,6 +28,12 @@ describe('RegistrarSelectorComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RegistrarSelectorComponent],
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(RegistrarSelectorComponent);

View File

@@ -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,35 +12,36 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { Component, computed, effect, signal } from '@angular/core';
import { RegistrarService } from './registrar.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { distinctUntilChanged } from 'rxjs';
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
@Component({
selector: 'app-registrar-selector',
templateUrl: './registrarSelector.component.html',
styleUrls: ['./registrarSelector.component.scss'],
})
export class RegistrarSelectorComponent implements OnInit {
protected isMobile: boolean = false;
export class RegistrarSelectorComponent {
registrarInput = signal<string>(this.registrarService.registrarId());
filteredOptions?: string[];
allRegistrarIds = computed(() =>
this.registrarService.registrars().map((r) => r.registrarId)
);
readonly breakpoint$ = this.breakpointObserver
.observe([MOBILE_LAYOUT_BREAKPOINT])
.pipe(distinctUntilChanged());
constructor(
protected registrarService: RegistrarService,
protected breakpointObserver: BreakpointObserver
) {}
ngOnInit(): void {
this.breakpoint$.subscribe(() => this.breakpointChanged());
constructor(protected registrarService: RegistrarService) {
effect(() => {
const filterValue = this.registrarInput().toLowerCase();
this.filteredOptions = this.allRegistrarIds().filter((option) =>
option.toLowerCase().includes(filterValue)
);
});
}
private breakpointChanged() {
this.isMobile = this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT);
onFocus() {
// We reset the list of options after selection, so that user doesn't have to clear it out
this.filteredOptions = this.allRegistrarIds();
}
onSelect(registrarId: string) {
this.registrarService.updateSelectedRegistrar(registrarId);
}
}

View File

@@ -1,4 +1,19 @@
@if(registrarService.inNewRegistrarMode()) {
<app-new-registrar />
} @else {
<div class="console-app__registrars">
<div class="console-app__registrars-header">
<h1 class="mat-headline-4">Registrars</h1>
<button
mat-flat-button
color="primary"
(click)="openNewRegistrar()"
aria-label="Add new registrar"
>
<mat-icon>add</mat-icon>
Add new registrar
</button>
</div>
<mat-form-field class="console-app__registrars-filter">
<mat-label>Search</mat-label>
<input
@@ -11,24 +26,10 @@
</mat-form-field>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z8"
class="mat-elevation-z0"
class="console-app__registrars-table"
matSort
>
<ng-container matColumnDef="edit">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let row">
<button
mat-icon-button
color="primary"
aria-label="Edit registrar"
(click)="openDetails($event, row)"
>
<mat-icon>edit</mat-icon>
</button>
</mat-cell>
</ng-container>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
@@ -39,16 +40,15 @@
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="registrarService.updateSelectedRegistrar(row.registrarId)"
(click)="openDetails(row.registrarId)"
></mat-row>
</mat-table>
<mat-paginator
class="mat-elevation-z8"
class="mat-elevation-z0"
[pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
></mat-paginator>
<app-dialog-bottom-sheet-wrapper
#registrarDetailsView
></app-dialog-bottom-sheet-wrapper>
</div>
}

View File

@@ -2,7 +2,6 @@
$min-width: 756px;
&__registrars {
margin-top: 1.5rem;
width: 100%;
overflow: auto;
}
@@ -16,6 +15,11 @@
min-width: $min-width !important;
}
&__registrars-header {
display: flex;
justify-content: space-between;
}
.mat-mdc-paginator {
min-width: $min-width !important;
}
@@ -27,7 +31,7 @@
}
&-driveId {
min-width: 200px;
word-break: break-all;
word-break: break-word;
}
&-registryLockAllowed {
max-width: 80px;

View File

@@ -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,12 +14,13 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrarComponent } from './registrarsTable.component';
import { BackendService } from '../shared/services/backend.service';
import { ActivatedRoute } from '@angular/router';
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 { ActivatedRoute } from '@angular/router';
import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarComponent } from './registrarsTable.component';
describe('RegistrarComponent', () => {
let component: RegistrarComponent;
@@ -28,14 +29,12 @@ describe('RegistrarComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RegistrarComponent],
imports: [
HttpClientTestingModule,
MaterialModule,
BrowserAnimationsModule,
],
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();

View File

@@ -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,13 +12,63 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
import { Registrar, RegistrarService } from './registrar.service';
import { Component, effect, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { RegistrarDetailsComponent } from './registrarDetails.component';
import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet.component';
import { Router } from '@angular/router';
import { Registrar, RegistrarService } from './registrar.service';
export const columns = [
{
columnDef: 'registrarId',
header: 'Registrar Id',
cell: (record: Registrar) => `${record.registrarId || ''}`,
hiddenOnDetailsCard: true,
},
{
columnDef: 'registrarName',
header: 'Name',
cell: (record: Registrar) => `${record.registrarName || ''}`,
hiddenOnDetailsCard: true,
},
{
columnDef: 'allowedTlds',
header: 'TLDs',
cell: (record: Registrar) => `${(record.allowedTlds || []).join(', ')}`,
},
{
columnDef: 'emailAddress',
header: 'Username',
cell: (record: Registrar) => `${record.emailAddress || ''}`,
},
{
columnDef: 'ianaIdentifier',
header: 'IANA ID',
cell: (record: Registrar) => `${record.ianaIdentifier || ''}`,
},
{
columnDef: 'billingAccountMap',
header: 'Billing Accounts',
cell: (record: Registrar) =>
`${Object.entries(record.billingAccountMap || {}).reduce(
(acc, [key, val]) => {
return `${acc}${key}=${val}<br/>`;
},
''
)}`,
},
{
columnDef: 'registryLockAllowed',
header: 'Registry Lock',
cell: (record: Registrar) => `${record.registryLockAllowed}`,
},
{
columnDef: 'driveId',
header: 'Drive ID',
cell: (record: Registrar) => `${record.driveFolderId || ''}`,
},
];
@Component({
selector: 'app-registrar',
@@ -29,66 +79,23 @@ import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet
export class RegistrarComponent {
public static PATH = 'registrars';
dataSource: MatTableDataSource<Registrar>;
columns = [
{
columnDef: 'registrarId',
header: 'Registrar Id',
cell: (record: Registrar) => `${record.registrarId || ''}`,
},
{
columnDef: 'registrarName',
header: 'Name',
cell: (record: Registrar) => `${record.registrarName || ''}`,
},
{
columnDef: 'allowedTlds',
header: 'TLDs',
cell: (record: Registrar) => `${(record.allowedTlds || []).join(', ')}`,
},
{
columnDef: 'emailAddress',
header: 'Username',
cell: (record: Registrar) => `${record.emailAddress || ''}`,
},
{
columnDef: 'ianaIdentifier',
header: 'IANA ID',
cell: (record: Registrar) => `${record.ianaIdentifier || ''}`,
},
{
columnDef: 'billingAccountMap',
header: 'Billing Accounts',
cell: (record: Registrar) =>
// @ts-ignore - completely legit line, but TS keeps complaining
`${Object.entries(record.billingAccountMap).reduce(
(acc, [key, val]) => {
return `${acc}${key}=${val}<br/>`;
},
''
)}`,
},
{
columnDef: 'registryLockAllowed',
header: 'Registry Lock',
cell: (record: Registrar) => `${record.registryLockAllowed}`,
},
{
columnDef: 'driveId',
header: 'Drive ID',
cell: (record: Registrar) => `${record.driveFolderId || ''}`,
},
];
displayedColumns = ['edit'].concat(this.columns.map((c) => c.columnDef));
columns = columns;
displayedColumns = this.columns.map((c) => c.columnDef);
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild('registrarDetailsView')
detailsComponentWrapper!: DialogBottomSheetWrapper;
constructor(protected registrarService: RegistrarService) {
constructor(
protected registrarService: RegistrarService,
private router: Router
) {
this.dataSource = new MatTableDataSource<Registrar>(
registrarService.registrars
registrarService.registrars()
);
effect(() => {
this.dataSource.data = registrarService.registrars();
});
}
ngAfterViewInit() {
@@ -96,16 +103,19 @@ export class RegistrarComponent {
this.dataSource.sort = this.sort;
}
openDetails(event: MouseEvent, registrar: Registrar) {
event.stopPropagation();
this.detailsComponentWrapper.open<RegistrarDetailsComponent>(
RegistrarDetailsComponent,
{ registrar }
);
openDetails(registrarId: string) {
this.router.navigate(['registrars/', registrarId], {
queryParamsHandling: 'merge',
});
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
// TODO: consider filteing out only by registrar name
this.dataSource.filter = filterValue.trim().toLowerCase();
}
openNewRegistrar() {
this.registrarService.inNewRegistrarMode.set(true);
}
}

View File

@@ -0,0 +1,16 @@
<h1 class="mat-headline-4">Resources</h1>
<div class="console-app__resources">
<div>
<div class="console-app__resources-subhead">Technical resources</div>
<a
class="text-l"
href="{{ userDataService.userData.technicalDocsUrl }}"
target="_blank"
>View onboarding FAQs, TLD information, and technical documentation on
Google Drive</a
>
</div>
<div>
<img src="./assets/resources.png" />
</div>
</div>

View File

@@ -0,0 +1,22 @@
.console-app__resources {
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: 30px;
}
img {
aspect-ratio: 1 / 1;
width: 100%;
}
&-subhead {
font-size: 20px;
margin-bottom: 20px;
}
}

View File

@@ -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,12 +13,14 @@
// limitations under the License.
import { Component } from '@angular/core';
import { UserDataService } from 'src/app/shared/services/userData.service';
import { UserDataService } from '../shared/services/userData.service';
@Component({
selector: '[app-contact-widget]',
templateUrl: './contactWidget.component.html',
selector: 'app-resources',
templateUrl: './resources.component.html',
styleUrls: ['./resources.component.scss'],
})
export class ContactWidgetComponent {
constructor(public userDataService: UserDataService) {}
export class ResourcesComponent {
public static PATH = 'resources';
constructor(protected userDataService: UserDataService) {}
}

View File

@@ -1,47 +1,40 @@
<div *ngIf="loading" class="contact__loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div *ngIf="!loading">
<div
class="contact__empty-contacts"
*ngIf="contactService.contacts.length === 0"
@if(contactService.isContactDetailsView || contactService.isContactNewView) {
<app-contact-details></app-contact-details>
} @else {
<div class="console-app__contacts-controls">
<button
mat-flat-button
color="primary"
(click)="openNewContact()"
aria-label="Add new contact"
>
<mat-icon class="contact__empty-contacts-icon secondary-text"
<mat-icon>add</mat-icon>
Add new contact
</button>
</div>
<div class="console-app__contacts">
@if (contactService.contacts().length === 0) {
<div class="console-app__empty-contacts">
<mat-icon class="console-app__empty-contacts-icon secondary-text"
>apps_outage</mat-icon
>
<h1>No contacts found</h1>
</div>
<div *ngFor="let group of groupedData">
<div class="contact__cards-wrapper" *ngIf="group.contacts.length">
<h3>{{ group.label }}s</h3>
<mat-divider></mat-divider>
<div class="contact__cards">
<mat-card class="contact__card" *ngFor="let contact of group.contacts">
<mat-card-title>{{ contact.name }}</mat-card-title>
<p *ngIf="contact.phoneNumber">{{ contact.phoneNumber }}</p>
<p *ngIf="contact.emailAddress">{{ contact.emailAddress }}</p>
<mat-card-actions class="contact__card-actions">
<button
mat-button
color="primary"
(click)="openDetails($event, contact)"
>
<mat-icon>edit</mat-icon>Edit
</button>
<button mat-button color="accent" (click)="deleteContact(contact)">
<mat-icon>delete</mat-icon>Delete
</button>
</mat-card-actions>
</mat-card>
</div>
</div>
</div>
<div class="contact__actions">
<button mat-raised-button color="primary" (click)="openCreateNew($event)">
<mat-icon>add</mat-icon>Create a Contact
</button>
</div>
<app-dialog-bottom-sheet-wrapper
#contactDetailsWrapper
></app-dialog-bottom-sheet-wrapper>
} @else {
<mat-table [dataSource]="dataSource" class="mat-elevation-z0">
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="openDetails(row)"
></mat-row>
</mat-table>
}
</div>
}

View File

@@ -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,30 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
.contact {
&__cards-wrapper {
margin-top: 22px;
}
&__card {
width: 400px;
padding: 15px;
}
&__card-actions {
padding: 0;
}
&__loading {
margin: 2rem 0;
}
&__cards {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
}
&__actions {
display: flex;
justify-content: flex-start;
margin: 2rem 0;
.console-app {
&__contacts {
margin-top: -10px;
width: 100%;
overflow: auto;
mat-table {
min-width: 800px;
}
.contact__name-column-title {
color: #5f6368;
font-weight: 500;
padding: 10px 0;
}
.contact__name-column-roles {
margin-bottom: 10px;
}
}
&__empty-contacts {
display: flex;
@@ -49,22 +41,9 @@
font-size: 4rem;
margin-top: 1.5rem;
}
}
.contact-details {
&__input {
width: 100%;
}
&__group {
margin: 20px 0;
display: flex;
align-items: baseline;
}
&__group-content {
display: flex;
flex-wrap: wrap;
max-width: 450px;
* {
min-width: 200px;
}
&__contacts-controls {
position: absolute;
right: 20px;
top: 5px;
}
}

Some files were not shown because too many files have changed in this diff Show More