diff --git a/.dockerignore b/.dockerignore index 8a2d915aee..63dd9bd94f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,10 @@ .dockerignore Dockerfile *.md -*.properties \ No newline at end of file +*.properties +.git +.gradle +**/.gradle +**/build +**/dist +**/node_modules diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d497d7ae8..4dee2e6823 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,7 +91,9 @@ jobs: if: github.event_name != 'pull_request' && (github.event.ref == 'refs/heads/develop' || startsWith(github.event.ref, 'refs/tags')) uses: softprops/action-gh-release@v2.6.1 with: - files: cwms-data-api/build/libs/cwms-data-api-${{steps.version.outputs.version}}.war + files: | + cwms-data-api/build/libs/cwms-data-api-${{steps.version.outputs.version}}.war + cda-gui/build/libs/cwms-data-ui-${{steps.version.outputs.version}}.war tag_name: ${{steps.version.outputs.version}} generate_release_notes: true token: ${{ secrets.token != null && secrets.token || secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4848a17113..4381687046 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,7 +75,8 @@ By making a contribution to this project, I certify that: ## Getting Started -This project is intended to replace what is currently hosted at https://cwms-data.usace.army.mil/cwms-data/ +This project provides the UI hosted at https://cwms-data.usace.army.mil/ and the API hosted under +https://cwms-data.usace.army.mil/cwms-data/ as separate web applications. Due to the current limits on releasing about database source we don't expect much random contribution. However, we will be moving more of the logic into this repository and any help on the formatting code and such will be greatly appreciated. diff --git a/Dockerfile b/Dockerfile index 75f9d71e55..6f881beb87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,4 @@ FROM gradle:8.5-jdk11 AS builder -USER $USER -RUN --mount=type=cache,target=/home/gradle/.gradle WORKDIR /builddir COPY . /builddir/ RUN apt update && apt install -y curl @@ -12,7 +10,11 @@ SHELL ["/bin/bash", "-c"] RUN . "$NVM_DIR/nvm.sh" && nvm install $NODE_VERSION && nvm use $NODE_VERSION ENV NODE_PATH=$NVM_DIR/v$NODE_VERSION/lib/node_modules ENV PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH -RUN gradle prepareDockerBuild --info --no-daemon +# Docker build contexts created from Git worktrees do not include the main +# repository's Git metadata. Use a deterministic build-only version rather +# than asking the Gradle Git version plugin to resolve an unavailable worktree. +RUN --mount=type=cache,target=/home/gradle/.gradle \ + gradle prepareDockerBuild --info --no-daemon -PversionOverride=docker FROM alpine:3.21.3 AS tomcat_base RUN apk --no-cache upgrade && \ @@ -31,9 +33,6 @@ RUN mkdir /download && \ mv apache-tomcat-9.0.118 /usr/local/tomcat/ && \ cd / && \ rm -rf /usr/local/tomcat/webapps/* && \ - mkdir /usr/local/tomcat/webapps/ROOT && \ - printf "<%% response.sendRedirect(\"/cwms-data/\"); %%>\n" > /usr/local/tomcat/webapps/ROOT/index.jsp && \ - printf "User-agent: *\nAllow: /cwms-data/\nDisallow: /cwms-data/auth/\nDisallow: /cwms-data/catalog/\nDisallow: /cwms-data/timeseries/\nDisallow: /cwms-data/swagger-docs\nDisallow: /auth/\nSitemap: https://cwms-data.usace.army.mil/sitemap.xml\n" > /usr/local/tomcat/webapps/ROOT/robots.txt && \ mkdir -p /usr/local/tomcat/conf/Catalina/localhost # Now replace the Tomcat logging with logback # NOTE: I have reviewed this jar in jd-gui and do not see anything malicious, packages are isolated to avoid issues @@ -52,7 +51,6 @@ CMD ["/usr/local/tomcat/bin/catalina.sh","run"] FROM tomcat_base AS api -COPY --from=builder /builddir/cda-gui/dist/sitemap.xml /usr/local/tomcat/webapps/ROOT/sitemap.xml COPY --from=builder /builddir/cwms-data-api/build/docker/cda/ /usr/local/tomcat COPY --from=builder /builddir/cwms-data-api/build/docker/context.xml /usr/local/tomcat/conf COPY --from=builder /builddir/cwms-data-api/build/docker/server.xml /usr/local/tomcat/conf diff --git a/RELEASE_DEPLOY.md b/RELEASE_DEPLOY.md index 01d18ee36b..386f0d2102 100644 --- a/RELEASE_DEPLOY.md +++ b/RELEASE_DEPLOY.md @@ -36,7 +36,9 @@ portions): The actual release will consist of the following elements: -1. A war file available under the (releases)[https://github.com/usace/cwms-data-api/releases] page +1. Two WAR files available under the [releases](https://github.com/usace/cwms-data-api/releases) page: + 1. `cwms-data-api-.war`, deployed as `cwms-data.war`, provides the API at `/cwms-data`. + 2. `cwms-data-ui-.war`, deployed as `ROOT.war`, provides the UI at `/`. 2. OCI container images available at `docker pull ghcr.io/usace/cwms-data-api:` 1. Note that there may be multiple tags for the same image, use what is appropriate to your current operation. @@ -126,4 +128,3 @@ You can monitor the progress of the action from the new instances that will show As this is just pushing images to the registry it *should* be fast. However, the actual activation by the environment is not deterministic, this action only pushes the images. It does not verify if anything actually changed or worked. You will have to use the monitoring tools appropriate to the given environment to determine if the new version is getting used. - diff --git a/cda-gui.README.md b/cda-gui.README.md index 1b6ec57d65..436ede0df2 100644 --- a/cda-gui.README.md +++ b/cda-gui.README.md @@ -18,16 +18,18 @@ You can run the CDA GUI on your local system without tomcat. #### Quick Start -1. `cd gda-gui` +1. `cd cda-gui` 2. `npm install` (NodeJS Required) 3. `npm run dev` -4. Open https://localhost:5173/cwms-data +4. Open http://localhost:5173/ Changes will now be hot reloaded in the browser as you update your React files. -### Deployed Locally in the WAR +### Deployed WAR -- 🚧 WIP +The GUI and API are packaged as separate applications. Deploy the GUI WAR as +`ROOT.war` to serve the UI at `/`, and deploy the API WAR as `cwms-data.war` +to serve API endpoints at `/cwms-data`. ## Helpful Tips diff --git a/cda-gui/.prettierignore b/cda-gui/.prettierignore new file mode 100644 index 0000000000..567609b123 --- /dev/null +++ b/cda-gui/.prettierignore @@ -0,0 +1 @@ +build/ diff --git a/cda-gui/build.gradle b/cda-gui/build.gradle index 06e5fa0237..47faba1fec 100644 --- a/cda-gui/build.gradle +++ b/cda-gui/build.gradle @@ -2,7 +2,9 @@ import com.github.gradle.node.npm.task.NpmTask import com.github.gradle.node.npm.task.NpxTask plugins { - id 'java' + id 'cda.deps-conventions' + id 'cda.java-conventions' + id 'war' id 'com.github.node-gradle.node' version '7.1.0' } @@ -20,6 +22,13 @@ def cdaGuiViteMode = providers.gradleProperty('cdaGuiMode') .orElse(providers.environmentVariable('CDA_GUI_MODE')) .orElse('production') +dependencies { + compileOnly(libs.javaee.web.api) + testImplementation(libs.javaee.web.api) + testImplementation(libs.bundles.junit) + testImplementation(libs.mockito.core) +} + task buildGuiVite(type:NpxTask) { dependsOn npmInstall command = "vite" @@ -62,16 +71,8 @@ task runGuiDev(type: Exec) { workingDir project.layout.projectDirectory } -sourceSets { - java { - main { - resources { - srcDir(project.layout.projectDirectory.dir('dist')) - } - } - } -} - -tasks.named('processResources') { +war { dependsOn buildGui + archiveBaseName = 'cwms-data-ui' + from(project.layout.projectDirectory.dir('dist')) } diff --git a/cda-gui/public/robots.txt b/cda-gui/public/robots.txt new file mode 100644 index 0000000000..b9dbaa29c8 --- /dev/null +++ b/cda-gui/public/robots.txt @@ -0,0 +1,8 @@ +User-agent: * +Allow: / +Disallow: /auth/ +Disallow: /cwms-data/auth/ +Disallow: /cwms-data/catalog/ +Disallow: /cwms-data/timeseries/ +Disallow: /cwms-data/swagger-docs +Sitemap: https://cwms-data.usace.army.mil/sitemap.xml diff --git a/cda-gui/scripts/generate-sitemap.mjs b/cda-gui/scripts/generate-sitemap.mjs index a1e09a7085..a8bfc122a9 100644 --- a/cda-gui/scripts/generate-sitemap.mjs +++ b/cda-gui/scripts/generate-sitemap.mjs @@ -12,7 +12,7 @@ const outputPath = path.join(projectDir, "dist", "sitemap.xml"); const siteOrigin = ( process.env.SITE_ORIGIN ?? "https://cwms-data.usace.army.mil" ).replace(/\/+$/, ""); -const siteBasePath = (process.env.SITE_BASE_PATH ?? "/cwms-data").replace(/\/+$/, ""); +const siteBasePath = (process.env.SITE_BASE_PATH ?? "").replace(/\/+$/, ""); const urls = sitemapPaths.map((routePath) => { const normalizedPath = routePath ? `/${routePath}` : ""; diff --git a/cda-gui/src/components/GlobalErrorBoundary.jsx b/cda-gui/src/components/GlobalErrorBoundary.jsx index cf8398bbc1..a399bbfc71 100644 --- a/cda-gui/src/components/GlobalErrorBoundary.jsx +++ b/cda-gui/src/components/GlobalErrorBoundary.jsx @@ -23,7 +23,7 @@ export default class GlobalErrorBoundary extends Component {

{this.state.error?.message ?? "An unexpected error occurred."}

- + Return to CDA diff --git a/cda-gui/src/links/header-links.js b/cda-gui/src/links/header-links.js index 9e6e03515b..b19e2026bb 100644 --- a/cda-gui/src/links/header-links.js +++ b/cda-gui/src/links/header-links.js @@ -1,3 +1,5 @@ +import { getBasePath } from "../utils/base"; + export default [ { id: "home", @@ -17,7 +19,7 @@ export default [ { id: "swagger-schema", text: "Swagger Docs Schema", - href: "/swagger-docs", + href: `${getBasePath()}/swagger-docs`, }, ], }, diff --git a/cda-gui/src/main.jsx b/cda-gui/src/main.jsx index e5fad9b0ee..6c70141696 100644 --- a/cda-gui/src/main.jsx +++ b/cda-gui/src/main.jsx @@ -57,7 +57,7 @@ const router = createBrowserRouter( ], }, ], - { basename: "/cwms-data" }, + { basename: "/" }, ); ReactDOM.createRoot(document.getElementById("root")).render( diff --git a/cwms-data-api/src/main/java/cwms/cda/servlet/SpaErrorStatusFilter.java b/cda-gui/src/main/java/cwms/cda/ui/SpaErrorStatusFilter.java similarity index 99% rename from cwms-data-api/src/main/java/cwms/cda/servlet/SpaErrorStatusFilter.java rename to cda-gui/src/main/java/cwms/cda/ui/SpaErrorStatusFilter.java index 86bcb71ec4..9b82a3daff 100644 --- a/cwms-data-api/src/main/java/cwms/cda/servlet/SpaErrorStatusFilter.java +++ b/cda-gui/src/main/java/cwms/cda/ui/SpaErrorStatusFilter.java @@ -1,4 +1,4 @@ -package cwms.cda.servlet; +package cwms.cda.ui; import java.io.IOException; import java.util.Set; diff --git a/cda-gui/src/main/webapp/WEB-INF/web.xml b/cda-gui/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..7b879bda64 --- /dev/null +++ b/cda-gui/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,13 @@ + + CWMS Data API UI + + + index.html + + + + + 404 + /index.html + + diff --git a/cda-gui/src/pages/swagger-ui/index.jsx b/cda-gui/src/pages/swagger-ui/index.jsx index d1a40ed567..df44e147fc 100644 --- a/cda-gui/src/pages/swagger-ui/index.jsx +++ b/cda-gui/src/pages/swagger-ui/index.jsx @@ -39,7 +39,7 @@ export default function SwaggerUI() { const cwmsAuthMethod = useMemo(() => { const basePath = getBasePath(); return createCwmsLoginAuthMethod({ - authUrl: `${window.location.origin}/CWMSLogin`, + authUrl: `${basePath}/CWMSLogin`, authCheckUrl: `${basePath}/auth/keys`, }); }, []); diff --git a/cwms-data-api/src/test/java/cwms/cda/servlet/SpaErrorStatusFilterTest.java b/cda-gui/src/test/java/cwms/cda/ui/SpaErrorStatusFilterTest.java similarity index 87% rename from cwms-data-api/src/test/java/cwms/cda/servlet/SpaErrorStatusFilterTest.java rename to cda-gui/src/test/java/cwms/cda/ui/SpaErrorStatusFilterTest.java index 7e6b27e300..fd17d1bcfb 100644 --- a/cwms-data-api/src/test/java/cwms/cda/servlet/SpaErrorStatusFilterTest.java +++ b/cda-gui/src/test/java/cwms/cda/ui/SpaErrorStatusFilterTest.java @@ -1,4 +1,4 @@ -package cwms.cda.servlet; +package cwms.cda.ui; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.mockito.Mockito.mock; @@ -43,7 +43,7 @@ void registersForIndexErrorDispatches() { "/user-lists" }) void returnsOkForClientRoutes(String route) throws ServletException, IOException { - HttpServletRequest request = buildRequest("GET", "/cwms-data" + route); + HttpServletRequest request = buildRequest("GET", route); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); @@ -55,7 +55,7 @@ void returnsOkForClientRoutes(String route) throws ServletException, IOException @Test void returnsOkForHeadRequest() throws ServletException, IOException { - HttpServletRequest request = buildRequest("HEAD", "/cwms-data/swagger-ui"); + HttpServletRequest request = buildRequest("HEAD", "/swagger-ui"); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); @@ -66,8 +66,8 @@ void returnsOkForHeadRequest() throws ServletException, IOException { } @Test - void returnsOkForAlternateContextPath() throws ServletException, IOException { - HttpServletRequest request = buildRequest("GET", "/spk-data/swagger-ui", "/spk-data"); + void supportsNonRootContextPath() throws ServletException, IOException { + HttpServletRequest request = buildRequest("GET", "/cwms-data-ui/swagger-ui", "/cwms-data-ui"); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); @@ -79,7 +79,7 @@ void returnsOkForAlternateContextPath() throws ServletException, IOException { @Test void preservesNotFoundStatusForUnknownRoutes() throws ServletException, IOException { - HttpServletRequest request = buildRequest("GET", "/cwms-data/not-a-client-route"); + HttpServletRequest request = buildRequest("GET", "/not-a-client-route"); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); @@ -91,7 +91,7 @@ void preservesNotFoundStatusForUnknownRoutes() throws ServletException, IOExcept @Test void preservesNotFoundStatusForNonPageRequests() throws ServletException, IOException { - HttpServletRequest request = buildRequest("POST", "/cwms-data/swagger-ui"); + HttpServletRequest request = buildRequest("POST", "/swagger-ui"); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); @@ -102,7 +102,7 @@ void preservesNotFoundStatusForNonPageRequests() throws ServletException, IOExce } private HttpServletRequest buildRequest(String method, String requestUri) { - return buildRequest(method, requestUri, "/cwms-data"); + return buildRequest(method, requestUri, ""); } private HttpServletRequest buildRequest(String method, String requestUri, String contextPath) { diff --git a/cda-gui/src/utils/base.js b/cda-gui/src/utils/base.js index 2386fa9545..c42e095436 100644 --- a/cda-gui/src/utils/base.js +++ b/cda-gui/src/utils/base.js @@ -1,8 +1,7 @@ // base.js export function getBasePath() { - const { pathname } = window.location; - const basePath = "/" + pathname.split("/")[1]; - return basePath; + const configuredRoot = import.meta.env.VITE_CDA_API_ROOT || "/cwms-data"; + return new URL(configuredRoot, window.location.origin).pathname.replace(/\/$/, ""); } export function getOrigin() { diff --git a/cda-gui/vite.config.js b/cda-gui/vite.config.js index 77b09e58e3..20f8783a4b 100644 --- a/cda-gui/vite.config.js +++ b/cda-gui/vite.config.js @@ -9,9 +9,8 @@ export default defineConfig(({ mode }) => { env.VITE_CDA_API_ROOT || env.CDA_API_ROOT || "http://localhost:8081", "http://localhost:8081", ).origin; - // const BASE_PATH = env?.BASE_PATH ?? "/cwms-data"; return { - base: "/cwms-data", + base: "/", plugins: [react()], server: { proxy: { diff --git a/cwms-data-api/build.gradle b/cwms-data-api/build.gradle index 9dbd17e740..6629c6606d 100644 --- a/cwms-data-api/build.gradle +++ b/cwms-data-api/build.gradle @@ -7,7 +7,6 @@ plugins { configurations { info - webjars docker baseLibs tomcatLibs @@ -183,9 +182,6 @@ dependencies { testImplementation(libs.cwms.tomcat.auth) testImplementation(libs.apache.freemarker) - webjars(project(":cda-gui")) - - // override versions implementation(libs.bundles.overrides) @@ -194,17 +190,7 @@ dependencies { implementation(libs.minio) } -task extractWebJars(type: Copy) { - dependsOn configurations.webjars - from zipTree(configurations.webjars.singleFile) - into file("$buildDir/extra") - includeEmptyDirs false - includeEmptyDirs = false -} - war { - dependsOn extractWebJars - from "$buildDir/extra" from "src/resources/" manifest { @@ -386,6 +372,11 @@ task prepareDockerBuild(type: Copy, dependsOn: war) { into "cda/webapps" rename(".*\\.war", "cwms-data.war") } + + from(project(":cda-gui").tasks.named("war")) { + into "cda/webapps" + rename(".*\\.war", "ROOT.war") + } } task generateTimeSeriesSamples(type: JavaExec) { diff --git a/cwms-data-api/src/main/webapp/WEB-INF/web.xml b/cwms-data-api/src/main/webapp/WEB-INF/web.xml index d037b4ab74..dba230c48f 100644 --- a/cwms-data-api/src/main/webapp/WEB-INF/web.xml +++ b/cwms-data-api/src/main/webapp/WEB-INF/web.xml @@ -38,10 +38,4 @@ CorsFilter /* - - - - 404 - /index.html - - + diff --git a/docker-compose.README.md b/docker-compose.README.md index b52be8816e..1e0516a8d2 100644 --- a/docker-compose.README.md +++ b/docker-compose.README.md @@ -38,7 +38,7 @@ The following users and permissions are available: |----------------------------------------------|-----------|----------------|-----------------------------------------|-----------------------| | [traefik]() | 8081 | 8081 | entry point - web traffic | http://localhost:8081 | | db | | 1521 | oracle database | | -| [api](./cwms-data-api/src/docker/Dockerfile) | | 7000 | tomcat CWMS Data API | | +| [api](./Dockerfile) | | 7000 | Tomcat hosting the UI at `/` and API at `/cwms-data` as separate WARs | http://localhost:8081/ | | [auth](./compose_files/keycloak/Dockerfile) | | 8080 | authentication-token service (keycloak) | | | db_install | | | connects to db and installs CWMS schema | | | db_webuser_ permissions | | | connects to db and sets permissions | | diff --git a/docker-compose.yml b/docker-compose.yml index dd79581ef0..1ada1085e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,7 +114,11 @@ services: labels: - "traefik.enable=true" - "traefik.http.routers.data-api.rule=PathPrefix(`/cwms-data`)" + - "traefik.http.routers.data-api.service=data-api" - "traefik.http.routers.data-api.entryPoints=web" + - "traefik.http.routers.data-ui.rule=PathPrefix(`/`)" + - "traefik.http.routers.data-ui.service=data-api" + - "traefik.http.routers.data-ui.entryPoints=web" - "traefik.http.services.data-api.loadbalancer.server.port=7000" auth: