diff --git a/.github/actions/bump-and-tag/package-lock.json b/.github/actions/bump-and-tag/package-lock.json index 8633d0ecef..1efb933cef 100644 --- a/.github/actions/bump-and-tag/package-lock.json +++ b/.github/actions/bump-and-tag/package-lock.json @@ -1238,9 +1238,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.8.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", - "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1816,9 +1816,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001677", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", - "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "dev": true, "funding": [ { @@ -1976,9 +1976,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "license": "MIT", "dependencies": { @@ -2187,9 +2187,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.50", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.50.tgz", - "integrity": "sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw==", + "version": "1.5.55", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", + "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", "dev": true, "license": "ISC" }, @@ -2308,9 +2308,9 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", - "integrity": "sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz", + "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2322,6 +2322,7 @@ "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "globalthis": "^1.0.4", + "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2", "has-proto": "^1.0.3", "has-symbols": "^1.0.3", @@ -5403,9 +5404,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, "license": "MIT", "engines": { diff --git a/.github/actions/run-full-stack/action.yml b/.github/actions/run-full-stack/action.yml index da18810477..924d9c2b89 100644 --- a/.github/actions/run-full-stack/action.yml +++ b/.github/actions/run-full-stack/action.yml @@ -9,5 +9,5 @@ runs: run: | set -x export JWT_PRIVATE_KEY="${{ env.JWT_PRIVATE_KEY }}" - docker compose -f docker-compose.yml down -v - docker compose -f docker-compose.yml up db data-import backend frontend-static --build -d + docker compose -f docker-compose.static.yml down -v + docker compose -f docker-compose.static.yml up --build -d diff --git a/.github/actions/setup-javascript/action.yml b/.github/actions/setup-javascript/action.yml index f473b82b24..f2128926a1 100644 --- a/.github/actions/setup-javascript/action.yml +++ b/.github/actions/setup-javascript/action.yml @@ -6,7 +6,7 @@ runs: - name: Set up Bun uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.0.11 + bun-version: latest - name: Install bun dependencies shell: bash working-directory: ./frontend diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 5038e937d6..fb7ab358b5 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -9,5 +9,5 @@ jobs: commitlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: wagoid/commitlint-github-action@v6 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + - uses: wagoid/commitlint-github-action@3d28780bbf0365e29b144e272b2121204d5be5f3 # v6 diff --git a/.github/workflows/nightly_scans.yml b/.github/workflows/nightly_scans.yml index c5dcc3d7ea..eee7f74e67 100644 --- a/.github/workflows/nightly_scans.yml +++ b/.github/workflows/nightly_scans.yml @@ -34,6 +34,6 @@ jobs: uses: SvanBoxel/zaproxy-to-ghas@cfc77481d74a17a4c3d6b753aa9d7abef453d501 # v1.0.2 - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3 + uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3 with: sarif_file: results.sarif diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..0c739dc226 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release +on: + push: + branches: + - main + +permissions: + contents: read + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write # to be able to publish a GitHub release + issues: write # to be able to comment on released issues + pull-requests: write # to be able to comment on released pull requests + id-token: write # to enable use of OIDC for npm provenance + steps: + - name: Checkout + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3 + with: + node-version: "lts/*" + - name: Install semantic-release + run: npm install -g semantic-release + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release diff --git a/.github/workflows/security_codeql.yml b/.github/workflows/security_codeql.yml index 0198c133b2..8bd6f307e4 100644 --- a/.github/workflows/security_codeql.yml +++ b/.github/workflows/security_codeql.yml @@ -22,7 +22,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3 + uses: github/codeql-action/init@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3 with: languages: javascript, python # If you wish to specify custom queries, you can do so here or in a config file. @@ -33,4 +33,4 @@ jobs: queries: +security-extended - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3 + uses: github/codeql-action/analyze@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3 diff --git a/.github/workflows/security_semgrep.yml b/.github/workflows/security_semgrep.yml index 8e073968a2..5438b6fcf9 100644 --- a/.github/workflows/security_semgrep.yml +++ b/.github/workflows/security_semgrep.yml @@ -25,7 +25,7 @@ jobs: SEMGREP_RULES: "p/default" - name: Upload SARIF file for GitHub Advanced Security Dashboard - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3 + uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3 with: sarif_file: ${{ env.SEMGREP_TO_UPLOAD }} if: always() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 930bc2da23..05ff838eba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,16 +12,10 @@ repos: - id: check-yaml - id: check-added-large-files - id: check-merge-conflict - - repo: https://github.com/hadolint/hadolint rev: v2.10.0 hooks: - id: hadolint - # We're running black, but doing it via nox session instead - see below - # - repo: https://github.com/psf/black - # rev: 22.6.0 - # hooks: - # - id: black - repo: https://github.com/pre-commit/mirrors-isort rev: v5.10.1 hooks: @@ -46,21 +40,16 @@ repos: - css - html pass_filenames: false - - repo: local - hooks: - id: trufflehog name: TruffleHog description: Detect secrets in your data. - # For running trufflehog locally, use the following: - # entry: bash -c 'trufflehog git file://. --since-commit HEAD --only-verified --fail' - # For running trufflehog in docker, use the following entry instead: - entry: bash -c 'docker run --rm -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir --since-commit HEAD --only-verified --fail' + entry: bash -c 'if command -v podman >/dev/null 2>&1; then podman run --rm -v "$(pwd):/workdir" -i trufflesecurity/trufflehog:latest git file:///workdir --since-commit HEAD --only-verified --fail; elif command -v docker >/dev/null 2>&1; then docker run --rm -v "$(pwd):/workdir" -i trufflesecurity/trufflehog:latest git file:///workdir --since-commit HEAD --only-verified --fail; else echo "Neither docker nor podman found. Please install one of them." && exit 1; fi' language: system stages: ["pre-commit", "pre-push"] - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v9.18.0 hooks: - id: commitlint - stages: [ commit-msg ] - additional_dependencies: [ "@commitlint/config-conventional" ] + stages: [commit-msg] + additional_dependencies: ["@commitlint/config-conventional"] language_version: 22.8.0 diff --git a/README.md b/README.md index fbe319f902..cbe9868a2a 100644 --- a/README.md +++ b/README.md @@ -87,19 +87,13 @@ docker compose up --build To run the application using the production server configuration... ```shell -docker compose up db data-import backend frontend-static --build +docker compose -f docker-compose.static.yml up --build ```` -To run the application using the minimal initial data set... - -```shell - docker compose --profile data-initial up --build -``` - To run the application using the demo data set... ```shell - docker compose --profile data-demo up --build +docker compose -f docker-compose.demo.yml up --build ``` diff --git a/backend/data_tools/Pipfile b/backend/data_tools/Pipfile index c2b2b18fac..946e2d7959 100644 --- a/backend/data_tools/Pipfile +++ b/backend/data_tools/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] SQLAlchemy = "==2.0.36" pandas = "==2.2.3" -json5 = "==0.9.25" +json5 = "==0.9.28" psycopg2-binary = "==2.9.10" cfenv = "==0.5.3" typing-extensions = "==4.12.2" @@ -14,7 +14,7 @@ desert = "2022.9.22" sqlalchemy-continuum = "==1.4.2" marshmallow-sqlalchemy = "==1.1.0" marshmallow-enum = "==1.5.1" -alembic = "==1.13.3" +alembic = "==1.14.0" alembic-postgresql-enum = "==1.3.0" azure-storage-blob = "==12.23.1" azure-identity = "==1.19.0" diff --git a/backend/data_tools/Pipfile.lock b/backend/data_tools/Pipfile.lock index 245455e205..0827cbb6f3 100644 --- a/backend/data_tools/Pipfile.lock +++ b/backend/data_tools/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0e6d269778b3fa13b39ee82858159006ff843a8a6b9cfab2e9c7cee41c5ad4ee" + "sha256": "8676e9b10cc75886b16dad0f4386a2a7d30500337b5ab1839b77278af3c78463" }, "pipfile-spec": 6, "requires": { @@ -132,12 +132,12 @@ }, "alembic": { "hashes": [ - "sha256:203503117415561e203aa14541740643a611f641517f0209fcae63e9fa09f1a2", - "sha256:908e905976d15235fae59c9ac42c4c5b75cfcefe3d27c0fbf7ae15a37715d80e" + "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25", + "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.13.3" + "version": "==1.14.0" }, "alembic-postgresql-enum": { "hashes": [ @@ -644,12 +644,12 @@ }, "json5": { "hashes": [ - "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f", - "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae" + "sha256:1f82f36e615bc5b42f1bbd49dbc94b12563c56408c6ffa06414ea310890e9a6e", + "sha256:29c56f1accdd8bc2e037321237662034a7e07921e2b7223281a5ce2c46f0c4df" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.9.25" + "markers": "python_full_version >= '3.8.0'", + "version": "==0.9.28" }, "loguru": { "hashes": [ @@ -952,11 +952,11 @@ }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "pandas": { "hashes": [ @@ -1909,11 +1909,11 @@ }, "jedi": { "hashes": [ - "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", - "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0" + "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", + "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9" ], "markers": "python_version >= '3.6'", - "version": "==0.19.1" + "version": "==0.19.2" }, "markdown-it-py": { "hashes": [ @@ -2064,11 +2064,11 @@ }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "parso": { "hashes": [ diff --git a/backend/data_tools/tests/docker-compose.yml b/backend/data_tools/tests/docker-compose.yml index acda2ed160..1382ca0f11 100644 --- a/backend/data_tools/tests/docker-compose.yml +++ b/backend/data_tools/tests/docker-compose.yml @@ -4,7 +4,6 @@ services: db: image: "postgres:16" - platform: linux/amd64 container_name: unit-test-db security_opt: - no-new-privileges:true # Resolve semgrep https://sg.run/0n8q diff --git a/backend/ops_api/Pipfile b/backend/ops_api/Pipfile index 1b7deb6d2d..53f4191a29 100644 --- a/backend/ops_api/Pipfile +++ b/backend/ops_api/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -alembic = "==1.13.3" +alembic = "==1.14.0" alembic-postgresql-enum = "==1.3.0" authlib = "==1.3.2" azure-identity = "==1.19.0" diff --git a/backend/ops_api/Pipfile.lock b/backend/ops_api/Pipfile.lock index 59ed975b38..523aaf8d2d 100644 --- a/backend/ops_api/Pipfile.lock +++ b/backend/ops_api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "47eadc35be9a7fe271f38bd0f98b1bab07fc82949b69698331c356f7d9ef2ac9" + "sha256": "cfba920e45218610ae7e31839169db22d9117c1f90708bb117617b4145cbdbb4" }, "pipfile-spec": 6, "requires": { @@ -18,12 +18,12 @@ "default": { "alembic": { "hashes": [ - "sha256:203503117415561e203aa14541740643a611f641517f0209fcae63e9fa09f1a2", - "sha256:908e905976d15235fae59c9ac42c4c5b75cfcefe3d27c0fbf7ae15a37715d80e" + "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25", + "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.13.3" + "version": "==1.14.0" }, "alembic-postgresql-enum": { "hashes": [ @@ -79,11 +79,11 @@ }, "blinker": { "hashes": [ - "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", - "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83" + "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", + "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" ], - "markers": "python_version >= '3.8'", - "version": "==1.8.2" + "markers": "python_version >= '3.9'", + "version": "==1.9.0" }, "certifi": { "hashes": [ @@ -659,11 +659,11 @@ }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "portalocker": { "hashes": [ @@ -956,11 +956,11 @@ }, "werkzeug": { "hashes": [ - "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", - "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5" + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" ], "markers": "python_version >= '3.9'", - "version": "==3.1.1" + "version": "==3.1.3" } }, "develop": { @@ -1133,11 +1133,11 @@ }, "blinker": { "hashes": [ - "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", - "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83" + "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", + "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" ], - "markers": "python_version >= '3.8'", - "version": "==1.8.2" + "markers": "python_version >= '3.9'", + "version": "==1.9.0" }, "click": { "hashes": [ @@ -1426,11 +1426,11 @@ }, "jedi": { "hashes": [ - "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", - "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0" + "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", + "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9" ], "markers": "python_version >= '3.6'", - "version": "==0.19.1" + "version": "==0.19.2" }, "jinja2": { "hashes": [ @@ -1711,11 +1711,11 @@ }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "parse": { "hashes": [ @@ -2026,11 +2026,11 @@ }, "werkzeug": { "hashes": [ - "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", - "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5" + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" ], "markers": "python_version >= '3.9'", - "version": "==3.1.1" + "version": "==3.1.3" }, "yarl": { "hashes": [ diff --git a/backend/ops_api/ops/document/azure_document_repository.py b/backend/ops_api/ops/document/azure_document_repository.py index b3f91a7df1..7d4af499eb 100644 --- a/backend/ops_api/ops/document/azure_document_repository.py +++ b/backend/ops_api/ops/document/azure_document_repository.py @@ -74,7 +74,14 @@ def generate_account_sas_url(account_name, account_key, expiry_hours=1): ) # Construct the SAS URL - sas_url = f"https://{account_name}.blob.core.windows.net/?{sas_token}" + DEFAULT_DEV_OPS_URL = "https://dev.ops.opre.acf.gov" + OPS_URL = ( + DEFAULT_DEV_OPS_URL + if "localhost" in current_app.config.get("OPS_FRONTEND_URL") + else current_app.config.get("OPS_FRONTEND_URL") + ) + # https://dev.ops.opre.acf.gov/?{sas_token} in dev + sas_url = f"{OPS_URL}/?{sas_token}" return sas_url except SasUrlGenerationError as e: diff --git a/backend/ops_api/ops/document/document_gateway.py b/backend/ops_api/ops/document/document_gateway.py index 599dddce45..159c9455f7 100644 --- a/backend/ops_api/ops/document/document_gateway.py +++ b/backend/ops_api/ops/document/document_gateway.py @@ -1,5 +1,4 @@ from flask import Config -from flask_jwt_extended import current_user from ops_api.ops.document.azure_document_repository import AzureDocumentRepository from ops_api.ops.document.document_repository import DocumentRepository @@ -16,11 +15,7 @@ def __init__(self, config: Config) -> None: # Validate and register providers with the factory self.register_providers() - # Select provider based on the current user - if current_user.id >= 500: # Users with id 5xx are test users - self.provider = DocumentProviders.fake.name - else: - self.provider = DocumentProviders.azure.name + self.provider = DocumentProviders.azure.name def register_providers(self) -> None: """ diff --git a/backend/ops_api/tests/docker-compose.yml b/backend/ops_api/tests/docker-compose.yml index 95cc9c6cac..1e18698e51 100644 --- a/backend/ops_api/tests/docker-compose.yml +++ b/backend/ops_api/tests/docker-compose.yml @@ -1,7 +1,6 @@ services: unittest_db: image: "postgres:16" - platform: linux/amd64 container_name: unit-test-db command: -c 'max_connections=400' security_opt: @@ -24,7 +23,6 @@ services: build: context: ../../../backend dockerfile: Dockerfile.data-tools - platform: linux/amd64 container_name: pytest-data-import environment: - ENV=pytest diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml new file mode 100644 index 0000000000..d1ea53dbbd --- /dev/null +++ b/docker-compose.demo.yml @@ -0,0 +1,90 @@ +services: + + db: + image: "postgres:16" + container_name: ops-db + security_opt: + - no-new-privileges:true # Resolve semgrep https://sg.run/0n8q + environment: + - POSTGRES_PASSWORD=local_password + read_only: true # Resolve semgrep https://sg.run/e4JE + tmpfs: /var/run/postgresql/ + volumes: + - ./backend/data_tools/ops_db_sql_init:/docker-entrypoint-initdb.d + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + disable-users: + build: + context: ./backend/ + dockerfile: Dockerfile.data-tools + container_name: disable-users + environment: + - ENV=local + - SQLALCHEMY_DATABASE_URI=postgresql://ops:ops@db:5432/postgres + command: ["/home/app/.venv/bin/python", "./data_tools/src/disable_users/disable_users.py"] + depends_on: + db: + condition: service_healthy + data-import: + condition: service_completed_successfully + + frontend: + build: + context: ./frontend/ + dockerfile: Dockerfile + environment: + - REACT_APP_BACKEND_DOMAIN=http://localhost:8080 + - VITE_BACKEND_DOMAIN=http://localhost:8080 + container_name: ops-frontend-demo + ports: + - "3000:3000" + depends_on: + - backend + volumes: + - ./frontend/src:/home/app/src + + backend: + build: + context: ./backend/ + dockerfile: Dockerfile.ops-api + container_name: ops-backend-demo + ports: + - "8080:8080" + command: /bin/sh -c " . .venv/bin/activate && python -m flask run --debug --host=0.0.0.0 --port=8080" + environment: + - JWT_PRIVATE_KEY + - JWT_PUBLIC_KEY + - OPS_CONFIG=environment/local/container.py + volumes: + - ./backend/ops_api/ops:/home/app/ops_api/ops + depends_on: + db: + condition: service_healthy + data-import: + condition: service_completed_successfully + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8080" ] + interval: 10s + timeout: 10s + retries: 10 + + data-import: + build: + context: ./backend/ + dockerfile: Dockerfile.data-tools + container_name: ops-data-demo + environment: + - ENV=local + - SQLALCHEMY_DATABASE_URI=postgresql://ops:ops@db:5432/postgres + command: /bin/sh -c "./data_tools/scripts/import_test_data.sh && ./data_tools/scripts/demo_data.sh" + volumes: + - ./backend/ops_api:/home/app/ops_api + depends_on: + db: + condition: service_healthy diff --git a/docker-compose.static.yml b/docker-compose.static.yml new file mode 100644 index 0000000000..2250c7f241 --- /dev/null +++ b/docker-compose.static.yml @@ -0,0 +1,91 @@ +services: + + db: + image: "postgres:16" + container_name: ops-db + security_opt: + - no-new-privileges:true # Resolve semgrep https://sg.run/0n8q + environment: + - POSTGRES_PASSWORD=local_password + read_only: true # Resolve semgrep https://sg.run/e4JE + tmpfs: /var/run/postgresql/ + volumes: + - ./backend/data_tools/ops_db_sql_init:/docker-entrypoint-initdb.d + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + data-import: + build: + context: ./backend/ + dockerfile: Dockerfile.data-tools + container_name: ops-data-import + environment: + - ENV=local + - SQLALCHEMY_DATABASE_URI=postgresql://ops:ops@db:5432/postgres + command: /bin/sh -c "./data_tools/scripts/import_test_data.sh" + volumes: + # See below for an explanation of this volume. The same reasoning applies, + # but in this case it's so we can run new migrations immediately without + # having to rebuild the migration container. + - ./backend/ops_api:/home/app/ops_api + depends_on: + db: + condition: service_healthy + + disable-users: + build: + context: ./backend/ + dockerfile: Dockerfile.data-tools + container_name: disable-users + environment: + - ENV=local + - SQLALCHEMY_DATABASE_URI=postgresql://ops:ops@db:5432/postgres + command: ["/home/app/.venv/bin/python", "./data_tools/src/disable_users/disable_users.py"] + depends_on: + db: + condition: service_healthy + data-import: + condition: service_completed_successfully + + backend: + build: + context: ./backend/ + dockerfile: Dockerfile.ops-api + container_name: ops-backend + ports: + - "8080:8080" + command: /bin/sh -c " . .venv/bin/activate && python -m flask run --debug --host=0.0.0.0 --port=8080" + environment: + - JWT_PRIVATE_KEY + - JWT_PUBLIC_KEY + - OPS_CONFIG=environment/local/container.py + volumes: + - ./backend/ops_api/ops:/home/app/ops_api/ops + depends_on: + db: + condition: service_healthy + data-import: + condition: service_completed_successfully + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8080" ] + interval: 10s + timeout: 10s + retries: 10 + + frontend-static: + build: + context: ./frontend/ + dockerfile: Dockerfile.azure + args: + VITE_BACKEND_DOMAIN: http://localhost:8080 + MODE: dev # set this to production to create a production build + container_name: ops-frontend + ports: + - "3000:3000" + depends_on: + - backend diff --git a/docker-compose.yml b/docker-compose.yml index cdbebf39cc..548c221cdc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,7 @@ services: db: - profiles: - - '' # This is the default profile, which is used for development - - data-initial # This profile is used to initialize the database with initial data - - data-demo # This profile is used to initialize the database with demo data image: "postgres:16" - platform: linux/amd64 container_name: ops-db security_opt: - no-new-privileges:true # Resolve semgrep https://sg.run/0n8q @@ -25,12 +20,9 @@ services: retries: 5 data-import: - profiles: - - '' # This is the default profile, which is used for development build: context: ./backend/ dockerfile: Dockerfile.data-tools - platform: linux/amd64 container_name: ops-data-import environment: - ENV=local @@ -46,12 +38,9 @@ services: condition: service_healthy disable-users: - profiles: - - '' # This is the default profile, which is used for development build: context: ./backend/ dockerfile: Dockerfile.data-tools - platform: linux/amd64 container_name: disable-users environment: - ENV=local @@ -64,12 +53,9 @@ services: condition: service_completed_successfully backend: - profiles: - - '' # This is the default profile, which is used for development build: context: ./backend/ dockerfile: Dockerfile.ops-api - platform: linux/amd64 container_name: ops-backend ports: - "8080:8080" @@ -92,12 +78,9 @@ services: retries: 10 frontend: - profiles: - - '' # This is the default profile, which is used for development build: context: ./frontend/ dockerfile: Dockerfile - platform: linux/amd64 container_name: ops-frontend environment: - REACT_APP_BACKEND_DOMAIN=http://localhost:8080 @@ -108,151 +91,3 @@ services: - backend volumes: - ./frontend/src:/home/app/src - - frontend-static: - profiles: - - static - build: - context: ./frontend/ - dockerfile: Dockerfile.azure - args: - VITE_BACKEND_DOMAIN: http://localhost:8080 - MODE: dev # set this to production to create a production build - platform: linux/amd64 - container_name: ops-frontend-static - ports: - - "3000:3000" - depends_on: - - backend - - - # The following services are used to initialize the database with initial data using the data-initial profile. - - frontend-static-initial: - profiles: - - data-initial - build: - context: ./frontend/ - dockerfile: Dockerfile.azure - args: - VITE_BACKEND_DOMAIN: http://localhost:8080 - MODE: dev # set this to production to create a production build - platform: linux/amd64 - container_name: ops-frontend-static-initial - ports: - - "3000:3000" - depends_on: - - backend-initial - - - backend-initial: - profiles: - - data-initial - build: - context: ./backend/ - dockerfile: Dockerfile.ops-api - platform: linux/amd64 - container_name: ops-backend-initial - ports: - - "8080:8080" - command: /bin/sh -c " . .venv/bin/activate && python -m flask run --debug --host=0.0.0.0 --port=8080" - environment: - - JWT_PRIVATE_KEY - - JWT_PUBLIC_KEY - - OPS_CONFIG=environment/local/container.py - volumes: - - ./backend/ops_api/ops:/home/app/ops_api/ops - depends_on: - db: - condition: service_healthy - data-initial: - condition: service_completed_successfully - healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:8080" ] - interval: 10s - timeout: 10s - retries: 10 - - data-initial: - profiles: - - data-initial - build: - context: ./backend/ - dockerfile: Dockerfile.data-tools - platform: linux/amd64 - container_name: ops-data-initial - environment: - - ENV=local - - SQLALCHEMY_DATABASE_URI=postgresql://ops:ops@db:5432/postgres - command: /bin/sh -c "./data_tools/scripts/initial_data.sh" - volumes: - - ./backend/ops_api:/home/app/ops_api - depends_on: - db: - condition: service_healthy - - # The following services are used to initialize the database with demo data using the data-demo profile. - - frontend-demo: - profiles: - - data-demo - build: - context: ./frontend/ - dockerfile: Dockerfile - environment: - - REACT_APP_BACKEND_DOMAIN=http://localhost:8080 - - VITE_BACKEND_DOMAIN=http://localhost:8080 - platform: linux/amd64 - container_name: ops-frontend-demo - ports: - - "3000:3000" - depends_on: - - backend-demo - volumes: - - ./frontend/src:/home/app/src - - backend-demo: - profiles: - - data-demo - build: - context: ./backend/ - dockerfile: Dockerfile.ops-api - platform: linux/amd64 - container_name: ops-backend-demo - ports: - - "8080:8080" - command: /bin/sh -c " . .venv/bin/activate && python -m flask run --debug --host=0.0.0.0 --port=8080" - environment: - - JWT_PRIVATE_KEY - - JWT_PUBLIC_KEY - - OPS_CONFIG=environment/local/container.py - volumes: - - ./backend/ops_api/ops:/home/app/ops_api/ops - depends_on: - db: - condition: service_healthy - data-demo: - condition: service_completed_successfully - healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:8080" ] - interval: 10s - timeout: 10s - retries: 10 - - data-demo: - profiles: - - data-demo - build: - context: ./backend/ - dockerfile: Dockerfile.data-tools - platform: linux/amd64 - container_name: ops-data-demo - environment: - - ENV=local - - SQLALCHEMY_DATABASE_URI=postgresql://ops:ops@db:5432/postgres - command: /bin/sh -c "./data_tools/scripts/import_test_data.sh && ./data_tools/scripts/demo_data.sh" - volumes: - - ./backend/ops_api:/home/app/ops_api - depends_on: - db: - condition: service_healthy diff --git a/docs/git-branching-and-releasing.md b/docs/git-branching-and-releasing.md new file mode 100644 index 0000000000..b3a107f902 --- /dev/null +++ b/docs/git-branching-and-releasing.md @@ -0,0 +1,43 @@ +# OPRE-OPS Branching Strategy + +The OPRE Team currently uses the following branching strategy: +### main +* `main` is the default and primary branch. This will always be the most up-to-date released code base. +* `main` currently has branch protections in place, and requires a `pull request` with at least `2` approvals from someone on the `dev team`. + +### Feature Branches +* Features should branch from `main` and utilize a naming format of `OPS-{Issue#}/{Feature_Name}`, example: `OPS-522/CAN_Details_Page`. + +```mermaid +--- +title: OPRE-OPS Git Branching +--- +%%{init: {'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchOrder': 4}} }%% +gitGraph + commit id: "initial commit" + commit + branch OPS-5xx/New_Feature_A + checkout OPS-5xx/New_Feature_A + commit + commit + checkout main + merge OPS-5xx/New_Feature_A + checkout main + commit + branch OPS-6xx/New_Feature_B + checkout OPS-6xx/New_Feature_B + commit + commit + commit + checkout main + merge OPS-6xx/New_Feature_B +``` + +# OPRE-OPS Release Strategy + +The OPRE Team currently uses the following release strategy: + +- When a PR is merged to `main` the GitHub Action `release.yml` will automatically create a new release in +GitHub with next version number. +- The `main` branch will be tagged with the new version number. +- The release will be published to the [GitHub Releases page](https://github.com/HHS/OPRE-OPS/releases). diff --git a/docs/git-branching.md b/docs/git-branching.md deleted file mode 100644 index b2a298f51f..0000000000 --- a/docs/git-branching.md +++ /dev/null @@ -1,60 +0,0 @@ -# OPRE-OPS Branching Strategy - -The OPRE Team currently uses the following branching strategy: -### main -* `main` is the default and primary branch. This will always be the most up-to-date released code base. -* `main` currently has branch protections in place, and requires a `pull request` with at least `1` approval from someone on the `dev team`. - -### Release Branches -* `development`, `staging`, `production` are reserved branches cooresponding the their respective release environments. Any commit will trigger a release to that specific environment (Space) within Cloud.gov. -* If you want to Cloud.gov, simply `push` to one of the environment branches. You should follow the standard progression though of `development` --> `staging` --> `production`. -* `staging` and `production` will have protections in place ensuring a release was processed to their lower tier environments prior to allowing a `push`. - -### Feature Branches -* Features should branch from `main` and utilize a naming format of `OPS-{Issue#}_{Feature_Name}`, example: `OPS-522_CAN_Details_Page`. -* During colaboration, it's okay to branch from someone else's branch instead of `main`. -* Always try to do new work in a feature branch, avoid using generic branches like `tim_custom_testing_branch`. Keep branches specific to a feature or subset of changes. - -```mermaid ---- -title: OPRE-OPS Git Branching ---- -%%{init: {'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchOrder': 4}} }%% -gitGraph - commit id: "initial commit" - commit - branch OPS-5xx_New_Feature_A order: 5 - checkout OPS-5xx_New_Feature_A - commit - commit - checkout main - merge OPS-5xx_New_Feature_A - commit id: "release" tag: "v1.0.3" - branch development order: 3 - checkout development - commit id: "dev release" type: HIGHLIGHT - - - branch staging order: 2 - checkout staging - commit id: "staging release" type: HIGHLIGHT - - branch production order: 1 - checkout production - commit id: "prod release" type: HIGHLIGHT - - checkout main - commit - branch OPS-6xx_New_Feature_B order: 6 - checkout OPS-6xx_New_Feature_B - commit - commit - commit - checkout development - merge OPS-6xx_New_Feature_B id: "dev preview" - checkout main - merge OPS-6xx_New_Feature_B - - commit id: "release" tag: "v1.0.4" - -``` diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 89c401619a..d36bfd05e2 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /home/app COPY --chown=app:app ./package.json ./bun.lockb /home/app/ -RUN bun install +RUN bun install --production --frozen-lockfile COPY --chown=app:app index.html /home/app/ COPY --chown=app:app src /home/app/src diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 19a2bc6cc4..e94d7fae4f 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/cypress/e2e/canDetail.cy.js b/frontend/cypress/e2e/canDetail.cy.js index 5542caa246..31081bb55a 100644 --- a/frontend/cypress/e2e/canDetail.cy.js +++ b/frontend/cypress/e2e/canDetail.cy.js @@ -11,7 +11,7 @@ afterEach(() => { }); describe("CAN detail page", () => { - it("loads", () => { + it("shows relevant CAN data", () => { cy.visit("/cans/502/"); cy.get("h1").should("contain", "G99PHS9"); // heading cy.get("p").should("contain", "SSRD - 5 Years"); // sub-heading @@ -19,4 +19,19 @@ describe("CAN detail page", () => { cy.get("span").should("contain", "Director Derrek"); // division director cy.get("span").should("contain", "Program Support"); // portfolio }); + it("shows the CAN Spending page", () => { + cy.visit("/cans/504/spending"); + cy.get("#fiscal-year-select").select("2021"); + cy.get("h1").should("contain", "G994426"); // heading + cy.get("p").should("contain", "HS - 5 Years"); // sub-heading + // should contain the budget line table + cy.get("table").should("exist"); + // table should have more than 1 row + cy.get("tbody").children().should("have.length.greaterThan", 1); + // switch to a different fiscal year + cy.get("#fiscal-year-select").select("2022"); + // table should not exist + cy.get("tbody").should("not.exist"); + cy.get("p").should("contain", "No budget lines have been added to this CAN."); + }); }); diff --git a/frontend/cypress/e2e/uploadDocument.cy.js b/frontend/cypress/e2e/uploadDocument.cy.js index ad75911db5..620a563d9f 100644 --- a/frontend/cypress/e2e/uploadDocument.cy.js +++ b/frontend/cypress/e2e/uploadDocument.cy.js @@ -14,7 +14,7 @@ it("should loads", () => { cy.get("h1").should("have.text", "Temporary Upload Document Page"); }); -it("should create a document database record and upload to in memory storage", () => { +it.skip("should create a document database record and upload to in memory storage", () => { // Entering an Agreement ID in the Upload Document section cy.get('#agreement-id-upload').type('1'); // Selecting a file @@ -47,7 +47,7 @@ it("should create a document database record and upload to in memory storage", ( }) }); -it("Should download document in memory storage and verify logs", () => { +it.skip("Should download document in memory storage and verify logs", () => { // set up spy on console.log let logSpy; cy.window().then((win) => { diff --git a/frontend/package.json b/frontend/package.json index 6e3f033d39..e5c4f1776f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,113 +1,114 @@ { - "name": "opre-ops", - "version": "1.0.1", - "license": "CC0-1.0", - "private": true, - "type": "module", - "dependencies": { - "@azure/storage-blob": "12.25.0", - "@eslint/compat": "1.2.2", - "@eslint/js": "9.14.0", - "@fortawesome/fontawesome-svg-core": "6.5.2", - "@fortawesome/free-regular-svg-icons": "6.5.2", - "@fortawesome/free-solid-svg-icons": "6.5.2", - "@fortawesome/react-fontawesome": "0.2.2", - "@nivo/bar": "0.87.0", - "@nivo/core": "0.87.0", - "@nivo/pie": "0.87.0", - "@reduxjs/toolkit": "2.3.0", - "@uswds/uswds": "3.9.0", - "@vitejs/plugin-react": "4.3.3", - "axios": "1.7.7", - "clsx": "2.1.1", - "crypto-random-string": "5.0.0", - "eslint": "9.14.0", - "eslint-config-prettier": "9.1.0", - "eslint-plugin-cypress": "4.1.0", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-jest": "28.8.3", - "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-prettier": "5.2.1", - "eslint-plugin-react": "7.37.2", - "eslint-plugin-react-hooks": "5.0.0", - "eslint-plugin-react-refresh": "0.4.14", - "eslint-plugin-testing-library": "6.4.0", - "jose": "5.9.6", - "js-cookie": "3.0.5", - "jsdom": "25.0.1", - "jwt-decode": "4.0.0", - "lodash": "4.17.21", - "react": "18.3.1", - "react-currency-format": "1.1.0", - "react-dom": "18.3.1", - "react-markdown": "9.0.1", - "react-modal": "3.16.1", - "react-redux": "9.1.2", - "react-router-dom": "6.27.0", - "react-select": "5.8.2", - "react-slider": "2.0.6", - "sass": "1.80.6", - "sass-loader": "16.0.3", - "styled-components": "6.1.13", - "vest": "5.4.3", - "vite": "5.4.10", - "vite-jsconfig-paths": "2.0.1", - "vite-plugin-babel-macros": "1.0.6", - "vite-plugin-eslint": "1.8.1", - "vite-plugin-svgr": "4.3.0" - }, - "overrides": { - "rollup": "4.24.4" - }, - "devDependencies": { - "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.0.1", - "@testing-library/user-event": "14.5.2", - "@types/testing-library__jest-dom": "6.0.0", - "@types/testing-library__react": "10.2.0", - "@vitest/coverage-istanbul": "2.1.4", - "@vitest/ui": "2.1.4", - "axe-core": "4.10.2", - "cypress": "13.15.2", - "cypress-axe": "1.5.0", - "cypress-localstorage-commands": "2.2.6", - "globals": "15.11.0", - "history": "5.3.0", - "msw": "2.6.0", - "prettier": "3.3.3", - "redux-mock-store": "1.5.5", - "@uswds/compile": "1.2.0", - "vitest": "2.1.4" - }, - "scripts": { - "start": "vite", - "start:debug": "vite --inspect=0.0.0.0:9229", - "build": "vite build", - "test": "vitest", - "test:coverage": "vitest --coverage", - "test:ui": "vitest --ui --coverage.enabled=true", - "test:e2e:interactive": "cypress open --config-file ./cypress.config.js", - "test:e2e": "cypress run --config-file ./cypress.config.js --headless", - "test:e2e:debug": "DEBUG=cypress:* cypress run --config-file ./cypress.config.js --headless", - "lint": "eslint './src/**'", - "cypress:open": "cypress open", - "uswds:update": "bunx gulp compile" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "babelMacros": { - "fontawesome-svg-core": { - "license": "free" - } + "name": "opre-ops", + "version": "1.0.1", + "license": "CC0-1.0", + "private": true, + "type": "module", + "dependencies": { + "@azure/storage-blob": "12.25.0", + "@eslint/compat": "1.2.2", + "@eslint/js": "9.14.0", + "@fortawesome/fontawesome-svg-core": "6.5.2", + "@fortawesome/free-regular-svg-icons": "6.5.2", + "@fortawesome/free-solid-svg-icons": "6.5.2", + "@fortawesome/react-fontawesome": "0.2.2", + "@nivo/bar": "0.87.0", + "@nivo/core": "0.87.0", + "@nivo/pie": "0.87.0", + "@reduxjs/toolkit": "2.3.0", + "@uswds/uswds": "3.9.0", + "@vitejs/plugin-react": "4.3.3", + "axios": "1.7.7", + "clsx": "2.1.1", + "crypto-random-string": "5.0.0", + "eslint": "9.14.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-cypress": "4.1.0", + "eslint-plugin-import": "2.31.0", + "eslint-plugin-jest": "28.9.0", + "eslint-plugin-jsx-a11y": "6.10.2", + "eslint-plugin-prettier": "5.2.1", + "eslint-plugin-react": "7.37.2", + "eslint-plugin-react-hooks": "5.0.0", + "eslint-plugin-react-refresh": "0.4.14", + "eslint-plugin-testing-library": "6.4.0", + "jose": "5.9.6", + "js-cookie": "3.0.5", + "jsdom": "25.0.1", + "jwt-decode": "4.0.0", + "lodash": "4.17.21", + "react": "18.3.1", + "react-currency-format": "1.1.0", + "react-dom": "18.3.1", + "react-markdown": "9.0.1", + "react-modal": "3.16.1", + "react-redux": "9.1.2", + "react-router-dom": "6.28.0", + "react-select": "5.8.3", + "react-slider": "2.0.6", + "sass": "1.80.6", + "sass-loader": "16.0.3", + "styled-components": "6.1.13", + "vest": "5.4.4", + "vite": "5.4.11", + "vite-jsconfig-paths": "2.0.1", + "vite-plugin-babel-macros": "1.0.6", + "vite-plugin-eslint": "1.8.1", + "vite-plugin-svgr": "4.3.0" + }, + "overrides": { + "rollup": "4.25.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "6.6.3", + "@testing-library/react": "16.0.1", + "@testing-library/user-event": "14.5.2", + "@types/testing-library__jest-dom": "6.0.0", + "@types/testing-library__react": "10.2.0", + "@uswds/compile": "1.2.0", + "@vitest/coverage-istanbul": "2.1.4", + "@vitest/ui": "2.1.4", + "axe-core": "4.10.2", + "cypress": "13.15.2", + "cypress-axe": "1.5.0", + "cypress-localstorage-commands": "2.2.6", + "globals": "15.12.0", + "history": "5.3.0", + "msw": "2.6.4", + "prettier": "3.3.3", + "redux-mock-store": "1.5.5", + "semantic-release": "24.2.0", + "vitest": "2.1.4" + }, + "scripts": { + "start": "vite", + "start:debug": "vite --inspect=0.0.0.0:9229", + "build": "vite build", + "test": "vitest", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui --coverage.enabled=true", + "test:e2e:interactive": "cypress open --config-file ./cypress.config.js", + "test:e2e": "cypress run --config-file ./cypress.config.js --headless", + "test:e2e:debug": "DEBUG=cypress:* cypress run --config-file ./cypress.config.js --headless", + "lint": "eslint './src/**'", + "cypress:open": "cypress open", + "uswds:update": "bunx gulp compile" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "babelMacros": { + "fontawesome-svg-core": { + "license": "free" } + } } diff --git a/frontend/src/components/Agreements/Documents/Document.test.js b/frontend/src/components/Agreements/Documents/Document.test.js index 3b04a1712a..59e5218e6a 100644 --- a/frontend/src/components/Agreements/Documents/Document.test.js +++ b/frontend/src/components/Agreements/Documents/Document.test.js @@ -58,7 +58,7 @@ describe("processUploading", () => { }); it("should upload to Azure Blob Storage", async () => { - const sasUrl = "https://mock.blob.core.windows.net"; + const sasUrl = "https://mock.ops.opre.acf.gov"; await processUploading(sasUrl, uuid, file, agreementId, mockUploadDocumentToBlob, mockUploadDocumentToBlob); @@ -77,7 +77,7 @@ describe("processUploading", () => { }); it("should handle errors gracefully", async () => { - const sasUrl = "https://mock.blob.core.windows.net"; + const sasUrl = "https://mock.ops.opre.acf.gov"; mockUploadDocumentToBlob.mockRejectedValue(new Error("Upload failed")); diff --git a/frontend/src/components/Agreements/Documents/Documents.constants.js b/frontend/src/components/Agreements/Documents/Documents.constants.js index dd24ca3421..0a58232d37 100644 --- a/frontend/src/components/Agreements/Documents/Documents.constants.js +++ b/frontend/src/components/Agreements/Documents/Documents.constants.js @@ -10,7 +10,7 @@ export const DOCUMENT_TYPES = [ export const VALID_EXTENSIONS = ["pdf", "doc", "docx", "xls", "xlsx"]; -export const DOCUMENT_CONTAINER_NAME = "documents"; +export const DOCUMENT_CONTAINER_NAME = "docs"; export const ALLOWED_FAKE_HOSTS = "FakeDocumentRepository"; -export const ALLOWED_HOSTS = "blob.core.windows.net"; +export const ALLOWED_HOSTS = "ops.opre.acf.gov"; diff --git a/frontend/src/components/CANs/CANBudgetLineTable/CABBudgetLineTable.constants.js b/frontend/src/components/CANs/CANBudgetLineTable/CABBudgetLineTable.constants.js new file mode 100644 index 0000000000..c141ef13b7 --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetLineTable/CABBudgetLineTable.constants.js @@ -0,0 +1 @@ +export const TABLE_HEADERS = ["BL ID #", "Agreement", "Obligate By", "FY", "Total", "% of CAN", "Status"]; diff --git a/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTable.jsx b/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTable.jsx new file mode 100644 index 0000000000..a52d7cf4eb --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTable.jsx @@ -0,0 +1,50 @@ +import { formatDateNeeded } from "../../../helpers/utils"; +import Table from "../../UI/Table"; +import { TABLE_HEADERS } from "./CABBudgetLineTable.constants"; +import CANBudgetLineTableRow from "./CANBudgetLineTableRow"; +/** + * @typedef {import("../../../components/BudgetLineItems/BudgetLineTypes").BudgetLine} BudgetLine + */ + +/** + * @typedef {Object} CANBudgetLineTableProps + * @property {BudgetLine[]} budgetLines + */ + +/** + * @component - The CAN Budget Line Table. + * @param {CANBudgetLineTableProps} props + * @returns {JSX.Element} - The component JSX. + */ +const CANBudgetLineTable = ({ budgetLines }) => { + if (budgetLines.length === 0) { + return

No budget lines have been added to this CAN.

; + } + + return ( + + {budgetLines.map((budgetLine) => ( + + ))} +
+ ); +}; + +export default CANBudgetLineTable; diff --git a/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTable.test.jsx b/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTable.test.jsx new file mode 100644 index 0000000000..8e4402f829 --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTable.test.jsx @@ -0,0 +1,33 @@ +import { render, screen } from "@testing-library/react"; +import { Provider } from "react-redux"; +import CANBudgetLineTable from "./CANBudgetLineTable"; +import store from "../../../store"; +import { budgetLine } from "../../../tests/data"; + +describe("CANBudgetLineTable", () => { + const mockBudgetLines = [ + { ...budgetLine, status: "Approved", amount: 1000 }, + { ...budgetLine, status: "Pending", amount: 2000 } + ]; + + it("renders 'No budget lines have been added to this CAN.' when there are no budget lines", () => { + render( + + + + ); + expect(screen.getByText("No budget lines have been added to this CAN.")).toBeInTheDocument(); + }); + + it("renders table with budget lines", () => { + render( + + + + ); + expect(screen.getByText("Approved")).toBeInTheDocument(); + expect(screen.getByText("Pending")).toBeInTheDocument(); + expect(screen.getByText("$1,000.00")).toBeInTheDocument(); + expect(screen.getByText("$2,000.00")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTableRow.jsx b/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTableRow.jsx new file mode 100644 index 0000000000..68cc051516 --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTableRow.jsx @@ -0,0 +1,225 @@ +import { faClock } from "@fortawesome/free-regular-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import CurrencyFormat from "react-currency-format"; +import { + formatDateToMonthDayYear, + totalBudgetLineAmountPlusFees, + totalBudgetLineFeeAmount +} from "../../../helpers/utils"; +import useGetUserFullNameFromId from "../../../hooks/user.hooks"; +import TableRowExpandable from "../../UI/TableRowExpandable"; +import { + changeBgColorIfExpanded, + expandedRowBGColor, + removeBorderBottomIfExpanded +} from "../../UI/TableRowExpandable/TableRowExpandable.helpers"; +import { useTableRow } from "../../UI/TableRowExpandable/TableRowExpandable.hooks"; +import TableTag from "../../UI/TableTag"; +import { useChangeRequestsForTooltip } from "../../../hooks/useChangeRequests.hooks"; + +/** + * @typedef {import("../../../components/BudgetLineItems/BudgetLineTypes").BudgetLine} BudgetLine + */ + +/** + * @typedef {Object} CANBudgetLineTableRowProps + * @property {BudgetLine} budgetLine + * @property {number} blId + * @property {string} agreementName - TODO + * @property {string} obligateDate + * @property {number | string } fiscalYear + * @property {number} amount + * @property {number} fee + * @property {number} percentOfCAN - TODO + * @property {string} status + * @property {boolean} inReview + * @property {number} creatorId + * @property {string} creationDate + * @property {string} procShopCode - TODO + * @property {number} procShopFeePercentage + * @property {string} notes + */ + +/** + * @component - The CAN Budget Line Table. + * @param {CANBudgetLineTableRowProps} props + * @returns {JSX.Element} - The component JSX. + */ +const CANBudgetLineTableRow = ({ + budgetLine, + blId, + agreementName, + obligateDate, + fiscalYear, + amount, + fee, + percentOfCAN, + status, + inReview, + creatorId, + creationDate, + procShopCode, + procShopFeePercentage, + notes +}) => { + const lockedMessage = useChangeRequestsForTooltip(budgetLine); + const { isExpanded, setIsRowActive, setIsExpanded } = useTableRow(); + const borderExpandedStyles = removeBorderBottomIfExpanded(isExpanded); + const bgExpandedStyles = changeBgColorIfExpanded(isExpanded); + const budgetLineCreatorName = useGetUserFullNameFromId(creatorId); + const feeTotal = totalBudgetLineFeeAmount(amount, fee); + const budgetLineTotalPlusFees = totalBudgetLineAmountPlusFees(amount, feeTotal); + const displayCreatedDate = formatDateToMonthDayYear(creationDate); + + const TableRowData = ( + <> + + {blId} + + + {agreementName} + + + {obligateDate} + + + {fiscalYear} + + + + + + {percentOfCAN}% + + + + + + ); + + const ExpandedData = ( + +
+
+
Created By
+
+ {budgetLineCreatorName} +
+
+ + {displayCreatedDate} +
+
+
+
Notes
+
+ {notes} +
+
+
+
+
Procurement Shop
+
+ {`${procShopCode}-Fee Rate: ${procShopFeePercentage * 100}%`} +
+
+
+
+
SubTotal
+
+ +
+
+
+
Fees
+
+ +
+
+
+
+
+ + ); + + return ( + + ); +}; + +export default CANBudgetLineTableRow; diff --git a/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTableRow.test.jsx b/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTableRow.test.jsx new file mode 100644 index 0000000000..bdc2d9e8af --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetLineTable/CANBudgetLineTableRow.test.jsx @@ -0,0 +1,84 @@ +import { render, screen } from "@testing-library/react"; +import CANBudgetLineTableRow from "./CANBudgetLineTableRow"; +import { formatDateNeeded } from "../../../helpers/utils"; +import { Provider } from "react-redux"; +import store from "../../../store"; +import { budgetLine } from "../../../tests/data"; +import userEvent from "@testing-library/user-event"; + +const mockBudgetLine = { + ...budgetLine, + id: 1, + date_needed: "2023-10-01", + fiscal_year: 2023, + amount: 1000, + proc_shop_fee_percentage: 0.05, + status: "Pending", + in_review: true, + created_by: 1, + created_on: "2023-09-01" +}; + +describe("CANBudgetLineTableRow", () => { + test("renders table row data correctly", () => { + render( + + + + ); + + expect(screen.getByText("TBD")).toBeInTheDocument(); + expect(screen.getByText(formatDateNeeded(mockBudgetLine.date_needed))).toBeInTheDocument(); + expect(screen.getByText(mockBudgetLine.fiscal_year)).toBeInTheDocument(); + expect(screen.getByText("$1,050.00")).toBeInTheDocument(); // amount + fee + expect(screen.getByText("3%")).toBeInTheDocument(); + }); + + test("renders expanded data correctly", async () => { + render( + + + + ); + + // Simulate expanding the row + await userEvent.click(screen.getByTestId("expand-row")); + + expect(screen.getByText("Created By")).toBeInTheDocument(); + expect(screen.getByText("comment one")).toBeInTheDocument(); + expect(screen.getByText("Procurement Shop")).toBeInTheDocument(); + expect(screen.getByText("$1,000.00")).toBeInTheDocument(); // amount + expect(screen.getByText("$50.00")).toBeInTheDocument(); // fee + }); +}); diff --git a/frontend/src/components/CANs/CANBudgetLineTable/index.js b/frontend/src/components/CANs/CANBudgetLineTable/index.js new file mode 100644 index 0000000000..41a7b91331 --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetLineTable/index.js @@ -0,0 +1 @@ +export { default } from "./CANBudgetLineTable"; diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index c1b93859fd..d82fbd06e9 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -247,7 +247,7 @@ const router = createBrowserRouter( to="/cans" className="text-primary" > - Cans + CANs ) }} diff --git a/frontend/src/pages/cans/detail/Can.jsx b/frontend/src/pages/cans/detail/Can.jsx index af5d31525c..fc717e5a16 100644 --- a/frontend/src/pages/cans/detail/Can.jsx +++ b/frontend/src/pages/cans/detail/Can.jsx @@ -9,9 +9,11 @@ import CANFiscalYearSelect from "../list/CANFiscalYearSelect"; import CanDetail from "./CanDetail"; import CanFunding from "./CanFunding"; import CanSpending from "./CanSpending"; +import React from "react"; /** - @typedef {import("../../../components/CANs/CANTypes").CAN} CAN -*/ + * @typedef {import("../../../components/CANs/CANTypes").CAN} CAN + * @typedef {import("../../../components/BudgetLineItems/BudgetLineTypes").BudgetLine} BudgetLine + */ const Can = () => { const urlPathParams = useParams(); @@ -21,6 +23,11 @@ const Can = () => { const selectedFiscalYear = useSelector((state) => state.canDetail.selectedFiscalYear); const fiscalYear = Number(selectedFiscalYear.value); + const filteredCANByFiscalYear = React.useMemo(() => { + if (!fiscalYear || !can) return {}; + return can.funding_details?.fiscal_year === fiscalYear ? can : {}; + }, [can, fiscalYear]); + if (isLoading) { return
Loading Can...
; } @@ -28,12 +35,18 @@ const Can = () => { return
Can not found
; } + const { number, description, nick_name: nickname, portfolio } = can; + + /** @type {{budget_line_items?: BudgetLine[]}} */ + const { budget_line_items: budgetLines } = filteredCANByFiscalYear; + const { division_id: divisionId, team_leaders: teamLeaders, name: portfolioName } = portfolio; + const noData = "TBD"; const subTitle = `${can.nick_name} - ${can.active_period} ${can.active_period > 1 ? "Years" : "Year"}`; return ( @@ -47,11 +60,20 @@ const Can = () => { } + element={ + + } /> } + element={} /> { - const canDivisionId = can.portfolio.division_id; - const { data: division, isSuccess } = useGetDivisionQuery(canDivisionId); +const CanDetail = ({ description, number, nickname, portfolioName, teamLeaders, divisionId }) => { + const { data: division, isSuccess } = useGetDivisionQuery(divisionId); const divisionDirectorFullName = useGetUserFullNameFromId(isSuccess ? division.division_director_id : null); return ( @@ -35,7 +39,7 @@ const CanDetail = ({ can }) => {
@@ -51,22 +55,22 @@ const CanDetail = ({ can }) => {
-
Team Leaders
- {can.portfolio?.team_leaders && - can.portfolio?.team_leaders.length > 0 && - can.portfolio.team_leaders.map((teamLeader) => ( +
Team Leader
+ {teamLeaders && + teamLeaders.length > 0 && + teamLeaders.map((teamLeader) => (
{ +import CANBudgetLineTable from "../../../components/CANs/CANBudgetLineTable"; +/** + @typedef {import("../../../components/CANs/CANTypes").CAN} CAN + @typedef {import("../../../components/BudgetLineItems/BudgetLineTypes").BudgetLine} BudgetLine +*/ + +/** + * @typedef {Object} CanSpendingProps + * @property {BudgetLine[]} budgetLines + */ + +/** + * @component - The CAN detail page. + * @param {CanSpendingProps} props + * @returns {JSX.Element} - The component JSX. + */ +const CanSpending = ({ budgetLines }) => { return ( -
-

Can Spending

-

coming soon...

-
+
+

CAN Spending Summary

+

The summary below shows the CANs total budget and spending across all budget lines

+ {/* Note: Cards go here */} +

CAN Budget Lines

+

This is a list of all budget lines allocating funding from this CAN for the selected fiscal year.

+ +
); };