From 0a99668ae03ef12852dcc8158dc8eb18b6c407b2 Mon Sep 17 00:00:00 2001 From: James Talton Date: Tue, 15 Aug 2023 11:20:40 -0400 Subject: [PATCH] Build standalone UIs (#766) Signed-off-by: James Talton --- .dockerignore | 11 +- .github/workflows/pull-request.yml | 341 ++++++++++-------- .gitignore | 3 + Dockerfile | 46 +-- README.md | 117 ++++-- cypress.afw.config.ts | 7 + cypress.awx.config.ts | 9 + cypress.config.ts => cypress.base.config.ts | 7 +- cypress.eda.config.ts | 9 + cypress.env.json | 12 - cypress.hub.config.ts | 9 + cypress/CYPRESS.md | 79 +--- cypress/e2e/awx/dashboard/welcomeModal.cy.tsx | 37 +- cypress/e2e/eda/General-UI/dashboard.cy.ts | 12 +- cypress/e2e/eda/General-UI/login-logout.cy.ts | 46 +-- cypress/support/auth.ts | 81 +---- cypress/support/commands.ts | 8 +- framework/README.md | 2 +- nginx/awx.conf | 52 +++ nginx/eda.conf | 52 +++ nginx/hub.conf | 41 +++ package-lock.json | 64 ++++ package.json | 74 ++-- proxy/constants.ts | 43 --- proxy/logger.ts | 10 - proxy/main.ts | 35 -- proxy/proxy-handler.ts | 150 -------- proxy/proxy.ts | 18 - proxy/readiness.ts | 6 - proxy/request-handler.ts | 34 -- proxy/rollup.config.ts | 28 -- proxy/serve.ts | 162 --------- proxy/server.ts | 281 --------------- proxy/tsconfig.json | 8 - tsconfig.json | 4 +- webpack.awx.cjs => webpack/webpack.awx.cjs | 9 +- .../webpack.config.cjs | 60 +-- webpack.eda.cjs => webpack/webpack.eda.cjs | 9 +- webpack.hub.cjs => webpack/webpack.hub.cjs | 9 +- 39 files changed, 673 insertions(+), 1312 deletions(-) create mode 100644 cypress.afw.config.ts create mode 100644 cypress.awx.config.ts rename cypress.config.ts => cypress.base.config.ts (95%) create mode 100644 cypress.eda.config.ts delete mode 100644 cypress.env.json create mode 100644 cypress.hub.config.ts create mode 100644 nginx/awx.conf create mode 100644 nginx/eda.conf create mode 100644 nginx/hub.conf delete mode 100644 proxy/constants.ts delete mode 100644 proxy/logger.ts delete mode 100644 proxy/main.ts delete mode 100644 proxy/proxy-handler.ts delete mode 100644 proxy/proxy.ts delete mode 100644 proxy/readiness.ts delete mode 100644 proxy/request-handler.ts delete mode 100644 proxy/rollup.config.ts delete mode 100644 proxy/serve.ts delete mode 100644 proxy/server.ts delete mode 100644 proxy/tsconfig.json rename webpack.awx.cjs => webpack/webpack.awx.cjs (74%) rename webpack.config.cjs => webpack/webpack.config.cjs (85%) rename webpack.eda.cjs => webpack/webpack.eda.cjs (64%) rename webpack.hub.cjs => webpack/webpack.hub.cjs (64%) diff --git a/.dockerignore b/.dockerignore index 46694cbf39..be4e444657 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,15 +3,18 @@ /.github /.husky /.nyc_output -/.storybook -/.vscode /.vscode /Dockerfile -/certs /coverage /cypress /docs +/framework +/frontend +/locales /node_modules +/playbooks /publish +/rulebooks /scripts -/stories \ No newline at end of file +/stories +/webpack \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5b57bc83f9..cd8d22383f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -54,55 +54,6 @@ jobs: with: paths: framework - container-image: - name: Container Image - runs-on: ubuntu-latest - needs: packages - timeout-minutes: 10 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - - name: PreCache - run: npm version 0.0.0 --no-git-tag-version - - - name: Cache dependencies - id: cache - uses: actions/cache@v3 - with: - path: | - ./node_modules - /home/runner/.cache/Cypress - key: modules-${{ hashFiles('package-lock.json') }} - - - name: Install dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: npm ci - - - name: NPM Build - run: npm run build - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Build Ansible UI - uses: docker/build-push-action@v4 - with: - context: . - target: ansible-ui - tags: ansible-ui - load: true - - - run: docker save ansible-ui > ansible-ui.tar - - uses: actions/upload-artifact@v3 - with: - name: ansible-ui.tar - path: ansible-ui.tar - checks: name: ESLint - Prettier - TSC runs-on: ubuntu-latest @@ -138,14 +89,11 @@ jobs: strategy: fail-fast: false matrix: - containers: [1] + containers: [1, 2] steps: - name: Check for changes id: check run: echo "skip=${{ needs.packages.outputs.awx != 'true' }}" >> "$GITHUB_OUTPUT" - - name: Skip job - if: steps.check.outputs.skip == 'true' - run: echo "No build required" - name: Checkout if: steps.check.outputs.skip != 'true' uses: actions/checkout@v3 @@ -179,7 +127,7 @@ jobs: install: false record: true parallel: true - spec: frontend/awx/**/*.cy.{js,jsx,ts,tsx} + config-file: cypress.awx.config.ts env: CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_AUI_AWX_CCT_PROJECT_ID }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_AUI_AWX_CCT_RECORD_KEY }} @@ -198,9 +146,6 @@ jobs: - name: Check for changes id: check run: echo "skip=${{ needs.packages.outputs.eda != 'true' }}" >> "$GITHUB_OUTPUT" - - name: Skip job - if: steps.check.outputs.skip == 'true' - run: echo "No build required" - name: Checkout if: steps.check.outputs.skip != 'true' uses: actions/checkout@v3 @@ -234,7 +179,7 @@ jobs: install: false record: true parallel: true - spec: frontend/eda/**/*.cy.{js,jsx,ts,tsx} + config-file: cypress.eda.config.ts env: CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_AUI_EDA_CCT_PROJECT_ID }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_AUI_EDA_CCT_RECORD_KEY }} @@ -253,9 +198,6 @@ jobs: - name: Check for changes id: check run: echo "skip=${{ needs.packages.outputs.hub != 'true' }}" >> "$GITHUB_OUTPUT" - - name: Skip job - if: steps.check.outputs.skip == 'true' - run: echo "No build required" - name: Checkout if: steps.check.outputs.skip != 'true' uses: actions/checkout@v3 @@ -289,7 +231,7 @@ jobs: install: false record: true parallel: true - spec: frontend/hub/**/*.cy.{js,jsx,ts,tsx} + config-file: cypress.hub.config.ts env: CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_AUI_HUB_CCT_PROJECT_ID }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_AUI_HUB_CCT_RECORD_KEY }} @@ -308,9 +250,6 @@ jobs: - name: Check for changes id: check run: echo "skip=${{ needs.packages.outputs.afw != 'true' }}" >> "$GITHUB_OUTPUT" - - name: Skip job - if: steps.check.outputs.skip == 'true' - run: echo "No build required" - name: Checkout if: steps.check.outputs.skip != 'true' uses: actions/checkout@v3 @@ -344,19 +283,63 @@ jobs: install: false record: true parallel: true - spec: framework/**/*.cy.{js,jsx,ts,tsx} + config-file: cypress.afw.config.ts env: CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_AUI_AFW_CCT_PROJECT_ID }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_AUI_AFW_CCT_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + awx-ui-image: + name: AWX UI Image + runs-on: ubuntu-latest + needs: packages + timeout-minutes: 10 + steps: + - name: Check for changes + id: check + run: echo "skip=${{ needs.packages.outputs.awx != 'true' }}" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v3 + if: steps.check.outputs.skip != 'true' + with: + fetch-depth: 1 + - name: PreCache + if: steps.check.outputs.skip != 'true' + run: npm version 0.0.0 --no-git-tag-version + - name: Cache dependencies + if: steps.check.outputs.skip != 'true' && steps.cache.outputs.cache-hit != 'true' + id: cache + uses: actions/cache@v3 + with: + path: | + ./node_modules + /home/runner/.cache/Cypress + key: modules-${{ hashFiles('package-lock.json') }} + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: npm ci + - name: NPM Build + if: steps.check.outputs.skip != 'true' + run: npm run build:awx + - name: Build Image + if: steps.check.outputs.skip != 'true' + run: npm run docker:build:awx + - name: Save Image + if: steps.check.outputs.skip != 'true' + run: docker save awx-ui > awx-ui.tar + - name: Upload Image + if: steps.check.outputs.skip != 'true' + uses: actions/upload-artifact@v3 + with: + name: awx-ui.tar + path: awx-ui.tar + awx-e2e: name: AWX E2E runs-on: ubuntu-latest timeout-minutes: 10 needs: - packages - - container-image + - awx-ui-image strategy: fail-fast: false matrix: @@ -365,17 +348,14 @@ jobs: - name: Check for changes id: check run: echo "skip=${{ needs.packages.outputs.awx != 'true' }}" >> "$GITHUB_OUTPUT" - - name: Skip job - if: steps.check.outputs.skip == 'true' - run: echo "No build required" - name: Download container image if: steps.check.outputs.skip != 'true' uses: actions/download-artifact@v3 with: - name: ansible-ui.tar + name: awx-ui.tar - name: Load container image if: steps.check.outputs.skip != 'true' - run: docker load --input ansible-ui.tar + run: docker load --input awx-ui.tar - name: Checkout if: steps.check.outputs.skip != 'true' uses: actions/checkout@v3 @@ -406,12 +386,14 @@ jobs: uses: cypress-io/github-action@v5 with: install: false - start: npm run docker:run - wait-on: 'https://localhost:3002' + start: npm run docker:run:awx + wait-on: 'http://localhost:4101' record: true parallel: true - config: specPattern=cypress/e2e/awx/**/*.cy.{js,jsx,ts,tsx} + config-file: cypress.awx.config.ts env: + AWX_HOST: ${{ secrets.AWX_HOST }} + AWX_PROTOCOL: ${{ secrets.AWX_PROTOCOL }} CYPRESS_AWX_SERVER: ${{ secrets.CYPRESS_AWX_SERVER }} CYPRESS_AWX_USERNAME: ${{ secrets.CYPRESS_AWX_USERNAME }} CYPRESS_AWX_PASSWORD: ${{ secrets.CYPRESS_AWX_PASSWORD }} @@ -420,13 +402,57 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_TLS_REJECT_UNAUTHORIZED: 0 - eda-e2e: - name: EDA E2E + hub-ui-image: + name: HUB UI Image + runs-on: ubuntu-latest + needs: packages + timeout-minutes: 10 + steps: + - name: Check for changes + id: check + run: echo "skip=${{ needs.packages.outputs.hub != 'true' }}" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v3 + if: steps.check.outputs.skip != 'true' + with: + fetch-depth: 1 + - name: PreCache + if: steps.check.outputs.skip != 'true' + run: npm version 0.0.0 --no-git-tag-version + - name: Cache dependencies + if: steps.check.outputs.skip != 'true' && steps.cache.outputs.cache-hit != 'true' + id: cache + uses: actions/cache@v3 + with: + path: | + ./node_modules + /home/runner/.cache/Cypress + key: modules-${{ hashFiles('package-lock.json') }} + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: npm ci + - name: NPM Build + if: steps.check.outputs.skip != 'true' + run: npm run build:hub + - name: Build Image + if: steps.check.outputs.skip != 'true' + run: npm run docker:build:hub + - name: Save Image + if: steps.check.outputs.skip != 'true' + run: docker save hub-ui > hub-ui.tar + - name: Upload Image + if: steps.check.outputs.skip != 'true' + uses: actions/upload-artifact@v3 + with: + name: hub-ui.tar + path: hub-ui.tar + + hub-e2e: + name: HUB E2E runs-on: ubuntu-latest timeout-minutes: 10 needs: - packages - - container-image + - hub-ui-image strategy: fail-fast: false matrix: @@ -434,7 +460,7 @@ jobs: steps: - name: Check for changes id: check - run: echo "skip=${{ needs.packages.outputs.eda != 'true' }}" >> "$GITHUB_OUTPUT" + run: echo "skip=${{ needs.packages.outputs.hub != 'true' }}" >> "$GITHUB_OUTPUT" - name: Skip job if: steps.check.outputs.skip == 'true' run: echo "No build required" @@ -447,10 +473,10 @@ jobs: if: steps.check.outputs.skip != 'true' uses: actions/download-artifact@v3 with: - name: ansible-ui.tar + name: hub-ui.tar - name: Load container image if: steps.check.outputs.skip != 'true' - run: docker load --input ansible-ui.tar + run: docker load --input hub-ui.tar - name: Setup Node if: steps.check.outputs.skip != 'true' uses: actions/setup-node@v3 @@ -471,36 +497,78 @@ jobs: - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' && steps.check.outputs.skip != 'true' run: npm ci - - name: Cypress + # - name: Cypress + # if: steps.check.outputs.skip != 'true' + # uses: cypress-io/github-action@v5 + # with: + # install: false + # start: npm run docker:run:hub + # wait-on: 'https://localhost:4102' + # record: true + # parallel: true + # config-file: cypress.hub.config.ts + # env: + # HUB_HOST: ${{ secrets.HUB_HOST }} + # HUB_PROTOCOL: ${{ secrets.HUB_PROTOCOL }} + # CYPRESS_HUB_SERVER: http://localhost:5001 + # CYPRESS_HUB_USERNAME: admin + # CYPRESS_HUB_PASSWORD: password + # CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_AUI_HUB_E2E_PROJECT_ID }} + # CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_AUI_HUB_E2E_RECORD_KEY }} + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # NODE_TLS_REJECT_UNAUTHORIZED: 0 + + eda-ui-image: + name: EDA UI Image + runs-on: ubuntu-latest + needs: packages + timeout-minutes: 10 + steps: + - name: Check for changes + id: check + run: echo "skip=${{ needs.packages.outputs.eda != 'true' }}" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v3 if: steps.check.outputs.skip != 'true' - uses: cypress-io/github-action@v5 with: - install: false - start: docker run -d -e LOG_LEVEL=none -p 3002:3002 -e PORT=3002 ansible-ui - wait-on: 'https://localhost:3002' - record: true - parallel: true - config: specPattern=cypress/e2e/eda/**/*.cy.{js,jsx,ts,tsx} - env: - CYPRESS_AWX_SERVER: ${{ secrets.CYPRESS_AWX_SERVER }} - CYPRESS_AWX_USERNAME: ${{ secrets.CYPRESS_AWX_USERNAME }} - CYPRESS_AWX_PASSWORD: ${{ secrets.CYPRESS_AWX_PASSWORD }} - CYPRESS_EDA_SERVER: ${{ secrets.CYPRESS_EDA_SERVER }} - CYPRESS_EDA_USERNAME: ${{ secrets.CYPRESS_EDA_USERNAME }} - CYPRESS_EDA_PASSWORD: ${{ secrets.CYPRESS_EDA_PASSWORD }} - CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_AUI_EDA_E2E_PROJECT_ID }} - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_AUI_EDA_E2E_RECORD_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NODE_TLS_REJECT_UNAUTHORIZED: 0 + fetch-depth: 1 + - name: PreCache + if: steps.check.outputs.skip != 'true' + run: npm version 0.0.0 --no-git-tag-version + - name: Cache dependencies + if: steps.check.outputs.skip != 'true' && steps.cache.outputs.cache-hit != 'true' + id: cache + uses: actions/cache@v3 + with: + path: | + ./node_modules + /home/runner/.cache/Cypress + key: modules-${{ hashFiles('package-lock.json') }} + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: npm ci + - name: NPM Build + if: steps.check.outputs.skip != 'true' + run: npm run build:eda + - name: Build Image + if: steps.check.outputs.skip != 'true' + run: npm run docker:build:eda + - name: Save Image + if: steps.check.outputs.skip != 'true' + run: docker save eda-ui > eda-ui.tar + - name: Upload Image + if: steps.check.outputs.skip != 'true' + uses: actions/upload-artifact@v3 + with: + name: eda-ui.tar + path: eda-ui.tar - # SEE: https://github.com/ansible/ansible-hub-ui/blob/master/.github/workflows/cypress.yml - hub-e2e: - name: HUB E2E + eda-e2e: + name: EDA E2E runs-on: ubuntu-latest timeout-minutes: 10 needs: - packages - - container-image + - eda-ui-image strategy: fail-fast: false matrix: @@ -508,7 +576,7 @@ jobs: steps: - name: Check for changes id: check - run: echo "skip=${{ needs.packages.outputs.hub != 'true' }}" >> "$GITHUB_OUTPUT" + run: echo "skip=${{ needs.packages.outputs.eda != 'true' }}" >> "$GITHUB_OUTPUT" - name: Skip job if: steps.check.outputs.skip == 'true' run: echo "No build required" @@ -521,10 +589,10 @@ jobs: if: steps.check.outputs.skip != 'true' uses: actions/download-artifact@v3 with: - name: ansible-ui.tar + name: eda-ui.tar - name: Load container image if: steps.check.outputs.skip != 'true' - run: docker load --input ansible-ui.tar + run: docker load --input eda-ui.tar - name: Setup Node if: steps.check.outputs.skip != 'true' uses: actions/setup-node@v3 @@ -545,59 +613,26 @@ jobs: - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' && steps.check.outputs.skip != 'true' run: npm ci - - name: 'Checkout galaxy_ng' - if: steps.check.outputs.skip != 'true' - uses: actions/checkout@v3 - with: - repository: 'ansible/galaxy_ng' - path: 'galaxy_ng' - - name: 'Checkout oci_env' - if: steps.check.outputs.skip != 'true' - uses: actions/checkout@v3 - with: - repository: 'pulp/oci_env' - path: 'oci_env' - - name: 'Configure oci_env' - if: steps.check.outputs.skip != 'true' - working-directory: 'oci_env' - run: pip install -e client - - name: Create compose.env - if: steps.check.outputs.skip != 'true' - working-directory: 'oci_env' - run: | - echo 'COMPOSE_PROFILE=galaxy_ng/base' >> compose.env - echo 'DEV_SOURCE_PATH=galaxy_ng' >> compose.env - echo 'COMPOSE_BINARY=docker' >> compose.env - echo 'API_HOST=localhost' >> compose.env - echo 'API_PORT=5001' >> compose.env - echo 'API_PROTOCOL=http' >> compose.env - - name: 'oci-env compose build' - if: steps.check.outputs.skip != 'true' - working-directory: 'oci_env' - run: oci-env compose build - - name: 'oci-env compose up' - if: steps.check.outputs.skip != 'true' - working-directory: 'oci_env' - run: oci-env compose up -d - name: Cypress if: steps.check.outputs.skip != 'true' uses: cypress-io/github-action@v5 with: install: false - start: docker run -d -e LOG_LEVEL=none -p 3002:3002 -e PORT=3002 ansible-ui - wait-on: 'https://localhost:3002' + start: npm run docker:run:eda + wait-on: 'http://localhost:4103' record: true parallel: true - config: specPattern=cypress/e2e/hub/**/*.cy.{js,jsx,ts,tsx} + config-file: cypress.eda.config.ts env: - CYPRESS_HUB_SERVER: http://localhost:5001 - CYPRESS_HUB_USERNAME: admin - CYPRESS_HUB_PASSWORD: password - CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_AUI_HUB_E2E_PROJECT_ID }} - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_AUI_HUB_E2E_RECORD_KEY }} + EDA_HOST: ${{ secrets.EDA_HOST }} + EDA_PROTOCOL: ${{ secrets.EDA_PROTOCOL }} + CYPRESS_AWX_SERVER: ${{ secrets.CYPRESS_AWX_SERVER }} + CYPRESS_AWX_USERNAME: ${{ secrets.CYPRESS_AWX_USERNAME }} + CYPRESS_AWX_PASSWORD: ${{ secrets.CYPRESS_AWX_PASSWORD }} + CYPRESS_EDA_SERVER: ${{ secrets.CYPRESS_EDA_SERVER }} + CYPRESS_EDA_USERNAME: ${{ secrets.CYPRESS_EDA_USERNAME }} + CYPRESS_EDA_PASSWORD: ${{ secrets.CYPRESS_EDA_PASSWORD }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_AUI_EDA_E2E_PROJECT_ID }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_AUI_EDA_E2E_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_TLS_REJECT_UNAUTHORIZED: 0 - - name: 'oci-env compose down' - if: steps.check.outputs.skip != 'true' - working-directory: 'oci_env' - run: oci-env compose down --volumes diff --git a/.gitignore b/.gitignore index 537e787183..93a9b505ff 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,11 @@ /build /certs /coverage +/dist /node_modules /publish .DS_Store /storybook-static .vscode +/cypress/.nyc_output +/cypress/coverage diff --git a/Dockerfile b/Dockerfile index 36420c0173..34f250ce13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,14 @@ +# awx-ui +FROM --platform=${TARGETPLATFORM:-linux/amd64} nginx:alpine as awx-ui +COPY /nginx/awx.conf /etc/nginx/templates/default.conf.template +COPY /build/awx /usr/share/nginx/html + +# hub-ui +FROM --platform=${TARGETPLATFORM:-linux/amd64} nginx:alpine as hub-ui +COPY /nginx/hub.conf /etc/nginx/templates/default.conf.template +COPY /build/hub /usr/share/nginx/html + # eda-ui FROM --platform=${TARGETPLATFORM:-linux/amd64} nginx:alpine as eda-ui -ARG NGINX_CONF=./nginx.conf -ARG NGINX_CONFIGURATION_PATH=/etc/nginx/nginx.conf -ENV DIST_UI="/opt/app-root/ui/eda" -COPY ${NGINX_CONF} ${NGINX_CONFIGURATION_PATH} -RUN mkdir -p ${DIST_UI}/ -COPY /build/eda/ ${DIST_UI} -ARG USER_ID=${USER_ID:-1001} -RUN adduser -S eda -u "$USER_ID" -G root -USER 0 -RUN for dir in \ - ${DIST_UI}/ \ - ${NGINX_CONF} \ - ${NGINX_CONFIGURATION_PATH} \ - /var/cache/nginx \ - /var/log/nginx \ - /var/lib/nginx ; \ - do mkdir -m 0775 -p $dir ; chmod g+rwx $dir ; chgrp root $dir ; done && \ - for file in \ - /var/run/nginx.pid ; \ - do touch $file ; chmod g+rw $file ; done -USER "$USER_ID" - -# ansible-ui -FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine as ansible-ui -ARG VERSION -COPY --from=node:18-alpine /usr/local/bin/node /usr/local/bin/node -RUN apk upgrade --no-cache -U && apk add --no-cache libstdc++ -RUN addgroup -g 1000 -S node && adduser -u 1000 -S node -G node -USER node -WORKDIR /home/node -ENV NODE_ENV production -ENV VERSION $VERSION -COPY --chown=node /build/ ./ -CMD ["node", "proxy.mjs"] \ No newline at end of file +COPY /nginx/eda.conf /etc/nginx/templates/default.conf.template +COPY /build/eda /usr/share/nginx/html diff --git a/README.md b/README.md index 3446b403a2..50497a5291 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Ansible UI -The UI projects for [Ansible](https://www.ansible.com). +UI projects for [Ansible](https://www.ansible.com). + +- [Getting Started](#getting-started) +- [Environment Variables](#environment-variables) +- [NPM Scripts](#npm-scripts) +- [Documentation](#documentation) +- [Code of Conduct](#code-of-conduct) ## Getting Started @@ -23,42 +29,79 @@ The UI projects for [Ansible](https://www.ansible.com). npm ci ``` -4. Start the Projects - - - AWX - Ansible Controller - - | Environment | Description | - | ----------: | ----------------------------- | - | AWX_SERVER | The AWX server to connect to. | - - ``` - npm run awx - ``` - - - HUB - Automation Hub - - | Environment | Description | - | ----------: | ----------------------------- | - | HUB_SERVER | The HUB server to connect to. | - - ``` - npm run hub - ``` - - - EDA - Event Driven Automation - - | Environment | Description | - | ----------: | ----------------------------- | - | EDA_SERVER | The EDA server to connect to. | - - ``` - npm run eda - ``` - -5. View the development documentation - - - [Development](./docs/DEVELOPMENT.md) - - [Framework](./framework/README.md) - A framework for building applications using [PatternFly](https://www.patternfly.org). +## Environment Variables + +| Environment Variable | Description | +| ---------------------: | ------------------------------------------ | +| `AWX_PROTOCOL` | The AWX server protocol (http) or (https). | +| `AWX_HOST` | The AWX server address with port. | +| `CYPRESS_AWX_SERVER` | The AWX server URL. | +| `CYPRESS_AWX_USERNAME` | The AWX server username. | +| `CYPRESS_AWX_PASSWORD` | The AWX server password. | +| | | +| `HUB_PROTOCOL` | The HUB server protocol (http) or (https). | +| `HUB_HOST` | The HUB server address with port. | +| `CYPRESS_HUB_SERVER` | The HUB server URL. | +| `CYPRESS_HUB_USERNAME` | The HUB server username. | +| `CYPRESS_HUB_PASSWORD` | The HUB server password. | +| | | +| `EDA_PROTOCOL` | The EDA server protocol (http) or (https). | +| `EDA_HOST` | The EDA server address with port. | +| `CYPRESS_EDA_SERVER` | The EDA server URL. | +| `CYPRESS_EDA_USERNAME` | The EDA server username. | +| `CYPRESS_EDA_PASSWORD` | The EDA server password. | + +``` +AWX_PROTOCOL=http +AWX_HOST=localhost:8043 +CYPRESS_AWX_SERVER=$AWX_PROTOCOL://$AWX_HOST +CYPRESS_AWX_USERNAME='my-user' +CYPRESS_AWX_PASSWORD='my-password' + +HUB_PROTOCOL=http +HUB_HOST=localhost:8000 +CYPRESS_HUB_SERVER=$HUB_PROTOCOL://$HUB_HOST +CYPRESS_HUB_USERNAME='my-user' +CYPRESS_HUB_PASSWORD='my-password' + +EDA_PROTOCOL=http +EDA_HOST=localhost:5001 +CYPRESS_EDA_SERVER=$EDA_PROTOCOL://$EDA_HOST +CYPRESS_EDA_USERNAME='my-user' +CYPRESS_EDA_PASSWORD='my-password' +``` + +## NPM Scripts + +| NPM Script | Description | +| --------------------------- | --------------------------------------- | +| `npm run awx` | Run AWX on | +| `npm run e2e:awx` | Run AWX E2E tests from Cypress UI | +| `npm run e2e:run:awx` | Run AWX E2E tests from CLI | +| `npm run component:awx` | Run AWX component tests from Cypress UI | +| `npm run component:run:awx` | Run AWX component tests from CLI | +| | | +| `npm run hub` | Run HUB on | +| `npm run e2e:hub` | Run HUB E2E tests from Cypress UI | +| `npm run e2e:run:hub` | Run HUB E2E tests from CLI | +| `npm run component:hub` | Run HUB component tests from Cypress UI | +| `npm run component:run:hub` | Run HUB component tests from CLI | +| | | +| `npm run eda` | Run EDA on | +| `npm run e2e:eda` | Run EDA E2E tests from Cypress UI | +| `npm run e2e:run:eda` | Run EDA E2E tests from CLI | +| `npm run component:eda` | Run EDA component tests from Cypress UI | +| `npm run component:run:eda` | Run EDA component tests from CLI | +| | | +| `npm run tsc` | Run Typescript compiler checks | +| `npm run eslint` | Run eslint checks | +| `npm run prettier` | Run prettier format checks | +| `npm run prettier:fix` | Fix prettier format of files | + +## Documentation + +- [Development](./docs/DEVELOPMENT.md) +- [Framework](./framework/README.md) - A framework for building applications using [PatternFly](https://www.patternfly.org). ## Code of Conduct diff --git a/cypress.afw.config.ts b/cypress.afw.config.ts new file mode 100644 index 0000000000..fec9ad3e72 --- /dev/null +++ b/cypress.afw.config.ts @@ -0,0 +1,7 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { defineConfig } from 'cypress'; +import { baseConfig } from './cypress.base.config'; + +baseConfig.component!.specPattern = 'framework/**/*.cy.{js,jsx,ts,tsx}'; + +module.exports = defineConfig(baseConfig); diff --git a/cypress.awx.config.ts b/cypress.awx.config.ts new file mode 100644 index 0000000000..31633a388c --- /dev/null +++ b/cypress.awx.config.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { defineConfig } from 'cypress'; +import { baseConfig } from './cypress.base.config'; + +baseConfig.e2e!.specPattern = 'cypress/e2e/awx/**/*.cy.ts'; +baseConfig.e2e!.baseUrl = 'http://localhost:4101'; +baseConfig.component!.specPattern = 'frontend/awx/**/*.cy.{js,jsx,ts,tsx}'; + +module.exports = defineConfig(baseConfig); diff --git a/cypress.config.ts b/cypress.base.config.ts similarity index 95% rename from cypress.config.ts rename to cypress.base.config.ts index 7ec92ac3ff..4bd13447df 100644 --- a/cypress.config.ts +++ b/cypress.base.config.ts @@ -1,10 +1,8 @@ import codeCoverage from '@cypress/code-coverage/task'; -import { defineConfig } from 'cypress'; import pkg from 'webpack'; - const { DefinePlugin } = pkg; -export default defineConfig({ +export const baseConfig: Cypress.ConfigOptions = { viewportWidth: 1600, viewportHeight: 1120, pageLoadTimeout: 120000, @@ -19,7 +17,6 @@ export default defineConfig({ codeCoverage(on, config); return config; }, - baseUrl: 'https://localhost:3002/', retries: { runMode: 2, openMode: 0 }, }, component: { @@ -77,4 +74,4 @@ export default defineConfig({ specPattern: ['frontend/**/*.cy.tsx', 'framework/**/*.cy.tsx'], supportFile: 'cypress/support/component.tsx', }, -}); +}; diff --git a/cypress.eda.config.ts b/cypress.eda.config.ts new file mode 100644 index 0000000000..55ee3c7b9b --- /dev/null +++ b/cypress.eda.config.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { defineConfig } from 'cypress'; +import { baseConfig } from './cypress.base.config'; + +baseConfig.e2e!.specPattern = 'cypress/e2e/eda/**/*.cy.ts'; +baseConfig.e2e!.baseUrl = 'http://localhost:4103'; +baseConfig.component!.specPattern = 'frontend/eda/**/*.cy.{js,jsx,ts,tsx}'; + +module.exports = defineConfig(baseConfig); diff --git a/cypress.env.json b/cypress.env.json deleted file mode 100644 index 7880e1d30e..0000000000 --- a/cypress.env.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "AWX_SERVER": "http://localhost:8043/", - "AWX_USERNAME": "admin", - "AWX_PASSWORD": "admin", - "EDA_SERVER": "http://ec2-3-18-42-173.us-east-2.compute.amazonaws.com:30001/", - "EDA_USERNAME": "admin", - "EDA_PASSWORD": "fishinabarrel", - "TEST_STANDALONE": "false", - "HUB_SERVER": "http://localhost:5001/", - "HUB_USERNAME": "admin", - "HUB_PASSWORD": "admin" -} diff --git a/cypress.hub.config.ts b/cypress.hub.config.ts new file mode 100644 index 0000000000..68aa570203 --- /dev/null +++ b/cypress.hub.config.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { defineConfig } from 'cypress'; +import { baseConfig } from './cypress.base.config'; + +baseConfig.e2e!.specPattern = 'cypress/e2e/hub/**/*.cy.ts'; +baseConfig.e2e!.baseUrl = 'http://localhost:4102'; +baseConfig.component!.specPattern = 'frontend/hub/**/*.cy.{js,jsx,ts,tsx}'; + +module.exports = defineConfig(baseConfig); diff --git a/cypress/CYPRESS.md b/cypress/CYPRESS.md index 43517fceaf..ef21fcb3a0 100644 --- a/cypress/CYPRESS.md +++ b/cypress/CYPRESS.md @@ -2,84 +2,7 @@ Cypress is being used for both end-to-end tests and component tests. -## NPM Test Scripts - -| Command | Description | -| ------------------------------- | ---------------------------------------------- | -| `npm run cypress:run` | Run E2E and component tests headless. | -| `npm run cypress:run:e2e` | Run E2E tests headless. | -| `npm run cypress:run:component` | Run component tests headless. | -| `npm run cypress:open` | Open the Cypress UI to run tests. | -| `npm run cypress:coverage` | After tests have finished, view test coverage. | - -## E2E Testing - -The Cypress E2E tests run against a live backend API. - -```mermaid -graph LR; - cypress --> frontend - frontend --> proxy - proxy --> api -``` - -### E2E Getting started - -1. Setup Environment Variables -
The E2E tests need a live API to test against. The following environment variables can be used to setup the E2E test server. - - ##### AWX - - | Environment Variable | Description | - | ---------------------- | ----------------------------------------------------------------------------------- | - | `CYPRESS_AWX_SERVER` | URL of the AWX server to run E2E tests against. `Default: ` | - | `CYPRESS_AWX_USERNAME` | username for logging into the AWX server. `Default: admin` | - | `CYPRESS_AWX_PASSWORD` | password for logging into the AWX server. `Default: admin` | - - > NOTE: Running AWX API locally defaults to which easily allows running E2E test against it. - - ##### EDA - - | Environment Variable | Description | - | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | - | `CYPRESS_EDA_SERVER` | URL of the EDA server to run E2E tests against. `Default: ` | - | `CYPRESS_EDA_USERNAME` | username for logging into the EDA server. `Default: testuser` | - | `CYPRESS_EDA_PASSWORD` | password for logging into the EDA server. `Default: testpass` | - | `CYPRESS_TEST_STANDALONE` | flag to indicate if UI should be tested standalone. (Login via route `/login` instead of `/automation-servers`) `Default: false` | - -2. Run the Ansible-UI frontend and proxy - - ``` - npm start - ``` - -3. Run Cypress - - Run Cypress E2E tests headless - - ``` - npm run cypress:run:e2e - ``` - - Open the Cypress UI to run e2e tests - - ``` - npm run cypress:open - ``` - -## Component Testing - -Run Cypress component tests headless - -``` -npm run cypress:run:component -``` - -Open the Cypress UI to run component tests - -``` -npm run cypress:open -``` +SEE: [README](../README.md) for instructions on setting up and running tests ## Coverage diff --git a/cypress/e2e/awx/dashboard/welcomeModal.cy.tsx b/cypress/e2e/awx/dashboard/welcomeModal.cy.tsx index 3eb5ff9724..9171d7655c 100644 --- a/cypress/e2e/awx/dashboard/welcomeModal.cy.tsx +++ b/cypress/e2e/awx/dashboard/welcomeModal.cy.tsx @@ -30,29 +30,18 @@ describe('dashboard: Welcome modal', () => { it('verifies the tech preview banner title in the new UI and the working links to and from the old UI', () => { cy.visit(`/ui_next/dashboard`); - if (Cypress.env('TEST_STANDALONE') === true) { - cy.get('div.pf-c-banner.pf-m-info p') - .should( - 'have.text', - ' You are currently viewing a tech preview of the new Ansible Automation Platform user interface. To return to the original interface, click here.' - ) - .should('be.visible'); - cy.contains('div.pf-c-banner.pf-m-info a', 'here').click(); - cy.url().should('not.include', '/ui_next'); - cy.contains( - 'div.pf-c-banner.pf-m-info p', - 'A tech preview of the new Ansible Automation Platform user interface can be found here.' - ).should('be.visible'); - cy.contains('h2', 'Dashboard').should('be.visible'); - } else { - cy.get('div.pf-c-banner.pf-m-info p') - .should( - 'have.text', - ' You are currently viewing a tech preview of the new AWX user interface. To return to the original interface, click here.' - ) - .should('be.visible'); - cy.contains('div.pf-c-banner.pf-m-info a', 'here').click(); - cy.url().should('include', '/'); - } + cy.get('div.pf-c-banner.pf-m-info p') + .should( + 'have.text', + ' You are currently viewing a tech preview of the new Ansible Automation Platform user interface. To return to the original interface, click here.' + ) + .should('be.visible'); + cy.contains('div.pf-c-banner.pf-m-info a', 'here').click(); + cy.url().should('not.include', '/ui_next'); + cy.contains( + 'div.pf-c-banner.pf-m-info p', + 'A tech preview of the new Ansible Automation Platform user interface can be found here.' + ).should('be.visible'); + cy.contains('h2', 'Dashboard').should('be.visible'); }); }); diff --git a/cypress/e2e/eda/General-UI/dashboard.cy.ts b/cypress/e2e/eda/General-UI/dashboard.cy.ts index 289a9c67f9..bb29e84b8a 100644 --- a/cypress/e2e/eda/General-UI/dashboard.cy.ts +++ b/cypress/e2e/eda/General-UI/dashboard.cy.ts @@ -37,11 +37,7 @@ describe('EDA Dashboard', () => { it('checks Ansible header title', () => { cy.visit('/eda/dashboard/'); - if (Cypress.env('TEST_STANDALONE') === true) { - cy.hasTitle('Welcome to Ansible Automation Platform').should('be.visible'); - } else { - cy.hasTitle('Welcome to EDA Server').should('be.visible'); - } + cy.hasTitle('Welcome to Event Driven Automation').should('be.visible'); }); // it('shows the user an RBA card with a list of RBAs visible including working links', () => { @@ -146,11 +142,7 @@ describe('dashboard checks when resources before any resources are created', () // }); it('checks the dashboard landing page titles ', () => { - if (Cypress.env('TEST_STANDALONE') === true) { - cy.hasTitle('Welcome to Ansible Automation Platform').should('be.visible'); - } else { - cy.hasTitle('Welcome to EDA Server').should('be.visible'); - } + cy.hasTitle('Welcome to Event Driven Automation').should('be.visible'); cy.contains( 'p span', 'Connect intelligence, analytics and service requests to enable more responsive and resilient automation.' diff --git a/cypress/e2e/eda/General-UI/login-logout.cy.ts b/cypress/e2e/eda/General-UI/login-logout.cy.ts index a91fdb3e53..17622f910c 100644 --- a/cypress/e2e/eda/General-UI/login-logout.cy.ts +++ b/cypress/e2e/eda/General-UI/login-logout.cy.ts @@ -14,15 +14,7 @@ describe('EDA Login / Logoff', () => { cy.wait('@loggedOut').then((result) => { expect(result?.response?.statusCode).to.eql(204); }); - if (edaUser && Cypress.env('TEST_STANDALONE') === true) { - cy.typeInputByLabel(/^Username$/, edaUser.username); - cy.typeInputByLabel(/^Password$/, Cypress.env('EDA_PASSWORD') as string); - cy.clickModalButton('Log in'); - cy.get('.pf-c-dropdown__toggle').eq(1).should('contain', edaUser.username); - } else if (edaUser && !Cypress.env('TEST_STANDALONE')) { - cy.get('#E2E-title').then(() => { - cy.contains('E2E').click(); - }); + if (edaUser) { cy.typeInputByLabel(/^Username$/, edaUser.username); cy.typeInputByLabel(/^Password$/, Cypress.env('EDA_PASSWORD') as string); cy.clickModalButton('Log in'); @@ -57,32 +49,14 @@ describe('EDA Login / Logoff', () => { cy.hasDetail('Last name', userDetails.LastName); cy.hasDetail('Email', userDetails.Email); cy.hasDetail('Username', userDetails.Username); - if (Cypress.env('TEST_STANDALONE') === true) { - cy.intercept('GET', '/api/logout/').as('loggedOut'); - cy.edaLogout(); - cy.wait('@loggedOut').then((result) => { - expect(result?.response?.statusCode).to.eql(200); - }); - } else { - cy.intercept('POST', '/api/eda/v1/auth/session/logout/').as('loggedOut'); - cy.edaLogout(); - cy.wait('@loggedOut').then((result) => { - expect(result?.response?.statusCode).to.eql(204); - }); - } - if (Cypress.env('TEST_STANDALONE') === true) { - cy.typeInputByLabel(/^Username$/, userDetails.Username); - cy.typeInputByLabel(/^Password$/, userDetails.Password); - cy.clickModalButton('Log in'); - cy.get('.pf-c-dropdown__toggle').eq(1).should('contain', userDetails.Username); - } else { - cy.get('#E2E-title').then(() => { - cy.contains('E2E').click(); - }); - cy.typeInputByLabel(/^Username$/, userDetails.Username); - cy.typeInputByLabel(/^Password$/, userDetails.Password); - cy.clickModalButton('Log in'); - cy.get('.pf-c-dropdown__toggle').eq(1).should('contain', userDetails.Username); - } + cy.intercept('GET', '/api/logout/').as('loggedOut'); + cy.edaLogout(); + cy.wait('@loggedOut').then((result) => { + expect(result?.response?.statusCode).to.eql(200); + }); + cy.typeInputByLabel(/^Username$/, userDetails.Username); + cy.typeInputByLabel(/^Password$/, userDetails.Password); + cy.clickModalButton('Log in'); + cy.get('.pf-c-dropdown__toggle').eq(1).should('contain', userDetails.Username); }); }); diff --git a/cypress/support/auth.ts b/cypress/support/auth.ts index f049c5415e..edef7b3bf6 100644 --- a/cypress/support/auth.ts +++ b/cypress/support/auth.ts @@ -1,66 +1,15 @@ -import { randomString } from '../../framework/utils/random-string'; -import { AutomationServerType } from '../../frontend/automation-servers/AutomationServer'; - -Cypress.Commands.add( - 'login', - (server: string, username: string, password: string, serverType: AutomationServerType) => { - window.localStorage.setItem('theme', 'light'); - window.localStorage.setItem('disclaimer', 'true'); - - if (Cypress.env('TEST_STANDALONE') === true) { - if (serverType === AutomationServerType.EDA) { - // Standalone EDA login - cy.visit(`/login`, { - retryOnStatusCodeFailure: true, - retryOnNetworkFailure: true, - }); - cy.typeInputByLabel(/^Username$/, username); - cy.typeInputByLabel(/^Password$/, password); - cy.get('button[type=submit]').click(); - return; - } else if (serverType === AutomationServerType.AWX) { - // Standalone AWX login - cy.visit(`/ui_next`, { - retryOnStatusCodeFailure: true, - retryOnNetworkFailure: true, - }); - cy.typeInputByLabel(/^Username$/, username); - cy.typeInputByLabel(/^Password$/, password); - cy.get('button[type=submit]').click(); - cy.contains('a', 'Return to dashboard').click(); - return; - } - } - - cy.visit(`/automation-servers`, { - retryOnStatusCodeFailure: true, - retryOnNetworkFailure: true, - }); - - cy.clickButton(/^Add automation server$/); - const automationServerName = 'E2E ' + randomString(4); - cy.getDialog().within(() => { - cy.typeInputByLabel(/^Name$/, automationServerName); - cy.typeInputByLabel(/^Url$/, server); - switch (serverType) { - case AutomationServerType.AWX: - cy.selectDropdownOptionByLabel(/^Automation type$/, 'AWX Ansible Server'); - break; - case AutomationServerType.EDA: - cy.selectDropdownOptionByLabel(/^Automation type$/, 'Event Driven Automation Server'); - break; - default: - cy.selectDropdownOptionByLabel(/^Automation type$/, 'AWX Ansible Server'); - } - cy.get('button[type=submit]').click(); - }); - - cy.contains('a', automationServerName).click(); - cy.typeInputByLabel(/^Username$/, username); - cy.typeInputByLabel(/^Password$/, password); - cy.get('button[type=submit]').click(); - } -); +Cypress.Commands.add('login', (server: string, username: string, password: string) => { + window.localStorage.setItem('theme', 'light'); + window.localStorage.setItem('disclaimer', 'true'); + + cy.visit(`/login`, { + retryOnStatusCodeFailure: true, + retryOnNetworkFailure: true, + }); + cy.typeInputByLabel(/^Username$/, username); + cy.typeInputByLabel(/^Password$/, password); + cy.get('button[type=submit]').click(); +}); Cypress.Commands.add('edaLogout', () => { cy.get('.pf-c-dropdown__toggle') @@ -78,8 +27,7 @@ Cypress.Commands.add('awxLogin', () => { cy.login( Cypress.env('AWX_SERVER') as string, Cypress.env('AWX_USERNAME') as string, - Cypress.env('AWX_PASSWORD') as string, - AutomationServerType.AWX + Cypress.env('AWX_PASSWORD') as string ); cy.hasTitle('Welcome to'); }, @@ -100,8 +48,7 @@ Cypress.Commands.add('edaLogin', () => { cy.login( Cypress.env('EDA_SERVER') as string, Cypress.env('EDA_USERNAME') as string, - Cypress.env('EDA_PASSWORD') as string, - AutomationServerType.EDA + Cypress.env('EDA_PASSWORD') as string ); cy.hasTitle('Welcome to'); }, diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index fccfafc7fe..30187fe89b 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -4,7 +4,6 @@ import '@cypress/code-coverage/support'; import '@4tw/cypress-drag-drop'; import { SetOptional, SetRequired } from 'type-fest'; -import { AutomationServerType } from '../../frontend/automation-servers/AutomationServer'; import { AwxToken } from '../../frontend/awx/interfaces/AwxToken'; import { Credential } from '../../frontend/awx/interfaces/Credential'; import { ExecutionEnvironment } from '../../frontend/awx/interfaces/ExecutionEnvironment'; @@ -40,12 +39,7 @@ import './rest-commands'; declare global { namespace Cypress { interface Chainable { - login( - server: string, - username: string, - password: string, - serverType: AutomationServerType - ): Chainable; + login(server: string, username: string, password: string): Chainable; edaLogout(): Chainable; awxLogin(): Chainable; edaLogin(): Chainable; diff --git a/framework/README.md b/framework/README.md index 0986e5ebbf..8d2b841817 100644 --- a/framework/README.md +++ b/framework/README.md @@ -1,5 +1,5 @@ # Ansible UI Framework -A framework for building applications using [PatternFly](https://www.patternfly.org), developed by the Ansible UI developers +A framework for building applications using [PatternFly](https://www.patternfly.org), developed by the Ansible UI developers. [Documentation](https://github.com/ansible/ansible-ui/wiki/Ansible-UI-Framework) diff --git a/nginx/awx.conf b/nginx/awx.conf new file mode 100644 index 0000000000..9ad24df3ee --- /dev/null +++ b/nginx/awx.conf @@ -0,0 +1,52 @@ +server { + listen 8080 default_server; + listen [::]:8080; + + server_name _; + server_tokens off; + + access_log off; + # error_log off; + + autoindex off; + + include mime.types; + types { + application/manifest+json webmanifest; + } + + sendfile on; + + root /usr/share/nginx/html; + + location /api { + proxy_pass $AWX_PROTOCOL://$AWX_HOST; + proxy_set_header Host $AWX_HOST; + proxy_set_header Origin $AWX_PROTOCOL://$AWX_HOST; + proxy_set_header Referer $AWX_PROTOCOL://$AWX_HOST; + } + + location /websocket { + proxy_pass $AWX_PROTOCOL://$AWX_HOST; + proxy_set_header Host $AWX_HOST; + proxy_set_header Origin $AWX_PROTOCOL://$AWX_HOST; + proxy_set_header Referer $AWX_PROTOCOL://$AWX_HOST; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location ~* \.(json|woff|woff2|jpe?g|png|gif|ico|svg|css|js)$ { + add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable"; + try_files $uri =404; + gzip_static on; + } + + location / { + autoindex off; + expires off; + add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always; + try_files $uri /index.html =404; + } +} diff --git a/nginx/eda.conf b/nginx/eda.conf new file mode 100644 index 0000000000..86f5ece7a2 --- /dev/null +++ b/nginx/eda.conf @@ -0,0 +1,52 @@ +server { + listen 8080 default_server; + listen [::]:8080; + + server_name _; + server_tokens off; + + access_log off; + # error_log off; + + autoindex off; + + include mime.types; + types { + application/manifest+json webmanifest; + } + + sendfile on; + + root /usr/share/nginx/html; + + location ~ ^/api/eda/ws/[0-9a-z-]+ { + proxy_pass $EDA_PROTOCOL://$EDA_HOST; + proxy_set_header Host $EDA_HOST; + proxy_set_header Origin $EDA_PROTOCOL://$EDA_HOST; + proxy_set_header Referer $EDA_PROTOCOL://$EDA_HOST; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location ~ ^/api/eda/v[0-9]+/ { + proxy_pass $EDA_PROTOCOL://$EDA_HOST; + proxy_set_header Host $EDA_HOST; + proxy_set_header Origin $EDA_PROTOCOL://$EDA_HOST; + proxy_set_header Referer $EDA_PROTOCOL://$EDA_HOST; + } + + location ~* \.(json|woff|woff2|jpe?g|png|gif|ico|svg|css|js)$ { + add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable"; + try_files $uri =404; + gzip_static on; + } + + location / { + autoindex off; + expires off; + add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always; + try_files $uri /index.html =404; + } +} diff --git a/nginx/hub.conf b/nginx/hub.conf new file mode 100644 index 0000000000..dd2b4335cf --- /dev/null +++ b/nginx/hub.conf @@ -0,0 +1,41 @@ +server { + listen 8080 default_server; + listen [::]:8080; + + server_name _; + server_tokens off; + + access_log off; + # error_log off; + + autoindex off; + + include mime.types; + types { + application/manifest+json webmanifest; + } + + sendfile on; + + root /usr/share/nginx/html; + + location /api { + proxy_pass $HUB_PROTOCOL://$HUB_HOST; + proxy_set_header Host $HUB_HOST; + proxy_set_header Origin $HUB_PROTOCOL://$HUB_HOST; + proxy_set_header Referer $HUB_PROTOCOL://$HUB_HOST; + } + + location ~* \.(json|woff|woff2|jpe?g|png|gif|ico|svg|css|js)$ { + add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable"; + try_files $uri =404; + gzip_static on; + } + + location / { + autoindex off; + expires off; + add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always; + try_files $uri /index.html =404; + } +} diff --git a/package-lock.json b/package-lock.json index 9241499506..eb7dc47598 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,6 +112,7 @@ "@types/js-yaml": "4.0.5", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", + "compression-webpack-plugin": "^10.0.0", "cypress": "12.17.2", "cypress-react-selector": "3.0.0", "eslint": "8.45.0", @@ -6089,6 +6090,45 @@ "node": ">= 0.8.0" } }, + "node_modules/compression-webpack-plugin": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-10.0.0.tgz", + "integrity": "sha512-wLXLIBwpul/ALcm7Aj+69X0pYT3BYt6DdPn3qrgBIh9YejV9Bju9ShhlAsjujLyWMo6SAweFIWaUoFmXZNuNrg==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/compression-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "license": "MIT", @@ -21160,6 +21200,30 @@ } } }, + "compression-webpack-plugin": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-10.0.0.tgz", + "integrity": "sha512-wLXLIBwpul/ALcm7Aj+69X0pYT3BYt6DdPn3qrgBIh9YejV9Bju9ShhlAsjujLyWMo6SAweFIWaUoFmXZNuNrg==", + "dev": true, + "requires": { + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } + } + }, "concat-map": { "version": "0.0.1" }, diff --git a/package.json b/package.json index 7f22786e1c..0cd0da5968 100644 --- a/package.json +++ b/package.json @@ -9,49 +9,56 @@ "url": "https://github.com/ansible/ansible-ui.git" }, "homepage": "https://github.com/ansible/ansible-ui#readme", - "type": "module", "scripts": { - "start": "concurrently npm:frontend npm:proxy --prefix-colors cyan,green", - "frontend": "webpack serve --mode development --env awx_route_prefix='/ui_next' --env hub_route_prefix='/hub' --env eda_route_prefix='/eda'", - "awx": "PRODUCT='AWX' webpack serve --port 4001 --open --config ./webpack.awx.cjs --mode development --env UI_MODE=AWX", - "hub": "PRODUCT='Automation Hub' webpack serve --port 4002 --open --config ./webpack.hub.cjs --mode development --env UI_MODE=HUB", - "eda": "PRODUCT='Event Driven Automation' webpack serve --port 4003 --open --config ./webpack.eda.cjs --mode development --env UI_MODE=EDA", - "galaxy": "PRODUCT='Galaxy' webpack serve --config ./webpack.hub.cjs --mode development --env UI_MODE=GALAXY --env hub_route_prefix='/hub'", - "proxy": "NODE_ENV=development PORT=3001 nodemon --watch './proxy/**/*.ts' --exec 'node --experimental-specifier-resolution=node --loader ts-node/esm/transpile-only' proxy/main.ts | pino-zen", - "build": "npm run clean && concurrently --kill-others-on-fail npm:build:frontend npm:build:proxy --prefix-colors cyan,green,blue,magenta,gray", - "build:proxy": "cd proxy && rollup --config rollup.config.ts --configPlugin typescript", - "build:frontend": "webpack --mode production --env pwa", - "build:awx": "webpack --mode production --env UI_MODE=AWX --env awx_route_prefix='/ui_next' --output-path build/awx && mv build/awx/index.html build/awx/index_awx.html", - "build:hub": "webpack --mode production --env UI_MODE=HUB --output-path build/hub", - "build:eda": "webpack --mode production --env UI_MODE=EDA --output-path build/eda", - "clean": "rm -rf build coverage .nyc_output", - "test": "concurrently --kill-others-on-fail npm:tsc npm:eslint npm:prettier npm:cypress:run:component --prefix-colors cyan,green,blue,magenta,gray", + "start": "concurrently npm:awx npm:hub npm:eda -c cyan,green,blue", + "awx": "UI_MODE=AWX webpack serve --mode development --config ./webpack/webpack.awx.cjs --port 4101 --open", + "hub": "UI_MODE=HUB webpack serve --mode development --config ./webpack/webpack.hub.cjs --port 4102 --open", + "eda": "UI_MODE=EDA webpack serve --mode development --config ./webpack/webpack.eda.cjs --port 4103 --open", + "build": "concurrently npm:build:awx npm:build:hub npm:build:eda -c cyan,green,blue", + "build:awx": "rm -rf build/awx && UI_MODE=AWX webpack --mode production --config ./webpack/webpack.awx.cjs --output-path build/awx", + "build:hub": "rm -rf build/hub && UI_MODE=HUB webpack --mode production --config ./webpack/webpack.hub.cjs --output-path build/hub", + "build:eda": "rm -rf build/eda && UI_MODE=EDA webpack --mode production --config ./webpack/webpack.eda.cjs --output-path build/eda", + "clean": "rm -rf dist build coverage .nyc_output cypress/coverage cypress/.nyc_output", + "test": "concurrently --kill-others-on-fail npm:tsc npm:eslint npm:prettier npm:component --prefix-colors cyan,green,blue,magenta,gray", "tsc": "tsc --noEmit", - "eslint": "eslint --max-warnings=0 frontend proxy framework cypress", - "eslint:fix": "eslint --fix frontend proxy framework cypress", + "eslint": "eslint --max-warnings=0 frontend framework cypress", + "eslint:fix": "eslint --fix frontend framework cypress", "eslint:changed": "eslint --max-warnings=0 $(git diff --name-only --diff-filter=M origin/main -- '*.ts' '*.tsx' '*.jsx' '*.js'| xargs)", - "prettier": "prettier --check !**/*.scss frontend proxy framework cypress", - "prettier:fix": "prettier --write frontend proxy framework cypress locales docs README.md", + "prettier": "prettier --check !**/*.scss frontend framework cypress", + "prettier:fix": "prettier --write frontend framework cypress locales docs README.md", "prettier:changed": "prettier --check $(git diff --name-only --diff-filter=M origin/main | xargs)", "prettier:changed:fix": "prettier --write $(git diff --name-only --diff-filter=M origin/main | xargs)", "checks": "concurrently --kill-others-on-fail npm:tsc npm:eslint npm:prettier --prefix-colors cyan,green,blue,magenta,gray", "upgrade": "npx npm-check-updates '/^@patternfly.*$/' --upgrade --target latest --doctor && npx npm-check-updates '/^(?!@patternfly).*$/' --upgrade --target minor --doctor && npm audit fix || true && npm dedup || true ", - "docker:build": "docker build --tag ansible-ui .", - "docker:run": "docker run --name ansible-ui -d -e LOG_LEVEL=debug -p 3002:3002 -e PORT=3002 ansible-ui", - "docker:rm": "docker rm ansible-ui", - "docker:build:eda": "docker build --target eda-ui --tag eda-ui .", - "docker:run:eda": "docker run --name eda-ui --rm -e LOG_LEVEL=debug -p 3002:3002 -e PORT=3002 eda-ui", - "cypress:run": "concurrently npm:cypress:run:awx npm:cypress:run:eda npm:cypress:run:component --prefix-colors cyan,blue,green", - "cypress:run:e2e": "cypress run --e2e", - "cypress:run:awx": "cypress run --e2e --spec=cypress/e2e/awx/**/*.cy.ts", - "cypress:run:eda": "cypress run --e2e --spec=cypress/e2e/eda/**/*.cy.ts", - "cypress:run:component": "cypress run --component", - "cypress:open": "cypress open --browser chrome", + "e2e:awx": "cypress open --browser chrome --e2e --config-file=cypress.awx.config.ts", + "e2e:hub": "cypress open --browser chrome --e2e --config-file=cypress.hub.config.ts", + "e2e:eda": "cypress open --browser chrome --e2e --config-file=cypress.eda.config.ts", + "e2e:run": "concurrently npm:e2e:run:awx npm:e2e:run:hub npm:e2e:run:eda -c cyan,green,blue", + "e2e:run:awx": "cypress run --e2e --config-file=cypress.awx.config.ts", + "e2e:run:hub": "cypress run --e2e --config-file=cypress.hub.config.ts", + "e2e:run:eda": "cypress run --e2e --config-file=cypress.eda.config.ts", + "component:awx": "cypress open --browser chrome --component --config-file=cypress.awx.config.ts", + "component:hub": "cypress open --browser chrome --component --config-file=cypress.hub.config.ts", + "component:eda": "cypress open --browser chrome --component --config-file=cypress.eda.config.ts", + "component:afw": "cypress open --browser chrome --component --config-file=cypress.afw.config.ts", + "component:run": "concurrently npm:component:run:awx npm:component:run:hub npm:component:run:eda -c cyan,green,blue", + "component:run:awx": "cypress run --component --config-file=cypress.awx.config.ts", + "component:run:hub": "cypress run --component --config-file=cypress.hub.config.ts", + "component:run:eda": "cypress run --component --config-file=cypress.eda.config.ts", + "component:run:afw": "cypress run --component --config-file=cypress.afw.config.ts", "coverage": "open coverage/lcov-report/index.html", - "cypress:check": "npx nyc report --check-coverage --statements 20 --branches 20 --functions 19 --lines 20 --report-dir ./coverage --temp-dir .nyc_output --reporter=text-summary --exclude-after-remap false", + "coverage:check": "npx nyc report --check-coverage --statements 20 --branches 20 --functions 19 --lines 20 --report-dir ./coverage --temp-dir .nyc_output --reporter=text-summary --exclude-after-remap false", "prepare": "husky install", "prepush": "concurrently --kill-others-on-fail npm:tsc npm:eslint:changed npm:prettier:changed --prefix-colors cyan,green,blue,magenta,gray", - "i18n": "npx i18next-parser --config i18next-parser.config.cjs" + "i18n": "npx i18next-parser --config i18next-parser.config.cjs", + "docker:build": "concurrently npm:docker:build:awx npm:docker:build:hub npm:docker:build:eda -c cyan,green,blue", + "docker:build:awx": "docker build --target awx-ui --tag awx-ui .", + "docker:build:hub": "docker build --target hub-ui --tag hub-ui .", + "docker:build:eda": "docker build --target eda-ui --tag eda-ui .", + "docker:run": "concurrently npm:docker:run:awx npm:docker:run:hub npm:docker:run:eda -c cyan,green,blue", + "docker:run:awx": "echo http://localhost:4101 && docker run --name awx-ui --rm -e LOG_LEVEL=debug -p 4101:8080 -e AWX_HOST=$AWX_HOST -e AWX_PROTOCOL=$AWX_PROTOCOL awx-ui", + "docker:run:hub": "echo http://localhost:4102 && docker run --name hub-ui --rm -e LOG_LEVEL=debug -p 4102:8080 -e HUB_HOST=$HUB_HOST -e HUB_PROTOCOL=$HUB_PROTOCOL hub-ui", + "docker:run:eda": "echo http://localhost:4103 && docker run --name eda-ui --rm -e LOG_LEVEL=debug -p 4103:8080 -e EDA_HOST=$EDA_HOST -e EDA_PROTOCOL=$EDA_PROTOCOL eda-ui" }, "dependencies": { "@babel/core": "7.22.9", @@ -157,6 +164,7 @@ "@types/js-yaml": "4.0.5", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", + "compression-webpack-plugin": "^10.0.0", "cypress": "12.17.2", "cypress-react-selector": "3.0.0", "eslint": "8.45.0", diff --git a/proxy/constants.ts b/proxy/constants.ts deleted file mode 100644 index 510495323f..0000000000 --- a/proxy/constants.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { constants } from 'http2'; - -export const HTTP2_HEADER_AUTHORITY = constants.HTTP2_HEADER_AUTHORITY; -export const HTTP2_HEADER_CACHE_CONTROL = constants.HTTP2_HEADER_CACHE_CONTROL; -export const HTTP2_HEADER_CONNECTION = constants.HTTP2_HEADER_CONNECTION; -export const HTTP2_HEADER_CONTENT_LENGTH = constants.HTTP2_HEADER_CONTENT_LENGTH; -export const HTTP2_HEADER_CONTENT_TYPE = constants.HTTP2_HEADER_CONTENT_TYPE; -export const HTTP2_HEADER_COOKIE = constants.HTTP2_HEADER_COOKIE; -export const HTTP2_HEADER_HOST = constants.HTTP2_HEADER_HOST; -export const HTTP2_HEADER_KEEP_ALIVE = constants.HTTP2_HEADER_KEEP_ALIVE; -export const HTTP2_HEADER_LOCATION = constants.HTTP2_HEADER_LOCATION; -export const HTTP2_HEADER_PROXY_AUTHENTICATE = constants.HTTP2_HEADER_PROXY_AUTHENTICATE; -export const HTTP2_HEADER_PROXY_AUTHORIZATION = constants.HTTP2_HEADER_PROXY_AUTHORIZATION; -export const HTTP2_HEADER_REFERER = constants.HTTP2_HEADER_REFERER; -export const HTTP2_HEADER_TE = constants.HTTP2_HEADER_TE; -export const HTTP2_HEADER_TRANSFER_ENCODING = constants.HTTP2_HEADER_TRANSFER_ENCODING; -export const HTTP2_HEADER_UPGRADE = constants.HTTP2_HEADER_UPGRADE; -export const HTTP2_HEADER_VARY = constants.HTTP2_HEADER_VARY; - -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses - -/** This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response. */ -export const HTTP_STATUS_BAD_GATEWAY = constants.HTTP_STATUS_BAD_GATEWAY; - -/** The server has encountered a situation it does not know how to handle. */ -export const HTTP_STATUS_INTERNAL_SERVER_ERROR = constants.HTTP_STATUS_INTERNAL_SERVER_ERROR; - -/** The server can not find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. */ -export const HTTP_STATUS_NOT_FOUND = constants.HTTP_STATUS_NOT_FOUND; - -/** The request succeeded. The result meaning of "success" depends on the HTTP method: -GET: The resource has been fetched and transmitted in the message body. -HEAD: The representation headers are included in the response without any message body. -PUT or POST: The resource describing the result of the action is transmitted in the message body. -TRACE: The message body contains the request message as received by the server. */ -export const HTTP_STATUS_OK = constants.HTTP_STATUS_OK; - -/** The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This response should be used for temporary conditions and the Retry-After HTTP header should, if possible, contain the estimated time before the recovery of the service. The webmaster must also take care about the caching-related headers that are sent along with this response, as these temporary condition responses should usually not be cached. */ -export const HTTP_STATUS_SERVICE_UNAVAILABLE = constants.HTTP_STATUS_SERVICE_UNAVAILABLE; - -export const HTTP_STATUS_TEMPORARY_REDIRECT = constants.HTTP_STATUS_TEMPORARY_REDIRECT; - -export const HTTP_STATUS_UNAUTHORIZED = constants.HTTP_STATUS_UNAUTHORIZED; diff --git a/proxy/logger.ts b/proxy/logger.ts deleted file mode 100644 index dca508485d..0000000000 --- a/proxy/logger.ts +++ /dev/null @@ -1,10 +0,0 @@ -import pino from 'pino'; - -export const logLevel = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes( - process.env.LOG_LEVEL ?? '' -) - ? process.env.LOG_LEVEL - : 'debug'; - -const options: pino.LoggerOptions = { level: logLevel, base: {} }; -export const logger = pino(options); diff --git a/proxy/main.ts b/proxy/main.ts deleted file mode 100644 index 1e1499b0ca..0000000000 --- a/proxy/main.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* Copyright Contributors to the Open Cluster Management project */ -import { cpus, totalmem } from 'os'; -import { logger } from './logger'; -import { start, stop } from './proxy'; - -logger.info({ - msg: `proxy start`, - NODE_ENV: process.env.NODE_ENV, - cpus: `${Object.keys(cpus()).length}`, - memory: `${(totalmem() / (1024 * 1024 * 1024)).toPrecision(2).toString()}GB`, - node: process.versions.node, - version: process.env.VERSION, -}); - -process.on('exit', function processExit(code) { - if (code !== 0) { - logger.error({ msg: `process exit`, code: code }); - } else { - logger.debug({ msg: `process exit`, code: code }); - } -}); - -process.on('SIGINT', () => { - // eslint-disable-next-line no-console - if (process.env.NODE_ENV === 'development') console.log(); - logger.debug({ msg: 'process SIGINT' }); - void stop(); -}); - -process.on('SIGTERM', () => { - logger.debug({ msg: 'process SIGTERM' }); - void stop(); -}); - -void start(); diff --git a/proxy/proxy-handler.ts b/proxy/proxy-handler.ts deleted file mode 100644 index e338fc94bb..0000000000 --- a/proxy/proxy-handler.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* eslint-disable no-console */ -/* Copyright Contributors to the Open Cluster Management project */ -import cookie from 'cookie'; -import http from 'http'; -import { Http2ServerRequest, Http2ServerResponse, OutgoingHttpHeaders } from 'http2'; -import { RequestOptions, request } from 'https'; -import { pipeline } from 'stream'; -import { - HTTP2_HEADER_CACHE_CONTROL, - HTTP2_HEADER_CONNECTION, - HTTP2_HEADER_COOKIE, - HTTP2_HEADER_HOST, - HTTP2_HEADER_KEEP_ALIVE, - HTTP2_HEADER_PROXY_AUTHENTICATE, - HTTP2_HEADER_PROXY_AUTHORIZATION, - HTTP2_HEADER_REFERER, - HTTP2_HEADER_TE, - HTTP2_HEADER_TRANSFER_ENCODING, - HTTP2_HEADER_UPGRADE, - HTTP2_HEADER_VARY, - HTTP_STATUS_BAD_GATEWAY, - HTTP_STATUS_NOT_FOUND, - HTTP_STATUS_SERVICE_UNAVAILABLE, - HTTP_STATUS_UNAUTHORIZED, -} from './constants'; -import { logger } from './logger'; - -export function proxyHandler(req: Http2ServerRequest, res: Http2ServerResponse): void { - const cookieHeader = req.headers[HTTP2_HEADER_COOKIE]; - let cookies: Record | undefined; - if (typeof cookieHeader === 'string') { - cookies = cookie.parse(cookieHeader); - } - - const target = cookies?.['server']; - if (!target || typeof target !== 'string') { - res.writeHead(HTTP_STATUS_UNAUTHORIZED).end(); - return; - } - - const url = req.url; - - const headers: OutgoingHttpHeaders = {}; - - for (const header in req.headers) { - if (header.startsWith(':')) continue; - headers[header] = req.headers[header]; - } - - const proxyUrl = new URL(target); - - headers[HTTP2_HEADER_HOST] = proxyUrl.port - ? `${proxyUrl.hostname}:${proxyUrl.port}` - : proxyUrl.hostname; - headers['origin'] = proxyUrl.port - ? `${proxyUrl.protocol}//${proxyUrl.hostname}:${proxyUrl.port}` - : `${proxyUrl.protocol}//${proxyUrl.hostname}`; - headers[HTTP2_HEADER_REFERER] = proxyUrl.port - ? `${proxyUrl.protocol}//${proxyUrl.hostname}:${proxyUrl.port}` - : `${proxyUrl.protocol}//${proxyUrl.hostname}`; - - // Remove x-forwarded-proto header - // fixes django - django.security.csrf Forbidden (Referer checking failed - Referer is insecure while host is secure.) - delete headers['x-forwarded-proto']; - - const requestOptions: RequestOptions = { - protocol: proxyUrl.protocol, - host: proxyUrl.host, - hostname: proxyUrl.hostname, - port: proxyUrl.port, - method: req.method, - path: url, - headers, - rejectUnauthorized: false, - }; - - const r = requestOptions.protocol === 'http:' ? http.request : request; - pipeline( - req, - r(requestOptions, (response) => { - if (!response) { - res.writeHead(HTTP_STATUS_NOT_FOUND).end(); - return; - } - // Remove hop-by-hop headers - const { - [HTTP2_HEADER_CONNECTION]: _connection, - [HTTP2_HEADER_KEEP_ALIVE]: _keepAlive, - [HTTP2_HEADER_PROXY_AUTHENTICATE]: _proxyAuthenticate, - [HTTP2_HEADER_PROXY_AUTHORIZATION]: _proxyAuthorization, - [HTTP2_HEADER_TE]: _te, - [HTTP2_HEADER_TRANSFER_ENCODING]: _transferEncoding, - [HTTP2_HEADER_UPGRADE]: _upgrade, - trailer: _trailer, - ...responseHeaders - } = response.headers; - - // Force no caching - responseHeaders[HTTP2_HEADER_CACHE_CONTROL] = 'no-store'; - delete responseHeaders[HTTP2_HEADER_VARY]; - - const statusCode = response.statusCode ?? 500; - res.writeHead(statusCode, responseHeaders); - pipeline(response, res as unknown as NodeJS.WritableStream, (err) => - handlePipelineError(err, res) - ); - }), - (err) => handleRequestError(err, res) - ); -} - -function handleRequestError(err: NodeJS.ErrnoException | null, res: Http2ServerResponse) { - if (err) { - switch (err.code) { - case 'ECONNREFUSED': - if (!res.headersSent) { - res.writeHead(HTTP_STATUS_SERVICE_UNAVAILABLE); - } - if (!res.closed) { - res.end(); - } - break; - default: - logger.error(err); - if (!res.headersSent) { - res.writeHead(HTTP_STATUS_SERVICE_UNAVAILABLE); - } - if (!res.closed) { - res.end(); - } - break; - } - } -} - -function handlePipelineError(err: NodeJS.ErrnoException | null, res: Http2ServerResponse) { - if (err) { - switch (err.code) { - default: - logger.error(err); - if (!res.headersSent) { - res.writeHead(HTTP_STATUS_BAD_GATEWAY); - } - if (!res.closed) { - res.end(); - } - break; - } - } -} diff --git a/proxy/proxy.ts b/proxy/proxy.ts deleted file mode 100644 index 158565b2e0..0000000000 --- a/proxy/proxy.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Http2Server } from 'http2'; -import { logger } from './logger'; -import { requestHandler } from './request-handler'; -import { startServer, stopServer } from './server'; - -export function start(): Promise { - return startServer({ requestHandler }); -} - -export async function stop(): Promise { - if (process.env.NODE_ENV === 'development') { - setTimeout(() => { - logger.warn('process stop timeout. exiting...'); - process.exit(1); - }, 0.5 * 1000).unref(); - } - await stopServer(); -} diff --git a/proxy/readiness.ts b/proxy/readiness.ts deleted file mode 100644 index 6bdde03576..0000000000 --- a/proxy/readiness.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Http2ServerRequest, Http2ServerResponse } from 'http2'; -import { HTTP_STATUS_OK } from './constants'; - -export function readiness(req: Http2ServerRequest, res: Http2ServerResponse) { - res.writeHead(HTTP_STATUS_OK).end(); -} diff --git a/proxy/request-handler.ts b/proxy/request-handler.ts deleted file mode 100644 index 68077b527e..0000000000 --- a/proxy/request-handler.ts +++ /dev/null @@ -1,34 +0,0 @@ -import cookie from 'cookie'; -import { Http2ServerRequest, Http2ServerResponse } from 'http2'; -import { HTTP2_HEADER_COOKIE, HTTP_STATUS_INTERNAL_SERVER_ERROR } from './constants'; -import { logger } from './logger'; -import { proxyHandler } from './proxy-handler'; -import { serve } from './serve'; - -export function requestHandler(req: Http2ServerRequest, res: Http2ServerResponse): void { - try { - let proxyRequest = false; - if (req.url.startsWith('/api/')) { - const cookieHeader = req.headers[HTTP2_HEADER_COOKIE]; - let cookies: Record | undefined; - if (typeof cookieHeader === 'string') { - cookies = cookie.parse(cookieHeader); - } - - const target = cookies?.['server']; - proxyRequest = !!target; - } - - if (proxyRequest) { - proxyHandler(req, res); - } else { - void serve(req, res); - } - } catch (err) { - logger.error(err); - if (!res.headersSent) { - res.writeHead(HTTP_STATUS_INTERNAL_SERVER_ERROR).end(); - return; - } - } -} diff --git a/proxy/rollup.config.ts b/proxy/rollup.config.ts deleted file mode 100644 index bfcacc9c9f..0000000000 --- a/proxy/rollup.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import commonjs from '@rollup/plugin-commonjs'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import replace from '@rollup/plugin-replace'; -import strip from '@rollup/plugin-strip'; -import typescript from '@rollup/plugin-typescript'; - -export default { - input: 'main.ts', - output: { - file: '../build/proxy.mjs', - format: 'es', - }, - plugins: [ - commonjs(), - nodeResolve({ - preferBuiltins: true, - }), - typescript(), - replace({ - preventAssignment: true, - values: { 'process.env.NODE_ENV': JSON.stringify('production') }, - }), - strip({ - include: ['**/*.(ts|js)'], - functions: ['console.*', 'assert.*', 'logger.trace'], - }), - ], -}; diff --git a/proxy/serve.ts b/proxy/serve.ts deleted file mode 100644 index 16d36431e8..0000000000 --- a/proxy/serve.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* Copyright Contributors to the Open Cluster Management project */ -import etag from 'etag'; -import { createReadStream, Stats } from 'fs'; -import { stat } from 'fs/promises'; -import { constants, Http2ServerRequest, Http2ServerResponse } from 'http2'; -import { extname } from 'path'; -import { pipeline } from 'stream'; -import { logger } from './logger'; - -const cacheControl = - process.env.NODE_ENV === 'production' - ? 'public, max-age=31536000, stale-if-error=60' - : 'no-store'; -const localesCacheControl = - process.env.NODE_ENV === 'production' ? 'public, max-age=3600, stale-if-error=60' : 'no-store'; - -export async function serve(req: Http2ServerRequest, res: Http2ServerResponse): Promise { - try { - let url = req.url.split('?')[0]; - - let ext = extname(url); - if (ext === '') { - ext = '.html'; - url = '/index.html'; - } - - // Security headers - if (url === '/index.html') { - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Strict-Transport-Security', 'max-age=31536000'); - res.setHeader('X-Frame-Options', 'deny'); - res.setHeader('X-XSS-Protection', '1; mode=block'); - res.setHeader('X-Content-Type-Options', 'nosniff'); - res.setHeader('X-Permitted-Cross-Domain-Policies', 'none'); - res.setHeader('Referrer-Policy', 'no-referrer'); - res.setHeader('X-DNS-Prefetch-Control', 'off'); - res.setHeader('Expect-CT', 'enforce, max-age=30'); - // res.setHeader('Content-Security-Policy', ["default-src 'self'"].join(';')) - } else if (url === '/manifest.webmanifest') { - res.setHeader('Cache-Control', 'public, no-cache'); - } else if (url === '/service-worker.js') { - res.setHeader('Cache-Control', 'public, no-cache'); - } else if (url.includes('/locales/')) { - res.setHeader('Cache-Control', localesCacheControl); - } else { - res.setHeader('Cache-Control', cacheControl); - } - - const acceptEncoding = (req.headers['accept-encoding'] as string) ?? ''; - const contentType = contentTypes[ext]; - if (contentType === undefined) { - logger.debug({ msg: 'unknown content type', ext, url: req.url }); - res.writeHead(404).end(); - return; - } - - const filePath = './public' + url; - let stats: Stats; - try { - stats = await stat(filePath); - } catch { - res.writeHead(404).end(); - return; - } - - const modificationTime = stats.mtime.toUTCString(); - res.setHeader(constants.HTTP2_HEADER_LAST_MODIFIED, modificationTime); - // Don't send content for cache revalidation - if (req.headers['if-modified-since'] === modificationTime) { - res.writeHead(constants.HTTP_STATUS_NOT_MODIFIED).end(); - } - - if (/\bbr\b/.test(acceptEncoding)) { - try { - const brStats = await stat(filePath + '.br'); - const readStream = createReadStream('./public' + url + '.br', { autoClose: true }); - readStream - .on('open', () => { - res.writeHead(200, { - [constants.HTTP2_HEADER_CONTENT_ENCODING]: 'br', - [constants.HTTP2_HEADER_CONTENT_TYPE]: contentType, - [constants.HTTP2_HEADER_CONTENT_LENGTH]: brStats.size.toString(), - [constants.HTTP2_HEADER_ETAG]: etag(brStats), - }); - }) - .on('error', (err) => { - logger.error(err); - res.writeHead(404).end(); - }); - pipeline(readStream, res as unknown as NodeJS.WritableStream, (err) => { - if (err) logger.error(err); - }); - return; - } catch { - // Do nothing - } - } - - if (/\bgzip\b/.test(acceptEncoding)) { - try { - const gzStats = await stat(filePath + '.gz'); - const readStream = createReadStream('./public' + url + '.gz', { autoClose: true }); - readStream - .on('open', () => { - res.writeHead(200, { - [constants.HTTP2_HEADER_CONTENT_ENCODING]: 'gzip', - [constants.HTTP2_HEADER_CONTENT_TYPE]: contentType, - [constants.HTTP2_HEADER_CONTENT_LENGTH]: gzStats.size.toString(), - [constants.HTTP2_HEADER_ETAG]: etag(gzStats), - }); - }) - .on('error', (err) => { - logger.error(err); - res.writeHead(404).end(); - }); - pipeline(readStream, res as unknown as NodeJS.WritableStream, (err) => { - if (err) logger.error(err); - }); - return; - } catch { - // Do nothing - } - } - - const readStream = createReadStream('./public' + url, { autoClose: true }); - readStream - .on('open', () => { - res.writeHead(200, { - [constants.HTTP2_HEADER_CONTENT_TYPE]: contentType, - [constants.HTTP2_HEADER_CONTENT_LENGTH]: stats.size.toString(), - [constants.HTTP2_HEADER_ETAG]: etag(stats), - }); - }) - .on('error', (err) => { - logger.error(err); - res.writeHead(404).end(); - }); - pipeline(readStream, res as unknown as NodeJS.WritableStream, (err) => { - if (err) logger.error(err); - }); - } catch (err) { - logger.error(err); - res.writeHead(404).end(); - return; - } -} - -const contentTypes: Record = { - '.txt': 'text/plain;charset=UTF-8', - '.html': 'text/html;charset=UTF-8', - '.css': 'text/css;charset=UTF-8', - '.js': 'application/javascript;charset=UTF-8', - '.map': 'application/json;charset=UTF-8', - '.jpg': 'image/jpeg', - '.json': 'application/json;charset=UTF-8', - '.svg': 'image/svg+xml;charset=UTF-8', - '.png': 'image/png', - '.ttf': 'font/ttf', - '.woff': 'font/woff', - '.woff2': 'font/woff2', - '.webmanifest': 'application/manifest+json;charset=UTF-8', -}; diff --git a/proxy/server.ts b/proxy/server.ts deleted file mode 100644 index 43d541cab5..0000000000 --- a/proxy/server.ts +++ /dev/null @@ -1,281 +0,0 @@ -/* Copyright Contributors to the Open Cluster Management project */ -/* istanbul ignore file */ -import { readFileSync } from 'fs'; -import { STATUS_CODES } from 'http'; -import { - constants, - createSecureServer, - createServer, - Http2Server, - Http2ServerRequest, - Http2ServerResponse, -} from 'http2'; -import { Socket } from 'net'; -import { join } from 'path'; -import { exit } from 'process'; -import selfsigned from 'selfsigned'; -import { TLSSocket } from 'tls'; -import { WebSocket, WebSocketServer } from 'ws'; -import { HTTP2_HEADER_CONTENT_LENGTH } from './constants'; -import { logger } from './logger'; - -let server: Http2Server | undefined; - -interface ISocketRequests { - socketID: number; - activeRequests: number; -} - -let nextSocketID = 0; -const sockets: { [id: string]: Socket | TLSSocket | undefined } = {}; - -export type ServerOptions = { - requestHandler: - | ((req: Http2ServerRequest, res: Http2ServerResponse) => void) - | ((req: Http2ServerRequest, res: Http2ServerResponse) => Promise); - logRequest?: (req: Http2ServerRequest, res: Http2ServerResponse) => void; -}; - -export function startServer(options: ServerOptions): Promise { - let cert: Buffer | undefined; - let key: Buffer | undefined; - try { - cert = readFileSync('./certs/tls.crt'); - key = readFileSync('./certs/tls.key'); - } catch (err) { - const pems = selfsigned.generate(undefined, { - keySize: 2048, - }); - cert = Buffer.from(pems.cert); - key = Buffer.from(pems.private); - logger.info({ msg: 'using self signed certificates' }); - } - - try { - if (cert && key) { - logger.debug({ msg: `server start`, secure: true }); - server = createSecureServer( - { cert, key, allowHTTP1: true }, - options.requestHandler as (req: Http2ServerRequest, res: Http2ServerResponse) => void - ); - } else { - logger.debug({ msg: `server start`, secure: false }); - server = createServer( - options.requestHandler as (req: Http2ServerRequest, res: Http2ServerResponse) => void - ); - } - - const wss = new WebSocketServer({ server: server as unknown as undefined }); - wss.on('listening', () => logger.info('websocket server listening')); - wss.on('connection', (ws, incommingMessage) => { - const cookies = incommingMessage.headers.cookie - ?.split(';') - .map((p) => p.trim()) - .reduce>((cookies, cookieValue) => { - const parts = cookieValue.split('='); - if (parts.length > 0) { - cookies[parts[0]] = parts.slice(1).join('='); - } - return cookies; - }, {}); - const server = cookies?.server; - logger.debug({ msg: 'websocket conection', targetServer: server }); - - let messageQueue: string[] | undefined = []; - - let websocket: WebSocket | undefined; - if (server) { - let new_uri: string; - if (server.startsWith('https')) { - new_uri = server.replace('https', 'wss'); - } else { - new_uri = server.replace('http', 'ws'); - } - new_uri = join(new_uri, `/websocket/`); - websocket = new WebSocket(new_uri, { - headers: incommingMessage.headers, - rejectUnauthorized: false, - }); - } - - websocket - ?.on('error', (err) => { - logger.error({ msg: 'websocket proxy error', error: err.message }); - }) - .on('open', () => { - logger.debug({ msg: 'websocket proxy open' }); - if (messageQueue) { - for (const message of messageQueue) { - websocket?.send(message); - } - messageQueue = undefined; - } - }) - .on('message', (data) => { - logger.debug({ - msg: 'websocket proxy message', - ...(JSON.parse(data.toString()) as unknown as object), - }); - ws.send(data.toString()); - }) - .on('close', () => { - logger.debug({ msg: 'websocket proxy close' }); - }); - - ws.on('error', (err) => logger.error({ msg: 'websocket error', message: err.message })) - .on('message', (data) => { - logger.debug({ - msg: 'websocket message', - ...(JSON.parse(data.toString()) as unknown as object), - }); - if (messageQueue) { - messageQueue.push(data.toString()); - } else { - websocket?.send(data.toString()); - } - }) - .on('close', () => { - logger.debug('websocket close'); - websocket?.close(); - }); - }); - wss.on('error', (err) => logger.error({ msg: 'websocket server error', error: err.message })); - wss.on('close', () => logger.info('websocket server closed')); - - return new Promise((resolve, reject) => { - server - ?.listen(process.env.PORT, () => { - const address = server?.address(); - if (address == null) { - logger.info({ msg: `server listening` }); - } else if (typeof address === 'string') { - logger.info({ msg: `server listening`, address }); - } else { - logger.info({ msg: `server listening`, port: address.port }); - } - resolve(server); - }) - .on('connection', (socket: Socket) => { - let socketID = nextSocketID++; - while (sockets[socketID] !== undefined) { - socketID = nextSocketID++; - } - sockets[socketID] = socket; - (socket as unknown as ISocketRequests).socketID = socketID; - (socket as unknown as ISocketRequests).activeRequests = 0; - socket.on('close', () => { - const socketID = (socket as unknown as ISocketRequests).socketID; - if (socketID < nextSocketID) nextSocketID = socketID; - sockets[socketID] = undefined; - }); - }) - .on('request', (req: Http2ServerRequest, res: Http2ServerResponse) => { - if (isStopping) { - res.setHeader(constants.HTTP2_HEADER_CONNECTION, 'close'); - } - const start = process.hrtime(); - const socket = req.socket as unknown as ISocketRequests; - socket.activeRequests++; - req.on('close', () => { - socket.activeRequests--; - if (isStopping) { - req.socket.destroy(); - } - - const msg: Record = { - msg: STATUS_CODES[res.statusCode], - status: res.statusCode, - method: req.method, - path: req.url, - }; - - if (process.env.NODE_ENV === 'development') { - const diff = process.hrtime(start); - const time = Math.round((diff[0] * 1e9 + diff[1]) / 1000); - msg.ms = time; - } - - if (res.hasHeader(HTTP2_HEADER_CONTENT_LENGTH)) { - try { - const length = Number(res.getHeader(HTTP2_HEADER_CONTENT_LENGTH)); - if (Number.isInteger(length)) { - msg.kb = length / 1000; - } - } catch { - // DO nothing - } - } - - if (res.statusCode >= 500) { - logger.error(msg); - } else { - logger.debug(msg); - } - }); - }) - .on('error', (err: NodeJS.ErrnoException) => { - if (err.code === 'EADDRINUSE') { - logger.error({ - msg: `server error`, - error: 'address already in use', - port: Number(process.env.PORT), - }); - reject(undefined); - } else { - logger.error({ msg: `server error`, error: err.message }); - } - if (server?.listening) server.close(); - }); - }); - } catch (err) { - if (err instanceof Error) { - logger.error({ msg: `server start error`, error: err.message, stack: err.stack }); - } else { - logger.error({ msg: `server start error` }); - } - void stopServer(); - return Promise.resolve(undefined); - } -} - -let isStopping = false; - -export async function stopServer(): Promise { - if (isStopping) return; - isStopping = true; - - for (const socketID of Object.keys(sockets)) { - const socket = sockets[socketID]; - if (socket !== undefined) { - if ((socket as unknown as ISocketRequests).activeRequests === 0) { - socket.destroy(); - } - } - } - - if (server?.listening) { - logger.debug({ msg: 'closing server' }); - - setTimeout(() => { - logger.error({ msg: 'server did not close after 60 seconds. killing process' }); - exit(1); - }, 60 * 1000).unref(); - - const delay = Number(process.env.CLOSE_DELAY); - if (Number.isInteger(delay) && delay > 0) { - logger.info({ msg: `waiting ${delay} seconds before closing the server` }); - await new Promise((resolve) => setTimeout(resolve, delay * 1000)); - } - - await new Promise((resolve) => - server?.close((err: Error | undefined) => { - if (err) { - logger.error({ msg: 'server close error', name: err.name, error: err.message }); - } else { - logger.debug({ msg: 'server closed' }); - } - resolve(); - }) - ); - } -} diff --git a/proxy/tsconfig.json b/proxy/tsconfig.json deleted file mode 100644 index bb02a738f6..0000000000 --- a/proxy/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["."], - "compilerOptions": { - "types": [], - "declaration": false - } -} diff --git a/tsconfig.json b/tsconfig.json index baa005cd75..0c9f56449a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,9 +30,7 @@ "frontend", "framework", "cypress", - "proxy", - "cypress.config.ts", - "cypress.eda.ts", + "*.config.ts", "cypress.d.ts", "i18next-parser.config.cjs" ] diff --git a/webpack.awx.cjs b/webpack/webpack.awx.cjs similarity index 74% rename from webpack.awx.cjs rename to webpack/webpack.awx.cjs index 26c25802c5..87eb63940e 100644 --- a/webpack.awx.cjs +++ b/webpack/webpack.awx.cjs @@ -1,10 +1,13 @@ const webpackConfig = require('./webpack.config'); -const awxServer = process.env.AWX_SERVER ?? 'http://localhost:8043'; +const awxServer = process.env.AWX_HOST + ? process.env.AWX_PROTOCOL + '://' + process.env.AWX_HOST + : 'http://localhost:8043'; + const proxyUrl = new URL(awxServer); -module.exports = function (_env, argv) { - const config = webpackConfig(_env, argv); +module.exports = function (env, argv) { + const config = webpackConfig(env, argv); config.devServer.proxy = { '/api': { target: awxServer, diff --git a/webpack.config.cjs b/webpack/webpack.config.cjs similarity index 85% rename from webpack.config.cjs rename to webpack/webpack.config.cjs index d3ef856af9..e840ae26a7 100644 --- a/webpack.config.cjs +++ b/webpack/webpack.config.cjs @@ -2,12 +2,42 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin' const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const path = require('path'); const webpack = require('webpack'); const { GenerateSW } = require('workbox-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); const MergeJsonWebpackPlugin = require('merge-jsons-webpack-plugin'); const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); +const CompressionPlugin = require('compression-webpack-plugin'); + +switch (process.env.UI_MODE) { + case 'AWX': + case 'HUB': + case 'EDA': + break; + case '': + case undefined: + console.error('UI_MODE is not set'); + exit(1); + break; + default: + console.error('UI_MODE is not valid'); + exit(1); + break; +} + +if (!process.env.PRODUCT) { + switch (process.env.UI_MODE) { + case 'AWX': + process.env.PRODUCT = 'Automation Controller'; + break; + case 'HUB': + process.env.PRODUCT = 'Automation Hub'; + break; + case 'EDA': + process.env.PRODUCT = 'Event Driven Automation'; + break; + } +} module.exports = function (env, argv) { var isProduction = argv.mode === 'production' || argv.mode === undefined; @@ -67,9 +97,7 @@ module.exports = function (env, argv) { ? JSON.stringify('') : JSON.stringify(process.env.DELAY ?? ''), 'process.env.PWA': env.pwa ? JSON.stringify('true') : JSON.stringify(''), - - 'process.env.UI_MODE': env.UI_MODE ? JSON.stringify(env.UI_MODE) : undefined, - + 'process.env.UI_MODE': JSON.stringify(process.env.UI_MODE), 'process.env.AWX_ROUTE_PREFIX': env.awx_route_prefix ? JSON.stringify(env.awx_route_prefix) : JSON.stringify('/ui_next'), @@ -93,7 +121,7 @@ module.exports = function (env, argv) { }); }), new HtmlWebpackPlugin({ - title: process.env.PRODUCT ? process.env.PRODUCT : 'AnsibleDev', + title: process.env.PRODUCT, template: 'frontend/index.html', }), new MiniCssExtractPlugin({ @@ -121,12 +149,12 @@ module.exports = function (env, argv) { }, ], }), + new CompressionPlugin(), ].filter(Boolean), output: { clean: true, filename: isProduction ? '[contenthash].js' : undefined, - path: path.resolve(__dirname, 'build/public'), - publicPath: isProduction && env.awx ? '/static/awx/' : '/', + publicPath: '/', }, optimization: { minimizer: [ @@ -139,26 +167,10 @@ module.exports = function (env, argv) { ], }, devServer: { - static: 'ansible', - port: 3002, historyApiFallback: true, compress: true, hot: true, - server: 'https', - proxy: { - '/api': { - target: 'https://localhost:3001', - secure: false, - }, - '/websocket/': { - target: 'https://localhost:3001', - secure: false, - ws: true, - }, - }, - devMiddleware: { - writeToDisk: true, - }, + devMiddleware: { writeToDisk: true }, }, devtool: isProduction ? 'source-map' : 'eval-source-map', watchOptions: { diff --git a/webpack.eda.cjs b/webpack/webpack.eda.cjs similarity index 64% rename from webpack.eda.cjs rename to webpack/webpack.eda.cjs index 5fda70dc5f..96d123e560 100644 --- a/webpack.eda.cjs +++ b/webpack/webpack.eda.cjs @@ -1,10 +1,13 @@ const webpackConfig = require('./webpack.config'); -const edaServer = process.env.EDA_SERVER ?? 'http://localhost:8000'; +const edaServer = process.env.EDA_HOST + ? process.env.EDA_PROTOCOL + '://' + process.env.EDA_HOST + : 'http://localhost:8000'; + const proxyUrl = new URL(edaServer); -module.exports = function (_env, argv) { - const config = webpackConfig(_env, argv); +module.exports = function (env, argv) { + const config = webpackConfig(env, argv); config.devServer.proxy = { '/api': { target: edaServer, diff --git a/webpack.hub.cjs b/webpack/webpack.hub.cjs similarity index 64% rename from webpack.hub.cjs rename to webpack/webpack.hub.cjs index c64b654384..c4b3c15f45 100644 --- a/webpack.hub.cjs +++ b/webpack/webpack.hub.cjs @@ -1,10 +1,13 @@ const webpackConfig = require('./webpack.config'); -const hubServer = process.env.HUB_SERVER ?? 'http://localhost:5001'; +const hubServer = process.env.HUB_HOST + ? process.env.HUB_PROTOCOL + '://' + process.env.HUB_HOST + : 'http://localhost:5001'; + const proxyUrl = new URL(hubServer); -module.exports = function (_env, argv) { - const config = webpackConfig(_env, argv); +module.exports = function (env, argv) { + const config = webpackConfig(env, argv); config.devServer.proxy = { '/api': { target: hubServer,