1
0
mirror of https://github.com/google/nomulus synced 2026-01-16 18:53:18 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Sarah Botwinick
3f3c426240 Add premium name check for default token 2023-04-24 15:23:19 -04:00
2255 changed files with 84614 additions and 104257 deletions

View File

@@ -6,6 +6,8 @@ on:
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'master' ]
schedule:
- cron: '24 4 * * 2'
jobs:
analyze:
@@ -25,17 +27,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set Java version
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -45,20 +41,11 @@ 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@v4
with:
build-scan-publish: true
build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
build-scan-terms-of-use-agree: "yes"
- name: Execute Gradle build
run: ./gradlew --no-daemon --no-build-cache --no-configuration-cache --rerun-tasks clean build -x test -x jIFC
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
#- name: Autobuild
# uses: github/codeql-action/autobuild@v3
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# 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
@@ -71,6 +58,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,25 +0,0 @@
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

View File

@@ -1,23 +0,0 @@
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

7
.gitignore vendored
View File

@@ -31,7 +31,6 @@ tmp/
local.properties
.settings/
.loadpath
.DS_Store
# Eclipse Core
.project
@@ -79,7 +78,6 @@ autogenerated/
**/*.iml
nomulus.ipr
nomulus.iws
**/classpath.index
# Auto-generated java classes by Intellij
*/src/main/generated/
@@ -105,7 +103,7 @@ nomulus.iws
.gradle/
**/build
cloudbuild-caches/
**/node_modules/**
node_modules/**
/repos/
# Compiled JS/CSS code
@@ -117,6 +115,3 @@ core/**/registrar_dbg*.css
# Appengine generated files
core/WEB-INF/appengine-generated/*.bin
core/WEB-INF/appengine-generated/*.xml
# jEnv
.java-version

View File

@@ -1 +0,0 @@
21

View File

@@ -34,8 +34,6 @@ Guy Bensky <guyben@google.com>
Weimin Yu <weiminyu@google.com>
Shicong Huang <shicong@google.com>
Gustav Brodman <gbrodman@google.com>
Aman Sanger <sangera@google.com>
Sarah Botwinick <sarahbot@google.com>
Legina Chen <legina@google.com>
Rachel Guan <rachelguan@google.com>
Juan Celhay <jicelhay@google.com>

View File

@@ -12,16 +12,16 @@ Nomulus is an open source, scalable, cloud-based service for operating
[top-level domains](https://en.wikipedia.org/wiki/Top-level_domain) (TLDs). It
is the authoritative source for the TLDs that it runs, meaning that it is
responsible for tracking domain name ownership and handling registrations,
renewals, availability checks, and WHOIS requests. End-user registrants (i.e.,
renewals, availability checks, and WHOIS requests. End-user registrants (i.e.
people or companies that want to register a domain name) use an intermediate
domain name registrar acting on their behalf to interact with the registry.
Nomulus runs on [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine)
and is written primarily in Java. It is the software that
[Google Registry](https://www.registry.google/) uses to operate TLDs such as .google,
.app, .how, .soy, and .みんな. It can run any number of TLDs in a single shared registry
system using horizontal scaling. Its source code is publicly available in this
repository under the [Apache 2.0 free and open source license](https://www.apache.org/licenses/LICENSE-2.0).
Nomulus runs on [Google App Engine][gae] and is written primarily in Java. It is
the software that [Google Registry](https://www.registry.google/) uses to
operate TLDs such as .google, .app, .how, .soy, and .みんな. It can run any
number of TLDs in a single shared registry system using horizontal scaling. Its
source code is publicly available in this repository under the [Apache 2.0 free
and open source license](https://www.apache.org/licenses/LICENSE-2.0).
## Getting started
@@ -30,8 +30,8 @@ running system:
* [Install
guide](https://github.com/google/nomulus/blob/master/docs/install.md)
* View the source code for the [Main HTTP server](https://github.com/google/nomulus/tree/master/core/src/main/java/google/registry)
and for the [EPP proxy](https://github.com/google/nomulus/tree/master/proxy/src/main/java/google/registry)
* View the source code for the [GAE app](https://github.com/google/nomulus/tree/master/core/src/main/java/google/registry)
and for the [GKE proxy](https://github.com/google/nomulus/tree/master/proxy/src/main/java/google/registry)
* [Other docs](https://github.com/google/nomulus/tree/master/docs)
* [Javadoc](https://javadoc.nomulus.foo/)
* [Nomulus discussion
@@ -54,11 +54,9 @@ Nomulus has the following capabilities:
checking, updating, and transferring domain names.
* **[DNS](https://en.wikipedia.org/wiki/Domain_Name_System) interface**: The
registry provides a pluggable interface that can be implemented to handle
different DNS providers. It includes a sample implementation using [Google
Cloud DNS](https://cloud.google.com/dns/), as well as an RFC 2136 compliant
implementation that works with BIND. If you are using Google Cloud DNS, you
may need to understand its capabilities and provide your own
multi-[AS](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\)) solution.
different DNS providers. It includes a sample implementation using Google
Cloud DNS as well as an RFC 2136 compliant implementation that works with
BIND.
* **[WHOIS](https://en.wikipedia.org/wiki/WHOIS)**: A text-based protocol that
returns ownership and contact information on registered domain names.
* **[Registration Data Access Protocol
@@ -70,7 +68,7 @@ Nomulus has the following capabilities:
provider to allow take-over by another registry operator in the event of
serious failure. This is required by ICANN for all [new
gTLDs](https://newgtlds.icann.org/).
* **Premium pricing**: Communicates prices for premium domain names (i.e.,
* **Premium pricing**: Communicates prices for premium domain names (i.e.
those that are highly desirable) and supports configurable premium
registration and renewal prices. An extensible interface allows fully
programmatic pricing.
@@ -93,50 +91,56 @@ Nomulus has the following capabilities:
* **Administrative tool**: Performs the full range of administrative tasks
needed to manage a running registry system, including creating and
configuring new TLDs.
* **Secure storage of cryptographic keys**: A keyring interface is
provided for plugging in your own implementation (see [configuration
doc](https://github.com/google/nomulus/blob/master/docs/configuration.md)
for details), and an implementation based on
[Google Cloud Secret Manager](https://cloud.google.com/security/products/secret-manager) is
available.
* **TPC Proxy**: Nomulus is built on top of the [Jetty](https://jetty.org/)
container that implements the [Jakarta Servlet](https://jakarta.ee/specifications/servlet/)
specification and only serves HTTP/S traffic. A proxy to translate raw TCP traffic (e.g., EPP)
to and from HTTP is provided.
Instructions on setting up the proxy
are [available](https://github.com/google/nomulus/blob/master/docs/proxy-setup.md).
The proxy can either run in a separate cluster and communicate to Nomulus public HTTP
endpoints via the Internet, or as a sidecar with the Nomulus image in the same pod and
communicate to it via loopback.
* **DNS interface**: An interface for DNS operations is provided so you can
write an implementation for your chosen provider, along with a sample
implementation that uses [Google Cloud DNS](https://cloud.google.com/dns/).
If you are using Google Cloud DNS you may need to understand its
capabilities and provide your own
multi-[AS](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\))
solution.
* **GAE Proxy**: App Engine Standard only serves HTTP/S traffic. A proxy to
forward traffic on EPP and WHOIS ports to App Engine via HTTPS is provided.
Instructions on setting up the proxy on
[Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)
is [available](https://github.com/google/nomulus/blob/master/docs/proxy-setup.md).
Running the proxy on GKE supports IPv4 and IPv6 access, per ICANN's
requirements for gTLDs. The proxy can also run as a single jar file, or on
other Kubernetes providers, with modifications.
## Additional components
Registry operators interested in deploying Nomulus will likely require some
additional components that need to be configured separately.
additional components that are need to be configured separately.
* A way to invoice registrars for domain name registrations and accept
payments. Nomulus records the information required to generate invoices in
[billing
events](https://github.com/google/nomulus/blob/master/docs/code-structure.md#billing-events).
* Fully automated reporting to meet ICANN's requirements for gTLDs. Nomulus
includes substantial reporting functionality, but some additional work will
includes substantial reporting functionality but some additional work will
be required by the operator in this area.
* A secure method for storing cryptographic keys. A keyring interface is
provided for plugging in your own implementation (see [configuration
doc](https://github.com/google/nomulus/blob/master/docs/configuration.md)
for details).
* System status and uptime monitoring.
## Outside references
* [Identity Digital](http://identity.digital) has helped review the code and
provided valuable feedback.
* [Donuts](http://donuts.domains) Registry has helped review the code and
provided valuable feedback
* [CoCCa](http://cocca.org.nz) and [FRED](https://fred.nic.cz) are other
open-source registry platforms in use by many TLDs.
open-source registry platforms in use by many TLDs
* We are not aware of any fully open source domain registrar projects, but
open source EPP Toolkits (not yet tested with Nomulus; may require
integration work) include:
* [Universal Registry/Registrar Toolkit](https://sourceforge.net/projects/epp-rtk/)
* [EPP RTK Project](http://epp-rtk.sourceforge.net/)
* [CentralNic](https://www.centralnic.com/registry/labs)
* [ari-toolkit](https://github.com/AusRegistry/ari-toolkit)
* [Net::DRI](https://metacpan.org/pod/Net::DRI)
* Some Open Source DNS Projects that may be useful, but which we have not
tested:
* [AtomiaDNS](https://github.com/atomia/atomiadns)
* [PowerDNS](https://github.com/PowerDNS/pdns)
* [AtomiaDNS](http://atomiadns.com/)
* [PowerDNS](https://doc.powerdns.com/md/)
[gae]:https://cloud.google.com/appengine/docs/about-the-standard-environment

View File

@@ -47,9 +47,24 @@ war {
if (project.path == ":services:default") {
war {
from("${coreResourcesDir}/google/registry/ui/html") {
include "*.html"
into("registrar")
from("${rootDir}/console-webapp/dist/console-webapp") {
include "**/*"
into("console")
}
from("${coreResourcesDir}/google/registry/ui") {
include "registrar_bin.js"
if (environment != "production") {
include "registrar_bin.js.map"
}
into("assets/js")
}
from("${coreResourcesDir}/google/registry/ui/css") {
include "registrar*"
into("assets/css")
}
from("${coreResourcesDir}/google/registry/ui/assets/images") {
include "**/*"
into("assets/images")
}
}
}
@@ -88,10 +103,11 @@ explodeWar.doLast {
file("${it.explodedAppDirectory}/WEB-INF/lib/tools.jar").setWritable(true)
}
appengineDeployAll.mustRunAfter ':console-webapp:deploy'
appengineDeployAll.finalizedBy ':deployCloudSchedulerAndQueue'
appengineDeployAll.finalizedBy ':cloudSchedulerDeployer'
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,26 +29,25 @@ buildscript {
dependencies {
classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.4.1'
classpath 'net.ltgt.gradle:gradle-errorprone-plugin:3.1.0'
classpath 'net.ltgt.gradle:gradle-errorprone-plugin:2.0.2'
classpath 'org.sonatype.aether:aether-api:1.13.1'
classpath 'org.sonatype.aether:aether-impl:1.13.1'
}
}
plugins {
// Java static analysis plugins.
// Re-enable when compatible with Gradle 8
// id 'nebula.lint' version '16.0.2'
id 'net.ltgt.errorprone' version '3.1.0'
// Java static analysis plugins. Keep versions consistent with
// ./buildSrc/build.gradle
id 'nebula.lint' version '16.0.2'
id 'net.ltgt.errorprone' version '2.0.2'
id 'checkstyle'
id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'com.github.johnrengelman.shadow' version '5.1.0'
// NodeJs plugin
id "com.github.node-gradle.node" version "3.0.1"
id 'idea'
id 'com.diffplug.spotless' version '6.20.0'
id 'com.diffplug.gradle.spotless' version '3.25.0'
id 'jacoco'
id 'com.dorongold.task-tree' version '2.1.0'
@@ -59,27 +58,59 @@ dependencyLocking {
}
node {
download = false
version = "22.7.0"
download = true
version = "16.14.0"
npmVersion = "6.14.11"
}
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.
// Only do linting if the build is successful.
// Re-enable when compatible with Gradle 8
// gradleLint.autoLintAfterFailure = false
gradleLint.autoLintAfterFailure = false
// Paths to main and test sources.
ext.projectRootDir = "${rootDir}"
@@ -95,27 +126,30 @@ task stage {
description = 'Generates application directories for all services.'
}
// App-engine environment configuration. We set up all of the variables in
// the root project.
def environments = ['production', 'sandbox', 'alpha', 'crash', 'qa']
def gcpProject = null
apply from: "${rootDir.path}/projects.gradle"
if (environment == '') {
// Keep the project null, this will prevent deployment. Set the
// Keep the project null, this will prevent deployment. Set the
// environment to "alpha" because other code needs this property to
// explode the war file.
environment = 'alpha'
} else {
} else if (environment != 'production' && environment != 'sandbox') {
gcpProject = projects[environment]
if (gcpProject == null) {
throw new GradleException("-Penvironment must be one of " +
"${projects.keySet()}.")
}
project(':console-webapp').setProperty('configuration', environment)
}
rootProject.ext.environment = environment
rootProject.ext.gcpProject = gcpProject
rootProject.ext.baseDomain = baseDomains[environment]
rootProject.ext.prodOrSandboxEnv = environment in ['production', 'sandbox']
// Function to verify that the deployment parameters have been set.
@@ -173,32 +207,8 @@ allprojects {
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
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"]
options.forkOptions.javaHome =
file("${System.env.REAL_JAVA_HOME}")
}
}
}
@@ -245,12 +255,6 @@ 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
@@ -265,19 +269,14 @@ subprojects {
project.tasks.create(
taskName, com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
mergeServiceFiles()
archiveBaseName = binaryName
baseName = binaryName
if (mainClass != '') {
manifest {
attributes 'Main-Class': mainClass
}
}
// Build as a multi-release jar since we've got member jars (e.g., dnsjava
// and snakeyaml) that are multi-release.
manifest {
attributes 'Multi-Release': true
}
zip64 = true
archiveClassifier = ''
classifier = ''
archiveVersion = ''
configurations = configs
from srcOutput
@@ -306,7 +305,8 @@ subprojects {
afterEvaluate {
if (rootProject.enableDependencyLocking.toBoolean()
&& project.name != 'integration') {
&& project.name != 'integration'
&& project.name != 'java8compatibility') {
// 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
@@ -314,6 +314,9 @@ 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.
@@ -329,6 +332,11 @@ subprojects {
}
}
def services = [':services:default',
':services:backend',
':services:tools',
':services:pubapi']
// Set up all of the deployment projects.
if (services.contains(project.path)) {
@@ -340,12 +348,12 @@ subprojects {
apply from: "${rootDir.path}/java_common.gradle"
// 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
if (project.name != 'docs') {
compileJava {
// TODO: Remove this once we migrate off AppEngine.
options.release = 8
}
}
project.tasks.test.dependsOn runPresubmits
@@ -370,9 +378,9 @@ subprojects {
}
}
// No need to produce javadoc for the jetty subproject, which has no APIs to
// No need to produce javadoc for the docs subproject, which has no APIs to
// expose to users.
if (project.name != 'jetty') {
if (project.name != 'docs') {
javadocSource << project.sourceSets.main.allJava
javadocClasspath << project.sourceSets.main.runtimeClasspath
javadocClasspath << "${buildDir}/generated/sources/annotationProcessor/java/main"
@@ -380,17 +388,6 @@ 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.
//
@@ -421,6 +418,9 @@ if (verboseTestOutput.toBoolean()) {
}
task checkDependenciesDotGradle {
def buildSrcDepsFile = File.createTempFile('buildSrc', 'deps')
buildSrcDepsFile.deleteOnExit()
dependsOn createGetBuildSrcDirectDepsTask(buildSrcDepsFile)
doLast {
Set<String> depsInUse = []
@@ -433,7 +433,9 @@ task checkDependenciesDotGradle {
}
}
}
if (buildSrcDepsFile.exists()) {
depsInUse.addAll(buildSrcDepsFile.readLines())
}
def unusedDeps =
rootProject.dependencyMap.keySet()
.findAll { !depsInUse.contains(it) }
@@ -450,23 +452,34 @@ task checkDependenciesDotGradle {
}
tasks.build.dependsOn(tasks.checkDependenciesDotGradle)
def createGetBuildSrcDirectDepsTask(outputFileName) {
return tasks
.create(
"getBuildSrcDeps_${java.util.UUID.randomUUID()}".toString(),
Exec) {
workingDir "${rootDir}/buildSrc"
commandLine '../gradlew', 'exportDependencies',
"-PdependencyExportFile=${outputFileName}"
}
}
rootProject.ext {
invokeJavaDiffFormatScript = { action ->
def javaHome = project.findProperty('org.gradle.java.home')
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
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 formatDiffScript = "${scriptDir}/google-java-format-git-diff.sh"
def pythonExe = getPythonExecutable()
return ext.execInBash(
"JAVA=${javaBin} PYTHON=${pythonExe} ${formatDiffScript} ${action}", "${workingDir}")
"PYTHON=${pythonExe} ${formatDiffScript} ${action}", "${workingDir}")
}
}
@@ -521,9 +534,7 @@ 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.
// 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('Xdoclint:all,-missing', true)
options.addBooleanOption("-allow-script-in-comments",true)
options.tags = ["type:a:Generic Type",
"error:a:Expected Error",
@@ -537,35 +548,25 @@ tasks.build.dependsOn(tasks.javadoc)
// core Nomulus codebase, and runs all presubmits.
task coreDev {
dependsOn 'javaIncrementalFormatApply'
dependsOn 'console-webapp:applyFormatting'
dependsOn 'javadoc'
dependsOn 'checkDependenciesDotGradle'
dependsOn 'checkLicense'
dependsOn ':console-webapp:runConsoleWebappUnitTests'
dependsOn ':core:check'
dependsOn 'assemble'
}
javadocDependentTasks.each { tasks.javadoc.dependsOn(it) }
// Runs the script, which deploys cloud scheduler and tasks based on the config
task deployCloudSchedulerAndQueue {
// Runs the script, which deploys cloud scheduler tasks based on the config
task cloudSchedulerDeployer {
doLast {
def env = environment
if (!prodOrSandboxEnv) {
exec {
workingDir "${rootDir}/release/builder/"
commandLine 'go', 'run',
"./deployCloudSchedulerAndQueue.go",
"${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml",
"${rootDir}/core/src/main/java/google/registry/config/files/tasks/cloud-scheduler-tasks-${env}.xml",
"domain-registry-${env}"
}
exec {
workingDir "${rootDir}/release/builder/"
commandLine 'go', 'run',
"./deployCloudSchedulerAndQueue.go",
"${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml",
"${rootDir}/core/src/main/java/google/registry/config/files/cloud-tasks-queue.xml",
"${rootDir}/release/builder/cloudSchedulerDeployer.go",
"${rootDir}/core/src/main/java/google/registry/env/${env}/default/WEB-INF/cloud-scheduler-tasks.xml",
"domain-registry-${env}"
}
}

128
buildSrc/build.gradle.kts Normal file
View File

@@ -0,0 +1,128 @@
// 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
// id("nebula.lint") version "16.0.2" // unsupported for kotlin
id("net.ltgt.errorprone") version "2.0.2"
checkstyle
id("com.diffplug.gradle.spotless") version "3.25.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")
project.the<SourceSetContainer>()["main"].java {
srcDir("${project.buildDir}/generated/source/apt/main")
}
// checkstyle {
// configDir file("../config/checkstyle")
// }
dependencies {
val deps = project.ext["dependencyMap"] as Map<String, String>
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") {
val outputFileProperty = "dependencyExportFile"
val output = if (project.hasProperty(outputFileProperty)) {
PrintStream(file(project.ext.properties[outputFileProperty]))
} else {
System.out
}
doLast {
project.configurations.forEach {
println("dependency is $it")
// it.dependencies.findAll {
// it.group != null
// }.each {
// output.println("${it.group}:${it.name}")
// }
}
}
}

View File

@@ -0,0 +1,24 @@
# 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.gradle.spotless:com.diffplug.gradle.spotless.gradle.plugin:3.25.0=classpath
com.diffplug.spotless:spotless-lib-extra:1.25.0=classpath
com.diffplug.spotless:spotless-lib:1.25.0=classpath
com.diffplug.spotless:spotless-plugin-gradle:3.25.0=classpath
com.googlecode.concurrent-trees:concurrent-trees:2.6.1=classpath
com.googlecode.javaewah:JavaEWAH:1.1.6=classpath
com.jcraft:jsch:0.1.55=classpath
com.jcraft:jzlib:1.1.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.bouncycastle:bcpg-jdk15on:1.61=classpath
org.bouncycastle:bcpkix-jdk15on:1.61=classpath
org.bouncycastle:bcprov-jdk15on:1.61=classpath
org.codehaus.groovy:groovy-xml:2.4.7=classpath
org.codehaus.groovy:groovy:2.4.7=classpath
org.eclipse.jgit:org.eclipse.jgit:5.5.0.201909110433-r=classpath
org.slf4j:slf4j-api:1.7.2=classpath
empty=

142
buildSrc/gradle.lockfile Normal file
View File

@@ -0,0 +1,142 @@
# 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.
antlr:antlr:2.7.7=checkstyle
aopalliance:aopalliance:1.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
args4j:args4j:2.0.23=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.14.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.14.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.github.ben-manes.caffeine:caffeine:2.7.0=annotationProcessor,testAnnotationProcessor
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,testAnnotationProcessor
com.google.android:annotations:4.1.1.4=testRuntimeClasspath
com.google.api-client:google-api-client:2.1.2=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:gapic-google-cloud-storage-v2:2.17.2-alpha=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:grpc-google-cloud-storage-v2:2.17.2-alpha=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-storage-v2:2.17.2-alpha=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-common-protos:2.13.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-iam-v1:1.8.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:api-common:2.5.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax-grpc:2.22.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax-httpjson:0.107.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax:2.22.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-storage:v1-rev20220705-2.0.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-credentials:1.14.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-oauth2-http:1.14.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto.value:auto-value-annotations:1.10.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto.value:auto-value:1.10.1=annotationProcessor,compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto:auto-common:0.10=annotationProcessor,testAnnotationProcessor
com.google.cloud:google-cloud-core-grpc:2.9.4=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-core-http:2.9.4=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-core:2.9.4=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-storage:2.17.2=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.code.findbugs:jFormatString:3.0.0=annotationProcessor,testAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,checkstyle,compileClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.code.gson:gson:2.10.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.common.html.types:types:1.0.6=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.errorprone:error_prone_annotation:2.3.4=annotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.3.4=annotationProcessor,checkstyle,testAnnotationProcessor
com.google.errorprone:error_prone_check_api:2.3.4=annotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_core:2.3.4=annotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_type_annotations:2.3.4=annotationProcessor,testAnnotationProcessor
com.google.escapevelocity:escapevelocity:0.9.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.guava:failureaccess:1.0.1=annotationProcessor,checkstyle,compileClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.guava:guava:27.0.1-jre=annotationProcessor,testAnnotationProcessor
com.google.guava:guava:29.0-jre=checkstyle
com.google.guava:guava:31.1-jre=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,compileClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-apache-v2:1.42.3=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-appengine:1.42.3=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-gson:1.42.3=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-jackson2:1.42.3=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client:1.42.3=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.inject.extensions:guice-multibindings:4.1.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.inject:guice:4.1.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.j2objc:j2objc-annotations:1.1=annotationProcessor,testAnnotationProcessor
com.google.j2objc:j2objc-annotations:1.3=checkstyle,compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.jsinterop:jsinterop-annotations:1.0.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.oauth-client:google-oauth-client:1.34.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.protobuf:protobuf-java-util:3.21.12=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.protobuf:protobuf-java:3.21.12=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.protobuf:protobuf-java:3.4.0=annotationProcessor,testAnnotationProcessor
com.google.re2j:re2j:1.6=testRuntimeClasspath
com.google.template:soy:2021-02-01=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.google.truth.extensions:truth-java8-extension:1.1.3=testCompileClasspath,testRuntimeClasspath
com.google.truth:truth:1.1.3=testCompileClasspath,testRuntimeClasspath
com.googlecode.java-diff-utils:diffutils:1.3.0=annotationProcessor,testAnnotationProcessor
com.ibm.icu:icu4j:57.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
com.puppycrawl.tools:checkstyle:8.37=checkstyle
commons-beanutils:commons-beanutils:1.9.4=checkstyle
commons-codec:commons-codec:1.15=compileClasspath,testCompileClasspath,testRuntimeClasspath
commons-collections:commons-collections:3.2.2=checkstyle
commons-logging:commons-logging:1.2=compileClasspath,testCompileClasspath,testRuntimeClasspath
info.picocli:picocli:4.5.2=checkstyle
io.grpc:grpc-alts:1.52.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-api:1.52.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-auth:1.52.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-context:1.52.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-core:1.52.1=testRuntimeClasspath
io.grpc:grpc-googleapis:1.52.1=testRuntimeClasspath
io.grpc:grpc-grpclb:1.52.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-netty-shaded:1.52.1=testRuntimeClasspath
io.grpc:grpc-protobuf-lite:1.52.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-protobuf:1.52.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-services:1.52.1=testRuntimeClasspath
io.grpc:grpc-stub:1.52.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-xds:1.52.1=testRuntimeClasspath
io.opencensus:opencensus-api:0.31.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.opencensus:opencensus-contrib-http-util:0.31.1=compileClasspath,testCompileClasspath,testRuntimeClasspath
io.opencensus:opencensus-proto:0.2.0=testRuntimeClasspath
io.perfmark:perfmark-api:0.26.0=testRuntimeClasspath
javax.annotation:javax.annotation-api:1.3.2=compileClasspath,testCompileClasspath,testRuntimeClasspath
javax.annotation:jsr250-api:1.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
javax.inject:javax.inject:1=compileClasspath,testCompileClasspath,testRuntimeClasspath
junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.12.22=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.12.22=testCompileClasspath,testRuntimeClasspath
net.sf.saxon:Saxon-HE:10.3=checkstyle
org.antlr:antlr4-runtime:4.8-1=checkstyle
org.apache.commons:commons-lang3:3.12.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-text:1.10.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.httpcomponents:httpclient:4.5.13=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.httpcomponents:httpcore:4.4.15=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
org.checkerframework:checker-qual:2.11.1=checkstyle
org.checkerframework:checker-qual:3.0.0=annotationProcessor,testAnnotationProcessor
org.checkerframework:checker-qual:3.29.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.checkerframework:dataflow:3.0.0=annotationProcessor,testAnnotationProcessor
org.checkerframework:javacutil:3.0.0=annotationProcessor,testAnnotationProcessor
org.codehaus.mojo:animal-sniffer-annotations:1.17=annotationProcessor,testAnnotationProcessor
org.codehaus.mojo:animal-sniffer-annotations:1.22=testRuntimeClasspath
org.conscrypt:conscrypt-openjdk-uber:2.5.2=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath
org.jacoco:org.jacoco.agent:0.8.7=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.7=jacocoAnt
org.jacoco:org.jacoco.core:0.8.7=jacocoAnt
org.jacoco:org.jacoco.report:0.8.7=jacocoAnt
org.javassist:javassist:3.26.0-GA=checkstyle
org.json:json:20160212=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.9.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.9.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.9.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.9.2=testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:5.9.2=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.0.0=testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=testRuntimeClasspath
org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:7.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.1=jacocoAnt
org.ow2.asm:asm-commons:7.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.1=jacocoAnt
org.ow2.asm:asm-tree:7.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-tree:9.1=jacocoAnt
org.ow2.asm:asm-util:7.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm:7.0=compileClasspath
org.ow2.asm:asm:9.1=jacocoAnt,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:2.1.2=annotationProcessor,testAnnotationProcessor
org.plumelib:plume-util:1.0.6=annotationProcessor,testAnnotationProcessor
org.plumelib:reflection-util:0.0.2=annotationProcessor,testAnnotationProcessor
org.plumelib:require-javadoc:0.1.0=annotationProcessor,testAnnotationProcessor
org.reflections:reflections:0.9.12=checkstyle
org.threeten:threetenbp:1.6.5=compileClasspath,testCompileClasspath,testRuntimeClasspath
empty=

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// 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.
@@ -12,24 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
.console-app {
&__home-widgets {
display: flex;
gap: 1rem;
h3 {
padding: 0;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
mat-card {
flex: 1;
}
}
&__home_tablet {
.console-app__home-widgets {
flex-direction: column;
// Alias this since it collides with the closure variable name
def allowInsecure = allowInsecureProtocol
if (!pluginsUrl.isEmpty()) {
pluginManagement {
repositories {
maven {
url pluginsUrl
allowInsecureProtocol = allowInsecure == "true"
}
}
}
} else {
println "Plugins: Using default repo..."
}

View File

@@ -0,0 +1,211 @@
// 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

@@ -0,0 +1,59 @@
// 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

@@ -0,0 +1,221 @@
// 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

@@ -0,0 +1,137 @@
// 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

@@ -0,0 +1,311 @@
// 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.getDestination();
// 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

@@ -0,0 +1,47 @@
// 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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,107 @@
// 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

@@ -0,0 +1,279 @@
// 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

@@ -0,0 +1,298 @@
// 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

@@ -0,0 +1 @@
mock-maker-inline

View File

@@ -4,15 +4,14 @@
com.diffplug.durian:durian-collect:1.2.0=classpath
com.diffplug.durian:durian-core:1.2.0=classpath
com.diffplug.durian:durian-io:1.2.0=classpath
com.diffplug.durian:durian-swt.os:4.2.0=classpath
com.diffplug.spotless:com.diffplug.spotless.gradle.plugin:6.20.0=classpath
com.diffplug.spotless:spotless-lib-extra:2.40.0=classpath
com.diffplug.spotless:spotless-lib:2.40.0=classpath
com.diffplug.spotless:spotless-plugin-gradle:6.20.0=classpath
com.diffplug.gradle.spotless:com.diffplug.gradle.spotless.gradle.plugin:3.25.0=classpath
com.diffplug.spotless:spotless-lib-extra:1.25.0=classpath
com.diffplug.spotless:spotless-lib:1.25.0=classpath
com.diffplug.spotless:spotless-plugin-gradle:3.25.0=classpath
com.dorongold.plugins:task-tree:2.1.0=classpath
com.dorongold.task-tree:com.dorongold.task-tree.gradle.plugin:2.1.0=classpath
com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:8.1.1=classpath
com.github.johnrengelman:shadow:8.1.1=classpath
com.github.jengelman.gradle.plugins:shadow:5.1.0=classpath
com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:5.1.0=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
@@ -25,38 +24,56 @@ com.google.guava:guava:28.2-jre=classpath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath
com.google.j2objc:j2objc-annotations:1.3=classpath
com.googlecode.concurrent-trees:concurrent-trees:2.6.1=classpath
com.googlecode.javaewah:JavaEWAH:1.2.3=classpath
com.squareup.okhttp3:okhttp:4.10.0=classpath
com.squareup.okio:okio-jvm:3.0.0=classpath
com.squareup.okio:okio:3.0.0=classpath
commons-io:commons-io:2.11.0=classpath
dev.equo.ide:solstice:1.3.1=classpath
net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:3.1.0=classpath
net.ltgt.gradle:gradle-errorprone-plugin:3.1.0=classpath
org.apache.ant:ant-launcher:1.10.13=classpath
org.apache.ant:ant:1.10.13=classpath
com.googlecode.javaewah:JavaEWAH:1.1.6=classpath
com.jcraft:jsch:0.1.55=classpath
com.jcraft:jzlib:1.1.1=classpath
com.netflix.nebula:gradle-lint-plugin:16.0.2=classpath
com.netflix.nebula:nebula-gradle-interop:1.0.11=classpath
commons-io:commons-io:2.6=classpath
commons-lang:commons-lang:2.6=classpath
javax.inject:javax.inject:1=classpath
junit:junit:4.12=classpath
nebula.lint:nebula.lint.gradle.plugin:16.0.2=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
org.apache.commons:commons-compress:1.20=classpath
org.apache.commons:commons-lang3:3.5=classpath
org.apache.commons:commons-lang3:3.8.1=classpath
org.apache.maven:maven-artifact:3.6.2=classpath
org.apache.maven:maven-builder-support:3.6.2=classpath
org.apache.maven:maven-model-builder:3.6.2=classpath
org.apache.maven:maven-model:3.6.2=classpath
org.bouncycastle:bcpg-jdk15on:1.61=classpath
org.bouncycastle:bcpkix-jdk15on:1.61=classpath
org.bouncycastle:bcprov-jdk15on:1.61=classpath
org.checkerframework:checker-qual:2.10.0=classpath
org.codehaus.plexus:plexus-utils:3.5.1=classpath
org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r=classpath
org.eclipse.platform:org.eclipse.osgi:3.18.300=classpath
org.codehaus.gpars:gpars:1.2.1=classpath
org.codehaus.groovy:groovy-xml:2.4.7=classpath
org.codehaus.groovy:groovy:2.4.7=classpath
org.codehaus.jsr166-mirror:jsr166y:1.7.0=classpath
org.codehaus.plexus:plexus-interpolation:1.25=classpath
org.codehaus.plexus:plexus-utils:3.2.1=classpath
org.eclipse.jgit:org.eclipse.jgit:5.5.0.201909110433-r=classpath
org.eclipse.sisu:org.eclipse.sisu.inject:0.3.3=classpath
org.glassfish:javax.json:1.0.4=classpath
org.jdom:jdom2:2.0.6.1=classpath
org.jetbrains.kotlin:kotlin-stdlib-common:1.6.20=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=classpath
org.jetbrains.kotlin:kotlin-stdlib:1.6.20=classpath
org.hamcrest:hamcrest-core:1.3=classpath
org.jdom:jdom2:2.0.6=classpath
org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.50=classpath
org.jetbrains.kotlin:kotlin-stdlib:1.3.50=classpath
org.jetbrains:annotations:13.0=classpath
org.ow2.asm:asm-commons:9.4=classpath
org.ow2.asm:asm-tree:9.4=classpath
org.ow2.asm:asm:9.4=classpath
org.slf4j:slf4j-api:1.7.36=classpath
org.multiverse:multiverse-core:0.7.0=classpath
org.ow2.asm:asm-analysis:7.1=classpath
org.ow2.asm:asm-commons:7.1=classpath
org.ow2.asm:asm-tree:7.1=classpath
org.ow2.asm:asm:7.1=classpath
org.slf4j:slf4j-api:1.7.2=classpath
org.sonatype.aether:aether-api:1.13.1=classpath
org.sonatype.aether:aether-impl:1.13.1=classpath
org.sonatype.aether:aether-spi:1.13.1=classpath
org.sonatype.aether:aether-util:1.13.1=classpath
org.tukaani:xz:1.9=classpath
org.vafer:jdependency:2.8.0=classpath
org.vafer:jdependency:2.1.1=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['jakarta.inject:jakarta.inject-api']
implementation deps['javax.inject:javax.inject']
implementation deps['joda-time:joda-time']
implementation deps['com.google.flogger:flogger']
implementation deps['io.github.java-diff-utils:java-diff-utils']
@@ -65,5 +65,4 @@ dependencies {
testImplementation deps['org.junit.jupiter:junit-jupiter-api']
testImplementation deps['org.junit.jupiter:junit-jupiter-engine']
testImplementation deps['org.junit.platform:junit-platform-launcher']
}

View File

@@ -1,71 +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.
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.2.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
antlr:antlr:2.7.7=checkstyle
com.github.ben-manes.caffeine:caffeine:2.7.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.github.ben-manes.caffeine:caffeine:2.9.3=compileClasspath,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.11.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.auto:auto-common:1.2.1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,checkstyle,compileClasspath,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
com.google.errorprone:error_prone_annotation:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.36.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.errorprone:error_prone_annotations:2.7.1=checkstyle
com.google.errorprone:error_prone_check_api:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_core:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_type_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:javac:9+181-r4173-1=errorproneJavac
com.google.flogger:flogger:0.8=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:failureaccess:1.0.1=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:failureaccess:1.0.2=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:guava-parent:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:guava:31.0.1-jre=checkstyle
com.google.guava:guava:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:guava:33.2.1-android=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=checkstyle,compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.inject:guice:5.1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.j2objc:j2objc-annotations:1.3=checkstyle
com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath,testCompileClasspath,testingCompileClasspath
com.google.protobuf:protobuf-java:3.19.6=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.truth:truth:1.4.4=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.puppycrawl.tools:checkstyle:9.3=checkstyle
com.google.auto.value:auto-value-annotations:1.8.1=compileClasspath,default,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.code.findbugs:jsr305:3.0.2=annotationProcessor,checkstyle,compileClasspath,default,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.11.0=compileClasspath,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.errorprone:error_prone_annotations:2.3.4=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor,testingAnnotationProcessor
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,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:failureaccess:1.0.1=annotationProcessor,checkstyle,compileClasspath,default,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
com.google.guava:guava:27.0.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:guava:29.0-jre=checkstyle
com.google.guava:guava:31.1-jre=compileClasspath,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,compileClasspath,default,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
com.google.j2objc:j2objc-annotations:1.1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.j2objc:j2objc-annotations:1.3=checkstyle,compileClasspath,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.protobuf:protobuf-java:3.4.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.truth:truth:1.1.3=compileClasspath,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.googlecode.java-diff-utils:diffutils:1.3.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.puppycrawl.tools:checkstyle:8.37=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,testingAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.15=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
joda-time:joda-time:2.13.1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
net.sf.saxon:Saxon-HE:10.6=checkstyle
org.antlr:antlr4-runtime:4.9.3=checkstyle
info.picocli:picocli:4.5.2=checkstyle
io.github.java-diff-utils:java-diff-utils:4.12=compileClasspath,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
javax.inject:javax.inject:1=compileClasspath,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
joda-time:joda-time:2.12.2=compileClasspath,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
junit:junit:4.13.2=default,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
net.sf.saxon:Saxon-HE:10.3=checkstyle
org.antlr:antlr4-runtime:4.8-1=checkstyle
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
org.checkerframework:checker-compat-qual:2.5.3=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.checkerframework:checker-qual:3.12.0=checkstyle
org.checkerframework:checker-qual:3.33.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
org.checkerframework:checker-qual:3.42.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.jacoco:org.jacoco.agent:0.8.12=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.12=jacocoAnt
org.jacoco:org.jacoco.core:0.8.12=jacocoAnt
org.jacoco:org.jacoco.report:0.8.12=jacocoAnt
org.javassist:javassist:3.28.0-GA=checkstyle
org.jspecify:jspecify:1.0.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.junit.jupiter:junit-jupiter-api:5.12.1=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.12.1=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.12.1=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.12.1=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-launcher:1.12.1=testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:5.12.1=testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.7=jacocoAnt
org.ow2.asm:asm-tree:9.7=jacocoAnt
org.ow2.asm:asm:9.7=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=testingCompile,testingRuntime,testingRuntimeClasspath
org.checkerframework:checker-compat-qual:2.5.3=compileClasspath,default,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.checkerframework:checker-qual:2.11.1=checkstyle
org.checkerframework:checker-qual:3.0.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
org.checkerframework:checker-qual:3.19.0=compileClasspath,default,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.hamcrest:hamcrest-core:1.3=default,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.jacoco:org.jacoco.agent:0.8.7=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.7=jacocoAnt
org.jacoco:org.jacoco.core:0.8.7=jacocoAnt
org.jacoco:org.jacoco.report:0.8.7=jacocoAnt
org.javassist:javassist:3.26.0-GA=checkstyle
org.junit.jupiter:junit-jupiter-api:5.9.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.9.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.9.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.9.2=testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:5.9.2=testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.1=jacocoAnt
org.ow2.asm:asm-commons:9.1=jacocoAnt
org.ow2.asm:asm-tree:9.1=jacocoAnt
org.ow2.asm:asm:9.1=compileClasspath,default,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.reflections:reflections:0.9.12=checkstyle
empty=archives,errorproneJavac,testingCompile,testingRuntime,testingRuntimeClasspath

View File

@@ -1,43 +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.
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

@@ -16,8 +16,8 @@ package google.registry.util;
import static org.joda.time.DateTimeZone.UTC;
import jakarta.inject.Inject;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import org.joda.time.DateTime;
/** Clock implementation that proxies to the real system clock. */

View File

@@ -17,9 +17,9 @@ package google.registry.util;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.util.concurrent.Uninterruptibles;
import jakarta.inject.Inject;
import java.io.Serializable;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import org.joda.time.ReadableDuration;
/** Implementation of {@link Sleeper} for production use. */

View File

@@ -1,65 +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.
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,6 +14,7 @@
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;
@@ -30,26 +31,31 @@ public final class SystemInfo {
private static final LoadingCache<String, Boolean> hasCommandCache =
Caffeine.newBuilder()
.build(
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;
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;
}
return true;
});
/**
* Returns {@code true} if system command can be run from the path.
* Returns {@code true} if system command can be run from 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 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.
* <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
*/
public static boolean hasCommand(String cmd) throws ExecutionException {
return hasCommandCache.get(cmd);

View File

@@ -63,7 +63,6 @@ public class TextDiffSubject extends Subject {
private final ImmutableList<String> actual;
private DiffFormat diffFormat = DiffFormat.SIDE_BY_SIDE_MARKDOWN;
private ImmutableList<String> comments = ImmutableList.of();
protected TextDiffSubject(FailureMetadata metadata, List<String> actual) {
super(metadata, actual);
@@ -84,22 +83,10 @@ public class TextDiffSubject extends Subject {
return this;
}
/** If set, ignore lines that start with the given string. */
public TextDiffSubject ignoringLinesStartingWith(String... comments) {
this.comments = ImmutableList.copyOf(comments);
return this;
}
private ImmutableList<String> filterComments(List<String> lines) {
return lines.stream()
.filter(line -> comments.stream().noneMatch(line::startsWith))
.collect(ImmutableList.toImmutableList());
}
public void hasSameContentAs(List<String> expectedContent) {
checkNotNull(expectedContent, "expectedContent");
ImmutableList<String> expected = filterComments(expectedContent);
if (filterComments(expected).equals(filterComments(actual))) {
ImmutableList<String> expected = ImmutableList.copyOf(expectedContent);
if (expected.equals(actual)) {
return;
}
String diffString = diffFormat.generateDiff(expected, actual);
@@ -188,7 +175,14 @@ public class TextDiffSubject extends Subject {
.orElse(0);
}
private record SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
private static class SideBySideRowFormatter {
private final int maxExpectedLineLength;
private final int maxActualLineLength;
private SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
this.maxExpectedLineLength = maxExpectedLineLength;
this.maxActualLineLength = maxActualLineLength;
}
public String formatRow(String expected, String actual, char padChar) {
return String.format(

View File

@@ -27,9 +27,6 @@
{
"moduleLicense": "Apache License V2.0"
},
{
"moduleLicense": "Apache License Version 2.0"
},
{
"moduleLicense": "Apache License, Version 2.0"
},
@@ -105,12 +102,6 @@
{
"moduleLicense": "BSD-2-Clause"
},
{
"moduleLicense": "BSD 3-Clause \"New\" or \"Revised\" License (BSD-3-Clause)"
},
{
"moduleLicense": "BSD licence"
},
{
"moduleLicense": "New BSD License"
},
@@ -150,9 +141,6 @@
{
"moduleLicense": "\\n Dual license consisting of the CDDL v1.1 and GPL v2\\n "
},
{
"moduleLicense": "EPL-2.0"
},
{
"moduleLicense": "Eclipse Distribution License (New BSD License)"
},
@@ -204,9 +192,6 @@
{
"moduleLicense": "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception"
},
{
"moduleLicense": "(GPL-2.0-only WITH Classpath-exception-2.0)"
},
{
"moduleLicense": "GNU Library General Public License v2.1 or later"
},
@@ -277,12 +262,6 @@
{
"moduleLicense": "Unicode/ICU License"
},
{
"moduleLicense": "Unicode-3.0"
},
{
"moduleLicense": "The W3C Software License"
},
{
"moduleLicense": "Public Domain",
"moduleName": "aopalliance:aopalliance"
@@ -295,24 +274,6 @@
"moduleLicense": "Public Domain",
"moduleName": "org.json:json"
},
{
// "Apache License, Version 2.0". The plugin is able to parse up to
// 2.11.3 correctly but then something changed with 2.12.* and it no
// longer parses correctly, even though it's still Apache 2.0.
"moduleLicense": null,
"moduleName": "com.fasterxml.jackson:jackson-bom"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleName": "com.google.cloud:libraries-bom"
},
{
// Part of Guava with "Apache License, Version 2.0". The plugin is unable
// to parse its license for unknown reason.
"moduleLicense": null,
"moduleName": "com.google.guava:guava-parent"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
@@ -320,44 +281,11 @@
"moduleName": "com.google.gwt:gwt-user"
},
{
// "Apache License, Version 2.0".
// "Apache License, Version 2.0". The plugin is able to parse up to
// 2.11.3 correctly but then something changed with 2.12.* and it no
// longer parses correctly, even though it's still Apache 2.0.
"moduleLicense": null,
"moduleName": "com.squareup.okhttp3:okhttp"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleVersion": "1.15.1",
"moduleName": "com.squareup:kotlinpoet"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleName": "com.squareup.okio:okio"
},
{
// "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"
"moduleName": "com.fasterxml.jackson:jackson-bom"
},
{
// "Apache License, Version 2.0". The plugin is able to parse up to
@@ -366,51 +294,13 @@
"moduleVersion": "2.0.46.Final",
"moduleName": "io.netty:netty-tcnative-classes"
},
// "Apache License, Version 2.0".
{
"moduleLicense": null,
"moduleName": "io.opentelemetry:opentelemetry-bom"
},
{
// "Apache License, Version 2.0".
"moduleLicense": null,
"moduleVersion": "1.4",
"moduleName": "jakarta-regexp:jakarta-regexp"
},
{
// Actually Eclipse Public License v2.0
"moduleLicense": null,
"moduleName": "org.junit:junit-bom"
},
{
// "Apache License, Version 2.0".
"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"
"moduleLicense": "The W3C Software License"
}
]
}

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,7 +58,6 @@ 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
@@ -89,10 +88,19 @@ 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',
@@ -270,7 +278,8 @@ 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, 'core')) or \
if not os.path.exists(os.path.join(cur_dir, 'buildSrc')) or \
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

@@ -25,11 +25,7 @@ import textwrap
import re
# We should never analyze any generated files
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/",
".gradle/", "/dist/", "/console-alpha/", "/console-crash/", "/console-qa",
"/console-sandbox", "/console-production", "karma.conf.js", "polyfills.ts",
"test.ts", "/docs/console-endpoints/", "/bin/generated-sources/",
"/bin/generated-test-sources/", "src/main/generated", "src/test/generated"}
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/", ".gradle/", "/dist/", "karma.conf.js", "polyfills.ts", "test.ts"}
# We can't rely on CI to have the Enum package installed so we do this instead.
FORBIDDEN = 1
REQUIRED = 2
@@ -91,7 +87,8 @@ PRESUBMITS = {
PresubmitCheck(
r".*Copyright 20\d{2} The Nomulus Authors\. All Rights Reserved\.",
("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"), {
".git", "/build/", "node_modules/", "LoggerConfig.java", "registrar_bin.",
".git", "/build/", "/generated/", "/generated_tests/",
"node_modules/", "LoggerConfig.java", "registrar_bin.",
"registrar_dbg.", "google-java-format-diff.py",
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js"
}, REQUIRED):
@@ -102,16 +99,23 @@ PRESUBMITS = {
{"node_modules/"}, REQUIRED):
"Source files must end in a newline.",
# System.(out|err).println should only appear in tools/ or load-testing/
# System.(out|err).println should only appear in tools/
PresubmitCheck(
r".*\bSystem\.(out|err)\.print", "java", {
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
"/load-testing/", "RegistryTestServerMain.java",
"TestServerExtension.java", "FlowDocumentationTool.java"
"RegistryTestServerMain.java", "TestServerExtension.java",
"FlowDocumentationTool.java"
}):
"System.(out|err).println is only allowed in tools/ packages. Please "
"use a logger instead.",
# PostgreSQLContainer instantiation must specify docker tag
# TODO(b/204572437): Fix the pattern to pass DatabaseSnapshotTest.java
PresubmitCheck(
r"[\s\S]*new\s+PostgreSQLContainer(<[\s\S]*>)?\(\s*\)[\s\S]*",
"java", {"DatabaseSnapshotTest.java"}):
"PostgreSQLContainer instantiation must specify docker tag.",
# Various Soy linting checks
PresubmitCheck(
r".* (/\*)?\* {?@param ",
@@ -174,48 +178,6 @@ 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.",
PresubmitCheck(
r".*javax\.inject\..*",
"java",
{"/node_modules/"},
):
"Do not use javax.inject.* Use jakarta.inject.* instead.",
}
# Note that this regex only works for one kind of Flyway file. If we want to

View File

@@ -1,50 +0,0 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"eol-last": [
"error",
"always"
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

View File

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

View File

@@ -1 +0,0 @@
{}

View File

@@ -13,7 +13,7 @@ Webapp is deployed with the nomulus default service war to Google App Engine.
During nomulus default service war build task, gradle script triggers the
following:
1) Console webapp build script `buildConsoleWebapp`, which installs
1) Console webapp build script `buildConsoleWebappProd`, which installs
dependencies, assembles a compiled ts -> js, minified, optimized static
artifact (html, css, js)
2) Artifact assembled in step 1 then gets copied to core project web artifact

View File

@@ -7,7 +7,7 @@
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
"style": "less"
}
},
"root": "",
@@ -15,39 +15,31 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": {
"base": "staged/dist/",
"browser": ""
},
"outputPath": "dist/console-webapp",
"index": "src/index.html",
"polyfills": [
"src/polyfills.ts"
],
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"inlineStyleLanguage": "less",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/theme.scss",
"src/styles.scss"
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.less"
],
"stylePreprocessorOptions": {
"includePaths": ["node_modules/"]
},
"scripts": [],
"browser": "src/main.ts"
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
@@ -63,50 +55,13 @@
],
"outputHashing": "all"
},
"sandbox": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.sandbox.ts"
}
],
"outputHashing": "all"
},
"crash": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"alpha": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"qa": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
@@ -115,22 +70,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "console-webapp:build:production"
},
"alpha": {
"buildTarget": "console-webapp:build:alpha"
},
"crash": {
"buildTarget": "console-webapp:build:crash"
},
"sandbox": {
"buildTarget": "console-webapp:build:sandbox"
},
"qa": {
"buildTarget": "console-webapp:build:qa"
"browserTarget": "console-webapp:build:production"
},
"development": {
"buildTarget": "console-webapp:build:development"
"browserTarget": "console-webapp:build:development"
}
},
"defaultConfiguration": "development"
@@ -138,7 +81,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "console-webapp:build"
"browserTarget": "console-webapp:build"
}
},
"test": {
@@ -148,40 +91,19 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"inlineStyleLanguage": "less",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/theme.scss",
"src/styles.scss"
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.less"
],
"stylePreprocessorOptions": {
"includePaths": ["node_modules/"]
},
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"cache": {
"enabled": false
},
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
]
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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}/staged/dist"
delete "${consoleDir}/dist"
}
task npmInstallDeps(type: Exec) {
@@ -37,73 +37,18 @@ task runConsoleWebappUnitTests(type: Exec) {
args 'run', 'test'
}
task buildConsoleWebapp(type: Exec) {
task buildConsoleWebappNonProd(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
def configuration = project.getProperty('configuration')
args 'run', "build", "--configuration=${configuration}"
doFirst {
println "Building console for environment: ${configuration}"
}
args 'run', 'build'
}
task buildConsoleForAll() {}
def createConsoleTask = { env ->
project.tasks.register("buildConsoleFor${env.capitalize()}", Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'build', "--configuration=${env}"
doFirst {
println "Building console for environment: ${env}"
}
doLast {
copy {
from "${consoleDir}/staged/dist/"
into "${consoleDir}/staged/console-${env}"
}
delete "${consoleDir}/staged/dist"
}
dependsOn(tasks.npmInstallDeps)
}
project.tasks.register("deleteConsoleFor${env.capitalize()}", Delete) {
delete "${consoleDir}/staged/console-${env}"
}
tasks.named('clean') {
dependsOn(tasks.named("deleteConsoleFor${env.capitalize()}"))
}
tasks.named('buildConsoleForAll') {
dependsOn(tasks.named("buildConsoleFor${env.capitalize()}"))
}
}
['alpha', 'crash', 'qa', 'sandbox', 'production'].forEach {env ->
createConsoleTask(env)
}
// Force an order so we don't run these tasks in parallel.
tasks.buildConsoleForCrash.mustRunAfter(tasks.buildConsoleForAlpha)
tasks.buildConsoleForQa.mustRunAfter(tasks.buildConsoleForCrash)
tasks.buildConsoleForSandbox.mustRunAfter(tasks.buildConsoleForQa)
tasks.buildConsoleForProduction.mustRunAfter(tasks.buildConsoleForSandbox)
// This task must run last, otherwise the previous tasks will have deleted the "dist" folder.
tasks.buildConsoleWebapp.mustRunAfter(tasks.buildConsoleForProduction)
task applyFormatting(type: Exec) {
// Keeping the same as non prod for now before we figure out optimization we want to include
task buildConsoleWebappProd(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'prettify'
args 'run', 'build'
}
task checkFormatting(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'prettify:check'
}
tasks.buildConsoleWebapp.dependsOn(tasks.npmInstallDeps)
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
tasks.build.dependsOn(tasks.checkFormatting)
tasks.build.dependsOn(tasks.runConsoleWebappUnitTests)
tasks.buildConsoleWebappProd.dependsOn(tasks.npmInstallDeps)

View File

@@ -1,9 +1,7 @@
{
"/console-api":
"/registrar":
{
"target": "http://localhost:8080",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
"target": "http://localhost:8080/registrar",
"secure": false
}
}

View File

@@ -1,47 +1,48 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
aopalliance:aopalliance:1.0=annotationProcessor,errorprone,testAnnotationProcessor
com.github.ben-manes.caffeine:caffeine:3.0.5=annotationProcessor,errorprone,testAnnotationProcessor
antlr:antlr:2.7.7=checkstyle
com.github.ben-manes.caffeine:caffeine:2.7.0=annotationProcessor,errorprone,testAnnotationProcessor
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,errorprone,testAnnotationProcessor
com.google.auto:auto-common:1.2.1=annotationProcessor,errorprone,testAnnotationProcessor
com.google.auto:auto-common:0.10=annotationProcessor,errorprone,testAnnotationProcessor
com.google.code.findbugs:jFormatString:3.0.0=annotationProcessor,errorprone,testAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor
com.google.errorprone:error_prone_annotation:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.7.1=checkstyle
com.google.errorprone:error_prone_check_api:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
com.google.errorprone:error_prone_core:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
com.google.errorprone:error_prone_type_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor
com.google.errorprone:javac:9+181-r4173-1=errorproneJavac
com.google.errorprone:error_prone_annotation:2.3.4=annotationProcessor,errorprone,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.3.4=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor
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.guava:failureaccess:1.0.1=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor
com.google.guava:guava-parent:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor
com.google.guava:guava:31.0.1-jre=checkstyle
com.google.guava:guava:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=checkstyle
com.google.inject:guice:5.1.0=annotationProcessor,errorprone,testAnnotationProcessor
com.google.guava:guava:27.0.1-jre=annotationProcessor,errorprone,testAnnotationProcessor
com.google.guava:guava:29.0-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.j2objc:j2objc-annotations:1.3=checkstyle
com.google.protobuf:protobuf-java:3.19.6=annotationProcessor,errorprone,testAnnotationProcessor
com.puppycrawl.tools:checkstyle:9.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.puppycrawl.tools:checkstyle:8.37=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.12.0=checkstyle
org.checkerframework:checker-qual:3.33.0=annotationProcessor,errorprone,testAnnotationProcessor
org.jacoco:org.jacoco.agent:0.8.12=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.12=jacocoAnt
org.jacoco:org.jacoco.core:0.8.12=jacocoAnt
org.jacoco:org.jacoco.report:0.8.12=jacocoAnt
org.javassist:javassist:3.28.0-GA=checkstyle
org.ow2.asm:asm-commons:9.7=jacocoAnt
org.ow2.asm:asm-tree:9.7=jacocoAnt
org.ow2.asm:asm:9.7=jacocoAnt
org.pcollections:pcollections:3.1.4=annotationProcessor,errorprone,testAnnotationProcessor
org.reflections:reflections:0.10.2=checkstyle
empty=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
info.picocli:picocli:4.5.2=checkstyle
net.sf.saxon:Saxon-HE:10.3=checkstyle
org.antlr:antlr4-runtime:4.8-1=checkstyle
org.checkerframework:checker-qual:2.11.1=checkstyle
org.checkerframework:checker-qual:3.0.0=annotationProcessor,errorprone,testAnnotationProcessor
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.7=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.7=jacocoAnt
org.jacoco:org.jacoco.core:0.8.7=jacocoAnt
org.jacoco:org.jacoco.report:0.8.7=jacocoAnt
org.javassist:javassist:3.26.0-GA=checkstyle
org.ow2.asm:asm-analysis:9.1=jacocoAnt
org.ow2.asm:asm-commons:9.1=jacocoAnt
org.ow2.asm:asm-tree:9.1=jacocoAnt
org.ow2.asm:asm:9.1=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.reflections:reflections:0.9.12=checkstyle
empty=archives,compileClasspath,default,deploy_jar,errorproneJavac,runtimeClasspath,testCompileClasspath,testRuntimeClasspath

View File

@@ -1 +0,0 @@
configuration=production

View File

@@ -3,17 +3,6 @@
module.exports = function (config) {
config.set({
customLaunchers: {
ChromeHeadless: {
base: 'Chrome',
flags: [
'--no-sandbox',
'--disable-gpu',
'--headless',
'--remote-debugging-port=9222'
]
}
},
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [

File diff suppressed because it is too large Load Diff

View File

@@ -3,55 +3,43 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config dev-proxy.config.json",
"build": "ng build --base-href=/console/ --configuration=$npm_config_configuration",
"start": "ng serve",
"build": "ng build --base-href=/console/",
"build:local": "ng build --base-href=/default/console/",
"watch": "ng build --watch --configuration=development",
"watch": "ng build --watch --configuration development",
"test": "ng test --browsers=ChromeHeadless --watch=false",
"run:dev": "",
"prettify": "npx prettier --write ./src/",
"prettify:check": "npx prettier --check ./src/",
"start:dev": "concurrently \"./../gradlew :core:runTestServer\" \"ng serve --proxy-config dev-proxy.config.json\"",
"lint": "ng lint"
"start:dev": "concurrently \"./../gradlew :core:runTestServer\" \"ng serve --proxy-config dev-proxy.config.json\""
},
"private": true,
"dependencies": {
"@angular/animations": "^19.1.4",
"@angular/cdk": "^19.1.2",
"@angular/common": "^19.1.4",
"@angular/compiler": "^19.1.4",
"@angular/core": "^19.1.4",
"@angular/forms": "^19.1.4",
"@angular/material": "^19.1.2",
"@angular/platform-browser": "^19.1.4",
"@angular/platform-browser-dynamic": "^19.1.4",
"@angular/router": "^19.1.4",
"@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",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.1.5",
"@angular-eslint/builder": "19.0.2",
"@angular-eslint/eslint-plugin": "19.0.2",
"@angular-eslint/eslint-plugin-template": "19.0.2",
"@angular-eslint/schematics": "19.0.2",
"@angular-eslint/template-parser": "19.0.2",
"@angular/cli": "~19.1.5",
"@angular/compiler-cli": "^19.1.4",
"@angular-devkit/build-angular": "^15.2.4",
"@angular/cli": "~15.2.4",
"@angular/compiler-cli": "^15.2.2",
"@types/jasmine": "~4.0.0",
"@types/node": "^18.19.74",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@types/node": "^18.11.18",
"concurrently": "^7.6.0",
"eslint": "^8.57.0",
"jasmine-core": "~4.3.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "2.8.7",
"typescript": "^5.7.3"
"typescript": "~4.9.4"
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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,132 +13,17 @@
// limitations under the License.
import { NgModule } from '@angular/core';
import { Route, RouterModule } from '@angular/router';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import { HomeComponent } from './home/home.component';
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { ResourcesComponent } from './resources/resources.component';
import ContactComponent from './settings/contact/contact.component';
import SecurityComponent from './settings/security/security.component';
import { SettingsComponent } from './settings/settings.component';
import { SupportComponent } from './support/support.component';
import RdapComponent from './settings/rdap/rdap.component';
import { RouterModule, Routes } from '@angular/router';
import {TldsComponent} from './tlds/tlds.component';
import {HomeComponent} from './home/home.component';
export interface RouteWithIcon extends Route {
iconName?: string;
}
export const PATHS = {
NewOteComponent: 'new-ote',
OteStatusComponent: 'ote-status/:registrarId',
UsersComponent: 'users',
};
export const routes: RouteWithIcon[] = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
path: RegistryLockVerifyComponent.PATH,
component: RegistryLockVerifyComponent,
},
{
path: PATHS.NewOteComponent,
loadComponent: () =>
import('./ote/newOte.component').then((mod) => mod.NewOteComponent),
},
{
path: PATHS.OteStatusComponent,
loadComponent: () =>
import('./ote/oteStatus.component').then((mod) => mod.OteStatusComponent),
},
{ path: 'registrars', component: RegistrarComponent },
{
path: 'home',
component: HomeComponent,
title: 'Dashboard',
iconName: 'view_comfy_alt',
},
// { path: 'tlds', component: TldsComponent, title: "TLDs", iconName: "event_list" },
{
path: DomainListComponent.PATH,
component: DomainListComponent,
title: 'Domains',
iconName: 'view_list',
},
{
path: SettingsComponent.PATH,
component: SettingsComponent,
title: 'Settings',
iconName: 'settings',
children: [
{
path: '',
redirectTo: ContactComponent.PATH,
pathMatch: 'full',
},
{
path: ContactComponent.PATH,
component: ContactComponent,
title: 'Contacts',
},
{
path: RdapComponent.PATH,
component: RdapComponent,
title: 'RDAP Info',
},
{
path: SecurityComponent.PATH,
component: SecurityComponent,
title: 'Security',
},
],
},
// {
// path: EppConsole.PATH,
// component: EppConsoleComponent,
// title: "EPP Console",
// iconName: "upgrade"
// },
{
path: RegistrarComponent.PATH,
component: RegistrarComponent,
title: 'Registrars',
iconName: 'account_circle',
},
{
path: RegistrarDetailsComponent.PATH,
component: RegistrarDetailsComponent,
},
{
path: BillingInfoComponent.PATH,
component: BillingInfoComponent,
title: 'Billing Info',
iconName: 'credit_card',
},
{
path: ResourcesComponent.PATH,
component: ResourcesComponent,
title: 'Resources',
iconName: 'description',
},
{
path: PATHS.UsersComponent,
title: 'Users',
iconName: 'manage_accounts',
loadComponent: () =>
import('./users/users.component').then((mod) => mod.UsersComponent),
},
{
path: SupportComponent.PATH,
component: SupportComponent,
title: 'Support',
iconName: 'help',
},
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'tlds', component: TldsComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule],
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
export class AppRoutingModule { }

View File

@@ -1,25 +1,14 @@
<div class="console-app mat-typography">
<app-header (toggleNavOpen)="toggleSidenav()"></app-header>
<div class="console-app__global-spinner">
<mat-progress-bar
mode="indeterminate"
*ngIf="globalLoader.isLoading"
></mat-progress-bar>
</div>
<mat-sidenav-container class="console-app__container">
<mat-sidenav-content class="console-app__content-wrapper">
<div class="console-app__content" role="main">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
<mat-sidenav
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
[opened]="!breakpointObserver.isMobileView()"
[disableClose]="!breakpointObserver.isMobileView()"
#sidenav
class="console-app__sidebar"
>
<app-navigation />
</mat-sidenav>
</mat-sidenav-container>
<div class="toolbar" role="banner">
Nomulus Console
</div>
<div class="content" role="main">
<nav>
<ul>
<li><a routerLink="/home" routerLinkActive="active" ariaCurrentWhenActive="page">Home page</a></li>
<li><a routerLink="/tlds" routerLinkActive="active" ariaCurrentWhenActive="page">TLDs</a></li>
</ul>
</nav>
</div>
<router-outlet></router-outlet>

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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,18 +12,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Location } from '@angular/common';
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[backButton]',
standalone: false,
})
export class LocationBackDirective {
constructor(private location: Location) {}
@HostListener('click')
onClick() {
this.location.back();
}
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px;
color: #333;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 8px 0;
}
p {
margin: 0;
}

View File

@@ -1,46 +0,0 @@
// 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.
:host {
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;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.console-app {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
&__container {
flex: 1;
padding-bottom: 36px;
background: transparent;
}
&__sidebar {
min-width: 240px;
border: 0;
}
&__content {
padding: 0 16px;
}
&__global-spinner {
position: absolute;
width: 100%;
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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,126 +12,26 @@
// 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 { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { routes } from './app-routing.module';
import { AppModule } from './app.module';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
import { RouterModule } from '@angular/router';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { UserData, UserDataService } from './shared/services/userData.service';
import { Registrar, RegistrarService } from './registrar/registrar.service';
import { MatSidenavModule } from '@angular/material/sidenav';
import { signal, WritableSignal } from '@angular/core';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let mockRegistrarService: {
registrar: WritableSignal<Partial<Registrar> | null | undefined>;
registrarId: WritableSignal<string>;
registrars: WritableSignal<Array<Partial<Registrar>>>;
};
let mockUserDataService: { userData: WritableSignal<Partial<UserData>> };
let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
const dummyPocReminderComponent = class {}; // Dummy class for type checking
beforeEach(async () => {
mockRegistrarService = {
registrar: signal<Registrar | null | undefined>(undefined),
registrarId: signal('123'),
registrars: signal([]),
};
mockUserDataService = {
userData: signal({
globalRole: 'NONE',
}),
};
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']);
await TestBed.configureTestingModule({
imports: [
MatSidenavModule,
NoopAnimationsModule,
MatSnackBarModule,
AppModule,
RouterModule.forRoot(routes),
RouterTestingModule
],
providers: [
{ provide: RegistrarService, useValue: mockRegistrarService },
{ provide: UserDataService, useValue: mockUserDataService },
{ provide: MatSnackBar, useValue: mockSnackBar },
{ provide: PocReminderComponent, useClass: dummyPocReminderComponent },
provideHttpClient(),
provideHttpClientTesting(),
declarations: [
AppComponent
],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
describe('PoC Verification Reminder', () => {
beforeEach(() => {
jasmine.clock().install();
});
it('should open snackbar if lastPocVerificationDate is older than one year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const twoYearsAgo = new Date(MOCK_TODAY);
twoYearsAgo.setFullYear(MOCK_TODAY.getFullYear() - 2);
mockRegistrarService.registrar.set({
lastPocVerificationDate: twoYearsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).toHaveBeenCalledWith(
PocReminderComponent,
{
horizontalPosition: 'center',
verticalPosition: 'top',
duration: 1000000000,
}
);
}));
it('should NOT open snackbar if lastPocVerificationDate is within last year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const sixMonthsAgo = new Date(MOCK_TODAY);
sixMonthsAgo.setMonth(MOCK_TODAY.getMonth() - 6);
mockRegistrarService.registrar.set({
lastPocVerificationDate: sixMonthsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).not.toHaveBeenCalled();
}));
});
});

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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,69 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { AfterViewInit, Component, effect, ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { NavigationEnd, Router } from '@angular/router';
import { RegistrarService } from './registrar/registrar.service';
import { BreakPointObserverService } from './shared/services/breakPoint.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { UserDataService } from './shared/services/userData.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: false,
styleUrls: ['./app.component.less']
})
export class AppComponent implements AfterViewInit {
@ViewChild(MatSidenav)
sidenav!: MatSidenav;
export class AppComponent {
constructor(
protected registrarService: RegistrarService,
protected userDataService: UserDataService,
protected globalLoader: GlobalLoaderService,
protected breakpointObserver: BreakPointObserverService,
private router: Router,
private _snackBar: MatSnackBar
) {
effect(() => {
const registrar = this.registrarService.registrar();
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
oneYearAgo.setHours(0, 0, 0, 0);
if (
registrar &&
registrar.lastPocVerificationDate &&
new Date(registrar.lastPocVerificationDate) < oneYearAgo &&
this.userDataService?.userData()?.globalRole === 'NONE'
) {
this._snackBar.openFromComponent(PocReminderComponent, {
horizontalPosition: 'center',
verticalPosition: 'top',
duration: 1000000000,
});
}
});
}
ngAfterViewInit() {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
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 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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,118 +13,29 @@
// limitations under the License.
import { NgModule } from '@angular/core';
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 { MaterialModule } from './material.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {MaterialModule} from './material.module';
import { BackendService } from './shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import {
DomainListComponent,
ReasonDialogComponent,
ResponseDialogComponent,
} from './domains/domainList.component';
import { RegistryLockComponent } from './domains/registryLock.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
import { NavigationComponent } from './navigation/navigation.component';
import NewRegistrarComponent from './registrar/newRegistrar.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { ResourcesComponent } from './resources/resources.component';
import SettingsContactComponent from './settings/contact/contact.component';
import { ContactDetailsComponent } from './settings/contact/contactDetails.component';
import EppPasswordEditComponent from './settings/security/eppPasswordEdit.component';
import SecurityComponent from './settings/security/security.component';
import SecurityEditComponent from './settings/security/securityEdit.component';
import { SettingsComponent } from './settings/settings.component';
import { NotificationsComponent } from './shared/components/notifications/notifications.component';
import { SelectedRegistrarWrapper } from './shared/components/selectedRegistrarWrapper/selectedRegistrarWrapper.component';
import { LocationBackDirective } from './shared/directives/locationBack.directive';
import { UserLevelVisibility } from './shared/directives/userLevelVisiblity.directive';
import { BreakPointObserverService } from './shared/services/breakPoint.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { UserDataService } from './shared/services/userData.service';
import { SnackBarModule } from './snackbar.module';
import { SupportComponent } from './support/support.component';
import { TldsComponent } from './tlds/tlds.component';
import { ForceFocusDirective } from './shared/directives/forceFocus.directive';
import RdapComponent from './settings/rdap/rdap.component';
import RdapEditComponent from './settings/rdap/rdapEdit.component';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
@NgModule({
declarations: [SelectedRegistrarWrapper],
imports: [MaterialModule],
exports: [SelectedRegistrarWrapper],
providers: [],
})
export class SelectedRegistrarModule {}
@NgModule({
declarations: [
AppComponent,
BillingInfoComponent,
ContactDetailsComponent,
DomainListComponent,
EppPasswordEditComponent,
ForceFocusDirective,
HeaderComponent,
HomeComponent,
LocationBackDirective,
NavigationComponent,
NewRegistrarComponent,
NotificationsComponent,
RdapComponent,
RdapEditComponent,
ReasonDialogComponent,
PocReminderComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistrarSelectorComponent,
RegistryLockComponent,
RegistryLockVerifyComponent,
ResourcesComponent,
ResponseDialogComponent,
SecurityComponent,
SecurityEditComponent,
SettingsComponent,
SettingsContactComponent,
SupportComponent,
TldsComponent,
UserLevelVisibility,
],
bootstrap: [AppComponent],
imports: [
AppRoutingModule,
BrowserAnimationsModule,
BrowserModule,
FormsModule,
MaterialModule,
SelectedRegistrarModule,
SnackBarModule,
],
providers: [
BackendService,
BreakPointObserverService,
GlobalLoaderService,
UserDataService,
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {
subscriptSizing: 'dynamic',
},
},
provideHttpClient(),
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
export class AppModule { }

View File

@@ -1,20 +0,0 @@
<app-selected-registrar-wrapper>
<h1 class="mat-headline-4" forceFocus>Billing Info</h1>
<div class="console-app__billing">
<div>
<div class="console-app__billing-subhead">
Billing records and information
</div>
<a
class="text-l"
href="{{ driveFolderUrl() }}"
target="_blank"
aria-label="View billing records on Google Drive"
>View on Google Drive</a
>
</div>
<div>
<img src="./assets/billing.png" alt="Generic billing image" />
</div>
</div>
</app-selected-registrar-wrapper>

View File

@@ -1,22 +0,0 @@
.console-app__billing {
display: flex;
flex-wrap: wrap-reverse;
> div {
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
text-align: left;
max-width: 300px;
min-width: 200px;
margin-top: -40px;
}
img {
aspect-ratio: 1 / 1;
width: 100%;
}
&-subhead {
font-size: 1.25rem;
margin-bottom: 20px;
}
}

View File

@@ -1,48 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, computed } from '@angular/core';
import { RegistrarService } from '../registrar/registrar.service';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-billingInfo',
templateUrl: './billingInfo.component.html',
styleUrls: ['./billingInfo.component.scss'],
standalone: false,
})
export class BillingInfoComponent {
public static PATH = 'billingInfo';
constructor(
public registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
driveFolderUrl = computed<string>(() => {
if (this.registrarService.registrar()?.driveFolderId) {
return (
'https://drive.google.com/drive/folders/' +
this.registrarService.registrar()?.driveFolderId
);
}
return '';
});
openBillingDetails(e: MouseEvent) {
if (!this.registrarService.registrar()?.driveFolderId) {
e.preventDefault();
this._snackBar.open('Billing Folder ID has not been assigned');
}
}
}

View File

@@ -1,230 +0,0 @@
<app-selected-registrar-wrapper>
<div class="console-app-domains">
<h1 class="mat-headline-4" forceFocus>Domains</h1>
<div
class="console-app-domains__actions-wrapper"
[hidden]="!domainListService.activeActionComponent"
>
<ng-container
v-if="domainListService.activeActionComponent"
*ngComponentOutlet="domainListService.activeActionComponent"
>
</ng-container>
</div>
@if (!isLoading && totalResults == 0) {
<div class="console-app__empty-domains">
<h1>
<mat-icon class="console-app__empty-domains-icon secondary-text"
>apps_outage</mat-icon
>
</h1>
<h1>No domains found</h1>
</div>
} @else {
<mat-menu #actions="matMenu">
<ng-template
matMenuContent
let-domainName="domainName"
let-domain="domain"
>
<button
mat-menu-item
(click)="openRegistryLock(domainName)"
aria-label="Access registry lock for domain"
>
<mat-icon>key</mat-icon>
<span>Registry Lock</span>
</button>
<button
mat-menu-item
(click)="onSuspendClick(domainName)"
[elementId]="getElementIdForSuspendUnsuspend()"
[disabled]="isDomainUnsuspendable(domain)"
>
<mat-icon>lock_clock</mat-icon>
<span>Suspend</span>
</button>
<button
mat-menu-item
(click)="onUnsuspendClick(domainName)"
[elementId]="getElementIdForSuspendUnsuspend()"
[disabled]="!isDomainUnsuspendable(domain)"
>
<mat-icon>lock_open</mat-icon>
<span>Unsuspend</span>
</button>
</ng-template>
</mat-menu>
<div
class="console-app__domains-table-parent"
[hidden]="domainListService.activeActionComponent"
>
<div class="console-app__scrollable-wrapper">
<div class="console-app__scrollable">
@if (isLoading) {
<div class="console-app__domains-spinner">
<mat-spinner />
</div>
}
<a
mat-stroked-button
color="primary"
href="/console-api/dum-download?registrarId={{
registrarService.registrarId()
}}"
class="console-app-domains__download"
>
<mat-icon>download</mat-icon>
Download domains (.csv)
</a>
<mat-form-field class="console-app__domains-filter">
<mat-label>Filter</mat-label>
<input
type="search"
matInput
[(ngModel)]="searchTerm"
(ngModelChange)="sendInput()"
#input
/>
</mat-form-field>
<div
class="console-app__domains-selection"
[elementId]="getElementIdForBulkDelete()"
[ngClass]="{ active: selection.hasValue() }"
>
<div class="console-app__domains-selection-text">
{{ selection.selected.length }} Selected
</div>
<div class="console-app__domains-selection-actions">
<button
mat-flat-button
aria-label="Delete Selected Domains"
[attr.aria-hidden]="!selection.hasValue()"
(click)="deleteSelectedDomains()"
>
Delete Selected Domains
</button>
</div>
</div>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__domains-table"
>
<!-- Checkbox Column -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox
(change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected"
[indeterminate]="selection.hasValue() && !isAllSelected"
[aria-label]="checkboxLabel()"
[elementId]="getElementIdForBulkDelete()"
>
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
[aria-label]="checkboxLabel(row)"
[elementId]="getElementIdForBulkDelete()"
>
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="domainName">
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
<mat-cell *matCellDef="let element">
<mat-icon
*ngIf="getOperationMessage(element.domainName)"
[matTooltip]="getOperationMessage(element.domainName)"
matTooltipPosition="above"
class="primary-text"
>info</mat-icon
>
<span>{{ element.domainName }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="creationTime">
<mat-header-cell *matHeaderCellDef>Creation Time</mat-header-cell>
<mat-cell *matCellDef="let element">
{{ element.creationTime.creationTime }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="registrationExpirationTime">
<mat-header-cell *matHeaderCellDef
>Expiration Time</mat-header-cell
>
<mat-cell *matCellDef="let element">
{{ element.registrationExpirationTime }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="statuses">
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
<mat-cell *matCellDef="let element">
<span>{{ element.statuses?.join(", ") }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="registryLock">
<mat-header-cell *matHeaderCellDef
>Registry-Locked</mat-header-cell
>
<mat-cell *matCellDef="let element">{{
isDomainLocked(element.domainName)
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
<mat-cell *matCellDef="let element">
<button
mat-icon-button
[matMenuTriggerFor]="actions"
[matMenuTriggerData]="{
domainName: element.domainName,
domain: element
}"
aria-label="Domain actions"
>
<mat-icon>more_horiz</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row
*matHeaderRowDef="displayedColumns"
></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
<!-- Row shown when there is no matching data. -->
<mat-row *matNoDataRow>
<mat-cell colspan="6">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>
</div>
}
</div>
</app-selected-registrar-wrapper>

View File

@@ -1,92 +0,0 @@
.console-app {
$min-width: 756px;
&__empty-domains {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
&-icon {
transform: scale(3);
margin-bottom: 0.5rem;
}
}
&__domains-selection {
height: 60px;
max-height: 0;
transition: max-height 0.2s linear;
display: flex;
align-items: center;
overflow: hidden;
gap: 20px;
&-text {
font-weight: bold;
}
&.active {
max-height: 60px;
}
}
&-domains__download {
position: absolute;
top: -55px;
right: 0;
}
&__domains-filter {
min-width: $min-width !important;
width: 100%;
}
&__domains-table-parent {
position: relative;
min-width: 100%;
}
&__domains-table {
min-width: $min-width !important;
.mat-column-actions {
max-width: 100px;
}
.mat-column-registryLock {
max-width: 150px;
}
.mat-column-statuses span {
padding: 10px 0;
overflow: hidden;
word-break: break-word;
}
.mat-column-select {
max-width: 60px;
padding-left: 15px;
}
.mat-column-domainName {
position: relative;
padding-left: 25px;
mat-icon {
position: absolute;
left: 0;
}
}
mat-cell:has([style*="display: none"]),
mat-header-cell:has([style*="display: none"]) {
display: none;
}
}
&__domains-spinner {
align-items: center;
display: flex;
justify-content: center;
position: absolute;
height: 100%;
width: 100%;
background: rgba(255, 255, 255, 0.6);
z-index: 2;
}
.mat-mdc-paginator {
min-width: $min-width !important;
}
}

View File

@@ -1,54 +0,0 @@
// 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 { 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';
import { FormsModule } from '@angular/forms';
import { AppModule } from '../app.module';
describe('DomainListComponent', () => {
let component: DomainListComponent;
let fixture: ComponentFixture<DomainListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DomainListComponent],
imports: [
MaterialModule,
BrowserAnimationsModule,
FormsModule,
AppModule,
],
providers: [
BackendService,
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(DomainListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,419 +0,0 @@
// 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 { SelectionModel } from '@angular/cdk/collections';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { Component, effect, Inject, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
import { debounceTime, filter, Subject, take } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import {
BULK_ACTION_NAME,
Domain,
DomainListService,
} from './domainList.service';
import { RegistryLockComponent } from './registryLock.component';
import { RegistryLockService } from './registryLock.service';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogRef,
} from '@angular/material/dialog';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
interface DomainResponse {
message: string;
responseCode: string;
}
interface DomainData {
[domain: string]: DomainResponse;
}
@Component({
selector: 'app-response-dialog',
template: `
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content [innerHTML]="data.content" />
<mat-dialog-actions>
<button mat-button (click)="onClose()">Close</button>
</mat-dialog-actions>
`,
standalone: false,
})
export class ResponseDialogComponent {
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { title: string; content: string }
) {}
onClose(): void {
this.dialogRef.close();
}
}
enum Operation {
deleting = 'deleting',
suspending = 'suspending',
unsuspending = 'unsuspending',
}
@Component({
selector: 'app-reason-dialog',
template: `
<h2 mat-dialog-title>
Please provide the (EPP) reason for {{ data.operation }} the domain(s):
</h2>
<mat-dialog-content>
<mat-form-field appearance="outline" style="width:100%">
<textarea matInput [(ngModel)]="reason" rows="4"></textarea>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-button color="warn" (click)="onSave()" [disabled]="!reason">
Save
</button>
</mat-dialog-actions>
`,
standalone: false,
})
export class ReasonDialogComponent {
reason: string = '';
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { operation: Operation }
) {}
onSave(): void {
this.dialogRef.close(this.reason);
}
onCancel(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'app-domain-list',
templateUrl: './domainList.component.html',
styleUrls: ['./domainList.component.scss'],
standalone: false,
})
export class DomainListComponent {
public static PATH = 'domain-list';
private static SUSPENDED_STATUSES = [
'SERVER_RENEW_PROHIBITED',
'SERVER_TRANSFER_PROHIBITED',
'SERVER_UPDATE_PROHIBITED',
'SERVER_DELETE_PROHIBITED',
'SERVER_HOLD',
];
private readonly DEBOUNCE_MS = 500;
isAllSelected = false;
displayedColumns: string[] = [
'select',
'domainName',
'creationTime',
'registrationExpirationTime',
'statuses',
'registryLock',
'actions',
];
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
selection = new SelectionModel<Domain>(true, [], undefined, this.isChecked());
isLoading = true;
searchTermSubject = new Subject<string>();
searchTerm?: string;
pageNumber?: number;
resultsPerPage = 50;
totalResults?: number = 0;
reason: string = '';
operationResult: DomainData | undefined;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
constructor(
protected domainListService: DomainListService,
protected registrarService: RegistrarService,
protected registryLockService: RegistryLockService,
private _snackBar: MatSnackBar,
private dialog: MatDialog
) {
effect(() => {
this.pageNumber = 0;
this.totalResults = 0;
if (this.registrarService.registrarId()) {
this.loadLocks();
this.reloadData();
}
});
}
ngOnInit() {
this.dataSource.paginator = this.paginator;
// Don't spam the server unnecessarily while the user is typing
this.searchTermSubject
.pipe(debounceTime(this.DEBOUNCE_MS))
.subscribe((searchTermValue) => {
this.reloadData();
});
}
ngOnDestroy() {
this.searchTermSubject.complete();
}
openRegistryLock(domainName: string) {
this.domainListService.selectedDomain = domainName;
this.domainListService.activeActionComponent = RegistryLockComponent;
}
loadLocks() {
this.registryLockService.retrieveLocks().subscribe({
error: (err: HttpErrorResponse) => {
if (err.status !== HttpStatusCode.Forbidden) {
// Some users may not have registry lock permissions and that's OK
this._snackBar.open(err.message);
}
},
});
}
isDomainLocked(domainName: string) {
return this.registryLockService.domainsLocks.some(
(d) => d.domainName === domainName
);
}
reloadData() {
this.isLoading = true;
this.domainListService
.retrieveDomains(
this.pageNumber,
this.resultsPerPage,
this.totalResults,
this.searchTerm
)
.subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.message);
this.isLoading = false;
},
next: (domainListResult) => {
this.dataSource.data = this.domainListService.domainsList;
this.totalResults = (domainListResult || {}).totalResults || 0;
this.isLoading = false;
},
});
}
sendInput() {
this.searchTermSubject.next(this.searchTerm!);
}
onPageChange(event: PageEvent) {
this.pageNumber = event.pageIndex;
this.resultsPerPage = event.pageSize;
this.selection.clear();
this.reloadData();
}
toggleAllRows() {
if (this.isAllSelected) {
this.selection.clear();
this.isAllSelected = false;
return;
}
this.selection.select(...this.dataSource.data);
this.isAllSelected = true;
}
checkboxLabel(row?: Domain): string {
if (!row) {
return `${this.isAllSelected ? 'deselect' : 'select'} all`;
}
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${
row.domainName
}`;
}
private isChecked(): ((o1: Domain, o2: Domain) => boolean) | undefined {
return (o1: Domain, o2: Domain) => {
if (!o1.domainName || !o2.domainName) {
return false;
}
return this.isAllSelected || o1.domainName === o2.domainName;
};
}
getElementIdForBulkDelete() {
return RESTRICTED_ELEMENTS.BULK_DELETE;
}
getElementIdForSuspendUnsuspend() {
return RESTRICTED_ELEMENTS.SUSPEND;
}
getOperationMessage(domain: string) {
if (this.operationResult && this.operationResult[domain])
return this.operationResult[domain].message;
return '';
}
isDomainUnsuspendable(domain: Domain) {
return DomainListComponent.SUSPENDED_STATUSES.every((s) =>
domain.statuses.includes(s)
);
}
sendDeleteRequest(reason: string) {
this.isLoading = true;
this.domainListService
.bulkDomainAction(
this.selection.selected.map((d) => d.domainName),
reason,
this.registrarService.registrarId(),
BULK_ACTION_NAME.DELETE
)
.pipe(take(1))
.subscribe({
next: (result: DomainData) => {
this.isLoading = false;
const successCount = Object.keys(result).filter((domainName) =>
result[domainName].responseCode.toString().startsWith('1')
).length;
const failureCount = Object.keys(result).length - successCount;
this.dialog.open(ResponseDialogComponent, {
data: {
title: 'Domain Deletion Results',
content: `Successfully deleted - ${successCount} domain(s)<br/>Failed to delete - ${failureCount} domain(s)<br/>${
failureCount
? 'Some domains could not be deleted due to ongoing processes or server errors. '
: ''
}Please check the table for more information.`,
},
});
this.selection.clear();
this.operationResult = result;
this.reloadData();
},
error: (err: HttpErrorResponse) => {
this.isLoading = false;
this._snackBar.open(err.error || err.message);
},
});
}
deleteSelectedDomains() {
const dialogRef = this.dialog.open(ReasonDialogComponent, {
data: {
operation: Operation.deleting,
},
});
dialogRef
.afterClosed()
.pipe(
take(1),
filter((reason) => !!reason)
)
.subscribe(this.sendDeleteRequest.bind(this));
}
sendSuspendUnsuspendRequest(
domainName: string,
reason: string,
actionName: BULK_ACTION_NAME
) {
this.isLoading = true;
this.domainListService
.bulkDomainAction(
[domainName],
reason,
this.registrarService.registrarId(),
actionName
)
.pipe(take(1))
.subscribe({
next: (result: DomainData) => {
this.isLoading = false;
if (result[domainName].responseCode.toString().startsWith('2')) {
this._snackBar.open(result[domainName].message);
} else {
this.reloadData();
}
},
error: (err: HttpErrorResponse) => {
this.isLoading = false;
this._snackBar.open(err.error || err.message);
},
});
}
onSuspendClick(domainName: string) {
const dialogRef = this.dialog.open(ReasonDialogComponent, {
data: {
operation: Operation.suspending,
},
});
dialogRef
.afterClosed()
.pipe(
take(1),
filter((reason) => !!reason)
)
.subscribe((reason) => {
this.sendSuspendUnsuspendRequest(
domainName,
reason,
BULK_ACTION_NAME.SUSPEND
);
});
}
onUnsuspendClick(domainName: string) {
const dialogRef = this.dialog.open(ReasonDialogComponent, {
data: {
operation: Operation.unsuspending,
},
});
dialogRef
.afterClosed()
.pipe(
take(1),
filter((reason) => !!reason)
)
.subscribe((reason) => {
this.sendSuspendUnsuspendRequest(
domainName,
reason,
BULK_ACTION_NAME.UNSUSPEND
);
});
}
}

View File

@@ -1,93 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Type } from '@angular/core';
import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
export interface CreateAutoTimestamp {
creationTime: string;
}
export interface Domain {
creationTime: CreateAutoTimestamp;
currentSponsorRegistrarId: string;
domainName: string;
registrationExpirationTime: string;
statuses: string[];
}
export interface DomainListResult {
checkpointTime: string;
domains: Domain[];
totalResults: number;
}
export enum BULK_ACTION_NAME {
DELETE = 'DELETE',
SUSPEND = 'SUSPEND',
UNSUSPEND = 'UNSUSPEND',
}
@Injectable({
providedIn: 'root',
})
export class DomainListService {
checkpointTime?: string;
selectedDomain?: string;
public activeActionComponent: Type<any> | null = null;
public domainsList: Domain[] = [];
constructor(
private backendService: BackendService,
private registrarService: RegistrarService
) {}
retrieveDomains(
pageNumber?: number,
resultsPerPage?: number,
totalResults?: number,
searchTerm?: string
) {
return this.backendService
.getDomains(
this.registrarService.registrarId(),
this.checkpointTime,
pageNumber,
resultsPerPage,
totalResults,
searchTerm
)
.pipe(
tap((domainListResult: DomainListResult) => {
this.checkpointTime = domainListResult?.checkpointTime;
this.domainsList = domainListResult?.domains;
})
);
}
bulkDomainAction(
domains: string[],
reason: string,
registrarId: string,
actionName: BULK_ACTION_NAME
) {
return this.backendService.bulkDomainAction(
domains,
reason,
actionName,
registrarId
);
}
}

View File

@@ -1,83 +0,0 @@
<div class="console-app__registry-lock">
<p>
<button
mat-icon-button
aria-label="Back to domains list"
(click)="goBack()"
>
<mat-icon>arrow_back</mat-icon>
</button>
</p>
@if(!registrarService.registrar()?.registryLockAllowed) {
<h1>
Sorry, your registrar hasn't enrolled in registry lock yet. To do so, please
contact {{ userDataService.userData()?.supportEmail }}.
</h1>
} @else if (isLocked()) {
<h1>Unlock the domain {{ domainListService.selectedDomain }}</h1>
<form (ngSubmit)="save(false)" [formGroup]="unlockDomain">
<p>
<mat-label for="password">Password: </mat-label>
<mat-form-field name="password" appearance="outline">
<input matInput type="text" formControlName="password" required />
</mat-form-field>
</p>
<p>
<mat-label for="relockTime"
>Automatically re-lock the domain after:</mat-label
>
<mat-radio-group
name="relockTime"
formControlName="relockTime"
aria-label="Automatically relock option"
>
@for (option of relockOptions; track option.name) {
<mat-radio-button [value]="option.duration">{{
option.name
}}</mat-radio-button>
}
</mat-radio-group>
</p>
<div class="console-app__registry-lock-notification">
<mat-icon>priority_high</mat-icon>Confirmation email will be sent to your
email address to confirm the unlock
</div>
<button
mat-flat-button
color="primary"
type="submit"
[disabled]="!unlockDomain.valid"
aria-label="Submit domain unlock request"
>
Save
</button>
</form>
} @else {
<h1>Lock the domain {{ domainListService.selectedDomain }}</h1>
<form (ngSubmit)="save(true)" [formGroup]="lockDomain">
<p>
<mat-label for="password">Password: </mat-label>
<mat-form-field name="password" appearance="outline">
<input matInput type="text" formControlName="password" required />
</mat-form-field>
</p>
<div class="console-app__registry-lock-notification">
<mat-icon>priority_high</mat-icon>The lock will not take effect until you
click the confirmation link that will be emailed to you. When it takes
effect, you will be billed the standard server status change billing cost.
</div>
<button
mat-flat-button
color="primary"
type="submit"
[disabled]="!lockDomain.valid"
aria-label="Submit domain lock request"
>
Save
</button>
</form>
}
</div>

View File

@@ -1,20 +0,0 @@
.console-app {
&__registry-lock {
mat-label {
display: block;
margin-bottom: 10px;
}
p {
margin-bottom: 40px;
}
}
&__registry-lock-notification {
padding: 20px;
border-radius: 10px;
background-color: var(--light-highlight);
margin-bottom: 20px;
width: max-content;
display: flex;
align-items: center;
}
}

View File

@@ -1,93 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { HttpErrorResponse } from '@angular/common/http';
import { Component, computed } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from '../registrar/registrar.service';
import { UserDataService } from '../shared/services/userData.service';
import { DomainListService } from './domainList.service';
import { RegistryLockService } from './registryLock.service';
@Component({
selector: 'app-registry-lock',
templateUrl: './registryLock.component.html',
styleUrls: ['./registryLock.component.scss'],
standalone: false,
})
export class RegistryLockComponent {
readonly isLocked = computed(() =>
this.registryLockService.domainsLocks.some(
(dl) => dl.domainName === this.domainListService.selectedDomain
)
);
relockOptions = [
{ name: '1 hour', duration: 3600000 },
{ name: '6 hours', duration: 21600000 },
{ name: '24 hours', duration: 86400000 },
{ name: 'Never', duration: undefined },
];
lockDomain = new FormGroup({
password: new FormControl(''),
});
unlockDomain = new FormGroup({
password: new FormControl(''),
relockTime: new FormControl(undefined),
});
constructor(
protected registrarService: RegistrarService,
protected domainListService: DomainListService,
protected registryLockService: RegistryLockService,
protected userDataService: UserDataService,
private _snackBar: MatSnackBar
) {}
goBack() {
this.domainListService.selectedDomain = undefined;
this.domainListService.activeActionComponent = null;
}
save(isLock: boolean) {
let request;
if (!isLock) {
request = this.registryLockService.registryLockDomain(
this.domainListService.selectedDomain || '',
this.unlockDomain.value.password || '',
this.unlockDomain.value.relockTime || undefined,
isLock
);
} else {
request = this.registryLockService.registryLockDomain(
this.domainListService.selectedDomain || '',
this.lockDomain.value.password || '',
undefined,
isLock
);
}
request.subscribe({
complete: () => {
this.goBack();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
});
}
}

View File

@@ -1,59 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
export interface DomainLocksResult {
domainName: string;
}
@Injectable({
providedIn: 'root',
})
export class RegistryLockService {
public domainsLocks: DomainLocksResult[] = [];
constructor(
private backendService: BackendService,
private registrarService: RegistrarService
) {}
retrieveLocks() {
return this.backendService
.getLocks(this.registrarService.registrarId())
.pipe(
tap((domainLocksResult) => {
this.domainsLocks = domainLocksResult;
})
);
}
registryLockDomain(
domainName: string,
password: string,
relockDurationMillis: number | undefined,
isLock: boolean
) {
return this.backendService.registryLockDomain(
domainName,
password,
relockDurationMillis,
this.registrarService.registrarId(),
isLock
);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,39 +0,0 @@
// 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.
.console-app {
&__logo {
color: inherit;
text-decoration: none;
margin-left: -15px;
}
&__menu-btn {
width: 30px;
height: 30px;
padding: 0;
}
&__header {
margin-top: 0;
margin-bottom: 15px;
.mat-toolbar {
background: transparent;
margin-bottom: 10px;
}
&-user-icon {
margin-left: 20px;
}
}
}

View File

@@ -1,57 +0,0 @@
// 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from '../material.module';
import { ActivatedRoute } from '@angular/router';
import { AppModule, SelectedRegistrarModule } from '../app.module';
import { AppRoutingModule } from '../app-routing.module';
import { BackendService } from '../shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
SelectedRegistrarModule,
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
AppModule,
],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
provideHttpClient(),
provideHttpClientTesting(),
],
declarations: [HeaderComponent],
}).compileComponents();
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,39 +0,0 @@
// 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, EventEmitter, Output } from '@angular/core';
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
standalone: false,
})
export class HeaderComponent {
private isNavOpen = false;
constructor(protected breakpointObserver: BreakPointObserverService) {}
@Output() toggleNavOpen = new EventEmitter<boolean>();
toggleNavPane() {
this.isNavOpen = !this.isNavOpen;
this.toggleNavOpen.emit(this.isNavOpen);
}
logOut() {
window.open('/console?gcp-iap-mode=CLEAR_LOGIN_COOKIE', '_self');
}
}

View File

@@ -1,55 +1,14 @@
<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">
<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>
<h3>Recent Activity</h3>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8 console-home__activity">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef>
{{column.header}}
</th>
<td mat-cell *matCellDef="let row">
{{column.cell(row)}}
</td>
</ng-container>
<mat-card appearance="outlined">
<mat-card-content>
<h3>
<mat-icon class="secondary-text">settings</mat-icon>
EPP Passwords
</h3>
<p class="secondary-text">View / Update EPP Password</p>
</mat-card-content>
<mat-card-actions>
<button mat-button color="primary" (click)="updateEppPassword()">
Update EPP Password
</button>
</mat-card-actions>
</mat-card>
<mat-card
appearance="outlined"
[elementId]="getElementIdForRegistrarsBlock()"
>
<mat-card-content>
<h3>
<mat-icon class="secondary-text">account_circle</mat-icon>
Registrars
</h3>
<p class="secondary-text">View all registrars</p>
</mat-card-content>
<mat-card-actions>
<button mat-button color="primary" (click)="viewRegistrars()">
Manage Registrars
</button>
</mat-card-actions>
</mat-card>
</div>
</div>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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,8 +12,3 @@
// See the License for the specific language governing permissions and
// limitations under the License.
.console-settings {
nav {
margin-bottom: 40px;
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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.
@@ -15,8 +15,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
import { MaterialModule } from '../material.module';
import { AppModule } from '../app.module';
import {MaterialModule} from '../material.module';
describe('HomeComponent', () => {
let component: HomeComponent;
@@ -24,9 +23,10 @@ describe('HomeComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule, AppModule],
declarations: [HomeComponent],
}).compileComponents();
imports: [MaterialModule],
declarations: [ HomeComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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,42 +13,82 @@
// limitations under the License.
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { DomainListComponent } from '../domains/domainList.component';
import { RegistrarComponent } from '../registrar/registrarsTable.component';
import SecurityComponent from '../settings/security/security.component';
import { SettingsComponent } from '../settings/settings.component';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
export interface ActivityRecord {
eventType: string;
userName: string;
registrarName: string;
timestamp: string;
details: string
}
const MOCK_DATA: ActivityRecord[] = [
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
];
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
standalone: false,
styleUrls: ['./home.component.less']
})
export class HomeComponent {
constructor(
protected breakPointObserverService: BreakPointObserverService,
private router: Router
) {}
getElementIdForRegistrarsBlock() {
return RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT;
}
viewRegistrars() {
this.router.navigate([RegistrarComponent.PATH], {
queryParamsHandling: 'merge',
});
}
updateEppPassword() {
this.router.navigate(
[SettingsComponent.PATH + '/' + SecurityComponent.PATH],
{ queryParamsHandling: 'merge' }
);
}
viewDums() {
this.router.navigate([DomainListComponent.PATH], {
queryParamsHandling: 'merge',
});
columns = [
{
columnDef: 'eventType',
header: 'Event Type',
cell:(record: ActivityRecord) => `${record.eventType}`,
},
{
columnDef: 'userName',
header: 'User',
cell: (record: ActivityRecord) => `${record.userName}`,
},
{
columnDef: 'registrarName',
header: 'Registrar',
cell: (record: ActivityRecord) => `${record.registrarName}`,
},
{
columnDef: 'timestamp',
header: 'Timestamp',
cell: (record: ActivityRecord) => `${record.timestamp}`,
},
{
columnDef: 'details',
header: 'Details',
cell: (record: ActivityRecord) => `${record.details}`,
},
];
dataSource = MOCK_DATA;
displayedColumns = this.columns.map(c => c.columnDef);
constructor() {
}
}

View File

@@ -1,28 +0,0 @@
@if (isLoading) {
<div class="console-app__registry-lock-verify-spinner">
<mat-spinner />
</div>
} @else if (domainName) {
<h1 class="mat-headline-4">Success!</h1>
<div class="console-app__registry-lock-content">
<div class="console-app__registry-lock-subhead">
The domain {{ domainName }} has been successfully {{ action }}ed.
</div>
</div>
<div>
<a
class="text-l"
routerLink="{{ DOMAIN_LIST_COMPONENT_PATH }}"
[queryParams]="{ registrarId: this.registrarService.registrarId() }"
>Return to the list of domains</a
>
</div>
} @else {
<h1 class="mat-headline-4">Failure</h1>
<div class="console-app__registry-lock-content">
<div class="console-app__registry-lock-subhead">
An error occurred: {{ errorMessage }}.<br /><br />Please double-check the
verification code and try again.
</div>
</div>
}

View File

@@ -1,9 +0,0 @@
.console-app__registry-lock {
&-content {
margin-top: 30px;
}
&-subhead {
font-size: 1.25rem;
margin-bottom: 20px;
}
}

View File

@@ -1,66 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { RegistrarService } from '../registrar/registrar.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { RegistryLockVerifyService } from './registryLockVerify.service';
import { HttpErrorResponse } from '@angular/common/http';
import { take } from 'rxjs';
import { DomainListComponent } from '../domains/domainList.component';
@Component({
selector: 'app-registry-lock-verify',
templateUrl: './registryLockVerify.component.html',
styleUrls: ['./registryLockVerify.component.scss'],
providers: [RegistryLockVerifyService],
standalone: false,
})
export class RegistryLockVerifyComponent {
public static PATH = 'registry-lock-verify';
readonly DOMAIN_LIST_COMPONENT_PATH = `/${DomainListComponent.PATH}`;
isLoading = true;
domainName?: string;
action?: string;
errorMessage?: string;
constructor(
protected registrarService: RegistrarService,
protected registryLockVerifyService: RegistryLockVerifyService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.route.queryParamMap.pipe(take(1)).subscribe((params: ParamMap) => {
this.registryLockVerifyService
.verifyRequest(params.get('lockVerificationCode') || '')
.subscribe({
error: (err: HttpErrorResponse) => {
this.isLoading = false;
this.errorMessage = err.error;
},
next: (verificationResponse) => {
this.domainName = verificationResponse.domainName;
this.action = verificationResponse.action;
this.registrarService.registrarId.set(
verificationResponse.registrarId
);
this.isLoading = false;
},
});
});
}
}

View File

@@ -1,31 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
export interface RegistryLockVerificationResponse {
action: string;
domainName: string;
registrarId: string;
}
@Injectable()
export class RegistryLockVerifyService {
constructor(private backendService: BackendService) {}
verifyRequest(lockVerificationCode: string) {
return this.backendService.verifyRegistryLockRequest(lockVerificationCode);
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2022 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,81 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
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 { 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';
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 { 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 {NgModule} from '@angular/core';
import {MatCardModule} from '@angular/material/card';
import {MatTableModule} from '@angular/material/table';
@NgModule({
exports: [
A11yModule,
CdkMenuModule,
CdkTableModule,
CdkTreeModule,
MatBadgeModule,
MatButtonModule,
MatButtonToggleModule,
MatBottomSheetModule,
MatCardModule,
MatCheckboxModule,
MatDialogModule,
MatDividerModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatTreeModule,
OverlayModule,
DialogModule,
MatSnackBarModule,
MatPaginatorModule,
MatChipsModule,
MatAutocompleteModule,
ReactiveFormsModule,
],
})
export class MaterialModule {}
const MATERIAL_MODULES = [
MatCardModule,
MatTableModule,
];
@NgModule({imports: MATERIAL_MODULES, exports: MATERIAL_MODULES})
export class MaterialModule {
}

View File

@@ -1,49 +0,0 @@
<mat-tree
[dataSource]="dataSource"
[treeControl]="treeControl"
class="console-app__nav-tree"
>
<mat-tree-node
*matTreeNodeDef="let node"
matTreeNodeToggle
tabindex="0"
(click)="onClick(node)"
(keyup.enter)="onClick(node)"
[class.active]="router.url.includes(node.path)"
[elementId]="getElementId(node)"
>
<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)"
tabindex="0"
(keyup.enter)="onClick(node)"
>
<div class="mat-tree-node" [class.active]="router.url.includes(node.path)">
<button
class="console-app__nav-icon_expand"
mat-icon-button
matTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.title"
>
<mat-icon>
{{ treeControl.isExpanded(node) ? "expand_more" : "chevron_right" }}
</mat-icon>
</button>
<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

@@ -1,71 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
$expand-icon-size: 26px;
.console-app {
&__sidebar {
min-width: 300px;
border: 0;
}
&__nav-icon {
width: $expand-icon-size;
height: $expand-icon-size;
color: var(--secondary) !important;
margin-right: $expand-icon-size;
&_expand {
position: absolute;
left: 0;
margin: auto;
padding: 0px;
width: $expand-icon-size;
height: $expand-icon-size;
}
}
&__nav-tree {
.mat-tree-node {
cursor: pointer;
padding-left: $expand-icon-size;
position: relative;
&:hover {
background-color: var(--light-highlight);
border-radius: 0 15px 15px 0;
}
&.active {
border-radius: 0 15px 15px 0;
background-color: var(--lightest);
}
}
div[role="group"] > .mat-tree-node {
// expand icon + regular icon + spacing = 3 * $expand-icon-size
padding-left: calc($expand-icon-size * 3);
}
ul,
li {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
&_invisible {
display: none;
}
}
}

View File

@@ -1,120 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NestedTreeControl } from '@angular/cdk/tree';
import { Component } from '@angular/core';
import { MatTreeNestedDataSource } from '@angular/material/tree';
import { NavigationEnd, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { RouteWithIcon, routes, PATHS } from '../app-routing.module';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
import { RegistrarComponent } from '../registrar/registrarsTable.component';
interface NavMenuNode extends RouteWithIcon {
parentRoute?: RouteWithIcon;
}
/**
* This component is responsible for rendering navigation menu based on the allowed routes
* and keeping UI in sync when route changes (eg highlights selected route in the menu).
*/
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
standalone: false,
})
export class NavigationComponent {
renderRouter: boolean = true;
treeControl = new NestedTreeControl<RouteWithIcon>((node) => node.children);
dataSource = new MatTreeNestedDataSource<RouteWithIcon>();
private subscription!: Subscription;
hasChild = (_: number, node: RouteWithIcon) =>
!!node.children && node.children.length > 0;
constructor(protected router: Router) {
this.dataSource.data = this.ngRoutesToNavMenuNodes(routes);
}
ngOnInit() {
this.subscription = this.router.events.subscribe((navigationParams) => {
if (navigationParams instanceof NavigationEnd) {
this.syncExpandedNavigationWithRoute(navigationParams.url);
}
});
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
getElementId(node: RouteWithIcon) {
if (node.path === RegistrarComponent.PATH) {
return RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT;
} else if (node.path === PATHS.UsersComponent) {
return RESTRICTED_ELEMENTS.USERS;
}
return null;
}
syncExpandedNavigationWithRoute(url: string) {
const maybeComponentWithChildren = this.dataSource.data.find((menuNode) => {
return (
// @ts-ignore - optional function added to components with children,
// there's no availble tools to get current active router component
typeof menuNode.component?.matchesUrl === 'function' &&
// @ts-ignore
menuNode.component?.matchesUrl(url)
);
});
if (maybeComponentWithChildren) {
this.treeControl.expand(maybeComponentWithChildren);
}
}
onClick(node: NavMenuNode) {
if (node.parentRoute) {
this.router.navigate([node.parentRoute.path + '/' + node.path], {
queryParamsHandling: 'merge',
});
} else {
this.router.navigate([node.path], { queryParamsHandling: 'merge' });
}
}
/**
* We only want to use routes with titles and we want to provide easy reference to parent node
*/
ngRoutesToNavMenuNodes(routes: RouteWithIcon[]): NavMenuNode[] {
return routes
.filter((r) => r.title)
.map((r) => {
if (r.children) {
return {
...r,
children: r.children
.filter((r) => r.title)
.map((childRoute) => {
return {
...childRoute,
parentRoute: r,
};
}),
};
}
return r;
});
}
}

View File

@@ -1,43 +0,0 @@
<h1 class="mat-headline-4">Generate OT&E Accounts</h1>
<div class="console-app__new-ote">
@if (oteCreateResponseFormatted()) {
<h1>Generated Successfully</h1>
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title>Epp Credentials</mat-card-title>
<mat-card-subtitle
>Copy and paste this into an email to the registrars</mat-card-subtitle
>
</mat-card-header>
<mat-card-content>
<p>{{ oteCreateResponseFormatted() }}</p>
</mat-card-content>
</mat-card>
} @else {
<form (ngSubmit)="onSubmit()" [formGroup]="createOte">
<p>
<mat-form-field name="registrarId" appearance="outline">
<mat-label>Base Registrar Id: </mat-label>
<input matInput type="text" formControlName="registrarId" required />
</mat-form-field>
</p>
<p>
<mat-form-field name="registrarEmail" appearance="outline">
<mat-label>Contact Email: </mat-label>
<input matInput type="text" formControlName="registrarEmail" required />
<mat-hint
>Will be granted web-console access to the OTE registrars.</mat-hint
>
</mat-form-field>
</p>
<button
mat-flat-button
color="primary"
type="submit"
aria-label="Submit new OT&E account"
>
Save
</button>
</form>
}
</div>

View File

@@ -1,11 +0,0 @@
.console-app__new-ote {
max-width: 720px;
mat-card-content {
white-space: break-spaces;
padding: 20px;
}
mat-form-field {
width: 100%;
max-width: 350px;
}
}

View File

@@ -1,82 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { HttpErrorResponse } from '@angular/common/http';
import { Component, computed, signal } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from '../registrar/registrar.service';
import { MaterialModule } from '../material.module';
import { SnackBarModule } from '../snackbar.module';
export interface OteCreateResponse extends Map<string, string> {
password: string;
}
@Component({
selector: 'app-ote',
imports: [MaterialModule, SnackBarModule],
templateUrl: './newOte.component.html',
styleUrls: ['./newOte.component.scss'],
})
export class NewOteComponent {
oteCreateResponse = signal<OteCreateResponse | undefined>(undefined);
readonly oteCreateResponseFormatted = computed(() => {
const oteCreateResponse = this.oteCreateResponse();
if (oteCreateResponse) {
const { password } = oteCreateResponse;
return Object.entries(oteCreateResponse)
.filter((entry) => entry[0] !== 'password')
.map(
([login, tld]) =>
`Login: ${login}\t\tPassword: ${password}\t\tTLD: ${tld}`
)
.join('\n');
}
return undefined;
});
createOte = new FormGroup({
registrarId: new FormControl('', [Validators.required]),
registrarEmail: new FormControl('', [Validators.required]),
});
constructor(
protected registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
onSubmit() {
if (this.createOte.valid) {
const { registrarId, registrarEmail } = this.createOte.value;
this.registrarService
.generateOte(
{
registrarId,
registrarEmail,
},
registrarId || ''
)
.subscribe({
next: (oteCreateResponse: OteCreateResponse) => {
this.oteCreateResponse.set(oteCreateResponse);
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
},
});
}
}
}

View File

@@ -1,28 +0,0 @@
<h1 class="mat-headline-4">OT&E Status Check</h1>
@if(registrarId() === null) {
<h1>Missing registrarId param</h1>
} @else if(isOte()) {
<h1 *ngIf="oteStatusResponse().length">
Status:
<span>{{ oteStatusUnfinished().length ? "Unfinished" : "Completed" }}</span>
</h1>
<div class="console-app__ote-status">
@if(oteStatusCompleted().length) {
<div class="console-app__ote-status_completed">
<h1>Completed</h1>
<div *ngFor="let entry of oteStatusCompleted()">
<mat-icon>check_box</mat-icon>{{ entry.description }}
</div>
</div>
} @if(oteStatusUnfinished().length) {
<div class="console-app__ote-status_unfinished">
<h1>Unfinished</h1>
<div *ngFor="let entry of oteStatusUnfinished()">
<mat-icon>check_box_outline_blank</mat-icon>{{ entry.description }}
</div>
</div>
}
</div>
} @else {
<h1>Registrar {{ registrarId() }} is not an OT&E registrar</h1>
}

View File

@@ -1,28 +0,0 @@
.console-app__ote-status {
max-width: 730px;
display: flex;
flex-wrap: wrap;
&_completed,
&_unfinished {
border: 1px solid #ddd;
padding: 20px;
border-radius: 10px;
margin: 0 20px 30px 0;
div {
display: flex;
min-width: 300px;
align-items: flex-start;
max-width: 300px;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #ddd;
&:last-child {
border: none;
}
}
mat-icon {
min-width: 30px;
}
}
}

View File

@@ -1,78 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { HttpErrorResponse } from '@angular/common/http';
import { Component, computed, OnInit, signal } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from '../registrar/registrar.service';
import { MaterialModule } from '../material.module';
import { SnackBarModule } from '../snackbar.module';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { take } from 'rxjs';
export interface OteStatusResponse {
description: string;
requirement: number;
timesPerformed: number;
completed: boolean;
}
@Component({
selector: 'app-ote-status',
imports: [MaterialModule, SnackBarModule, CommonModule],
templateUrl: './oteStatus.component.html',
styleUrls: ['./oteStatus.component.scss'],
})
export class OteStatusComponent implements OnInit {
registrarId = signal<string | null>(null);
oteStatusResponse = signal<OteStatusResponse[]>([]);
oteStatusCompleted = computed(() =>
this.oteStatusResponse().filter((v) => v.completed)
);
oteStatusUnfinished = computed(() =>
this.oteStatusResponse().filter((v) => !v.completed)
);
isOte = computed(
() =>
this.registrarService
.registrars()
.find((r) => r.registrarId === this.registrarId())
?.type?.toLowerCase() === 'ote'
);
constructor(
private route: ActivatedRoute,
protected registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
ngOnInit(): void {
this.route.paramMap.pipe(take(1)).subscribe((params: ParamMap) => {
this.registrarId.set(params.get('registrarId'));
const registrarId = this.registrarId();
if (!registrarId) throw 'Missing registrarId param';
this.registrarService.oteStatus(registrarId).subscribe({
next: (oteStatusResponse: OteStatusResponse[]) => {
this.oteStatusResponse.set(oteStatusResponse);
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
},
});
});
}
}

View File

@@ -1,183 +0,0 @@
<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]="true"
[(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"
aria-label="Submit new registrar request"
>
Save
</button>
</form>
</div>

View File

@@ -1,20 +0,0 @@
.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

@@ -1,99 +0,0 @@
// 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,
standalone: false,
})
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: '',
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,42 +0,0 @@
// 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 { TestBed } from '@angular/core/testing';
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 { RegistrarService } from './registrar.service';
describe('RegistrarService', () => {
let service: RegistrarService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
BackendService,
MatSnackBar,
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(RegistrarService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,168 +0,0 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, computed, signal } from '@angular/core';
import { Observable, switchMap, tap } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { OteCreateResponse } from '../ote/newOte.component';
import { OteStatusResponse } from '../ote/oteStatus.component';
import { BackendService } from '../shared/services/backend.service';
import {
GlobalLoader,
GlobalLoaderService,
} from '../shared/services/globalLoader.service';
export interface IpAllowListItem {
value: string;
}
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 RdapRegistrarFields {
ianaIdentifier?: number;
icannReferralEmail: string;
localizedAddress: Address;
registrarId: string;
url: string;
}
export interface Registrar
extends RdapRegistrarFields,
SecuritySettingsBackendModel {
allowedTlds?: string[];
billingAccountMap?: object;
driveFolderId?: string;
emailAddress?: string;
faxNumber?: string;
phoneNumber?: string;
registrarId: string;
registrarName: string;
registryLockAllowed?: boolean;
type?: string;
lastPocVerificationDate?: string;
}
@Injectable({
providedIn: 'root',
})
export class RegistrarService implements GlobalLoader {
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 router: Router
) {
this.registrarsLoaded = new Promise((resolve) => {
this.loadRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
resolve();
});
});
this.globalLoader.startGlobalLoader(this);
}
public updateSelectedRegistrar(registrarId: string) {
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.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');
}
generateOte(
oteForm: Object,
registrarId: string
): Observable<OteCreateResponse> {
return this.backend
.generateOte(oteForm, registrarId)
.pipe(tap((_) => this.loadRegistrars()));
}
oteStatus(registrarId: string): Observable<OteStatusResponse[]> {
return this.backend.getOteStatus(registrarId);
}
}

View File

@@ -1,112 +0,0 @@
<div
class="console-app__registrar-view"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<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
*ngIf="oteButtonVisible"
mat-stroked-button
(click)="checkOteStatus()"
aria-label="Check OT&E account status"
[elementId]="getElementIdForOteBlock()"
>
Check OT&E Status
</button>
<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()">
<div>
<mat-form-field appearance="outline">
<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>
</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,19 +0,0 @@
.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,110 +0,0 @@
// 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, OnInit } from '@angular/core';
import { MatChipInputEvent } from '@angular/material/chips';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
import { Registrar, RegistrarService } from './registrar.service';
import { RegistrarComponent, columns } from './registrarsTable.component';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-registrar-details',
templateUrl: './registrarDetails.component.html',
styleUrls: ['./registrarDetails.component.scss'],
standalone: false,
})
export class RegistrarDetailsComponent implements OnInit {
public static PATH = 'registrars/:id';
inEdit: boolean = false;
oteButtonVisible = environment.sandbox;
registrarInEdit!: Registrar;
registrarNotFound: boolean = true;
columns = columns.filter((c) => !c.hiddenOnDetailsCard);
private subscription!: Subscription;
constructor(
protected registrarService: RegistrarService,
private route: ActivatedRoute,
private _snackBar: MatSnackBar,
private router: Router
) {}
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,
e.value.toLowerCase(),
];
}
checkOteStatus() {
this.router.navigate(['ote-status/', this.registrarInEdit.registrarId], {
queryParamsHandling: 'merge',
});
}
getElementIdForOteBlock() {
return RESTRICTED_ELEMENTS.OTE;
}
removeTLD(tld: string) {
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.filter(
(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 && this.subscription.unsubscribe();
}
}

View File

@@ -1,30 +0,0 @@
<div class="console-app__registrar">
<mat-form-field class="field-small" 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"
spellcheck="false"
/>
<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-autocomplete>
</mat-form-field>
</div>

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